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