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 }