/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at
* trunk/opends/resource/legal-notices/OpenDS.LICENSE
* or https://OpenDS.dev.java.net/OpenDS.LICENSE.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at
* trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable,
* add the following below this CDDL HEADER, with the fields enclosed
* by brackets "[]" replaced with your own identifying information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Copyright 2008-2009 Sun Microsystems, Inc.
* Portions copyright 2011-2013 ForgeRock AS.
*/
package org.opends.server.extensions;
import static org.opends.messages.ExtensionMessages.*;
import static org.opends.server.loggers.debug.DebugLogger.*;
import static org.opends.server.util.ServerConstants.*;
import static org.opends.server.util.StaticUtils.*;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.locks.Lock;
import javax.security.auth.Subject;
import javax.security.auth.callback.*;
import javax.security.auth.login.LoginContext;
import javax.security.sasl.*;
import org.ietf.jgss.GSSException;
import org.opends.messages.Message;
import org.opends.server.api.AuthenticationPolicyState;
import org.opends.server.api.ClientConnection;
import org.opends.server.api.IdentityMapper;
import org.opends.server.core.AccessControlConfigManager;
import org.opends.server.core.BindOperation;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.PasswordPolicyState;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.ldap.LDAPClientConnection;
import org.opends.server.types.*;
/**
* This class defines the SASL context needed to process GSSAPI and DIGEST-MD5
* bind requests from clients.
*/
public class SASLContext implements CallbackHandler,
PrivilegedExceptionAction<Boolean>
{
// The tracer object for the debug logger.
private static final DebugTracer TRACER = getTracer();
/**
* Instantiate a GSSAPI/DIGEST-MD5 SASL context using the specified
* parameters.
*
* @param saslProps
* The properties to use in creating the SASL server.
* @param serverFQDN
* The fully qualified domain name to use in creating the SASL
* server.
* @param mechanism
* The SASL mechanism name.
* @param identityMapper
* The identity mapper to use in mapping identities.
* @return A fully instantiated SASL context to use in processing a SASL bind
* for the GSSAPI or DIGEST-MD5 mechanisms.
* @throws SaslException
* If the SASL server can not be instantiated.
*/
public static SASLContext createSASLContext(
final HashMap<String, String> saslProps, final String serverFQDN,
final String mechanism, final IdentityMapper<?> identityMapper)
throws SaslException
{
return (new SASLContext(saslProps, serverFQDN, mechanism, identityMapper));
}
// The SASL server to use in the authentication.
private SaslServer saslServer = null;
// The identity mapper to use when mapping identities.
private final IdentityMapper<?> identityMapper;
// The property set to use when creating the SASL server.
private final HashMap<String, String> saslProps;
// The fully qualified domain name to use when creating the SASL server.
private final String serverFQDN;
// The SASL mechanism name.
private final String mechanism;
// The authorization entry used in the authentication.
private Entry authEntry = null;
// The authorization entry used in the authentication.
private Entry authzEntry = null;
// The user name used in the authentication taken from the name callback.
private String userName;
// Error message used by callbacks.
private Message cbMsg;
// Error code used by callbacks.
private ResultCode cbResultCode;
// The current bind operation used by the callbacks.
private BindOperation bindOp;
// Used to check if negotiated QOP is confidentiality or integrity.
private static final String confidentiality = "auth-conf";
private static final String integrity = "auth-int";
/**
* Create a SASL context using the specified parameters. A SASL server will be
* instantiated only for the DIGEST-MD5 mechanism. The GSSAPI mechanism must
* instantiate the SASL server as the login context in a separate step.
*
* @param saslProps
* The properties to use in creating the SASL server.
* @param serverFQDN
* The fully qualified domain name to use in creating the SASL
* server.
* @param mechanism
* The SASL mechanism name.
* @param identityMapper
* The identity mapper to use in mapping identities.
* @throws SaslException
* If the SASL server can not be instantiated.
*/
private SASLContext(final HashMap<String, String> saslProps,
final String serverFQDN, final String mechanism,
final IdentityMapper<?> identityMapper) throws SaslException
{
this.identityMapper = identityMapper;
this.mechanism = mechanism;
this.saslProps = saslProps;
this.serverFQDN = serverFQDN;
if (mechanism.equals(SASL_MECHANISM_DIGEST_MD5))
{
initSASLServer();
}
}
/**
* Process the specified callback array.
*
* @param callbacks
* An array of callbacks that need processing.
* @throws UnsupportedCallbackException
* If a callback is not supported.
*/
@Override
public void handle(final Callback[] callbacks)
throws UnsupportedCallbackException
{
for (final Callback callback : callbacks)
{
if (callback instanceof NameCallback)
{
nameCallback((NameCallback) callback);
}
else if (callback instanceof PasswordCallback)
{
passwordCallback((PasswordCallback) callback);
}
else if (callback instanceof RealmCallback)
{
realmCallback((RealmCallback) callback);
}
else if (callback instanceof AuthorizeCallback)
{
authorizeCallback((AuthorizeCallback) callback);
}
else
{
final Message message = INFO_SASL_UNSUPPORTED_CALLBACK.get(mechanism,
String.valueOf(callback));
throw new UnsupportedCallbackException(callback, message.toString());
}
}
}
/**
* The method performs all GSSAPI processing. It is run as the context of the
* login context performed by the GSSAPI mechanism handler. See comments for
* processing overview.
*
* @return {@code true} if the authentication processing was successful.
*/
@Override
public Boolean run()
{
final ClientConnection clientConn = bindOp.getClientConnection();
// If the SASL server is null then this is the first handshake and the
// server needs to be initialized before any processing can be performed.
// If the SASL server cannot be created then all processing is abandoned
// and INVALID_CREDENTIALS is returned to the client.
if (saslServer == null)
{
try
{
initSASLServer();
}
catch (final SaslException ex)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, ex);
}
final GSSException gex = (GSSException) ex.getCause();
final Message msg;
if (gex != null)
{
msg = ERR_SASL_CONTEXT_CREATE_ERROR.get(SASL_MECHANISM_GSSAPI,
GSSAPISASLMechanismHandler.getGSSExceptionMessage(gex));
}
else
{
msg = ERR_SASL_CONTEXT_CREATE_ERROR.get(SASL_MECHANISM_GSSAPI,
getExceptionMessage(ex));
}
clientConn.setSASLAuthStateInfo(null);
bindOp.setAuthFailureReason(msg);
bindOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
return false;
}
}
final ByteString clientCredentials = bindOp.getSASLCredentials();
clientConn.setSASLAuthStateInfo(null);
try
{
final ByteString responseAuthStr = evaluateResponse(clientCredentials);
// If the bind has not been completed,then
// more handshake is needed and SASL_BIND_IN_PROGRESS is returned back
// to the client.
if (isBindComplete())
{
bindOp.setResultCode(ResultCode.SUCCESS);
bindOp.setSASLAuthUserEntry(authEntry);
final AuthenticationInfo authInfo = new AuthenticationInfo(authEntry,
authzEntry, mechanism, clientCredentials,
DirectoryServer.isRootDN(authEntry.getDN()));
bindOp.setAuthenticationInfo(authInfo);
// If confidentiality/integrity has been negotiated then
// create a SASL security provider and save it in the client
// connection. If confidentiality/integrity has not been
// negotiated, dispose of the SASL server.
if (isConfidentialIntegrity())
{
final SASLByteChannel saslByteChannel = SASLByteChannel
.getSASLByteChannel(clientConn, mechanism, this);
final LDAPClientConnection ldapConn =
(LDAPClientConnection) clientConn;
ldapConn.setSASLPendingProvider(saslByteChannel);
}
else
{
dispose();
clientConn.setSASLAuthStateInfo(null);
}
}
else
{
bindOp.setServerSASLCredentials(responseAuthStr);
clientConn.setSASLAuthStateInfo(this);
bindOp.setResultCode(ResultCode.SASL_BIND_IN_PROGRESS);
}
}
catch (final SaslException e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
final Message msg = ERR_SASL_PROTOCOL_ERROR.get(mechanism,
getExceptionMessage(e));
handleError(msg);
return false;
}
return true;
}
/**
* Dispose of the SASL server instance.
*/
void dispose()
{
try
{
saslServer.dispose();
}
catch (final SaslException e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
}
}
/**
* Evaluate the final stage of a DIGEST-MD5 SASL bind using the specified bind
* operation.
*
* @param bindOp
* The bind operation to use in processing.
*/
void evaluateFinalStage(final BindOperation bindOp)
{
this.bindOp = bindOp;
final ByteString clientCredentials = bindOp.getSASLCredentials();
if ((clientCredentials == null) || (clientCredentials.length() == 0))
{
final Message msg = ERR_SASL_NO_CREDENTIALS.get(mechanism, mechanism);
handleError(msg);
return;
}
final ClientConnection clientConn = bindOp.getClientConnection();
clientConn.setSASLAuthStateInfo(null);
try
{
final ByteString responseAuthStr = evaluateResponse(clientCredentials);
bindOp.setResultCode(ResultCode.SUCCESS);
bindOp.setServerSASLCredentials(responseAuthStr);
bindOp.setSASLAuthUserEntry(authEntry);
final AuthenticationInfo authInfo = new AuthenticationInfo(authEntry,
authzEntry, mechanism, clientCredentials,
DirectoryServer.isRootDN(authEntry.getDN()));
bindOp.setAuthenticationInfo(authInfo);
// If confidentiality/integrity has been negotiated, then create a
// SASL security provider and save it in the client connection for
// use in later processing.
if (isConfidentialIntegrity())
{
final SASLByteChannel saslByteChannel = SASLByteChannel
.getSASLByteChannel(clientConn, mechanism, this);
final LDAPClientConnection ldapConn = (LDAPClientConnection) clientConn;
ldapConn.setSASLPendingProvider(saslByteChannel);
}
else
{
dispose();
clientConn.setSASLAuthStateInfo(null);
}
}
catch (final SaslException e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
final Message msg = ERR_SASL_PROTOCOL_ERROR.get(mechanism,
getExceptionMessage(e));
handleError(msg);
}
}
/**
* Process the initial stage of a DIGEST-MD5 SASL bind using the specified
* bind operation.
*
* @param bindOp
* The bind operation to use in processing.
*/
void evaluateInitialStage(final BindOperation bindOp)
{
this.bindOp = bindOp;
final ClientConnection clientConn = bindOp.getClientConnection();
try
{
final ByteString challenge = evaluateResponse(ByteString.empty());
bindOp.setResultCode(ResultCode.SASL_BIND_IN_PROGRESS);
bindOp.setServerSASLCredentials(challenge);
clientConn.setSASLAuthStateInfo(this);
}
catch (final SaslException e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
final Message msg = ERR_SASL_PROTOCOL_ERROR.get(mechanism,
getExceptionMessage(e));
handleError(msg);
}
}
/**
* Returns the negotiated maximum size of protected data which can be received
* from the client.
*
* @return The negotiated maximum size of protected data which can be received
* from the client.
*/
int getMaxReceiveBufferSize()
{
String str = (String) saslServer.getNegotiatedProperty(Sasl.MAX_BUFFER);
if (str != null)
{
try
{
return Integer.parseInt(str);
}
catch (NumberFormatException e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
}
}
// Default buffer size if not specified according to Java SASL
// documentation.
return 65536;
}
/**
* Returns the negotiated maximum size of raw data which can be sent to the
* client.
*
* @return The negotiated maximum size of raw data which can be sent to the
* client.
*/
int getMaxRawSendBufferSize()
{
String str = (String) saslServer.getNegotiatedProperty(Sasl.RAW_SEND_SIZE);
if (str != null)
{
try
{
return Integer.parseInt(str);
}
catch (NumberFormatException e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
}
}
// Default buffer size if not specified according to Java SASL
// documentation.
return 65536;
}
/**
* Return the Security Strength Factor of the cipher if the QOP property is
* confidentiality, or, 1 if it is integrity.
*
* @return The SSF of the cipher used during confidentiality or integrity
* processing.
*/
int getSSF()
{
int ssf = 0;
final String qop = (String) saslServer.getNegotiatedProperty(Sasl.QOP);
if (qop.equalsIgnoreCase(integrity))
{
ssf = 1;
}
else
{
final String negStrength = (String) saslServer
.getNegotiatedProperty(Sasl.STRENGTH);
if (negStrength.equalsIgnoreCase("low"))
{
ssf = 40;
}
else if (negStrength.equalsIgnoreCase("medium"))
{
ssf = 56;
}
else
{
ssf = 128;
}
}
return ssf;
}
/**
* Return {@code true} if the bind has been completed. If the context is
* supporting confidentiality or integrity, the security provider will need to
* check if the context has completed the handshake with the client and is
* ready to process confidentiality or integrity messages.
*
* @return {@code true} if the handshaking is complete.
*/
boolean isBindComplete()
{
return saslServer.isComplete();
}
/**
* Perform the authentication as the specified login context. The specified
* bind operation needs to be saved so the callbacks have access to it. Only
* used by the GSSAPI mechanism.
*
* @param loginContext
* The login context to perform the authentication as.
* @param bindOp
* The bind operation needed by the callbacks to process the
* authentication.
*/
void performAuthentication(final LoginContext loginContext,
final BindOperation bindOp)
{
this.bindOp = bindOp;
try
{
Subject.doAs(loginContext.getSubject(), this);
}
catch (final PrivilegedActionException e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
final Message msg = ERR_SASL_PROTOCOL_ERROR.get(mechanism,
getExceptionMessage(e));
handleError(msg);
}
}
/**
* Unwrap the specified byte array using the provided offset and length
* values. Used only when the SASL server has negotiated confidentiality or
* integrity processing.
*
* @param bytes
* The byte array to unwrap.
* @param offset
* The offset in the array.
* @param len
* The length from the offset of the number of bytes to unwrap.
* @return A byte array containing the clear or unwrapped bytes.
* @throws SaslException
* If the bytes cannot be unwrapped.
*/
byte[] unwrap(final byte[] bytes, final int offset, final int len)
throws SaslException
{
return saslServer.unwrap(bytes, offset, len);
}
/**
* Wrap the specified clear byte array using the provided offset and length
* values. Used only when the SASL server has negotiated
* confidentiality/integrity processing.
*
* @param clearBytes
* The clear byte array to wrap.
* @param offset
* The offset into the clear byte array..
* @param len
* The length from the offset of the number of bytes to wrap.
* @return A byte array containing the wrapped bytes.
* @throws SaslException
* If the clear bytes cannot be wrapped.
*/
byte[] wrap(final byte[] clearBytes, final int offset, final int len)
throws SaslException
{
return saslServer.wrap(clearBytes, offset, len);
}
/**
* This callback is used to process the authorize callback. It is used during
* both GSSAPI and DIGEST-MD5 processing. When processing the GSSAPI
* mechanism, this is the only callback invoked. When processing the
* DIGEST-MD5 mechanism, it is the last callback invoked after the name and
* password callbacks respectively.
*
* @param callback
* The authorize callback instance to process.
*/
private void authorizeCallback(final AuthorizeCallback callback)
{
final String responseAuthzID = callback.getAuthorizationID();
// If the authEntry is null, then we are processing a GSSAPI SASL bind,
// and first need to try to map the authentication ID to an user entry.
// The authEntry is never null, when processing a DIGEST-MD5 SASL bind.
if (authEntry == null)
{
final String authid = callback.getAuthenticationID();
try
{
authEntry = identityMapper.getEntryForID(authid);
if (authEntry == null)
{
setCallbackMsg(ERR_SASL_AUTHENTRY_NO_MAPPED_ENTRY.get(authid));
callback.setAuthorized(false);
return;
}
}
catch (final DirectoryException de)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, de);
}
setCallbackMsg(ERR_SASL_CANNOT_MAP_AUTHENTRY.get(authid,
de.getMessage()));
callback.setAuthorized(false);
return;
}
userName = authid;
}
if (responseAuthzID.length() == 0)
{
setCallbackMsg(ERR_SASLDIGESTMD5_EMPTY_AUTHZID.get());
callback.setAuthorized(false);
return;
}
else if (!responseAuthzID.equals(userName))
{
final String lowerAuthzID = toLowerCase(responseAuthzID);
// Process the callback differently depending on if the authzid
// string begins with the string "dn:" or not.
if (lowerAuthzID.startsWith("dn:"))
{
authzDNCheck(callback);
}
else
{
authzIDCheck(callback);
}
}
else
{
authzEntry = authEntry;
callback.setAuthorized(true);
}
}
/**
* Process the specified authorize callback. This method is called if the
* callback's authorization ID begins with the string "dn:".
*
* @param callback
* The authorize callback to process.
*/
private void authzDNCheck(final AuthorizeCallback callback)
{
final String responseAuthzID = callback.getAuthorizationID();
DN authzDN;
callback.setAuthorized(true);
try
{
authzDN = DN.decode(responseAuthzID.substring(3));
}
catch (final DirectoryException e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
setCallbackMsg(ERR_SASL_AUTHZID_INVALID_DN.get(responseAuthzID,
e.getMessageObject()));
callback.setAuthorized(false);
return;
}
final DN actualAuthzDN = DirectoryServer.getActualRootBindDN(authzDN);
if (actualAuthzDN != null)
{
authzDN = actualAuthzDN;
}
if (!authzDN.equals(authEntry.getDN()))
{
if (authzDN.isNullDN())
{
authzEntry = null;
}
else
{
try
{
if ((authzEntry = DirectoryServer.getEntry(authzDN)) == null)
{
setCallbackMsg(ERR_SASL_AUTHZID_NO_SUCH_ENTRY.get(String
.valueOf(authzDN)));
callback.setAuthorized(false);
return;
}
}
catch (final DirectoryException e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
setCallbackMsg(ERR_SASL_AUTHZID_CANNOT_GET_ENTRY.get(
String.valueOf(authzDN), e.getMessageObject()));
callback.setAuthorized(false);
return;
}
}
final AuthenticationInfo authInfo = new AuthenticationInfo(authEntry,
DirectoryServer.isRootDN(authEntry.getDN()));
if (!hasPrivilege(authInfo))
{
callback.setAuthorized(false);
}
else
{
callback.setAuthorized(hasPermission(authInfo));
}
}
}
/**
* Process the specified authorize callback. This method is called if the
* callback's authorization ID does not begin with the string "dn:".
*
* @param callback
* The authorize callback to process.
*/
private void authzIDCheck(final AuthorizeCallback callback)
{
final String authzid = callback.getAuthorizationID();
final String lowerAuthzID = toLowerCase(authzid);
String idStr;
callback.setAuthorized(true);
if (lowerAuthzID.startsWith("u:"))
{
idStr = authzid.substring(2);
}
else
{
idStr = authzid;
}
if (idStr.length() == 0)
{
authzEntry = null;
}
else
{
try
{
authzEntry = identityMapper.getEntryForID(idStr);
if (authzEntry == null)
{
setCallbackMsg(ERR_SASL_AUTHZID_NO_MAPPED_ENTRY.get(authzid));
callback.setAuthorized(false);
return;
}
}
catch (final DirectoryException e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
setCallbackMsg(ERR_SASL_AUTHZID_NO_MAPPED_ENTRY.get(authzid));
callback.setAuthorized(false);
return;
}
}
if ((authzEntry == null) || (!authzEntry.getDN().equals(authEntry.getDN())))
{
// Create temporary authorization information and run it both
// through the privilege and then the access control subsystems.
final AuthenticationInfo authInfo = new AuthenticationInfo(authEntry,
DirectoryServer.isRootDN(authEntry.getDN()));
if (!hasPrivilege(authInfo))
{
callback.setAuthorized(false);
}
else
{
callback.setAuthorized(hasPermission(authInfo));
}
}
}
/**
* Helper routine to call the SASL server evaluateResponse method with the
* specified byte array.
*
* @param bytes
* The byte array to pass to the SASL server.
* @return A byte array containing the result of the evaluation.
* @throws SaslException
* If the SASL server cannot evaluate the byte array.
*/
private ByteString evaluateResponse(ByteString response) throws SaslException
{
if (response == null)
{
response = ByteString.empty();
}
final byte[] evalResponse = saslServer.evaluateResponse(response
.toByteArray());
if (evalResponse == null)
{
return ByteString.empty();
}
else
{
return ByteString.wrap(evalResponse);
}
}
/**
* Try to get a entry from the directory using the specified DN. Used only for
* DIGEST-MD5 SASL mechanism.
*
* @param userDN
* The DN of the entry to retrieve from the server.
*/
private void getAuthEntry(final DN userDN)
{
final Lock readLock = LockManager.lockRead(userDN);
if (readLock == null)
{
setCallbackMsg(INFO_SASL_CANNOT_LOCK_ENTRY.get(String.valueOf(userDN)));
return;
}
try
{
authEntry = DirectoryServer.getEntry(userDN);
}
catch (final DirectoryException e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
setCallbackMsg(ERR_SASL_CANNOT_GET_ENTRY_BY_DN.get(
String.valueOf(userDN), SASL_MECHANISM_DIGEST_MD5,
e.getMessageObject()));
return;
}
finally
{
LockManager.unlock(userDN, readLock);
}
}
/**
* This method is used to process an exception that is thrown during bind
* processing. It will try to determine if the exception is a result of
* callback processing, and if it is, will try to use a more informative
* failure message set by the callback. If the exception is a result of a
* error during the the SASL server processing, the callback message will be
* null, and the method will use the specified message parameter as the
* failure reason. This is a more cryptic exception message hard-coded in the
* SASL server internals. The method also disposes of the SASL server, clears
* the authentication state and sets the result code to INVALID_CREDENTIALs
*
* @param msg
* The message to use if the callback message is not null.
*/
private void handleError(final Message msg)
{
dispose();
final ClientConnection clientConn = bindOp.getClientConnection();
clientConn.setSASLAuthStateInfo(null);
// Check if the callback message is null and use that message if not.
if (cbResultCode != null)
{
bindOp.setResultCode(cbResultCode);
}
else
{
bindOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
}
if (cbMsg != null)
{
bindOp.setAuthFailureReason(cbMsg);
}
else
{
bindOp.setAuthFailureReason(msg);
}
}
/**
* Checks the specified authentication information parameter against the
* access control subsystem to see if it has the "proxy" right.
*
* @param authInfo
* The authentication information to check access on.
* @return {@code true} if the authentication information has proxy access.
*/
private boolean hasPermission(final AuthenticationInfo authInfo)
{
boolean ret = true;
Entry e = authzEntry;
// If the authz entry is null, use the entry associated with the NULL DN.
if (e == null)
{
try
{
e = DirectoryServer.getEntry(DN.nullDN());
}
catch (final DirectoryException ex)
{
return false;
}
}
if (AccessControlConfigManager.getInstance().getAccessControlHandler()
.mayProxy(authInfo.getAuthenticationEntry(), e, bindOp) == false)
{
setCallbackMsg(ERR_SASL_AUTHZID_INSUFFICIENT_ACCESS.get(String
.valueOf(authEntry.getDN())));
ret = false;
}
return ret;
}
/**
* Checks the specified authentication information parameter against the
* privilege subsystem to see if it has PROXIED_AUTH privileges.
*
* @param authInfo
* The authentication information to use in the check.
* @return {@code true} if the authentication information has PROXIED_AUTH
* privileges.
*/
private boolean hasPrivilege(final AuthenticationInfo authInfo)
{
boolean ret = true;
final InternalClientConnection tempConn = new InternalClientConnection(
authInfo);
if (!tempConn.hasPrivilege(Privilege.PROXIED_AUTH, bindOp))
{
setCallbackMsg(ERR_SASL_AUTHZID_INSUFFICIENT_PRIVILEGES.get(String
.valueOf(authEntry.getDN())));
ret = false;
}
return ret;
}
/**
* Initialize the SASL server using parameters specified in the constructor.
*/
private void initSASLServer() throws SaslException
{
saslServer = Sasl.createSaslServer(mechanism, SASL_DEFAULT_PROTOCOL,
serverFQDN, saslProps, this);
if (saslServer == null)
{
final Message msg = ERR_SASL_CREATE_SASL_SERVER_FAILED.get(mechanism,
serverFQDN);
throw new SaslException(Message.toString(msg));
}
}
/**
* Return true if the SASL server has negotiated with the client to support
* confidentiality or integrity.
*
* @return {@code true} if the context supports confidentiality or integrity.
*/
private boolean isConfidentialIntegrity()
{
boolean ret = false;
final String qop = (String) saslServer.getNegotiatedProperty(Sasl.QOP);
if (qop.equalsIgnoreCase(confidentiality)
|| qop.equalsIgnoreCase(integrity))
{
ret = true;
}
return ret;
}
/**
* Process the specified name callback. Used only for DIGEST-MD5 SASL
* mechanism.
*
* @param nameCallback
* The name callback to process.
*/
private void nameCallback(final NameCallback nameCallback)
{
userName = nameCallback.getDefaultName();
final String lowerUserName = toLowerCase(userName);
// Process the user name differently if it starts with the string "dn:".
if (lowerUserName.startsWith("dn:"))
{
DN userDN;
try
{
userDN = DN.decode(userName.substring(3));
}
catch (final DirectoryException e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
setCallbackMsg(ERR_SASL_CANNOT_DECODE_USERNAME_AS_DN.get(mechanism,
userName, e.getMessageObject()));
return;
}
if (userDN.isNullDN())
{
setCallbackMsg(ERR_SASL_USERNAME_IS_NULL_DN.get(mechanism));
return;
}
final DN rootDN = DirectoryServer.getActualRootBindDN(userDN);
if (rootDN != null)
{
userDN = rootDN;
}
getAuthEntry(userDN);
}
else
{
// The entry name is not a DN, try to map it using the identity
// mapper.
String entryID = userName;
if (lowerUserName.startsWith("u:"))
{
if (lowerUserName.equals("u:"))
{
setCallbackMsg(ERR_SASL_ZERO_LENGTH_USERNAME
.get(mechanism, mechanism));
return;
}
entryID = userName.substring(2);
}
try
{
authEntry = identityMapper.getEntryForID(entryID);
}
catch (final DirectoryException e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
setCallbackMsg(ERR_SASLDIGESTMD5_CANNOT_MAP_USERNAME.get(
String.valueOf(userName), e.getMessageObject()));
return;
}
}
if (authEntry == null)
{
// The authEntry is null, this is an error. The password callback
// will catch this error. There is no way to stop the processing
// from the name callback.
return;
}
}
/**
* Process the specified password callback. Used only for the DIGEST-MD5 SASL
* mechanism. The password callback is processed after the name callback.
*
* @param passwordCallback
* The password callback to process.
*/
private void passwordCallback(final PasswordCallback passwordCallback)
{
// If there is no authEntry this is an error.
if (authEntry == null)
{
setCallbackMsg(ERR_SASL_NO_MATCHING_ENTRIES.get(userName));
return;
}
// Try to get a clear password to use.
List<ByteString> clearPasswords;
try
{
final AuthenticationPolicyState authState = AuthenticationPolicyState
.forUser(authEntry, false);
if (!authState.isPasswordPolicy())
{
final Message message = ERR_SASL_ACCOUNT_NOT_LOCAL.get(mechanism,
String.valueOf(authEntry.getDN()));
setCallbackMsg(ResultCode.INAPPROPRIATE_AUTHENTICATION, message);
return;
}
final PasswordPolicyState pwPolicyState = (PasswordPolicyState) authState;
clearPasswords = pwPolicyState.getClearPasswords();
if ((clearPasswords == null) || clearPasswords.isEmpty())
{
setCallbackMsg(ERR_SASL_NO_REVERSIBLE_PASSWORDS.get(mechanism,
String.valueOf(authEntry.getDN())));
return;
}
}
catch (final Exception e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
setCallbackMsg(ERR_SASL_CANNOT_GET_REVERSIBLE_PASSWORDS.get(
String.valueOf(authEntry.getDN()), mechanism, String.valueOf(e)));
return;
}
// Use the first password.
final char[] password = clearPasswords.get(0).toString().toCharArray();
passwordCallback.setPassword(password);
return;
}
/**
* This callback is used to process realm information. It is not used.
*
* @param callback
* The realm callback instance to process.
*/
private void realmCallback(final RealmCallback callback)
{
}
/**
* Sets the callback message to the specified message.
*
* @param cbMsg
* The message to set the callback message to.
*/
private void setCallbackMsg(final Message cbMsg)
{
setCallbackMsg(ResultCode.INVALID_CREDENTIALS, cbMsg);
}
/**
* Sets the callback message to the specified message.
*
* @param cbResultCode
* The result code.
* @param cbMsg
* The message.
*/
private void setCallbackMsg(final ResultCode cbResultCode,
final Message cbMsg)
{
this.cbResultCode = cbResultCode;
this.cbMsg = cbMsg;
}
}