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 /** A {@link Directory} based either on LDAP or LDIF. */
38 public abstract class AbstractLdapDirectory
implements Directory
, XAResourceProvider
{
39 protected static final String SHARED_STATE_USERNAME
= "javax.security.auth.login.name";
40 protected static final String SHARED_STATE_PASSWORD
= "javax.security.auth.login.password";
42 private final LdapName baseDn
;
43 private final Hashtable
<String
, Object
> configProperties
;
44 private final Rdn userBaseRdn
, groupBaseRdn
, systemRoleBaseRdn
;
45 private final String userObjectClass
, groupObjectClass
;
46 private String memberAttributeId
= "member";
48 private final boolean readOnly
;
49 private final boolean disabled
;
50 private final String uri
;
52 private String forcedPassword
;
54 private final boolean scoped
;
56 private List
<String
> credentialAttributeIds
= Arrays
57 .asList(new String
[] { LdapAttrs
.userPassword
.name(), LdapAttrs
.authPassword
.name() });
59 private WorkControl transactionControl
;
60 private WorkingCopyXaResource
<LdapEntryWorkingCopy
> xaResource
;
62 private LdapDirectoryDao directoryDao
;
64 public AbstractLdapDirectory(URI uriArg
, Dictionary
<String
, ?
> props
, boolean scoped
) {
65 this.configProperties
= new Hashtable
<String
, Object
>();
66 for (Enumeration
<String
> keys
= props
.keys(); keys
.hasMoreElements();) {
67 String key
= keys
.nextElement();
68 configProperties
.put(key
, props
.get(key
));
71 String baseDnStr
= DirectoryConf
.baseDn
.getValue(configProperties
);
72 if (baseDnStr
== null)
73 throw new IllegalArgumentException("Base DN must be specified: " + configProperties
);
74 baseDn
= toLdapName(baseDnStr
);
78 uri
= uriArg
.toString();
79 // uri from properties is ignored
81 String uriStr
= DirectoryConf
.uri
.getValue(configProperties
);
88 forcedPassword
= DirectoryConf
.forcedPassword
.getValue(configProperties
);
90 userObjectClass
= DirectoryConf
.userObjectClass
.getValue(configProperties
);
91 groupObjectClass
= DirectoryConf
.groupObjectClass
.getValue(configProperties
);
93 String userBase
= DirectoryConf
.userBase
.getValue(configProperties
);
94 String groupBase
= DirectoryConf
.groupBase
.getValue(configProperties
);
95 String systemRoleBase
= DirectoryConf
.systemRoleBase
.getValue(configProperties
);
97 // baseDn = new LdapName(UserAdminConf.baseDn.getValue(properties));
98 userBaseRdn
= new Rdn(userBase
);
99 // userBaseDn = new LdapName(userBase + "," + baseDn);
100 groupBaseRdn
= new Rdn(groupBase
);
101 // groupBaseDn = new LdapName(groupBase + "," + baseDn);
102 systemRoleBaseRdn
= new Rdn(systemRoleBase
);
103 } catch (InvalidNameException e
) {
104 throw new IllegalArgumentException(
105 "Badly formated base DN " + DirectoryConf
.baseDn
.getValue(configProperties
), e
);
109 String readOnlyStr
= DirectoryConf
.readOnly
.getValue(configProperties
);
110 if (readOnlyStr
== null) {
111 readOnly
= readOnlyDefault(uri
);
112 configProperties
.put(DirectoryConf
.readOnly
.name(), Boolean
.toString(readOnly
));
114 readOnly
= Boolean
.parseBoolean(readOnlyStr
);
117 String disabledStr
= DirectoryConf
.disabled
.getValue(configProperties
);
118 if (disabledStr
!= null)
119 disabled
= Boolean
.parseBoolean(disabledStr
);
122 if (!getRealm().isEmpty()) {
123 // IPA multiple LDAP causes URI parsing to fail
124 // TODO manage generic redundant LDAP case
125 directoryDao
= new LdapDao(this);
128 URI u
= URI
.create(uri
);
129 if (DirectoryConf
.SCHEME_LDAP
.equals(u
.getScheme())
130 || DirectoryConf
.SCHEME_LDAPS
.equals(u
.getScheme())) {
131 directoryDao
= new LdapDao(this);
132 } else if (DirectoryConf
.SCHEME_FILE
.equals(u
.getScheme())) {
133 directoryDao
= new LdifDao(this);
134 } else if (DirectoryConf
.SCHEME_OS
.equals(u
.getScheme())) {
135 directoryDao
= new OsUserDirectory(this);
136 // singleUser = true;
138 throw new IllegalArgumentException("Unsupported scheme " + u
.getScheme());
142 directoryDao
= new LdifDao(this);
145 if (directoryDao
!= null)
146 xaResource
= new WorkingCopyXaResource
<>(directoryDao
);
154 getDirectoryDao().init();
157 public void destroy() {
158 getDirectoryDao().destroy();
164 protected abstract LdapEntry
newUser(LdapName name
);
166 protected abstract LdapEntry
newGroup(LdapName name
);
172 public boolean isEditing() {
173 return xaResource
.wc() != null;
176 public LdapEntryWorkingCopy
getWorkingCopy() {
177 LdapEntryWorkingCopy wc
= xaResource
.wc();
183 public void checkEdit() {
184 if (xaResource
.wc() == null) {
186 transactionControl
.getWorkContext().registerXAResource(xaResource
, null);
187 } catch (Exception e
) {
188 throw new IllegalStateException("Cannot enlist " + xaResource
, e
);
194 public void setTransactionControl(WorkControl transactionControl
) {
195 this.transactionControl
= transactionControl
;
198 public XAResource
getXaResource() {
202 public boolean removeEntry(LdapName dn
) {
204 LdapEntryWorkingCopy wc
= getWorkingCopy();
205 boolean actuallyDeleted
;
206 if (getDirectoryDao().entryExists(dn
) || wc
.getNewData().containsKey(dn
)) {
207 LdapEntry user
= doGetRole(dn
);
208 wc
.getDeletedData().put(dn
, user
);
209 actuallyDeleted
= true;
210 } else {// just removing from groups (e.g. system roles)
211 actuallyDeleted
= false;
213 for (LdapName groupDn
: getDirectoryDao().getDirectGroups(dn
)) {
214 LdapEntry group
= doGetRole(groupDn
);
215 group
.getAttributes().get(getMemberAttributeId()).remove(dn
.toString());
217 return actuallyDeleted
;
224 protected LdapEntry
doGetRole(LdapName dn
) {
225 LdapEntryWorkingCopy wc
= getWorkingCopy();
228 user
= getDirectoryDao().doGetEntry(dn
);
229 } catch (NameNotFoundException e
) {
233 if (user
== null && wc
.getNewData().containsKey(dn
))
234 user
= wc
.getNewData().get(dn
);
235 else if (wc
.getDeletedData().containsKey(dn
))
241 protected void collectGroups(LdapEntry user
, List
<LdapEntry
> allRoles
) {
242 Attributes attrs
= user
.getAttributes();
243 // TODO centralize attribute name
244 Attribute memberOf
= attrs
.get(LdapAttrs
.memberOf
.name());
245 // if user belongs to this directory, we only check memberOf
246 if (memberOf
!= null && user
.getDn().startsWith(getBaseDn())) {
248 NamingEnumeration
<?
> values
= memberOf
.getAll();
249 while (values
.hasMore()) {
250 Object value
= values
.next();
251 LdapName groupDn
= new LdapName(value
.toString());
252 LdapEntry group
= doGetRole(groupDn
);
256 // user doesn't have the right to retrieve role, but we know it exists
257 // otherwise memberOf would not work
258 // Attributes a = new BasicAttributes();
259 // a.put(LdapNameUtils.getLastRdn(groupDn).getType(),
260 // LdapNameUtils.getLastRdn(groupDn).getValue());
261 // a.put(LdapAttrs.objectClass.name(), LdapObjs.groupOfNames.name());
262 group
= newGroup(groupDn
);
266 } catch (NamingException e
) {
267 throw new IllegalStateException("Cannot get memberOf groups for " + user
, e
);
270 directGroups
: for (LdapName groupDn
: getDirectoryDao().getDirectGroups(user
.getDn())) {
271 LdapEntry group
= doGetRole(groupDn
);
273 if (allRoles
.contains(group
)) {
274 // important in order to avoi loops
275 continue directGroups
;
278 collectGroups(group
, allRoles
);
288 public HierarchyUnit
getHierarchyUnit(String path
) {
289 LdapName dn
= pathToName(path
);
290 return directoryDao
.doGetHierarchyUnit(dn
);
294 public Iterable
<HierarchyUnit
> getDirectHierarchyUnits(boolean functionalOnly
) {
295 return directoryDao
.doGetDirectHierarchyUnits(baseDn
, functionalOnly
);
299 public String
getHierarchyUnitName() {
304 public HierarchyUnit
getParent() {
309 public boolean isFunctional() {
314 public Directory
getDirectory() {
319 public HierarchyUnit
createHierarchyUnit(String path
) {
321 LdapEntryWorkingCopy wc
= getWorkingCopy();
322 LdapName dn
= pathToName(path
);
323 if ((getDirectoryDao().entryExists(dn
) && !wc
.getDeletedData().containsKey(dn
))
324 || wc
.getNewData().containsKey(dn
))
325 throw new IllegalArgumentException("Already a hierarchy unit " + path
);
326 BasicAttributes attrs
= new BasicAttributes(true);
327 attrs
.put(LdapAttrs
.objectClass
.name(), LdapObjs
.organizationalUnit
.name());
328 Rdn nameRdn
= dn
.getRdn(dn
.size() - 1);
329 // TODO deal with multiple attr RDN
330 attrs
.put(nameRdn
.getType(), nameRdn
.getValue());
331 wc
.getModifiedData().put(dn
, attrs
);
332 LdapHierarchyUnit newHierarchyUnit
= new LdapHierarchyUnit(this, dn
);
333 wc
.getNewData().put(dn
, newHierarchyUnit
);
334 return newHierarchyUnit
;
342 public String
getBase() {
343 return getBaseDn().toString();
347 public String
getName() {
348 return nameToSimple(getBaseDn(), ".");
351 protected String
nameToRelativePath(LdapName dn
) {
352 LdapName name
= LdapNameUtils
.relativeName(getBaseDn(), dn
);
353 return nameToSimple(name
, "/");
356 protected String
nameToSimple(LdapName name
, String separator
) {
357 StringJoiner path
= new StringJoiner(separator
);
358 for (int i
= 0; i
< name
.size(); i
++) {
359 path
.add(name
.getRdn(i
).getValue().toString());
361 return path
.toString();
365 protected LdapName
pathToName(String path
) {
367 LdapName name
= (LdapName
) getBaseDn().clone();
368 String
[] segments
= path
.split("/");
369 Rdn parentRdn
= null;
370 // segments[0] is the directory itself
371 for (int i
= 0; i
< segments
.length
; i
++) {
372 String segment
= segments
[i
];
373 // TODO make attr names configurable ?
374 String attr
= path
.startsWith("accounts/")/* IPA */ ? LdapAttrs
.cn
.name() : LdapAttrs
.ou
.name();
375 if (parentRdn
!= null) {
376 if (getUserBaseRdn().equals(parentRdn
))
377 attr
= LdapAttrs
.uid
.name();
378 else if (getGroupBaseRdn().equals(parentRdn
))
379 attr
= LdapAttrs
.cn
.name();
380 else if (getSystemRoleBaseRdn().equals(parentRdn
))
381 attr
= LdapAttrs
.cn
.name();
383 Rdn rdn
= new Rdn(attr
, segment
);
388 } catch (InvalidNameException e
) {
389 throw new IllegalStateException("Cannot get role " + path
, e
);
397 protected boolean isExternal(LdapName name
) {
398 return !name
.startsWith(baseDn
);
401 protected static boolean hasObjectClass(Attributes attrs
, LdapObjs objectClass
) {
402 return hasObjectClass(attrs
, objectClass
.name());
405 protected static boolean hasObjectClass(Attributes attrs
, String objectClass
) {
407 Attribute attr
= attrs
.get(LdapAttrs
.objectClass
.name());
408 NamingEnumeration
<?
> en
= attr
.getAll();
409 while (en
.hasMore()) {
410 String v
= en
.next().toString();
411 if (v
.equalsIgnoreCase(objectClass
))
416 } catch (NamingException e
) {
417 throw new IllegalStateException("Cannot search for objectClass " + objectClass
, e
);
421 private static boolean readOnlyDefault(String uriStr
) {
424 /// TODO make it more generic
427 uri
= new URI(uriStr
.split(" ")[0]);
428 } catch (URISyntaxException e
) {
429 throw new IllegalArgumentException(e
);
431 if (uri
.getScheme() == null)
432 return false;// assume relative file to be writable
433 if (uri
.getScheme().equals(DirectoryConf
.SCHEME_FILE
)) {
434 File file
= new File(uri
);
436 return !file
.canWrite();
438 return !file
.getParentFile().canWrite();
439 } else if (uri
.getScheme().equals(DirectoryConf
.SCHEME_LDAP
)) {
440 if (uri
.getAuthority() != null)// assume writable if authenticated
442 } else if (uri
.getScheme().equals(DirectoryConf
.SCHEME_OS
)) {
445 return true;// read only by default
451 public LdapEntry
asLdapEntry() {
453 return directoryDao
.doGetEntry(baseDn
);
454 } catch (NameNotFoundException e
) {
455 throw new IllegalStateException("Cannot get " + baseDn
+ " entry", e
);
459 public Dictionary
<String
, Object
> getProperties() {
460 return asLdapEntry().getProperties();
467 public Optional
<String
> getRealm() {
468 Object realm
= configProperties
.get(DirectoryConf
.realm
.name());
470 return Optional
.empty();
471 return Optional
.of(realm
.toString());
474 public LdapName
getBaseDn() {
475 return (LdapName
) baseDn
.clone();
478 public boolean isReadOnly() {
482 public boolean isDisabled() {
486 public Rdn
getUserBaseRdn() {
490 public Rdn
getGroupBaseRdn() {
494 public Rdn
getSystemRoleBaseRdn() {
495 return systemRoleBaseRdn
;
498 // public Dictionary<String, Object> getConfigProperties() {
499 // return configProperties;
502 public Dictionary
<String
, Object
> cloneConfigProperties() {
503 return new Hashtable
<>(configProperties
);
506 public String
getForcedPassword() {
507 return forcedPassword
;
510 public boolean isScoped() {
514 public List
<String
> getCredentialAttributeIds() {
515 return credentialAttributeIds
;
518 public String
getUri() {
522 public LdapDirectoryDao
getDirectoryDao() {
526 /** dn can be null, in that case a default should be returned. */
527 public String
getUserObjectClass() {
528 return userObjectClass
;
531 public String
getGroupObjectClass() {
532 return groupObjectClass
;
535 public String
getMemberAttributeId() {
536 return memberAttributeId
;
544 public int hashCode() {
545 return baseDn
.hashCode();
549 public String
toString() {
550 return "Directory " + baseDn
.toString();