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