1 package org
.argeo
.cms
.directory
.ldap
;
3 import static org
.argeo
.cms
.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
.Locale
;
14 import java
.util
.Optional
;
15 import java
.util
.StringJoiner
;
17 import javax
.naming
.Context
;
18 import javax
.naming
.InvalidNameException
;
19 import javax
.naming
.NameNotFoundException
;
20 import javax
.naming
.NamingEnumeration
;
21 import javax
.naming
.NamingException
;
22 import javax
.naming
.directory
.Attribute
;
23 import javax
.naming
.directory
.Attributes
;
24 import javax
.naming
.directory
.BasicAttributes
;
25 import javax
.naming
.ldap
.LdapName
;
26 import javax
.naming
.ldap
.Rdn
;
27 import javax
.transaction
.xa
.XAResource
;
29 import org
.argeo
.api
.acr
.ldap
.LdapAttr
;
30 import org
.argeo
.api
.acr
.ldap
.LdapObj
;
31 import org
.argeo
.api
.cms
.directory
.CmsDirectory
;
32 import org
.argeo
.api
.cms
.directory
.HierarchyUnit
;
33 import org
.argeo
.api
.cms
.transaction
.WorkControl
;
34 import org
.argeo
.api
.cms
.transaction
.WorkingCopyXaResource
;
35 import org
.argeo
.api
.cms
.transaction
.XAResourceProvider
;
36 import org
.argeo
.cms
.osgi
.useradmin
.OsUserDirectory
;
37 import org
.argeo
.cms
.runtime
.DirectoryConf
;
39 /** A {@link CmsDirectory} based either on LDAP or LDIF. */
40 public abstract class AbstractLdapDirectory
implements CmsDirectory
, XAResourceProvider
{
41 protected static final String SHARED_STATE_USERNAME
= "javax.security.auth.login.name";
42 protected static final String SHARED_STATE_PASSWORD
= "javax.security.auth.login.password";
44 private final LdapName baseDn
;
45 private final Hashtable
<String
, Object
> configProperties
;
46 private final Rdn userBaseRdn
, groupBaseRdn
, systemRoleBaseRdn
;
47 private final String userObjectClass
, groupObjectClass
;
48 private String memberAttributeId
= "member";
50 private final boolean readOnly
;
51 private final boolean disabled
;
52 private final String uri
;
54 private String forcedPassword
;
56 private final boolean scoped
;
58 private List
<String
> credentialAttributeIds
= Arrays
59 .asList(new String
[] { LdapAttr
.userPassword
.name(), LdapAttr
.authPassword
.name() });
61 private WorkControl transactionControl
;
62 private WorkingCopyXaResource
<LdapEntryWorkingCopy
> xaResource
;
64 private LdapDirectoryDao directoryDao
;
66 /** Whether the the directory has is authenticated via a service user. */
67 private boolean authenticated
= false;
69 public AbstractLdapDirectory(URI uriArg
, Dictionary
<String
, ?
> props
, boolean scoped
) {
70 this.configProperties
= new Hashtable
<String
, Object
>();
71 for (Enumeration
<String
> keys
= props
.keys(); keys
.hasMoreElements();) {
72 String key
= keys
.nextElement();
73 configProperties
.put(key
, props
.get(key
));
76 String baseDnStr
= DirectoryConf
.baseDn
.getValue(configProperties
);
77 if (baseDnStr
== null)
78 throw new IllegalArgumentException("Base DN must be specified: " + configProperties
);
79 baseDn
= toLdapName(baseDnStr
);
83 uri
= uriArg
.toString();
84 // uri from properties is ignored
86 String uriStr
= DirectoryConf
.uri
.getValue(configProperties
);
93 forcedPassword
= DirectoryConf
.forcedPassword
.getValue(configProperties
);
95 userObjectClass
= DirectoryConf
.userObjectClass
.getValue(configProperties
);
96 groupObjectClass
= DirectoryConf
.groupObjectClass
.getValue(configProperties
);
98 String userBase
= DirectoryConf
.userBase
.getValue(configProperties
);
99 String groupBase
= DirectoryConf
.groupBase
.getValue(configProperties
);
100 String systemRoleBase
= DirectoryConf
.systemRoleBase
.getValue(configProperties
);
102 // baseDn = new LdapName(UserAdminConf.baseDn.getValue(properties));
103 userBaseRdn
= new Rdn(userBase
);
104 // userBaseDn = new LdapName(userBase + "," + baseDn);
105 groupBaseRdn
= new Rdn(groupBase
);
106 // groupBaseDn = new LdapName(groupBase + "," + baseDn);
107 systemRoleBaseRdn
= new Rdn(systemRoleBase
);
108 } catch (InvalidNameException e
) {
109 throw new IllegalArgumentException(
110 "Badly formated base DN " + DirectoryConf
.baseDn
.getValue(configProperties
), e
);
114 String readOnlyStr
= DirectoryConf
.readOnly
.getValue(configProperties
);
115 if (readOnlyStr
== null) {
116 readOnly
= readOnlyDefault(uri
);
117 configProperties
.put(DirectoryConf
.readOnly
.name(), Boolean
.toString(readOnly
));
119 readOnly
= Boolean
.parseBoolean(readOnlyStr
);
122 String disabledStr
= DirectoryConf
.disabled
.getValue(configProperties
);
123 if (disabledStr
!= null)
124 disabled
= Boolean
.parseBoolean(disabledStr
);
127 if (!getRealm().isEmpty()) {
128 // IPA multiple LDAP causes URI parsing to fail
129 // TODO manage generic redundant LDAP case
130 directoryDao
= new LdapDao(this);
133 URI u
= URI
.create(uri
);
134 if (DirectoryConf
.SCHEME_LDAP
.equals(u
.getScheme())
135 || DirectoryConf
.SCHEME_LDAPS
.equals(u
.getScheme())) {
136 directoryDao
= new LdapDao(this);
137 authenticated
= configProperties
.get(Context
.SECURITY_PRINCIPAL
) != null;
138 } else if (DirectoryConf
.SCHEME_FILE
.equals(u
.getScheme())) {
139 directoryDao
= new LdifDao(this);
140 authenticated
= true;
141 } else if (DirectoryConf
.SCHEME_OS
.equals(u
.getScheme())) {
142 directoryDao
= new OsUserDirectory(this);
143 authenticated
= true;
144 // singleUser = true;
146 throw new IllegalArgumentException("Unsupported scheme " + u
.getScheme());
150 directoryDao
= new LdifDao(this);
153 if (directoryDao
!= null)
154 xaResource
= new WorkingCopyXaResource
<>(directoryDao
);
162 getDirectoryDao().init();
165 public void destroy() {
166 getDirectoryDao().destroy();
172 protected abstract LdapEntry
newUser(LdapName name
);
174 protected abstract LdapEntry
newGroup(LdapName name
);
180 public boolean isEditing() {
181 return xaResource
.wc() != null;
184 public LdapEntryWorkingCopy
getWorkingCopy() {
185 LdapEntryWorkingCopy wc
= xaResource
.wc();
191 public void checkEdit() {
192 if (xaResource
.wc() == null) {
194 transactionControl
.getWorkContext().registerXAResource(xaResource
, null);
195 } catch (Exception e
) {
196 throw new IllegalStateException("Cannot enlist " + xaResource
, e
);
202 public void setTransactionControl(WorkControl transactionControl
) {
203 this.transactionControl
= transactionControl
;
206 public XAResource
getXaResource() {
210 public boolean removeEntry(LdapName dn
) {
212 LdapEntryWorkingCopy wc
= getWorkingCopy();
213 boolean actuallyDeleted
;
214 if (getDirectoryDao().entryExists(dn
) || wc
.getNewData().containsKey(dn
)) {
215 LdapEntry user
= doGetRole(dn
);
216 wc
.getDeletedData().put(dn
, user
);
217 actuallyDeleted
= true;
218 } else {// just removing from groups (e.g. system roles)
219 actuallyDeleted
= false;
221 for (LdapName groupDn
: getDirectoryDao().getDirectGroups(dn
)) {
222 LdapEntry group
= doGetRole(groupDn
);
223 group
.getAttributes().get(getMemberAttributeId()).remove(dn
.toString());
225 return actuallyDeleted
;
232 protected LdapEntry
doGetRole(LdapName dn
) {
233 LdapEntryWorkingCopy wc
= getWorkingCopy();
236 user
= getDirectoryDao().doGetEntry(dn
);
237 } catch (NameNotFoundException e
) {
241 if (user
== null && wc
.getNewData().containsKey(dn
))
242 user
= wc
.getNewData().get(dn
);
243 else if (wc
.getDeletedData().containsKey(dn
))
249 protected void collectGroups(LdapEntry user
, List
<LdapEntry
> allRoles
) {
250 Attributes attrs
= user
.getAttributes();
251 // TODO centralize attribute name
252 Attribute memberOf
= attrs
.get(LdapAttr
.memberOf
.name());
253 // if user belongs to this directory, we only check memberOf
254 if (memberOf
!= null && user
.getDn().startsWith(getBaseDn())) {
256 NamingEnumeration
<?
> values
= memberOf
.getAll();
257 while (values
.hasMore()) {
258 Object value
= values
.next();
259 LdapName groupDn
= new LdapName(value
.toString());
260 LdapEntry group
= doGetRole(groupDn
);
264 // user doesn't have the right to retrieve role, but we know it exists
265 // otherwise memberOf would not work
266 group
= newGroup(groupDn
);
270 } catch (NamingException e
) {
271 throw new IllegalStateException("Cannot get memberOf groups for " + user
, e
);
274 directGroups
: for (LdapName groupDn
: getDirectoryDao().getDirectGroups(user
.getDn())) {
275 LdapEntry group
= doGetRole(groupDn
);
277 if (allRoles
.contains(group
)) {
278 // important in order to avoi loops
279 continue directGroups
;
282 collectGroups(group
, allRoles
);
292 public HierarchyUnit
getHierarchyUnit(String path
) {
293 LdapName dn
= pathToName(path
);
294 return directoryDao
.doGetHierarchyUnit(dn
);
298 public Iterable
<HierarchyUnit
> getDirectHierarchyUnits(boolean functionalOnly
) {
299 return directoryDao
.doGetDirectHierarchyUnits(baseDn
, functionalOnly
);
303 public HierarchyUnit
getDirectChild(Type type
) {
304 // TODO factorise with hierarchy unit?
305 return switch (type
) {
306 case ROLES
-> getDirectoryDao().doGetHierarchyUnit((LdapName
) getBaseDn().add(getSystemRoleBaseRdn()));
307 case PEOPLE
-> getDirectoryDao().doGetHierarchyUnit((LdapName
) getBaseDn().add(getUserBaseRdn()));
308 case GROUPS
-> getDirectoryDao().doGetHierarchyUnit((LdapName
) getBaseDn().add(getGroupBaseRdn()));
309 case FUNCTIONAL
-> throw new IllegalArgumentException("Type must be a technical type");
314 public String
getHierarchyUnitName() {
319 public String
getHierarchyUnitLabel(Locale locale
) {
320 String key
= LdapNameUtils
.getLastRdn(getBaseDn()).getType();
321 Object value
= LdapEntry
.getLocalized(asLdapEntry().getProperties(), key
, locale
);
323 value
= getHierarchyUnitName();
324 assert value
!= null;
325 return value
.toString();
329 public HierarchyUnit
getParent() {
334 public boolean isType(Type type
) {
335 return Type
.FUNCTIONAL
.equals(type
);
339 public CmsDirectory
getDirectory() {
344 public HierarchyUnit
createHierarchyUnit(String path
) {
346 LdapEntryWorkingCopy wc
= getWorkingCopy();
347 LdapName dn
= pathToName(path
);
348 if ((getDirectoryDao().entryExists(dn
) && !wc
.getDeletedData().containsKey(dn
))
349 || wc
.getNewData().containsKey(dn
))
350 throw new IllegalArgumentException("Already a hierarchy unit " + path
);
351 BasicAttributes attrs
= new BasicAttributes(true);
352 attrs
.put(LdapAttr
.objectClass
.name(), LdapObj
.organizationalUnit
.name());
353 Rdn nameRdn
= dn
.getRdn(dn
.size() - 1);
354 // TODO deal with multiple attr RDN
355 attrs
.put(nameRdn
.getType(), nameRdn
.getValue());
356 wc
.getModifiedData().put(dn
, attrs
);
357 LdapHierarchyUnit newHierarchyUnit
= new LdapHierarchyUnit(this, dn
);
358 wc
.getNewData().put(dn
, newHierarchyUnit
);
359 return newHierarchyUnit
;
367 public String
getBase() {
368 return getBaseDn().toString();
372 public String
getName() {
373 return nameToSimple(getBaseDn(), ".");
376 protected String
nameToRelativePath(LdapName dn
) {
377 LdapName name
= LdapNameUtils
.relativeName(getBaseDn(), dn
);
378 return nameToSimple(name
, "/");
381 protected String
nameToSimple(LdapName name
, String separator
) {
382 StringJoiner path
= new StringJoiner(separator
);
383 for (int i
= 0; i
< name
.size(); i
++) {
384 path
.add(name
.getRdn(i
).getValue().toString());
386 return path
.toString();
390 protected LdapName
pathToName(String path
) {
392 LdapName name
= (LdapName
) getBaseDn().clone();
393 String
[] segments
= path
.split("/");
394 Rdn parentRdn
= null;
395 // segments[0] is the directory itself
396 for (int i
= 0; i
< segments
.length
; i
++) {
397 String segment
= segments
[i
];
398 // TODO make attr names configurable ?
399 String attr
= getDirectory().getRealm().isPresent()/* IPA */ ? LdapAttr
.cn
.name() : LdapAttr
.ou
.name();
400 if (parentRdn
!= null) {
401 if (getUserBaseRdn().equals(parentRdn
))
402 attr
= LdapAttr
.uid
.name();
403 else if (getGroupBaseRdn().equals(parentRdn
))
404 attr
= LdapAttr
.cn
.name();
405 else if (getSystemRoleBaseRdn().equals(parentRdn
))
406 attr
= LdapAttr
.cn
.name();
408 Rdn rdn
= new Rdn(attr
, segment
);
413 } catch (InvalidNameException e
) {
414 throw new IllegalStateException("Cannot convert " + path
+ " to LDAP name", e
);
422 protected boolean isExternal(LdapName name
) {
423 return !name
.startsWith(baseDn
);
426 protected static boolean hasObjectClass(Attributes attrs
, LdapObj objectClass
) {
427 return hasObjectClass(attrs
, objectClass
.name());
430 protected static boolean hasObjectClass(Attributes attrs
, String objectClass
) {
432 Attribute attr
= attrs
.get(LdapAttr
.objectClass
.name());
433 NamingEnumeration
<?
> en
= attr
.getAll();
434 while (en
.hasMore()) {
435 String v
= en
.next().toString();
436 if (v
.equalsIgnoreCase(objectClass
))
441 } catch (NamingException e
) {
442 throw new IllegalStateException("Cannot search for objectClass " + objectClass
, e
);
446 private static boolean readOnlyDefault(String uriStr
) {
449 /// TODO make it more generic
452 uri
= new URI(uriStr
.split(" ")[0]);
453 } catch (URISyntaxException e
) {
454 throw new IllegalArgumentException(e
);
456 if (uri
.getScheme() == null)
457 return false;// assume relative file to be writable
458 if (uri
.getScheme().equals(DirectoryConf
.SCHEME_FILE
)) {
459 File file
= new File(uri
);
461 return !file
.canWrite();
463 return !file
.getParentFile().canWrite();
464 } else if (uri
.getScheme().equals(DirectoryConf
.SCHEME_LDAP
)) {
465 if (uri
.getAuthority() != null)// assume writable if authenticated
467 } else if (uri
.getScheme().equals(DirectoryConf
.SCHEME_OS
)) {
470 return true;// read only by default
476 public LdapEntry
asLdapEntry() {
478 return directoryDao
.doGetEntry(baseDn
);
479 } catch (NameNotFoundException e
) {
480 throw new IllegalStateException("Cannot get " + baseDn
+ " entry", e
);
484 public Dictionary
<String
, Object
> getProperties() {
485 return asLdapEntry().getProperties();
492 public Optional
<String
> getRealm() {
493 Object realm
= configProperties
.get(DirectoryConf
.realm
.name());
495 return Optional
.empty();
496 return Optional
.of(realm
.toString());
499 public LdapName
getBaseDn() {
500 return (LdapName
) baseDn
.clone();
503 public boolean isReadOnly() {
507 public boolean isDisabled() {
511 public boolean isAuthenticated() {
512 return authenticated
;
515 public Rdn
getUserBaseRdn() {
519 public Rdn
getGroupBaseRdn() {
523 public Rdn
getSystemRoleBaseRdn() {
524 return systemRoleBaseRdn
;
527 // public Dictionary<String, Object> getConfigProperties() {
528 // return configProperties;
531 public Dictionary
<String
, Object
> cloneConfigProperties() {
532 return new Hashtable
<>(configProperties
);
535 public String
getForcedPassword() {
536 return forcedPassword
;
539 public boolean isScoped() {
543 public List
<String
> getCredentialAttributeIds() {
544 return credentialAttributeIds
;
547 public String
getUri() {
551 public LdapDirectoryDao
getDirectoryDao() {
555 /** dn can be null, in that case a default should be returned. */
556 public String
getUserObjectClass() {
557 return userObjectClass
;
560 public String
getGroupObjectClass() {
561 return groupObjectClass
;
564 public String
getMemberAttributeId() {
565 return memberAttributeId
;
573 public int hashCode() {
574 return baseDn
.hashCode();
578 public String
toString() {
579 return "Directory " + baseDn
.toString();