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
;
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
;
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
;
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";
48 private final Hashtable
<String
, Object
> properties
;
49 private final LdapName baseDn
, userBaseDn
, groupBaseDn
;
50 private final String userObjectClass
, userBase
, groupObjectClass
, groupBase
;
52 private final boolean readOnly
;
53 private final boolean disabled
;
54 private final String uri
;
56 private UserAdmin externalRoles
;
57 // private List<String> indexedUserProperties = Arrays
58 // .asList(new String[] { LdapAttrs.uid.name(), LdapAttrs.mail.name(),
59 // LdapAttrs.cn.name() });
61 private final boolean scoped
;
63 private String memberAttributeId
= "member";
64 private List
<String
> credentialAttributeIds
= Arrays
65 .asList(new String
[] { LdapAttrs
.userPassword
.name(), LdapAttrs
.authPassword
.name() });
68 // private TransactionManager transactionManager;
69 private WorkControl transactionControl
;
70 private WcXaResource xaResource
= new WcXaResource(this);
72 private String forcedPassword
;
74 AbstractUserDirectory(URI uriArg
, Dictionary
<String
, ?
> props
, boolean 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
));
83 uri
= uriArg
.toString();
84 // uri from properties is ignored
86 String uriStr
= UserAdminConf
.uri
.getValue(properties
);
93 forcedPassword
= UserAdminConf
.forcedPassword
.getValue(properties
);
95 userObjectClass
= UserAdminConf
.userObjectClass
.getValue(properties
);
96 userBase
= UserAdminConf
.userBase
.getValue(properties
);
97 groupObjectClass
= UserAdminConf
.groupObjectClass
.getValue(properties
);
98 groupBase
= UserAdminConf
.groupBase
.getValue(properties
);
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
),
107 String readOnlyStr
= UserAdminConf
.readOnly
.getValue(properties
);
108 if (readOnlyStr
== null) {
109 readOnly
= readOnlyDefault(uri
);
110 properties
.put(UserAdminConf
.readOnly
.name(), Boolean
.toString(readOnly
));
112 readOnly
= Boolean
.parseBoolean(readOnlyStr
);
113 String disabledStr
= UserAdminConf
.disabled
.getValue(properties
);
114 if (disabledStr
!= null)
115 disabled
= Boolean
.parseBoolean(disabledStr
);
120 /** Returns the groups this user is a direct member of. */
121 protected abstract List
<LdapName
> getDirectGroups(LdapName dn
);
123 protected abstract Boolean
daoHasRole(LdapName dn
);
125 protected abstract DirectoryUser
daoGetRole(LdapName key
) throws NameNotFoundException
;
127 protected abstract List
<DirectoryUser
> doGetRoles(LdapName searchBase
, Filter f
, boolean deep
);
129 protected abstract AbstractUserDirectory
scope(User user
);
135 public void destroy() {
140 public String
getBasePath() {
141 return getBaseDn().toString();
145 public Optional
<String
> getRealm() {
146 Object realm
= getProperties().get(UserAdminConf
.realm
.name());
148 return Optional
.empty();
149 return Optional
.of(realm
.toString());
152 protected boolean isEditing() {
153 return xaResource
.wc() != null;
156 protected UserDirectoryWorkingCopy
getWorkingCopy() {
157 UserDirectoryWorkingCopy wc
= xaResource
.wc();
163 protected void checkEdit() {
164 if (xaResource
.wc() == null) {
166 transactionControl
.getWorkContext().registerXAResource(xaResource
, null);
167 } catch (Exception e
) {
168 throw new IllegalStateException("Cannot enlist " + xaResource
, e
);
174 protected List
<Role
> getAllRoles(DirectoryUser user
) {
175 List
<Role
> allRoles
= new ArrayList
<Role
>();
177 collectRoles(user
, allRoles
);
180 collectAnonymousRoles(allRoles
);
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())) {
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
);
199 } catch (NamingException e
) {
200 throw new IllegalStateException("Cannot get memberOf groups for " + user
, e
);
203 for (LdapName groupDn
: getDirectGroups(user
.getDn())) {
204 // TODO check for loops
205 DirectoryUser group
= doGetRole(groupDn
);
208 collectRoles(group
, allRoles
);
214 private void collectAnonymousRoles(List
<Role
> allRoles
) {
215 // TODO gather anonymous roles
220 public Role
getRole(String name
) {
221 return doGetRole(toLdapName(name
));
224 protected DirectoryUser
doGetRole(LdapName dn
) {
225 UserDirectoryWorkingCopy wc
= getWorkingCopy();
228 user
= daoGetRole(dn
);
229 } catch (NameNotFoundException e
) {
233 if (user
== null && wc
.getNewUsers().containsKey(dn
))
234 user
= wc
.getNewUsers().get(dn
);
235 else if (wc
.getDeletedUsers().containsKey(dn
))
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);
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))
253 // for (DirectoryUser user : wc.getNewUsers().values()) {
254 // if (f == null || f.match(user.getProperties()))
257 // // no need to check modified users,
258 // // since doGetRoles was already based on the modified attributes
260 List
<?
extends Role
> res
= getRoles(getBaseDn(), filter
, true);
261 return res
.toArray(new Role
[res
.size()]);
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
);
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
))
275 for (DirectoryUser user
: wc
.getNewUsers().values()) {
276 if (f
== null || f
.match(user
.getProperties()))
279 // no need to check modified users,
280 // since doGetRoles was already based on the modified attributes
283 // if non deep we also search users and groups
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));
293 } catch (InvalidNameException e
) {
294 throw new IllegalStateException("Cannot search users and groups", e
);
301 public User
getUser(String key
, String value
) {
302 // TODO check value null or empty
303 List
<DirectoryUser
> collectedUsers
= new ArrayList
<DirectoryUser
>();
305 doGetUser(key
, value
, collectedUsers
);
307 throw new IllegalArgumentException("Key cannot be null");
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 + "=" :
319 protected void doGetUser(String key
, String value
, List
<DirectoryUser
> collectedUsers
) {
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
);
330 public Authorization
getAuthorization(User user
) {
331 if (user
== null || user
instanceof DirectoryUser
) {
332 return new LdifAuthorization(user
, getAllRoles((DirectoryUser
) user
));
335 AbstractUserDirectory scopedUserAdmin
= scope(user
);
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
;
344 scopedUserAdmin
.destroy();
350 public Role
createRole(String name
, int type
) {
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
);
366 wc
.getModifiedUsers().put(dn
, attrs
);
367 DirectoryUser newRole
= newRole(dn
, type
, attrs
);
368 wc
.getNewUsers().put(dn
, newRole
);
373 protected DirectoryUser
newRole(LdapName dn
, int type
, Attributes attrs
) {
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());
385 objClass
.add(top
.name());
386 objClass
.add(extensibleObject
.name());
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());
395 newRole
= new LdifGroup(this, dn
, attrs
);
397 throw new IllegalArgumentException("Unsupported type " + type
);
402 public boolean removeRole(String name
) {
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;
414 for (LdapName groupDn
: getDirectGroups(dn
)) {
415 DirectoryUser group
= doGetRole(groupDn
);
416 group
.getAttributes().get(getMemberAttributeId()).remove(dn
.toString());
418 return actuallyDeleted
;
422 protected void prepare(UserDirectoryWorkingCopy wc
) {
426 protected void commit(UserDirectoryWorkingCopy wc
) {
430 protected void rollback(UserDirectoryWorkingCopy wc
) {
438 public int getHierarchyChildCount() {
443 public HierarchyUnit
getHierarchyChild(int i
) {
444 throw new IllegalArgumentException("No child hierarchy unit available");
448 public HierarchyUnit
getParent() {
453 public int getHierarchyUnitType() {
458 public String
getHierarchyUnitName() {
459 String name
= LdapNameUtils
.getLastRdnAsString(baseDn
);
460 // TODO check ou, o, etc.
465 public HierarchyUnit
getHierarchyUnit(String path
) {
470 public HierarchyUnit
getHierarchyUnit(Role role
) {
475 public List
<?
extends Role
> getRoles(String filter
, boolean deep
) {
477 return getRoles(getBaseDn(), filter
, deep
);
478 } catch (InvalidSyntaxException e
) {
479 throw new IllegalArgumentException("Cannot filter " + filter
+ " " + getBaseDn(), e
);
484 protected String
getMemberAttributeId() {
485 return memberAttributeId
;
488 protected List
<String
> getCredentialAttributeIds() {
489 return credentialAttributeIds
;
492 protected String
getUri() {
496 private static boolean readOnlyDefault(String uriStr
) {
499 /// TODO make it more generic
502 uri
= new URI(uriStr
.split(" ")[0]);
503 } catch (URISyntaxException e
) {
504 throw new IllegalArgumentException(e
);
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
);
511 return !file
.canWrite();
513 return !file
.getParentFile().canWrite();
514 } else if (uri
.getScheme().equals(UserAdminConf
.SCHEME_LDAP
)) {
515 if (uri
.getAuthority() != null)// assume writable if authenticated
517 } else if (uri
.getScheme().equals(UserAdminConf
.SCHEME_OS
)) {
520 return true;// read only by default
523 public boolean isReadOnly() {
527 public boolean isDisabled() {
531 protected UserAdmin
getExternalRoles() {
532 return externalRoles
;
535 protected int roleType(LdapName dn
) {
536 if (dn
.startsWith(groupBaseDn
))
538 else if (dn
.startsWith(userBaseDn
))
544 /** dn can be null, in that case a default should be returned. */
545 public String
getUserObjectClass() {
546 return userObjectClass
;
549 public String
getUserBase() {
553 protected String
newUserObjectClass(LdapName dn
) {
554 return getUserObjectClass();
557 public String
getGroupObjectClass() {
558 return groupObjectClass
;
561 public String
getGroupBase() {
565 public LdapName
getBaseDn() {
566 return (LdapName
) baseDn
.clone();
569 public Dictionary
<String
, Object
> getProperties() {
573 public Dictionary
<String
, Object
> cloneProperties() {
574 return new Hashtable
<>(properties
);
577 public void setExternalRoles(UserAdmin externalRoles
) {
578 this.externalRoles
= externalRoles
;
581 // public void setTransactionManager(TransactionManager transactionManager) {
582 // this.transactionManager = transactionManager;
585 public String
getForcedPassword() {
586 return forcedPassword
;
589 public void setTransactionControl(WorkControl transactionControl
) {
590 this.transactionControl
= transactionControl
;
593 public WcXaResource
getXaResource() {
597 public boolean isScoped() {
602 public int hashCode() {
603 return baseDn
.hashCode();
607 public String
toString() {
608 return "User Directory " + baseDn
.toString();
614 static LdapName
toLdapName(String name
) {
616 return new LdapName(name
);
617 } catch (InvalidNameException e
) {
618 throw new IllegalArgumentException(name
+ " is not an LDAP name", e
);