]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/directory/ldap/AbstractLdapDirectory.java
Improve ACR attribute typing.
[lgpl/argeo-commons.git] / org.argeo.cms / src / org / argeo / cms / directory / ldap / 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.LdapAttr;
30 import org.argeo.api.acr.ldap.LdapObj;
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[] { LdapAttr.userPassword.name(), LdapAttr.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(LdapAttr.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 HierarchyUnit getDirectChild(Type type) {
304 // TODO factorise with hierarchy unit?
305 return switch (type) {
306 case ROLES -> getDirectoryDao().doGetHierarchyUnit((LdapName) getBaseDn().add(getSystemRoleBaseRdn()));
307 case PEOPLE -> getDirectoryDao().doGetHierarchyUnit((LdapName) getBaseDn().add(getUserBaseRdn()));
308 case GROUPS -> getDirectoryDao().doGetHierarchyUnit((LdapName) getBaseDn().add(getGroupBaseRdn()));
309 case FUNCTIONAL -> throw new IllegalArgumentException("Type must be a technical type");
310 };
311 }
312
313 @Override
314 public String getHierarchyUnitName() {
315 return getName();
316 }
317
318 @Override
319 public String getHierarchyUnitLabel(Locale locale) {
320 String key = LdapNameUtils.getLastRdn(getBaseDn()).getType();
321 Object value = LdapEntry.getLocalized(asLdapEntry().getProperties(), key, locale);
322 if (value == null)
323 value = getHierarchyUnitName();
324 assert value != null;
325 return value.toString();
326 }
327
328 @Override
329 public HierarchyUnit getParent() {
330 return null;
331 }
332
333 @Override
334 public boolean isType(Type type) {
335 return Type.FUNCTIONAL.equals(type);
336 }
337
338 @Override
339 public CmsDirectory getDirectory() {
340 return this;
341 }
342
343 @Override
344 public HierarchyUnit createHierarchyUnit(String path) {
345 checkEdit();
346 LdapEntryWorkingCopy wc = getWorkingCopy();
347 LdapName dn = pathToName(path);
348 if ((getDirectoryDao().entryExists(dn) && !wc.getDeletedData().containsKey(dn))
349 || wc.getNewData().containsKey(dn))
350 throw new IllegalArgumentException("Already a hierarchy unit " + path);
351 BasicAttributes attrs = new BasicAttributes(true);
352 attrs.put(LdapAttr.objectClass.name(), LdapObj.organizationalUnit.name());
353 Rdn nameRdn = dn.getRdn(dn.size() - 1);
354 // TODO deal with multiple attr RDN
355 attrs.put(nameRdn.getType(), nameRdn.getValue());
356 wc.getModifiedData().put(dn, attrs);
357 LdapHierarchyUnit newHierarchyUnit = new LdapHierarchyUnit(this, dn);
358 wc.getNewData().put(dn, newHierarchyUnit);
359 return newHierarchyUnit;
360 }
361
362 /*
363 * PATHS
364 */
365
366 @Override
367 public String getBase() {
368 return getBaseDn().toString();
369 }
370
371 @Override
372 public String getName() {
373 return nameToSimple(getBaseDn(), ".");
374 }
375
376 protected String nameToRelativePath(LdapName dn) {
377 LdapName name = LdapNameUtils.relativeName(getBaseDn(), dn);
378 return nameToSimple(name, "/");
379 }
380
381 protected String nameToSimple(LdapName name, String separator) {
382 StringJoiner path = new StringJoiner(separator);
383 for (int i = 0; i < name.size(); i++) {
384 path.add(name.getRdn(i).getValue().toString());
385 }
386 return path.toString();
387
388 }
389
390 protected LdapName pathToName(String path) {
391 try {
392 LdapName name = (LdapName) getBaseDn().clone();
393 String[] segments = path.split("/");
394 Rdn parentRdn = null;
395 // segments[0] is the directory itself
396 for (int i = 0; i < segments.length; i++) {
397 String segment = segments[i];
398 // TODO make attr names configurable ?
399 String attr = getDirectory().getRealm().isPresent()/* IPA */ ? LdapAttr.cn.name() : LdapAttr.ou.name();
400 if (parentRdn != null) {
401 if (getUserBaseRdn().equals(parentRdn))
402 attr = LdapAttr.uid.name();
403 else if (getGroupBaseRdn().equals(parentRdn))
404 attr = LdapAttr.cn.name();
405 else if (getSystemRoleBaseRdn().equals(parentRdn))
406 attr = LdapAttr.cn.name();
407 }
408 Rdn rdn = new Rdn(attr, segment);
409 name.add(rdn);
410 parentRdn = rdn;
411 }
412 return name;
413 } catch (InvalidNameException e) {
414 throw new IllegalStateException("Cannot convert " + path + " to LDAP name", e);
415 }
416
417 }
418
419 /*
420 * UTILITIES
421 */
422 protected boolean isExternal(LdapName name) {
423 return !name.startsWith(baseDn);
424 }
425
426 protected static boolean hasObjectClass(Attributes attrs, LdapObj objectClass) {
427 return hasObjectClass(attrs, objectClass.name());
428 }
429
430 protected static boolean hasObjectClass(Attributes attrs, String objectClass) {
431 try {
432 Attribute attr = attrs.get(LdapAttr.objectClass.name());
433 NamingEnumeration<?> en = attr.getAll();
434 while (en.hasMore()) {
435 String v = en.next().toString();
436 if (v.equalsIgnoreCase(objectClass))
437 return true;
438
439 }
440 return false;
441 } catch (NamingException e) {
442 throw new IllegalStateException("Cannot search for objectClass " + objectClass, e);
443 }
444 }
445
446 private static boolean readOnlyDefault(String uriStr) {
447 if (uriStr == null)
448 return true;
449 /// TODO make it more generic
450 URI uri;
451 try {
452 uri = new URI(uriStr.split(" ")[0]);
453 } catch (URISyntaxException e) {
454 throw new IllegalArgumentException(e);
455 }
456 if (uri.getScheme() == null)
457 return false;// assume relative file to be writable
458 if (uri.getScheme().equals(DirectoryConf.SCHEME_FILE)) {
459 File file = new File(uri);
460 if (file.exists())
461 return !file.canWrite();
462 else
463 return !file.getParentFile().canWrite();
464 } else if (uri.getScheme().equals(DirectoryConf.SCHEME_LDAP)) {
465 if (uri.getAuthority() != null)// assume writable if authenticated
466 return false;
467 } else if (uri.getScheme().equals(DirectoryConf.SCHEME_OS)) {
468 return true;
469 }
470 return true;// read only by default
471 }
472
473 /*
474 * AS AN ENTRY
475 */
476 public LdapEntry asLdapEntry() {
477 try {
478 return directoryDao.doGetEntry(baseDn);
479 } catch (NameNotFoundException e) {
480 throw new IllegalStateException("Cannot get " + baseDn + " entry", e);
481 }
482 }
483
484 public Dictionary<String, Object> getProperties() {
485 return asLdapEntry().getProperties();
486 }
487
488 /*
489 * ACCESSORS
490 */
491 @Override
492 public Optional<String> getRealm() {
493 Object realm = configProperties.get(DirectoryConf.realm.name());
494 if (realm == null)
495 return Optional.empty();
496 return Optional.of(realm.toString());
497 }
498
499 public LdapName getBaseDn() {
500 return (LdapName) baseDn.clone();
501 }
502
503 public boolean isReadOnly() {
504 return readOnly;
505 }
506
507 public boolean isDisabled() {
508 return disabled;
509 }
510
511 public boolean isAuthenticated() {
512 return authenticated;
513 }
514
515 public Rdn getUserBaseRdn() {
516 return userBaseRdn;
517 }
518
519 public Rdn getGroupBaseRdn() {
520 return groupBaseRdn;
521 }
522
523 public Rdn getSystemRoleBaseRdn() {
524 return systemRoleBaseRdn;
525 }
526
527 // public Dictionary<String, Object> getConfigProperties() {
528 // return configProperties;
529 // }
530
531 public Dictionary<String, Object> cloneConfigProperties() {
532 return new Hashtable<>(configProperties);
533 }
534
535 public String getForcedPassword() {
536 return forcedPassword;
537 }
538
539 public boolean isScoped() {
540 return scoped;
541 }
542
543 public List<String> getCredentialAttributeIds() {
544 return credentialAttributeIds;
545 }
546
547 public String getUri() {
548 return uri;
549 }
550
551 public LdapDirectoryDao getDirectoryDao() {
552 return directoryDao;
553 }
554
555 /** dn can be null, in that case a default should be returned. */
556 public String getUserObjectClass() {
557 return userObjectClass;
558 }
559
560 public String getGroupObjectClass() {
561 return groupObjectClass;
562 }
563
564 public String getMemberAttributeId() {
565 return memberAttributeId;
566 }
567
568 /*
569 * OBJECT METHODS
570 */
571
572 @Override
573 public int hashCode() {
574 return baseDn.hashCode();
575 }
576
577 @Override
578 public String toString() {
579 return "Directory " + baseDn.toString();
580 }
581
582 }