1 package org
.argeo
.osgi
.useradmin
;
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
;
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 import java
.util
.StringJoiner
;
23 import javax
.naming
.InvalidNameException
;
24 import javax
.naming
.NameNotFoundException
;
25 import javax
.naming
.NamingEnumeration
;
26 import javax
.naming
.NamingException
;
27 import javax
.naming
.directory
.Attribute
;
28 import javax
.naming
.directory
.Attributes
;
29 import javax
.naming
.directory
.BasicAttribute
;
30 import javax
.naming
.directory
.BasicAttributes
;
31 import javax
.naming
.ldap
.LdapName
;
32 import javax
.naming
.ldap
.Rdn
;
33 import javax
.transaction
.xa
.XAResource
;
35 import org
.argeo
.util
.naming
.LdapAttrs
;
36 import org
.argeo
.util
.naming
.LdapObjs
;
37 import org
.argeo
.util
.transaction
.WorkControl
;
38 import org
.argeo
.util
.transaction
.WorkingCopyProcessor
;
39 import org
.argeo
.util
.transaction
.WorkingCopyXaResource
;
40 import org
.argeo
.util
.transaction
.XAResourceProvider
;
41 import org
.osgi
.framework
.Filter
;
42 import org
.osgi
.framework
.FrameworkUtil
;
43 import org
.osgi
.framework
.InvalidSyntaxException
;
44 import org
.osgi
.service
.useradmin
.Authorization
;
45 import org
.osgi
.service
.useradmin
.Role
;
46 import org
.osgi
.service
.useradmin
.User
;
47 import org
.osgi
.service
.useradmin
.UserAdmin
;
49 /** Base class for a {@link UserDirectory}. */
50 abstract class AbstractUserDirectory
51 implements UserAdmin
, UserDirectory
, WorkingCopyProcessor
<DirectoryUserWorkingCopy
>, XAResourceProvider
{
52 static final String SHARED_STATE_USERNAME
= "javax.security.auth.login.name";
53 static final String SHARED_STATE_PASSWORD
= "javax.security.auth.login.password";
55 private final Hashtable
<String
, Object
> properties
;
56 private final LdapName baseDn
;
57 // private final LdapName userBaseDn, groupBaseDn;
58 private final Rdn userBaseRdn
, groupBaseRdn
, systemRoleBaseRdn
;
59 private final String userObjectClass
, groupObjectClass
;
61 private final boolean readOnly
;
62 private final boolean disabled
;
63 private final String uri
;
65 private UserAdmin externalRoles
;
66 // private List<String> indexedUserProperties = Arrays
67 // .asList(new String[] { LdapAttrs.uid.name(), LdapAttrs.mail.name(),
68 // LdapAttrs.cn.name() });
70 private final boolean scoped
;
72 private String memberAttributeId
= "member";
73 private List
<String
> credentialAttributeIds
= Arrays
74 .asList(new String
[] { LdapAttrs
.userPassword
.name(), LdapAttrs
.authPassword
.name() });
77 // private TransactionManager transactionManager;
78 private WorkControl transactionControl
;
79 private WorkingCopyXaResource
<DirectoryUserWorkingCopy
> xaResource
= new WorkingCopyXaResource
<>(this);
81 private String forcedPassword
;
83 AbstractUserDirectory(URI uriArg
, Dictionary
<String
, ?
> props
, boolean scoped
) {
85 properties
= new Hashtable
<String
, Object
>();
86 for (Enumeration
<String
> keys
= props
.keys(); keys
.hasMoreElements();) {
87 String key
= keys
.nextElement();
88 properties
.put(key
, props
.get(key
));
92 uri
= uriArg
.toString();
93 // uri from properties is ignored
95 String uriStr
= UserAdminConf
.uri
.getValue(properties
);
102 forcedPassword
= UserAdminConf
.forcedPassword
.getValue(properties
);
104 userObjectClass
= UserAdminConf
.userObjectClass
.getValue(properties
);
105 String userBase
= UserAdminConf
.userBase
.getValue(properties
);
106 groupObjectClass
= UserAdminConf
.groupObjectClass
.getValue(properties
);
107 String groupBase
= UserAdminConf
.groupBase
.getValue(properties
);
108 String systemRoleBase
= UserAdminConf
.systemRoleBase
.getValue(properties
);
110 baseDn
= new LdapName(UserAdminConf
.baseDn
.getValue(properties
));
111 userBaseRdn
= new Rdn(userBase
);
112 // userBaseDn = new LdapName(userBase + "," + baseDn);
113 groupBaseRdn
= new Rdn(groupBase
);
114 // groupBaseDn = new LdapName(groupBase + "," + baseDn);
115 systemRoleBaseRdn
= new Rdn(systemRoleBase
);
116 } catch (InvalidNameException e
) {
117 throw new IllegalArgumentException("Badly formated base DN " + UserAdminConf
.baseDn
.getValue(properties
),
122 String readOnlyStr
= UserAdminConf
.readOnly
.getValue(properties
);
123 if (readOnlyStr
== null) {
124 readOnly
= readOnlyDefault(uri
);
125 properties
.put(UserAdminConf
.readOnly
.name(), Boolean
.toString(readOnly
));
127 readOnly
= Boolean
.parseBoolean(readOnlyStr
);
130 String disabledStr
= UserAdminConf
.disabled
.getValue(properties
);
131 if (disabledStr
!= null)
132 disabled
= Boolean
.parseBoolean(disabledStr
);
141 /** Returns the groups this user is a direct member of. */
142 protected abstract List
<LdapName
> getDirectGroups(LdapName dn
);
144 protected abstract Boolean
daoHasRole(LdapName dn
);
146 protected abstract DirectoryUser
daoGetRole(LdapName key
) throws NameNotFoundException
;
148 protected abstract List
<DirectoryUser
> doGetRoles(LdapName searchBase
, Filter f
, boolean deep
);
150 protected abstract AbstractUserDirectory
scope(User user
);
152 protected abstract HierarchyUnit
doGetHierarchyUnit(LdapName dn
);
154 protected abstract Iterable
<HierarchyUnit
> doGetDirectHierarchyUnits(LdapName searchBase
, boolean functionalOnly
);
164 public void destroy() {
173 public String
getContext() {
174 return getBaseDn().toString();
178 public String
getName() {
179 return nameToSimple(getBaseDn(), ".");
183 public String
getRolePath(Role role
) {
184 return nameToRelativePath(((DirectoryUser
) role
).getDn());
188 public String
getRoleSimpleName(Role role
) {
189 LdapName dn
= LdapNameUtils
.toLdapName(role
.getName());
190 String name
= LdapNameUtils
.getLastRdnValue(dn
);
194 protected String
nameToRelativePath(LdapName dn
) {
195 LdapName name
= LdapNameUtils
.relativeName(getBaseDn(), dn
);
196 return nameToSimple(name
, "/");
199 protected String
nameToSimple(LdapName name
, String separator
) {
200 StringJoiner path
= new StringJoiner(separator
);
201 for (int i
= 0; i
< name
.size(); i
++) {
202 path
.add(name
.getRdn(i
).getValue().toString());
204 return path
.toString();
208 protected LdapName
pathToName(String path
) {
210 LdapName name
= (LdapName
) getBaseDn().clone();
211 String
[] segments
= path
.split("/");
212 Rdn parentRdn
= null;
213 for (String segment
: segments
) {
214 // TODO make attr names configurable ?
215 String attr
= LdapAttrs
.ou
.name();
216 if (parentRdn
!= null) {
217 if (getUserBaseRdn().equals(parentRdn
))
218 attr
= LdapAttrs
.uid
.name();
219 else if (getGroupBaseRdn().equals(parentRdn
))
220 attr
= LdapAttrs
.cn
.name();
221 else if (getSystemRoleBaseRdn().equals(parentRdn
))
222 attr
= LdapAttrs
.cn
.name();
224 Rdn rdn
= new Rdn(attr
, segment
);
229 } catch (InvalidNameException e
) {
230 throw new IllegalStateException("Cannot get role " + path
, e
);
236 public Role
getRoleByPath(String path
) {
237 return doGetRole(pathToName(path
));
241 public Optional
<String
> getRealm() {
242 Object realm
= getProperties().get(UserAdminConf
.realm
.name());
244 return Optional
.empty();
245 return Optional
.of(realm
.toString());
252 protected boolean isEditing() {
253 return xaResource
.wc() != null;
256 protected DirectoryUserWorkingCopy
getWorkingCopy() {
257 DirectoryUserWorkingCopy wc
= xaResource
.wc();
263 protected void checkEdit() {
264 if (xaResource
.wc() == null) {
266 transactionControl
.getWorkContext().registerXAResource(xaResource
, null);
267 } catch (Exception e
) {
268 throw new IllegalStateException("Cannot enlist " + xaResource
, e
);
274 protected List
<Role
> getAllRoles(DirectoryUser user
) {
275 List
<Role
> allRoles
= new ArrayList
<Role
>();
277 collectRoles(user
, allRoles
);
280 collectAnonymousRoles(allRoles
);
284 private void collectRoles(DirectoryUser user
, List
<Role
> allRoles
) {
285 Attributes attrs
= user
.getAttributes();
286 // TODO centralize attribute name
287 Attribute memberOf
= attrs
.get(LdapAttrs
.memberOf
.name());
288 // if user belongs to this directory, we only check meberOf
289 if (memberOf
!= null && user
.getDn().startsWith(getBaseDn())) {
291 NamingEnumeration
<?
> values
= memberOf
.getAll();
292 while (values
.hasMore()) {
293 Object value
= values
.next();
294 LdapName groupDn
= new LdapName(value
.toString());
295 DirectoryUser group
= doGetRole(groupDn
);
299 } catch (NamingException e
) {
300 throw new IllegalStateException("Cannot get memberOf groups for " + user
, e
);
303 for (LdapName groupDn
: getDirectGroups(user
.getDn())) {
304 // TODO check for loops
305 DirectoryUser group
= doGetRole(groupDn
);
308 collectRoles(group
, allRoles
);
314 private void collectAnonymousRoles(List
<Role
> allRoles
) {
315 // TODO gather anonymous roles
320 public Role
getRole(String name
) {
321 return doGetRole(toLdapName(name
));
324 protected DirectoryUser
doGetRole(LdapName dn
) {
325 DirectoryUserWorkingCopy wc
= getWorkingCopy();
328 user
= daoGetRole(dn
);
329 } catch (NameNotFoundException e
) {
333 if (user
== null && wc
.getNewData().containsKey(dn
))
334 user
= wc
.getNewData().get(dn
);
335 else if (wc
.getDeletedData().containsKey(dn
))
342 public Role
[] getRoles(String filter
) throws InvalidSyntaxException
{
343 List
<?
extends Role
> res
= getRoles(getBaseDn(), filter
, true);
344 return res
.toArray(new Role
[res
.size()]);
347 List
<DirectoryUser
> getRoles(LdapName searchBase
, String filter
, boolean deep
) throws InvalidSyntaxException
{
348 DirectoryUserWorkingCopy wc
= getWorkingCopy();
349 Filter f
= filter
!= null ? FrameworkUtil
.createFilter(filter
) : null;
350 List
<DirectoryUser
> res
= doGetRoles(searchBase
, f
, deep
);
352 for (Iterator
<DirectoryUser
> it
= res
.iterator(); it
.hasNext();) {
353 DirectoryUser user
= it
.next();
354 LdapName dn
= user
.getDn();
355 if (wc
.getDeletedData().containsKey(dn
))
358 for (DirectoryUser user
: wc
.getNewData().values()) {
359 if (f
== null || f
.match(user
.getProperties()))
362 // no need to check modified users,
363 // since doGetRoles was already based on the modified attributes
366 // if non deep we also search users and groups
369 // if (!(searchBase.endsWith(new LdapName(getUserBase()))
370 // || searchBase.endsWith(new LdapName(getGroupBase())))) {
371 // LdapName usersBase = (LdapName) ((LdapName) searchBase.clone()).add(getUserBase());
372 // res.addAll(getRoles(usersBase, filter, false));
373 // LdapName groupsBase = (LdapName) ((LdapName) searchBase.clone()).add(getGroupBase());
374 // res.addAll(getRoles(groupsBase, filter, false));
376 // } catch (InvalidNameException e) {
377 // throw new IllegalStateException("Cannot search users and groups", e);
384 public User
getUser(String key
, String value
) {
385 // TODO check value null or empty
386 List
<DirectoryUser
> collectedUsers
= new ArrayList
<DirectoryUser
>();
388 doGetUser(key
, value
, collectedUsers
);
390 throw new IllegalArgumentException("Key cannot be null");
393 if (collectedUsers
.size() == 1) {
394 return collectedUsers
.get(0);
395 } else if (collectedUsers
.size() > 1) {
396 // log.warn(collectedUsers.size() + " users for " + (key != null ? key + "=" :
402 protected void doGetUser(String key
, String value
, List
<DirectoryUser
> collectedUsers
) {
404 Filter f
= FrameworkUtil
.createFilter("(" + key
+ "=" + value
+ ")");
405 List
<DirectoryUser
> users
= doGetRoles(getBaseDn(), f
, true);
406 collectedUsers
.addAll(users
);
407 } catch (InvalidSyntaxException e
) {
408 throw new IllegalArgumentException("Cannot get user with " + key
+ "=" + value
, e
);
413 public Authorization
getAuthorization(User user
) {
414 if (user
== null || user
instanceof DirectoryUser
) {
415 return new LdifAuthorization(user
, getAllRoles((DirectoryUser
) user
));
418 AbstractUserDirectory scopedUserAdmin
= scope(user
);
420 DirectoryUser directoryUser
= (DirectoryUser
) scopedUserAdmin
.getRole(user
.getName());
421 if (directoryUser
== null)
422 throw new IllegalStateException("No scoped user found for " + user
);
423 LdifAuthorization authorization
= new LdifAuthorization(directoryUser
,
424 scopedUserAdmin
.getAllRoles(directoryUser
));
425 return authorization
;
427 scopedUserAdmin
.destroy();
433 public Role
createRole(String name
, int type
) {
435 DirectoryUserWorkingCopy wc
= getWorkingCopy();
436 LdapName dn
= toLdapName(name
);
437 if ((daoHasRole(dn
) && !wc
.getDeletedData().containsKey(dn
)) || wc
.getNewData().containsKey(dn
))
438 throw new IllegalArgumentException("Already a role " + name
);
439 BasicAttributes attrs
= new BasicAttributes(true);
440 // attrs.put(LdifName.dn.name(), dn.toString());
441 Rdn nameRdn
= dn
.getRdn(dn
.size() - 1);
442 // TODO deal with multiple attr RDN
443 attrs
.put(nameRdn
.getType(), nameRdn
.getValue());
444 if (wc
.getDeletedData().containsKey(dn
)) {
445 wc
.getDeletedData().remove(dn
);
446 wc
.getModifiedData().put(dn
, attrs
);
447 return getRole(name
);
449 wc
.getModifiedData().put(dn
, attrs
);
450 DirectoryUser newRole
= newRole(dn
, type
, attrs
);
451 wc
.getNewData().put(dn
, newRole
);
456 protected DirectoryUser
newRole(LdapName dn
, int type
, Attributes attrs
) {
457 DirectoryUser newRole
;
458 BasicAttribute objClass
= new BasicAttribute(objectClass
.name());
459 if (type
== Role
.USER
) {
460 String userObjClass
= newUserObjectClass(dn
);
461 objClass
.add(userObjClass
);
462 if (inetOrgPerson
.name().equals(userObjClass
)) {
463 objClass
.add(organizationalPerson
.name());
464 objClass
.add(person
.name());
465 } else if (organizationalPerson
.name().equals(userObjClass
)) {
466 objClass
.add(person
.name());
468 objClass
.add(top
.name());
469 objClass
.add(extensibleObject
.name());
471 newRole
= newUser(dn
, attrs
);
472 } else if (type
== Role
.GROUP
) {
473 String groupObjClass
= getGroupObjectClass();
474 objClass
.add(groupObjClass
);
475 // objClass.add(LdifName.extensibleObject.name());
476 objClass
.add(top
.name());
478 newRole
= newGroup(dn
, attrs
);
480 throw new IllegalArgumentException("Unsupported type " + type
);
485 public boolean removeRole(String name
) {
487 DirectoryUserWorkingCopy wc
= getWorkingCopy();
488 LdapName dn
= toLdapName(name
);
489 boolean actuallyDeleted
;
490 if (daoHasRole(dn
) || wc
.getNewData().containsKey(dn
)) {
491 DirectoryUser user
= (DirectoryUser
) getRole(name
);
492 wc
.getDeletedData().put(dn
, user
);
493 actuallyDeleted
= true;
494 } else {// just removing from groups (e.g. system roles)
495 actuallyDeleted
= false;
497 for (LdapName groupDn
: getDirectGroups(dn
)) {
498 DirectoryUser group
= doGetRole(groupDn
);
499 group
.getAttributes().get(getMemberAttributeId()).remove(dn
.toString());
501 return actuallyDeleted
;
508 public DirectoryUserWorkingCopy
newWorkingCopy() {
509 return new DirectoryUserWorkingCopy();
516 public HierarchyUnit
getHierarchyUnit(String path
) {
517 LdapName dn
= pathToName(path
);
518 return doGetHierarchyUnit(dn
);
522 public HierarchyUnit
getHierarchyUnit(Role role
) {
523 LdapName dn
= LdapNameUtils
.toLdapName(role
.getName());
524 LdapName huDn
= LdapNameUtils
.getParent(dn
);
525 HierarchyUnit hierarchyUnit
= doGetHierarchyUnit(huDn
);
526 if (hierarchyUnit
== null)
527 throw new IllegalStateException("No hierarchy unit found for " + role
);
528 return hierarchyUnit
;
532 public Iterable
<HierarchyUnit
> getDirectHierarchyUnits(boolean functionalOnly
) {
533 return doGetDirectHierarchyUnits(baseDn
, functionalOnly
);
539 protected DirectoryUser
newUser(LdapName name
, Attributes attrs
) {
540 // TODO support devices, applications, etc.
541 return new LdifUser
.LdifPerson(this, name
, attrs
);
544 protected DirectoryGroup
newGroup(LdapName name
, Attributes attrs
) {
545 if (LdapNameUtils
.getParentRdn(name
).equals(getSystemRoleBaseRdn()))
546 return new LdifGroup
.LdifSystemPermissions(this, name
, attrs
);
548 if (hasObjectClass(attrs
, LdapObjs
.organization
))
549 return new LdifGroup
.LdifOrganization(this, name
, attrs
);
551 return new LdifGroup
.LdifFunctionalGroup(this, name
, attrs
);
555 private boolean hasObjectClass(Attributes attrs
, LdapObjs objectClass
) {
557 Attribute attr
= attrs
.get(LdapAttrs
.objectClass
.name());
558 NamingEnumeration
<?
> en
= attr
.getAll();
559 while (en
.hasMore()) {
560 String v
= en
.next().toString();
561 if (v
.equalsIgnoreCase(objectClass
.name()))
566 } catch (NamingException e
) {
567 throw new IllegalStateException("Cannot search for objectClass " + objectClass
.name(), e
);
572 protected String
getMemberAttributeId() {
573 return memberAttributeId
;
576 protected List
<String
> getCredentialAttributeIds() {
577 return credentialAttributeIds
;
580 protected String
getUri() {
584 private static boolean readOnlyDefault(String uriStr
) {
587 /// TODO make it more generic
590 uri
= new URI(uriStr
.split(" ")[0]);
591 } catch (URISyntaxException e
) {
592 throw new IllegalArgumentException(e
);
594 if (uri
.getScheme() == null)
595 return false;// assume relative file to be writable
596 if (uri
.getScheme().equals(UserAdminConf
.SCHEME_FILE
)) {
597 File file
= new File(uri
);
599 return !file
.canWrite();
601 return !file
.getParentFile().canWrite();
602 } else if (uri
.getScheme().equals(UserAdminConf
.SCHEME_LDAP
)) {
603 if (uri
.getAuthority() != null)// assume writable if authenticated
605 } else if (uri
.getScheme().equals(UserAdminConf
.SCHEME_OS
)) {
608 return true;// read only by default
611 public boolean isReadOnly() {
615 public boolean isDisabled() {
619 protected UserAdmin
getExternalRoles() {
620 return externalRoles
;
623 protected int roleType(LdapName dn
) {
624 Rdn technicalRdn
= LdapNameUtils
.getParentRdn(dn
);
625 if (getGroupBaseRdn().equals(technicalRdn
) || getSystemRoleBaseRdn().equals(technicalRdn
))
627 else if (userBaseRdn
.equals(technicalRdn
))
630 throw new IllegalArgumentException(
631 "Cannot dind role type, " + technicalRdn
+ " is not a technical RDN for " + dn
);
634 /** dn can be null, in that case a default should be returned. */
635 public String
getUserObjectClass() {
636 return userObjectClass
;
639 Rdn
getUserBaseRdn() {
643 protected String
newUserObjectClass(LdapName dn
) {
644 return getUserObjectClass();
647 public String
getGroupObjectClass() {
648 return groupObjectClass
;
651 Rdn
getGroupBaseRdn() {
655 Rdn
getSystemRoleBaseRdn() {
656 return systemRoleBaseRdn
;
659 LdapName
getBaseDn() {
660 return (LdapName
) baseDn
.clone();
663 public Dictionary
<String
, Object
> getProperties() {
667 public Dictionary
<String
, Object
> cloneProperties() {
668 return new Hashtable
<>(properties
);
671 public void setExternalRoles(UserAdmin externalRoles
) {
672 this.externalRoles
= externalRoles
;
675 // public void setTransactionManager(TransactionManager transactionManager) {
676 // this.transactionManager = transactionManager;
679 public String
getForcedPassword() {
680 return forcedPassword
;
683 public void setTransactionControl(WorkControl transactionControl
) {
684 this.transactionControl
= transactionControl
;
687 public XAResource
getXaResource() {
691 public boolean isScoped() {
696 public int hashCode() {
697 return baseDn
.hashCode();
701 public String
toString() {
702 return "User Directory " + baseDn
.toString();
708 static LdapName
toLdapName(String name
) {
710 return new LdapName(name
);
711 } catch (InvalidNameException e
) {
712 throw new IllegalArgumentException(name
+ " is not an LDAP name", e
);