001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2010 Sun Microsystems, Inc.
015 * Portions Copyright 2012-2016 ForgeRock AS.
016 */
017package org.forgerock.opendj.ldap.controls;
018
019import static com.forgerock.opendj.util.StaticUtils.byteToHex;
020import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
021import static com.forgerock.opendj.ldap.CoreMessages.*;
022
023import java.io.IOException;
024
025import org.forgerock.i18n.LocalizableMessage;
026import org.forgerock.i18n.slf4j.LocalizedLogger;
027import org.forgerock.opendj.io.ASN1;
028import org.forgerock.opendj.io.ASN1Reader;
029import org.forgerock.opendj.io.ASN1Writer;
030import org.forgerock.opendj.ldap.ByteString;
031import org.forgerock.opendj.ldap.ByteStringBuilder;
032import org.forgerock.opendj.ldap.DecodeException;
033import org.forgerock.opendj.ldap.DecodeOptions;
034import org.forgerock.util.Reject;
035
036/**
037 * The password policy response control as defined in
038 * draft-behera-ldap-password-policy.
039 *
040 * <pre>
041 * Connection connection = ...;
042 * String DN = ...;
043 * char[] password = ...;
044 *
045 * try {
046 *     BindRequest request = Requests.newSimpleBindRequest(DN, password)
047 *             .addControl(PasswordPolicyRequestControl.newControl(true));
048 *
049 *     BindResult result = connection.bind(request);
050 *
051 *     PasswordPolicyResponseControl control =
052 *             result.getControl(PasswordPolicyResponseControl.DECODER,
053 *                     new DecodeOptions());
054 *     if (!(control == null) && !(control.getWarningType() == null)) {
055 *         // Password policy warning, use control.getWarningType(),
056 *         // and control.getWarningValue().
057 *     }
058 * } catch (LdapException e) {
059 *     Result result = e.getResult();
060 *     try {
061 *         PasswordPolicyResponseControl control =
062 *                 result.getControl(PasswordPolicyResponseControl.DECODER,
063 *                         new DecodeOptions());
064 *         if (!(control == null)) {
065 *             // Password policy error, use control.getErrorType().
066 *         }
067 *     } catch (DecodeException de) {
068 *         // Failed to decode the response control.
069 *     }
070 * } catch (DecodeException e) {
071 *     // Failed to decode the response control.
072 * }
073 * </pre>
074 *
075 * If the client has sent a passwordPolicyRequest control, the server (when
076 * solicited by the inclusion of the request control) sends this control with
077 * the following operation responses: bindResponse, modifyResponse, addResponse,
078 * compareResponse and possibly extendedResponse, to inform of various
079 * conditions, and MAY be sent with other operations (in the case of the
080 * changeAfterReset error).
081 *
082 * @see PasswordPolicyRequestControl
083 * @see PasswordPolicyWarningType
084 * @see PasswordPolicyErrorType
085 * @see <a href="http://tools.ietf.org/html/draft-behera-ldap-password-policy">
086 *      draft-behera-ldap-password-policy - Password Policy for LDAP Directories
087 *      </a>
088 */
089public final class PasswordPolicyResponseControl implements Control {
090
091    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
092    /** The OID for the password policy control from draft-behera-ldap-password-policy. */
093    public static final String OID = PasswordPolicyRequestControl.OID;
094
095    private final int warningValue;
096
097    private final PasswordPolicyErrorType errorType;
098
099    private final PasswordPolicyWarningType warningType;
100
101    /** A decoder which can be used for decoding the password policy response control. */
102    public static final ControlDecoder<PasswordPolicyResponseControl> DECODER =
103            new ControlDecoder<PasswordPolicyResponseControl>() {
104
105                @Override
106                public PasswordPolicyResponseControl decodeControl(final Control control,
107                        final DecodeOptions options) throws DecodeException {
108                    Reject.ifNull(control);
109
110                    if (control instanceof PasswordPolicyResponseControl) {
111                        return (PasswordPolicyResponseControl) control;
112                    }
113
114                    if (!control.getOID().equals(OID)) {
115                        final LocalizableMessage message =
116                                ERR_PWPOLICYRES_CONTROL_BAD_OID.get(control.getOID(), OID);
117                        throw DecodeException.error(message);
118                    }
119
120                    if (!control.hasValue()) {
121                        // The response control must always have a value.
122                        final LocalizableMessage message = ERR_PWPOLICYRES_NO_CONTROL_VALUE.get();
123                        throw DecodeException.error(message);
124                    }
125
126                    final ASN1Reader reader = ASN1.getReader(control.getValue());
127                    try {
128                        PasswordPolicyWarningType warningType = null;
129                        PasswordPolicyErrorType errorType = null;
130                        int warningValue = -1;
131
132                        reader.readStartSequence();
133
134                        if (reader.hasNextElement() && (reader.peekType() == TYPE_WARNING_ELEMENT)) {
135                            // Its a CHOICE element. Read as sequence to
136                            // retrieve
137                            // nested element.
138                            reader.readStartSequence();
139                            final int warningChoiceValue = (0x7F & reader.peekType());
140                            if (warningChoiceValue < 0
141                                    || warningChoiceValue >= PasswordPolicyWarningType.values().length) {
142                                final LocalizableMessage message =
143                                        ERR_PWPOLICYRES_INVALID_WARNING_TYPE.get(byteToHex(reader
144                                                .peekType()));
145                                throw DecodeException.error(message);
146                            } else {
147                                warningType =
148                                        PasswordPolicyWarningType.values()[warningChoiceValue];
149                            }
150                            warningValue = (int) reader.readInteger();
151                            reader.readEndSequence();
152                        }
153
154                        if (reader.hasNextElement() && (reader.peekType() == TYPE_ERROR_ELEMENT)) {
155                            final int errorValue = reader.readEnumerated();
156                            if (errorValue < 0
157                                    || errorValue >= PasswordPolicyErrorType.values().length) {
158                                final LocalizableMessage message =
159                                        ERR_PWPOLICYRES_INVALID_ERROR_TYPE.get(errorValue);
160                                throw DecodeException.error(message);
161                            } else {
162                                errorType = PasswordPolicyErrorType.values()[errorValue];
163                            }
164                        }
165
166                        reader.readEndSequence();
167
168                        return new PasswordPolicyResponseControl(control.isCritical(), warningType,
169                                warningValue, errorType);
170                    } catch (final IOException e) {
171                        logger.debug(LocalizableMessage.raw("%s", e));
172
173                        final LocalizableMessage message =
174                                ERR_PWPOLICYRES_DECODE_ERROR.get(getExceptionMessage(e));
175                        throw DecodeException.error(message);
176                    }
177                }
178
179                @Override
180                public String getOID() {
181                    return OID;
182                }
183            };
184
185    /**
186     * Creates a new password policy response control with the provided error.
187     *
188     * @param errorType
189     *            The password policy error type.
190     * @return The new control.
191     * @throws NullPointerException
192     *             If {@code errorType} was {@code null}.
193     */
194    public static PasswordPolicyResponseControl newControl(final PasswordPolicyErrorType errorType) {
195        Reject.ifNull(errorType);
196
197        return new PasswordPolicyResponseControl(false, null, -1, errorType);
198    }
199
200    /**
201     * Creates a new password policy response control with the provided warning.
202     *
203     * @param warningType
204     *            The password policy warning type.
205     * @param warningValue
206     *            The password policy warning value.
207     * @return The new control.
208     * @throws IllegalArgumentException
209     *             If {@code warningValue} was negative.
210     * @throws NullPointerException
211     *             If {@code warningType} was {@code null}.
212     */
213    public static PasswordPolicyResponseControl newControl(
214            final PasswordPolicyWarningType warningType, final int warningValue) {
215        Reject.ifNull(warningType);
216        Reject.ifFalse(warningValue >= 0, "warningValue is negative");
217
218        return new PasswordPolicyResponseControl(false, warningType, warningValue, null);
219    }
220
221    /**
222     * Creates a new password policy response control with the provided warning
223     * and error.
224     *
225     * @param warningType
226     *            The password policy warning type.
227     * @param warningValue
228     *            The password policy warning value.
229     * @param errorType
230     *            The password policy error type.
231     * @return The new control.
232     * @throws IllegalArgumentException
233     *             If {@code warningValue} was negative.
234     * @throws NullPointerException
235     *             If {@code warningType} or {@code errorType} was {@code null}.
236     */
237    public static PasswordPolicyResponseControl newControl(
238            final PasswordPolicyWarningType warningType, final int warningValue,
239            final PasswordPolicyErrorType errorType) {
240        Reject.ifNull(warningType);
241        Reject.ifNull(errorType);
242        Reject.ifFalse(warningValue >= 0, "warningValue is negative");
243
244        return new PasswordPolicyResponseControl(false, warningType, warningValue, errorType);
245    }
246
247    private final boolean isCritical;
248
249    /** The BER type value for the warning element of the control value. */
250    private static final byte TYPE_WARNING_ELEMENT = (byte) 0xA0;
251
252    /** The BER type value for the error element of the control value. */
253    private static final byte TYPE_ERROR_ELEMENT = (byte) 0x81;
254
255    private PasswordPolicyResponseControl(final boolean isCritical,
256            final PasswordPolicyWarningType warningType, final int warningValue,
257            final PasswordPolicyErrorType errorType) {
258        this.isCritical = isCritical;
259        this.warningType = warningType;
260        this.warningValue = warningValue;
261        this.errorType = errorType;
262    }
263
264    /**
265     * Returns the password policy error type, if available.
266     *
267     * @return The password policy error type, or {@code null} if this control
268     *         does not contain a error.
269     */
270    public PasswordPolicyErrorType getErrorType() {
271        return errorType;
272    }
273
274    @Override
275    public String getOID() {
276        return OID;
277    }
278
279    @Override
280    public ByteString getValue() {
281        final ByteStringBuilder buffer = new ByteStringBuilder();
282        final ASN1Writer writer = ASN1.getWriter(buffer);
283        try {
284            writer.writeStartSequence();
285            if (warningType != null) {
286                // Just write the CHOICE element as a single element SEQUENCE.
287                writer.writeStartSequence(TYPE_WARNING_ELEMENT);
288                writer.writeInteger((byte) (0x80 | warningType.intValue()), warningValue);
289                writer.writeEndSequence();
290            }
291
292            if (errorType != null) {
293                writer.writeInteger(TYPE_ERROR_ELEMENT, errorType.intValue());
294            }
295            writer.writeEndSequence();
296            return buffer.toByteString();
297        } catch (final IOException ioe) {
298            // This should never happen unless there is a bug somewhere.
299            throw new RuntimeException(ioe);
300        }
301    }
302
303    /**
304     * Returns the password policy warning type, if available.
305     *
306     * @return The password policy warning type, or {@code null} if this control
307     *         does not contain a warning.
308     */
309    public PasswordPolicyWarningType getWarningType() {
310        return warningType;
311    }
312
313    /**
314     * Returns the password policy warning value, if available. The value is
315     * undefined if this control does not contain a warning.
316     *
317     * @return The password policy warning value, or {@code -1} if this control
318     *         does not contain a warning.
319     */
320    public int getWarningValue() {
321        return warningValue;
322    }
323
324    @Override
325    public boolean hasValue() {
326        return true;
327    }
328
329    @Override
330    public boolean isCritical() {
331        return isCritical;
332    }
333
334    @Override
335    public String toString() {
336        final StringBuilder builder = new StringBuilder();
337        builder.append("PasswordPolicyResponseControl(oid=");
338        builder.append(getOID());
339        builder.append(", criticality=");
340        builder.append(isCritical());
341        if (warningType != null) {
342            builder.append(", warningType=");
343            builder.append(warningType);
344            builder.append(", warningValue=");
345            builder.append(warningValue);
346        }
347        if (errorType != null) {
348            builder.append(", errorType=");
349            builder.append(errorType);
350        }
351        builder.append(")");
352        return builder.toString();
353    }
354}