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