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