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