1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package net.sf.michaelo.dirctxsrc;
17
18 import java.io.OutputStream;
19 import java.security.PrivilegedActionException;
20 import java.security.PrivilegedExceptionAction;
21 import java.util.ArrayList;
22 import java.util.Hashtable;
23 import java.util.List;
24 import java.util.logging.Level;
25 import java.util.logging.Logger;
26
27 import javax.naming.Context;
28 import javax.naming.NamingException;
29 import javax.naming.directory.DirContext;
30 import javax.naming.directory.InitialDirContext;
31 import javax.security.auth.Subject;
32 import javax.security.auth.login.LoginContext;
33 import javax.security.auth.login.LoginException;
34 import javax.security.sasl.Sasl;
35
36 import org.apache.commons.lang3.StringUtils;
37 import static org.apache.commons.lang3.Validate.*;
38 import org.ietf.jgss.GSSCredential;
39 import org.ietf.jgss.GSSException;
40 import org.ietf.jgss.GSSManager;
41 import org.ietf.jgss.Oid;
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82 public class DirContextSource {
83
84
85
86
87 public enum Auth {
88
89 NONE("none"), GSSAPI("GSSAPI");
90
91 private String securityAuthName;
92
93 Auth(String securityAuthName) {
94 this.securityAuthName = securityAuthName;
95 }
96
97 String getSecurityAuthName() {
98 return securityAuthName;
99 }
100 }
101
102 protected final static Oid KRB5_MECHANISM;
103
104 static {
105 try {
106 KRB5_MECHANISM = new Oid("1.2.840.113554.1.2.2");
107 } catch (GSSException e) {
108 throw new IllegalStateException("failed to create OID for Kerberos 5 mechanism");
109 }
110 }
111
112 private static class GSSInitialDirContext extends InitialDirContext {
113
114 public GSSInitialDirContext(Hashtable<?, ?> environment) throws NamingException {
115 super(environment);
116 }
117
118 @Override
119 public void close() throws NamingException {
120 GSSCredential credential = null;
121
122 try {
123 credential = (GSSCredential) getEnvironment().get(Sasl.CREDENTIALS);
124 } finally {
125 super.close();
126 }
127
128 if (credential != null) {
129 try {
130 credential.dispose();
131 } catch (GSSException e) {
132
133 }
134 }
135 }
136
137 }
138
139 private static final Logger logger = Logger.getLogger(DirContextSource.class.getName());
140 private final Hashtable<String, Object> env;
141 private final String loginEntryName;
142 private final int retries;
143 private final int retryWait;
144 private final Auth auth;
145
146 private DirContextSource(Builder builder) {
147 env = new Hashtable<String, Object>();
148
149 env.put(Context.INITIAL_CONTEXT_FACTORY, builder.contextFactory);
150 env.put(Context.PROVIDER_URL, StringUtils.join(builder.urls, ' '));
151 env.put(Context.SECURITY_AUTHENTICATION, builder.auth.getSecurityAuthName());
152 auth = builder.auth;
153 loginEntryName = builder.loginEntryName;
154 if (builder.objectFactories != null)
155 env.put(Context.OBJECT_FACTORIES, StringUtils.join(builder.objectFactories, ':'));
156 env.put(Sasl.SERVER_AUTH, Boolean.toString(builder.mutualAuth));
157 if (builder.qop != null)
158 env.put(Sasl.QOP, StringUtils.join(builder.qop, ','));
159 if (builder.debug)
160 env.put("com.sun.jndi.ldap.trace.ber", builder.debugStream);
161 retries = builder.retries;
162 retryWait = builder.retryWait;
163 if (builder.referral != null)
164 env.put(Context.REFERRAL, builder.referral);
165 if (builder.binaryAttributes != null)
166 env.put("java.naming.ldap.attributes.binary",
167 StringUtils.join(builder.binaryAttributes, ' '));
168 env.putAll(builder.additionalProperties);
169 }
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186 public static final class Builder {
187
188
189 private String contextFactory;
190 private String[] urls;
191 private Auth auth;
192 private String loginEntryName;
193 private String[] objectFactories;
194 private boolean mutualAuth;
195 private String[] qop;
196 private boolean debug;
197 private OutputStream debugStream;
198 private int retries;
199 private int retryWait;
200 private String[] binaryAttributes;
201 private String referral;
202 private Hashtable<String, Object> additionalProperties;
203
204 private boolean done;
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221 public Builder(String... urls) {
222
223 contextFactory("com.sun.jndi.ldap.LdapCtxFactory");
224 auth(Auth.NONE);
225 retries(1);
226 retryWait(2000);
227 additionalProperties = new Hashtable<String, Object>();
228
229 urls(urls);
230 }
231
232
233
234
235
236
237
238
239
240
241
242
243 public Builder contextFactory(String contextFactory) {
244 check();
245 this.contextFactory = validateAndReturnString("contextFactory", contextFactory);
246 return this;
247 }
248
249 private String[] validateAndReturnStringArray(String name, String[] value) {
250 notEmpty(value, "property '%s' cannot be null or empty", name);
251
252 List<String> validatedElements = new ArrayList<String>();
253 for (String elem : value)
254 if (StringUtils.isNotEmpty(elem))
255 validatedElements.add(elem);
256
257 notEmpty(validatedElements, "property '%s' cannot be null or empty", name);
258
259 return validatedElements.toArray(new String[validatedElements.size()]);
260 }
261
262 private String validateAndReturnString(String name, String value) {
263 return notEmpty(value, "property '%s' cannot be null or empty", name);
264 }
265
266 private <T> T validateAndReturnObject(String name, T value) {
267 return notNull(value, "property '%s' cannot be null", name);
268 }
269
270 private Builder urls(String... urls) {
271 check();
272 this.urls = validateAndReturnStringArray("urls", urls);
273 return this;
274 }
275
276
277
278
279
280
281
282
283
284
285 public Builder auth(Auth auth) {
286 check();
287 this.auth = validateAndReturnObject("auth", auth);
288
289
290
291 if (auth == Auth.GSSAPI)
292 mutualAuth().qop("auth-int");
293
294 return this;
295 }
296
297
298
299
300
301
302
303
304
305
306
307
308 public Builder loginEntryName(String loginEntryName) {
309 check();
310 this.loginEntryName = validateAndReturnString("loginEntryName", loginEntryName);
311 return this;
312 }
313
314
315
316
317
318
319 public Builder anonymousAuth() {
320 return auth(Auth.NONE);
321 }
322
323
324
325
326
327
328 public Builder gssApiAuth() {
329 return gssApiAuth("DirContextSource");
330 }
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345 public Builder gssApiAuth(String loginEntryName) {
346 auth(Auth.GSSAPI).loginEntryName(loginEntryName);
347 return this;
348 }
349
350
351
352
353
354
355
356
357
358
359
360
361 public Builder objectFactories(String... objectFactories) {
362 check();
363 this.objectFactories = validateAndReturnStringArray("objectFactories", objectFactories);
364 return this;
365 }
366
367
368
369
370
371
372
373 public Builder mutualAuth() {
374 return mutualAuth(true);
375 }
376
377
378
379
380
381
382
383
384
385 public Builder mutualAuth(boolean mutualAuth) {
386 check();
387 this.mutualAuth = mutualAuth;
388 return this;
389 }
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407 public Builder qop(String... qop) {
408 check();
409 this.qop = validateAndReturnStringArray("qop", qop);
410 return this;
411 }
412
413
414
415
416
417
418
419 public Builder debug() {
420 return debug(true);
421 }
422
423
424
425
426
427
428
429
430 public Builder debug(boolean debug) {
431 check();
432 this.debug = debug;
433 this.debugStream = debug ? System.err : null;
434 return this;
435 }
436
437
438
439
440
441
442
443
444
445
446 public Builder debug(OutputStream stream) {
447 check();
448 this.debugStream = validateAndReturnObject("stream", stream);
449 this.debug = true;
450 return this;
451 }
452
453
454
455
456
457
458
459
460
461
462 public Builder retries(int retries) {
463 check();
464 isTrue(retries > 0, "property 'retries' must be greater than zero but is %d", retries);
465 this.retries = retries;
466 return this;
467 }
468
469
470
471
472
473
474
475
476
477
478 public Builder retryWait(int retryWait) {
479 check();
480 isTrue(retryWait > 0, "property 'retryWait' must be greater than zero but is %d",
481 retryWait);
482 this.retryWait = retryWait;
483 return this;
484 }
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500 public Builder binaryAttributes(String... attributes) {
501 check();
502 this.binaryAttributes = validateAndReturnStringArray("binaryAttributes", attributes);
503 return this;
504 }
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520 public Builder referral(String referral) {
521 check();
522 this.referral = validateAndReturnString("referral", referral);
523 return this;
524 }
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539 public Builder additionalProperty(String name, Object value) {
540 check();
541 notEmpty(name, "additional property's name cannot be null or empty");
542 this.additionalProperties.put(name, value);
543 return this;
544 }
545
546
547
548
549
550
551
552
553
554
555 public DirContextSource build() {
556
557 if (auth == Auth.GSSAPI && StringUtils.isEmpty(loginEntryName))
558 throw new IllegalStateException(
559 "auth 'GSS-API' is set but no login entry name configured");
560
561 DirContextSource contextSource = new DirContextSource(this);
562 done = true;
563
564 return contextSource;
565 }
566
567 private void check() {
568 if (done)
569 throw new IllegalStateException("cannot modify an already used builder");
570 }
571
572 }
573
574 protected DirContext getGssApiDirContext() throws NamingException {
575
576 DirContext context = null;
577
578 try {
579
580 LoginContext lc = new LoginContext(loginEntryName);
581 lc.login();
582
583 context = Subject.doAs(lc.getSubject(), new PrivilegedExceptionAction<DirContext>() {
584
585 public DirContext run() throws NamingException {
586
587 GSSManager manager = GSSManager.getInstance();
588 GSSCredential credential;
589 try {
590 credential = manager.createCredential(null,
591 GSSCredential.INDEFINITE_LIFETIME, KRB5_MECHANISM,
592 GSSCredential.INITIATE_ONLY);
593 } catch (GSSException e) {
594 NamingException ne = new NamingException("failed to obtain GSS credential");
595 ne.setRootCause(e);
596 throw ne;
597 }
598
599 int r = retries;
600 InitialDirContext idc = null;
601
602 while (r-- > 0) {
603
604 try {
605 env.put(Sasl.CREDENTIALS, credential);
606 idc = new GSSInitialDirContext(env);
607 break;
608 } catch (NamingException e) {
609 if (r == 0)
610 throw e;
611
612 logger.log(Level.WARNING,
613 String.format(
614 "Connecting to [%s] failed, remaining retries: %d",
615 env.get(Context.PROVIDER_URL), r), e);
616
617 try {
618 Thread.sleep(retryWait);
619 } catch (InterruptedException e1) {
620 throw new NamingException(e1.getMessage());
621 }
622 }
623
624 }
625
626 return idc;
627 }
628 });
629
630 lc.logout();
631 } catch (LoginException e) {
632 NamingException ne = new NamingException(e.getMessage());
633 ne.initCause(e);
634 throw ne;
635 } catch (SecurityException e) {
636 NamingException ne = new NamingException(e.getMessage());
637 ne.initCause(e);
638 throw ne;
639 } catch (PrivilegedActionException e) {
640 throw (NamingException) e.getException();
641 }
642
643 return context;
644 }
645
646 protected DirContext getAnonymousDirContext() throws NamingException {
647
648 DirContext context = null;
649
650 int r = retries;
651
652 while (r-- > 0) {
653
654 try {
655 context = new InitialDirContext(env);
656 break;
657 } catch (NamingException e) {
658 if (r == 0)
659 throw e;
660
661 logger.log(Level.WARNING,
662 String.format(
663 "Connecting to [%s] failed, remaining retries: %d",
664 env.get(Context.PROVIDER_URL), r), e);
665
666 try {
667 Thread.sleep(retryWait);
668 } catch (InterruptedException e1) {
669 throw new NamingException(e1.getMessage());
670 }
671 }
672
673 }
674
675 return context;
676 }
677
678
679
680
681
682
683
684
685
686 public DirContext getDirContext() throws NamingException {
687
688 switch (auth) {
689 case NONE:
690 return getAnonymousDirContext();
691 case GSSAPI:
692 return getGssApiDirContext();
693 default:
694 throw new AssertionError(auth);
695 }
696
697 }
698
699 }