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
.Locale
;
14 import java
.util
.Optional
;
15 import java
.util
.StringJoiner
;
17 import javax
.naming
.InvalidNameException
;
18 import javax
.naming
.NameNotFoundException
;
19 import javax
.naming
.NamingEnumeration
;
20 import javax
.naming
.NamingException
;
21 import javax
.naming
.directory
.Attribute
;
22 import javax
.naming
.directory
.Attributes
;
23 import javax
.naming
.directory
.BasicAttributes
;
24 import javax
.naming
.ldap
.LdapName
;
25 import javax
.naming
.ldap
.Rdn
;
26 import javax
.transaction
.xa
.XAResource
;
28 import org
.argeo
.osgi
.useradmin
.OsUserDirectory
;
29 import org
.argeo
.util
.directory
.Directory
;
30 import org
.argeo
.util
.directory
.DirectoryConf
;
31 import org
.argeo
.util
.directory
.HierarchyUnit
;
32 import org
.argeo
.util
.naming
.LdapAttrs
;
33 import org
.argeo
.util
.naming
.LdapObjs
;
34 import org
.argeo
.util
.transaction
.WorkControl
;
35 import org
.argeo
.util
.transaction
.WorkingCopyXaResource
;
36 import org
.argeo
.util
.transaction
.XAResourceProvider
;
38 /** A {@link Directory} based either on LDAP or LDIF. */
39 public abstract class AbstractLdapDirectory
implements Directory
, XAResourceProvider
{
40 protected static final String SHARED_STATE_USERNAME
= "javax.security.auth.login.name";
41 protected static final String SHARED_STATE_PASSWORD
= "javax.security.auth.login.password";
43 private final LdapName baseDn
;
44 private final Hashtable
<String
, Object
> configProperties
;
45 private final Rdn userBaseRdn
, groupBaseRdn
, systemRoleBaseRdn
;
46 private final String userObjectClass
, groupObjectClass
;
47 private String memberAttributeId
= "member";
49 private final boolean readOnly
;
50 private final boolean disabled
;
51 private final String uri
;
53 private String forcedPassword
;
55 private final boolean scoped
;
57 private List
<String
> credentialAttributeIds
= Arrays
58 .asList(new String
[] { LdapAttrs
.userPassword
.name(), LdapAttrs
.authPassword
.name() });
60 private WorkControl transactionControl
;
61 private WorkingCopyXaResource
<LdapEntryWorkingCopy
> xaResource
;
63 private LdapDirectoryDao directoryDao
;
65 public AbstractLdapDirectory(URI uriArg
, Dictionary
<String
, ?
> props
, boolean scoped
) {
66 this.configProperties
= new Hashtable
<String
, Object
>();
67 for (Enumeration
<String
> keys
= props
.keys(); keys
.hasMoreElements();) {
68 String key
= keys
.nextElement();
69 configProperties
.put(key
, props
.get(key
));
72 String baseDnStr
= DirectoryConf
.baseDn
.getValue(configProperties
);
73 if (baseDnStr
== null)
74 throw new IllegalArgumentException("Base DN must be specified: " + configProperties
);
75 baseDn
= toLdapName(baseDnStr
);
79 uri
= uriArg
.toString();
80 // uri from properties is ignored
82 String uriStr
= DirectoryConf
.uri
.getValue(configProperties
);
89 forcedPassword
= DirectoryConf
.forcedPassword
.getValue(configProperties
);
91 userObjectClass
= DirectoryConf
.userObjectClass
.getValue(configProperties
);
92 groupObjectClass
= DirectoryConf
.groupObjectClass
.getValue(configProperties
);
94 String userBase
= DirectoryConf
.userBase
.getValue(configProperties
);
95 String groupBase
= DirectoryConf
.groupBase
.getValue(configProperties
);
96 String systemRoleBase
= DirectoryConf
.systemRoleBase
.getValue(configProperties
);
98 // baseDn = new LdapName(UserAdminConf.baseDn.getValue(properties));
99 userBaseRdn
= new Rdn(userBase
);
100 // userBaseDn = new LdapName(userBase + "," + baseDn);
101 groupBaseRdn
= new Rdn(groupBase
);
102 // groupBaseDn = new LdapName(groupBase + "," + baseDn);
103 systemRoleBaseRdn
= new Rdn(systemRoleBase
);
104 } catch (InvalidNameException e
) {
105 throw new IllegalArgumentException(
106 "Badly formated base DN " + DirectoryConf
.baseDn
.getValue(configProperties
), e
);
110 String readOnlyStr
= DirectoryConf
.readOnly
.getValue(configProperties
);
111 if (readOnlyStr
== null) {
112 readOnly
= readOnlyDefault(uri
);
113 configProperties
.put(DirectoryConf
.readOnly
.name(), Boolean
.toString(readOnly
));
115 readOnly
= Boolean
.parseBoolean(readOnlyStr
);
118 String disabledStr
= DirectoryConf
.disabled
.getValue(configProperties
);
119 if (disabledStr
!= null)
120 disabled
= Boolean
.parseBoolean(disabledStr
);
123 if (!getRealm().isEmpty()) {
124 // IPA multiple LDAP causes URI parsing to fail
125 // TODO manage generic redundant LDAP case
126 directoryDao
= new LdapDao(this);
129 URI u
= URI
.create(uri
);
130 if (DirectoryConf
.SCHEME_LDAP
.equals(u
.getScheme())
131 || DirectoryConf
.SCHEME_LDAPS
.equals(u
.getScheme())) {
132 directoryDao
= new LdapDao(this);
133 } else if (DirectoryConf
.SCHEME_FILE
.equals(u
.getScheme())) {
134 directoryDao
= new LdifDao(this);
135 } else if (DirectoryConf
.SCHEME_OS
.equals(u
.getScheme())) {
136 directoryDao
= new OsUserDirectory(this);
137 // singleUser = true;
139 throw new IllegalArgumentException("Unsupported scheme " + u
.getScheme());
143 directoryDao
= new LdifDao(this);
146 if (directoryDao
!= null)
147 xaResource
= new WorkingCopyXaResource
<>(directoryDao
);
155 getDirectoryDao().init();
158 public void destroy() {
159 getDirectoryDao().destroy();
165 protected abstract LdapEntry
newUser(LdapName name
);
167 protected abstract LdapEntry
newGroup(LdapName name
);
173 public boolean isEditing() {
174 return xaResource
.wc() != null;
177 public LdapEntryWorkingCopy
getWorkingCopy() {
178 LdapEntryWorkingCopy wc
= xaResource
.wc();
184 public void checkEdit() {
185 if (xaResource
.wc() == null) {
187 transactionControl
.getWorkContext().registerXAResource(xaResource
, null);
188 } catch (Exception e
) {
189 throw new IllegalStateException("Cannot enlist " + xaResource
, e
);
195 public void setTransactionControl(WorkControl transactionControl
) {
196 this.transactionControl
= transactionControl
;
199 public XAResource
getXaResource() {
203 public boolean removeEntry(LdapName dn
) {
205 LdapEntryWorkingCopy wc
= getWorkingCopy();
206 boolean actuallyDeleted
;
207 if (getDirectoryDao().entryExists(dn
) || wc
.getNewData().containsKey(dn
)) {
208 LdapEntry user
= doGetRole(dn
);
209 wc
.getDeletedData().put(dn
, user
);
210 actuallyDeleted
= true;
211 } else {// just removing from groups (e.g. system roles)
212 actuallyDeleted
= false;
214 for (LdapName groupDn
: getDirectoryDao().getDirectGroups(dn
)) {
215 LdapEntry group
= doGetRole(groupDn
);
216 group
.getAttributes().get(getMemberAttributeId()).remove(dn
.toString());
218 return actuallyDeleted
;
225 protected LdapEntry
doGetRole(LdapName dn
) {
226 LdapEntryWorkingCopy wc
= getWorkingCopy();
229 user
= getDirectoryDao().doGetEntry(dn
);
230 } catch (NameNotFoundException e
) {
234 if (user
== null && wc
.getNewData().containsKey(dn
))
235 user
= wc
.getNewData().get(dn
);
236 else if (wc
.getDeletedData().containsKey(dn
))
242 protected void collectGroups(LdapEntry user
, List
<LdapEntry
> allRoles
) {
243 Attributes attrs
= user
.getAttributes();
244 // TODO centralize attribute name
245 Attribute memberOf
= attrs
.get(LdapAttrs
.memberOf
.name());
246 // if user belongs to this directory, we only check memberOf
247 if (memberOf
!= null && user
.getDn().startsWith(getBaseDn())) {
249 NamingEnumeration
<?
> values
= memberOf
.getAll();
250 while (values
.hasMore()) {
251 Object value
= values
.next();
252 LdapName groupDn
= new LdapName(value
.toString());
253 LdapEntry group
= doGetRole(groupDn
);
257 // user doesn't have the right to retrieve role, but we know it exists
258 // otherwise memberOf would not work
259 group
= newGroup(groupDn
);
263 } catch (NamingException e
) {
264 throw new IllegalStateException("Cannot get memberOf groups for " + user
, e
);
267 directGroups
: for (LdapName groupDn
: getDirectoryDao().getDirectGroups(user
.getDn())) {
268 LdapEntry group
= doGetRole(groupDn
);
270 if (allRoles
.contains(group
)) {
271 // important in order to avoi loops
272 continue directGroups
;
275 collectGroups(group
, allRoles
);
285 public HierarchyUnit
getHierarchyUnit(String path
) {
286 LdapName dn
= pathToName(path
);
287 return directoryDao
.doGetHierarchyUnit(dn
);
291 public Iterable
<HierarchyUnit
> getDirectHierarchyUnits(boolean functionalOnly
) {
292 return directoryDao
.doGetDirectHierarchyUnits(baseDn
, functionalOnly
);
296 public String
getHierarchyUnitName() {
301 public String
getHierarchyUnitLabel(Locale locale
) {
302 String key
= LdapNameUtils
.getLastRdn(getBaseDn()).getType();
303 Object value
= LdapEntry
.getLocalized(asLdapEntry().getProperties(), key
, locale
);
305 value
= getHierarchyUnitName();
306 assert value
!= null;
307 return value
.toString();
311 public HierarchyUnit
getParent() {
316 public boolean isFunctional() {
321 public Directory
getDirectory() {
326 public HierarchyUnit
createHierarchyUnit(String path
) {
328 LdapEntryWorkingCopy wc
= getWorkingCopy();
329 LdapName dn
= pathToName(path
);
330 if ((getDirectoryDao().entryExists(dn
) && !wc
.getDeletedData().containsKey(dn
))
331 || wc
.getNewData().containsKey(dn
))
332 throw new IllegalArgumentException("Already a hierarchy unit " + path
);
333 BasicAttributes attrs
= new BasicAttributes(true);
334 attrs
.put(LdapAttrs
.objectClass
.name(), LdapObjs
.organizationalUnit
.name());
335 Rdn nameRdn
= dn
.getRdn(dn
.size() - 1);
336 // TODO deal with multiple attr RDN
337 attrs
.put(nameRdn
.getType(), nameRdn
.getValue());
338 wc
.getModifiedData().put(dn
, attrs
);
339 LdapHierarchyUnit newHierarchyUnit
= new LdapHierarchyUnit(this, dn
);
340 wc
.getNewData().put(dn
, newHierarchyUnit
);
341 return newHierarchyUnit
;
349 public String
getBase() {
350 return getBaseDn().toString();
354 public String
getName() {
355 return nameToSimple(getBaseDn(), ".");
358 protected String
nameToRelativePath(LdapName dn
) {
359 LdapName name
= LdapNameUtils
.relativeName(getBaseDn(), dn
);
360 return nameToSimple(name
, "/");
363 protected String
nameToSimple(LdapName name
, String separator
) {
364 StringJoiner path
= new StringJoiner(separator
);
365 for (int i
= 0; i
< name
.size(); i
++) {
366 path
.add(name
.getRdn(i
).getValue().toString());
368 return path
.toString();
372 protected LdapName
pathToName(String path
) {
374 LdapName name
= (LdapName
) getBaseDn().clone();
375 String
[] segments
= path
.split("/");
376 Rdn parentRdn
= null;
377 // segments[0] is the directory itself
378 for (int i
= 0; i
< segments
.length
; i
++) {
379 String segment
= segments
[i
];
380 // TODO make attr names configurable ?
381 String attr
= path
.startsWith("accounts/")/* IPA */ ? LdapAttrs
.cn
.name() : LdapAttrs
.ou
.name();
382 if (parentRdn
!= null) {
383 if (getUserBaseRdn().equals(parentRdn
))
384 attr
= LdapAttrs
.uid
.name();
385 else if (getGroupBaseRdn().equals(parentRdn
))
386 attr
= LdapAttrs
.cn
.name();
387 else if (getSystemRoleBaseRdn().equals(parentRdn
))
388 attr
= LdapAttrs
.cn
.name();
390 Rdn rdn
= new Rdn(attr
, segment
);
395 } catch (InvalidNameException e
) {
396 throw new IllegalStateException("Cannot convert " + path
+ " to LDAP name", e
);
404 protected boolean isExternal(LdapName name
) {
405 return !name
.startsWith(baseDn
);
408 protected static boolean hasObjectClass(Attributes attrs
, LdapObjs objectClass
) {
409 return hasObjectClass(attrs
, objectClass
.name());
412 protected static boolean hasObjectClass(Attributes attrs
, String objectClass
) {
414 Attribute attr
= attrs
.get(LdapAttrs
.objectClass
.name());
415 NamingEnumeration
<?
> en
= attr
.getAll();
416 while (en
.hasMore()) {
417 String v
= en
.next().toString();
418 if (v
.equalsIgnoreCase(objectClass
))
423 } catch (NamingException e
) {
424 throw new IllegalStateException("Cannot search for objectClass " + objectClass
, e
);
428 private static boolean readOnlyDefault(String uriStr
) {
431 /// TODO make it more generic
434 uri
= new URI(uriStr
.split(" ")[0]);
435 } catch (URISyntaxException e
) {
436 throw new IllegalArgumentException(e
);
438 if (uri
.getScheme() == null)
439 return false;// assume relative file to be writable
440 if (uri
.getScheme().equals(DirectoryConf
.SCHEME_FILE
)) {
441 File file
= new File(uri
);
443 return !file
.canWrite();
445 return !file
.getParentFile().canWrite();
446 } else if (uri
.getScheme().equals(DirectoryConf
.SCHEME_LDAP
)) {
447 if (uri
.getAuthority() != null)// assume writable if authenticated
449 } else if (uri
.getScheme().equals(DirectoryConf
.SCHEME_OS
)) {
452 return true;// read only by default
458 public LdapEntry
asLdapEntry() {
460 return directoryDao
.doGetEntry(baseDn
);
461 } catch (NameNotFoundException e
) {
462 throw new IllegalStateException("Cannot get " + baseDn
+ " entry", e
);
466 public Dictionary
<String
, Object
> getProperties() {
467 return asLdapEntry().getProperties();
474 public Optional
<String
> getRealm() {
475 Object realm
= configProperties
.get(DirectoryConf
.realm
.name());
477 return Optional
.empty();
478 return Optional
.of(realm
.toString());
481 public LdapName
getBaseDn() {
482 return (LdapName
) baseDn
.clone();
485 public boolean isReadOnly() {
489 public boolean isDisabled() {
493 public Rdn
getUserBaseRdn() {
497 public Rdn
getGroupBaseRdn() {
501 public Rdn
getSystemRoleBaseRdn() {
502 return systemRoleBaseRdn
;
505 // public Dictionary<String, Object> getConfigProperties() {
506 // return configProperties;
509 public Dictionary
<String
, Object
> cloneConfigProperties() {
510 return new Hashtable
<>(configProperties
);
513 public String
getForcedPassword() {
514 return forcedPassword
;
517 public boolean isScoped() {
521 public List
<String
> getCredentialAttributeIds() {
522 return credentialAttributeIds
;
525 public String
getUri() {
529 public LdapDirectoryDao
getDirectoryDao() {
533 /** dn can be null, in that case a default should be returned. */
534 public String
getUserObjectClass() {
535 return userObjectClass
;
538 public String
getGroupObjectClass() {
539 return groupObjectClass
;
542 public String
getMemberAttributeId() {
543 return memberAttributeId
;
551 public int hashCode() {
552 return baseDn
.hashCode();
556 public String
toString() {
557 return "Directory " + baseDn
.toString();