1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.catalina.realm;
19
20 import java.io.IOException;
21 import java.security.Principal;
22 import java.text.MessageFormat;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.Hashtable;
26 import java.util.List;
27
28 import javax.naming.Context;
29 import javax.naming.CommunicationException;
30 import javax.naming.CompositeName;
31 import javax.naming.InvalidNameException;
32 import javax.naming.NameNotFoundException;
33 import javax.naming.NamingEnumeration;
34 import javax.naming.NamingException;
35 import javax.naming.NameParser;
36 import javax.naming.Name;
37 import javax.naming.AuthenticationException;
38 import javax.naming.ServiceUnavailableException;
39 import javax.naming.directory.Attribute;
40 import javax.naming.directory.Attributes;
41 import javax.naming.directory.DirContext;
42 import javax.naming.directory.InitialDirContext;
43 import javax.naming.directory.SearchControls;
44 import javax.naming.directory.SearchResult;
45 import org.apache.catalina.LifecycleException;
46 import org.apache.catalina.util.Base64;
47 import org.apache.tomcat.util.buf.ByteChunk;
48 import org.apache.tomcat.util.buf.CharChunk;
49
50 /**
51 * <p>Implementation of <strong>Realm</strong> that works with a directory
52 * server accessed via the Java Naming and Directory Interface (JNDI) APIs.
53 * The following constraints are imposed on the data structure in the
54 * underlying directory server:</p>
55 * <ul>
56 *
57 * <li>Each user that can be authenticated is represented by an individual
58 * element in the top level <code>DirContext</code> that is accessed
59 * via the <code>connectionURL</code> property.</li>
60 *
61 * <li>If a socket connection can not be made to the <code>connectURL</code>
62 * an attempt will be made to use the <code>alternateURL</code> if it
63 * exists.</li>
64 *
65 * <li>Each user element has a distinguished name that can be formed by
66 * substituting the presented username into a pattern configured by the
67 * <code>userPattern</code> property.</li>
68 *
69 * <li>Alternatively, if the <code>userPattern</code> property is not
70 * specified, a unique element can be located by searching the directory
71 * context. In this case:
72 * <ul>
73 * <li>The <code>userSearch</code> pattern specifies the search filter
74 * after substitution of the username.</li>
75 * <li>The <code>userBase</code> property can be set to the element that
76 * is the base of the subtree containing users. If not specified,
77 * the search base is the top-level context.</li>
78 * <li>The <code>userSubtree</code> property can be set to
79 * <code>true</code> if you wish to search the entire subtree of the
80 * directory context. The default value of <code>false</code>
81 * requests a search of only the current level.</li>
82 * </ul>
83 * </li>
84 *
85 * <li>The user may be authenticated by binding to the directory with the
86 * username and password presented. This method is used when the
87 * <code>userPassword</code> property is not specified.</li>
88 *
89 * <li>The user may be authenticated by retrieving the value of an attribute
90 * from the directory and comparing it explicitly with the value presented
91 * by the user. This method is used when the <code>userPassword</code>
92 * property is specified, in which case:
93 * <ul>
94 * <li>The element for this user must contain an attribute named by the
95 * <code>userPassword</code> property.
96 * <li>The value of the user password attribute is either a cleartext
97 * String, or the result of passing a cleartext String through the
98 * <code>RealmBase.digest()</code> method (using the standard digest
99 * support included in <code>RealmBase</code>).
100 * <li>The user is considered to be authenticated if the presented
101 * credentials (after being passed through
102 * <code>RealmBase.digest()</code>) are equal to the retrieved value
103 * for the user password attribute.</li>
104 * </ul></li>
105 *
106 * <li>Each group of users that has been assigned a particular role may be
107 * represented by an individual element in the top level
108 * <code>DirContext</code> that is accessed via the
109 * <code>connectionURL</code> property. This element has the following
110 * characteristics:
111 * <ul>
112 * <li>The set of all possible groups of interest can be selected by a
113 * search pattern configured by the <code>roleSearch</code>
114 * property.</li>
115 * <li>The <code>roleSearch</code> pattern optionally includes pattern
116 * replacements "{0}" for the distinguished name, and/or "{1}" for
117 * the username, of the authenticated user for which roles will be
118 * retrieved.</li>
119 * <li>The <code>roleBase</code> property can be set to the element that
120 * is the base of the search for matching roles. If not specified,
121 * the entire context will be searched.</li>
122 * <li>The <code>roleSubtree</code> property can be set to
123 * <code>true</code> if you wish to search the entire subtree of the
124 * directory context. The default value of <code>false</code>
125 * requests a search of only the current level.</li>
126 * <li>The element includes an attribute (whose name is configured by
127 * the <code>roleName</code> property) containing the name of the
128 * role represented by this element.</li>
129 * </ul></li>
130 *
131 * <li>In addition, roles may be represented by the values of an attribute
132 * in the user's element whose name is configured by the
133 * <code>userRoleName</code> property.</li>
134 *
135 * <li>Note that the standard <code><security-role-ref></code> element in
136 * the web application deployment descriptor allows applications to refer
137 * to roles programmatically by names other than those used in the
138 * directory server itself.</li>
139 * </ul>
140 *
141 * <p><strong>TODO</strong> - Support connection pooling (including message
142 * format objects) so that <code>authenticate()</code> does not have to be
143 * synchronized.</p>
144 *
145 * <p><strong>WARNING</strong> - There is a reported bug against the Netscape
146 * provider code (com.netscape.jndi.ldap.LdapContextFactory) with respect to
147 * successfully authenticated a non-existing user. The
148 * report is here: http://issues.apache.org/bugzilla/show_bug.cgi?id=11210 .
149 * With luck, Netscape has updated their provider code and this is not an
150 * issue. </p>
151 *
152 * @author John Holman
153 * @author Craig R. McClanahan
154 * @version $Revision: 572859 $ $Date: 2007-09-05 04:14:45 +0200 (mer., 05 sept. 2007) $
155 */
156
157 public class JNDIRealm extends RealmBase {
158
159
160 // ----------------------------------------------------- Instance Variables
161
162 /**
163 * The type of authentication to use
164 */
165 protected String authentication = null;
166
167 /**
168 * The connection username for the server we will contact.
169 */
170 protected String connectionName = null;
171
172
173 /**
174 * The connection password for the server we will contact.
175 */
176 protected String connectionPassword = null;
177
178
179 /**
180 * The connection URL for the server we will contact.
181 */
182 protected String connectionURL = null;
183
184
185 /**
186 * The directory context linking us to our directory server.
187 */
188 protected DirContext context = null;
189
190
191 /**
192 * The JNDI context factory used to acquire our InitialContext. By
193 * default, assumes use of an LDAP server using the standard JNDI LDAP
194 * provider.
195 */
196 protected String contextFactory = "com.sun.jndi.ldap.LdapCtxFactory";
197
198
199 /**
200 * How aliases should be dereferenced during search operations.
201 */
202 protected String derefAliases = null;
203
204 /**
205 * Constant that holds the name of the environment property for specifying
206 * the manner in which aliases should be dereferenced.
207 */
208 public final static String DEREF_ALIASES = "java.naming.ldap.derefAliases";
209
210 /**
211 * Descriptive information about this Realm implementation.
212 */
213 protected static final String info =
214 "org.apache.catalina.realm.JNDIRealm/1.0";
215
216
217 /**
218 * Descriptive information about this Realm implementation.
219 */
220 protected static final String name = "JNDIRealm";
221
222
223 /**
224 * The protocol that will be used in the communication with the
225 * directory server.
226 */
227 protected String protocol = null;
228
229
230 /**
231 * How should we handle referrals? Microsoft Active Directory can't handle
232 * the default case, so an application authenticating against AD must
233 * set referrals to "follow".
234 */
235 protected String referrals = null;
236
237
238 /**
239 * The base element for user searches.
240 */
241 protected String userBase = "";
242
243
244 /**
245 * The message format used to search for a user, with "{0}" marking
246 * the spot where the username goes.
247 */
248 protected String userSearch = null;
249
250
251 /**
252 * The MessageFormat object associated with the current
253 * <code>userSearch</code>.
254 */
255 protected MessageFormat userSearchFormat = null;
256
257
258 /**
259 * Should we search the entire subtree for matching users?
260 */
261 protected boolean userSubtree = false;
262
263
264 /**
265 * The attribute name used to retrieve the user password.
266 */
267 protected String userPassword = null;
268
269
270 /**
271 * A string of LDAP user patterns or paths, ":"-separated
272 * These will be used to form the distinguished name of a
273 * user, with "{0}" marking the spot where the specified username
274 * goes.
275 * This is similar to userPattern, but allows for multiple searches
276 * for a user.
277 */
278 protected String[] userPatternArray = null;
279
280
281 /**
282 * The message format used to form the distinguished name of a
283 * user, with "{0}" marking the spot where the specified username
284 * goes.
285 */
286 protected String userPattern = null;
287
288
289 /**
290 * An array of MessageFormat objects associated with the current
291 * <code>userPatternArray</code>.
292 */
293 protected MessageFormat[] userPatternFormatArray = null;
294
295
296 /**
297 * The base element for role searches.
298 */
299 protected String roleBase = "";
300
301
302 /**
303 * The MessageFormat object associated with the current
304 * <code>roleSearch</code>.
305 */
306 protected MessageFormat roleFormat = null;
307
308
309 /**
310 * The name of an attribute in the user's entry containing
311 * roles for that user
312 */
313 protected String userRoleName = null;
314
315
316 /**
317 * The name of the attribute containing roles held elsewhere
318 */
319 protected String roleName = null;
320
321
322 /**
323 * The message format used to select roles for a user, with "{0}" marking
324 * the spot where the distinguished name of the user goes.
325 */
326 protected String roleSearch = null;
327
328
329 /**
330 * Should we search the entire subtree for matching memberships?
331 */
332 protected boolean roleSubtree = false;
333
334 /**
335 * An alternate URL, to which, we should connect if connectionURL fails.
336 */
337 protected String alternateURL;
338
339 /**
340 * The number of connection attempts. If greater than zero we use the
341 * alternate url.
342 */
343 protected int connectionAttempt = 0;
344
345 /**
346 * The current user pattern to be used for lookup and binding of a user.
347 */
348 protected int curUserPattern = 0;
349
350 // ------------------------------------------------------------- Properties
351
352 /**
353 * Return the type of authentication to use.
354 */
355 public String getAuthentication() {
356
357 return authentication;
358
359 }
360
361 /**
362 * Set the type of authentication to use.
363 *
364 * @param authentication The authentication
365 */
366 public void setAuthentication(String authentication) {
367
368 this.authentication = authentication;
369
370 }
371
372 /**
373 * Return the connection username for this Realm.
374 */
375 public String getConnectionName() {
376
377 return (this.connectionName);
378
379 }
380
381
382 /**
383 * Set the connection username for this Realm.
384 *
385 * @param connectionName The new connection username
386 */
387 public void setConnectionName(String connectionName) {
388
389 this.connectionName = connectionName;
390
391 }
392
393
394 /**
395 * Return the connection password for this Realm.
396 */
397 public String getConnectionPassword() {
398
399 return (this.connectionPassword);
400
401 }
402
403
404 /**
405 * Set the connection password for this Realm.
406 *
407 * @param connectionPassword The new connection password
408 */
409 public void setConnectionPassword(String connectionPassword) {
410
411 this.connectionPassword = connectionPassword;
412
413 }
414
415
416 /**
417 * Return the connection URL for this Realm.
418 */
419 public String getConnectionURL() {
420
421 return (this.connectionURL);
422
423 }
424
425
426 /**
427 * Set the connection URL for this Realm.
428 *
429 * @param connectionURL The new connection URL
430 */
431 public void setConnectionURL(String connectionURL) {
432
433 this.connectionURL = connectionURL;
434
435 }
436
437
438 /**
439 * Return the JNDI context factory for this Realm.
440 */
441 public String getContextFactory() {
442
443 return (this.contextFactory);
444
445 }
446
447
448 /**
449 * Set the JNDI context factory for this Realm.
450 *
451 * @param contextFactory The new context factory
452 */
453 public void setContextFactory(String contextFactory) {
454
455 this.contextFactory = contextFactory;
456
457 }
458
459 /**
460 * Return the derefAliases setting to be used.
461 */
462 public java.lang.String getDerefAliases() {
463 return derefAliases;
464 }
465
466 /**
467 * Set the value for derefAliases to be used when searching the directory.
468 *
469 * @param derefAliases New value of property derefAliases.
470 */
471 public void setDerefAliases(java.lang.String derefAliases) {
472 this.derefAliases = derefAliases;
473 }
474
475 /**
476 * Return the protocol to be used.
477 */
478 public String getProtocol() {
479
480 return protocol;
481
482 }
483
484 /**
485 * Set the protocol for this Realm.
486 *
487 * @param protocol The new protocol.
488 */
489 public void setProtocol(String protocol) {
490
491 this.protocol = protocol;
492
493 }
494
495
496 /**
497 * Returns the current settings for handling JNDI referrals.
498 */
499 public String getReferrals () {
500 return referrals;
501 }
502
503
504 /**
505 * How do we handle JNDI referrals? ignore, follow, or throw
506 * (see javax.naming.Context.REFERRAL for more information).
507 */
508 public void setReferrals (String referrals) {
509 this.referrals = referrals;
510 }
511
512
513 /**
514 * Return the base element for user searches.
515 */
516 public String getUserBase() {
517
518 return (this.userBase);
519
520 }
521
522
523 /**
524 * Set the base element for user searches.
525 *
526 * @param userBase The new base element
527 */
528 public void setUserBase(String userBase) {
529
530 this.userBase = userBase;
531
532 }
533
534
535 /**
536 * Return the message format pattern for selecting users in this Realm.
537 */
538 public String getUserSearch() {
539
540 return (this.userSearch);
541
542 }
543
544
545 /**
546 * Set the message format pattern for selecting users in this Realm.
547 *
548 * @param userSearch The new user search pattern
549 */
550 public void setUserSearch(String userSearch) {
551
552 this.userSearch = userSearch;
553 if (userSearch == null)
554 userSearchFormat = null;
555 else
556 userSearchFormat = new MessageFormat(userSearch);
557
558 }
559
560
561 /**
562 * Return the "search subtree for users" flag.
563 */
564 public boolean getUserSubtree() {
565
566 return (this.userSubtree);
567
568 }
569
570
571 /**
572 * Set the "search subtree for users" flag.
573 *
574 * @param userSubtree The new search flag
575 */
576 public void setUserSubtree(boolean userSubtree) {
577
578 this.userSubtree = userSubtree;
579
580 }
581
582
583 /**
584 * Return the user role name attribute name for this Realm.
585 */
586 public String getUserRoleName() {
587
588 return userRoleName;
589 }
590
591
592 /**
593 * Set the user role name attribute name for this Realm.
594 *
595 * @param userRoleName The new userRole name attribute name
596 */
597 public void setUserRoleName(String userRoleName) {
598
599 this.userRoleName = userRoleName;
600
601 }
602
603
604 /**
605 * Return the base element for role searches.
606 */
607 public String getRoleBase() {
608
609 return (this.roleBase);
610
611 }
612
613
614 /**
615 * Set the base element for role searches.
616 *
617 * @param roleBase The new base element
618 */
619 public void setRoleBase(String roleBase) {
620
621 this.roleBase = roleBase;
622
623 }
624
625
626 /**
627 * Return the role name attribute name for this Realm.
628 */
629 public String getRoleName() {
630
631 return (this.roleName);
632
633 }
634
635
636 /**
637 * Set the role name attribute name for this Realm.
638 *
639 * @param roleName The new role name attribute name
640 */
641 public void setRoleName(String roleName) {
642
643 this.roleName = roleName;
644
645 }
646
647
648 /**
649 * Return the message format pattern for selecting roles in this Realm.
650 */
651 public String getRoleSearch() {
652
653 return (this.roleSearch);
654
655 }
656
657
658 /**
659 * Set the message format pattern for selecting roles in this Realm.
660 *
661 * @param roleSearch The new role search pattern
662 */
663 public void setRoleSearch(String roleSearch) {
664
665 this.roleSearch = roleSearch;
666 if (roleSearch == null)
667 roleFormat = null;
668 else
669 roleFormat = new MessageFormat(roleSearch);
670
671 }
672
673
674 /**
675 * Return the "search subtree for roles" flag.
676 */
677 public boolean getRoleSubtree() {
678
679 return (this.roleSubtree);
680
681 }
682
683
684 /**
685 * Set the "search subtree for roles" flag.
686 *
687 * @param roleSubtree The new search flag
688 */
689 public void setRoleSubtree(boolean roleSubtree) {
690
691 this.roleSubtree = roleSubtree;
692
693 }
694
695
696 /**
697 * Return the password attribute used to retrieve the user password.
698 */
699 public String getUserPassword() {
700
701 return (this.userPassword);
702
703 }
704
705
706 /**
707 * Set the password attribute used to retrieve the user password.
708 *
709 * @param userPassword The new password attribute
710 */
711 public void setUserPassword(String userPassword) {
712
713 this.userPassword = userPassword;
714
715 }
716
717
718 /**
719 * Return the message format pattern for selecting users in this Realm.
720 */
721 public String getUserPattern() {
722
723 return (this.userPattern);
724
725 }
726
727
728 /**
729 * Set the message format pattern for selecting users in this Realm.
730 * This may be one simple pattern, or multiple patterns to be tried,
731 * separated by parentheses. (for example, either "cn={0}", or
732 * "(cn={0})(cn={0},o=myorg)" Full LDAP search strings are also supported,
733 * but only the "OR", "|" syntax, so "(|(cn={0})(cn={0},o=myorg))" is
734 * also valid. Complex search strings with &, etc are NOT supported.
735 *
736 * @param userPattern The new user pattern
737 */
738 public void setUserPattern(String userPattern) {
739
740 this.userPattern = userPattern;
741 if (userPattern == null)
742 userPatternArray = null;
743 else {
744 userPatternArray = parseUserPatternString(userPattern);
745 int len = this.userPatternArray.length;
746 userPatternFormatArray = new MessageFormat[len];
747 for (int i=0; i < len; i++) {
748 userPatternFormatArray[i] =
749 new MessageFormat(userPatternArray[i]);
750 }
751 }
752 }
753
754
755 /**
756 * Getter for property alternateURL.
757 *
758 * @return Value of property alternateURL.
759 */
760 public String getAlternateURL() {
761
762 return this.alternateURL;
763
764 }
765
766
767 /**
768 * Setter for property alternateURL.
769 *
770 * @param alternateURL New value of property alternateURL.
771 */
772 public void setAlternateURL(String alternateURL) {
773
774 this.alternateURL = alternateURL;
775
776 }
777
778
779 // ---------------------------------------------------------- Realm Methods
780
781
782 /**
783 * Return the Principal associated with the specified username and
784 * credentials, if there is one; otherwise return <code>null</code>.
785 *
786 * If there are any errors with the JDBC connection, executing
787 * the query or anything we return null (don't authenticate). This
788 * event is also logged, and the connection will be closed so that
789 * a subsequent request will automatically re-open it.
790 *
791 * @param username Username of the Principal to look up
792 * @param credentials Password or other credentials to use in
793 * authenticating this username
794 */
795 public Principal authenticate(String username, String credentials) {
796
797 DirContext context = null;
798 Principal principal = null;
799
800 try {
801
802 // Ensure that we have a directory context available
803 context = open();
804
805 // Occassionally the directory context will timeout. Try one more
806 // time before giving up.
807 try {
808
809 // Authenticate the specified username if possible
810 principal = authenticate(context, username, credentials);
811
812 } catch (NullPointerException e) {
813 /* BZ 42449 - Kludge Sun's LDAP provider
814 with broken SSL
815 */
816 // log the exception so we know it's there.
817 containerLog.warn(sm.getString("jndiRealm.exception"), e);
818
819 // close the connection so we know it will be reopened.
820 if (context != null)
821 close(context);
822
823 // open a new directory context.
824 context = open();
825
826 // Try the authentication again.
827 principal = authenticate(context, username, credentials);
828
829 } catch (CommunicationException e) {
830
831 // log the exception so we know it's there.
832 containerLog.warn(sm.getString("jndiRealm.exception"), e);
833
834 // close the connection so we know it will be reopened.
835 if (context != null)
836 close(context);
837
838 // open a new directory context.
839 context = open();
840
841 // Try the authentication again.
842 principal = authenticate(context, username, credentials);
843
844 } catch (ServiceUnavailableException e) {
845
846 // log the exception so we know it's there.
847 containerLog.warn(sm.getString("jndiRealm.exception"), e);
848
849 // close the connection so we know it will be reopened.
850 if (context != null)
851 close(context);
852
853 // open a new directory context.
854 context = open();
855
856 // Try the authentication again.
857 principal = authenticate(context, username, credentials);
858
859 }
860
861
862 // Release this context
863 release(context);
864
865 // Return the authenticated Principal (if any)
866 return (principal);
867
868 } catch (NamingException e) {
869
870 // Log the problem for posterity
871 containerLog.error(sm.getString("jndiRealm.exception"), e);
872
873 // Close the connection so that it gets reopened next time
874 if (context != null)
875 close(context);
876
877 // Return "not authenticated" for this request
878 return (null);
879
880 }
881
882 }
883
884
885 // -------------------------------------------------------- Package Methods
886
887
888 // ------------------------------------------------------ Protected Methods
889
890
891 /**
892 * Return the Principal associated with the specified username and
893 * credentials, if there is one; otherwise return <code>null</code>.
894 *
895 * @param context The directory context
896 * @param username Username of the Principal to look up
897 * @param credentials Password or other credentials to use in
898 * authenticating this username
899 *
900 * @exception NamingException if a directory server error occurs
901 */
902 public synchronized Principal authenticate(DirContext context,
903 String username,
904 String credentials)
905 throws NamingException {
906
907 if (username == null || username.equals("")
908 || credentials == null || credentials.equals(""))
909 return (null);
910
911 if (userPatternArray != null) {
912 for (curUserPattern = 0;
913 curUserPattern < userPatternFormatArray.length;
914 curUserPattern++) {
915 // Retrieve user information
916 User user = getUser(context, username);
917 if (user != null) {
918 try {
919 // Check the user's credentials
920 if (checkCredentials(context, user, credentials)) {
921 // Search for additional roles
922 List<String> roles = getRoles(context, user);
923 return (new GenericPrincipal(this,
924 username,
925 credentials,
926 roles));
927 }
928 } catch (InvalidNameException ine) {
929 // Log the problem for posterity
930 containerLog.warn(sm.getString("jndiRealm.exception"), ine);
931 // ignore; this is probably due to a name not fitting
932 // the search path format exactly, as in a fully-
933 // qualified name being munged into a search path
934 // that already contains cn= or vice-versa
935 }
936 }
937 }
938 return null;
939 } else {
940 // Retrieve user information
941 User user = getUser(context, username);
942 if (user == null)
943 return (null);
944
945 // Check the user's credentials
946 if (!checkCredentials(context, user, credentials))
947 return (null);
948
949 // Search for additional roles
950 List<String> roles = getRoles(context, user);
951
952 // Create and return a suitable Principal for this user
953 return (new GenericPrincipal(this, username, credentials, roles));
954 }
955 }
956
957
958 /**
959 * Return a User object containing information about the user
960 * with the specified username, if found in the directory;
961 * otherwise return <code>null</code>.
962 *
963 * If the <code>userPassword</code> configuration attribute is
964 * specified, the value of that attribute is retrieved from the
965 * user's directory entry. If the <code>userRoleName</code>
966 * configuration attribute is specified, all values of that
967 * attribute are retrieved from the directory entry.
968 *
969 * @param context The directory context
970 * @param username Username to be looked up
971 *
972 * @exception NamingException if a directory server error occurs
973 */
974 protected User getUser(DirContext context, String username)
975 throws NamingException {
976
977 User user = null;
978
979 // Get attributes to retrieve from user entry
980 ArrayList<String> list = new ArrayList<String>();
981 if (userPassword != null)
982 list.add(userPassword);
983 if (userRoleName != null)
984 list.add(userRoleName);
985 String[] attrIds = new String[list.size()];
986 list.toArray(attrIds);
987
988 // Use pattern or search for user entry
989 if (userPatternFormatArray != null) {
990 user = getUserByPattern(context, username, attrIds);
991 } else {
992 user = getUserBySearch(context, username, attrIds);
993 }
994
995 return user;
996 }
997
998
999 /**
1000 * Use the <code>UserPattern</code> configuration attribute to
1001 * locate the directory entry for the user with the specified
1002 * username and return a User object; otherwise return
1003 * <code>null</code>.
1004 *
1005 * @param context The directory context
1006 * @param username The username
1007 * @param attrIds String[]containing names of attributes to
1008 * retrieve.
1009 *
1010 * @exception NamingException if a directory server error occurs
1011 */
1012 protected User getUserByPattern(DirContext context,
1013 String username,
1014 String[] attrIds)
1015 throws NamingException {
1016
1017 if (username == null || userPatternFormatArray[curUserPattern] == null)
1018 return (null);
1019
1020 // Form the dn from the user pattern
1021 String dn = userPatternFormatArray[curUserPattern].format(new String[] { username });
1022
1023 // Get required attributes from user entry
1024 Attributes attrs = null;
1025 try {
1026 attrs = context.getAttributes(dn, attrIds);
1027 } catch (NameNotFoundException e) {
1028 return (null);
1029 }
1030 if (attrs == null)
1031 return (null);
1032
1033 // Retrieve value of userPassword
1034 String password = null;
1035 if (userPassword != null)
1036 password = getAttributeValue(userPassword, attrs);
1037
1038 // Retrieve values of userRoleName attribute
1039 ArrayList<String> roles = null;
1040 if (userRoleName != null)
1041 roles = addAttributeValues(userRoleName, attrs, roles);
1042
1043 return new User(username, dn, password, roles);
1044 }
1045
1046
1047 /**
1048 * Search the directory to return a User object containing
1049 * information about the user with the specified username, if
1050 * found in the directory; otherwise return <code>null</code>.
1051 *
1052 * @param context The directory context
1053 * @param username The username
1054 * @param attrIds String[]containing names of attributes to retrieve.
1055 *
1056 * @exception NamingException if a directory server error occurs
1057 */
1058 protected User getUserBySearch(DirContext context,
1059 String username,
1060 String[] attrIds)
1061 throws NamingException {
1062
1063 if (username == null || userSearchFormat == null)
1064 return (null);
1065
1066 // Form the search filter
1067 String filter = userSearchFormat.format(new String[] { username });
1068
1069 // Set up the search controls
1070 SearchControls constraints = new SearchControls();
1071
1072 if (userSubtree) {
1073 constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
1074 }
1075 else {
1076 constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
1077 }
1078
1079 // Specify the attributes to be retrieved
1080 if (attrIds == null)
1081 attrIds = new String[0];
1082 constraints.setReturningAttributes(attrIds);
1083
1084 NamingEnumeration results =
1085 context.search(userBase, filter, constraints);
1086
1087
1088 // Fail if no entries found
1089 if (results == null || !results.hasMore()) {
1090 return (null);
1091 }
1092
1093 // Get result for the first entry found
1094 SearchResult result = (SearchResult)results.next();
1095
1096 // Check no further entries were found
1097 if (results.hasMore()) {
1098 if(containerLog.isInfoEnabled())
1099 containerLog.info("username " + username + " has multiple entries");
1100 return (null);
1101 }
1102
1103 // Get the entry's distinguished name
1104 NameParser parser = context.getNameParser("");
1105 Name contextName = parser.parse(context.getNameInNamespace());
1106 Name baseName = parser.parse(userBase);
1107
1108 // Bugzilla 32269
1109 Name entryName = parser.parse(new CompositeName(result.getName()).get(0));
1110
1111 Name name = contextName.addAll(baseName);
1112 name = name.addAll(entryName);
1113 String dn = name.toString();
1114
1115 if (containerLog.isTraceEnabled())
1116 containerLog.trace(" entry found for " + username + " with dn " + dn);
1117
1118 // Get the entry's attributes
1119 Attributes attrs = result.getAttributes();
1120 if (attrs == null)
1121 return null;
1122
1123 // Retrieve value of userPassword
1124 String password = null;
1125 if (userPassword != null)
1126 password = getAttributeValue(userPassword, attrs);
1127
1128 // Retrieve values of userRoleName attribute
1129 ArrayList<String> roles = null;
1130 if (userRoleName != null)
1131 roles = addAttributeValues(userRoleName, attrs, roles);
1132
1133 return new User(username, dn, password, roles);
1134 }
1135
1136
1137 /**
1138 * Check whether the given User can be authenticated with the
1139 * given credentials. If the <code>userPassword</code>
1140 * configuration attribute is specified, the credentials
1141 * previously retrieved from the directory are compared explicitly
1142 * with those presented by the user. Otherwise the presented
1143 * credentials are checked by binding to the directory as the
1144 * user.
1145 *
1146 * @param context The directory context
1147 * @param user The User to be authenticated
1148 * @param credentials The credentials presented by the user
1149 *
1150 * @exception NamingException if a directory server error occurs
1151 */
1152 protected boolean checkCredentials(DirContext context,
1153 User user,
1154 String credentials)
1155 throws NamingException {
1156
1157 boolean validated = false;
1158
1159 if (userPassword == null) {
1160 validated = bindAsUser(context, user, credentials);
1161 } else {
1162 validated = compareCredentials(context, user, credentials);
1163 }
1164
1165 if (containerLog.isTraceEnabled()) {
1166 if (validated) {
1167 containerLog.trace(sm.getString("jndiRealm.authenticateSuccess",
1168 user.username));
1169 } else {
1170 containerLog.trace(sm.getString("jndiRealm.authenticateFailure",
1171 user.username));
1172 }
1173 }
1174 return (validated);
1175 }
1176
1177
1178
1179 /**
1180 * Check whether the credentials presented by the user match those
1181 * retrieved from the directory.
1182 *
1183 * @param context The directory context
1184 * @param info The User to be authenticated
1185 * @param credentials Authentication credentials
1186 *
1187 * @exception NamingException if a directory server error occurs
1188 */
1189 protected boolean compareCredentials(DirContext context,
1190 User info,
1191 String credentials)
1192 throws NamingException {
1193
1194 if (info == null || credentials == null)
1195 return (false);
1196
1197 String password = info.password;
1198 if (password == null)
1199 return (false);
1200
1201 // Validate the credentials specified by the user
1202 if (containerLog.isTraceEnabled())
1203 containerLog.trace(" validating credentials");
1204
1205 boolean validated = false;
1206 if (hasMessageDigest()) {
1207 // iPlanet support if the values starts with {SHA1}
1208 // The string is in a format compatible with Base64.encode not
1209 // the Hex encoding of the parent class.
1210 if (password.startsWith("{SHA}")) {
1211 /* sync since super.digest() does this same thing */
1212 synchronized (this) {
1213 password = password.substring(5);
1214 md.reset();
1215 md.update(credentials.getBytes());
1216 String digestedPassword =
1217 new String(Base64.encode(md.digest()));
1218 validated = password.equals(digestedPassword);
1219 }
1220 } else if (password.startsWith("{SSHA}")) {
1221 // Bugzilla 32938
1222 /* sync since super.digest() does this same thing */
1223 synchronized (this) {
1224 password = password.substring(6);
1225
1226 md.reset();
1227 md.update(credentials.getBytes());
1228
1229 // Decode stored password.
1230 ByteChunk pwbc = new ByteChunk(password.length());
1231 try {
1232 pwbc.append(password.getBytes(), 0, password.length());
1233 } catch (IOException e) {
1234 // Should never happen
1235 containerLog.error("Could not append password bytes to chunk: ", e);
1236 }
1237
1238 CharChunk decoded = new CharChunk();
1239 Base64.decode(pwbc, decoded);
1240 char[] pwarray = decoded.getBuffer();
1241
1242 // Split decoded password into hash and salt.
1243 final int saltpos = 20;
1244 byte[] hash = new byte[saltpos];
1245 for (int i=0; i< hash.length; i++) {
1246 hash[i] = (byte) pwarray[i];
1247 }
1248
1249 byte[] salt = new byte[pwarray.length - saltpos];
1250 for (int i=0; i< salt.length; i++)
1251 salt[i] = (byte)pwarray[i+saltpos];
1252
1253 md.update(salt);
1254 byte[] dp = md.digest();
1255
1256 validated = Arrays.equals(dp, hash);
1257 } // End synchronized(this) block
1258 } else {
1259 // Hex hashes should be compared case-insensitive
1260 validated = (digest(credentials).equalsIgnoreCase(password));
1261 }
1262 } else
1263 validated = (digest(credentials).equals(password));
1264 return (validated);
1265
1266 }
1267
1268
1269
1270 /**
1271 * Check credentials by binding to the directory as the user
1272 *
1273 * @param context The directory context
1274 * @param user The User to be authenticated
1275 * @param credentials Authentication credentials
1276 *
1277 * @exception NamingException if a directory server error occurs
1278 */
1279 protected boolean bindAsUser(DirContext context,
1280 User user,
1281 String credentials)
1282 throws NamingException {
1283
1284 if (credentials == null || user == null)
1285 return (false);
1286
1287 String dn = user.dn;
1288 if (dn == null)
1289 return (false);
1290
1291 // Validate the credentials specified by the user
1292 if (containerLog.isTraceEnabled()) {
1293 containerLog.trace(" validating credentials by binding as the user");
1294 }
1295
1296 // Set up security environment to bind as the user
1297 context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn);
1298 context.addToEnvironment(Context.SECURITY_CREDENTIALS, credentials);
1299
1300 // Elicit an LDAP bind operation
1301 boolean validated = false;
1302 try {
1303 if (containerLog.isTraceEnabled()) {
1304 containerLog.trace(" binding as " + dn);
1305 }
1306 context.getAttributes("", null);
1307 validated = true;
1308 }
1309 catch (AuthenticationException e) {
1310 if (containerLog.isTraceEnabled()) {
1311 containerLog.trace(" bind attempt failed");
1312 }
1313 }
1314
1315 // Restore the original security environment
1316 if (connectionName != null) {
1317 context.addToEnvironment(Context.SECURITY_PRINCIPAL,
1318 connectionName);
1319 } else {
1320 context.removeFromEnvironment(Context.SECURITY_PRINCIPAL);
1321 }
1322
1323 if (connectionPassword != null) {
1324 context.addToEnvironment(Context.SECURITY_CREDENTIALS,
1325 connectionPassword);
1326 }
1327 else {
1328 context.removeFromEnvironment(Context.SECURITY_CREDENTIALS);
1329 }
1330
1331 return (validated);
1332 }
1333
1334
1335 /**
1336 * Return a List of roles associated with the given User. Any
1337 * roles present in the user's directory entry are supplemented by
1338 * a directory search. If no roles are associated with this user,
1339 * a zero-length List is returned.
1340 *
1341 * @param context The directory context we are searching
1342 * @param user The User to be checked
1343 *
1344 * @exception NamingException if a directory server error occurs
1345 */
1346 protected List<String> getRoles(DirContext context, User user)
1347 throws NamingException {
1348
1349 if (user == null)
1350 return (null);
1351
1352 String dn = user.dn;
1353 String username = user.username;
1354
1355 if (dn == null || username == null)
1356 return (null);
1357
1358 if (containerLog.isTraceEnabled())
1359 containerLog.trace(" getRoles(" + dn + ")");
1360
1361 // Start with roles retrieved from the user entry
1362 ArrayList<String> list = user.roles;
1363 if (list == null) {
1364 list = new ArrayList<String>();
1365 }
1366
1367 // Are we configured to do role searches?
1368 if ((roleFormat == null) || (roleName == null))
1369 return (list);
1370
1371 // Set up parameters for an appropriate search
1372 String filter = roleFormat.format(new String[] { doRFC2254Encoding(dn), username });
1373 SearchControls controls = new SearchControls();
1374 if (roleSubtree)
1375 controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
1376 else
1377 controls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
1378 controls.setReturningAttributes(new String[] {roleName});
1379
1380 // Perform the configured search and process the results
1381 NamingEnumeration results =
1382 context.search(roleBase, filter, controls);
1383 if (results == null)
1384 return (list); // Should never happen, but just in case ...
1385 while (results.hasMore()) {
1386 SearchResult result = (SearchResult) results.next();
1387 Attributes attrs = result.getAttributes();
1388 if (attrs == null)
1389 continue;
1390 list = addAttributeValues(roleName, attrs, list);
1391 }
1392
1393
1394 if (containerLog.isTraceEnabled()) {
1395 if (list != null) {
1396 containerLog.trace(" Returning " + list.size() + " roles");
1397 for (int i=0; i<list.size(); i++)
1398 containerLog.trace( " Found role " + list.get(i));
1399 } else {
1400 containerLog.trace(" getRoles about to return null ");
1401 }
1402 }
1403
1404 return (list);
1405 }
1406
1407
1408 /**
1409 * Return a String representing the value of the specified attribute.
1410 *
1411 * @param attrId Attribute name
1412 * @param attrs Attributes containing the required value
1413 *
1414 * @exception NamingException if a directory server error occurs
1415 */
1416 private String getAttributeValue(String attrId, Attributes attrs)
1417 throws NamingException {
1418
1419 if (containerLog.isTraceEnabled())
1420 containerLog.trace(" retrieving attribute " + attrId);
1421
1422 if (attrId == null || attrs == null)
1423 return null;
1424
1425 Attribute attr = attrs.get(attrId);
1426 if (attr == null)
1427 return (null);
1428 Object value = attr.get();
1429 if (value == null)
1430 return (null);
1431 String valueString = null;
1432 if (value instanceof byte[])
1433 valueString = new String((byte[]) value);
1434 else
1435 valueString = value.toString();
1436
1437 return valueString;
1438 }
1439
1440
1441
1442 /**
1443 * Add values of a specified attribute to a list
1444 *
1445 * @param attrId Attribute name
1446 * @param attrs Attributes containing the new values
1447 * @param values ArrayList containing values found so far
1448 *
1449 * @exception NamingException if a directory server error occurs
1450 */
1451 private ArrayList<String> addAttributeValues(String attrId,
1452 Attributes attrs,
1453 ArrayList<String> values)
1454 throws NamingException{
1455
1456 if (containerLog.isTraceEnabled())
1457 containerLog.trace(" retrieving values for attribute " + attrId);
1458 if (attrId == null || attrs == null)
1459 return values;
1460 if (values == null)
1461 values = new ArrayList<String>();
1462 Attribute attr = attrs.get(attrId);
1463 if (attr == null)
1464 return (values);
1465 NamingEnumeration e = attr.getAll();
1466 while(e.hasMore()) {
1467 String value = (String)e.next();
1468 values.add(value);
1469 }
1470 return values;
1471 }
1472
1473
1474 /**
1475 * Close any open connection to the directory server for this Realm.
1476 *
1477 * @param context The directory context to be closed
1478 */
1479 protected void close(DirContext context) {
1480
1481 // Do nothing if there is no opened connection
1482 if (context == null)
1483 return;
1484
1485 // Close our opened connection
1486 try {
1487 if (containerLog.isDebugEnabled())
1488 containerLog.debug("Closing directory context");
1489 context.close();
1490 } catch (NamingException e) {
1491 containerLog.error(sm.getString("jndiRealm.close"), e);
1492 }
1493 this.context = null;
1494
1495 }
1496
1497
1498 /**
1499 * Return a short name for this Realm implementation.
1500 */
1501 protected String getName() {
1502
1503 return (name);
1504
1505 }
1506
1507
1508 /**
1509 * Return the password associated with the given principal's user name.
1510 */
1511 protected String getPassword(String username) {
1512
1513 return (null);
1514
1515 }
1516
1517 /**
1518 * Return the Principal associated with the given user name.
1519 */
1520 protected Principal getPrincipal(String username) {
1521
1522 DirContext context = null;
1523 Principal principal = null;
1524
1525 try {
1526
1527 // Ensure that we have a directory context available
1528 context = open();
1529
1530 // Occassionally the directory context will timeout. Try one more
1531 // time before giving up.
1532 try {
1533
1534 // Authenticate the specified username if possible
1535 principal = getPrincipal(context, username);
1536
1537 } catch (CommunicationException e) {
1538
1539 // log the exception so we know it's there.
1540 containerLog.warn(sm.getString("jndiRealm.exception"), e);
1541
1542 // close the connection so we know it will be reopened.
1543 if (context != null)
1544 close(context);
1545
1546 // open a new directory context.
1547 context = open();
1548
1549 // Try the authentication again.
1550 principal = getPrincipal(context, username);
1551
1552 } catch (ServiceUnavailableException e) {
1553
1554 // log the exception so we know it's there.
1555 containerLog.warn(sm.getString("jndiRealm.exception"), e);
1556
1557 // close the connection so we know it will be reopened.
1558 if (context != null)
1559 close(context);
1560
1561 // open a new directory context.
1562 context = open();
1563
1564 // Try the authentication again.
1565 principal = getPrincipal(context, username);
1566
1567 }
1568
1569
1570 // Release this context
1571 release(context);
1572
1573 // Return the authenticated Principal (if any)
1574 return (principal);
1575
1576 } catch (NamingException e) {
1577
1578 // Log the problem for posterity
1579 containerLog.error(sm.getString("jndiRealm.exception"), e);
1580
1581 // Close the connection so that it gets reopened next time
1582 if (context != null)
1583 close(context);
1584
1585 // Return "not authenticated" for this request
1586 return (null);
1587
1588 }
1589
1590
1591 }
1592
1593
1594 /**
1595 * Return the Principal associated with the given user name.
1596 */
1597 protected synchronized Principal getPrincipal(DirContext context,
1598 String username)
1599 throws NamingException {
1600
1601 User user = getUser(context, username);
1602
1603 return new GenericPrincipal(this, user.username, user.password ,
1604 getRoles(context, user));
1605 }
1606
1607 /**
1608 * Open (if necessary) and return a connection to the configured
1609 * directory server for this Realm.
1610 *
1611 * @exception NamingException if a directory server error occurs
1612 */
1613 protected DirContext open() throws NamingException {
1614
1615 // Do nothing if there is a directory server connection already open
1616 if (context != null)
1617 return (context);
1618
1619 try {
1620
1621 // Ensure that we have a directory context available
1622 context = new InitialDirContext(getDirectoryContextEnvironment());
1623
1624 } catch (Exception e) {
1625
1626 connectionAttempt = 1;
1627
1628 // log the first exception.
1629 containerLog.warn(sm.getString("jndiRealm.exception"), e);
1630
1631 // Try connecting to the alternate url.
1632 context = new InitialDirContext(getDirectoryContextEnvironment());
1633
1634 } finally {
1635
1636 // reset it in case the connection times out.
1637 // the primary may come back.
1638 connectionAttempt = 0;
1639
1640 }
1641
1642 return (context);
1643
1644 }
1645
1646 /**
1647 * Create our directory context configuration.
1648 *
1649 * @return java.util.Hashtable the configuration for the directory context.
1650 */
1651 protected Hashtable getDirectoryContextEnvironment() {
1652
1653 Hashtable<String,String> env = new Hashtable<String,String>();
1654
1655 // Configure our directory context environment.
1656 if (containerLog.isDebugEnabled() && connectionAttempt == 0)
1657 containerLog.debug("Connecting to URL " + connectionURL);
1658 else if (containerLog.isDebugEnabled() && connectionAttempt > 0)
1659 containerLog.debug("Connecting to URL " + alternateURL);
1660 env.put(Context.INITIAL_CONTEXT_FACTORY, contextFactory);
1661 if (connectionName != null)
1662 env.put(Context.SECURITY_PRINCIPAL, connectionName);
1663 if (connectionPassword != null)
1664 env.put(Context.SECURITY_CREDENTIALS, connectionPassword);
1665 if (connectionURL != null && connectionAttempt == 0)
1666 env.put(Context.PROVIDER_URL, connectionURL);
1667 else if (alternateURL != null && connectionAttempt > 0)
1668 env.put(Context.PROVIDER_URL, alternateURL);
1669 if (authentication != null)
1670 env.put(Context.SECURITY_AUTHENTICATION, authentication);
1671 if (protocol != null)
1672 env.put(Context.SECURITY_PROTOCOL, protocol);
1673 if (referrals != null)
1674 env.put(Context.REFERRAL, referrals);
1675 if (derefAliases != null)
1676 env.put(JNDIRealm.DEREF_ALIASES, derefAliases);
1677
1678 return env;
1679
1680 }
1681
1682
1683 /**
1684 * Release our use of this connection so that it can be recycled.
1685 *
1686 * @param context The directory context to release
1687 */
1688 protected void release(DirContext context) {
1689
1690 ; // NO-OP since we are not pooling anything
1691
1692 }
1693
1694
1695 // ------------------------------------------------------ Lifecycle Methods
1696
1697
1698 /**
1699 * Prepare for active use of the public methods of this Component.
1700 *
1701 * @exception LifecycleException if this component detects a fatal error
1702 * that prevents it from being started
1703 */
1704 public void start() throws LifecycleException {
1705
1706 // Perform normal superclass initialization
1707 super.start();
1708
1709 // Validate that we can open our connection
1710 try {
1711 open();
1712 } catch (NamingException e) {
1713 throw new LifecycleException(sm.getString("jndiRealm.open"), e);
1714 }
1715
1716 }
1717
1718
1719 /**
1720 * Gracefully shut down active use of the public methods of this Component.
1721 *
1722 * @exception LifecycleException if this component detects a fatal error
1723 * that needs to be reported
1724 */
1725 public void stop() throws LifecycleException {
1726
1727 // Perform normal superclass finalization
1728 super.stop();
1729
1730 // Close any open directory server connection
1731 close(this.context);
1732
1733 }
1734
1735 /**
1736 * Given a string containing LDAP patterns for user locations (separated by
1737 * parentheses in a pseudo-LDAP search string format -
1738 * "(location1)(location2)", returns an array of those paths. Real LDAP
1739 * search strings are supported as well (though only the "|" "OR" type).
1740 *
1741 * @param userPatternString - a string LDAP search paths surrounded by
1742 * parentheses
1743 */
1744 protected String[] parseUserPatternString(String userPatternString) {
1745
1746 if (userPatternString != null) {
1747 ArrayList<String> pathList = new ArrayList<String>();
1748 int startParenLoc = userPatternString.indexOf('(');
1749 if (startParenLoc == -1) {
1750 // no parens here; return whole thing
1751 return new String[] {userPatternString};
1752 }
1753 int startingPoint = 0;
1754 while (startParenLoc > -1) {
1755 int endParenLoc = 0;
1756 // weed out escaped open parens and parens enclosing the
1757 // whole statement (in the case of valid LDAP search
1758 // strings: (|(something)(somethingelse))
1759 while ( (userPatternString.charAt(startParenLoc + 1) == '|') ||
1760 (startParenLoc != 0 && userPatternString.charAt(startParenLoc - 1) == '\\') ) {
1761 startParenLoc = userPatternString.indexOf("(", startParenLoc+1);
1762 }
1763 endParenLoc = userPatternString.indexOf(")", startParenLoc+1);
1764 // weed out escaped end-parens
1765 while (userPatternString.charAt(endParenLoc - 1) == '\\') {
1766 endParenLoc = userPatternString.indexOf(")", endParenLoc+1);
1767 }
1768 String nextPathPart = userPatternString.substring
1769 (startParenLoc+1, endParenLoc);
1770 pathList.add(nextPathPart);
1771 startingPoint = endParenLoc+1;
1772 startParenLoc = userPatternString.indexOf('(', startingPoint);
1773 }
1774 return (String[])pathList.toArray(new String[] {});
1775 }
1776 return null;
1777
1778 }
1779
1780
1781 /**
1782 * Given an LDAP search string, returns the string with certain characters
1783 * escaped according to RFC 2254 guidelines.
1784 * The character mapping is as follows:
1785 * char -> Replacement
1786 * ---------------------------
1787 * * -> \2a
1788 * ( -> \28
1789 * ) -> \29
1790 * \ -> \5c
1791 * \0 -> \00
1792 * @param inString string to escape according to RFC 2254 guidelines
1793 * @return String the escaped/encoded result
1794 */
1795 protected String doRFC2254Encoding(String inString) {
1796 StringBuffer buf = new StringBuffer(inString.length());
1797 for (int i = 0; i < inString.length(); i++) {
1798 char c = inString.charAt(i);
1799 switch (c) {
1800 case '\\':
1801 buf.append("\\5c");
1802 break;
1803 case '*':
1804 buf.append("\\2a");
1805 break;
1806 case '(':
1807 buf.append("\\28");
1808 break;
1809 case ')':
1810 buf.append("\\29");
1811 break;
1812 case '\0':
1813 buf.append("\\00");
1814 break;
1815 default:
1816 buf.append(c);
1817 break;
1818 }
1819 }
1820 return buf.toString();
1821 }
1822
1823
1824 }
1825
1826 // ------------------------------------------------------ Private Classes
1827
1828 /**
1829 * A private class representing a User
1830 */
1831 class User {
1832 String username = null;
1833 String dn = null;
1834 String password = null;
1835 ArrayList<String> roles = null;
1836
1837
1838 User(String username, String dn, String password,
1839 ArrayList<String> roles) {
1840 this.username = username;
1841 this.dn = dn;
1842 this.password = password;
1843 this.roles = roles;
1844 }
1845
1846 }