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 2008-2010 Sun Microsystems, Inc.
025 *      Portions Copyright 2011-2015 ForgeRock AS.
026 */
027package org.opends.server.workflowelement.localbackend;
028
029import java.util.List;
030
031import org.forgerock.i18n.LocalizableMessage;
032import org.forgerock.i18n.LocalizableMessageDescriptor.Arg1;
033import org.forgerock.i18n.LocalizableMessageDescriptor.Arg2;
034import org.forgerock.i18n.slf4j.LocalizedLogger;
035import org.forgerock.opendj.ldap.ByteString;
036import org.forgerock.opendj.ldap.ResultCode;
037import org.opends.server.admin.std.meta.PasswordPolicyCfgDefn;
038import org.opends.server.api.AuthenticationPolicyState;
039import org.opends.server.api.Backend;
040import org.opends.server.api.ClientConnection;
041import org.opends.server.api.SASLMechanismHandler;
042import org.opends.server.controls.*;
043import org.opends.server.core.*;
044import org.opends.server.types.*;
045import org.opends.server.types.operation.PostOperationBindOperation;
046import org.opends.server.types.operation.PostResponseBindOperation;
047import org.opends.server.types.operation.PreOperationBindOperation;
048
049import static org.opends.messages.CoreMessages.*;
050import static org.opends.server.config.ConfigConstants.*;
051import static org.opends.server.types.AbstractOperation.*;
052import static org.opends.server.types.Privilege.*;
053import static org.opends.server.util.ServerConstants.*;
054import static org.opends.server.util.StaticUtils.*;
055
056/**
057 * This class defines an operation used to bind against the Directory Server,
058 * with the bound user entry within a local backend.
059 */
060public class LocalBackendBindOperation
061       extends BindOperationWrapper
062       implements PreOperationBindOperation, PostOperationBindOperation,
063                  PostResponseBindOperation
064{
065  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
066
067  /** The backend in which the bind operation should be processed. */
068  private Backend<?> backend;
069
070  /**
071   * Indicates whether the bind response should include the first warning
072   * for an upcoming password expiration.
073   */
074  private boolean isFirstWarning;
075  /** Indicates whether this bind is using a grace login for the user. */
076  private boolean isGraceLogin;
077
078  /** Indicates whether the user must change his/her password before doing anything else. */
079  private boolean mustChangePassword;
080
081  /** Indicates whether the user requested the password policy control. */
082  private boolean pwPolicyControlRequested;
083
084  /**
085   * Indicates whether the server should return the authorization ID as a
086   * control in the bind response.
087   */
088  private boolean returnAuthzID;
089
090  /** Indicates whether to execute post-operation plugins. */
091  private boolean executePostOpPlugins;
092
093  /** The client connection associated with this bind operation. */
094  private ClientConnection clientConnection;
095
096  /** The bind DN provided by the client. */
097  private DN bindDN;
098
099  /** The value to use for the password policy warning. */
100  private int pwPolicyWarningValue;
101  /** The lookthrough limit that should be enforced for the user. */
102  private int lookthroughLimit;
103  /** The size limit that should be enforced for the user. */
104  private int sizeLimit;
105  /** The time limit that should be enforced for the user. */
106  private int timeLimit;
107  /** The idle time limit that should be enforced for the user. */
108  private long idleTimeLimit;
109
110  /** Authentication policy state. */
111  private AuthenticationPolicyState authPolicyState;
112
113  /** The password policy error type for this bind operation. */
114  private PasswordPolicyErrorType pwPolicyErrorType;
115  /** The password policy warning type for this bind operation. */
116  private PasswordPolicyWarningType pwPolicyWarningType;
117
118  /** The plugin config manager for the Directory Server. */
119  private PluginConfigManager pluginConfigManager;
120
121  /** The SASL mechanism used for this bind operation. */
122  private String saslMechanism;
123
124  /**
125   * Creates a new operation that may be used to bind where
126   * the bound user entry is stored in a local backend of the Directory Server.
127   *
128   * @param bind The operation to enhance.
129   */
130  LocalBackendBindOperation(BindOperation bind)
131  {
132    super(bind);
133    LocalBackendWorkflowElement.attachLocalOperation (bind, this);
134  }
135
136  /**
137   * Process this bind operation in a local backend.
138   *
139   * @param wfe
140   *          The local backend work-flow element.
141   */
142  public void processLocalBind(LocalBackendWorkflowElement wfe)
143  {
144    this.backend = wfe.getBackend();
145
146    // Initialize a number of variables for use during the bind processing.
147    clientConnection         = getClientConnection();
148    returnAuthzID            = false;
149    executePostOpPlugins     = false;
150    sizeLimit                = DirectoryServer.getSizeLimit();
151    timeLimit                = DirectoryServer.getTimeLimit();
152    lookthroughLimit         = DirectoryServer.getLookthroughLimit();
153    idleTimeLimit            = DirectoryServer.getIdleTimeLimit();
154    bindDN                   = getBindDN();
155    saslMechanism            = getSASLMechanism();
156    authPolicyState          = null;
157    pwPolicyErrorType        = null;
158    pwPolicyControlRequested = false;
159    isGraceLogin             = false;
160    isFirstWarning           = false;
161    mustChangePassword       = false;
162    pwPolicyWarningType      = null;
163    pwPolicyWarningValue     = -1 ;
164    pluginConfigManager      = DirectoryServer.getPluginConfigManager();
165
166    processBind();
167
168    // Update the user's account with any password policy changes that may be
169    // required.
170    try
171    {
172      if (authPolicyState != null)
173      {
174        authPolicyState.finalizeStateAfterBind();
175      }
176    }
177    catch (DirectoryException de)
178    {
179      logger.traceException(de);
180
181      setResponseData(de);
182    }
183
184    // Invoke the post-operation bind plugins.
185    if (executePostOpPlugins)
186    {
187      processOperationResult(this, pluginConfigManager.invokePostOperationBindPlugins(this));
188    }
189
190    // Update the authentication information for the user.
191    AuthenticationInfo authInfo = getAuthenticationInfo();
192    if (getResultCode() == ResultCode.SUCCESS && authInfo != null)
193    {
194      clientConnection.setAuthenticationInfo(authInfo);
195      clientConnection.setSizeLimit(sizeLimit);
196      clientConnection.setTimeLimit(timeLimit);
197      clientConnection.setIdleTimeLimit(idleTimeLimit);
198      clientConnection.setLookthroughLimit(lookthroughLimit);
199      clientConnection.setMustChangePassword(mustChangePassword);
200
201      if (returnAuthzID)
202      {
203        addResponseControl(new AuthorizationIdentityResponseControl(
204                                    authInfo.getAuthorizationDN()));
205      }
206    }
207
208    // See if we need to send a password policy control to the client.  If so,
209    // then add it to the response.
210    if (pwPolicyControlRequested)
211    {
212      addResponseControl(new PasswordPolicyResponseControl(
213          pwPolicyWarningType, pwPolicyWarningValue, pwPolicyErrorType));
214    }
215    else
216    {
217      if (getResultCode() == ResultCode.SUCCESS)
218      {
219        if (pwPolicyErrorType == PasswordPolicyErrorType.PASSWORD_EXPIRED)
220        {
221          addResponseControl(new PasswordExpiredControl());
222        }
223        else if (pwPolicyWarningType == PasswordPolicyWarningType.TIME_BEFORE_EXPIRATION)
224        {
225          addResponseControl(new PasswordExpiringControl(pwPolicyWarningValue));
226        }
227        else if (mustChangePassword)
228        {
229          addResponseControl(new PasswordExpiredControl());
230        }
231      }
232      else
233      {
234        if (pwPolicyErrorType == PasswordPolicyErrorType.PASSWORD_EXPIRED)
235        {
236          addResponseControl(new PasswordExpiredControl());
237        }
238      }
239    }
240  }
241
242  /**
243   * Performs the checks and processing necessary for the current bind operation
244   * (simple or SASL).
245   */
246  private void processBind()
247  {
248    // Check to see if the client has permission to perform the bind.
249
250    // FIXME: for now assume that this will check all permission
251    // pertinent to the operation. This includes any controls specified.
252    try
253    {
254      if (!AccessControlConfigManager.getInstance().getAccessControlHandler().isAllowed(this))
255      {
256        setResultCode(ResultCode.INVALID_CREDENTIALS);
257        setAuthFailureReason(ERR_BIND_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS.get());
258        return;
259      }
260    }
261    catch (DirectoryException e)
262    {
263      setResultCode(e.getResultCode());
264      setAuthFailureReason(e.getMessageObject());
265      return;
266    }
267
268    // Check to see if there are any controls in the request. If so, then see
269    // if there is any special processing required.
270    try
271    {
272      handleRequestControls();
273    }
274    catch (DirectoryException de)
275    {
276      logger.traceException(de);
277
278      setResponseData(de);
279      return;
280    }
281
282    // Check to see if this is a simple bind or a SASL bind and process
283    // accordingly.
284    try
285    {
286      switch (getAuthenticationType())
287      {
288      case SIMPLE:
289        processSimpleBind();
290        break;
291
292      case SASL:
293        processSASLBind();
294        break;
295
296      default:
297        // Send a protocol error response to the client and disconnect.
298        // We should never come here.
299        setResultCode(ResultCode.PROTOCOL_ERROR);
300      }
301    }
302    catch (DirectoryException de)
303    {
304      logger.traceException(de);
305
306      if (de.getResultCode() == ResultCode.INVALID_CREDENTIALS)
307      {
308        setResultCode(ResultCode.INVALID_CREDENTIALS);
309        setAuthFailureReason(de.getMessageObject());
310      }
311      else
312      {
313        setResponseData(de);
314      }
315    }
316  }
317
318  /**
319   * Handles request control processing for this bind operation.
320   *
321   * @throws  DirectoryException  If there is a problem with any of the
322   *                              controls.
323   */
324  private void handleRequestControls() throws DirectoryException
325  {
326    LocalBackendWorkflowElement.removeAllDisallowedControls(bindDN, this);
327
328    List<Control> requestControls = getRequestControls();
329    if (requestControls != null && !requestControls.isEmpty())
330    {
331      for (Control c : requestControls)
332      {
333        final String  oid = c.getOID();
334
335        if (OID_AUTHZID_REQUEST.equals(oid))
336        {
337          returnAuthzID = true;
338        }
339        else if (OID_PASSWORD_POLICY_CONTROL.equals(oid))
340        {
341          pwPolicyControlRequested = true;
342        }
343
344        // NYI -- Add support for additional controls.
345        else if (c.isCritical())
346        {
347          throw new DirectoryException(
348                         ResultCode.UNAVAILABLE_CRITICAL_EXTENSION,
349                         ERR_BIND_UNSUPPORTED_CRITICAL_CONTROL.get(oid));
350        }
351      }
352    }
353  }
354
355  /**
356   * Performs the processing necessary for a simple bind operation.
357   *
358   * @return  {@code true} if processing should continue for the operation, or
359   *          {@code false} if not.
360   *
361   * @throws  DirectoryException  If a problem occurs that should cause the bind
362   *                              operation to fail.
363   */
364  private boolean processSimpleBind() throws DirectoryException
365  {
366    // See if this is an anonymous bind.  If so, then determine whether
367    // to allow it.
368    ByteString simplePassword = getSimplePassword();
369    if (simplePassword == null || simplePassword.length() == 0)
370    {
371      return processAnonymousSimpleBind();
372    }
373
374    // See if the bind DN is actually one of the alternate root DNs
375    // defined in the server.  If so, then replace it with the actual DN
376    // for that user.
377    DN actualRootDN = DirectoryServer.getActualRootBindDN(bindDN);
378    if (actualRootDN != null)
379    {
380      bindDN = actualRootDN;
381    }
382
383    Entry userEntry;
384    try
385    {
386      userEntry = backend.getEntry(bindDN);
387    }
388    catch (DirectoryException de)
389    {
390      logger.traceException(de);
391
392      userEntry = null;
393
394      if (de.getResultCode() == ResultCode.REFERRAL)
395      {
396        // Re-throw referral exceptions - these should be passed back to the client.
397        throw de;
398      }
399      else
400      {
401        // Replace other exceptions in case they expose any sensitive information.
402        throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, de.getMessageObject());
403      }
404    }
405
406    if (userEntry == null)
407    {
408      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
409                                   ERR_BIND_OPERATION_UNKNOWN_USER.get());
410    }
411    setUserEntryDN(userEntry.getName());
412
413    // Check to see if the user has a password. If not, then fail.
414    // FIXME -- We need to have a way to enable/disable debugging.
415    authPolicyState = AuthenticationPolicyState.forUser(userEntry, false);
416    if (authPolicyState.isPasswordPolicy())
417    {
418      // Account is managed locally.
419      PasswordPolicyState pwPolicyState = (PasswordPolicyState) authPolicyState;
420      PasswordPolicy policy = pwPolicyState.getAuthenticationPolicy();
421
422      AttributeType pwType = policy.getPasswordAttribute();
423      List<Attribute> pwAttr = userEntry.getAttribute(pwType);
424      if (pwAttr == null || pwAttr.isEmpty())
425      {
426        throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
427            ERR_BIND_OPERATION_NO_PASSWORD.get());
428      }
429
430      // Perform a number of password policy state checks for the
431      // non-authenticated user.
432      checkUnverifiedPasswordPolicyState(userEntry, null);
433
434      // Invoke pre-operation plugins.
435      if (!invokePreOpPlugins())
436      {
437        return false;
438      }
439
440      // Determine whether the provided password matches any of the stored
441      // passwords for the user.
442      if (pwPolicyState.passwordMatches(simplePassword))
443      {
444        setResultCode(ResultCode.SUCCESS);
445
446        checkVerifiedPasswordPolicyState(userEntry, null);
447
448        if (DirectoryServer.lockdownMode()
449            && !ClientConnection.hasPrivilege(userEntry, BYPASS_LOCKDOWN))
450        {
451          throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
452              ERR_BIND_REJECTED_LOCKDOWN_MODE.get());
453        }
454        setAuthenticationInfo(new AuthenticationInfo(userEntry, getBindDN(),
455            DirectoryServer.isRootDN(userEntry.getName())));
456
457        // Set resource limits for the authenticated user.
458        setResourceLimits(userEntry);
459
460        // Perform any remaining processing for a successful simple
461        // authentication.
462        pwPolicyState.handleDeprecatedStorageSchemes(simplePassword);
463        pwPolicyState.clearFailureLockout();
464
465        if (isFirstWarning)
466        {
467          pwPolicyState.setWarnedTime();
468
469          int numSeconds = pwPolicyState.getSecondsUntilExpiration();
470          LocalizableMessage m = WARN_BIND_PASSWORD_EXPIRING
471              .get(secondsToTimeString(numSeconds));
472
473          pwPolicyState.generateAccountStatusNotification(
474              AccountStatusNotificationType.PASSWORD_EXPIRING, userEntry, m,
475              AccountStatusNotification.createProperties(pwPolicyState,
476                  false, numSeconds, null, null));
477        }
478
479        if (isGraceLogin)
480        {
481          pwPolicyState.updateGraceLoginTimes();
482        }
483
484        pwPolicyState.setLastLoginTime();
485      }
486      else
487      {
488        setResultCode(ResultCode.INVALID_CREDENTIALS);
489        setAuthFailureReason(ERR_BIND_OPERATION_WRONG_PASSWORD.get());
490
491        if (policy.getLockoutFailureCount() > 0)
492        {
493          generateAccountStatusNotificationForLockedBindAccount(userEntry,
494              pwPolicyState);
495        }
496      }
497    }
498    else
499    {
500      // Check to see if the user is administratively disabled or locked.
501      if (authPolicyState.isDisabled())
502      {
503        throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
504            ERR_BIND_OPERATION_ACCOUNT_DISABLED.get());
505      }
506
507      // Invoke pre-operation plugins.
508      if (!invokePreOpPlugins())
509      {
510        return false;
511      }
512
513      if (authPolicyState.passwordMatches(simplePassword))
514      {
515        setResultCode(ResultCode.SUCCESS);
516
517        if (DirectoryServer.lockdownMode()
518            && !ClientConnection.hasPrivilege(userEntry, BYPASS_LOCKDOWN))
519        {
520          throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
521              ERR_BIND_REJECTED_LOCKDOWN_MODE.get());
522        }
523        setAuthenticationInfo(new AuthenticationInfo(userEntry, getBindDN(),
524            DirectoryServer.isRootDN(userEntry.getName())));
525
526        // Set resource limits for the authenticated user.
527        setResourceLimits(userEntry);
528      }
529      else
530      {
531        setResultCode(ResultCode.INVALID_CREDENTIALS);
532        setAuthFailureReason(ERR_BIND_OPERATION_WRONG_PASSWORD.get());
533      }
534    }
535
536    return true;
537  }
538
539  /**
540   * Performs the processing necessary for an anonymous simple bind.
541   *
542   * @return  {@code true} if processing should continue for the operation, or
543   *          {@code false} if not.
544   * @throws  DirectoryException  If a problem occurs that should cause the bind
545   *                              operation to fail.
546   */
547  private boolean processAnonymousSimpleBind() throws DirectoryException
548  {
549    // If the server is in lockdown mode, then fail.
550    if (DirectoryServer.lockdownMode())
551    {
552      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
553                                   ERR_BIND_REJECTED_LOCKDOWN_MODE.get());
554    }
555
556    // If there is a bind DN, then see whether that is acceptable.
557    if (DirectoryServer.bindWithDNRequiresPassword()
558        && bindDN != null && !bindDN.isRootDN())
559    {
560      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
561                                   ERR_BIND_DN_BUT_NO_PASSWORD.get());
562    }
563
564    // Invoke pre-operation plugins.
565    if (!invokePreOpPlugins())
566    {
567      return false;
568    }
569
570    setResultCode(ResultCode.SUCCESS);
571    setAuthenticationInfo(new AuthenticationInfo());
572    return true;
573  }
574
575  /**
576   * Performs the processing necessary for a SASL bind operation.
577   *
578   * @return  {@code true} if processing should continue for the operation, or
579   *          {@code false} if not.
580   *
581   * @throws  DirectoryException  If a problem occurs that should cause the bind
582   *                              operation to fail.
583   */
584  private boolean processSASLBind() throws DirectoryException
585  {
586    // Get the appropriate authentication handler for this request based
587    // on the SASL mechanism.  If there is none, then fail.
588    SASLMechanismHandler<?> saslHandler =
589         DirectoryServer.getSASLMechanismHandler(saslMechanism);
590    if (saslHandler == null)
591    {
592      throw new DirectoryException(ResultCode.AUTH_METHOD_NOT_SUPPORTED,
593                     ERR_BIND_OPERATION_UNKNOWN_SASL_MECHANISM.get(
594                          saslMechanism));
595    }
596
597    // Check to see if the client has sufficient permission to perform the bind.
598    // NYI
599
600    // Invoke pre-operation plugins.
601    if (!invokePreOpPlugins())
602    {
603      return false;
604    }
605
606    // Actually process the SASL bind.
607    saslHandler.processSASLBind(this);
608
609    // If the server is operating in lockdown mode, then we will need to
610    // ensure that the authentication was successful and performed as a
611    // root user to continue.
612    Entry saslAuthUserEntry = getSASLAuthUserEntry();
613    if (DirectoryServer.lockdownMode())
614    {
615      ResultCode resultCode = getResultCode();
616      if (resultCode != ResultCode.SASL_BIND_IN_PROGRESS
617          && (resultCode != ResultCode.SUCCESS
618              || saslAuthUserEntry == null
619              || !ClientConnection.hasPrivilege(saslAuthUserEntry, BYPASS_LOCKDOWN)))
620      {
621        throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
622                                     ERR_BIND_REJECTED_LOCKDOWN_MODE.get());
623      }
624    }
625
626    // Create the password policy state object.
627    if (saslAuthUserEntry != null)
628    {
629      setUserEntryDN(saslAuthUserEntry.getName());
630
631      // FIXME -- Need to have a way to enable debugging.
632      authPolicyState = AuthenticationPolicyState.forUser(
633          saslAuthUserEntry, false);
634      if (authPolicyState.isPasswordPolicy())
635      {
636        // Account is managed locally: perform password policy checks that can
637        // be completed before we have checked authentication was successful.
638        checkUnverifiedPasswordPolicyState(saslAuthUserEntry, saslHandler);
639      }
640    }
641
642    // Determine whether the authentication was successful and perform
643    // any remaining password policy processing accordingly.
644    ResultCode resultCode = getResultCode();
645    if (resultCode == ResultCode.SUCCESS)
646    {
647      if (authPolicyState != null && authPolicyState.isPasswordPolicy())
648      {
649        checkVerifiedPasswordPolicyState(saslAuthUserEntry, saslHandler);
650
651        PasswordPolicyState pwPolicyState =
652          (PasswordPolicyState) authPolicyState;
653
654        if (saslHandler.isPasswordBased(saslMechanism) &&
655            pwPolicyState.mustChangePassword())
656        {
657          mustChangePassword = true;
658        }
659
660        if (isFirstWarning)
661        {
662          pwPolicyState.setWarnedTime();
663
664          int numSeconds = pwPolicyState.getSecondsUntilExpiration();
665          LocalizableMessage m = WARN_BIND_PASSWORD_EXPIRING.get(
666                                 secondsToTimeString(numSeconds));
667
668          pwPolicyState.generateAccountStatusNotification(
669               AccountStatusNotificationType.PASSWORD_EXPIRING,
670               saslAuthUserEntry, m,
671               AccountStatusNotification.createProperties(pwPolicyState,
672                     false, numSeconds, null, null));
673        }
674
675        if (isGraceLogin)
676        {
677          pwPolicyState.updateGraceLoginTimes();
678        }
679
680        pwPolicyState.setLastLoginTime();
681      }
682
683      // Set appropriate resource limits for the user (note that SASL ANONYMOUS
684      // does not have a user).
685      if (saslAuthUserEntry != null)
686      {
687        setResourceLimits(saslAuthUserEntry);
688      }
689    }
690    else if (resultCode == ResultCode.SASL_BIND_IN_PROGRESS)
691    {
692      // FIXME -- Is any special processing needed here?
693      return false;
694    }
695    else
696    {
697      if (authPolicyState != null && authPolicyState.isPasswordPolicy())
698      {
699        PasswordPolicyState pwPolicyState = (PasswordPolicyState) authPolicyState;
700
701        if (saslHandler.isPasswordBased(saslMechanism)
702            && pwPolicyState.getAuthenticationPolicy().getLockoutFailureCount() > 0)
703        {
704          generateAccountStatusNotificationForLockedBindAccount(
705              saslAuthUserEntry, pwPolicyState);
706        }
707      }
708    }
709
710    return true;
711  }
712
713  private void generateAccountStatusNotificationForLockedBindAccount(
714      Entry userEntry, PasswordPolicyState pwPolicyState)
715  {
716    pwPolicyState.updateAuthFailureTimes();
717    if (pwPolicyState.lockedDueToFailures())
718    {
719      AccountStatusNotificationType notificationType;
720      boolean tempLocked;
721      LocalizableMessage m;
722
723      int lockoutDuration = pwPolicyState.getSecondsUntilUnlock();
724      if (lockoutDuration > -1)
725      {
726        notificationType = AccountStatusNotificationType.ACCOUNT_TEMPORARILY_LOCKED;
727        tempLocked = true;
728        m =
729            ERR_BIND_ACCOUNT_TEMPORARILY_LOCKED
730                .get(secondsToTimeString(lockoutDuration));
731      }
732      else
733      {
734        notificationType = AccountStatusNotificationType.ACCOUNT_PERMANENTLY_LOCKED;
735        tempLocked = false;
736        m = ERR_BIND_ACCOUNT_PERMANENTLY_LOCKED.get();
737      }
738
739      pwPolicyState.generateAccountStatusNotification(notificationType,
740          userEntry, m, AccountStatusNotification.createProperties(
741              pwPolicyState, tempLocked, -1, null, null));
742    }
743  }
744
745  private boolean invokePreOpPlugins()
746  {
747    executePostOpPlugins = true;
748    return processOperationResult(this, pluginConfigManager.invokePreOperationBindPlugins(this));
749  }
750
751  /**
752   * Validates a number of password policy state constraints for the user. This
753   * will be called before the offered credentials are checked.
754   *
755   * @param userEntry
756   *          The entry for the user that is authenticating.
757   * @param saslHandler
758   *          The SASL mechanism handler if this is a SASL bind, or {@code null}
759   *          for a simple bind.
760   * @throws DirectoryException
761   *           If a problem occurs that should cause the bind to fail.
762   */
763  private void checkUnverifiedPasswordPolicyState(
764      Entry userEntry, SASLMechanismHandler<?> saslHandler)
765      throws DirectoryException
766  {
767    PasswordPolicyState pwPolicyState = (PasswordPolicyState) authPolicyState;
768    PasswordPolicy policy = pwPolicyState.getAuthenticationPolicy();
769
770
771    // If the password policy is configured to track authentication failures or
772    // keep the last login time and the associated backend is disabled, then we
773    // may need to reject the bind immediately.
774    if ((policy.getStateUpdateFailurePolicy() ==
775         PasswordPolicyCfgDefn.StateUpdateFailurePolicy.PROACTIVE) &&
776        ((policy.getLockoutFailureCount() > 0) ||
777         ((policy.getLastLoginTimeAttribute() != null) &&
778          (policy.getLastLoginTimeFormat() != null))) &&
779        ((DirectoryServer.getWritabilityMode() == WritabilityMode.DISABLED) ||
780         (backend.getWritabilityMode() == WritabilityMode.DISABLED)))
781    {
782      // This policy isn't applicable to root users, so if it's a root
783      // user then ignore it.
784      if (! DirectoryServer.isRootDN(userEntry.getName()))
785      {
786        throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
787            ERR_BIND_OPERATION_WRITABILITY_DISABLED.get(userEntry.getName()));
788      }
789    }
790
791    // Check to see if the authentication must be done in a secure
792    // manner.  If so, then the client connection must be secure.
793    if (policy.isRequireSecureAuthentication()
794        && !clientConnection.isSecure())
795    {
796      boolean isSASLBind = saslHandler != null;
797      if (isSASLBind)
798      {
799        if (! saslHandler.isSecure(saslMechanism))
800        {
801          throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
802              ERR_BIND_OPERATION_INSECURE_SASL_BIND.get(saslMechanism, userEntry.getName()));
803        }
804      }
805      else
806      {
807        throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
808                       ERR_BIND_OPERATION_INSECURE_SIMPLE_BIND.get());
809      }
810    }
811  }
812
813  /**
814   * Perform policy checks for accounts when the credentials are correct.
815   *
816   * @param userEntry
817   *          The entry for the user that is authenticating.
818   * @param saslHandler
819   *          The SASL mechanism handler if this is a SASL bind, or {@code null}
820   *          for a simple bind.
821   * @throws DirectoryException
822   *           If a problem occurs that should cause the bind to fail.
823   */
824  private void checkVerifiedPasswordPolicyState(
825      Entry userEntry, SASLMechanismHandler<?> saslHandler)
826      throws DirectoryException
827  {
828    PasswordPolicyState pwPolicyState = (PasswordPolicyState) authPolicyState;
829    PasswordPolicy policy = pwPolicyState.getAuthenticationPolicy();
830
831    // Check to see if the user is administratively disabled or locked.
832    if (pwPolicyState.isDisabled())
833    {
834      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
835                                   ERR_BIND_OPERATION_ACCOUNT_DISABLED.get());
836    }
837    else if (pwPolicyState.isAccountExpired())
838    {
839      LocalizableMessage m = ERR_BIND_OPERATION_ACCOUNT_EXPIRED.get();
840      pwPolicyState.generateAccountStatusNotification(
841           AccountStatusNotificationType.ACCOUNT_EXPIRED, userEntry, m,
842           AccountStatusNotification.createProperties(pwPolicyState,
843                 false, -1, null, null));
844
845      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, m);
846    }
847    else if (pwPolicyState.lockedDueToFailures())
848    {
849      if (pwPolicyErrorType == null)
850      {
851        pwPolicyErrorType = PasswordPolicyErrorType.ACCOUNT_LOCKED;
852      }
853
854      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
855                     ERR_BIND_OPERATION_ACCOUNT_FAILURE_LOCKED.get());
856    }
857    else if (pwPolicyState.lockedDueToIdleInterval())
858    {
859      if (pwPolicyErrorType == null)
860      {
861        pwPolicyErrorType = PasswordPolicyErrorType.ACCOUNT_LOCKED;
862      }
863
864      LocalizableMessage m = ERR_BIND_OPERATION_ACCOUNT_IDLE_LOCKED.get();
865      pwPolicyState.generateAccountStatusNotification(
866           AccountStatusNotificationType.ACCOUNT_IDLE_LOCKED, userEntry, m,
867           AccountStatusNotification.createProperties(pwPolicyState, false, -1,
868                                                      null, null));
869
870      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, m);
871    }
872
873    // If it's a simple bind, or if it's a password-based SASL bind, then
874    // perform a number of password-based checks.
875    boolean isSASLBind = saslHandler != null;
876    if (!isSASLBind || saslHandler.isPasswordBased(saslMechanism))
877    {
878      // Check to see if the account is locked due to the maximum reset age.
879      if (pwPolicyState.lockedDueToMaximumResetAge())
880      {
881        if (pwPolicyErrorType == null)
882        {
883          pwPolicyErrorType = PasswordPolicyErrorType.ACCOUNT_LOCKED;
884        }
885
886        LocalizableMessage m = ERR_BIND_OPERATION_ACCOUNT_RESET_LOCKED.get();
887        pwPolicyState.generateAccountStatusNotification(
888             AccountStatusNotificationType.ACCOUNT_RESET_LOCKED, userEntry, m,
889             AccountStatusNotification.createProperties(pwPolicyState, false,
890                                                        -1, null, null));
891
892        throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, m);
893      }
894
895      // Determine whether the password is expired, or whether the user
896      // should be warned about an upcoming expiration.
897      if (pwPolicyState.isPasswordExpired())
898      {
899        if (pwPolicyErrorType == null)
900        {
901          pwPolicyErrorType = PasswordPolicyErrorType.PASSWORD_EXPIRED;
902        }
903
904        int maxGraceLogins = policy.getGraceLoginCount();
905        if (maxGraceLogins > 0 && pwPolicyState.mayUseGraceLogin())
906        {
907          List<Long> graceLoginTimes = pwPolicyState.getGraceLoginTimes();
908          if (graceLoginTimes == null ||
909              graceLoginTimes.size() < maxGraceLogins)
910          {
911            isGraceLogin       = true;
912            mustChangePassword = true;
913
914            if (pwPolicyWarningType == null)
915            {
916              pwPolicyWarningType =
917                   PasswordPolicyWarningType.GRACE_LOGINS_REMAINING;
918              pwPolicyWarningValue = maxGraceLogins -
919                                     (graceLoginTimes.size() + 1);
920            }
921          }
922          else
923          {
924            LocalizableMessage m = ERR_BIND_OPERATION_PASSWORD_EXPIRED.get();
925
926            pwPolicyState.generateAccountStatusNotification(
927                 AccountStatusNotificationType.PASSWORD_EXPIRED, userEntry, m,
928                 AccountStatusNotification.createProperties(pwPolicyState,
929                                                            false, -1, null,
930                                                            null));
931
932            throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, m);
933          }
934        }
935        else
936        {
937          LocalizableMessage m = ERR_BIND_OPERATION_PASSWORD_EXPIRED.get();
938
939          pwPolicyState.generateAccountStatusNotification(
940               AccountStatusNotificationType.PASSWORD_EXPIRED, userEntry, m,
941               AccountStatusNotification.createProperties(pwPolicyState, false,
942                                                          -1, null, null));
943
944          throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, m);
945        }
946      }
947      else if (pwPolicyState.shouldWarn())
948      {
949        int numSeconds = pwPolicyState.getSecondsUntilExpiration();
950
951        if (pwPolicyWarningType == null)
952        {
953          pwPolicyWarningType = PasswordPolicyWarningType.TIME_BEFORE_EXPIRATION;
954          pwPolicyWarningValue = numSeconds;
955        }
956
957        isFirstWarning = pwPolicyState.isFirstWarning();
958      }
959
960      // Check to see if the user's password has been reset.
961      if (pwPolicyState.mustChangePassword())
962      {
963        mustChangePassword = true;
964
965        if (pwPolicyErrorType == null)
966        {
967          pwPolicyErrorType = PasswordPolicyErrorType.CHANGE_AFTER_RESET;
968        }
969      }
970    }
971  }
972
973  /**
974   * Sets resource limits for the authenticated user.
975   *
976   * @param  userEntry  The entry for the authenticated user.
977   */
978  private void setResourceLimits(Entry userEntry)
979  {
980    // See if the user's entry contains a custom size limit.
981    Integer customSizeLimit =
982        getIntegerUserAttribute(userEntry, OP_ATTR_USER_SIZE_LIMIT,
983            WARN_BIND_MULTIPLE_USER_SIZE_LIMITS,
984            WARN_BIND_CANNOT_PROCESS_USER_SIZE_LIMIT);
985    if (customSizeLimit != null)
986    {
987      sizeLimit = customSizeLimit;
988    }
989
990    // See if the user's entry contains a custom time limit.
991    Integer customTimeLimit =
992        getIntegerUserAttribute(userEntry, OP_ATTR_USER_TIME_LIMIT,
993            WARN_BIND_MULTIPLE_USER_TIME_LIMITS,
994            WARN_BIND_CANNOT_PROCESS_USER_TIME_LIMIT);
995    if (customTimeLimit != null)
996    {
997      timeLimit = customTimeLimit;
998    }
999
1000    // See if the user's entry contains a custom idle time limit.
1001    // idleTimeLimit = 1000L * Long.parseLong(v.toString());
1002    Integer customIdleTimeLimitInSec =
1003        getIntegerUserAttribute(userEntry, OP_ATTR_USER_IDLE_TIME_LIMIT,
1004            WARN_BIND_MULTIPLE_USER_IDLE_TIME_LIMITS,
1005            WARN_BIND_CANNOT_PROCESS_USER_IDLE_TIME_LIMIT);
1006    if (customIdleTimeLimitInSec != null)
1007    {
1008      idleTimeLimit = 1000L * customIdleTimeLimitInSec;
1009    }
1010
1011    // See if the user's entry contains a custom lookthrough limit.
1012    Integer customLookthroughLimit =
1013        getIntegerUserAttribute(userEntry, OP_ATTR_USER_LOOKTHROUGH_LIMIT,
1014            WARN_BIND_MULTIPLE_USER_LOOKTHROUGH_LIMITS,
1015            WARN_BIND_CANNOT_PROCESS_USER_LOOKTHROUGH_LIMIT);
1016    if (customLookthroughLimit != null)
1017    {
1018      lookthroughLimit = customLookthroughLimit;
1019    }
1020  }
1021
1022  private Integer getIntegerUserAttribute(Entry userEntry,
1023      String attributeTypeName,
1024      Arg1<Object> nonUniqueAttributeMessage,
1025      Arg2<Object, Object> cannotProcessAttributeMessage)
1026  {
1027    AttributeType attrType = DirectoryServer.getAttributeTypeOrDefault(attributeTypeName);
1028    List<Attribute> attrList = userEntry.getAttribute(attrType);
1029    if (attrList != null && attrList.size() == 1)
1030    {
1031      Attribute a = attrList.get(0);
1032      if (a.size() == 1)
1033      {
1034        ByteString v = a.iterator().next();
1035        try
1036        {
1037          return Integer.valueOf(v.toString());
1038        }
1039        catch (Exception e)
1040        {
1041          logger.traceException(e);
1042          logger.error(cannotProcessAttributeMessage.get(v, userEntry.getName()));
1043        }
1044      }
1045      else if (a.size() > 1)
1046      {
1047        logger.error(nonUniqueAttributeMessage.get(userEntry.getName()));
1048      }
1049    }
1050    return null;
1051  }
1052}