]> git.argeo.org Git - lgpl/argeo-commons.git/blob - useradmin/AbstractUserDirectory.java
Prepare next development cycle
[lgpl/argeo-commons.git] / useradmin / AbstractUserDirectory.java
1 package org.argeo.osgi.useradmin;
2
3 import static org.argeo.util.naming.LdapAttrs.objectClass;
4 import static org.argeo.util.naming.LdapObjs.extensibleObject;
5 import static org.argeo.util.naming.LdapObjs.inetOrgPerson;
6 import static org.argeo.util.naming.LdapObjs.organizationalPerson;
7 import static org.argeo.util.naming.LdapObjs.person;
8 import static org.argeo.util.naming.LdapObjs.top;
9
10 import java.io.File;
11 import java.net.URI;
12 import java.net.URISyntaxException;
13 import java.util.ArrayList;
14 import java.util.Arrays;
15 import java.util.Dictionary;
16 import java.util.Enumeration;
17 import java.util.Hashtable;
18 import java.util.Iterator;
19 import java.util.List;
20 import java.util.Optional;
21
22 import javax.naming.InvalidNameException;
23 import javax.naming.NameNotFoundException;
24 import javax.naming.NamingEnumeration;
25 import javax.naming.NamingException;
26 import javax.naming.directory.Attribute;
27 import javax.naming.directory.Attributes;
28 import javax.naming.directory.BasicAttribute;
29 import javax.naming.directory.BasicAttributes;
30 import javax.naming.ldap.LdapName;
31 import javax.naming.ldap.Rdn;
32
33 import org.argeo.osgi.transaction.WorkControl;
34 import org.argeo.util.naming.LdapAttrs;
35 import org.osgi.framework.Filter;
36 import org.osgi.framework.FrameworkUtil;
37 import org.osgi.framework.InvalidSyntaxException;
38 import org.osgi.service.useradmin.Authorization;
39 import org.osgi.service.useradmin.Role;
40 import org.osgi.service.useradmin.User;
41 import org.osgi.service.useradmin.UserAdmin;
42
43 /** Base class for a {@link UserDirectory}. */
44 abstract class AbstractUserDirectory implements UserAdmin, UserDirectory {
45 static final String SHARED_STATE_USERNAME = "javax.security.auth.login.name";
46 static final String SHARED_STATE_PASSWORD = "javax.security.auth.login.password";
47
48 private final Hashtable<String, Object> properties;
49 private final LdapName baseDn, userBaseDn, groupBaseDn;
50 private final String userObjectClass, userBase, groupObjectClass, groupBase;
51
52 private final boolean readOnly;
53 private final boolean disabled;
54 private final String uri;
55
56 private UserAdmin externalRoles;
57 // private List<String> indexedUserProperties = Arrays
58 // .asList(new String[] { LdapAttrs.uid.name(), LdapAttrs.mail.name(),
59 // LdapAttrs.cn.name() });
60
61 private final boolean scoped;
62
63 private String memberAttributeId = "member";
64 private List<String> credentialAttributeIds = Arrays
65 .asList(new String[] { LdapAttrs.userPassword.name(), LdapAttrs.authPassword.name() });
66
67 // Transaction
68 // private TransactionManager transactionManager;
69 private WorkControl transactionControl;
70 private WcXaResource xaResource = new WcXaResource(this);
71
72 private String forcedPassword;
73
74 AbstractUserDirectory(URI uriArg, Dictionary<String, ?> props, boolean scoped) {
75 this.scoped = scoped;
76 properties = new Hashtable<String, Object>();
77 for (Enumeration<String> keys = props.keys(); keys.hasMoreElements();) {
78 String key = keys.nextElement();
79 properties.put(key, props.get(key));
80 }
81
82 if (uriArg != null) {
83 uri = uriArg.toString();
84 // uri from properties is ignored
85 } else {
86 String uriStr = UserAdminConf.uri.getValue(properties);
87 if (uriStr == null)
88 uri = null;
89 else
90 uri = uriStr;
91 }
92
93 forcedPassword = UserAdminConf.forcedPassword.getValue(properties);
94
95 userObjectClass = UserAdminConf.userObjectClass.getValue(properties);
96 userBase = UserAdminConf.userBase.getValue(properties);
97 groupObjectClass = UserAdminConf.groupObjectClass.getValue(properties);
98 groupBase = UserAdminConf.groupBase.getValue(properties);
99 try {
100 baseDn = new LdapName(UserAdminConf.baseDn.getValue(properties));
101 userBaseDn = new LdapName(userBase + "," + baseDn);
102 groupBaseDn = new LdapName(groupBase + "," + baseDn);
103 } catch (InvalidNameException e) {
104 throw new IllegalArgumentException("Badly formated base DN " + UserAdminConf.baseDn.getValue(properties),
105 e);
106 }
107 String readOnlyStr = UserAdminConf.readOnly.getValue(properties);
108 if (readOnlyStr == null) {
109 readOnly = readOnlyDefault(uri);
110 properties.put(UserAdminConf.readOnly.name(), Boolean.toString(readOnly));
111 } else
112 readOnly = Boolean.parseBoolean(readOnlyStr);
113 String disabledStr = UserAdminConf.disabled.getValue(properties);
114 if (disabledStr != null)
115 disabled = Boolean.parseBoolean(disabledStr);
116 else
117 disabled = false;
118 }
119
120 /** Returns the groups this user is a direct member of. */
121 protected abstract List<LdapName> getDirectGroups(LdapName dn);
122
123 protected abstract Boolean daoHasRole(LdapName dn);
124
125 protected abstract DirectoryUser daoGetRole(LdapName key) throws NameNotFoundException;
126
127 protected abstract List<DirectoryUser> doGetRoles(LdapName searchBase, Filter f, boolean deep);
128
129 protected abstract AbstractUserDirectory scope(User user);
130
131 public void init() {
132
133 }
134
135 public void destroy() {
136
137 }
138
139 @Override
140 public String getBasePath() {
141 return getBaseDn().toString();
142 }
143
144 @Override
145 public Optional<String> getRealm() {
146 Object realm = getProperties().get(UserAdminConf.realm.name());
147 if (realm == null)
148 return Optional.empty();
149 return Optional.of(realm.toString());
150 }
151
152 protected boolean isEditing() {
153 return xaResource.wc() != null;
154 }
155
156 protected UserDirectoryWorkingCopy getWorkingCopy() {
157 UserDirectoryWorkingCopy wc = xaResource.wc();
158 if (wc == null)
159 return null;
160 return wc;
161 }
162
163 protected void checkEdit() {
164 if (xaResource.wc() == null) {
165 try {
166 transactionControl.getWorkContext().registerXAResource(xaResource, null);
167 } catch (Exception e) {
168 throw new IllegalStateException("Cannot enlist " + xaResource, e);
169 }
170 } else {
171 }
172 }
173
174 protected List<Role> getAllRoles(DirectoryUser user) {
175 List<Role> allRoles = new ArrayList<Role>();
176 if (user != null) {
177 collectRoles(user, allRoles);
178 allRoles.add(user);
179 } else
180 collectAnonymousRoles(allRoles);
181 return allRoles;
182 }
183
184 private void collectRoles(DirectoryUser user, List<Role> allRoles) {
185 Attributes attrs = user.getAttributes();
186 // TODO centralize attribute name
187 Attribute memberOf = attrs.get(LdapAttrs.memberOf.name());
188 // if user belongs to this directory, we only check meberOf
189 if (memberOf != null && user.getDn().startsWith(getBaseDn())) {
190 try {
191 NamingEnumeration<?> values = memberOf.getAll();
192 while (values.hasMore()) {
193 Object value = values.next();
194 LdapName groupDn = new LdapName(value.toString());
195 DirectoryUser group = doGetRole(groupDn);
196 if (group != null)
197 allRoles.add(group);
198 }
199 } catch (NamingException e) {
200 throw new IllegalStateException("Cannot get memberOf groups for " + user, e);
201 }
202 } else {
203 for (LdapName groupDn : getDirectGroups(user.getDn())) {
204 // TODO check for loops
205 DirectoryUser group = doGetRole(groupDn);
206 if (group != null) {
207 allRoles.add(group);
208 collectRoles(group, allRoles);
209 }
210 }
211 }
212 }
213
214 private void collectAnonymousRoles(List<Role> allRoles) {
215 // TODO gather anonymous roles
216 }
217
218 // USER ADMIN
219 @Override
220 public Role getRole(String name) {
221 return doGetRole(toLdapName(name));
222 }
223
224 protected DirectoryUser doGetRole(LdapName dn) {
225 UserDirectoryWorkingCopy wc = getWorkingCopy();
226 DirectoryUser user;
227 try {
228 user = daoGetRole(dn);
229 } catch (NameNotFoundException e) {
230 user = null;
231 }
232 if (wc != null) {
233 if (user == null && wc.getNewUsers().containsKey(dn))
234 user = wc.getNewUsers().get(dn);
235 else if (wc.getDeletedUsers().containsKey(dn))
236 user = null;
237 }
238 return user;
239 }
240
241 @Override
242 public Role[] getRoles(String filter) throws InvalidSyntaxException {
243 // UserDirectoryWorkingCopy wc = getWorkingCopy();
244 // Filter f = filter != null ? FrameworkUtil.createFilter(filter) : null;
245 // List<DirectoryUser> res = doGetRoles(getBaseDn(), f, true);
246 // if (wc != null) {
247 // for (Iterator<DirectoryUser> it = res.iterator(); it.hasNext();) {
248 // DirectoryUser user = it.next();
249 // LdapName dn = user.getDn();
250 // if (wc.getDeletedUsers().containsKey(dn))
251 // it.remove();
252 // }
253 // for (DirectoryUser user : wc.getNewUsers().values()) {
254 // if (f == null || f.match(user.getProperties()))
255 // res.add(user);
256 // }
257 // // no need to check modified users,
258 // // since doGetRoles was already based on the modified attributes
259 // }
260 List<? extends Role> res = getRoles(getBaseDn(), filter, true);
261 return res.toArray(new Role[res.size()]);
262 }
263
264 List<DirectoryUser> getRoles(LdapName searchBase, String filter, boolean deep) throws InvalidSyntaxException {
265 UserDirectoryWorkingCopy wc = getWorkingCopy();
266 Filter f = filter != null ? FrameworkUtil.createFilter(filter) : null;
267 List<DirectoryUser> res = doGetRoles(searchBase, f, deep);
268 if (wc != null) {
269 for (Iterator<DirectoryUser> it = res.iterator(); it.hasNext();) {
270 DirectoryUser user = it.next();
271 LdapName dn = user.getDn();
272 if (wc.getDeletedUsers().containsKey(dn))
273 it.remove();
274 }
275 for (DirectoryUser user : wc.getNewUsers().values()) {
276 if (f == null || f.match(user.getProperties()))
277 res.add(user);
278 }
279 // no need to check modified users,
280 // since doGetRoles was already based on the modified attributes
281 }
282
283 // if non deep we also search users and groups
284 if (!deep) {
285 try {
286 if (!(searchBase.endsWith(new LdapName(getUserBase()))
287 || searchBase.endsWith(new LdapName(getGroupBase())))) {
288 LdapName usersBase = (LdapName) ((LdapName) searchBase.clone()).add(getUserBase());
289 res.addAll(getRoles(usersBase, filter, false));
290 LdapName groupsBase = (LdapName) ((LdapName) searchBase.clone()).add(getGroupBase());
291 res.addAll(getRoles(groupsBase, filter, false));
292 }
293 } catch (InvalidNameException e) {
294 throw new IllegalStateException("Cannot search users and groups", e);
295 }
296 }
297 return res;
298 }
299
300 @Override
301 public User getUser(String key, String value) {
302 // TODO check value null or empty
303 List<DirectoryUser> collectedUsers = new ArrayList<DirectoryUser>();
304 if (key != null) {
305 doGetUser(key, value, collectedUsers);
306 } else {
307 throw new IllegalArgumentException("Key cannot be null");
308 }
309
310 if (collectedUsers.size() == 1) {
311 return collectedUsers.get(0);
312 } else if (collectedUsers.size() > 1) {
313 // log.warn(collectedUsers.size() + " users for " + (key != null ? key + "=" :
314 // "") + value);
315 }
316 return null;
317 }
318
319 protected void doGetUser(String key, String value, List<DirectoryUser> collectedUsers) {
320 try {
321 Filter f = FrameworkUtil.createFilter("(" + key + "=" + value + ")");
322 List<DirectoryUser> users = doGetRoles(getBaseDn(), f, true);
323 collectedUsers.addAll(users);
324 } catch (InvalidSyntaxException e) {
325 throw new IllegalArgumentException("Cannot get user with " + key + "=" + value, e);
326 }
327 }
328
329 @Override
330 public Authorization getAuthorization(User user) {
331 if (user == null || user instanceof DirectoryUser) {
332 return new LdifAuthorization(user, getAllRoles((DirectoryUser) user));
333 } else {
334 // bind
335 AbstractUserDirectory scopedUserAdmin = scope(user);
336 try {
337 DirectoryUser directoryUser = (DirectoryUser) scopedUserAdmin.getRole(user.getName());
338 if (directoryUser == null)
339 throw new IllegalStateException("No scoped user found for " + user);
340 LdifAuthorization authorization = new LdifAuthorization(directoryUser,
341 scopedUserAdmin.getAllRoles(directoryUser));
342 return authorization;
343 } finally {
344 scopedUserAdmin.destroy();
345 }
346 }
347 }
348
349 @Override
350 public Role createRole(String name, int type) {
351 checkEdit();
352 UserDirectoryWorkingCopy wc = getWorkingCopy();
353 LdapName dn = toLdapName(name);
354 if ((daoHasRole(dn) && !wc.getDeletedUsers().containsKey(dn)) || wc.getNewUsers().containsKey(dn))
355 throw new IllegalArgumentException("Already a role " + name);
356 BasicAttributes attrs = new BasicAttributes(true);
357 // attrs.put(LdifName.dn.name(), dn.toString());
358 Rdn nameRdn = dn.getRdn(dn.size() - 1);
359 // TODO deal with multiple attr RDN
360 attrs.put(nameRdn.getType(), nameRdn.getValue());
361 if (wc.getDeletedUsers().containsKey(dn)) {
362 wc.getDeletedUsers().remove(dn);
363 wc.getModifiedUsers().put(dn, attrs);
364 return getRole(name);
365 } else {
366 wc.getModifiedUsers().put(dn, attrs);
367 DirectoryUser newRole = newRole(dn, type, attrs);
368 wc.getNewUsers().put(dn, newRole);
369 return newRole;
370 }
371 }
372
373 protected DirectoryUser newRole(LdapName dn, int type, Attributes attrs) {
374 LdifUser newRole;
375 BasicAttribute objClass = new BasicAttribute(objectClass.name());
376 if (type == Role.USER) {
377 String userObjClass = newUserObjectClass(dn);
378 objClass.add(userObjClass);
379 if (inetOrgPerson.name().equals(userObjClass)) {
380 objClass.add(organizationalPerson.name());
381 objClass.add(person.name());
382 } else if (organizationalPerson.name().equals(userObjClass)) {
383 objClass.add(person.name());
384 }
385 objClass.add(top.name());
386 objClass.add(extensibleObject.name());
387 attrs.put(objClass);
388 newRole = new LdifUser(this, dn, attrs);
389 } else if (type == Role.GROUP) {
390 String groupObjClass = getGroupObjectClass();
391 objClass.add(groupObjClass);
392 // objClass.add(LdifName.extensibleObject.name());
393 objClass.add(top.name());
394 attrs.put(objClass);
395 newRole = new LdifGroup(this, dn, attrs);
396 } else
397 throw new IllegalArgumentException("Unsupported type " + type);
398 return newRole;
399 }
400
401 @Override
402 public boolean removeRole(String name) {
403 checkEdit();
404 UserDirectoryWorkingCopy wc = getWorkingCopy();
405 LdapName dn = toLdapName(name);
406 boolean actuallyDeleted;
407 if (daoHasRole(dn) || wc.getNewUsers().containsKey(dn)) {
408 DirectoryUser user = (DirectoryUser) getRole(name);
409 wc.getDeletedUsers().put(dn, user);
410 actuallyDeleted = true;
411 } else {// just removing from groups (e.g. system roles)
412 actuallyDeleted = false;
413 }
414 for (LdapName groupDn : getDirectGroups(dn)) {
415 DirectoryUser group = doGetRole(groupDn);
416 group.getAttributes().get(getMemberAttributeId()).remove(dn.toString());
417 }
418 return actuallyDeleted;
419 }
420
421 // TRANSACTION
422 protected void prepare(UserDirectoryWorkingCopy wc) {
423
424 }
425
426 protected void commit(UserDirectoryWorkingCopy wc) {
427
428 }
429
430 protected void rollback(UserDirectoryWorkingCopy wc) {
431
432 }
433
434 /*
435 * HIERARCHY
436 */
437 @Override
438 public int getHierarchyChildCount() {
439 return 0;
440 }
441
442 @Override
443 public HierarchyUnit getHierarchyChild(int i) {
444 throw new IllegalArgumentException("No child hierarchy unit available");
445 }
446
447 @Override
448 public HierarchyUnit getParent() {
449 return null;
450 }
451
452 @Override
453 public int getHierarchyUnitType() {
454 return 0;
455 }
456
457 @Override
458 public String getHierarchyUnitName() {
459 String name = LdapNameUtils.getLastRdnAsString(baseDn);
460 // TODO check ou, o, etc.
461 return name;
462 }
463
464 @Override
465 public HierarchyUnit getHierarchyUnit(String path) {
466 return null;
467 }
468
469 @Override
470 public HierarchyUnit getHierarchyUnit(Role role) {
471 return null;
472 }
473
474 @Override
475 public List<? extends Role> getRoles(String filter, boolean deep) {
476 try {
477 return getRoles(getBaseDn(), filter, deep);
478 } catch (InvalidSyntaxException e) {
479 throw new IllegalArgumentException("Cannot filter " + filter + " " + getBaseDn(), e);
480 }
481 }
482
483 // GETTERS
484 protected String getMemberAttributeId() {
485 return memberAttributeId;
486 }
487
488 protected List<String> getCredentialAttributeIds() {
489 return credentialAttributeIds;
490 }
491
492 protected String getUri() {
493 return uri;
494 }
495
496 private static boolean readOnlyDefault(String uriStr) {
497 if (uriStr == null)
498 return true;
499 /// TODO make it more generic
500 URI uri;
501 try {
502 uri = new URI(uriStr.split(" ")[0]);
503 } catch (URISyntaxException e) {
504 throw new IllegalArgumentException(e);
505 }
506 if (uri.getScheme() == null)
507 return false;// assume relative file to be writable
508 if (uri.getScheme().equals(UserAdminConf.SCHEME_FILE)) {
509 File file = new File(uri);
510 if (file.exists())
511 return !file.canWrite();
512 else
513 return !file.getParentFile().canWrite();
514 } else if (uri.getScheme().equals(UserAdminConf.SCHEME_LDAP)) {
515 if (uri.getAuthority() != null)// assume writable if authenticated
516 return false;
517 } else if (uri.getScheme().equals(UserAdminConf.SCHEME_OS)) {
518 return true;
519 }
520 return true;// read only by default
521 }
522
523 public boolean isReadOnly() {
524 return readOnly;
525 }
526
527 public boolean isDisabled() {
528 return disabled;
529 }
530
531 protected UserAdmin getExternalRoles() {
532 return externalRoles;
533 }
534
535 protected int roleType(LdapName dn) {
536 if (dn.startsWith(groupBaseDn))
537 return Role.GROUP;
538 else if (dn.startsWith(userBaseDn))
539 return Role.USER;
540 else
541 return Role.GROUP;
542 }
543
544 /** dn can be null, in that case a default should be returned. */
545 public String getUserObjectClass() {
546 return userObjectClass;
547 }
548
549 public String getUserBase() {
550 return userBase;
551 }
552
553 protected String newUserObjectClass(LdapName dn) {
554 return getUserObjectClass();
555 }
556
557 public String getGroupObjectClass() {
558 return groupObjectClass;
559 }
560
561 public String getGroupBase() {
562 return groupBase;
563 }
564
565 public LdapName getBaseDn() {
566 return (LdapName) baseDn.clone();
567 }
568
569 public Dictionary<String, Object> getProperties() {
570 return properties;
571 }
572
573 public Dictionary<String, Object> cloneProperties() {
574 return new Hashtable<>(properties);
575 }
576
577 public void setExternalRoles(UserAdmin externalRoles) {
578 this.externalRoles = externalRoles;
579 }
580
581 // public void setTransactionManager(TransactionManager transactionManager) {
582 // this.transactionManager = transactionManager;
583 // }
584
585 public String getForcedPassword() {
586 return forcedPassword;
587 }
588
589 public void setTransactionControl(WorkControl transactionControl) {
590 this.transactionControl = transactionControl;
591 }
592
593 public WcXaResource getXaResource() {
594 return xaResource;
595 }
596
597 public boolean isScoped() {
598 return scoped;
599 }
600
601 @Override
602 public int hashCode() {
603 return baseDn.hashCode();
604 }
605
606 @Override
607 public String toString() {
608 return "User Directory " + baseDn.toString();
609 }
610
611 /*
612 * STATIC UTILITIES
613 */
614 static LdapName toLdapName(String name) {
615 try {
616 return new LdapName(name);
617 } catch (InvalidNameException e) {
618 throw new IllegalArgumentException(name + " is not an LDAP name", e);
619 }
620 }
621 }