1 package org
.argeo
.util
.directory
.ldap
;
3 import static org
.argeo
.util
.directory
.ldap
.LdapNameUtils
.toLdapName
;
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
;
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
.directory
.BasicAttributes
;
23 import javax
.naming
.ldap
.LdapName
;
24 import javax
.naming
.ldap
.Rdn
;
25 import javax
.transaction
.xa
.XAResource
;
27 import org
.argeo
.osgi
.useradmin
.OsUserDirectory
;
28 import org
.argeo
.util
.directory
.Directory
;
29 import org
.argeo
.util
.directory
.DirectoryConf
;
30 import org
.argeo
.util
.directory
.HierarchyUnit
;
31 import org
.argeo
.util
.naming
.LdapAttrs
;
32 import org
.argeo
.util
.naming
.LdapObjs
;
33 import org
.argeo
.util
.transaction
.WorkControl
;
34 import org
.argeo
.util
.transaction
.WorkingCopyXaResource
;
35 import org
.argeo
.util
.transaction
.XAResourceProvider
;
37 public abstract class AbstractLdapDirectory
implements Directory
, XAResourceProvider
{
38 protected static final String SHARED_STATE_USERNAME
= "javax.security.auth.login.name";
39 protected static final String SHARED_STATE_PASSWORD
= "javax.security.auth.login.password";
41 private final LdapName baseDn
;
42 private final Hashtable
<String
, Object
> configProperties
;
43 private final Rdn userBaseRdn
, groupBaseRdn
, systemRoleBaseRdn
;
44 private final String userObjectClass
, groupObjectClass
;
45 private String memberAttributeId
= "member";
47 private final boolean readOnly
;
48 private final boolean disabled
;
49 private final String uri
;
51 private String forcedPassword
;
53 private final boolean scoped
;
55 private List
<String
> credentialAttributeIds
= Arrays
56 .asList(new String
[] { LdapAttrs
.userPassword
.name(), LdapAttrs
.authPassword
.name() });
58 private WorkControl transactionControl
;
59 private WorkingCopyXaResource
<LdapEntryWorkingCopy
> xaResource
;
61 private LdapDirectoryDao directoryDao
;
63 public AbstractLdapDirectory(URI uriArg
, Dictionary
<String
, ?
> props
, boolean scoped
) {
64 this.configProperties
= new Hashtable
<String
, Object
>();
65 for (Enumeration
<String
> keys
= props
.keys(); keys
.hasMoreElements();) {
66 String key
= keys
.nextElement();
67 configProperties
.put(key
, props
.get(key
));
70 String baseDnStr
= DirectoryConf
.baseDn
.getValue(configProperties
);
71 if (baseDnStr
== null)
72 throw new IllegalArgumentException("Base DN must be specified: " + configProperties
);
73 baseDn
= toLdapName(baseDnStr
);
77 uri
= uriArg
.toString();
78 // uri from properties is ignored
80 String uriStr
= DirectoryConf
.uri
.getValue(configProperties
);
87 forcedPassword
= DirectoryConf
.forcedPassword
.getValue(configProperties
);
89 userObjectClass
= DirectoryConf
.userObjectClass
.getValue(configProperties
);
90 groupObjectClass
= DirectoryConf
.groupObjectClass
.getValue(configProperties
);
92 String userBase
= DirectoryConf
.userBase
.getValue(configProperties
);
93 String groupBase
= DirectoryConf
.groupBase
.getValue(configProperties
);
94 String systemRoleBase
= DirectoryConf
.systemRoleBase
.getValue(configProperties
);
96 // baseDn = new LdapName(UserAdminConf.baseDn.getValue(properties));
97 userBaseRdn
= new Rdn(userBase
);
98 // userBaseDn = new LdapName(userBase + "," + baseDn);
99 groupBaseRdn
= new Rdn(groupBase
);
100 // groupBaseDn = new LdapName(groupBase + "," + baseDn);
101 systemRoleBaseRdn
= new Rdn(systemRoleBase
);
102 } catch (InvalidNameException e
) {
103 throw new IllegalArgumentException(
104 "Badly formated base DN " + DirectoryConf
.baseDn
.getValue(configProperties
), e
);
108 String readOnlyStr
= DirectoryConf
.readOnly
.getValue(configProperties
);
109 if (readOnlyStr
== null) {
110 readOnly
= readOnlyDefault(uri
);
111 configProperties
.put(DirectoryConf
.readOnly
.name(), Boolean
.toString(readOnly
));
113 readOnly
= Boolean
.parseBoolean(readOnlyStr
);
116 String disabledStr
= DirectoryConf
.disabled
.getValue(configProperties
);
117 if (disabledStr
!= null)
118 disabled
= Boolean
.parseBoolean(disabledStr
);
121 if (!getRealm().isEmpty()) {
122 // IPA multiple LDAP causes URI parsing to fail
123 // TODO manage generic redundant LDAP case
124 directoryDao
= new LdapDao(this);
127 URI u
= URI
.create(uri
);
128 if (DirectoryConf
.SCHEME_LDAP
.equals(u
.getScheme())
129 || DirectoryConf
.SCHEME_LDAPS
.equals(u
.getScheme())) {
130 directoryDao
= new LdapDao(this);
131 } else if (DirectoryConf
.SCHEME_FILE
.equals(u
.getScheme())) {
132 directoryDao
= new LdifDao(this);
133 } else if (DirectoryConf
.SCHEME_OS
.equals(u
.getScheme())) {
134 directoryDao
= new OsUserDirectory(this);
135 // singleUser = true;
137 throw new IllegalArgumentException("Unsupported scheme " + u
.getScheme());
141 directoryDao
= new LdifDao(this);
144 if (directoryDao
!= null)
145 xaResource
= new WorkingCopyXaResource
<>(directoryDao
);
152 // public abstract HierarchyUnit doGetHierarchyUnit(LdapName dn);
154 // public abstract Iterable<HierarchyUnit> doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly);
156 // protected abstract Boolean daoHasEntry(LdapName dn);
158 // protected abstract LdapEntry daoGetEntry(LdapName key) throws NameNotFoundException;
160 // protected abstract List<LdapEntry> doGetEntries(LdapName searchBase, Filter f, boolean deep);
162 // /** Returns the groups this user is a direct member of. */
163 // protected abstract List<LdapName> getDirectGroups(LdapName dn);
169 getDirectoryDao().init();
172 public void destroy() {
173 getDirectoryDao().destroy();
179 protected abstract LdapEntry
newUser(LdapName name
, Attributes attrs
);
181 protected abstract LdapEntry
newGroup(LdapName name
, Attributes attrs
);
187 public boolean isEditing() {
188 return xaResource
.wc() != null;
191 public LdapEntryWorkingCopy
getWorkingCopy() {
192 LdapEntryWorkingCopy wc
= xaResource
.wc();
198 public void checkEdit() {
199 if (xaResource
.wc() == null) {
201 transactionControl
.getWorkContext().registerXAResource(xaResource
, null);
202 } catch (Exception e
) {
203 throw new IllegalStateException("Cannot enlist " + xaResource
, e
);
209 public void setTransactionControl(WorkControl transactionControl
) {
210 this.transactionControl
= transactionControl
;
213 public XAResource
getXaResource() {
217 public boolean removeEntry(LdapName dn
) {
219 LdapEntryWorkingCopy wc
= getWorkingCopy();
220 boolean actuallyDeleted
;
221 if (getDirectoryDao().entryExists(dn
) || wc
.getNewData().containsKey(dn
)) {
222 LdapEntry user
= doGetRole(dn
);
223 wc
.getDeletedData().put(dn
, user
);
224 actuallyDeleted
= true;
225 } else {// just removing from groups (e.g. system roles)
226 actuallyDeleted
= false;
228 for (LdapName groupDn
: getDirectoryDao().getDirectGroups(dn
)) {
229 LdapEntry group
= doGetRole(groupDn
);
230 group
.getAttributes().get(getMemberAttributeId()).remove(dn
.toString());
232 return actuallyDeleted
;
239 protected LdapEntry
doGetRole(LdapName dn
) {
240 LdapEntryWorkingCopy wc
= getWorkingCopy();
243 user
= getDirectoryDao().doGetEntry(dn
);
244 } catch (NameNotFoundException e
) {
248 if (user
== null && wc
.getNewData().containsKey(dn
))
249 user
= wc
.getNewData().get(dn
);
250 else if (wc
.getDeletedData().containsKey(dn
))
256 protected void collectGroups(LdapEntry user
, List
<LdapEntry
> allRoles
) {
257 Attributes attrs
= user
.getAttributes();
258 // TODO centralize attribute name
259 Attribute memberOf
= attrs
.get(LdapAttrs
.memberOf
.name());
260 // if user belongs to this directory, we only check memberOf
261 if (memberOf
!= null && user
.getDn().startsWith(getBaseDn())) {
263 NamingEnumeration
<?
> values
= memberOf
.getAll();
264 while (values
.hasMore()) {
265 Object value
= values
.next();
266 LdapName groupDn
= new LdapName(value
.toString());
267 LdapEntry group
= doGetRole(groupDn
);
271 // user doesn't have the right to retrieve role, but we know it exists
272 // otherwise memberOf would not work
273 Attributes a
= new BasicAttributes();
274 a
.put(LdapNameUtils
.getLastRdn(groupDn
).getType(),
275 LdapNameUtils
.getLastRdn(groupDn
).getValue());
276 a
.put(LdapAttrs
.objectClass
.name(), LdapObjs
.groupOfNames
.name());
277 group
= newGroup(groupDn
, a
);
281 } catch (NamingException e
) {
282 throw new IllegalStateException("Cannot get memberOf groups for " + user
, e
);
285 for (LdapName groupDn
: getDirectoryDao().getDirectGroups(user
.getDn())) {
286 // TODO check for loops
287 LdapEntry group
= doGetRole(groupDn
);
290 collectGroups(group
, allRoles
);
300 public HierarchyUnit
getHierarchyUnit(String path
) {
301 LdapName dn
= pathToName(path
);
302 return directoryDao
.doGetHierarchyUnit(dn
);
306 public Iterable
<HierarchyUnit
> getDirectHierarchyUnits(boolean functionalOnly
) {
307 return directoryDao
.doGetDirectHierarchyUnits(baseDn
, functionalOnly
);
311 public String
getHierarchyUnitName() {
316 public HierarchyUnit
getParent() {
321 public boolean isFunctional() {
326 public Directory
getDirectory() {
335 public String
getContext() {
336 return getBaseDn().toString();
340 public String
getName() {
341 return nameToSimple(getBaseDn(), ".");
344 protected String
nameToRelativePath(LdapName dn
) {
345 LdapName name
= LdapNameUtils
.relativeName(getBaseDn(), dn
);
346 return nameToSimple(name
, "/");
349 protected String
nameToSimple(LdapName name
, String separator
) {
350 StringJoiner path
= new StringJoiner(separator
);
351 for (int i
= 0; i
< name
.size(); i
++) {
352 path
.add(name
.getRdn(i
).getValue().toString());
354 return path
.toString();
358 protected LdapName
pathToName(String path
) {
360 LdapName name
= (LdapName
) getBaseDn().clone();
361 String
[] segments
= path
.split("/");
362 Rdn parentRdn
= null;
363 // segments[0] is the directory itself
364 for (int i
= 0; i
< segments
.length
; i
++) {
365 String segment
= segments
[i
];
366 // TODO make attr names configurable ?
367 String attr
= path
.startsWith("accounts/")/* IPA */ ? LdapAttrs
.cn
.name() : LdapAttrs
.ou
.name();
368 if (parentRdn
!= null) {
369 if (getUserBaseRdn().equals(parentRdn
))
370 attr
= LdapAttrs
.uid
.name();
371 else if (getGroupBaseRdn().equals(parentRdn
))
372 attr
= LdapAttrs
.cn
.name();
373 else if (getSystemRoleBaseRdn().equals(parentRdn
))
374 attr
= LdapAttrs
.cn
.name();
376 Rdn rdn
= new Rdn(attr
, segment
);
381 } catch (InvalidNameException e
) {
382 throw new IllegalStateException("Cannot get role " + path
, e
);
390 protected boolean isExternal(LdapName name
) {
391 return !name
.startsWith(baseDn
);
394 protected static boolean hasObjectClass(Attributes attrs
, LdapObjs objectClass
) {
395 return hasObjectClass(attrs
, objectClass
.name());
398 protected static boolean hasObjectClass(Attributes attrs
, String objectClass
) {
400 Attribute attr
= attrs
.get(LdapAttrs
.objectClass
.name());
401 NamingEnumeration
<?
> en
= attr
.getAll();
402 while (en
.hasMore()) {
403 String v
= en
.next().toString();
404 if (v
.equalsIgnoreCase(objectClass
))
409 } catch (NamingException e
) {
410 throw new IllegalStateException("Cannot search for objectClass " + objectClass
, e
);
414 private static boolean readOnlyDefault(String uriStr
) {
417 /// TODO make it more generic
420 uri
= new URI(uriStr
.split(" ")[0]);
421 } catch (URISyntaxException e
) {
422 throw new IllegalArgumentException(e
);
424 if (uri
.getScheme() == null)
425 return false;// assume relative file to be writable
426 if (uri
.getScheme().equals(DirectoryConf
.SCHEME_FILE
)) {
427 File file
= new File(uri
);
429 return !file
.canWrite();
431 return !file
.getParentFile().canWrite();
432 } else if (uri
.getScheme().equals(DirectoryConf
.SCHEME_LDAP
)) {
433 if (uri
.getAuthority() != null)// assume writable if authenticated
435 } else if (uri
.getScheme().equals(DirectoryConf
.SCHEME_OS
)) {
438 return true;// read only by default
444 public LdapEntry
asLdapEntry() {
446 return directoryDao
.doGetEntry(baseDn
);
447 } catch (NameNotFoundException e
) {
448 throw new IllegalStateException("Cannot get " + baseDn
+ " entry", e
);
452 public Dictionary
<String
, Object
> getProperties() {
453 return asLdapEntry().getProperties();
460 public Optional
<String
> getRealm() {
461 Object realm
= configProperties
.get(DirectoryConf
.realm
.name());
463 return Optional
.empty();
464 return Optional
.of(realm
.toString());
467 public LdapName
getBaseDn() {
468 return (LdapName
) baseDn
.clone();
471 public boolean isReadOnly() {
475 public boolean isDisabled() {
479 public Rdn
getUserBaseRdn() {
483 public Rdn
getGroupBaseRdn() {
487 public Rdn
getSystemRoleBaseRdn() {
488 return systemRoleBaseRdn
;
491 // public Dictionary<String, Object> getConfigProperties() {
492 // return configProperties;
495 public Dictionary
<String
, Object
> cloneConfigProperties() {
496 return new Hashtable
<>(configProperties
);
499 public String
getForcedPassword() {
500 return forcedPassword
;
503 public boolean isScoped() {
507 public List
<String
> getCredentialAttributeIds() {
508 return credentialAttributeIds
;
511 public String
getUri() {
515 public LdapDirectoryDao
getDirectoryDao() {
519 /** dn can be null, in that case a default should be returned. */
520 public String
getUserObjectClass() {
521 return userObjectClass
;
524 public String
getGroupObjectClass() {
525 return groupObjectClass
;
528 public String
getMemberAttributeId() {
529 return memberAttributeId
;
537 public int hashCode() {
538 return baseDn
.hashCode();
542 public String
toString() {
543 return "Directory " + baseDn
.toString();