]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapDirectory.java
JDK HTTP server authentication
[lgpl/argeo-commons.git] / org.argeo.util / src / org / argeo / util / directory / ldap / AbstractLdapDirectory.java
1 package org.argeo.util.directory.ldap;
2
3 import static org.argeo.util.directory.ldap.LdapNameUtils.toLdapName;
4
5 import java.io.File;
6 import java.net.URI;
7 import java.net.URISyntaxException;
8 import java.util.Arrays;
9 import java.util.Dictionary;
10 import java.util.Enumeration;
11 import java.util.Hashtable;
12 import java.util.List;
13 import java.util.Optional;
14 import java.util.StringJoiner;
15
16 import javax.naming.InvalidNameException;
17 import javax.naming.NameNotFoundException;
18 import javax.naming.NamingEnumeration;
19 import javax.naming.NamingException;
20 import javax.naming.directory.Attribute;
21 import javax.naming.directory.Attributes;
22 import javax.naming.directory.BasicAttributes;
23 import javax.naming.ldap.LdapName;
24 import javax.naming.ldap.Rdn;
25 import javax.transaction.xa.XAResource;
26
27 import org.argeo.osgi.useradmin.OsUserDirectory;
28 import org.argeo.util.directory.Directory;
29 import org.argeo.util.directory.DirectoryConf;
30 import org.argeo.util.directory.HierarchyUnit;
31 import org.argeo.util.naming.LdapAttrs;
32 import org.argeo.util.naming.LdapObjs;
33 import org.argeo.util.transaction.WorkControl;
34 import org.argeo.util.transaction.WorkingCopyXaResource;
35 import org.argeo.util.transaction.XAResourceProvider;
36
37 public abstract class AbstractLdapDirectory implements Directory, XAResourceProvider {
38 protected static final String SHARED_STATE_USERNAME = "javax.security.auth.login.name";
39 protected static final String SHARED_STATE_PASSWORD = "javax.security.auth.login.password";
40
41 private final LdapName baseDn;
42 private final Hashtable<String, Object> configProperties;
43 private final Rdn userBaseRdn, groupBaseRdn, systemRoleBaseRdn;
44 private final String userObjectClass, groupObjectClass;
45 private String memberAttributeId = "member";
46
47 private final boolean readOnly;
48 private final boolean disabled;
49 private final String uri;
50
51 private String forcedPassword;
52
53 private final boolean scoped;
54
55 private List<String> credentialAttributeIds = Arrays
56 .asList(new String[] { LdapAttrs.userPassword.name(), LdapAttrs.authPassword.name() });
57
58 private WorkControl transactionControl;
59 private WorkingCopyXaResource<LdapEntryWorkingCopy> xaResource;
60
61 private LdapDirectoryDao directoryDao;
62
63 public AbstractLdapDirectory(URI uriArg, Dictionary<String, ?> props, boolean scoped) {
64 this.configProperties = new Hashtable<String, Object>();
65 for (Enumeration<String> keys = props.keys(); keys.hasMoreElements();) {
66 String key = keys.nextElement();
67 configProperties.put(key, props.get(key));
68 }
69 baseDn = toLdapName(DirectoryConf.baseDn.getValue(configProperties));
70 this.scoped = scoped;
71
72 if (uriArg != null) {
73 uri = uriArg.toString();
74 // uri from properties is ignored
75 } else {
76 String uriStr = DirectoryConf.uri.getValue(configProperties);
77 if (uriStr == null)
78 uri = null;
79 else
80 uri = uriStr;
81 }
82
83 forcedPassword = DirectoryConf.forcedPassword.getValue(configProperties);
84
85 userObjectClass = DirectoryConf.userObjectClass.getValue(configProperties);
86 groupObjectClass = DirectoryConf.groupObjectClass.getValue(configProperties);
87
88 String userBase = DirectoryConf.userBase.getValue(configProperties);
89 String groupBase = DirectoryConf.groupBase.getValue(configProperties);
90 String systemRoleBase = DirectoryConf.systemRoleBase.getValue(configProperties);
91 try {
92 // baseDn = new LdapName(UserAdminConf.baseDn.getValue(properties));
93 userBaseRdn = new Rdn(userBase);
94 // userBaseDn = new LdapName(userBase + "," + baseDn);
95 groupBaseRdn = new Rdn(groupBase);
96 // groupBaseDn = new LdapName(groupBase + "," + baseDn);
97 systemRoleBaseRdn = new Rdn(systemRoleBase);
98 } catch (InvalidNameException e) {
99 throw new IllegalArgumentException(
100 "Badly formated base DN " + DirectoryConf.baseDn.getValue(configProperties), e);
101 }
102
103 // read only
104 String readOnlyStr = DirectoryConf.readOnly.getValue(configProperties);
105 if (readOnlyStr == null) {
106 readOnly = readOnlyDefault(uri);
107 configProperties.put(DirectoryConf.readOnly.name(), Boolean.toString(readOnly));
108 } else
109 readOnly = Boolean.parseBoolean(readOnlyStr);
110
111 // disabled
112 String disabledStr = DirectoryConf.disabled.getValue(configProperties);
113 if (disabledStr != null)
114 disabled = Boolean.parseBoolean(disabledStr);
115 else
116 disabled = false;
117 if (!getRealm().isEmpty()) {
118 // IPA multiple LDAP causes URI parsing to fail
119 // TODO manage generic redundant LDAP case
120 directoryDao = new LdapDao(this);
121 } else {
122 URI u = URI.create(uri);
123 if (DirectoryConf.SCHEME_LDAP.equals(u.getScheme()) || DirectoryConf.SCHEME_LDAPS.equals(u.getScheme())) {
124 directoryDao = new LdapDao(this);
125 } else if (DirectoryConf.SCHEME_FILE.equals(u.getScheme())) {
126 directoryDao = new LdifDao(this);
127 } else if (DirectoryConf.SCHEME_OS.equals(u.getScheme())) {
128 directoryDao = new OsUserDirectory(this);
129 // singleUser = true;
130 } else {
131 throw new IllegalArgumentException("Unsupported scheme " + u.getScheme());
132 }
133 }
134 xaResource = new WorkingCopyXaResource<>(directoryDao);
135 }
136
137 /*
138 * ABSTRACT METHODS
139 */
140
141 // public abstract HierarchyUnit doGetHierarchyUnit(LdapName dn);
142 //
143 // public abstract Iterable<HierarchyUnit> doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly);
144 //
145 // protected abstract Boolean daoHasEntry(LdapName dn);
146 //
147 // protected abstract LdapEntry daoGetEntry(LdapName key) throws NameNotFoundException;
148 //
149 // protected abstract List<LdapEntry> doGetEntries(LdapName searchBase, Filter f, boolean deep);
150 //
151 // /** Returns the groups this user is a direct member of. */
152 // protected abstract List<LdapName> getDirectGroups(LdapName dn);
153 /*
154 * INITIALIZATION
155 */
156
157 public void init() {
158 getDirectoryDao().init();
159 }
160
161 public void destroy() {
162 getDirectoryDao().destroy();
163 }
164
165 /*
166 * CREATION
167 */
168 protected abstract LdapEntry newUser(LdapName name, Attributes attrs);
169
170 protected abstract LdapEntry newGroup(LdapName name, Attributes attrs);
171
172 /*
173 * EDITION
174 */
175
176 public boolean isEditing() {
177 return xaResource.wc() != null;
178 }
179
180 public LdapEntryWorkingCopy getWorkingCopy() {
181 LdapEntryWorkingCopy wc = xaResource.wc();
182 if (wc == null)
183 return null;
184 return wc;
185 }
186
187 public void checkEdit() {
188 if (xaResource.wc() == null) {
189 try {
190 transactionControl.getWorkContext().registerXAResource(xaResource, null);
191 } catch (Exception e) {
192 throw new IllegalStateException("Cannot enlist " + xaResource, e);
193 }
194 } else {
195 }
196 }
197
198 public void setTransactionControl(WorkControl transactionControl) {
199 this.transactionControl = transactionControl;
200 }
201
202 public XAResource getXaResource() {
203 return xaResource;
204 }
205
206 public boolean removeEntry(LdapName dn) {
207 checkEdit();
208 LdapEntryWorkingCopy wc = getWorkingCopy();
209 boolean actuallyDeleted;
210 if (getDirectoryDao().entryExists(dn) || wc.getNewData().containsKey(dn)) {
211 LdapEntry user = doGetRole(dn);
212 wc.getDeletedData().put(dn, user);
213 actuallyDeleted = true;
214 } else {// just removing from groups (e.g. system roles)
215 actuallyDeleted = false;
216 }
217 for (LdapName groupDn : getDirectoryDao().getDirectGroups(dn)) {
218 LdapEntry group = doGetRole(groupDn);
219 group.getAttributes().get(getMemberAttributeId()).remove(dn.toString());
220 }
221 return actuallyDeleted;
222 }
223
224 /*
225 * RETRIEVAL
226 */
227
228 protected LdapEntry doGetRole(LdapName dn) {
229 LdapEntryWorkingCopy wc = getWorkingCopy();
230 LdapEntry user;
231 try {
232 user = getDirectoryDao().doGetEntry(dn);
233 } catch (NameNotFoundException e) {
234 user = null;
235 }
236 if (wc != null) {
237 if (user == null && wc.getNewData().containsKey(dn))
238 user = wc.getNewData().get(dn);
239 else if (wc.getDeletedData().containsKey(dn))
240 user = null;
241 }
242 return user;
243 }
244
245 protected void collectGroups(LdapEntry user, List<LdapEntry> allRoles) {
246 Attributes attrs = user.getAttributes();
247 // TODO centralize attribute name
248 Attribute memberOf = attrs.get(LdapAttrs.memberOf.name());
249 // if user belongs to this directory, we only check memberOf
250 if (memberOf != null && user.getDn().startsWith(getBaseDn())) {
251 try {
252 NamingEnumeration<?> values = memberOf.getAll();
253 while (values.hasMore()) {
254 Object value = values.next();
255 LdapName groupDn = new LdapName(value.toString());
256 LdapEntry group = doGetRole(groupDn);
257 if (group != null) {
258 allRoles.add(group);
259 }else {
260 // user doesn't have the right to retrieve role, but we know it exists
261 // otherwise memberOf would not work
262 Attributes a = new BasicAttributes();
263 a.put(LdapNameUtils.getLastRdn(groupDn).getType(), LdapNameUtils.getLastRdn(groupDn).getValue());
264 a.put(LdapAttrs.objectClass.name(), LdapObjs.groupOfNames.name());
265 group = newGroup(groupDn, a);
266 allRoles.add(group);
267 }
268 }
269 } catch (NamingException e) {
270 throw new IllegalStateException("Cannot get memberOf groups for " + user, e);
271 }
272 } else {
273 for (LdapName groupDn : getDirectoryDao().getDirectGroups(user.getDn())) {
274 // TODO check for loops
275 LdapEntry group = doGetRole(groupDn);
276 if (group != null) {
277 allRoles.add(group);
278 collectGroups(group, allRoles);
279 }
280 }
281 }
282 }
283
284 /*
285 * HIERARCHY
286 */
287 @Override
288 public HierarchyUnit getHierarchyUnit(String path) {
289 LdapName dn = pathToName(path);
290 return directoryDao.doGetHierarchyUnit(dn);
291 }
292
293 @Override
294 public Iterable<HierarchyUnit> getDirectHierarchyUnits(boolean functionalOnly) {
295 return directoryDao.doGetDirectHierarchyUnits(baseDn, functionalOnly);
296 }
297
298 @Override
299 public String getHierarchyUnitName() {
300 return getName();
301 }
302
303 @Override
304 public HierarchyUnit getParent() {
305 return null;
306 }
307
308 @Override
309 public boolean isFunctional() {
310 return true;
311 }
312
313 @Override
314 public Directory getDirectory() {
315 return this;
316 }
317
318 /*
319 * PATHS
320 */
321
322 @Override
323 public String getContext() {
324 return getBaseDn().toString();
325 }
326
327 @Override
328 public String getName() {
329 return nameToSimple(getBaseDn(), ".");
330 }
331
332 protected String nameToRelativePath(LdapName dn) {
333 LdapName name = LdapNameUtils.relativeName(getBaseDn(), dn);
334 return nameToSimple(name, "/");
335 }
336
337 protected String nameToSimple(LdapName name, String separator) {
338 StringJoiner path = new StringJoiner(separator);
339 for (int i = 0; i < name.size(); i++) {
340 path.add(name.getRdn(i).getValue().toString());
341 }
342 return path.toString();
343
344 }
345
346 protected LdapName pathToName(String path) {
347 try {
348 LdapName name = (LdapName) getBaseDn().clone();
349 String[] segments = path.split("/");
350 Rdn parentRdn = null;
351 // segments[0] is the directory itself
352 for (int i = 0; i < segments.length; i++) {
353 String segment = segments[i];
354 // TODO make attr names configurable ?
355 String attr = path.startsWith("accounts/")/* IPA */ ? LdapAttrs.cn.name() : LdapAttrs.ou.name();
356 if (parentRdn != null) {
357 if (getUserBaseRdn().equals(parentRdn))
358 attr = LdapAttrs.uid.name();
359 else if (getGroupBaseRdn().equals(parentRdn))
360 attr = LdapAttrs.cn.name();
361 else if (getSystemRoleBaseRdn().equals(parentRdn))
362 attr = LdapAttrs.cn.name();
363 }
364 Rdn rdn = new Rdn(attr, segment);
365 name.add(rdn);
366 parentRdn = rdn;
367 }
368 return name;
369 } catch (InvalidNameException e) {
370 throw new IllegalStateException("Cannot get role " + path, e);
371 }
372
373 }
374
375 /*
376 * UTILITIES
377 */
378 protected boolean isExternal(LdapName name) {
379 return !name.startsWith(baseDn);
380 }
381
382 protected static boolean hasObjectClass(Attributes attrs, LdapObjs objectClass) {
383 return hasObjectClass(attrs, objectClass.name());
384 }
385
386 protected static boolean hasObjectClass(Attributes attrs, String objectClass) {
387 try {
388 Attribute attr = attrs.get(LdapAttrs.objectClass.name());
389 NamingEnumeration<?> en = attr.getAll();
390 while (en.hasMore()) {
391 String v = en.next().toString();
392 if (v.equalsIgnoreCase(objectClass))
393 return true;
394
395 }
396 return false;
397 } catch (NamingException e) {
398 throw new IllegalStateException("Cannot search for objectClass " + objectClass, e);
399 }
400 }
401
402 private static boolean readOnlyDefault(String uriStr) {
403 if (uriStr == null)
404 return true;
405 /// TODO make it more generic
406 URI uri;
407 try {
408 uri = new URI(uriStr.split(" ")[0]);
409 } catch (URISyntaxException e) {
410 throw new IllegalArgumentException(e);
411 }
412 if (uri.getScheme() == null)
413 return false;// assume relative file to be writable
414 if (uri.getScheme().equals(DirectoryConf.SCHEME_FILE)) {
415 File file = new File(uri);
416 if (file.exists())
417 return !file.canWrite();
418 else
419 return !file.getParentFile().canWrite();
420 } else if (uri.getScheme().equals(DirectoryConf.SCHEME_LDAP)) {
421 if (uri.getAuthority() != null)// assume writable if authenticated
422 return false;
423 } else if (uri.getScheme().equals(DirectoryConf.SCHEME_OS)) {
424 return true;
425 }
426 return true;// read only by default
427 }
428
429 /*
430 * AS AN ENTRY
431 */
432 public LdapEntry asLdapEntry() {
433 try {
434 return directoryDao.doGetEntry(baseDn);
435 } catch (NameNotFoundException e) {
436 throw new IllegalStateException("Cannot get " + baseDn + " entry", e);
437 }
438 }
439
440 public Dictionary<String, Object> getProperties() {
441 return asLdapEntry().getProperties();
442 }
443
444 /*
445 * ACCESSORS
446 */
447 @Override
448 public Optional<String> getRealm() {
449 Object realm = configProperties.get(DirectoryConf.realm.name());
450 if (realm == null)
451 return Optional.empty();
452 return Optional.of(realm.toString());
453 }
454
455 public LdapName getBaseDn() {
456 return (LdapName) baseDn.clone();
457 }
458
459 public boolean isReadOnly() {
460 return readOnly;
461 }
462
463 public boolean isDisabled() {
464 return disabled;
465 }
466
467 public Rdn getUserBaseRdn() {
468 return userBaseRdn;
469 }
470
471 public Rdn getGroupBaseRdn() {
472 return groupBaseRdn;
473 }
474
475 public Rdn getSystemRoleBaseRdn() {
476 return systemRoleBaseRdn;
477 }
478
479 // public Dictionary<String, Object> getConfigProperties() {
480 // return configProperties;
481 // }
482
483 public Dictionary<String, Object> cloneConfigProperties() {
484 return new Hashtable<>(configProperties);
485 }
486
487 public String getForcedPassword() {
488 return forcedPassword;
489 }
490
491 public boolean isScoped() {
492 return scoped;
493 }
494
495 public List<String> getCredentialAttributeIds() {
496 return credentialAttributeIds;
497 }
498
499 public String getUri() {
500 return uri;
501 }
502
503 public LdapDirectoryDao getDirectoryDao() {
504 return directoryDao;
505 }
506
507 /** dn can be null, in that case a default should be returned. */
508 public String getUserObjectClass() {
509 return userObjectClass;
510 }
511
512 public String getGroupObjectClass() {
513 return groupObjectClass;
514 }
515
516 public String getMemberAttributeId() {
517 return memberAttributeId;
518 }
519
520 /*
521 * OBJECT METHODS
522 */
523
524 @Override
525 public int hashCode() {
526 return baseDn.hashCode();
527 }
528
529 @Override
530 public String toString() {
531 return "Directory " + baseDn.toString();
532 }
533
534 }