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 }