001/*
002 * Copyright 2011-2019 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2011-2019 Ping Identity Corporation
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.util.ArrayList;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.List;
029import java.util.Map;
030import java.util.TreeMap;
031
032import com.unboundid.ldap.sdk.ANONYMOUSBindRequest;
033import com.unboundid.ldap.sdk.Control;
034import com.unboundid.ldap.sdk.CRAMMD5BindRequest;
035import com.unboundid.ldap.sdk.DIGESTMD5BindRequest;
036import com.unboundid.ldap.sdk.DIGESTMD5BindRequestProperties;
037import com.unboundid.ldap.sdk.EXTERNALBindRequest;
038import com.unboundid.ldap.sdk.GSSAPIBindRequest;
039import com.unboundid.ldap.sdk.GSSAPIBindRequestProperties;
040import com.unboundid.ldap.sdk.LDAPException;
041import com.unboundid.ldap.sdk.PLAINBindRequest;
042import com.unboundid.ldap.sdk.ResultCode;
043import com.unboundid.ldap.sdk.SASLBindRequest;
044import com.unboundid.ldap.sdk.SASLQualityOfProtection;
045import com.unboundid.ldap.sdk.unboundidds.SingleUseTOTPBindRequest;
046import com.unboundid.ldap.sdk.unboundidds.
047            UnboundIDCertificatePlusPasswordBindRequest;
048import com.unboundid.ldap.sdk.unboundidds.UnboundIDDeliveredOTPBindRequest;
049import com.unboundid.ldap.sdk.unboundidds.UnboundIDTOTPBindRequest;
050import com.unboundid.ldap.sdk.unboundidds.UnboundIDYubiKeyOTPBindRequest;
051
052import static com.unboundid.util.UtilityMessages.*;
053
054
055
056/**
057 * This class provides a utility that may be used to help process SASL bind
058 * operations using the LDAP SDK.
059 */
060@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
061public final class SASLUtils
062{
063  /**
064   * The name of the SASL option that specifies the authentication ID.  It may
065   * be used in conjunction with the CRAM-MD5, DIGEST-MD5, GSSAPI, and PLAIN
066   * mechanisms.
067   */
068  public static final String SASL_OPTION_AUTH_ID = "authID";
069
070
071
072  /**
073   * The name of the SASL option that specifies the authorization ID.  It may
074   * be used in conjunction with the DIGEST-MD5, GSSAPI, and PLAIN mechanisms.
075   */
076  public static final String SASL_OPTION_AUTHZ_ID = "authzID";
077
078
079
080  /**
081   * The name of the SASL option that specifies the path to the JAAS config
082   * file.  It may be used in conjunction with the GSSAPI mechanism.
083   */
084  public static final String SASL_OPTION_CONFIG_FILE = "configFile";
085
086
087
088  /**
089   * The name of the SASL option that indicates whether debugging should be
090   * enabled.  It may be used in conjunction with the GSSAPI mechanism.
091   */
092  public static final String SASL_OPTION_DEBUG = "debug";
093
094
095
096  /**
097   * The name of the SASL option that specifies the KDC address.  It may be used
098   * in conjunction with the GSSAPI mechanism.
099   */
100  public static final String SASL_OPTION_KDC_ADDRESS = "kdcAddress";
101
102
103
104  /**
105   * The name of the SASL option that specifies the desired SASL mechanism to
106   * use to authenticate to the server.
107   */
108  public static final String SASL_OPTION_MECHANISM = "mech";
109
110
111
112  /**
113   * The name of the SASL option that specifies a one-time password.  It may be
114   * used in conjunction with the UNBOUNDID-DELIVERED-OTP and
115   * UNBOUNDID-YUBIKEY-OTP mechanisms.
116   */
117  public static final String SASL_OPTION_OTP = "otp";
118
119
120
121  /**
122   * The name of the SASL option that may be used to indicate whether to
123   * prompt for a static password.  It may be used in conjunction with the
124   * UNBOUNDID-TOTP and UNBOUNDID-YUBIKEY-OTP mechanisms.
125   */
126  public static final String SASL_OPTION_PROMPT_FOR_STATIC_PW =
127       "promptForStaticPassword";
128
129
130
131  /**
132   * The name of the SASL option that specifies the GSSAPI service principal
133   * protocol.  It may be used in conjunction with the GSSAPI mechanism.
134   */
135  public static final String SASL_OPTION_PROTOCOL = "protocol";
136
137
138
139  /**
140   * The name of the SASL option that specifies the quality of protection that
141   * should be used for communication that occurs after the authentication has
142   * completed.
143   */
144  public static final String SASL_OPTION_QOP = "qop";
145
146
147
148  /**
149   * The name of the SASL option that specifies the realm name.  It may be used
150   * in conjunction with the DIGEST-MD5 and GSSAPI mechanisms.
151   */
152  public static final String SASL_OPTION_REALM = "realm";
153
154
155
156  /**
157   * The name of the SASL option that indicates whether to require an existing
158   * Kerberos session from the ticket cache.  It may be used in conjunction with
159   * the GSSAPI mechanism.
160   */
161  public static final String SASL_OPTION_REQUIRE_CACHE = "requireCache";
162
163
164
165  /**
166   * The name of the SASL option that indicates whether to attempt to renew the
167   * Kerberos TGT for an existing session.  It may be used in conjunction with
168   * the GSSAPI mechanism.
169   */
170  public static final String SASL_OPTION_RENEW_TGT = "renewTGT";
171
172
173
174  /**
175   * The name of the SASL option that specifies the path to the Kerberos ticket
176   * cache to use.  It may be used in conjunction with the GSSAPI mechanism.
177   */
178  public static final String SASL_OPTION_TICKET_CACHE_PATH = "ticketCache";
179
180
181
182  /**
183   * The name of the SASL option that specifies the TOTP authentication code.
184   * It may be used in conjunction with the UNBOUNDID-TOTP mechanism.
185   */
186  public static final String SASL_OPTION_TOTP_PASSWORD = "totpPassword";
187
188
189
190  /**
191   * The name of the SASL option that specifies the trace string.  It may be
192   * used in conjunction with the ANONYMOUS mechanism.
193   */
194  public static final String SASL_OPTION_TRACE = "trace";
195
196
197
198  /**
199   * The name of the SASL option that specifies whether to use a Kerberos ticket
200   * cache.  It may be used in conjunction with the GSSAPI mechanism.
201   */
202  public static final String SASL_OPTION_USE_TICKET_CACHE = "useTicketCache";
203
204
205
206  /**
207   * A map with information about all supported SASL mechanisms, mapped from
208   * lowercase mechanism name to an object with information about that
209   * mechanism.
210   */
211  private static final Map<String,SASLMechanismInfo> SASL_MECHANISMS;
212
213
214
215  static
216  {
217    final TreeMap<String,SASLMechanismInfo> m = new TreeMap<>();
218
219    m.put(
220         StaticUtils.toLowerCase(ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME),
221         new SASLMechanismInfo(ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME,
222              INFO_SASL_ANONYMOUS_DESCRIPTION.get(), false, false,
223              new SASLOption(SASL_OPTION_TRACE,
224                   INFO_SASL_ANONYMOUS_OPTION_TRACE.get(), false, false)));
225
226    m.put(StaticUtils.toLowerCase(CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME),
227         new SASLMechanismInfo(CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME,
228              INFO_SASL_CRAM_MD5_DESCRIPTION.get(), true, true,
229              new SASLOption(SASL_OPTION_AUTH_ID,
230                   INFO_SASL_CRAM_MD5_OPTION_AUTH_ID.get(), true, false)));
231
232    m.put(
233         StaticUtils.toLowerCase(DIGESTMD5BindRequest.DIGESTMD5_MECHANISM_NAME),
234         new SASLMechanismInfo(DIGESTMD5BindRequest.DIGESTMD5_MECHANISM_NAME,
235              INFO_SASL_DIGEST_MD5_DESCRIPTION.get(), true, true,
236              new SASLOption(SASL_OPTION_AUTH_ID,
237                   INFO_SASL_DIGEST_MD5_OPTION_AUTH_ID.get(), true, false),
238              new SASLOption(SASL_OPTION_AUTHZ_ID,
239                   INFO_SASL_DIGEST_MD5_OPTION_AUTHZ_ID.get(), false, false),
240              new SASLOption(SASL_OPTION_REALM,
241                   INFO_SASL_DIGEST_MD5_OPTION_REALM.get(), false, false),
242              new SASLOption(SASL_OPTION_QOP,
243                   INFO_SASL_DIGEST_MD5_OPTION_QOP.get(), false, false)));
244
245    m.put(StaticUtils.toLowerCase(EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME),
246         new SASLMechanismInfo(EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME,
247              INFO_SASL_EXTERNAL_DESCRIPTION.get(), false, false));
248
249    m.put(StaticUtils.toLowerCase(GSSAPIBindRequest.GSSAPI_MECHANISM_NAME),
250         new SASLMechanismInfo(GSSAPIBindRequest.GSSAPI_MECHANISM_NAME,
251              INFO_SASL_GSSAPI_DESCRIPTION.get(), true, false,
252              new SASLOption(SASL_OPTION_AUTH_ID,
253                   INFO_SASL_GSSAPI_OPTION_AUTH_ID.get(), true, false),
254              new SASLOption(SASL_OPTION_AUTHZ_ID,
255                   INFO_SASL_GSSAPI_OPTION_AUTHZ_ID.get(), false, false),
256              new SASLOption(SASL_OPTION_CONFIG_FILE,
257                   INFO_SASL_GSSAPI_OPTION_CONFIG_FILE.get(), false, false),
258              new SASLOption(SASL_OPTION_DEBUG,
259                   INFO_SASL_GSSAPI_OPTION_DEBUG.get(), false, false),
260              new SASLOption(SASL_OPTION_KDC_ADDRESS,
261                   INFO_SASL_GSSAPI_OPTION_KDC_ADDRESS.get(), false, false),
262              new SASLOption(SASL_OPTION_PROTOCOL,
263                   INFO_SASL_GSSAPI_OPTION_PROTOCOL.get(), false, false),
264              new SASLOption(SASL_OPTION_REALM,
265                   INFO_SASL_GSSAPI_OPTION_REALM.get(), false, false),
266              new SASLOption(SASL_OPTION_QOP,
267                   INFO_SASL_GSSAPI_OPTION_QOP.get(), false, false),
268              new SASLOption(SASL_OPTION_RENEW_TGT,
269                   INFO_SASL_GSSAPI_OPTION_RENEW_TGT.get(), false, false),
270              new SASLOption(SASL_OPTION_REQUIRE_CACHE,
271                   INFO_SASL_GSSAPI_OPTION_REQUIRE_TICKET_CACHE.get(), false,
272                   false),
273              new SASLOption(SASL_OPTION_TICKET_CACHE_PATH,
274                   INFO_SASL_GSSAPI_OPTION_TICKET_CACHE.get(), false, false),
275              new SASLOption(SASL_OPTION_USE_TICKET_CACHE,
276                   INFO_SASL_GSSAPI_OPTION_USE_TICKET_CACHE.get(), false,
277                   false)));
278
279    m.put(StaticUtils.toLowerCase(PLAINBindRequest.PLAIN_MECHANISM_NAME),
280         new SASLMechanismInfo(PLAINBindRequest.PLAIN_MECHANISM_NAME,
281              INFO_SASL_PLAIN_DESCRIPTION.get(), true, true,
282              new SASLOption(SASL_OPTION_AUTH_ID,
283                   INFO_SASL_PLAIN_OPTION_AUTH_ID.get(), true, false),
284              new SASLOption(SASL_OPTION_AUTHZ_ID,
285                   INFO_SASL_PLAIN_OPTION_AUTHZ_ID.get(), false, false)));
286
287    m.put(StaticUtils.toLowerCase(
288         UnboundIDCertificatePlusPasswordBindRequest.
289              UNBOUNDID_CERT_PLUS_PW_MECHANISM_NAME),
290         new SASLMechanismInfo(
291              UnboundIDCertificatePlusPasswordBindRequest.
292                   UNBOUNDID_CERT_PLUS_PW_MECHANISM_NAME,
293              INFO_SASL_UNBOUNDID_CERT_PLUS_PASSWORD_DESCRIPTION.get(), true,
294              true));
295
296    m.put(
297         StaticUtils.toLowerCase(
298              UnboundIDDeliveredOTPBindRequest.
299                   UNBOUNDID_DELIVERED_OTP_MECHANISM_NAME),
300         new SASLMechanismInfo(
301              UnboundIDDeliveredOTPBindRequest.
302                   UNBOUNDID_DELIVERED_OTP_MECHANISM_NAME,
303              INFO_SASL_UNBOUNDID_DELIVERED_OTP_DESCRIPTION.get(), false, false,
304              new SASLOption(SASL_OPTION_AUTH_ID,
305                   INFO_SASL_UNBOUNDID_TOTP_OPTION_AUTH_ID.get(), true, false),
306              new SASLOption(SASL_OPTION_AUTHZ_ID,
307                   INFO_SASL_UNBOUNDID_TOTP_OPTION_AUTHZ_ID.get(), false,
308                   false),
309              new SASLOption(SASL_OPTION_OTP,
310                   INFO_SASL_UNBOUNDID_DELIVERED_OTP_OPTION_OTP.get(), true,
311                   false)));
312
313    m.put(
314         StaticUtils.toLowerCase(
315              UnboundIDTOTPBindRequest.UNBOUNDID_TOTP_MECHANISM_NAME),
316         new SASLMechanismInfo(
317              UnboundIDTOTPBindRequest.UNBOUNDID_TOTP_MECHANISM_NAME,
318              INFO_SASL_UNBOUNDID_TOTP_DESCRIPTION.get(), true, false,
319              new SASLOption(SASL_OPTION_AUTH_ID,
320                   INFO_SASL_UNBOUNDID_TOTP_OPTION_AUTH_ID.get(), true, false),
321              new SASLOption(SASL_OPTION_AUTHZ_ID,
322                   INFO_SASL_UNBOUNDID_TOTP_OPTION_AUTHZ_ID.get(), false,
323                   false),
324              new SASLOption(SASL_OPTION_PROMPT_FOR_STATIC_PW,
325                   INFO_SASL_UNBOUNDID_TOTP_OPTION_PROMPT_FOR_PW.get(), false,
326                   false),
327              new SASLOption(SASL_OPTION_TOTP_PASSWORD,
328                   INFO_SASL_UNBOUNDID_TOTP_OPTION_TOTP_PASSWORD.get(), true,
329                   false)));
330
331    m.put(
332         StaticUtils.toLowerCase(
333              UnboundIDYubiKeyOTPBindRequest.
334                   UNBOUNDID_YUBIKEY_OTP_MECHANISM_NAME),
335         new SASLMechanismInfo(
336              UnboundIDYubiKeyOTPBindRequest.
337                   UNBOUNDID_YUBIKEY_OTP_MECHANISM_NAME,
338              INFO_SASL_UNBOUNDID_YUBIKEY_OTP_DESCRIPTION.get(), true, false,
339              new SASLOption(SASL_OPTION_AUTH_ID,
340                   INFO_SASL_UNBOUNDID_YUBIKEY_OTP_OPTION_AUTH_ID.get(), true,
341                   false),
342              new SASLOption(SASL_OPTION_AUTHZ_ID,
343                   INFO_SASL_UNBOUNDID_YUBIKEY_OTP_OPTION_AUTHZ_ID.get(), false,
344                   false),
345              new SASLOption(SASL_OPTION_OTP,
346                   INFO_SASL_UNBOUNDID_YUBIKEY_OTP_OPTION_OTP.get(), true,
347                   false),
348              new SASLOption(SASL_OPTION_PROMPT_FOR_STATIC_PW,
349                   INFO_SASL_UNBOUNDID_YUBIKEY_OTP_OPTION_PROMPT_FOR_PW.get(),
350                   false, false)));
351
352    SASL_MECHANISMS = Collections.unmodifiableMap(m);
353  }
354
355
356
357  /**
358   * Prevent this utility class from being instantiated.
359   */
360  private SASLUtils()
361  {
362    // No implementation required.
363  }
364
365
366
367  /**
368   * Retrieves information about the SASL mechanisms supported for use by this
369   * class.
370   *
371   * @return  Information about the SASL mechanisms supported for use by this
372   *          class.
373   */
374  public static List<SASLMechanismInfo> getSupportedSASLMechanisms()
375  {
376    return Collections.unmodifiableList(
377         new ArrayList<>(SASL_MECHANISMS.values()));
378  }
379
380
381
382  /**
383   * Retrieves information about the specified SASL mechanism.
384   *
385   * @param  mechanism  The name of the SASL mechanism for which to retrieve
386   *                    information.  It will not be treated in a case-sensitive
387   *                    manner.
388   *
389   * @return  Information about the requested SASL mechanism, or {@code null} if
390   *          no information about the specified mechanism is available.
391   */
392  public static SASLMechanismInfo getSASLMechanismInfo(final String mechanism)
393  {
394    return SASL_MECHANISMS.get(StaticUtils.toLowerCase(mechanism));
395  }
396
397
398
399  /**
400   * Creates a new SASL bind request using the provided information.
401   *
402   * @param  bindDN     The bind DN to use for the SASL bind request.  For most
403   *                    SASL mechanisms, this should be {@code null}, since the
404   *                    identity of the target user should be specified in some
405   *                    other way (e.g., via an "authID" SASL option).
406   * @param  password   The password to use for the SASL bind request.  It may
407   *                    be {@code null} if no password is required for the
408   *                    desired SASL mechanism.
409   * @param  mechanism  The name of the SASL mechanism to use.  It may be
410   *                    {@code null} if the provided set of options contains a
411   *                    "mech" option to specify the desired SASL option.
412   * @param  options    The set of SASL options to use when creating the bind
413   *                    request, in the form "name=value".  It may be
414   *                    {@code null} or empty if no SASL options are needed and
415   *                    a value was provided for the {@code mechanism} argument.
416   *                    If the set of SASL options includes a "mech" option,
417   *                    then the {@code mechanism} argument must be {@code null}
418   *                    or have a value that matches the value of the "mech"
419   *                    SASL option.
420   *
421   * @return  The SASL bind request created using the provided information.
422   *
423   * @throws  LDAPException  If a problem is encountered while trying to create
424   *                         the SASL bind request.
425   */
426  public static SASLBindRequest createBindRequest(final String bindDN,
427                                                  final String password,
428                                                  final String mechanism,
429                                                  final String... options)
430         throws LDAPException
431  {
432    return createBindRequest(bindDN,
433         (password == null ? null : StaticUtils.getBytes(password)), mechanism,
434         StaticUtils.toList(options));
435  }
436
437
438
439  /**
440   * Creates a new SASL bind request using the provided information.
441   *
442   * @param  bindDN     The bind DN to use for the SASL bind request.  For most
443   *                    SASL mechanisms, this should be {@code null}, since the
444   *                    identity of the target user should be specified in some
445   *                    other way (e.g., via an "authID" SASL option).
446   * @param  password   The password to use for the SASL bind request.  It may
447   *                    be {@code null} if no password is required for the
448   *                    desired SASL mechanism.
449   * @param  mechanism  The name of the SASL mechanism to use.  It may be
450   *                    {@code null} if the provided set of options contains a
451   *                    "mech" option to specify the desired SASL option.
452   * @param  options    The set of SASL options to use when creating the bind
453   *                    request, in the form "name=value".  It may be
454   *                    {@code null} or empty if no SASL options are needed and
455   *                    a value was provided for the {@code mechanism} argument.
456   *                    If the set of SASL options includes a "mech" option,
457   *                    then the {@code mechanism} argument must be {@code null}
458   *                    or have a value that matches the value of the "mech"
459   *                    SASL option.
460   * @param  controls   The set of controls to include in the request.
461   *
462   * @return  The SASL bind request created using the provided information.
463   *
464   * @throws  LDAPException  If a problem is encountered while trying to create
465   *                         the SASL bind request.
466   */
467  public static SASLBindRequest createBindRequest(final String bindDN,
468                                                  final String password,
469                                                  final String mechanism,
470                                                  final List<String> options,
471                                                  final Control... controls)
472         throws LDAPException
473  {
474    return createBindRequest(bindDN,
475         (password == null
476              ? null
477              : StaticUtils.getBytes(password)), mechanism, options,
478         controls);
479  }
480
481
482
483  /**
484   * Creates a new SASL bind request using the provided information.
485   *
486   * @param  bindDN     The bind DN to use for the SASL bind request.  For most
487   *                    SASL mechanisms, this should be {@code null}, since the
488   *                    identity of the target user should be specified in some
489   *                    other way (e.g., via an "authID" SASL option).
490   * @param  password   The password to use for the SASL bind request.  It may
491   *                    be {@code null} if no password is required for the
492   *                    desired SASL mechanism.
493   * @param  mechanism  The name of the SASL mechanism to use.  It may be
494   *                    {@code null} if the provided set of options contains a
495   *                    "mech" option to specify the desired SASL option.
496   * @param  options    The set of SASL options to use when creating the bind
497   *                    request, in the form "name=value".  It may be
498   *                    {@code null} or empty if no SASL options are needed and
499   *                    a value was provided for the {@code mechanism} argument.
500   *                    If the set of SASL options includes a "mech" option,
501   *                    then the {@code mechanism} argument must be {@code null}
502   *                    or have a value that matches the value of the "mech"
503   *                    SASL option.
504   *
505   * @return  The SASL bind request created using the provided information.
506   *
507   * @throws  LDAPException  If a problem is encountered while trying to create
508   *                         the SASL bind request.
509   */
510  public static SASLBindRequest createBindRequest(final String bindDN,
511                                                  final byte[] password,
512                                                  final String mechanism,
513                                                  final String... options)
514         throws LDAPException
515  {
516    return createBindRequest(bindDN, password, mechanism,
517         StaticUtils.toList(options));
518  }
519
520
521
522  /**
523   * Creates a new SASL bind request using the provided information.
524   *
525   * @param  bindDN     The bind DN to use for the SASL bind request.  For most
526   *                    SASL mechanisms, this should be {@code null}, since the
527   *                    identity of the target user should be specified in some
528   *                    other way (e.g., via an "authID" SASL option).
529   * @param  password   The password to use for the SASL bind request.  It may
530   *                    be {@code null} if no password is required for the
531   *                    desired SASL mechanism.
532   * @param  mechanism  The name of the SASL mechanism to use.  It may be
533   *                    {@code null} if the provided set of options contains a
534   *                    "mech" option to specify the desired SASL option.
535   * @param  options    The set of SASL options to use when creating the bind
536   *                    request, in the form "name=value".  It may be
537   *                    {@code null} or empty if no SASL options are needed and
538   *                    a value was provided for the {@code mechanism} argument.
539   *                    If the set of SASL options includes a "mech" option,
540   *                    then the {@code mechanism} argument must be {@code null}
541   *                    or have a value that matches the value of the "mech"
542   *                    SASL option.
543   * @param  controls   The set of controls to include in the request.
544   *
545   * @return  The SASL bind request created using the provided information.
546   *
547   * @throws  LDAPException  If a problem is encountered while trying to create
548   *                         the SASL bind request.
549   */
550  public static SASLBindRequest createBindRequest(final String bindDN,
551                                                  final byte[] password,
552                                                  final String mechanism,
553                                                  final List<String> options,
554                                                  final Control... controls)
555         throws LDAPException
556  {
557    return createBindRequest(bindDN, password, false, null, mechanism, options,
558         controls);
559  }
560
561
562
563  /**
564   * Creates a new SASL bind request using the provided information.
565   *
566   * @param  bindDN             The bind DN to use for the SASL bind request.
567   *                            For most SASL mechanisms, this should be
568   *                            {@code null}, since the identity of the target
569   *                            user should be specified in some other way
570   *                            (e.g., via an "authID" SASL option).
571   * @param  password           The password to use for the SASL bind request.
572   *                            It may be {@code null} if no password is
573   *                            required for the desired SASL mechanism.
574   * @param  promptForPassword  Indicates whether to interactively prompt for
575   *                            the password if one is needed but none was
576   *                            provided.
577   * @param  tool               The command-line tool whose input and output
578   *                            streams should be used when prompting for the
579   *                            bind password.  It may be {@code null} if
580   *                            {@code promptForPassword} is {@code false}.
581   * @param  mechanism          The name of the SASL mechanism to use.  It may
582   *                            be {@code null} if the provided set of options
583   *                            contains a "mech" option to specify the desired
584   *                            SASL option.
585   * @param  options            The set of SASL options to use when creating the
586   *                            bind request, in the form "name=value".  It may
587   *                            be {@code null} or empty if no SASL options are
588   *                            needed and a value was provided for the
589   *                            {@code mechanism} argument.  If the set of SASL
590   *                            options includes a "mech" option, then the
591   *                            {@code mechanism} argument must be {@code null}
592   *                            or have a value that matches the value of the
593   *                            "mech" SASL option.
594   * @param  controls           The set of controls to include in the request.
595   *
596   * @return  The SASL bind request created using the provided information.
597   *
598   * @throws  LDAPException  If a problem is encountered while trying to create
599   *                         the SASL bind request.
600   */
601  public static SASLBindRequest createBindRequest(final String bindDN,
602                                     final byte[] password,
603                                     final boolean promptForPassword,
604                                     final CommandLineTool tool,
605                                     final String mechanism,
606                                     final List<String> options,
607                                     final Control... controls)
608         throws LDAPException
609  {
610    if (promptForPassword)
611    {
612      Validator.ensureNotNull(tool);
613    }
614
615    // Parse the provided set of options to ensure that they are properly
616    // formatted in name-value form, and extract the SASL mechanism.
617    final String mech;
618    final Map<String,String> optionsMap = parseOptions(options);
619    final String mechOption =
620         optionsMap.remove(StaticUtils.toLowerCase(SASL_OPTION_MECHANISM));
621    if (mechOption != null)
622    {
623      mech = mechOption;
624      if ((mechanism != null) && (! mech.equalsIgnoreCase(mechanism)))
625      {
626        throw new LDAPException(ResultCode.PARAM_ERROR,
627             ERR_SASL_OPTION_MECH_CONFLICT.get(mechanism, mech));
628      }
629    }
630    else
631    {
632      mech = mechanism;
633    }
634
635    if (mech == null)
636    {
637      throw new LDAPException(ResultCode.PARAM_ERROR,
638           ERR_SASL_OPTION_NO_MECH.get());
639    }
640
641    if (mech.equalsIgnoreCase(ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME))
642    {
643      return createANONYMOUSBindRequest(password, optionsMap, controls);
644    }
645    else if (mech.equalsIgnoreCase(CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME))
646    {
647      return createCRAMMD5BindRequest(password, promptForPassword, tool,
648           optionsMap, controls);
649    }
650    else if (mech.equalsIgnoreCase(
651                  DIGESTMD5BindRequest.DIGESTMD5_MECHANISM_NAME))
652    {
653      return createDIGESTMD5BindRequest(password, promptForPassword, tool,
654           optionsMap, controls);
655    }
656    else if (mech.equalsIgnoreCase(EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME))
657    {
658      return createEXTERNALBindRequest(password, optionsMap, controls);
659    }
660    else if (mech.equalsIgnoreCase(GSSAPIBindRequest.GSSAPI_MECHANISM_NAME))
661    {
662      return createGSSAPIBindRequest(password, promptForPassword, tool,
663           optionsMap, controls);
664    }
665    else if (mech.equalsIgnoreCase(PLAINBindRequest.PLAIN_MECHANISM_NAME))
666    {
667      return createPLAINBindRequest(password, promptForPassword, tool,
668           optionsMap, controls);
669    }
670    else if (mech.equalsIgnoreCase(UnboundIDCertificatePlusPasswordBindRequest.
671             UNBOUNDID_CERT_PLUS_PW_MECHANISM_NAME))
672    {
673      return createUnboundIDCertificatePlusPasswordBindRequest(password, tool,
674           optionsMap, controls);
675    }
676    else if (mech.equalsIgnoreCase(UnboundIDDeliveredOTPBindRequest.
677             UNBOUNDID_DELIVERED_OTP_MECHANISM_NAME))
678    {
679      return createUNBOUNDIDDeliveredOTPBindRequest(password, optionsMap,
680           controls);
681    }
682    else if (mech.equalsIgnoreCase(
683             UnboundIDTOTPBindRequest.UNBOUNDID_TOTP_MECHANISM_NAME))
684    {
685      return createUNBOUNDIDTOTPBindRequest(password, tool, optionsMap,
686           controls);
687    }
688    else if (mech.equalsIgnoreCase(
689         UnboundIDYubiKeyOTPBindRequest.UNBOUNDID_YUBIKEY_OTP_MECHANISM_NAME))
690    {
691      return createUNBOUNDIDYUBIKEYOTPBindRequest(password, tool, optionsMap,
692           controls);
693    }
694    else
695    {
696      throw new LDAPException(ResultCode.PARAM_ERROR,
697           ERR_SASL_OPTION_UNSUPPORTED_MECH.get(mech));
698    }
699  }
700
701
702
703  /**
704   * Creates a SASL ANONYMOUS bind request using the provided set of options.
705   *
706   * @param  password  The password to use for the bind request.
707   * @param  options   The set of SASL options for the bind request.
708   * @param  controls  The set of controls to include in the request.
709   *
710   * @return  The SASL ANONYMOUS bind request that was created.
711   *
712   * @throws  LDAPException  If a problem is encountered while trying to create
713   *                         the SASL bind request.
714   */
715  private static ANONYMOUSBindRequest createANONYMOUSBindRequest(
716                                           final byte[] password,
717                                           final Map<String,String> options,
718                                           final Control[] controls)
719          throws LDAPException
720  {
721    if (password != null)
722    {
723      throw new LDAPException(ResultCode.PARAM_ERROR,
724           ERR_SASL_OPTION_MECH_DOESNT_ACCEPT_PASSWORD.get(
725                ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME));
726    }
727
728
729    // The trace option is optional.
730    final String trace =
731         options.remove(StaticUtils.toLowerCase(SASL_OPTION_TRACE));
732
733    // Ensure no unsupported options were provided.
734    ensureNoUnsupportedOptions(options,
735         ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME);
736
737    return new ANONYMOUSBindRequest(trace, controls);
738  }
739
740
741
742  /**
743   * Creates a SASL CRAM-MD5 bind request using the provided password and set of
744   * options.
745   *
746   * @param  password           The password to use for the bind request.
747   * @param  promptForPassword  Indicates whether to interactively prompt for
748   *                            the password if one is needed but none was
749   *                            provided.
750   * @param  tool               The command-line tool whose input and output
751   *                            streams should be used when prompting for the
752   *                            bind password.  It may be {@code null} if
753   *                            {@code promptForPassword} is {@code false}.
754   * @param  options            The set of SASL options for the bind request.
755   * @param  controls           The set of controls to include in the request.
756   *
757   * @return  The SASL CRAM-MD5 bind request that was created.
758   *
759   * @throws  LDAPException  If a problem is encountered while trying to create
760   *                         the SASL bind request.
761   */
762  private static CRAMMD5BindRequest createCRAMMD5BindRequest(
763                                         final byte[] password,
764                                         final boolean promptForPassword,
765                                         final CommandLineTool tool,
766                                         final Map<String,String> options,
767                                         final Control[] controls)
768          throws LDAPException
769  {
770    final byte[] pw;
771    if (password == null)
772    {
773      if (promptForPassword)
774      {
775        tool.getOriginalOut().print(INFO_LDAP_TOOL_ENTER_BIND_PASSWORD.get());
776        pw = PasswordReader.readPassword();
777        tool.getOriginalOut().println();
778      }
779      else
780      {
781        throw new LDAPException(ResultCode.PARAM_ERROR,
782             ERR_SASL_OPTION_MECH_REQUIRES_PASSWORD.get(
783                  CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME));
784      }
785    }
786    else
787    {
788      pw = password;
789    }
790
791
792    // The authID option is required.
793    final String authID =
794         options.remove(StaticUtils.toLowerCase(SASL_OPTION_AUTH_ID));
795    if (authID == null)
796    {
797      throw new LDAPException(ResultCode.PARAM_ERROR,
798           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_AUTH_ID,
799                CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME));
800    }
801
802
803    // Ensure no unsupported options were provided.
804    ensureNoUnsupportedOptions(options,
805         CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME);
806
807    return new CRAMMD5BindRequest(authID, pw, controls);
808  }
809
810
811
812  /**
813   * Creates a SASL DIGEST-MD5 bind request using the provided password and set
814   * of options.
815   *
816   * @param  password           The password to use for the bind request.
817   * @param  promptForPassword  Indicates whether to interactively prompt for
818   *                            the password if one is needed but none was
819   *                            provided.
820   * @param  tool               The command-line tool whose input and output
821   *                            streams should be used when prompting for the
822   *                            bind password.  It may be {@code null} if
823   *                            {@code promptForPassword} is {@code false}.
824   * @param  options            The set of SASL options for the bind request.
825   * @param  controls           The set of controls to include in the request.
826   *
827   * @return  The SASL DIGEST-MD5 bind request that was created.
828   *
829   * @throws  LDAPException  If a problem is encountered while trying to create
830   *                         the SASL bind request.
831   */
832  private static DIGESTMD5BindRequest createDIGESTMD5BindRequest(
833                                           final byte[] password,
834                                           final boolean promptForPassword,
835                                           final CommandLineTool tool,
836                                           final Map<String,String> options,
837                                           final Control[] controls)
838          throws LDAPException
839  {
840    final byte[] pw;
841    if (password == null)
842    {
843      if (promptForPassword)
844      {
845        tool.getOriginalOut().print(INFO_LDAP_TOOL_ENTER_BIND_PASSWORD.get());
846        pw = PasswordReader.readPassword();
847        tool.getOriginalOut().println();
848      }
849      else
850      {
851        throw new LDAPException(ResultCode.PARAM_ERROR,
852             ERR_SASL_OPTION_MECH_REQUIRES_PASSWORD.get(
853                  CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME));
854      }
855    }
856    else
857    {
858      pw = password;
859    }
860
861    // The authID option is required.
862    final String authID =
863         options.remove(StaticUtils.toLowerCase(SASL_OPTION_AUTH_ID));
864    if (authID == null)
865    {
866      throw new LDAPException(ResultCode.PARAM_ERROR,
867           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_AUTH_ID,
868                CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME));
869    }
870
871    final DIGESTMD5BindRequestProperties properties =
872         new DIGESTMD5BindRequestProperties(authID, pw);
873
874    // The authzID option is optional.
875    properties.setAuthorizationID(
876         options.remove(StaticUtils.toLowerCase(SASL_OPTION_AUTHZ_ID)));
877
878    // The realm option is optional.
879    properties.setRealm(options.remove(
880         StaticUtils.toLowerCase(SASL_OPTION_REALM)));
881
882    // The QoP option is optional, and may contain multiple values that need to
883    // be parsed.
884    final String qopString =
885         options.remove(StaticUtils.toLowerCase(SASL_OPTION_QOP));
886    if (qopString != null)
887    {
888      properties.setAllowedQoP(
889           SASLQualityOfProtection.decodeQoPList(qopString));
890    }
891
892    // Ensure no unsupported options were provided.
893    ensureNoUnsupportedOptions(options,
894         DIGESTMD5BindRequest.DIGESTMD5_MECHANISM_NAME);
895
896    return new DIGESTMD5BindRequest(properties, controls);
897  }
898
899
900
901  /**
902   * Creates a SASL EXTERNAL bind request using the provided set of options.
903   *
904   * @param  password  The password to use for the bind request.
905   * @param  options   The set of SASL options for the bind request.
906   * @param  controls  The set of controls to include in the request.
907   *
908   * @return  The SASL EXTERNAL bind request that was created.
909   *
910   * @throws  LDAPException  If a problem is encountered while trying to create
911   *                         the SASL bind request.
912   */
913  private static EXTERNALBindRequest createEXTERNALBindRequest(
914                                          final byte[] password,
915                                          final Map<String,String> options,
916                                          final Control[] controls)
917          throws LDAPException
918  {
919    if (password != null)
920    {
921      throw new LDAPException(ResultCode.PARAM_ERROR,
922           ERR_SASL_OPTION_MECH_DOESNT_ACCEPT_PASSWORD.get(
923                EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME));
924    }
925
926    // Ensure no unsupported options were provided.
927    ensureNoUnsupportedOptions(options,
928         EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME);
929
930    return new EXTERNALBindRequest(controls);
931  }
932
933
934
935  /**
936   * Creates a SASL GSSAPI bind request using the provided password and set of
937   * options.
938   *
939   * @param  password           The password to use for the bind request.
940   * @param  promptForPassword  Indicates whether to interactively prompt for
941   *                            the password if one is needed but none was
942   *                            provided.
943   * @param  tool               The command-line tool whose input and output
944   *                            streams should be used when prompting for the
945   *                            bind password.  It may be {@code null} if
946   *                            {@code promptForPassword} is {@code false}.
947   * @param  options            The set of SASL options for the bind request.
948   * @param  controls           The set of controls to include in the request.
949   *
950   * @return  The SASL GSSAPI bind request that was created.
951   *
952   * @throws  LDAPException  If a problem is encountered while trying to create
953   *                         the SASL bind request.
954   */
955  private static GSSAPIBindRequest createGSSAPIBindRequest(
956                                        final byte[] password,
957                                        final boolean promptForPassword,
958                                        final CommandLineTool tool,
959                                        final Map<String,String> options,
960                                        final Control[] controls)
961          throws LDAPException
962  {
963    // The authID option is required.
964    final String authID =
965         options.remove(StaticUtils.toLowerCase(SASL_OPTION_AUTH_ID));
966    if (authID == null)
967    {
968      throw new LDAPException(ResultCode.PARAM_ERROR,
969           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_AUTH_ID,
970                GSSAPIBindRequest.GSSAPI_MECHANISM_NAME));
971    }
972    final GSSAPIBindRequestProperties gssapiProperties =
973         new GSSAPIBindRequestProperties(authID, password);
974
975    // The authzID option is optional.
976    gssapiProperties.setAuthorizationID(
977         options.remove(StaticUtils.toLowerCase(SASL_OPTION_AUTHZ_ID)));
978
979    // The configFile option is optional.
980    gssapiProperties.setConfigFilePath(options.remove(
981         StaticUtils.toLowerCase(SASL_OPTION_CONFIG_FILE)));
982
983    // The debug option is optional.
984    gssapiProperties.setEnableGSSAPIDebugging(getBooleanValue(options,
985         SASL_OPTION_DEBUG, false));
986
987    // The kdcAddress option is optional.
988    gssapiProperties.setKDCAddress(options.remove(
989         StaticUtils.toLowerCase(SASL_OPTION_KDC_ADDRESS)));
990
991    // The protocol option is optional.
992    final String protocol =
993         options.remove(StaticUtils.toLowerCase(SASL_OPTION_PROTOCOL));
994    if (protocol != null)
995    {
996      gssapiProperties.setServicePrincipalProtocol(protocol);
997    }
998
999    // The realm option is optional.
1000    gssapiProperties.setRealm(options.remove(
1001         StaticUtils.toLowerCase(SASL_OPTION_REALM)));
1002
1003    // The QoP option is optional, and may contain multiple values that need to
1004    // be parsed.
1005    final String qopString =
1006         options.remove(StaticUtils.toLowerCase(SASL_OPTION_QOP));
1007    if (qopString != null)
1008    {
1009      gssapiProperties.setAllowedQoP(
1010           SASLQualityOfProtection.decodeQoPList(qopString));
1011    }
1012
1013    // The renewTGT option is optional.
1014    gssapiProperties.setRenewTGT(getBooleanValue(options, SASL_OPTION_RENEW_TGT,
1015         false));
1016
1017    // The requireCache option is optional.
1018    gssapiProperties.setRequireCachedCredentials(getBooleanValue(options,
1019         SASL_OPTION_REQUIRE_CACHE, false));
1020
1021    // The ticketCache option is optional.
1022    gssapiProperties.setTicketCachePath(options.remove(
1023         StaticUtils.toLowerCase(SASL_OPTION_TICKET_CACHE_PATH)));
1024
1025    // The useTicketCache option is optional.
1026    gssapiProperties.setUseTicketCache(getBooleanValue(options,
1027         SASL_OPTION_USE_TICKET_CACHE, true));
1028
1029    // Ensure no unsupported options were provided.
1030    ensureNoUnsupportedOptions(options,
1031         GSSAPIBindRequest.GSSAPI_MECHANISM_NAME);
1032
1033    // A password must have been provided unless useTicketCache=true and
1034    // requireTicketCache=true.
1035    if (password == null)
1036    {
1037      if (! (gssapiProperties.useTicketCache() &&
1038           gssapiProperties.requireCachedCredentials()))
1039      {
1040        if (promptForPassword)
1041        {
1042          tool.getOriginalOut().print(INFO_LDAP_TOOL_ENTER_BIND_PASSWORD.get());
1043          gssapiProperties.setPassword(PasswordReader.readPassword());
1044          tool.getOriginalOut().println();
1045        }
1046        else
1047        {
1048          throw new LDAPException(ResultCode.PARAM_ERROR,
1049               ERR_SASL_OPTION_GSSAPI_PASSWORD_REQUIRED.get());
1050        }
1051      }
1052    }
1053
1054    return new GSSAPIBindRequest(gssapiProperties, controls);
1055  }
1056
1057
1058
1059  /**
1060   * Creates a SASL PLAIN bind request using the provided password and set of
1061   * options.
1062   *
1063   * @param  password           The password to use for the bind request.
1064   * @param  promptForPassword  Indicates whether to interactively prompt for
1065   *                            the password if one is needed but none was
1066   *                            provided.
1067   * @param  tool               The command-line tool whose input and output
1068   *                            streams should be used when prompting for the
1069   *                            bind password.  It may be {@code null} if
1070   *                            {@code promptForPassword} is {@code false}.
1071   * @param  options            The set of SASL options for the bind request.
1072   * @param  controls           The set of controls to include in the request.
1073   *
1074   * @return  The SASL PLAIN bind request that was created.
1075   *
1076   * @throws  LDAPException  If a problem is encountered while trying to create
1077   *                         the SASL bind request.
1078   */
1079  private static PLAINBindRequest createPLAINBindRequest(
1080                                        final byte[] password,
1081                                        final boolean promptForPassword,
1082                                        final CommandLineTool tool,
1083                                        final Map<String,String> options,
1084                                        final Control[] controls)
1085          throws LDAPException
1086  {
1087    final byte[] pw;
1088    if (password == null)
1089    {
1090      if (promptForPassword)
1091      {
1092        tool.getOriginalOut().print(INFO_LDAP_TOOL_ENTER_BIND_PASSWORD.get());
1093        pw = PasswordReader.readPassword();
1094        tool.getOriginalOut().println();
1095      }
1096      else
1097      {
1098        throw new LDAPException(ResultCode.PARAM_ERROR,
1099             ERR_SASL_OPTION_MECH_REQUIRES_PASSWORD.get(
1100                  CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME));
1101      }
1102    }
1103    else
1104    {
1105      pw = password;
1106    }
1107
1108    // The authID option is required.
1109    final String authID =
1110         options.remove(StaticUtils.toLowerCase(SASL_OPTION_AUTH_ID));
1111    if (authID == null)
1112    {
1113      throw new LDAPException(ResultCode.PARAM_ERROR,
1114           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_AUTH_ID,
1115                PLAINBindRequest.PLAIN_MECHANISM_NAME));
1116    }
1117
1118    // The authzID option is optional.
1119    final String authzID =
1120         options.remove(StaticUtils.toLowerCase(SASL_OPTION_AUTHZ_ID));
1121
1122    // Ensure no unsupported options were provided.
1123    ensureNoUnsupportedOptions(options,
1124         PLAINBindRequest.PLAIN_MECHANISM_NAME);
1125
1126    return new PLAINBindRequest(authID, authzID, pw, controls);
1127  }
1128
1129
1130
1131  /**
1132   * Creates a SASL UNBOUNDID-CERTIFICATE-PLUS-PASSWORD bind request using the
1133   * provided set of options.
1134   *
1135   * @param  password  The password to use for the bind request.
1136   * @param  tool      The command-line tool whose input and output streams
1137   *                   should be used when prompting for the bind password.  It
1138   *                   may be {@code null} if {@code promptForPassword} is
1139   *                   {@code false}.
1140   * @param  options   The set of SASL options for the bind request.
1141   * @param  controls  The set of controls to include in the request.
1142   *
1143   * @return  The SASL UNBOUNDID-CERTIFICATE-PLUS-PASSWORD bind request that was
1144   *          created.
1145   *
1146   * @throws  LDAPException  If a problem is encountered while trying to create
1147   *                         the SASL bind request.
1148   */
1149  private static UnboundIDCertificatePlusPasswordBindRequest
1150                      createUnboundIDCertificatePlusPasswordBindRequest(
1151                           final byte[] password, final CommandLineTool tool,
1152                           final Map<String,String> options,
1153                           final Control[] controls)
1154          throws LDAPException
1155  {
1156    final byte[] pw;
1157    if (password == null)
1158    {
1159      tool.getOriginalOut().print(INFO_LDAP_TOOL_ENTER_BIND_PASSWORD.get());
1160      pw = PasswordReader.readPassword();
1161      tool.getOriginalOut().println();
1162    }
1163    else
1164    {
1165      pw = password;
1166    }
1167
1168    // Ensure no unsupported options were provided.
1169    ensureNoUnsupportedOptions(options,
1170         UnboundIDCertificatePlusPasswordBindRequest.
1171              UNBOUNDID_CERT_PLUS_PW_MECHANISM_NAME);
1172
1173    return new UnboundIDCertificatePlusPasswordBindRequest(pw, controls);
1174  }
1175
1176
1177
1178  /**
1179   * Creates a SASL UNBOUNDID-DELIVERED-OTP bind request using the provided
1180   * password and set of options.
1181   *
1182   * @param  password  The password to use for the bind request.
1183   * @param  options   The set of SASL options for the bind request.
1184   * @param  controls  The set of controls to include in the request.
1185   *
1186   * @return  The SASL UNBOUNDID-DELIVERED-OTP bind request that was created.
1187   *
1188   * @throws  LDAPException  If a problem is encountered while trying to create
1189   *                         the SASL bind request.
1190   */
1191  private static UnboundIDDeliveredOTPBindRequest
1192                      createUNBOUNDIDDeliveredOTPBindRequest(
1193                           final byte[] password,
1194                           final Map<String,String> options,
1195                           final Control... controls)
1196          throws LDAPException
1197  {
1198    if (password != null)
1199    {
1200      throw new LDAPException(ResultCode.PARAM_ERROR,
1201           ERR_SASL_OPTION_MECH_DOESNT_ACCEPT_PASSWORD.get(
1202                UnboundIDDeliveredOTPBindRequest.
1203                     UNBOUNDID_DELIVERED_OTP_MECHANISM_NAME));
1204    }
1205
1206    // The authID option is required.
1207    final String authID =
1208         options.remove(StaticUtils.toLowerCase(SASL_OPTION_AUTH_ID));
1209    if (authID == null)
1210    {
1211      throw new LDAPException(ResultCode.PARAM_ERROR,
1212           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_AUTH_ID,
1213                UnboundIDDeliveredOTPBindRequest.
1214                     UNBOUNDID_DELIVERED_OTP_MECHANISM_NAME));
1215    }
1216
1217    // The OTP option is required.
1218    final String otp = options.remove(StaticUtils.toLowerCase(SASL_OPTION_OTP));
1219    if (otp == null)
1220    {
1221      throw new LDAPException(ResultCode.PARAM_ERROR,
1222           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_OTP,
1223                UnboundIDDeliveredOTPBindRequest.
1224                     UNBOUNDID_DELIVERED_OTP_MECHANISM_NAME));
1225    }
1226
1227    // The authzID option is optional.
1228    final String authzID =
1229         options.remove(StaticUtils.toLowerCase(SASL_OPTION_AUTHZ_ID));
1230
1231    // Ensure no unsupported options were provided.
1232    ensureNoUnsupportedOptions(options,
1233         UnboundIDDeliveredOTPBindRequest.
1234              UNBOUNDID_DELIVERED_OTP_MECHANISM_NAME);
1235
1236    return new UnboundIDDeliveredOTPBindRequest(authID, authzID, otp, controls);
1237  }
1238
1239
1240
1241  /**
1242   * Creates a SASL UNBOUNDID-TOTP bind request using the provided password and
1243   * set of options.
1244   *
1245   * @param  password  The password to use for the bind request.
1246   * @param  tool      The command-line tool whose input and output streams
1247   *                   should be used when prompting for the bind password.  It
1248   *                   may be {@code null} if {@code promptForPassword} is
1249   *                   {@code false}.
1250   * @param  options   The set of SASL options for the bind request.
1251   * @param  controls  The set of controls to include in the request.
1252   *
1253   * @return  The SASL UNBOUNDID-TOTP bind request that was created.
1254   *
1255   * @throws  LDAPException  If a problem is encountered while trying to create
1256   *                         the SASL bind request.
1257   */
1258  private static SingleUseTOTPBindRequest createUNBOUNDIDTOTPBindRequest(
1259                                               final byte[] password,
1260                                               final CommandLineTool tool,
1261                                               final Map<String,String> options,
1262                                               final Control... controls)
1263          throws LDAPException
1264  {
1265    // The authID option is required.
1266    final String authID =
1267         options.remove(StaticUtils.toLowerCase(SASL_OPTION_AUTH_ID));
1268    if (authID == null)
1269    {
1270      throw new LDAPException(ResultCode.PARAM_ERROR,
1271           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_AUTH_ID,
1272                UnboundIDTOTPBindRequest.UNBOUNDID_TOTP_MECHANISM_NAME));
1273    }
1274
1275    // The TOTP password option is required.
1276    final String totpPassword =
1277         options.remove(StaticUtils.toLowerCase(SASL_OPTION_TOTP_PASSWORD));
1278    if (totpPassword == null)
1279    {
1280      throw new LDAPException(ResultCode.PARAM_ERROR,
1281           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_TOTP_PASSWORD,
1282                UnboundIDTOTPBindRequest.UNBOUNDID_TOTP_MECHANISM_NAME));
1283    }
1284
1285    // The authzID option is optional.
1286    byte[] pwBytes = password;
1287    final String authzID =
1288         options.remove(StaticUtils.toLowerCase(SASL_OPTION_AUTHZ_ID));
1289
1290    // The promptForStaticPassword option is optional.
1291    final String promptStr = options.remove(StaticUtils.toLowerCase(
1292         SASL_OPTION_PROMPT_FOR_STATIC_PW));
1293    if (promptStr != null)
1294    {
1295      if (promptStr.equalsIgnoreCase("true"))
1296      {
1297        if (pwBytes == null)
1298        {
1299          tool.getOriginalOut().print(INFO_SASL_ENTER_STATIC_PW.get());
1300          pwBytes = PasswordReader.readPassword();
1301          tool.getOriginalOut().println();
1302        }
1303        else
1304        {
1305          throw new LDAPException(ResultCode.PARAM_ERROR,
1306               ERR_SASL_PROMPT_FOR_PROVIDED_PW.get(
1307                    SASL_OPTION_PROMPT_FOR_STATIC_PW));
1308        }
1309      }
1310      else if (! promptStr.equalsIgnoreCase("false"))
1311      {
1312        throw new LDAPException(ResultCode.PARAM_ERROR,
1313             ERR_SASL_PROMPT_FOR_STATIC_PW_BAD_VALUE.get(
1314                  SASL_OPTION_PROMPT_FOR_STATIC_PW));
1315      }
1316    }
1317
1318    // Ensure no unsupported options were provided.
1319    ensureNoUnsupportedOptions(options,
1320         UnboundIDTOTPBindRequest.UNBOUNDID_TOTP_MECHANISM_NAME);
1321
1322    return new SingleUseTOTPBindRequest(authID, authzID, totpPassword, pwBytes,
1323         controls);
1324  }
1325
1326
1327
1328  /**
1329   * Creates a SASL UNBOUNDID-YUBIKEY-OTP bind request using the provided
1330   * password and set of options.
1331   *
1332   * @param  password  The password to use for the bind request.
1333   * @param  tool      The command-line tool whose input and output streams
1334   *                   should be used when prompting for the bind password.  It
1335   *                   may be {@code null} if {@code promptForPassword} is
1336   *                   {@code false}.
1337   * @param  options   The set of SASL options for the bind request.
1338   * @param  controls  The set of controls to include in the request.
1339   *
1340   * @return  The SASL UNBOUNDID-YUBIKEY-OTP bind request that was created.
1341   *
1342   * @throws  LDAPException  If a problem is encountered while trying to create
1343   *                         the SASL bind request.
1344   */
1345  private static UnboundIDYubiKeyOTPBindRequest
1346                      createUNBOUNDIDYUBIKEYOTPBindRequest(
1347                           final byte[] password, final CommandLineTool tool,
1348                           final Map<String,String> options,
1349                           final Control... controls)
1350          throws LDAPException
1351  {
1352    // The authID option is required.
1353    final String authID =
1354         options.remove(StaticUtils.toLowerCase(SASL_OPTION_AUTH_ID));
1355    if (authID == null)
1356    {
1357      throw new LDAPException(ResultCode.PARAM_ERROR,
1358           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_AUTH_ID,
1359                UnboundIDYubiKeyOTPBindRequest.
1360                     UNBOUNDID_YUBIKEY_OTP_MECHANISM_NAME));
1361    }
1362
1363    // The otp option is required.
1364    final String otp = options.remove(StaticUtils.toLowerCase(SASL_OPTION_OTP));
1365    if (otp == null)
1366    {
1367      throw new LDAPException(ResultCode.PARAM_ERROR,
1368           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_OTP,
1369                UnboundIDYubiKeyOTPBindRequest.
1370                     UNBOUNDID_YUBIKEY_OTP_MECHANISM_NAME));
1371    }
1372
1373    // The authzID option is optional.
1374    final String authzID =
1375         options.remove(StaticUtils.toLowerCase(SASL_OPTION_AUTHZ_ID));
1376
1377    // The promptForStaticPassword option is optional.
1378    byte[] pwBytes = password;
1379    final String promptStr = options.remove(StaticUtils.toLowerCase(
1380         SASL_OPTION_PROMPT_FOR_STATIC_PW));
1381    if (promptStr != null)
1382    {
1383      if (promptStr.equalsIgnoreCase("true"))
1384      {
1385        if (pwBytes == null)
1386        {
1387          tool.getOriginalOut().print(INFO_SASL_ENTER_STATIC_PW.get());
1388          pwBytes = PasswordReader.readPassword();
1389          tool.getOriginalOut().println();
1390        }
1391        else
1392        {
1393          throw new LDAPException(ResultCode.PARAM_ERROR,
1394               ERR_SASL_PROMPT_FOR_PROVIDED_PW.get(
1395                    SASL_OPTION_PROMPT_FOR_STATIC_PW));
1396        }
1397      }
1398      else if (! promptStr.equalsIgnoreCase("false"))
1399      {
1400        throw new LDAPException(ResultCode.PARAM_ERROR,
1401             ERR_SASL_PROMPT_FOR_STATIC_PW_BAD_VALUE.get(
1402                  SASL_OPTION_PROMPT_FOR_STATIC_PW));
1403      }
1404    }
1405
1406    // Ensure no unsupported options were provided.
1407    ensureNoUnsupportedOptions(options,
1408         UnboundIDYubiKeyOTPBindRequest.UNBOUNDID_YUBIKEY_OTP_MECHANISM_NAME);
1409
1410    return new UnboundIDYubiKeyOTPBindRequest(authID, authzID, pwBytes, otp,
1411         controls);
1412  }
1413
1414
1415
1416  /**
1417   * Parses the provided list of SASL options.
1418   *
1419   * @param  options  The list of options to be parsed.
1420   *
1421   * @return  A map with the parsed set of options.
1422   *
1423   * @throws  LDAPException  If a problem is encountered while parsing options.
1424   */
1425  private static Map<String,String>
1426                      parseOptions(final List<String> options)
1427          throws LDAPException
1428  {
1429    if (options == null)
1430    {
1431      return new HashMap<>(0);
1432    }
1433
1434    final HashMap<String,String> m =
1435         new HashMap<>(StaticUtils.computeMapCapacity(options.size()));
1436    for (final String s : options)
1437    {
1438      final int equalPos = s.indexOf('=');
1439      if (equalPos < 0)
1440      {
1441        throw new LDAPException(ResultCode.PARAM_ERROR,
1442             ERR_SASL_OPTION_MISSING_EQUAL.get(s));
1443      }
1444      else if (equalPos == 0)
1445      {
1446        throw new LDAPException(ResultCode.PARAM_ERROR,
1447             ERR_SASL_OPTION_STARTS_WITH_EQUAL.get(s));
1448      }
1449
1450      final String name = s.substring(0, equalPos);
1451      final String value = s.substring(equalPos + 1);
1452      if (m.put(StaticUtils.toLowerCase(name), value) != null)
1453      {
1454        throw new LDAPException(ResultCode.PARAM_ERROR,
1455             ERR_SASL_OPTION_NOT_MULTI_VALUED.get(name));
1456      }
1457    }
1458
1459    return m;
1460  }
1461
1462
1463
1464  /**
1465   * Ensures that the provided map is empty, and will throw an exception if it
1466   * isn't.  This method is intended for internal use only.
1467   *
1468   * @param  options    The map of options to ensure is empty.
1469   * @param  mechanism  The associated SASL mechanism.
1470   *
1471   * @throws  LDAPException  If the map of SASL options is not empty.
1472   */
1473  @InternalUseOnly()
1474  public static void ensureNoUnsupportedOptions(
1475                          final Map<String,String> options,
1476                          final String mechanism)
1477          throws LDAPException
1478  {
1479    if (! options.isEmpty())
1480    {
1481      for (final String s : options.keySet())
1482      {
1483        throw new LDAPException(ResultCode.PARAM_ERROR,
1484             ERR_SASL_OPTION_UNSUPPORTED_FOR_MECH.get(s,mechanism));
1485      }
1486    }
1487  }
1488
1489
1490
1491  /**
1492   * Retrieves the value of the specified option and parses it as a boolean.
1493   * Values of "true", "t", "yes", "y", "on", and "1" will be treated as
1494   * {@code true}.  Values of "false", "f", "no", "n", "off", and "0" will be
1495   * treated as {@code false}.
1496   *
1497   * @param  m  The map from which to retrieve the option.  It must not be
1498   *            {@code null}.
1499   * @param  o  The name of the option to examine.
1500   * @param  d  The default value to use if the given option was not provided.
1501   *
1502   * @return  The parsed boolean value.
1503   *
1504   * @throws  LDAPException  If the option value cannot be parsed as a boolean.
1505   */
1506  static boolean getBooleanValue(final Map<String,String> m, final String o,
1507                                 final boolean d)
1508         throws LDAPException
1509  {
1510    final String s =
1511         StaticUtils.toLowerCase(m.remove(StaticUtils.toLowerCase(o)));
1512    if (s == null)
1513    {
1514      return d;
1515    }
1516    else if (s.equals("true") ||
1517             s.equals("t") ||
1518             s.equals("yes") ||
1519             s.equals("y") ||
1520             s.equals("on") ||
1521             s.equals("1"))
1522    {
1523      return true;
1524    }
1525    else if (s.equals("false") ||
1526             s.equals("f") ||
1527             s.equals("no") ||
1528             s.equals("n") ||
1529             s.equals("off") ||
1530             s.equals("0"))
1531    {
1532      return false;
1533    }
1534    else
1535    {
1536      throw new LDAPException(ResultCode.PARAM_ERROR,
1537           ERR_SASL_OPTION_MALFORMED_BOOLEAN_VALUE.get(o));
1538    }
1539  }
1540
1541
1542
1543  /**
1544   * Retrieves a string representation of the SASL usage information.  This will
1545   * include the supported SASL mechanisms and the properties that may be used
1546   * with each.
1547   *
1548   * @param  maxWidth  The maximum line width to use for the output.  If this is
1549   *                   less than or equal to zero, then no wrapping will be
1550   *                   performed.
1551   *
1552   * @return  A string representation of the usage information
1553   */
1554  public static String getUsageString(final int maxWidth)
1555  {
1556    final StringBuilder buffer = new StringBuilder();
1557
1558    for (final String line : getUsage(maxWidth))
1559    {
1560      buffer.append(line);
1561      buffer.append(StaticUtils.EOL);
1562    }
1563
1564    return buffer.toString();
1565  }
1566
1567
1568
1569  /**
1570   * Retrieves lines that make up the SASL usage information, optionally
1571   * wrapping long lines.
1572   *
1573   * @param  maxWidth  The maximum line width to use for the output.  If this is
1574   *                   less than or equal to zero, then no wrapping will be
1575   *                   performed.
1576   *
1577   * @return  The lines that make up the SASL usage information.
1578   */
1579  public static List<String> getUsage(final int maxWidth)
1580  {
1581    final ArrayList<String> lines = new ArrayList<>(100);
1582
1583    boolean first = true;
1584    for (final SASLMechanismInfo i : getSupportedSASLMechanisms())
1585    {
1586      if (first)
1587      {
1588        first = false;
1589      }
1590      else
1591      {
1592        lines.add("");
1593        lines.add("");
1594      }
1595
1596      lines.addAll(
1597           StaticUtils.wrapLine(INFO_SASL_HELP_MECHANISM.get(i.getName()),
1598                maxWidth));
1599      lines.add("");
1600
1601      for (final String line :
1602           StaticUtils.wrapLine(i.getDescription(), maxWidth - 4))
1603      {
1604        lines.add("  " + line);
1605      }
1606      lines.add("");
1607
1608      for (final String line :
1609           StaticUtils.wrapLine(INFO_SASL_HELP_MECHANISM_OPTIONS.get(
1610                i.getName()), maxWidth - 4))
1611      {
1612        lines.add("  " + line);
1613      }
1614
1615      if (i.acceptsPassword())
1616      {
1617        lines.add("");
1618        if (i.requiresPassword())
1619        {
1620          for (final String line :
1621               StaticUtils.wrapLine(INFO_SASL_HELP_PASSWORD_REQUIRED.get(
1622                    i.getName()), maxWidth - 4))
1623          {
1624            lines.add("  " + line);
1625          }
1626        }
1627        else
1628        {
1629          for (final String line :
1630               StaticUtils.wrapLine(INFO_SASL_HELP_PASSWORD_OPTIONAL.get(
1631                    i.getName()), maxWidth - 4))
1632          {
1633            lines.add("  " + line);
1634          }
1635        }
1636      }
1637
1638      for (final SASLOption o : i.getOptions())
1639      {
1640        lines.add("");
1641        lines.add("  * " + o.getName());
1642        for (final String line :
1643             StaticUtils.wrapLine(o.getDescription(), maxWidth - 14))
1644        {
1645          lines.add("       " + line);
1646        }
1647      }
1648    }
1649
1650    return lines;
1651  }
1652}