001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 *
019 */
020package org.apache.directory.server.ldap.handlers.sasl;
021
022
023import java.util.Hashtable;
024
025import javax.naming.Context;
026import javax.naming.ldap.InitialLdapContext;
027import javax.naming.ldap.LdapContext;
028import javax.security.auth.callback.Callback;
029import javax.security.auth.callback.CallbackHandler;
030import javax.security.auth.callback.NameCallback;
031import javax.security.auth.callback.PasswordCallback;
032import javax.security.sasl.AuthorizeCallback;
033import javax.security.sasl.RealmCallback;
034
035import org.apache.commons.lang3.exception.ExceptionUtils;
036import org.apache.directory.api.ldap.model.constants.AuthenticationLevel;
037import org.apache.directory.api.ldap.model.entry.Attribute;
038import org.apache.directory.api.ldap.model.exception.LdapOperationException;
039import org.apache.directory.api.ldap.model.message.BindRequest;
040import org.apache.directory.api.ldap.model.message.Control;
041import org.apache.directory.api.ldap.model.message.LdapResult;
042import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
043import org.apache.directory.api.ldap.model.name.Dn;
044import org.apache.directory.api.ldap.util.JndiUtils;
045import org.apache.directory.api.util.Strings;
046import org.apache.directory.server.constants.ServerDNConstants;
047import org.apache.directory.server.core.api.CoreSession;
048import org.apache.directory.server.core.api.DirectoryService;
049import org.apache.directory.server.i18n.I18n;
050import org.apache.directory.server.ldap.LdapSession;
051import org.apache.mina.core.session.IoSession;
052import org.slf4j.Logger;
053import org.slf4j.LoggerFactory;
054
055
056/**
057 * Base class for all SASL {@link CallbackHandler}s.  Implementations of SASL mechanisms
058 * selectively override the methods relevant to their mechanism.
059 *
060 * @see javax.security.auth.callback.CallbackHandler
061 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
062 */
063public abstract class AbstractSaslCallbackHandler implements CallbackHandler
064{
065    /** The logger instance */
066    private static final Logger LOG = LoggerFactory.getLogger( AbstractSaslCallbackHandler.class );
067
068    /** An empty control array */
069    private static final Control[] EMPTY = new Control[0];
070
071    private String username;
072    private String realm;
073
074    /** The reference on the user ldap session */
075    protected LdapSession ldapSession;
076
077    /** The admin core session */
078    protected CoreSession adminSession;
079
080    /** A reference on the DirectoryService instance */
081    protected final DirectoryService directoryService;
082
083    /** The associated BindRequest */
084    protected final BindRequest bindRequest;
085
086
087    /**
088     * Creates a new instance of AbstractSaslCallbackHandler.
089     *
090     * @param directoryService The DirectoryService instance
091     * @param bindRequest The Bind request
092     */
093    protected AbstractSaslCallbackHandler( DirectoryService directoryService, BindRequest bindRequest )
094    {
095        this.directoryService = directoryService;
096        this.bindRequest = bindRequest;
097    }
098
099
100    /**
101     * Implementors use this method to access the username resulting from a callback.
102     * Callback default name will be username, eg 'hnelson', for CRAM-MD5 and DIGEST-MD5.
103     * The {@link NameCallback} is not used by GSSAPI.
104     *
105     * @return The user name
106     */
107    protected String getUsername()
108    {
109        return username;
110    }
111
112
113    /**
114     * Implementors use this method to access the realm resulting from a callback.
115     * Callback default text will be realm name, eg 'example.com', for DIGEST-MD5.
116     * The {@link RealmCallback} is not used by GSSAPI nor by CRAM-MD5.
117     *
118     * @return The realm
119     */
120    protected String getRealm()
121    {
122        return realm;
123    }
124
125
126    /**
127     * Implementors set the password based on a lookup, using the username and
128     * realm as keys.
129     * <ul>
130     * <li>For DIGEST-MD5, lookup password based on username and realm.
131     * <li>For CRAM-MD5, lookup password based on username.
132     * <li>For GSSAPI, this callback is unused.
133     * </ul>
134     * @param username The username.
135     * @param realm The realm.
136     * @return The Password entry attribute resulting from the lookup. It may contain more than one password
137     */
138    protected abstract Attribute lookupPassword( String username, String realm );
139
140
141    /**
142     * Final check to authorize user.  Used by all SASL mechanisms.  This
143     * is the only callback used by GSSAPI.
144     *
145     * Implementors use setAuthorizedID() to set the base Dn after canonicalization.
146     * Implementors must setAuthorized() to <code>true</code> if authentication was successful.
147     *
148     * @param callback An {@link AuthorizeCallback}.
149     * @throws Exception If the authorization failed
150     */
151    protected abstract void authorize( AuthorizeCallback callback ) throws Exception;
152
153
154    /**
155     * SaslServer will use this method to call various callbacks, depending on the SASL
156     * mechanism in use for a session.
157     *
158     * @param callbacks An array of one or more callbacks.
159     */
160    @Override
161    public void handle( Callback[] callbacks )
162    {
163        for ( int i = 0; i < callbacks.length; i++ )
164        {
165            Callback callback = callbacks[i];
166
167            if ( LOG.isDebugEnabled() )
168            {
169                LOG.debug( "Processing callback {} of {}: {}", callback.getClass(), ( i + 1 ), callbacks.length );
170            }
171
172            if ( callback instanceof NameCallback )
173            {
174                NameCallback nameCB = ( NameCallback ) callback;
175                LOG.debug( "NameCallback default name:  {}", nameCB.getDefaultName() );
176
177                username = nameCB.getDefaultName();
178            }
179            else if ( callback instanceof RealmCallback )
180            {
181                RealmCallback realmCB = ( RealmCallback ) callback;
182                LOG.debug( "RealmCallback default text:  {}", realmCB.getDefaultText() );
183
184                realm = realmCB.getDefaultText();
185            }
186            else if ( callback instanceof PasswordCallback )
187            {
188                PasswordCallback passwordCB = ( PasswordCallback ) callback;
189                Attribute userPassword = lookupPassword( getUsername(), getRealm() );
190
191                if ( userPassword != null )
192                {
193                    // We assume that we have only one password available
194                    byte[] password = userPassword.get().getBytes();
195
196                    String strPassword = Strings.utf8ToString( password );
197                    passwordCB.setPassword( strPassword.toCharArray() );
198                }
199            }
200            else if ( callback instanceof AuthorizeCallback )
201            {
202                AuthorizeCallback authorizeCB = ( AuthorizeCallback ) callback;
203
204                // hnelson (CRAM-MD5, DIGEST-MD5)
205                // hnelson@EXAMPLE.COM (GSSAPI)
206                LOG.debug( "AuthorizeCallback authnID:  {}", authorizeCB.getAuthenticationID() );
207
208                // hnelson (CRAM-MD5, DIGEST-MD5)
209                // hnelson@EXAMPLE.COM (GSSAPI)
210                LOG.debug( "AuthorizeCallback authzID:  {}", authorizeCB.getAuthorizationID() );
211
212                // null (CRAM-MD5, DIGEST-MD5, GSSAPI)
213                LOG.debug( "AuthorizeCallback authorizedID:  {}", authorizeCB.getAuthorizedID() );
214
215                // false (CRAM-MD5, DIGEST-MD5, GSSAPI)
216                LOG.debug( "AuthorizeCallback isAuthorized:  {}", authorizeCB.isAuthorized() );
217
218                try
219                {
220                    authorize( authorizeCB );
221                }
222                catch ( Exception e )
223                {
224                    // TODO - figure out how to handle this properly.
225                    throw new RuntimeException( I18n.err( I18n.ERR_677 ), e );
226                }
227            }
228        }
229    }
230
231
232    /**
233     * Convenience method for acquiring an {@link LdapContext} for the client to use for the
234     * duration of a session.
235     *
236     * @param session The current session.
237     * @param bindRequest The current BindRequest.
238     * @param env An environment to be used to acquire an {@link LdapContext}.
239     * @return An {@link LdapContext} for the client.
240     */
241    protected LdapContext getContext( IoSession session, BindRequest bindRequest, Hashtable<String, Object> env )
242    {
243        LdapResult result = bindRequest.getResultResponse().getLdapResult();
244
245        LdapContext ctx = null;
246
247        try
248        {
249            Control[] connCtls = bindRequest.getControls().values().toArray( EMPTY );
250            env.put( DirectoryService.JNDI_KEY, directoryService );
251            ctx = new InitialLdapContext( env, JndiUtils.toJndiControls( directoryService.getLdapCodecService(),
252                connCtls ) );
253        }
254        catch ( Exception e )
255        {
256            ResultCodeEnum code;
257            Dn dn = null;
258
259            if ( e instanceof LdapOperationException )
260            {
261                code = ( ( LdapOperationException ) e ).getResultCode();
262                result.setResultCode( code );
263                dn = ( ( LdapOperationException ) e ).getResolvedDn();
264            }
265            else
266            {
267                code = ResultCodeEnum.getBestEstimate( e, bindRequest.getType() );
268                result.setResultCode( code );
269            }
270
271            String msg = "Bind failed: " + e.getLocalizedMessage();
272
273            if ( LOG.isDebugEnabled() )
274            {
275                msg += ":\n" + ExceptionUtils.getStackTrace( e );
276                msg += "\n\nBindRequest = \n" + bindRequest.toString();
277            }
278
279            if ( ( dn != null )
280                && ( ( code == ResultCodeEnum.NO_SUCH_OBJECT ) || ( code == ResultCodeEnum.ALIAS_PROBLEM )
281                    || ( code == ResultCodeEnum.INVALID_DN_SYNTAX ) || ( code == ResultCodeEnum.ALIAS_DEREFERENCING_PROBLEM ) ) )
282            {
283                result.setMatchedDn( dn );
284            }
285
286            result.setDiagnosticMessage( msg );
287            session.write( bindRequest.getResultResponse() );
288            ctx = null;
289        }
290
291        return ctx;
292    }
293
294
295    /**
296     * Convenience method for getting an environment suitable for acquiring
297     * an {@link LdapContext} for the client.
298     *
299     * @param session The current session.
300     * @return An environment suitable for acquiring an {@link LdapContext} for the client.
301     */
302    protected Hashtable<String, Object> getEnvironment( IoSession session )
303    {
304        Hashtable<String, Object> env = new Hashtable<>();
305        env.put( Context.PROVIDER_URL, session.getAttribute( "baseDn" ) );
306        env.put( Context.INITIAL_CONTEXT_FACTORY, "org.apache.directory.server.core.jndi.CoreContextFactory" );
307        env.put( Context.SECURITY_PRINCIPAL, ServerDNConstants.ADMIN_SYSTEM_DN );
308        env.put( Context.SECURITY_CREDENTIALS, "secret" );
309        env.put( Context.SECURITY_AUTHENTICATION, AuthenticationLevel.SIMPLE.toString() );
310
311        return env;
312    }
313}