001 /* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at 010 * trunk/opends/resource/legal-notices/OpenDS.LICENSE 011 * or https://OpenDS.dev.java.net/OpenDS.LICENSE. 012 * See the License for the specific language governing permissions 013 * and limitations under the License. 014 * 015 * When distributing Covered Code, include this CDDL HEADER in each 016 * file and include the License file at 017 * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable, 018 * add the following below this CDDL HEADER, with the fields enclosed 019 * by brackets "[]" replaced with your own identifying information: 020 * Portions Copyright [yyyy] [name of copyright owner] 021 * 022 * CDDL HEADER END 023 * 024 * 025 * Copyright 2006-2008 Sun Microsystems, Inc. 026 */ 027 package org.opends.server.extensions; 028 import org.opends.messages.Message; 029 030 031 032 import java.io.BufferedWriter; 033 import java.io.File; 034 import java.io.FileWriter; 035 import java.net.InetAddress; 036 import java.util.ArrayList; 037 import java.util.List; 038 039 import org.opends.server.admin.server.ConfigurationChangeListener; 040 import org.opends.server.admin.std.server.GSSAPISASLMechanismHandlerCfg; 041 import org.opends.server.admin.std.server.SASLMechanismHandlerCfg; 042 import org.opends.server.api.ClientConnection; 043 import org.opends.server.api.IdentityMapper; 044 import org.opends.server.api.SASLMechanismHandler; 045 import org.opends.server.config.ConfigException; 046 import org.opends.server.core.BindOperation; 047 import org.opends.server.core.DirectoryServer; 048 import org.opends.server.types.AuthenticationInfo; 049 import org.opends.server.types.ConfigChangeResult; 050 import org.opends.server.types.DirectoryException; 051 import org.opends.server.types.DN; 052 import org.opends.server.types.Entry; 053 import org.opends.server.types.InitializationException; 054 import org.opends.server.types.ResultCode; 055 056 import static org.opends.server.loggers.debug.DebugLogger.*; 057 import org.opends.server.loggers.debug.DebugTracer; 058 import org.opends.server.types.DebugLogLevel; 059 import static org.opends.messages.ExtensionMessages.*; 060 061 import static org.opends.server.util.ServerConstants.*; 062 import static org.opends.server.util.StaticUtils.*; 063 064 065 066 /** 067 * This class provides an implementation of a SASL mechanism that authenticates 068 * clients through Kerberos over GSSAPI. 069 */ 070 public class GSSAPISASLMechanismHandler 071 extends SASLMechanismHandler<GSSAPISASLMechanismHandlerCfg> 072 implements ConfigurationChangeListener< 073 GSSAPISASLMechanismHandlerCfg> 074 { 075 /** 076 * The tracer object for the debug logger. 077 */ 078 private static final DebugTracer TRACER = getTracer(); 079 080 // The DN of the configuration entry for this SASL mechanism handler. 081 private DN configEntryDN; 082 083 // The current configuration for this SASL mechanism handler. 084 private GSSAPISASLMechanismHandlerCfg currentConfig; 085 086 // The identity mapper that will be used to map the Kerberos principal to a 087 // directory user. 088 private IdentityMapper<?> identityMapper; 089 090 // The fully-qualified domain name for the server system. 091 private String serverFQDN; 092 093 094 095 /** 096 * Creates a new instance of this SASL mechanism handler. No initialization 097 * should be done in this method, as it should all be performed in the 098 * <CODE>initializeSASLMechanismHandler</CODE> method. 099 */ 100 public GSSAPISASLMechanismHandler() 101 { 102 super(); 103 } 104 105 106 107 /** 108 * {@inheritDoc} 109 */ 110 @Override() 111 public void initializeSASLMechanismHandler( 112 GSSAPISASLMechanismHandlerCfg configuration) 113 throws ConfigException, InitializationException 114 { 115 configuration.addGSSAPIChangeListener(this); 116 117 currentConfig = configuration; 118 configEntryDN = configuration.dn(); 119 120 121 // Get the identity mapper that should be used to find users. 122 DN identityMapperDN = configuration.getIdentityMapperDN(); 123 identityMapper = DirectoryServer.getIdentityMapper(identityMapperDN); 124 125 126 // Determine the fully-qualified hostname for this system. It may be 127 // provided, but if not, then try to determine it programmatically. 128 serverFQDN = configuration.getServerFqdn(); 129 if (serverFQDN == null) 130 { 131 try 132 { 133 serverFQDN = InetAddress.getLocalHost().getCanonicalHostName(); 134 } 135 catch (Exception e) 136 { 137 if (debugEnabled()) 138 { 139 TRACER.debugCaught(DebugLogLevel.ERROR, e); 140 } 141 142 Message message = ERR_SASLGSSAPI_CANNOT_GET_SERVER_FQDN.get( 143 String.valueOf(configEntryDN), getExceptionMessage(e)); 144 throw new InitializationException(message, e); 145 } 146 } 147 148 149 // Since we're going to be using JAAS behind the scenes, we need to have a 150 // JAAS configuration. Rather than always requiring the user to provide it, 151 // we'll write one to a temporary file that will be deleted when the JVM 152 // exits. 153 String configFileName; 154 try 155 { 156 File tempFile = File.createTempFile("login", "conf"); 157 configFileName = tempFile.getAbsolutePath(); 158 tempFile.deleteOnExit(); 159 BufferedWriter w = new BufferedWriter(new FileWriter(tempFile, false)); 160 161 w.write(getClass().getName() + " {"); 162 w.newLine(); 163 164 w.write(" com.sun.security.auth.module.Krb5LoginModule required " + 165 "storeKey=true useKeyTab=true "); 166 167 String keyTabFile = configuration.getKeytab(); 168 if (keyTabFile != null) 169 { 170 w.write("keyTab=\"" + keyTabFile + "\" "); 171 } 172 173 // FIXME -- Should we add the ability to include "debug=true"? 174 175 // FIXME -- Can we get away from hard-coding a protocol here? 176 w.write("principal=\"ldap/" + serverFQDN); 177 178 String realm = configuration.getRealm(); 179 if (realm != null) 180 { 181 w.write("@" + realm); 182 } 183 w.write("\";"); 184 185 w.newLine(); 186 187 w.write("};"); 188 w.newLine(); 189 190 w.flush(); 191 w.close(); 192 } 193 catch (Exception e) 194 { 195 if (debugEnabled()) 196 { 197 TRACER.debugCaught(DebugLogLevel.ERROR, e); 198 } 199 200 Message message = 201 ERR_SASLGSSAPI_CANNOT_CREATE_JAAS_CONFIG.get(getExceptionMessage(e)); 202 throw new InitializationException(message, e); 203 } 204 205 System.setProperty(JAAS_PROPERTY_CONFIG_FILE, configFileName); 206 System.setProperty(JAAS_PROPERTY_SUBJECT_CREDS_ONLY, "false"); 207 208 209 DirectoryServer.registerSASLMechanismHandler(SASL_MECHANISM_GSSAPI, this); 210 } 211 212 213 214 /** 215 * {@inheritDoc} 216 */ 217 @Override() 218 public void finalizeSASLMechanismHandler() 219 { 220 currentConfig.removeGSSAPIChangeListener(this); 221 DirectoryServer.deregisterSASLMechanismHandler(SASL_MECHANISM_GSSAPI); 222 } 223 224 225 226 227 /** 228 * {@inheritDoc} 229 */ 230 @Override() 231 public void processSASLBind(BindOperation bindOperation) 232 { 233 // GSSAPI binds use multiple stages, so we need to determine whether this is 234 // the first stage or a subsequent one. To do that, see if we have SASL 235 // state information in the client connection. 236 ClientConnection clientConnection = bindOperation.getClientConnection(); 237 if (clientConnection == null) 238 { 239 Message message = ERR_SASLGSSAPI_NO_CLIENT_CONNECTION.get(); 240 241 bindOperation.setAuthFailureReason(message); 242 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 243 return; 244 } 245 246 GSSAPIStateInfo stateInfo = null; 247 Object saslBindState = clientConnection.getSASLAuthStateInfo(); 248 if ((saslBindState != null) && (saslBindState instanceof GSSAPIStateInfo)) 249 { 250 stateInfo = (GSSAPIStateInfo) saslBindState; 251 } 252 else 253 { 254 try 255 { 256 stateInfo = new GSSAPIStateInfo(this, bindOperation, serverFQDN); 257 } 258 catch (InitializationException ie) 259 { 260 if (debugEnabled()) 261 { 262 TRACER.debugCaught(DebugLogLevel.ERROR, ie); 263 } 264 265 bindOperation.setAuthFailureReason(ie.getMessageObject()); 266 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 267 clientConnection.setSASLAuthStateInfo(null); 268 return; 269 } 270 } 271 272 stateInfo.setBindOperation(bindOperation); 273 stateInfo.processAuthenticationStage(); 274 275 276 if (bindOperation.getResultCode() == ResultCode.SUCCESS) 277 { 278 // The authentication was successful, so set the proper state information 279 // in the client connection and return success. 280 Entry userEntry = stateInfo.getUserEntry(); 281 AuthenticationInfo authInfo = 282 new AuthenticationInfo(userEntry, SASL_MECHANISM_GSSAPI, 283 DirectoryServer.isRootDN(userEntry.getDN())); 284 bindOperation.setAuthenticationInfo(authInfo); 285 bindOperation.setResultCode(ResultCode.SUCCESS); 286 287 // FIXME -- If we're using integrity or confidentiality, then we can't do 288 // this. 289 clientConnection.setSASLAuthStateInfo(null); 290 291 try 292 { 293 stateInfo.dispose(); 294 } 295 catch (Exception e) 296 { 297 if (debugEnabled()) 298 { 299 TRACER.debugCaught(DebugLogLevel.ERROR, e); 300 } 301 } 302 } 303 else if (bindOperation.getResultCode() == ResultCode.SASL_BIND_IN_PROGRESS) 304 { 305 // We need to store the SASL auth state with the client connection so we 306 // can resume authentication the next time around. 307 clientConnection.setSASLAuthStateInfo(stateInfo); 308 } 309 else 310 { 311 // The authentication failed. We don't want to keep the SASL state 312 // around. 313 // FIXME -- Are there other result codes that we need to check for and 314 // preserve the auth state? 315 clientConnection.setSASLAuthStateInfo(null); 316 } 317 } 318 319 320 321 /** 322 * Retrieves the user account for the user associated with the provided 323 * authorization ID. 324 * 325 * @param bindOperation The bind operation from which the provided 326 * authorization ID was derived. 327 * @param authzID The authorization ID for which to retrieve the 328 * associated user. 329 * 330 * @return The user entry for the user with the specified authorization ID, 331 * or <CODE>null</CODE> if none is identified. 332 * 333 * @throws DirectoryException If a problem occurs while searching the 334 * directory for the associated user, or if 335 * multiple matching entries are found. 336 */ 337 public Entry getUserForAuthzID(BindOperation bindOperation, String authzID) 338 throws DirectoryException 339 { 340 return identityMapper.getEntryForID(authzID); 341 } 342 343 344 345 /** 346 * {@inheritDoc} 347 */ 348 @Override() 349 public boolean isPasswordBased(String mechanism) 350 { 351 // This is not a password-based mechanism. 352 return false; 353 } 354 355 356 357 /** 358 * {@inheritDoc} 359 */ 360 @Override() 361 public boolean isSecure(String mechanism) 362 { 363 // This may be considered a secure mechanism. 364 return true; 365 } 366 367 368 369 /** 370 * {@inheritDoc} 371 */ 372 @Override() 373 public boolean isConfigurationAcceptable( 374 SASLMechanismHandlerCfg configuration, 375 List<Message> unacceptableReasons) 376 { 377 GSSAPISASLMechanismHandlerCfg config = 378 (GSSAPISASLMechanismHandlerCfg) configuration; 379 return isConfigurationChangeAcceptable(config, unacceptableReasons); 380 } 381 382 383 384 /** 385 * {@inheritDoc} 386 */ 387 public boolean isConfigurationChangeAcceptable( 388 GSSAPISASLMechanismHandlerCfg configuration, 389 List<Message> unacceptableReasons) 390 { 391 return true; 392 } 393 394 395 396 /** 397 * {@inheritDoc} 398 */ 399 public ConfigChangeResult applyConfigurationChange( 400 GSSAPISASLMechanismHandlerCfg configuration) 401 { 402 ResultCode resultCode = ResultCode.SUCCESS; 403 boolean adminActionRequired = false; 404 ArrayList<Message> messages = new ArrayList<Message>(); 405 406 407 // Get the identity mapper that should be used to find users. 408 DN identityMapperDN = configuration.getIdentityMapperDN(); 409 IdentityMapper<?> newIdentityMapper = 410 DirectoryServer.getIdentityMapper(identityMapperDN); 411 412 413 // Determine the fully-qualified hostname for this system. It may be 414 // provided, but if not, then try to determine it programmatically. 415 String newFQDN = configuration.getServerFqdn(); 416 if (newFQDN == null) 417 { 418 try 419 { 420 newFQDN = InetAddress.getLocalHost().getCanonicalHostName(); 421 } 422 catch (Exception e) 423 { 424 if (debugEnabled()) 425 { 426 TRACER.debugCaught(DebugLogLevel.ERROR, e); 427 } 428 429 if (resultCode == ResultCode.SUCCESS) 430 { 431 resultCode = DirectoryServer.getServerErrorResultCode(); 432 } 433 434 435 messages.add(ERR_SASLGSSAPI_CANNOT_GET_SERVER_FQDN.get( 436 String.valueOf(configEntryDN), 437 getExceptionMessage(e))); 438 } 439 } 440 441 442 if (resultCode == ResultCode.SUCCESS) 443 { 444 String configFileName; 445 try 446 { 447 File tempFile = File.createTempFile("login", "conf"); 448 configFileName = tempFile.getAbsolutePath(); 449 tempFile.deleteOnExit(); 450 BufferedWriter w = new BufferedWriter(new FileWriter(tempFile, false)); 451 452 w.write(getClass().getName() + " {"); 453 w.newLine(); 454 455 w.write(" com.sun.security.auth.module.Krb5LoginModule required " + 456 "storeKey=true useKeyTab=true "); 457 458 String keyTabFile = configuration.getKeytab(); 459 if (keyTabFile != null) 460 { 461 w.write("keyTab=\"" + keyTabFile + "\" "); 462 } 463 464 // FIXME -- Should we add the ability to include "debug=true"? 465 466 // FIXME -- Can we get away from hard-coding a protocol here? 467 w.write("principal=\"ldap/" + serverFQDN); 468 469 String realm = configuration.getRealm(); 470 if (realm != null) 471 { 472 w.write("@" + realm); 473 } 474 w.write("\";"); 475 476 w.newLine(); 477 478 w.write("};"); 479 w.newLine(); 480 481 w.flush(); 482 w.close(); 483 } 484 catch (Exception e) 485 { 486 if (debugEnabled()) 487 { 488 TRACER.debugCaught(DebugLogLevel.ERROR, e); 489 } 490 491 resultCode = DirectoryServer.getServerErrorResultCode(); 492 493 messages.add(ERR_SASLGSSAPI_CANNOT_CREATE_JAAS_CONFIG.get( 494 getExceptionMessage(e))); 495 496 return new ConfigChangeResult(resultCode, adminActionRequired, messages); 497 } 498 499 System.setProperty(JAAS_PROPERTY_CONFIG_FILE, configFileName); 500 501 identityMapper = newIdentityMapper; 502 serverFQDN = newFQDN; 503 currentConfig = configuration; 504 } 505 506 507 return new ConfigChangeResult(resultCode, adminActionRequired, messages); 508 } 509 } 510