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