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
.LdapAttrs
;
30 import org
.argeo
.api
.acr
.ldap
.LdapObjs
;
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
[] { LdapAttrs
.userPassword
.name(), LdapAttrs
.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(LdapAttrs
.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 String
getHierarchyUnitName() {
308 public String
getHierarchyUnitLabel(Locale locale
) {
309 String key
= LdapNameUtils
.getLastRdn(getBaseDn()).getType();
310 Object value
= LdapEntry
.getLocalized(asLdapEntry().getProperties(), key
, locale
);
312 value
= getHierarchyUnitName();
313 assert value
!= null;
314 return value
.toString();
318 public HierarchyUnit
getParent() {
323 public boolean isType(Type type
) {
324 return Type
.FUNCTIONAL
.equals(type
);
328 public CmsDirectory
getDirectory() {
333 public HierarchyUnit
createHierarchyUnit(String path
) {
335 LdapEntryWorkingCopy wc
= getWorkingCopy();
336 LdapName dn
= pathToName(path
);
337 if ((getDirectoryDao().entryExists(dn
) && !wc
.getDeletedData().containsKey(dn
))
338 || wc
.getNewData().containsKey(dn
))
339 throw new IllegalArgumentException("Already a hierarchy unit " + path
);
340 BasicAttributes attrs
= new BasicAttributes(true);
341 attrs
.put(LdapAttrs
.objectClass
.name(), LdapObjs
.organizationalUnit
.name());
342 Rdn nameRdn
= dn
.getRdn(dn
.size() - 1);
343 // TODO deal with multiple attr RDN
344 attrs
.put(nameRdn
.getType(), nameRdn
.getValue());
345 wc
.getModifiedData().put(dn
, attrs
);
346 LdapHierarchyUnit newHierarchyUnit
= new LdapHierarchyUnit(this, dn
);
347 wc
.getNewData().put(dn
, newHierarchyUnit
);
348 return newHierarchyUnit
;
356 public String
getBase() {
357 return getBaseDn().toString();
361 public String
getName() {
362 return nameToSimple(getBaseDn(), ".");
365 protected String
nameToRelativePath(LdapName dn
) {
366 LdapName name
= LdapNameUtils
.relativeName(getBaseDn(), dn
);
367 return nameToSimple(name
, "/");
370 protected String
nameToSimple(LdapName name
, String separator
) {
371 StringJoiner path
= new StringJoiner(separator
);
372 for (int i
= 0; i
< name
.size(); i
++) {
373 path
.add(name
.getRdn(i
).getValue().toString());
375 return path
.toString();
379 protected LdapName
pathToName(String path
) {
381 LdapName name
= (LdapName
) getBaseDn().clone();
382 String
[] segments
= path
.split("/");
383 Rdn parentRdn
= null;
384 // segments[0] is the directory itself
385 for (int i
= 0; i
< segments
.length
; i
++) {
386 String segment
= segments
[i
];
387 // TODO make attr names configurable ?
388 String attr
= getDirectory().getRealm().isPresent()/* IPA */ ? LdapAttrs
.cn
.name()
389 : LdapAttrs
.ou
.name();
390 if (parentRdn
!= null) {
391 if (getUserBaseRdn().equals(parentRdn
))
392 attr
= LdapAttrs
.uid
.name();
393 else if (getGroupBaseRdn().equals(parentRdn
))
394 attr
= LdapAttrs
.cn
.name();
395 else if (getSystemRoleBaseRdn().equals(parentRdn
))
396 attr
= LdapAttrs
.cn
.name();
398 Rdn rdn
= new Rdn(attr
, segment
);
403 } catch (InvalidNameException e
) {
404 throw new IllegalStateException("Cannot convert " + path
+ " to LDAP name", e
);
412 protected boolean isExternal(LdapName name
) {
413 return !name
.startsWith(baseDn
);
416 protected static boolean hasObjectClass(Attributes attrs
, LdapObjs objectClass
) {
417 return hasObjectClass(attrs
, objectClass
.name());
420 protected static boolean hasObjectClass(Attributes attrs
, String objectClass
) {
422 Attribute attr
= attrs
.get(LdapAttrs
.objectClass
.name());
423 NamingEnumeration
<?
> en
= attr
.getAll();
424 while (en
.hasMore()) {
425 String v
= en
.next().toString();
426 if (v
.equalsIgnoreCase(objectClass
))
431 } catch (NamingException e
) {
432 throw new IllegalStateException("Cannot search for objectClass " + objectClass
, e
);
436 private static boolean readOnlyDefault(String uriStr
) {
439 /// TODO make it more generic
442 uri
= new URI(uriStr
.split(" ")[0]);
443 } catch (URISyntaxException e
) {
444 throw new IllegalArgumentException(e
);
446 if (uri
.getScheme() == null)
447 return false;// assume relative file to be writable
448 if (uri
.getScheme().equals(DirectoryConf
.SCHEME_FILE
)) {
449 File file
= new File(uri
);
451 return !file
.canWrite();
453 return !file
.getParentFile().canWrite();
454 } else if (uri
.getScheme().equals(DirectoryConf
.SCHEME_LDAP
)) {
455 if (uri
.getAuthority() != null)// assume writable if authenticated
457 } else if (uri
.getScheme().equals(DirectoryConf
.SCHEME_OS
)) {
460 return true;// read only by default
466 public LdapEntry
asLdapEntry() {
468 return directoryDao
.doGetEntry(baseDn
);
469 } catch (NameNotFoundException e
) {
470 throw new IllegalStateException("Cannot get " + baseDn
+ " entry", e
);
474 public Dictionary
<String
, Object
> getProperties() {
475 return asLdapEntry().getProperties();
482 public Optional
<String
> getRealm() {
483 Object realm
= configProperties
.get(DirectoryConf
.realm
.name());
485 return Optional
.empty();
486 return Optional
.of(realm
.toString());
489 public LdapName
getBaseDn() {
490 return (LdapName
) baseDn
.clone();
493 public boolean isReadOnly() {
497 public boolean isDisabled() {
501 public boolean isAuthenticated() {
502 return authenticated
;
505 public Rdn
getUserBaseRdn() {
509 public Rdn
getGroupBaseRdn() {
513 public Rdn
getSystemRoleBaseRdn() {
514 return systemRoleBaseRdn
;
517 // public Dictionary<String, Object> getConfigProperties() {
518 // return configProperties;
521 public Dictionary
<String
, Object
> cloneConfigProperties() {
522 return new Hashtable
<>(configProperties
);
525 public String
getForcedPassword() {
526 return forcedPassword
;
529 public boolean isScoped() {
533 public List
<String
> getCredentialAttributeIds() {
534 return credentialAttributeIds
;
537 public String
getUri() {
541 public LdapDirectoryDao
getDirectoryDao() {
545 /** dn can be null, in that case a default should be returned. */
546 public String
getUserObjectClass() {
547 return userObjectClass
;
550 public String
getGroupObjectClass() {
551 return groupObjectClass
;
554 public String
getMemberAttributeId() {
555 return memberAttributeId
;
563 public int hashCode() {
564 return baseDn
.hashCode();
568 public String
toString() {
569 return "Directory " + baseDn
.toString();