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-2009 Sun Microsystems, Inc.
025 *      Portions Copyright 2011-2015 ForgeRock AS.
026 */
027package org.opends.server.extensions;
028
029import static org.opends.messages.ExtensionMessages.*;
030import static org.opends.server.util.ServerConstants.*;
031import static org.opends.server.util.StaticUtils.*;
032
033import java.security.PrivilegedActionException;
034import java.security.PrivilegedExceptionAction;
035import java.util.HashMap;
036import java.util.List;
037import javax.security.auth.Subject;
038import javax.security.auth.callback.*;
039import javax.security.auth.login.LoginContext;
040import javax.security.sasl.*;
041
042import org.ietf.jgss.GSSException;
043import org.forgerock.i18n.LocalizableMessage;
044import org.opends.server.api.AuthenticationPolicyState;
045import org.opends.server.api.ClientConnection;
046import org.opends.server.api.IdentityMapper;
047import org.opends.server.core.AccessControlConfigManager;
048import org.opends.server.core.BindOperation;
049import org.opends.server.core.DirectoryServer;
050import org.opends.server.core.PasswordPolicyState;
051import org.forgerock.i18n.slf4j.LocalizedLogger;
052import org.opends.server.protocols.internal.InternalClientConnection;
053import org.opends.server.protocols.ldap.LDAPClientConnection;
054import org.opends.server.types.*;
055import org.forgerock.opendj.ldap.ResultCode;
056import org.forgerock.opendj.ldap.ByteString;
057
058/**
059 * This class defines the SASL context needed to process GSSAPI and DIGEST-MD5
060 * bind requests from clients.
061 */
062public class SASLContext implements CallbackHandler,
063    PrivilegedExceptionAction<Boolean>
064{
065  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
066
067
068
069  /**
070   * Instantiate a GSSAPI/DIGEST-MD5 SASL context using the specified
071   * parameters.
072   *
073   * @param saslProps
074   *          The properties to use in creating the SASL server.
075   * @param serverFQDN
076   *          The fully qualified domain name to use in creating the SASL
077   *          server.
078   * @param mechanism
079   *          The SASL mechanism name.
080   * @param identityMapper
081   *          The identity mapper to use in mapping identities.
082   * @return A fully instantiated SASL context to use in processing a SASL bind
083   *         for the GSSAPI or DIGEST-MD5 mechanisms.
084   * @throws SaslException
085   *           If the SASL server can not be instantiated.
086   */
087  public static SASLContext createSASLContext(
088      final HashMap<String, String> saslProps, final String serverFQDN,
089      final String mechanism, final IdentityMapper<?> identityMapper)
090      throws SaslException
091  {
092    return new SASLContext(saslProps, serverFQDN, mechanism, identityMapper);
093  }
094
095
096
097  /** The SASL server to use in the authentication. */
098  private SaslServer saslServer;
099
100  /** The identity mapper to use when mapping identities. */
101  private final IdentityMapper<?> identityMapper;
102
103  /** The property set to use when creating the SASL server. */
104  private final HashMap<String, String> saslProps;
105
106  /** The fully qualified domain name to use when creating the SASL server. */
107  private final String serverFQDN;
108
109  /** The SASL mechanism name. */
110  private final String mechanism;
111
112  /** The authorization entry used in the authentication. */
113  private Entry authEntry;
114
115  /** The authorization entry used in the authentication. */
116  private Entry authzEntry;
117
118  /** The user name used in the authentication taken from the name callback. */
119  private String userName;
120
121  /** Error message used by callbacks. */
122  private LocalizableMessage cbMsg;
123
124  /** Error code used by callbacks. */
125  private ResultCode cbResultCode;
126
127  /** The current bind operation used by the callbacks. */
128  private BindOperation bindOp;
129
130  /** Used to check if negotiated QOP is confidentiality or integrity. */
131  private static final String confidentiality = "auth-conf";
132  private static final String integrity = "auth-int";
133
134
135
136  /**
137   * Create a SASL context using the specified parameters. A SASL server will be
138   * instantiated only for the DIGEST-MD5 mechanism. The GSSAPI mechanism must
139   * instantiate the SASL server as the login context in a separate step.
140   *
141   * @param saslProps
142   *          The properties to use in creating the SASL server.
143   * @param serverFQDN
144   *          The fully qualified domain name to use in creating the SASL
145   *          server.
146   * @param mechanism
147   *          The SASL mechanism name.
148   * @param identityMapper
149   *          The identity mapper to use in mapping identities.
150   * @throws SaslException
151   *           If the SASL server can not be instantiated.
152   */
153  private SASLContext(final HashMap<String, String> saslProps,
154      final String serverFQDN, final String mechanism,
155      final IdentityMapper<?> identityMapper) throws SaslException
156  {
157    this.identityMapper = identityMapper;
158    this.mechanism = mechanism;
159    this.saslProps = saslProps;
160    this.serverFQDN = serverFQDN;
161
162    if (mechanism.equals(SASL_MECHANISM_DIGEST_MD5))
163    {
164      initSASLServer();
165    }
166  }
167
168
169
170  /**
171   * Process the specified callback array.
172   *
173   * @param callbacks
174   *          An array of callbacks that need processing.
175   * @throws UnsupportedCallbackException
176   *           If a callback is not supported.
177   */
178  @Override
179  public void handle(final Callback[] callbacks)
180      throws UnsupportedCallbackException
181  {
182    for (final Callback callback : callbacks)
183    {
184      if (callback instanceof NameCallback)
185      {
186        nameCallback((NameCallback) callback);
187      }
188      else if (callback instanceof PasswordCallback)
189      {
190        passwordCallback((PasswordCallback) callback);
191      }
192      else if (callback instanceof RealmCallback)
193      {
194        realmCallback((RealmCallback) callback);
195      }
196      else if (callback instanceof AuthorizeCallback)
197      {
198        authorizeCallback((AuthorizeCallback) callback);
199      }
200      else
201      {
202        final LocalizableMessage message = INFO_SASL_UNSUPPORTED_CALLBACK.get(mechanism, callback);
203        throw new UnsupportedCallbackException(callback, message.toString());
204      }
205    }
206  }
207
208
209
210  /**
211   * The method performs all GSSAPI processing. It is run as the context of the
212   * login context performed by the GSSAPI mechanism handler. See comments for
213   * processing overview.
214   *
215   * @return {@code true} if the authentication processing was successful.
216   */
217  @Override
218  public Boolean run()
219  {
220    final ClientConnection clientConn = bindOp.getClientConnection();
221
222    // If the SASL server is null then this is the first handshake and the
223    // server needs to be initialized before any processing can be performed.
224    // If the SASL server cannot be created then all processing is abandoned
225    // and INVALID_CREDENTIALS is returned to the client.
226    if (saslServer == null)
227    {
228      try
229      {
230        initSASLServer();
231      }
232      catch (final SaslException ex)
233      {
234        logger.traceException(ex);
235        final GSSException gex = (GSSException) ex.getCause();
236
237        final LocalizableMessage msg;
238        if (gex != null)
239        {
240          msg = ERR_SASL_CONTEXT_CREATE_ERROR.get(SASL_MECHANISM_GSSAPI,
241              GSSAPISASLMechanismHandler.getGSSExceptionMessage(gex));
242        }
243        else
244        {
245          msg = ERR_SASL_CONTEXT_CREATE_ERROR.get(SASL_MECHANISM_GSSAPI,
246              getExceptionMessage(ex));
247        }
248
249        clientConn.setSASLAuthStateInfo(null);
250        bindOp.setAuthFailureReason(msg);
251        bindOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
252        return false;
253      }
254    }
255
256    final ByteString clientCredentials = bindOp.getSASLCredentials();
257    clientConn.setSASLAuthStateInfo(null);
258    try
259    {
260      final ByteString responseAuthStr = evaluateResponse(clientCredentials);
261
262      // If the bind has not been completed,then
263      // more handshake is needed and SASL_BIND_IN_PROGRESS is returned back
264      // to the client.
265      if (isBindComplete())
266      {
267        bindOp.setResultCode(ResultCode.SUCCESS);
268        bindOp.setSASLAuthUserEntry(authEntry);
269        final AuthenticationInfo authInfo = new AuthenticationInfo(authEntry,
270            authzEntry, mechanism, clientCredentials,
271            DirectoryServer.isRootDN(authEntry.getName()));
272        bindOp.setAuthenticationInfo(authInfo);
273
274        // If confidentiality/integrity has been negotiated then
275        // create a SASL security provider and save it in the client
276        // connection. If confidentiality/integrity has not been
277        // negotiated, dispose of the SASL server.
278        if (isConfidentialIntegrity())
279        {
280          final SASLByteChannel saslByteChannel = SASLByteChannel
281              .getSASLByteChannel(clientConn, mechanism, this);
282          final LDAPClientConnection ldapConn =
283              (LDAPClientConnection) clientConn;
284          ldapConn.setSASLPendingProvider(saslByteChannel);
285        }
286        else
287        {
288          dispose();
289          clientConn.setSASLAuthStateInfo(null);
290        }
291      }
292      else
293      {
294        bindOp.setServerSASLCredentials(responseAuthStr);
295        clientConn.setSASLAuthStateInfo(this);
296        bindOp.setResultCode(ResultCode.SASL_BIND_IN_PROGRESS);
297      }
298    }
299    catch (final SaslException e)
300    {
301      logger.traceException(e);
302
303      final LocalizableMessage msg = ERR_SASL_PROTOCOL_ERROR.get(mechanism,
304          getExceptionMessage(e));
305      handleError(msg);
306      return false;
307    }
308
309    return true;
310  }
311
312
313
314  /**
315   * Dispose of the SASL server instance.
316   */
317  void dispose()
318  {
319    try
320    {
321      saslServer.dispose();
322    }
323    catch (final SaslException e)
324    {
325      logger.traceException(e);
326    }
327  }
328
329
330
331  /**
332   * Evaluate the final stage of a DIGEST-MD5 SASL bind using the specified bind
333   * operation.
334   *
335   * @param bindOp
336   *          The bind operation to use in processing.
337   */
338  void evaluateFinalStage(final BindOperation bindOp)
339  {
340    this.bindOp = bindOp;
341    final ByteString clientCredentials = bindOp.getSASLCredentials();
342
343    if (clientCredentials == null || clientCredentials.length() == 0)
344    {
345      final LocalizableMessage msg = ERR_SASL_NO_CREDENTIALS.get(mechanism, mechanism);
346      handleError(msg);
347      return;
348    }
349
350    final ClientConnection clientConn = bindOp.getClientConnection();
351    clientConn.setSASLAuthStateInfo(null);
352
353    try
354    {
355      final ByteString responseAuthStr = evaluateResponse(clientCredentials);
356      bindOp.setResultCode(ResultCode.SUCCESS);
357      bindOp.setServerSASLCredentials(responseAuthStr);
358      bindOp.setSASLAuthUserEntry(authEntry);
359      final AuthenticationInfo authInfo = new AuthenticationInfo(authEntry,
360          authzEntry, mechanism, clientCredentials,
361          DirectoryServer.isRootDN(authEntry.getName()));
362      bindOp.setAuthenticationInfo(authInfo);
363
364      // If confidentiality/integrity has been negotiated, then create a
365      // SASL security provider and save it in the client connection for
366      // use in later processing.
367      if (isConfidentialIntegrity())
368      {
369        final SASLByteChannel saslByteChannel = SASLByteChannel
370            .getSASLByteChannel(clientConn, mechanism, this);
371        final LDAPClientConnection ldapConn = (LDAPClientConnection) clientConn;
372        ldapConn.setSASLPendingProvider(saslByteChannel);
373      }
374      else
375      {
376        dispose();
377        clientConn.setSASLAuthStateInfo(null);
378      }
379    }
380    catch (final SaslException e)
381    {
382      logger.traceException(e);
383
384      final LocalizableMessage msg = ERR_SASL_PROTOCOL_ERROR.get(mechanism,
385          getExceptionMessage(e));
386      handleError(msg);
387    }
388  }
389
390
391
392  /**
393   * Process the initial stage of a DIGEST-MD5 SASL bind using the specified
394   * bind operation.
395   *
396   * @param bindOp
397   *          The bind operation to use in processing.
398   */
399  void evaluateInitialStage(final BindOperation bindOp)
400  {
401    this.bindOp = bindOp;
402    final ClientConnection clientConn = bindOp.getClientConnection();
403
404    try
405    {
406      final ByteString challenge = evaluateResponse(ByteString.empty());
407      bindOp.setResultCode(ResultCode.SASL_BIND_IN_PROGRESS);
408      bindOp.setServerSASLCredentials(challenge);
409      clientConn.setSASLAuthStateInfo(this);
410    }
411    catch (final SaslException e)
412    {
413      logger.traceException(e);
414      final LocalizableMessage msg = ERR_SASL_PROTOCOL_ERROR.get(mechanism,
415          getExceptionMessage(e));
416      handleError(msg);
417    }
418  }
419
420
421
422  /**
423   * Returns the negotiated maximum size of protected data which can be received
424   * from the client.
425   *
426   * @return The negotiated maximum size of protected data which can be received
427   *         from the client.
428   */
429  int getMaxReceiveBufferSize()
430  {
431    String str = (String) saslServer.getNegotiatedProperty(Sasl.MAX_BUFFER);
432    if (str != null)
433    {
434      try
435      {
436        return Integer.parseInt(str);
437      }
438      catch (NumberFormatException e)
439      {
440        logger.traceException(e);
441      }
442    }
443
444    // Default buffer size if not specified according to Java SASL
445    // documentation.
446    return 65536;
447  }
448
449
450
451  /**
452   * Returns the negotiated maximum size of raw data which can be sent to the
453   * client.
454   *
455   * @return The negotiated maximum size of raw data which can be sent to the
456   *         client.
457   */
458  int getMaxRawSendBufferSize()
459  {
460    String str = (String) saslServer.getNegotiatedProperty(Sasl.RAW_SEND_SIZE);
461    if (str != null)
462    {
463      try
464      {
465        return Integer.parseInt(str);
466      }
467      catch (NumberFormatException e)
468      {
469        logger.traceException(e);
470      }
471    }
472
473    // Default buffer size if not specified according to Java SASL
474    // documentation.
475    return 65536;
476  }
477
478
479
480  /**
481   * Return the Security Strength Factor of the cipher if the QOP property is
482   * confidentiality, or, 1 if it is integrity.
483   *
484   * @return The SSF of the cipher used during confidentiality or integrity
485   *         processing.
486   */
487  int getSSF()
488  {
489    int ssf = 0;
490    final String qop = (String) saslServer.getNegotiatedProperty(Sasl.QOP);
491    if (integrity.equalsIgnoreCase(qop))
492    {
493      ssf = 1;
494    }
495    else if (confidentiality.equalsIgnoreCase(qop))
496    {
497      final String negStrength = (String) saslServer
498          .getNegotiatedProperty(Sasl.STRENGTH);
499      if ("low".equalsIgnoreCase(negStrength))
500      {
501        ssf = 40;
502      }
503      else if ("medium".equalsIgnoreCase(negStrength))
504      {
505        ssf = 56;
506      }
507      else if ("high".equalsIgnoreCase(negStrength))
508      {
509        ssf = 128;
510      }
511      /* Treat anything else as if not security is provided and keep the
512        server running
513       */
514    }
515    return ssf;
516  }
517
518
519
520  /**
521   * Return {@code true} if the bind has been completed. If the context is
522   * supporting confidentiality or integrity, the security provider will need to
523   * check if the context has completed the handshake with the client and is
524   * ready to process confidentiality or integrity messages.
525   *
526   * @return {@code true} if the handshaking is complete.
527   */
528  boolean isBindComplete()
529  {
530    return saslServer.isComplete();
531  }
532
533
534
535  /**
536   * Perform the authentication as the specified login context. The specified
537   * bind operation needs to be saved so the callbacks have access to it. Only
538   * used by the GSSAPI mechanism.
539   *
540   * @param loginContext
541   *          The login context to perform the authentication as.
542   * @param bindOp
543   *          The bind operation needed by the callbacks to process the
544   *          authentication.
545   */
546  void performAuthentication(final LoginContext loginContext,
547      final BindOperation bindOp)
548  {
549    this.bindOp = bindOp;
550    try
551    {
552      Subject.doAs(loginContext.getSubject(), this);
553    }
554    catch (final PrivilegedActionException e)
555    {
556      logger.traceException(e);
557      final LocalizableMessage msg = ERR_SASL_PROTOCOL_ERROR.get(mechanism,
558          getExceptionMessage(e));
559      handleError(msg);
560    }
561  }
562
563
564
565  /**
566   * Unwrap the specified byte array using the provided offset and length
567   * values. Used only when the SASL server has negotiated confidentiality or
568   * integrity processing.
569   *
570   * @param bytes
571   *          The byte array to unwrap.
572   * @param offset
573   *          The offset in the array.
574   * @param len
575   *          The length from the offset of the number of bytes to unwrap.
576   * @return A byte array containing the clear or unwrapped bytes.
577   * @throws SaslException
578   *           If the bytes cannot be unwrapped.
579   */
580  byte[] unwrap(final byte[] bytes, final int offset, final int len)
581      throws SaslException
582  {
583    return saslServer.unwrap(bytes, offset, len);
584  }
585
586
587
588  /**
589   * Wrap the specified clear byte array using the provided offset and length
590   * values. Used only when the SASL server has negotiated
591   * confidentiality/integrity processing.
592   *
593   * @param clearBytes
594   *          The clear byte array to wrap.
595   * @param offset
596   *          The offset into the clear byte array..
597   * @param len
598   *          The length from the offset of the number of bytes to wrap.
599   * @return A byte array containing the wrapped bytes.
600   * @throws SaslException
601   *           If the clear bytes cannot be wrapped.
602   */
603  byte[] wrap(final byte[] clearBytes, final int offset, final int len)
604      throws SaslException
605  {
606    return saslServer.wrap(clearBytes, offset, len);
607  }
608
609
610
611  /**
612   * This callback is used to process the authorize callback. It is used during
613   * both GSSAPI and DIGEST-MD5 processing. When processing the GSSAPI
614   * mechanism, this is the only callback invoked. When processing the
615   * DIGEST-MD5 mechanism, it is the last callback invoked after the name and
616   * password callbacks respectively.
617   *
618   * @param callback
619   *          The authorize callback instance to process.
620   */
621  private void authorizeCallback(final AuthorizeCallback callback)
622  {
623    final String responseAuthzID = callback.getAuthorizationID();
624
625    // If the authEntry is null, then we are processing a GSSAPI SASL bind,
626    // and first need to try to map the authentication ID to an user entry.
627    // The authEntry is never null, when processing a DIGEST-MD5 SASL bind.
628    if (authEntry == null)
629    {
630      final String authid = callback.getAuthenticationID();
631      try
632      {
633        authEntry = identityMapper.getEntryForID(authid);
634        if (authEntry == null)
635        {
636          setCallbackMsg(ERR_SASL_AUTHENTRY_NO_MAPPED_ENTRY.get(authid));
637          callback.setAuthorized(false);
638          return;
639        }
640      }
641      catch (final DirectoryException de)
642      {
643        logger.traceException(de);
644        setCallbackMsg(ERR_SASL_CANNOT_MAP_AUTHENTRY.get(authid,
645            de.getMessage()));
646        callback.setAuthorized(false);
647        return;
648      }
649      userName = authid;
650    }
651
652    if (responseAuthzID.length() == 0)
653    {
654      setCallbackMsg(ERR_SASLDIGESTMD5_EMPTY_AUTHZID.get());
655      callback.setAuthorized(false);
656    }
657    else if (!responseAuthzID.equals(userName))
658    {
659      final String lowerAuthzID = toLowerCase(responseAuthzID);
660
661      // Process the callback differently depending on if the authzid
662      // string begins with the string "dn:" or not.
663      if (lowerAuthzID.startsWith("dn:"))
664      {
665        authzDNCheck(callback);
666      }
667      else
668      {
669        authzIDCheck(callback);
670      }
671    }
672    else
673    {
674      authzEntry = authEntry;
675      callback.setAuthorized(true);
676    }
677  }
678
679
680
681  /**
682   * Process the specified authorize callback. This method is called if the
683   * callback's authorization ID begins with the string "dn:".
684   *
685   * @param callback
686   *          The authorize callback to process.
687   */
688  private void authzDNCheck(final AuthorizeCallback callback)
689  {
690    final String responseAuthzID = callback.getAuthorizationID();
691    DN authzDN;
692    callback.setAuthorized(true);
693
694    try
695    {
696      authzDN = DN.valueOf(responseAuthzID.substring(3));
697    }
698    catch (final DirectoryException e)
699    {
700      logger.traceException(e);
701      setCallbackMsg(ERR_SASL_AUTHZID_INVALID_DN.get(responseAuthzID,
702          e.getMessageObject()));
703      callback.setAuthorized(false);
704      return;
705    }
706
707    final DN actualAuthzDN = DirectoryServer.getActualRootBindDN(authzDN);
708    if (actualAuthzDN != null)
709    {
710      authzDN = actualAuthzDN;
711    }
712
713    if (!authzDN.equals(authEntry.getName()))
714    {
715      if (authzDN.isRootDN())
716      {
717        authzEntry = null;
718      }
719      else
720      {
721        try
722        {
723          authzEntry = DirectoryServer.getEntry(authzDN);
724          if (authzEntry == null)
725          {
726            setCallbackMsg(ERR_SASL_AUTHZID_NO_SUCH_ENTRY.get(authzDN));
727            callback.setAuthorized(false);
728            return;
729          }
730        }
731        catch (final DirectoryException e)
732        {
733          logger.traceException(e);
734          setCallbackMsg(ERR_SASL_AUTHZID_CANNOT_GET_ENTRY.get(authzDN, e.getMessageObject()));
735          callback.setAuthorized(false);
736          return;
737        }
738      }
739      final AuthenticationInfo authInfo = new AuthenticationInfo(authEntry,
740          DirectoryServer.isRootDN(authEntry.getName()));
741      if (!hasPrivilege(authInfo))
742      {
743        callback.setAuthorized(false);
744      }
745      else
746      {
747        callback.setAuthorized(hasPermission(authInfo));
748      }
749    }
750  }
751
752
753
754  /**
755   * Process the specified authorize callback. This method is called if the
756   * callback's authorization ID does not begin with the string "dn:".
757   *
758   * @param callback
759   *          The authorize callback to process.
760   */
761  private void authzIDCheck(final AuthorizeCallback callback)
762  {
763    final String authzid = callback.getAuthorizationID();
764    final String lowerAuthzID = toLowerCase(authzid);
765    String idStr;
766    callback.setAuthorized(true);
767
768    if (lowerAuthzID.startsWith("u:"))
769    {
770      idStr = authzid.substring(2);
771    }
772    else
773    {
774      idStr = authzid;
775    }
776
777    if (idStr.length() == 0)
778    {
779      authzEntry = null;
780    }
781    else
782    {
783      try
784      {
785        authzEntry = identityMapper.getEntryForID(idStr);
786        if (authzEntry == null)
787        {
788          setCallbackMsg(ERR_SASL_AUTHZID_NO_MAPPED_ENTRY.get(authzid));
789          callback.setAuthorized(false);
790          return;
791        }
792      }
793      catch (final DirectoryException e)
794      {
795        logger.traceException(e);
796        setCallbackMsg(ERR_SASL_AUTHZID_NO_MAPPED_ENTRY.get(authzid));
797        callback.setAuthorized(false);
798        return;
799      }
800    }
801
802    if (authzEntry == null || !authzEntry.getName().equals(authEntry.getName()))
803    {
804      // Create temporary authorization information and run it both
805      // through the privilege and then the access control subsystems.
806      final AuthenticationInfo authInfo = new AuthenticationInfo(authEntry,
807          DirectoryServer.isRootDN(authEntry.getName()));
808      if (!hasPrivilege(authInfo))
809      {
810        callback.setAuthorized(false);
811      }
812      else
813      {
814        callback.setAuthorized(hasPermission(authInfo));
815      }
816    }
817  }
818
819
820
821  /**
822   * Helper routine to call the SASL server evaluateResponse method with the
823   * specified ByteString.
824   *
825   * @param response A ByteString containing the response to pass to the
826   *                 SASL server.
827   * @return A ByteString containing the result of the evaluation.
828   * @throws SaslException
829   *           If the SASL server cannot evaluate the byte array.
830   */
831  private ByteString evaluateResponse(ByteString response) throws SaslException
832  {
833    if (response == null)
834    {
835      response = ByteString.empty();
836    }
837
838    final byte[] evalResponse = saslServer.evaluateResponse(response
839        .toByteArray());
840    if (evalResponse == null)
841    {
842      return ByteString.empty();
843    }
844    else
845    {
846      return ByteString.wrap(evalResponse);
847    }
848  }
849
850
851
852  /**
853   * Try to get a entry from the directory using the specified DN. Used only for
854   * DIGEST-MD5 SASL mechanism.
855   *
856   * @param userDN
857   *          The DN of the entry to retrieve from the server.
858   */
859  private void getAuthEntry(final DN userDN)
860  {
861    try
862    {
863      authEntry = DirectoryServer.getEntry(userDN);
864    }
865    catch (final DirectoryException e)
866    {
867      logger.traceException(e);
868      setCallbackMsg(ERR_SASL_CANNOT_GET_ENTRY_BY_DN.get(
869          userDN, SASL_MECHANISM_DIGEST_MD5, e.getMessageObject()));
870    }
871  }
872
873
874
875  /**
876   * This method is used to process an exception that is thrown during bind
877   * processing. It will try to determine if the exception is a result of
878   * callback processing, and if it is, will try to use a more informative
879   * failure message set by the callback. If the exception is a result of a
880   * error during the the SASL server processing, the callback message will be
881   * null, and the method will use the specified message parameter as the
882   * failure reason. This is a more cryptic exception message hard-coded in the
883   * SASL server internals. The method also disposes of the SASL server, clears
884   * the authentication state and sets the result code to INVALID_CREDENTIALs
885   *
886   * @param msg
887   *          The message to use if the callback message is not null.
888   */
889  private void handleError(final LocalizableMessage msg)
890  {
891    dispose();
892    final ClientConnection clientConn = bindOp.getClientConnection();
893    clientConn.setSASLAuthStateInfo(null);
894
895    // Check if the callback message is null and use that message if not.
896    if (cbResultCode != null)
897    {
898      bindOp.setResultCode(cbResultCode);
899    }
900    else
901    {
902      bindOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
903    }
904
905    if (cbMsg != null)
906    {
907      bindOp.setAuthFailureReason(cbMsg);
908    }
909    else
910    {
911      bindOp.setAuthFailureReason(msg);
912    }
913  }
914
915
916
917  /**
918   * Checks the specified authentication information parameter against the
919   * access control subsystem to see if it has the "proxy" right.
920   *
921   * @param authInfo
922   *          The authentication information to check access on.
923   * @return {@code true} if the authentication information has proxy access.
924   */
925  private boolean hasPermission(final AuthenticationInfo authInfo)
926  {
927    boolean ret = true;
928    Entry e = authzEntry;
929
930    // If the authz entry is null, use the entry associated with the NULL DN.
931    if (e == null)
932    {
933      try
934      {
935        e = DirectoryServer.getEntry(DN.rootDN());
936      }
937      catch (final DirectoryException ex)
938      {
939        return false;
940      }
941    }
942
943    if (!AccessControlConfigManager.getInstance().getAccessControlHandler()
944        .mayProxy(authInfo.getAuthenticationEntry(), e, bindOp))
945    {
946      setCallbackMsg(ERR_SASL_AUTHZID_INSUFFICIENT_ACCESS.get(authEntry.getName()));
947      ret = false;
948    }
949
950    return ret;
951  }
952
953
954
955  /**
956   * Checks the specified authentication information parameter against the
957   * privilege subsystem to see if it has PROXIED_AUTH privileges.
958   *
959   * @param authInfo
960   *          The authentication information to use in the check.
961   * @return {@code true} if the authentication information has PROXIED_AUTH
962   *         privileges.
963   */
964  private boolean hasPrivilege(final AuthenticationInfo authInfo)
965  {
966    boolean ret = true;
967    final InternalClientConnection tempConn = new InternalClientConnection(
968        authInfo);
969    if (!tempConn.hasPrivilege(Privilege.PROXIED_AUTH, bindOp))
970    {
971      setCallbackMsg(ERR_SASL_AUTHZID_INSUFFICIENT_PRIVILEGES.get(authEntry.getName()));
972      ret = false;
973    }
974    return ret;
975  }
976
977
978
979  /**
980   * Initialize the SASL server using parameters specified in the constructor.
981   */
982  private void initSASLServer() throws SaslException
983  {
984    saslServer = Sasl.createSaslServer(mechanism, SASL_DEFAULT_PROTOCOL,
985        serverFQDN, saslProps, this);
986    if (saslServer == null)
987    {
988      final LocalizableMessage msg = ERR_SASL_CREATE_SASL_SERVER_FAILED.get(mechanism,
989          serverFQDN);
990      throw new SaslException(msg.toString());
991    }
992  }
993
994
995
996  /**
997   * Return true if the SASL server has negotiated with the client to support
998   * confidentiality or integrity.
999   *
1000   * @return {@code true} if the context supports confidentiality or integrity.
1001   */
1002  private boolean isConfidentialIntegrity()
1003  {
1004    boolean ret = false;
1005    final String qop = (String) saslServer.getNegotiatedProperty(Sasl.QOP);
1006    if (qop.equalsIgnoreCase(confidentiality)
1007        || qop.equalsIgnoreCase(integrity))
1008    {
1009      ret = true;
1010    }
1011    return ret;
1012  }
1013
1014
1015
1016  /**
1017   * Process the specified name callback. Used only for DIGEST-MD5 SASL
1018   * mechanism.
1019   *
1020   * @param nameCallback
1021   *          The name callback to process.
1022   */
1023  private void nameCallback(final NameCallback nameCallback)
1024  {
1025    userName = nameCallback.getDefaultName();
1026    final String lowerUserName = toLowerCase(userName);
1027
1028    // Process the user name differently if it starts with the string "dn:".
1029    if (lowerUserName.startsWith("dn:"))
1030    {
1031      DN userDN;
1032      try
1033      {
1034        userDN = DN.valueOf(userName.substring(3));
1035      }
1036      catch (final DirectoryException e)
1037      {
1038        logger.traceException(e);
1039        setCallbackMsg(ERR_SASL_CANNOT_DECODE_USERNAME_AS_DN.get(mechanism,
1040            userName, e.getMessageObject()));
1041        return;
1042      }
1043
1044      if (userDN.isRootDN())
1045      {
1046        setCallbackMsg(ERR_SASL_USERNAME_IS_NULL_DN.get(mechanism));
1047        return;
1048      }
1049
1050      final DN rootDN = DirectoryServer.getActualRootBindDN(userDN);
1051      if (rootDN != null)
1052      {
1053        userDN = rootDN;
1054      }
1055      getAuthEntry(userDN);
1056    }
1057    else
1058    {
1059      // The entry name is not a DN, try to map it using the identity
1060      // mapper.
1061      String entryID = userName;
1062      if (lowerUserName.startsWith("u:"))
1063      {
1064        if (lowerUserName.equals("u:"))
1065        {
1066          setCallbackMsg(ERR_SASL_ZERO_LENGTH_USERNAME
1067              .get(mechanism, mechanism));
1068          return;
1069        }
1070        entryID = userName.substring(2);
1071      }
1072      try
1073      {
1074        authEntry = identityMapper.getEntryForID(entryID);
1075      }
1076      catch (final DirectoryException e)
1077      {
1078        logger.traceException(e);
1079        setCallbackMsg(ERR_SASLDIGESTMD5_CANNOT_MAP_USERNAME.get(userName, e.getMessageObject()));
1080      }
1081    }
1082    /*
1083      At this point, the authEntry should not be null.
1084      If it is, it's an error, but the password callback will catch it.
1085      There is no way to stop the processing from the name callback.
1086    */
1087  }
1088
1089
1090
1091  /**
1092   * Process the specified password callback. Used only for the DIGEST-MD5 SASL
1093   * mechanism. The password callback is processed after the name callback.
1094   *
1095   * @param passwordCallback
1096   *          The password callback to process.
1097   */
1098  private void passwordCallback(final PasswordCallback passwordCallback)
1099  {
1100    // If there is no authEntry this is an error.
1101    if (authEntry == null)
1102    {
1103      setCallbackMsg(ERR_SASL_NO_MATCHING_ENTRIES.get(userName));
1104      return;
1105    }
1106
1107    // Try to get a clear password to use.
1108    List<ByteString> clearPasswords;
1109    try
1110    {
1111      final AuthenticationPolicyState authState = AuthenticationPolicyState
1112          .forUser(authEntry, false);
1113
1114      if (!authState.isPasswordPolicy())
1115      {
1116        final LocalizableMessage message = ERR_SASL_ACCOUNT_NOT_LOCAL.get(mechanism,authEntry.getName());
1117        setCallbackMsg(ResultCode.INAPPROPRIATE_AUTHENTICATION, message);
1118        return;
1119      }
1120
1121      final PasswordPolicyState pwPolicyState = (PasswordPolicyState) authState;
1122
1123      clearPasswords = pwPolicyState.getClearPasswords();
1124      if (clearPasswords == null || clearPasswords.isEmpty())
1125      {
1126        setCallbackMsg(ERR_SASL_NO_REVERSIBLE_PASSWORDS.get(mechanism, authEntry.getName()));
1127        return;
1128      }
1129    }
1130    catch (final Exception e)
1131    {
1132      logger.traceException(e);
1133      setCallbackMsg(ERR_SASL_CANNOT_GET_REVERSIBLE_PASSWORDS.get(authEntry.getName(), mechanism, e));
1134      return;
1135    }
1136
1137    // Use the first password.
1138    final char[] password = clearPasswords.get(0).toString().toCharArray();
1139    passwordCallback.setPassword(password);
1140  }
1141
1142
1143
1144  /**
1145   * This callback is used to process realm information. It is not used.
1146   *
1147   * @param callback
1148   *          The realm callback instance to process.
1149   */
1150  private void realmCallback(final RealmCallback callback)
1151  {
1152  }
1153
1154
1155
1156  /**
1157   * Sets the callback message to the specified message.
1158   *
1159   * @param cbMsg
1160   *          The message to set the callback message to.
1161   */
1162  private void setCallbackMsg(final LocalizableMessage cbMsg)
1163  {
1164    setCallbackMsg(ResultCode.INVALID_CREDENTIALS, cbMsg);
1165  }
1166
1167
1168
1169  /**
1170   * Sets the callback message to the specified message.
1171   *
1172   * @param cbResultCode
1173   *          The result code.
1174   * @param cbMsg
1175   *          The message.
1176   */
1177  private void setCallbackMsg(final ResultCode cbResultCode,
1178      final LocalizableMessage cbMsg)
1179  {
1180    this.cbResultCode = cbResultCode;
1181    this.cbMsg = cbMsg;
1182  }
1183}