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