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 */ 020package org.apache.directory.server.ldap.handlers.sasl; 021 022 023import java.util.Hashtable; 024 025import javax.naming.Context; 026import javax.naming.ldap.InitialLdapContext; 027import javax.naming.ldap.LdapContext; 028import javax.security.auth.callback.Callback; 029import javax.security.auth.callback.CallbackHandler; 030import javax.security.auth.callback.NameCallback; 031import javax.security.auth.callback.PasswordCallback; 032import javax.security.sasl.AuthorizeCallback; 033import javax.security.sasl.RealmCallback; 034 035import org.apache.commons.lang3.exception.ExceptionUtils; 036import org.apache.directory.api.ldap.model.constants.AuthenticationLevel; 037import org.apache.directory.api.ldap.model.entry.Attribute; 038import org.apache.directory.api.ldap.model.exception.LdapOperationException; 039import org.apache.directory.api.ldap.model.message.BindRequest; 040import org.apache.directory.api.ldap.model.message.Control; 041import org.apache.directory.api.ldap.model.message.LdapResult; 042import org.apache.directory.api.ldap.model.message.ResultCodeEnum; 043import org.apache.directory.api.ldap.model.name.Dn; 044import org.apache.directory.api.ldap.util.JndiUtils; 045import org.apache.directory.api.util.Strings; 046import org.apache.directory.server.constants.ServerDNConstants; 047import org.apache.directory.server.core.api.CoreSession; 048import org.apache.directory.server.core.api.DirectoryService; 049import org.apache.directory.server.i18n.I18n; 050import org.apache.directory.server.ldap.LdapSession; 051import org.apache.mina.core.session.IoSession; 052import org.slf4j.Logger; 053import 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 */ 063public abstract class AbstractSaslCallbackHandler implements CallbackHandler 064{ 065 /** The logger instance */ 066 private static final Logger LOG = LoggerFactory.getLogger( AbstractSaslCallbackHandler.class ); 067 068 /** An empty control array */ 069 private static final Control[] EMPTY = new Control[0]; 070 071 private String username; 072 private String realm; 073 074 /** The reference on the user ldap session */ 075 protected LdapSession ldapSession; 076 077 /** The admin core session */ 078 protected CoreSession adminSession; 079 080 /** A reference on the DirectoryService instance */ 081 protected final DirectoryService directoryService; 082 083 /** The associated BindRequest */ 084 protected final BindRequest bindRequest; 085 086 087 /** 088 * Creates a new instance of AbstractSaslCallbackHandler. 089 * 090 * @param directoryService The DirectoryService instance 091 * @param bindRequest The Bind request 092 */ 093 protected AbstractSaslCallbackHandler( DirectoryService directoryService, BindRequest 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 * @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}