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