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 *      Portions Copyright 2011-2015 ForgeRock AS.
025 *      Portions Copyright 2014 ForgeRock AS
026 */
027package org.opends.server.api;
028
029import java.util.List;
030
031import org.forgerock.i18n.LocalizableMessage;
032import org.forgerock.i18n.slf4j.LocalizedLogger;
033import org.forgerock.opendj.ldap.ByteString;
034import org.forgerock.opendj.ldap.ConditionResult;
035import org.forgerock.opendj.ldap.GeneralizedTime;
036import org.forgerock.opendj.ldap.ResultCode;
037import org.opends.server.core.DirectoryServer;
038import org.opends.server.types.*;
039
040import static org.opends.messages.CoreMessages.*;
041import static org.opends.server.config.ConfigConstants.*;
042import static org.opends.server.util.StaticUtils.*;
043
044/**
045 * The authentication policy context associated with a user's entry, which is
046 * responsible for managing the user's account, their password, as well as
047 * authenticating the user.
048 */
049public abstract class AuthenticationPolicyState
050{
051  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
052
053
054
055  /**
056   * Returns the authentication policy state for the user provided user. This
057   * method is equivalent to the following:
058   *
059   * <pre>
060   * AuthenticationPolicy policy = AuthenticationPolicy.forUser(userEntry,
061   *     useDefaultOnError);
062   * AuthenticationPolicyState state = policy
063   *     .createAuthenticationPolicyState(userEntry);
064   * </pre>
065   *
066   * See the documentation of {@link AuthenticationPolicy#forUser} for a
067   * description of the algorithm used to find a user's authentication policy.
068   *
069   * @param userEntry
070   *          The user entry.
071   * @param useDefaultOnError
072   *          Indicates whether the server should fall back to using the default
073   *          password policy if there is a problem with the configured policy
074   *          for the user.
075   * @return The password policy for the user.
076   * @throws DirectoryException
077   *           If a problem occurs while attempting to determine the password
078   *           policy for the user.
079   * @see AuthenticationPolicy#forUser(Entry, boolean)
080   */
081  public static AuthenticationPolicyState forUser(final Entry userEntry,
082      final boolean useDefaultOnError) throws DirectoryException
083  {
084    final AuthenticationPolicy policy = AuthenticationPolicy.forUser(userEntry,
085        useDefaultOnError);
086    return policy.createAuthenticationPolicyState(userEntry);
087  }
088
089
090
091  /**
092   * A utility method which may be used by implementations in order to obtain
093   * the value of the specified attribute from the provided entry as a boolean.
094   *
095   * @param entry
096   *          The entry whose attribute is to be parsed as a boolean.
097   * @param attributeType
098   *          The attribute type whose value should be parsed as a boolean.
099   * @return The attribute's value represented as a ConditionResult value, or
100   *         ConditionResult.UNDEFINED if the specified attribute does not exist
101   *         in the entry.
102   * @throws DirectoryException
103   *           If the value cannot be decoded as a boolean.
104   */
105  protected static ConditionResult getBoolean(final Entry entry,
106      final AttributeType attributeType) throws DirectoryException
107  {
108    final List<Attribute> attrList = entry.getAttribute(attributeType);
109    if (attrList != null)
110    {
111      for (final Attribute a : attrList)
112      {
113        if (a.isEmpty())
114        {
115          continue;
116        }
117
118        final String valueString = toLowerCase(a.iterator().next().toString());
119
120        if (valueString.equals("true") || valueString.equals("yes")
121            || valueString.equals("on") || valueString.equals("1"))
122        {
123          if (logger.isTraceEnabled())
124          {
125            logger.trace("Attribute %s resolves to true for user entry %s",
126                attributeType.getNameOrOID(), entry.getName());
127          }
128
129          return ConditionResult.TRUE;
130        }
131
132        if (valueString.equals("false") || valueString.equals("no")
133            || valueString.equals("off") || valueString.equals("0"))
134        {
135          if (logger.isTraceEnabled())
136          {
137            logger.trace("Attribute %s resolves to false for user entry %s",
138                attributeType.getNameOrOID(), entry.getName());
139          }
140
141          return ConditionResult.FALSE;
142        }
143
144        if (logger.isTraceEnabled())
145        {
146          logger.trace("Unable to resolve value %s for attribute %s "
147              + "in user entry %s as a Boolean.", valueString,
148              attributeType.getNameOrOID(), entry.getName());
149        }
150
151        final LocalizableMessage message = ERR_PWPSTATE_CANNOT_DECODE_BOOLEAN
152            .get(valueString, attributeType.getNameOrOID(), entry.getName());
153        throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
154            message);
155      }
156    }
157
158    if (logger.isTraceEnabled())
159    {
160      logger.trace("Returning %s because attribute %s does not exist "
161          + "in user entry %s", ConditionResult.UNDEFINED,
162          attributeType.getNameOrOID(), entry.getName());
163    }
164
165    return ConditionResult.UNDEFINED;
166  }
167
168
169
170  /**
171   * A utility method which may be used by implementations in order to obtain
172   * the value of the specified attribute from the provided entry as a time in
173   * generalized time format.
174   *
175   * @param entry
176   *          The entry whose attribute is to be parsed as a boolean.
177   * @param attributeType
178   *          The attribute type whose value should be parsed as a generalized
179   *          time value.
180   * @return The requested time, or -1 if it could not be determined.
181   * @throws DirectoryException
182   *           If a problem occurs while attempting to decode the value as a
183   *           generalized time.
184   */
185  protected static long getGeneralizedTime(final Entry entry,
186      final AttributeType attributeType) throws DirectoryException
187  {
188    long timeValue = -1;
189
190    final List<Attribute> attrList = entry.getAttribute(attributeType);
191    if (attrList != null)
192    {
193      for (final Attribute a : attrList)
194      {
195        if (a.isEmpty())
196        {
197          continue;
198        }
199
200        final ByteString v = a.iterator().next();
201        try
202        {
203          timeValue = GeneralizedTime.valueOf(v.toString()).getTimeInMillis();
204        }
205        catch (final Exception e)
206        {
207          logger.traceException(e, "Unable to decode value %s for attribute %s in user entry %s",
208              v, attributeType.getNameOrOID(), entry.getName());
209
210          final LocalizableMessage message = ERR_PWPSTATE_CANNOT_DECODE_GENERALIZED_TIME
211              .get(v, attributeType.getNameOrOID(), entry.getName(), e);
212          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
213              message, e);
214        }
215        break;
216      }
217    }
218
219    if (timeValue == -1 && logger.isTraceEnabled())
220     {
221      logger.trace("Returning -1 because attribute %s does not "
222          + "exist in user entry %s", attributeType.getNameOrOID(), entry.getName());
223    }
224
225    return timeValue;
226  }
227
228
229
230  /**
231   * A boolean indicating whether or not the account associated with this
232   * authentication state has been administratively disabled.
233   */
234  protected ConditionResult isDisabled = ConditionResult.UNDEFINED;
235
236  /**
237   * The user entry associated with this authentication policy state.
238   */
239  protected final Entry userEntry;
240
241
242
243  /**
244   * Creates a new abstract authentication policy context.
245   *
246   * @param userEntry
247   *          The user's entry.
248   */
249  protected AuthenticationPolicyState(final Entry userEntry)
250  {
251    this.userEntry = userEntry;
252  }
253
254
255
256  /**
257   * Performs any finalization required after a bind operation has completed.
258   * Implementations may perform internal operations in order to persist
259   * internal state to the user's entry if needed.
260   *
261   * @throws DirectoryException
262   *           If a problem occurs during finalization.
263   */
264  public void finalizeStateAfterBind() throws DirectoryException
265  {
266    // Do nothing by default.
267  }
268
269
270
271  /**
272   * Returns the authentication policy associated with this state.
273   *
274   * @return The authentication policy associated with this state.
275   */
276  public abstract AuthenticationPolicy getAuthenticationPolicy();
277
278
279
280  /**
281   * Returns {@code true} if this authentication policy state is associated with
282   * a user whose account has been administratively disabled.
283   * <p>
284   * The default implementation is use the value of the "ds-pwp-account-disable"
285   * attribute in the user's entry.
286   *
287   * @return {@code true} if this authentication policy state is associated with
288   *         a user whose account has been administratively disabled.
289   */
290  public boolean isDisabled()
291  {
292    final AttributeType type = DirectoryServer.getAttributeTypeOrDefault(OP_ATTR_ACCOUNT_DISABLED);
293    try
294    {
295      isDisabled = getBoolean(userEntry, type);
296    }
297    catch (final Exception e)
298    {
299      logger.traceException(e, "User %s is considered administratively "
300          + "disabled because an error occurred while "
301          + "attempting to make the determination.", userEntry.getName());
302
303      isDisabled = ConditionResult.TRUE;
304      return true;
305    }
306
307    if (isDisabled == ConditionResult.UNDEFINED)
308    {
309      isDisabled = ConditionResult.FALSE;
310      if (logger.isTraceEnabled())
311      {
312        logger.trace("User %s is not administratively disabled since "
313            + "the attribute \"%s\" is not present in the entry.", userEntry.getName(), OP_ATTR_ACCOUNT_DISABLED);
314      }
315      return false;
316    }
317
318    final boolean result = isDisabled == ConditionResult.TRUE;
319    if (logger.isTraceEnabled())
320    {
321      logger.trace("User %s is%s administratively disabled.", userEntry.getName(),
322          result ? "" : " not");
323    }
324    return result;
325  }
326
327
328
329  /**
330   * Returns {@code true} if this authentication policy state is associated with
331   * a password policy and the method {@link #getAuthenticationPolicy} will
332   * return a {@code PasswordPolicy}.
333   *
334   * @return {@code true} if this authentication policy state is associated with
335   *         a password policy, otherwise {@code false}.
336   */
337  public boolean isPasswordPolicy()
338  {
339    return getAuthenticationPolicy().isPasswordPolicy();
340  }
341
342
343
344  /**
345   * Returns {@code true} if the provided password value matches any of the
346   * user's passwords.
347   *
348   * @param password
349   *          The user-provided password to verify.
350   * @return {@code true} if the provided password value matches any of the
351   *         user's passwords.
352   * @throws DirectoryException
353   *           If verification unexpectedly failed.
354   */
355  public abstract boolean passwordMatches(ByteString password)
356      throws DirectoryException;
357}