View Javadoc
1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one
3    *  or more contributor license agreements.  See the NOTICE file
4    *  distributed with this work for additional information
5    *  regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    *  "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    *
10   *    http://www.apache.org/licenses/LICENSE-2.0
11   *
12   *  Unless required by applicable law or agreed to in writing,
13   *  software distributed under the License is distributed on an
14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *  KIND, either express or implied.  See the License for the
16   *  specific language governing permissions and limitations
17   *  under the License.
18   *
19   */
20  package org.apache.directory.server.ldap.handlers.sasl;
21  
22  
23  import java.util.Hashtable;
24  
25  import javax.naming.Context;
26  import javax.naming.ldap.InitialLdapContext;
27  import javax.naming.ldap.LdapContext;
28  import javax.security.auth.callback.Callback;
29  import javax.security.auth.callback.CallbackHandler;
30  import javax.security.auth.callback.NameCallback;
31  import javax.security.auth.callback.PasswordCallback;
32  import javax.security.sasl.AuthorizeCallback;
33  import javax.security.sasl.RealmCallback;
34  
35  import org.apache.commons.lang3.exception.ExceptionUtils;
36  import org.apache.directory.api.ldap.model.constants.AuthenticationLevel;
37  import org.apache.directory.api.ldap.model.entry.Attribute;
38  import org.apache.directory.api.ldap.model.exception.LdapOperationException;
39  import org.apache.directory.api.ldap.model.message.BindRequest;
40  import org.apache.directory.api.ldap.model.message.Control;
41  import org.apache.directory.api.ldap.model.message.LdapResult;
42  import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
43  import org.apache.directory.api.ldap.model.name.Dn;
44  import org.apache.directory.api.ldap.util.JndiUtils;
45  import org.apache.directory.api.util.Strings;
46  import org.apache.directory.server.constants.ServerDNConstants;
47  import org.apache.directory.server.core.api.CoreSession;
48  import org.apache.directory.server.core.api.DirectoryService;
49  import org.apache.directory.server.i18n.I18n;
50  import org.apache.directory.server.ldap.LdapSession;
51  import org.apache.mina.core.session.IoSession;
52  import org.slf4j.Logger;
53  import org.slf4j.LoggerFactory;
54  
55  
56  /**
57   * Base class for all SASL {@link CallbackHandler}s.  Implementations of SASL mechanisms
58   * selectively override the methods relevant to their mechanism.
59   *
60   * @see javax.security.auth.callback.CallbackHandler
61   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
62   */
63  public abstract class AbstractSaslCallbackHandler implements CallbackHandler
64  {
65      /** The logger instance */
66      private static final Logger LOG = LoggerFactory.getLogger( AbstractSaslCallbackHandler.class );
67  
68      /** An empty control array */
69      private static final Control[] EMPTY = new Control[0];
70  
71      private String username;
72      private String realm;
73  
74      /** The reference on the user ldap session */
75      protected LdapSession ldapSession;
76  
77      /** The admin core session */
78      protected CoreSession adminSession;
79  
80      /** A reference on the DirectoryService instance */
81      protected final DirectoryService directoryService;
82  
83      /** The associated BindRequest */
84      protected final BindRequest bindRequest;
85  
86  
87      /**
88       * Creates a new instance of AbstractSaslCallbackHandler.
89       *
90       * @param directoryService The DirectoryService instance
91       * @param bindRequest The Bind request
92       */
93      protected AbstractSaslCallbackHandler( DirectoryService directoryService, BindRequest bindRequest )
94      {
95          this.directoryService = directoryService;
96          this.bindRequest = bindRequest;
97      }
98  
99  
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 }