blob: d1b1d9383504e10485d74488a4990af5c0f88664 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
package org.apache.directory.server.ldap.handlers.bind;
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.sasl.AuthorizeCallback;
import javax.security.sasl.RealmCallback;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.directory.server.constants.ServerDNConstants;
import org.apache.directory.server.core.CoreSession;
import org.apache.directory.server.core.DirectoryService;
import org.apache.directory.server.i18n.I18n;
import org.apache.directory.server.ldap.LdapSession;
import org.apache.directory.shared.ldap.model.constants.AuthenticationLevel;
import org.apache.directory.shared.ldap.model.entry.EntryAttribute;
import org.apache.directory.shared.ldap.model.exception.LdapOperationException;
import org.apache.directory.shared.ldap.model.message.BindRequest;
import org.apache.directory.shared.ldap.model.message.Control;
import org.apache.directory.shared.ldap.model.message.LdapResult;
import org.apache.directory.shared.ldap.model.message.ResultCodeEnum;
import org.apache.directory.shared.ldap.model.name.Dn;
import org.apache.directory.shared.ldap.util.JndiUtils;
import org.apache.directory.shared.util.Strings;
import org.apache.mina.core.session.IoSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Base class for all SASL {@link CallbackHandler}s. Implementations of SASL mechanisms
* selectively override the methods relevant to their mechanism.
*
* @see javax.security.auth.callback.CallbackHandler
* @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
*/
public abstract class AbstractSaslCallbackHandler implements CallbackHandler
{
/** The logger instance */
private static final Logger LOG = LoggerFactory.getLogger( AbstractSaslCallbackHandler.class );
/** An empty control array */
private static final Control[] EMPTY = new Control[0];
private String username;
private String realm;
/** The reference on the user ldap session */
protected LdapSession ldapSession;
/** The admin core session */
protected CoreSession adminSession;
/** A reference on the DirectoryService instance */
protected final DirectoryService directoryService;
/** The associated BindRequest */
protected final BindRequest bindRequest;
/**
* Creates a new instance of AbstractSaslCallbackHandler.
*
* @param directoryService
*/
protected AbstractSaslCallbackHandler( DirectoryService directoryService, BindRequest bindRequest )
{
this.directoryService = directoryService;
this.bindRequest = bindRequest;
}
/**
* Implementors use this method to access the username resulting from a callback.
* Callback default name will be username, eg 'hnelson', for CRAM-MD5 and DIGEST-MD5.
* The {@link NameCallback} is not used by GSSAPI.
*/
protected String getUsername()
{
return username;
}
/**
* Implementors use this method to access the realm resulting from a callback.
* Callback default text will be realm name, eg 'example.com', for DIGEST-MD5.
* The {@link RealmCallback} is not used by GSSAPI nor by CRAM-MD5.
*/
protected String getRealm()
{
return realm;
}
/**
* Implementors set the password based on a lookup, using the username and
* realm as keys.
* <ul>
* <li>For DIGEST-MD5, lookup password based on username and realm.
* <li>For CRAM-MD5, lookup password based on username.
* <li>For GSSAPI, this callback is unused.
* </ul>
* @param username The username.
* @param realm The realm.
* @return The Password entry attribute resulting from the lookup. It may contain more than one password
*/
protected abstract EntryAttribute lookupPassword( String username, String realm );
/**
* Final check to authorize user. Used by all SASL mechanisms. This
* is the only callback used by GSSAPI.
*
* Implementors use setAuthorizedID() to set the base Dn after canonicalization.
* Implementors must setAuthorized() to <code>true</code> if authentication was successful.
*
* @param callback An {@link AuthorizeCallback}.
*/
protected abstract void authorize( AuthorizeCallback callback ) throws Exception;
/**
* SaslServer will use this method to call various callbacks, depending on the SASL
* mechanism in use for a session.
*
* @param callbacks An array of one or more callbacks.
*/
public void handle( Callback[] callbacks )
{
for ( int i = 0; i < callbacks.length; i++ )
{
Callback callback = callbacks[i];
if ( LOG.isDebugEnabled() )
{
LOG.debug( "Processing callback {} of {}: {}" + callback.getClass(), ( i + 1 ), callbacks.length );
}
if ( callback instanceof NameCallback )
{
NameCallback nameCB = ( NameCallback ) callback;
LOG.debug( "NameCallback default name: {}", nameCB.getDefaultName() );
username = nameCB.getDefaultName();
}
else if ( callback instanceof RealmCallback )
{
RealmCallback realmCB = ( RealmCallback ) callback;
LOG.debug( "RealmCallback default text: {}", realmCB.getDefaultText() );
realm = realmCB.getDefaultText();
}
else if ( callback instanceof PasswordCallback )
{
PasswordCallback passwordCB = ( PasswordCallback ) callback;
EntryAttribute userPassword = lookupPassword( getUsername(), getRealm() );
if ( userPassword != null )
{
// We assume that we have only one password available
byte[] password = userPassword.get().getBytes();
String strPassword = Strings.utf8ToString(password);
passwordCB.setPassword( strPassword.toCharArray() );
}
}
else if ( callback instanceof AuthorizeCallback )
{
AuthorizeCallback authorizeCB = ( AuthorizeCallback ) callback;
// hnelson (CRAM-MD5, DIGEST-MD5)
// hnelson@EXAMPLE.COM (GSSAPI)
LOG.debug( "AuthorizeCallback authnID: {}", authorizeCB.getAuthenticationID() );
// hnelson (CRAM-MD5, DIGEST-MD5)
// hnelson@EXAMPLE.COM (GSSAPI)
LOG.debug( "AuthorizeCallback authzID: {}", authorizeCB.getAuthorizationID() );
// null (CRAM-MD5, DIGEST-MD5, GSSAPI)
LOG.debug( "AuthorizeCallback authorizedID: {}", authorizeCB.getAuthorizedID() );
// false (CRAM-MD5, DIGEST-MD5, GSSAPI)
LOG.debug( "AuthorizeCallback isAuthorized: {}", authorizeCB.isAuthorized() );
try
{
authorize( authorizeCB );
}
catch ( Exception e )
{
// TODO - figure out how to handle this properly.
throw new RuntimeException( I18n.err( I18n.ERR_677 ), e );
}
}
}
}
/**
* Convenience method for acquiring an {@link LdapContext} for the client to use for the
* duration of a session.
*
* @param session The current session.
* @param bindRequest The current BindRequest.
* @param env An environment to be used to acquire an {@link LdapContext}.
* @return An {@link LdapContext} for the client.
*/
protected LdapContext getContext( IoSession session, BindRequest bindRequest, Hashtable<String, Object> env )
{
LdapResult result = bindRequest.getResultResponse().getLdapResult();
LdapContext ctx = null;
try
{
Control[] connCtls = bindRequest.getControls().values().toArray( EMPTY );
env.put( DirectoryService.JNDI_KEY, directoryService );
ctx = new InitialLdapContext( env, JndiUtils.toJndiControls( directoryService.getLdapCodecService(),
connCtls ) );
}
catch ( Exception e )
{
ResultCodeEnum code;
Dn dn = null;
if ( e instanceof LdapOperationException )
{
code = ( (LdapOperationException) e ).getResultCode();
result.setResultCode( code );
dn = ( ( LdapOperationException ) e ).getResolvedDn();
}
else
{
code = ResultCodeEnum.getBestEstimate(e, bindRequest.getType());
result.setResultCode( code );
}
String msg = "Bind failed: " + e.getLocalizedMessage();
if ( LOG.isDebugEnabled() )
{
msg += ":\n" + ExceptionUtils.getStackTrace( e );
msg += "\n\nBindRequest = \n" + bindRequest.toString();
}
if ( ( dn != null )
&& ( ( code == ResultCodeEnum.NO_SUCH_OBJECT ) || ( code == ResultCodeEnum.ALIAS_PROBLEM )
|| ( code == ResultCodeEnum.INVALID_DN_SYNTAX ) || ( code == ResultCodeEnum.ALIAS_DEREFERENCING_PROBLEM ) ) )
{
result.setMatchedDn( dn );
}
result.setErrorMessage( msg );
session.write( bindRequest.getResultResponse() );
ctx = null;
}
return ctx;
}
/**
* Convenience method for getting an environment suitable for acquiring
* an {@link LdapContext} for the client.
*
* @param session The current session.
* @return An environment suitable for acquiring an {@link LdapContext} for the client.
*/
protected Hashtable<String, Object> getEnvironment( IoSession session )
{
Hashtable<String, Object> env = new Hashtable<String, Object>();
env.put( Context.PROVIDER_URL, session.getAttribute( "baseDn" ) );
env.put( Context.INITIAL_CONTEXT_FACTORY, "org.apache.directory.server.core.jndi.CoreContextFactory" );
env.put( Context.SECURITY_PRINCIPAL, ServerDNConstants.ADMIN_SYSTEM_DN );
env.put( Context.SECURITY_CREDENTIALS, "secret" );
env.put( Context.SECURITY_AUTHENTICATION, AuthenticationLevel.SIMPLE.toString() );
return env;
}
}