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