001/*
002 * Copyright 2011-2014 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2011-2014 UnboundID Corp.
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021package com.unboundid.util;
022
023
024
025import java.lang.reflect.InvocationTargetException;
026import java.lang.reflect.Method;
027import java.util.ArrayList;
028import java.util.Collections;
029import java.util.HashMap;
030import java.util.List;
031import java.util.Map;
032import java.util.TreeMap;
033
034import com.unboundid.ldap.sdk.ANONYMOUSBindRequest;
035import com.unboundid.ldap.sdk.Control;
036import com.unboundid.ldap.sdk.CRAMMD5BindRequest;
037import com.unboundid.ldap.sdk.DIGESTMD5BindRequest;
038import com.unboundid.ldap.sdk.DIGESTMD5BindRequestProperties;
039import com.unboundid.ldap.sdk.EXTERNALBindRequest;
040import com.unboundid.ldap.sdk.GSSAPIBindRequest;
041import com.unboundid.ldap.sdk.GSSAPIBindRequestProperties;
042import com.unboundid.ldap.sdk.LDAPException;
043import com.unboundid.ldap.sdk.PLAINBindRequest;
044import com.unboundid.ldap.sdk.ResultCode;
045import com.unboundid.ldap.sdk.SASLBindRequest;
046import com.unboundid.ldap.sdk.SASLQualityOfProtection;
047
048import static com.unboundid.util.StaticUtils.*;
049import static com.unboundid.util.UtilityMessages.*;
050
051
052
053/**
054 * This class provides a utility that may be used to help process SASL bind
055 * operations using the LDAP SDK.
056 */
057@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
058public final class SASLUtils
059{
060  /**
061   * The name of the SASL option that specifies the authentication ID.  It may
062   * be used in conjunction with the CRAM-MD5, DIGEST-MD5, GSSAPI, and PLAIN
063   * mechanisms.
064   */
065  public static final String SASL_OPTION_AUTH_ID = "authID";
066
067
068
069  /**
070   * The name of the SASL option that specifies the authorization ID.  It may
071   * be used in conjunction with the DIGEST-MD5, GSSAPI, and PLAIN mechanisms.
072   */
073  public static final String SASL_OPTION_AUTHZ_ID = "authzID";
074
075
076
077  /**
078   * The name of the SASL option that specifies the path to the JAAS config
079   * file.  It may be used in conjunction with the GSSAPI mechanism.
080   */
081  public static final String SASL_OPTION_CONFIG_FILE = "configFile";
082
083
084
085  /**
086   * The name of the SASL option that indicates whether debugging should be
087   * enabled.  It may be used in conjunction with the GSSAPI mechanism.
088   */
089  public static final String SASL_OPTION_DEBUG = "debug";
090
091
092
093  /**
094   * The name of the SASL option that specifies the KDC address.  It may be used
095   * in conjunction with the GSSAPI mechanism.
096   */
097  public static final String SASL_OPTION_KDC_ADDRESS = "kdcAddress";
098
099
100
101
102  /**
103   * The name of the SASL option that specifies the desired SASL mechanism to
104   * use to authenticate to the server.
105   */
106  public static final String SASL_OPTION_MECHANISM = "mech";
107
108
109
110  /**
111   * The name of the SASL option that specifies the GSSAPI service principal
112   * protocol.  It may be used in conjunction with the GSSAPI mechanism.
113   */
114  public static final String SASL_OPTION_PROTOCOL = "protocol";
115
116
117
118  /**
119   * The name of the SASL option that specifies the quality of protection that
120   * should be used for communication that occurs after the authentication has
121   * completed.
122   */
123  public static final String SASL_OPTION_QOP = "qop";
124
125
126
127  /**
128   * The name of the SASL option that specifies the realm name.  It may be used
129   * in conjunction with the DIGEST-MD5 and GSSAPI mechanisms.
130   */
131  public static final String SASL_OPTION_REALM = "realm";
132
133
134
135  /**
136   * The name of the SASL option that indicates whether to require an existing
137   * Kerberos session from the ticket cache.  It may be used in conjunction with
138   * the GSSAPI mechanism.
139   */
140  public static final String SASL_OPTION_REQUIRE_CACHE = "requireCache";
141
142
143
144  /**
145   * The name of the SASL option that indicates whether to attempt to renew the
146   * Kerberos TGT for an existing session.  It may be used in conjunction with
147   * the GSSAPI mechanism.
148   */
149  public static final String SASL_OPTION_RENEW_TGT = "renewTGT";
150
151
152
153  /**
154   * The name of the SASL option that specifies the path to the Kerberos ticket
155   * cache to use.  It may be used in conjunction with the GSSAPI mechanism.
156   */
157  public static final String SASL_OPTION_TICKET_CACHE_PATH = "ticketCache";
158
159
160
161  /**
162   * The name of the SASL option that specifies the trace string.  It may be
163   * used in conjunction with the ANONYMOUS mechanism.
164   */
165  public static final String SASL_OPTION_TRACE = "trace";
166
167
168
169  /**
170   * The name of the SASL option that specifies whether to use a Kerberos ticket
171   * cache.  It may be used in conjunction with the GSSAPI mechanism.
172   */
173  public static final String SASL_OPTION_USE_TICKET_CACHE = "useTicketCache";
174
175
176
177  /**
178   * A map with information about all supported SASL mechanisms, mapped from
179   * lowercase mechanism name to an object with information about that
180   * mechanism.
181   */
182  private static final Map<String,SASLMechanismInfo> SASL_MECHANISMS;
183
184
185
186  static
187  {
188    final TreeMap<String,SASLMechanismInfo> m =
189         new TreeMap<String,SASLMechanismInfo>();
190
191    m.put(toLowerCase(ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME),
192         new SASLMechanismInfo(ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME,
193              INFO_SASL_ANONYMOUS_DESCRIPTION.get(), false, false,
194              new SASLOption(SASL_OPTION_TRACE,
195                   INFO_SASL_ANONYMOUS_OPTION_TRACE.get(), false, false)));
196
197    m.put(toLowerCase(CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME),
198         new SASLMechanismInfo(CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME,
199              INFO_SASL_CRAM_MD5_DESCRIPTION.get(), true, true,
200              new SASLOption(SASL_OPTION_AUTH_ID,
201                   INFO_SASL_CRAM_MD5_OPTION_AUTH_ID.get(), true, false)));
202
203    m.put(toLowerCase(DIGESTMD5BindRequest.DIGESTMD5_MECHANISM_NAME),
204         new SASLMechanismInfo(DIGESTMD5BindRequest.DIGESTMD5_MECHANISM_NAME,
205              INFO_SASL_DIGEST_MD5_DESCRIPTION.get(), true, true,
206              new SASLOption(SASL_OPTION_AUTH_ID,
207                   INFO_SASL_DIGEST_MD5_OPTION_AUTH_ID.get(), true, false),
208              new SASLOption(SASL_OPTION_AUTHZ_ID,
209                   INFO_SASL_DIGEST_MD5_OPTION_AUTHZ_ID.get(), false, false),
210              new SASLOption(SASL_OPTION_REALM,
211                   INFO_SASL_DIGEST_MD5_OPTION_REALM.get(), false, false),
212              new SASLOption(SASL_OPTION_QOP,
213                   INFO_SASL_DIGEST_MD5_OPTION_QOP.get(), false, false)));
214
215    m.put(toLowerCase(EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME),
216         new SASLMechanismInfo(EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME,
217              INFO_SASL_EXTERNAL_DESCRIPTION.get(), false, false));
218
219    m.put(toLowerCase(GSSAPIBindRequest.GSSAPI_MECHANISM_NAME),
220         new SASLMechanismInfo(GSSAPIBindRequest.GSSAPI_MECHANISM_NAME,
221              INFO_SASL_GSSAPI_DESCRIPTION.get(), true, false,
222              new SASLOption(SASL_OPTION_AUTH_ID,
223                   INFO_SASL_GSSAPI_OPTION_AUTH_ID.get(), true, false),
224              new SASLOption(SASL_OPTION_AUTHZ_ID,
225                   INFO_SASL_GSSAPI_OPTION_AUTHZ_ID.get(), false, false),
226              new SASLOption(SASL_OPTION_CONFIG_FILE,
227                   INFO_SASL_GSSAPI_OPTION_CONFIG_FILE.get(), false, false),
228              new SASLOption(SASL_OPTION_DEBUG,
229                   INFO_SASL_GSSAPI_OPTION_DEBUG.get(), false, false),
230              new SASLOption(SASL_OPTION_KDC_ADDRESS,
231                   INFO_SASL_GSSAPI_OPTION_KDC_ADDRESS.get(), false, false),
232              new SASLOption(SASL_OPTION_PROTOCOL,
233                   INFO_SASL_GSSAPI_OPTION_PROTOCOL.get(), false, false),
234              new SASLOption(SASL_OPTION_REALM,
235                   INFO_SASL_GSSAPI_OPTION_REALM.get(), false, false),
236              new SASLOption(SASL_OPTION_QOP,
237                   INFO_SASL_GSSAPI_OPTION_QOP.get(), false, false),
238              new SASLOption(SASL_OPTION_RENEW_TGT,
239                   INFO_SASL_GSSAPI_OPTION_RENEW_TGT.get(), false, false),
240              new SASLOption(SASL_OPTION_REQUIRE_CACHE,
241                   INFO_SASL_GSSAPI_OPTION_REQUIRE_TICKET_CACHE.get(), false,
242                   false),
243              new SASLOption(SASL_OPTION_TICKET_CACHE_PATH,
244                   INFO_SASL_GSSAPI_OPTION_TICKET_CACHE.get(), false, false),
245              new SASLOption(SASL_OPTION_USE_TICKET_CACHE,
246                   INFO_SASL_GSSAPI_OPTION_USE_TICKET_CACHE.get(), false,
247                   false)));
248
249    m.put(toLowerCase(PLAINBindRequest.PLAIN_MECHANISM_NAME),
250         new SASLMechanismInfo(PLAINBindRequest.PLAIN_MECHANISM_NAME,
251              INFO_SASL_PLAIN_DESCRIPTION.get(), true, true,
252              new SASLOption(SASL_OPTION_AUTH_ID,
253                   INFO_SASL_PLAIN_OPTION_AUTH_ID.get(), true, false),
254              new SASLOption(SASL_OPTION_AUTHZ_ID,
255                   INFO_SASL_PLAIN_OPTION_AUTHZ_ID.get(), false, false)));
256
257
258    // If Commercial Edition classes are available, then register support for
259    // any additional SASL mechanisms that it provides.
260    try
261    {
262      final Class<?> c =
263           Class.forName("com.unboundid.ldap.sdk.unboundidds.SASLHelper");
264      final Method addCESASLInfoMethod =
265           c.getMethod("addCESASLInfo", Map.class);
266      addCESASLInfoMethod.invoke(null, m);
267    }
268    catch (final Exception e)
269    {
270        // This is fine.  It simply means that the Commercial Edition classes
271        // are not available.
272      Debug.debugException(e);
273    }
274
275    SASL_MECHANISMS = Collections.unmodifiableMap(m);
276  }
277
278
279
280  /**
281   * Prevent this utility class from being instantiated.
282   */
283  private SASLUtils()
284  {
285    // No implementation required.
286  }
287
288
289
290  /**
291   * Retrieves information about the SASL mechanisms supported for use by this
292   * class.
293   *
294   * @return  Information about the SASL mechanisms supported for use by this
295   *          class.
296   */
297  public static List<SASLMechanismInfo> getSupportedSASLMechanisms()
298  {
299    return Collections.unmodifiableList(new ArrayList<SASLMechanismInfo>(
300         SASL_MECHANISMS.values()));
301  }
302
303
304
305  /**
306   * Retrieves information about the specified SASL mechanism.
307   *
308   * @param  mechanism  The name of the SASL mechanism for which to retrieve
309   *                    information.  It will not be treated in a case-sensitive
310   *                    manner.
311   *
312   * @return  Information about the requested SASL mechanism, or {@code null} if
313   *          no information about the specified mechanism is available.
314   */
315  public static SASLMechanismInfo getSASLMechanismInfo(final String mechanism)
316  {
317    return SASL_MECHANISMS.get(toLowerCase(mechanism));
318  }
319
320
321
322  /**
323   * Creates a new SASL bind request using the provided information.
324   *
325   * @param  bindDN     The bind DN to use for the SASL bind request.  For most
326   *                    SASL mechanisms, this should be {@code null}, since the
327   *                    identity of the target user should be specified in some
328   *                    other way (e.g., via an "authID" SASL option).
329   * @param  password   The password to use for the SASL bind request.  It may
330   *                    be {@code null} if no password is required for the
331   *                    desired SASL mechanism.
332   * @param  mechanism  The name of the SASL mechanism to use.  It may be
333   *                    {@code null} if the provided set of options contains a
334   *                    "mech" option to specify the desired SASL option.
335   * @param  options    The set of SASL options to use when creating the bind
336   *                    request, in the form "name=value".  It may be
337   *                    {@code null} or empty if no SASL options are needed and
338   *                    a value was provided for the {@code mechanism} argument.
339   *                    If the set of SASL options includes a "mech" option,
340   *                    then the {@code mechanism} argument must be {@code null}
341   *                    or have a value that matches the value of the "mech"
342   *                    SASL option.
343   *
344   * @return  The SASL bind request created using the provided information.
345   *
346   * @throws  LDAPException  If a problem is encountered while trying to create
347   *                         the SASL bind request.
348   */
349  public static SASLBindRequest createBindRequest(final String bindDN,
350                                                  final String password,
351                                                  final String mechanism,
352                                                  final String... options)
353         throws LDAPException
354  {
355    return createBindRequest(bindDN,
356         (password == null ? null : getBytes(password)), mechanism,
357         StaticUtils.toList(options));
358  }
359
360
361
362  /**
363   * Creates a new SASL bind request using the provided information.
364   *
365   * @param  bindDN     The bind DN to use for the SASL bind request.  For most
366   *                    SASL mechanisms, this should be {@code null}, since the
367   *                    identity of the target user should be specified in some
368   *                    other way (e.g., via an "authID" SASL option).
369   * @param  password   The password to use for the SASL bind request.  It may
370   *                    be {@code null} if no password is required for the
371   *                    desired SASL mechanism.
372   * @param  mechanism  The name of the SASL mechanism to use.  It may be
373   *                    {@code null} if the provided set of options contains a
374   *                    "mech" option to specify the desired SASL option.
375   * @param  options    The set of SASL options to use when creating the bind
376   *                    request, in the form "name=value".  It may be
377   *                    {@code null} or empty if no SASL options are needed and
378   *                    a value was provided for the {@code mechanism} argument.
379   *                    If the set of SASL options includes a "mech" option,
380   *                    then the {@code mechanism} argument must be {@code null}
381   *                    or have a value that matches the value of the "mech"
382   *                    SASL option.
383   * @param  controls   The set of controls to include in the request.
384   *
385   * @return  The SASL bind request created using the provided information.
386   *
387   * @throws  LDAPException  If a problem is encountered while trying to create
388   *                         the SASL bind request.
389   */
390  public static SASLBindRequest createBindRequest(final String bindDN,
391                                                  final String password,
392                                                  final String mechanism,
393                                                  final List<String> options,
394                                                  final Control... controls)
395         throws LDAPException
396  {
397    return createBindRequest(bindDN,
398         (password == null ? null : getBytes(password)), mechanism, options,
399         controls);
400  }
401
402
403
404  /**
405   * Creates a new SASL bind request using the provided information.
406   *
407   * @param  bindDN     The bind DN to use for the SASL bind request.  For most
408   *                    SASL mechanisms, this should be {@code null}, since the
409   *                    identity of the target user should be specified in some
410   *                    other way (e.g., via an "authID" SASL option).
411   * @param  password   The password to use for the SASL bind request.  It may
412   *                    be {@code null} if no password is required for the
413   *                    desired SASL mechanism.
414   * @param  mechanism  The name of the SASL mechanism to use.  It may be
415   *                    {@code null} if the provided set of options contains a
416   *                    "mech" option to specify the desired SASL option.
417   * @param  options    The set of SASL options to use when creating the bind
418   *                    request, in the form "name=value".  It may be
419   *                    {@code null} or empty if no SASL options are needed and
420   *                    a value was provided for the {@code mechanism} argument.
421   *                    If the set of SASL options includes a "mech" option,
422   *                    then the {@code mechanism} argument must be {@code null}
423   *                    or have a value that matches the value of the "mech"
424   *                    SASL option.
425   *
426   * @return  The SASL bind request created using the provided information.
427   *
428   * @throws  LDAPException  If a problem is encountered while trying to create
429   *                         the SASL bind request.
430   */
431  public static SASLBindRequest createBindRequest(final String bindDN,
432                                                  final byte[] password,
433                                                  final String mechanism,
434                                                  final String... options)
435         throws LDAPException
436  {
437    return createBindRequest(bindDN, password, mechanism,
438         StaticUtils.toList(options));
439  }
440
441
442
443  /**
444   * Creates a new SASL bind request using the provided information.
445   *
446   * @param  bindDN     The bind DN to use for the SASL bind request.  For most
447   *                    SASL mechanisms, this should be {@code null}, since the
448   *                    identity of the target user should be specified in some
449   *                    other way (e.g., via an "authID" SASL option).
450   * @param  password   The password to use for the SASL bind request.  It may
451   *                    be {@code null} if no password is required for the
452   *                    desired SASL mechanism.
453   * @param  mechanism  The name of the SASL mechanism to use.  It may be
454   *                    {@code null} if the provided set of options contains a
455   *                    "mech" option to specify the desired SASL option.
456   * @param  options    The set of SASL options to use when creating the bind
457   *                    request, in the form "name=value".  It may be
458   *                    {@code null} or empty if no SASL options are needed and
459   *                    a value was provided for the {@code mechanism} argument.
460   *                    If the set of SASL options includes a "mech" option,
461   *                    then the {@code mechanism} argument must be {@code null}
462   *                    or have a value that matches the value of the "mech"
463   *                    SASL option.
464   * @param  controls   The set of controls to include in the request.
465   *
466   * @return  The SASL bind request created using the provided information.
467   *
468   * @throws  LDAPException  If a problem is encountered while trying to create
469   *                         the SASL bind request.
470   */
471  public static SASLBindRequest createBindRequest(final String bindDN,
472                                                  final byte[] password,
473                                                  final String mechanism,
474                                                  final List<String> options,
475                                                  final Control... controls)
476         throws LDAPException
477  {
478    // Parse the provided set of options to ensure that they are properly
479    // formatted in name-value form, and extract the SASL mechanism.
480    final String mech;
481    final Map<String,String> optionsMap = parseOptions(options);
482    final String mechOption =
483         optionsMap.remove(toLowerCase(SASL_OPTION_MECHANISM));
484    if (mechOption != null)
485    {
486      mech = mechOption;
487      if ((mechanism != null) && (! mech.equalsIgnoreCase(mechanism)))
488      {
489        throw new LDAPException(ResultCode.PARAM_ERROR,
490             ERR_SASL_OPTION_MECH_CONFLICT.get(mechanism, mech));
491      }
492    }
493    else
494    {
495      mech = mechanism;
496    }
497
498    if (mech == null)
499    {
500      throw new LDAPException(ResultCode.PARAM_ERROR,
501           ERR_SASL_OPTION_NO_MECH.get());
502    }
503
504    if (mech.equalsIgnoreCase(ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME))
505    {
506      return createANONYMOUSBindRequest(password, optionsMap, controls);
507    }
508    else if (mech.equalsIgnoreCase(CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME))
509    {
510      return createCRAMMD5BindRequest(password, optionsMap, controls);
511    }
512    else if (mech.equalsIgnoreCase(
513                  DIGESTMD5BindRequest.DIGESTMD5_MECHANISM_NAME))
514    {
515      return createDIGESTMD5BindRequest(password, optionsMap, controls);
516    }
517    else if (mech.equalsIgnoreCase(EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME))
518    {
519      return createEXTERNALBindRequest(password, optionsMap, controls);
520    }
521    else if (mech.equalsIgnoreCase(GSSAPIBindRequest.GSSAPI_MECHANISM_NAME))
522    {
523      return createGSSAPIBindRequest(password, optionsMap, controls);
524    }
525    else if (mech.equalsIgnoreCase(PLAINBindRequest.PLAIN_MECHANISM_NAME))
526    {
527      return createPLAINBindRequest(password, optionsMap, controls);
528    }
529    else
530    {
531      // If Commercial Edition classes are available, then see if the
532      // authentication attempt is for one of the Commercial Edition mechanisms.
533      try
534      {
535        final Class<?> c =
536             Class.forName("com.unboundid.ldap.sdk.unboundidds.SASLHelper");
537        final Method createBindRequestMethod = c.getMethod("createBindRequest",
538             String.class, StaticUtils.NO_BYTES.getClass(), String.class,
539             Map.class, StaticUtils.NO_CONTROLS.getClass());
540        final Object bindRequestObject = createBindRequestMethod.invoke(null,
541             bindDN, password, mech, optionsMap, controls);
542        if (bindRequestObject != null)
543        {
544          return (SASLBindRequest) bindRequestObject;
545        }
546      }
547      catch (final Exception e)
548      {
549        Debug.debugException(e);
550
551        // This may mean that there was a problem with the provided arguments.
552        // If it's an InvocationTargetException that wraps an LDAPException,
553        // then throw that LDAPException.
554        if (e instanceof InvocationTargetException)
555        {
556          final InvocationTargetException ite = (InvocationTargetException) e;
557          final Throwable t = ite.getTargetException();
558          if (t instanceof LDAPException)
559          {
560            throw (LDAPException) t;
561          }
562        }
563      }
564
565      throw new LDAPException(ResultCode.PARAM_ERROR,
566           ERR_SASL_OPTION_UNSUPPORTED_MECH.get(mech));
567    }
568  }
569
570
571
572  /**
573   * Creates a SASL ANONYMOUS bind request using the provided set of options.
574   *
575   * @param  password  The password to use for the bind request.
576   * @param  options   The set of SASL options for the bind request.
577   * @param  controls  The set of controls to include in the request.
578   *
579   * @return  The SASL ANONYMOUS bind request that was created.
580   *
581   * @throws  LDAPException  If a problem is encountered while trying to create
582   *                         the SASL bind request.
583   */
584  private static ANONYMOUSBindRequest createANONYMOUSBindRequest(
585                                           final byte[] password,
586                                           final Map<String,String> options,
587                                           final Control[] controls)
588          throws LDAPException
589  {
590    if (password != null)
591    {
592      throw new LDAPException(ResultCode.PARAM_ERROR,
593           ERR_SASL_OPTION_MECH_DOESNT_ACCEPT_PASSWORD.get(
594                ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME));
595    }
596
597
598    // The trace option is optional.
599    final String trace = options.remove(toLowerCase(SASL_OPTION_TRACE));
600
601    // Ensure no unsupported options were provided.
602    ensureNoUnsupportedOptions(options,
603         ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME);
604
605    return new ANONYMOUSBindRequest(trace, controls);
606  }
607
608
609
610  /**
611   * Creates a SASL CRAM-MD5 bind request using the provided password and set of
612   * options.
613   *
614   * @param  password  The password to use for the bind request.
615   * @param  options   The set of SASL options for the bind request.
616   * @param  controls  The set of controls to include in the request.
617   *
618   * @return  The SASL CRAM-MD5 bind request that was created.
619   *
620   * @throws  LDAPException  If a problem is encountered while trying to create
621   *                         the SASL bind request.
622   */
623  private static CRAMMD5BindRequest createCRAMMD5BindRequest(
624                                         final byte[] password,
625                                         final Map<String,String> options,
626                                         final Control[] controls)
627          throws LDAPException
628  {
629    if (password == null)
630    {
631      throw new LDAPException(ResultCode.PARAM_ERROR,
632           ERR_SASL_OPTION_MECH_REQUIRES_PASSWORD.get(
633                CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME));
634    }
635
636
637    // The authID option is required.
638    final String authID = options.remove(toLowerCase(SASL_OPTION_AUTH_ID));
639    if (authID == null)
640    {
641      throw new LDAPException(ResultCode.PARAM_ERROR,
642           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_AUTH_ID,
643                CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME));
644    }
645
646
647    // Ensure no unsupported options were provided.
648    ensureNoUnsupportedOptions(options,
649         CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME);
650
651    return new CRAMMD5BindRequest(authID, password, controls);
652  }
653
654
655
656  /**
657   * Creates a SASL DIGEST-MD5 bind request using the provided password and set
658   * of options.
659   *
660   * @param  password  The password to use for the bind request.
661   * @param  options   The set of SASL options for the bind request.
662   * @param  controls  The set of controls to include in the request.
663   *
664   * @return  The SASL DIGEST-MD5 bind request that was created.
665   *
666   * @throws  LDAPException  If a problem is encountered while trying to create
667   *                         the SASL bind request.
668   */
669  private static DIGESTMD5BindRequest createDIGESTMD5BindRequest(
670                                           final byte[] password,
671                                           final Map<String,String> options,
672                                           final Control[] controls)
673          throws LDAPException
674  {
675    if (password == null)
676    {
677      throw new LDAPException(ResultCode.PARAM_ERROR,
678           ERR_SASL_OPTION_MECH_REQUIRES_PASSWORD.get(
679                DIGESTMD5BindRequest.DIGESTMD5_MECHANISM_NAME));
680    }
681
682    // The authID option is required.
683    final String authID = options.remove(toLowerCase(SASL_OPTION_AUTH_ID));
684    if (authID == null)
685    {
686      throw new LDAPException(ResultCode.PARAM_ERROR,
687           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_AUTH_ID,
688                CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME));
689    }
690
691    final DIGESTMD5BindRequestProperties properties =
692         new DIGESTMD5BindRequestProperties(authID, password);
693
694    // The authzID option is optional.
695    properties.setAuthorizationID(
696         options.remove(toLowerCase(SASL_OPTION_AUTHZ_ID)));
697
698    // The realm option is optional.
699    properties.setRealm(options.remove(toLowerCase(SASL_OPTION_REALM)));
700
701    // The QoP option is optional, and may contain multiple values that need to
702    // be parsed.
703    final String qopString = options.remove(toLowerCase(SASL_OPTION_QOP));
704    if (qopString != null)
705    {
706      properties.setAllowedQoP(
707           SASLQualityOfProtection.decodeQoPList(qopString));
708    }
709
710    // Ensure no unsupported options were provided.
711    ensureNoUnsupportedOptions(options,
712         DIGESTMD5BindRequest.DIGESTMD5_MECHANISM_NAME);
713
714    return new DIGESTMD5BindRequest(properties, controls);
715  }
716
717
718
719  /**
720   * Creates a SASL EXTERNAL bind request using the provided set of options.
721   *
722   * @param  password  The password to use for the bind request.
723   * @param  options   The set of SASL options for the bind request.
724   * @param  controls  The set of controls to include in the request.
725   *
726   * @return  The SASL EXTERNAL bind request that was created.
727   *
728   * @throws  LDAPException  If a problem is encountered while trying to create
729   *                         the SASL bind request.
730   */
731  private static EXTERNALBindRequest createEXTERNALBindRequest(
732                                          final byte[] password,
733                                          final Map<String,String> options,
734                                          final Control[] controls)
735          throws LDAPException
736  {
737    if (password != null)
738    {
739      throw new LDAPException(ResultCode.PARAM_ERROR,
740           ERR_SASL_OPTION_MECH_DOESNT_ACCEPT_PASSWORD.get(
741                EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME));
742    }
743
744    // Ensure no unsupported options were provided.
745    ensureNoUnsupportedOptions(options,
746         EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME);
747
748    return new EXTERNALBindRequest(controls);
749  }
750
751
752
753  /**
754   * Creates a SASL GSSAPI bind request using the provided password and set of
755   * options.
756   *
757   * @param  password  The password to use for the bind request.
758   * @param  options   The set of SASL options for the bind request.
759   * @param  controls  The set of controls to include in the request.
760   *
761   * @return  The SASL GSSAPI bind request that was created.
762   *
763   * @throws  LDAPException  If a problem is encountered while trying to create
764   *                         the SASL bind request.
765   */
766  private static GSSAPIBindRequest createGSSAPIBindRequest(
767                                        final byte[] password,
768                                        final Map<String,String> options,
769                                        final Control[] controls)
770          throws LDAPException
771  {
772    // The authID option is required.
773    final String authID = options.remove(toLowerCase(SASL_OPTION_AUTH_ID));
774    if (authID == null)
775    {
776      throw new LDAPException(ResultCode.PARAM_ERROR,
777           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_AUTH_ID,
778                GSSAPIBindRequest.GSSAPI_MECHANISM_NAME));
779    }
780    final GSSAPIBindRequestProperties gssapiProperties =
781         new GSSAPIBindRequestProperties(authID, password);
782
783    // The authzID option is optional.
784    gssapiProperties.setAuthorizationID(
785         options.remove(toLowerCase(SASL_OPTION_AUTHZ_ID)));
786
787    // The configFile option is optional.
788    gssapiProperties.setConfigFilePath(options.remove(toLowerCase(
789         SASL_OPTION_CONFIG_FILE)));
790
791    // The debug option is optional.
792    gssapiProperties.setEnableGSSAPIDebugging(getBooleanValue(options,
793         SASL_OPTION_DEBUG, false));
794
795    // The kdcAddress option is optional.
796    gssapiProperties.setKDCAddress(options.remove(
797         toLowerCase(SASL_OPTION_KDC_ADDRESS)));
798
799    // The protocol option is optional.
800    final String protocol = options.remove(toLowerCase(SASL_OPTION_PROTOCOL));
801    if (protocol != null)
802    {
803      gssapiProperties.setServicePrincipalProtocol(protocol);
804    }
805
806    // The realm option is optional.
807    gssapiProperties.setRealm(options.remove(toLowerCase(SASL_OPTION_REALM)));
808
809    // The QoP option is optional, and may contain multiple values that need to
810    // be parsed.
811    final String qopString = options.remove(toLowerCase(SASL_OPTION_QOP));
812    if (qopString != null)
813    {
814      gssapiProperties.setAllowedQoP(
815           SASLQualityOfProtection.decodeQoPList(qopString));
816    }
817
818    // The renewTGT option is optional.
819    gssapiProperties.setRenewTGT(getBooleanValue(options, SASL_OPTION_RENEW_TGT,
820         false));
821
822    // The requireCache option is optional.
823    gssapiProperties.setRequireCachedCredentials(getBooleanValue(options,
824         SASL_OPTION_REQUIRE_CACHE, false));
825
826    // The ticketCache option is optional.
827    gssapiProperties.setTicketCachePath(options.remove(toLowerCase(
828         SASL_OPTION_TICKET_CACHE_PATH)));
829
830    // The useTicketCache option is optional.
831    gssapiProperties.setUseTicketCache(getBooleanValue(options,
832         SASL_OPTION_USE_TICKET_CACHE, true));
833
834    // Ensure no unsupported options were provided.
835    ensureNoUnsupportedOptions(options,
836         GSSAPIBindRequest.GSSAPI_MECHANISM_NAME);
837
838    // A password must have been provided unless useTicketCache=true and
839    // requireTicketCache=true.
840    if (password == null)
841    {
842      if (! (gssapiProperties.useTicketCache() &&
843           gssapiProperties.requireCachedCredentials()))
844      {
845        throw new LDAPException(ResultCode.PARAM_ERROR,
846             ERR_SASL_OPTION_GSSAPI_PASSWORD_REQUIRED.get());
847      }
848    }
849
850    return new GSSAPIBindRequest(gssapiProperties, controls);
851  }
852
853
854
855  /**
856   * Creates a SASL PLAIN bind request using the provided password and set of
857   * options.
858   *
859   * @param  password  The password to use for the bind request.
860   * @param  options   The set of SASL options for the bind request.
861   * @param  controls  The set of controls to include in the request.
862   *
863   * @return  The SASL PLAIN bind request that was created.
864   *
865   * @throws  LDAPException  If a problem is encountered while trying to create
866   *                         the SASL bind request.
867   */
868  private static PLAINBindRequest createPLAINBindRequest(
869                                        final byte[] password,
870                                        final Map<String,String> options,
871                                        final Control[] controls)
872          throws LDAPException
873  {
874    if (password == null)
875    {
876      throw new LDAPException(ResultCode.PARAM_ERROR,
877           ERR_SASL_OPTION_MECH_REQUIRES_PASSWORD.get(
878                PLAINBindRequest.PLAIN_MECHANISM_NAME));
879    }
880
881    // The authID option is required.
882    final String authID = options.remove(toLowerCase(SASL_OPTION_AUTH_ID));
883    if (authID == null)
884    {
885      throw new LDAPException(ResultCode.PARAM_ERROR,
886           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_AUTH_ID,
887                PLAINBindRequest.PLAIN_MECHANISM_NAME));
888    }
889
890    // The authzID option is optional.
891    final String authzID = options.remove(toLowerCase(SASL_OPTION_AUTHZ_ID));
892
893    // Ensure no unsupported options were provided.
894    ensureNoUnsupportedOptions(options,
895         PLAINBindRequest.PLAIN_MECHANISM_NAME);
896
897    return new PLAINBindRequest(authID, authzID, password, controls);
898  }
899
900
901
902  /**
903   * Parses the provided list of SASL options.
904   *
905   * @param  options  The list of options to be parsed.
906   *
907   * @return  A map with the parsed set of options.
908   *
909   * @throws  LDAPException  If a problem is encountered while parsing options.
910   */
911  private static Map<String,String>
912                      parseOptions(final List<String> options)
913          throws LDAPException
914  {
915    if (options == null)
916    {
917      return new HashMap<String,String>(0);
918    }
919
920    final HashMap<String,String> m = new HashMap<String,String>(options.size());
921    for (final String s : options)
922    {
923      final int equalPos = s.indexOf('=');
924      if (equalPos < 0)
925      {
926        throw new LDAPException(ResultCode.PARAM_ERROR,
927             ERR_SASL_OPTION_MISSING_EQUAL.get(s));
928      }
929      else if (equalPos == 0)
930      {
931        throw new LDAPException(ResultCode.PARAM_ERROR,
932             ERR_SASL_OPTION_STARTS_WITH_EQUAL.get(s));
933      }
934
935      final String name = s.substring(0, equalPos);
936      final String value = s.substring(equalPos + 1);
937      if (m.put(toLowerCase(name), value) != null)
938      {
939        throw new LDAPException(ResultCode.PARAM_ERROR,
940             ERR_SASL_OPTION_NOT_MULTI_VALUED.get(name));
941      }
942    }
943
944    return m;
945  }
946
947
948
949  /**
950   * Ensures that the provided map is empty, and will throw an exception if it
951   * isn't.  This method is intended for internal use only.
952   *
953   * @param  options    The map of options to ensure is empty.
954   * @param  mechanism  The associated SASL mechanism.
955   *
956   * @throws  LDAPException  If the map of SASL options is not empty.
957   */
958  @InternalUseOnly()
959  public static void ensureNoUnsupportedOptions(
960                          final Map<String,String> options,
961                          final String mechanism)
962          throws LDAPException
963  {
964    if (! options.isEmpty())
965    {
966      for (final String s : options.keySet())
967      {
968        throw new LDAPException(ResultCode.PARAM_ERROR,
969             ERR_SASL_OPTION_UNSUPPORTED_FOR_MECH.get(s,mechanism));
970      }
971    }
972  }
973
974
975
976  /**
977   * Retrieves the value of the specified option and parses it as a boolean.
978   * Values of "true", "t", "yes", "y", "on", and "1" will be treated as
979   * {@code true}.  Values of "false", "f", "no", "n", "off", and "0" will be
980   * treated as {@code false}.
981   *
982   * @param  m  The map from which to retrieve the option.  It must not be
983   *            {@code null}.
984   * @param  o  The name of the option to examine.
985   * @param  d  The default value to use if the given option was not provided.
986   *
987   * @return  The parsed boolean value.
988   *
989   * @throws  LDAPException  If the option value cannot be parsed as a boolean.
990   */
991  static boolean getBooleanValue(final Map<String,String> m, final String o,
992                                 final boolean d)
993         throws LDAPException
994  {
995    final String s = toLowerCase(m.remove(toLowerCase(o)));
996    if (s == null)
997    {
998      return d;
999    }
1000    else if (s.equals("true") ||
1001             s.equals("t") ||
1002             s.equals("yes") ||
1003             s.equals("y") ||
1004             s.equals("on") ||
1005             s.equals("1"))
1006    {
1007      return true;
1008    }
1009    else if (s.equals("false") ||
1010             s.equals("f") ||
1011             s.equals("no") ||
1012             s.equals("n") ||
1013             s.equals("off") ||
1014             s.equals("0"))
1015    {
1016      return false;
1017    }
1018    else
1019    {
1020      throw new LDAPException(ResultCode.PARAM_ERROR,
1021           ERR_SASL_OPTION_MALFORMED_BOOLEAN_VALUE.get(o));
1022    }
1023  }
1024}