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