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