/*
|
* 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 legal-notices/CDDLv1_0.txt
|
* or http://forgerock.org/license/CDDLv1.0.html.
|
* 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 legal-notices/CDDLv1_0.txt.
|
* 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-2014 ForgeRock AS.
|
*/
|
package org.opends.server.extensions;
|
|
import static org.opends.messages.ExtensionMessages.*;
|
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.forgerock.i18n.LocalizableMessage;
|
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.forgerock.i18n.slf4j.LocalizedLogger;
|
import org.opends.server.protocols.internal.InternalClientConnection;
|
import org.opends.server.protocols.ldap.LDAPClientConnection;
|
import org.opends.server.types.*;
|
import org.forgerock.opendj.ldap.ResultCode;
|
import org.forgerock.opendj.ldap.ByteString;
|
|
|
|
/**
|
* This class defines the SASL context needed to process GSSAPI and DIGEST-MD5
|
* bind requests from clients.
|
*/
|
public class SASLContext implements CallbackHandler,
|
PrivilegedExceptionAction<Boolean>
|
{
|
private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
|
|
|
|
/**
|
* 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 LocalizableMessage 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 LocalizableMessage message = INFO_SASL_UNSUPPORTED_CALLBACK.get(mechanism, 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)
|
{
|
logger.traceException(ex);
|
final GSSException gex = (GSSException) ex.getCause();
|
|
final LocalizableMessage 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.getName()));
|
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)
|
{
|
logger.traceException(e);
|
|
final LocalizableMessage 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)
|
{
|
logger.traceException(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 LocalizableMessage 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.getName()));
|
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)
|
{
|
logger.traceException(e);
|
|
final LocalizableMessage 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)
|
{
|
logger.traceException(e);
|
final LocalizableMessage 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)
|
{
|
logger.traceException(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)
|
{
|
logger.traceException(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 (integrity.equalsIgnoreCase(qop))
|
{
|
ssf = 1;
|
}
|
else if (confidentiality.equalsIgnoreCase(qop))
|
{
|
final String negStrength = (String) saslServer
|
.getNegotiatedProperty(Sasl.STRENGTH);
|
if ("low".equalsIgnoreCase(negStrength))
|
{
|
ssf = 40;
|
}
|
else if ("medium".equalsIgnoreCase(negStrength))
|
{
|
ssf = 56;
|
}
|
else if ("high".equalsIgnoreCase(negStrength))
|
{
|
ssf = 128;
|
}
|
/* Treat anything else as if not security is provided and keep the
|
server running
|
*/
|
}
|
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)
|
{
|
logger.traceException(e);
|
final LocalizableMessage 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)
|
{
|
logger.traceException(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);
|
}
|
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.valueOf(responseAuthzID.substring(3));
|
}
|
catch (final DirectoryException e)
|
{
|
logger.traceException(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.getName()))
|
{
|
if (authzDN.isRootDN())
|
{
|
authzEntry = null;
|
}
|
else
|
{
|
try
|
{
|
if ((authzEntry = DirectoryServer.getEntry(authzDN)) == null)
|
{
|
setCallbackMsg(ERR_SASL_AUTHZID_NO_SUCH_ENTRY.get(authzDN));
|
callback.setAuthorized(false);
|
return;
|
}
|
}
|
catch (final DirectoryException e)
|
{
|
logger.traceException(e);
|
setCallbackMsg(ERR_SASL_AUTHZID_CANNOT_GET_ENTRY.get(authzDN, e.getMessageObject()));
|
callback.setAuthorized(false);
|
return;
|
}
|
}
|
final AuthenticationInfo authInfo = new AuthenticationInfo(authEntry,
|
DirectoryServer.isRootDN(authEntry.getName()));
|
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)
|
{
|
logger.traceException(e);
|
setCallbackMsg(ERR_SASL_AUTHZID_NO_MAPPED_ENTRY.get(authzid));
|
callback.setAuthorized(false);
|
return;
|
}
|
}
|
|
if ((authzEntry == null) || (!authzEntry.getName().
|
equals(authEntry.getName())))
|
{
|
// 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.getName()));
|
if (!hasPrivilege(authInfo))
|
{
|
callback.setAuthorized(false);
|
}
|
else
|
{
|
callback.setAuthorized(hasPermission(authInfo));
|
}
|
}
|
}
|
|
|
|
/**
|
* Helper routine to call the SASL server evaluateResponse method with the
|
* specified ByteString.
|
*
|
* @param response A ByteString containing the response to pass to the
|
* SASL server.
|
* @return A ByteString 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(userDN));
|
return;
|
}
|
|
try
|
{
|
authEntry = DirectoryServer.getEntry(userDN);
|
}
|
catch (final DirectoryException e)
|
{
|
logger.traceException(e);
|
setCallbackMsg(ERR_SASL_CANNOT_GET_ENTRY_BY_DN.get(
|
userDN, SASL_MECHANISM_DIGEST_MD5, e.getMessageObject()));
|
}
|
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 LocalizableMessage 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.rootDN());
|
}
|
catch (final DirectoryException ex)
|
{
|
return false;
|
}
|
}
|
|
if (!AccessControlConfigManager.getInstance().getAccessControlHandler()
|
.mayProxy(authInfo.getAuthenticationEntry(), e, bindOp))
|
{
|
setCallbackMsg(ERR_SASL_AUTHZID_INSUFFICIENT_ACCESS.get(authEntry.getName()));
|
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(authEntry.getName()));
|
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 LocalizableMessage msg = ERR_SASL_CREATE_SASL_SERVER_FAILED.get(mechanism,
|
serverFQDN);
|
throw new SaslException(msg.toString());
|
}
|
}
|
|
|
|
/**
|
* 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.valueOf(userName.substring(3));
|
}
|
catch (final DirectoryException e)
|
{
|
logger.traceException(e);
|
setCallbackMsg(ERR_SASL_CANNOT_DECODE_USERNAME_AS_DN.get(mechanism,
|
userName, e.getMessageObject()));
|
return;
|
}
|
|
if (userDN.isRootDN())
|
{
|
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)
|
{
|
logger.traceException(e);
|
setCallbackMsg(ERR_SASLDIGESTMD5_CANNOT_MAP_USERNAME.get(userName, e.getMessageObject()));
|
}
|
}
|
/*
|
At this point, the authEntry should not be null.
|
If it is, it's an error, but the password callback will catch it.
|
There is no way to stop the processing from the name callback.
|
*/
|
}
|
|
|
|
/**
|
* 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 LocalizableMessage message = ERR_SASL_ACCOUNT_NOT_LOCAL.get(mechanism,authEntry.getName());
|
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, authEntry.getName()));
|
return;
|
}
|
}
|
catch (final Exception e)
|
{
|
logger.traceException(e);
|
setCallbackMsg(ERR_SASL_CANNOT_GET_REVERSIBLE_PASSWORDS.get(authEntry.getName(), mechanism, e));
|
return;
|
}
|
|
// Use the first password.
|
final char[] password = clearPasswords.get(0).toString().toCharArray();
|
passwordCallback.setPassword(password);
|
}
|
|
|
|
/**
|
* 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 LocalizableMessage 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 LocalizableMessage cbMsg)
|
{
|
this.cbResultCode = cbResultCode;
|
this.cbMsg = cbMsg;
|
}
|
}
|