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