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 }