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
.ldap
.LdapName
;
23 import javax
.naming
.ldap
.Rdn
;
24 import javax
.transaction
.xa
.XAResource
;
26 import org
.argeo
.osgi
.useradmin
.OsUserDirectory
;
27 import org
.argeo
.util
.directory
.Directory
;
28 import org
.argeo
.util
.directory
.DirectoryConf
;
29 import org
.argeo
.util
.directory
.HierarchyUnit
;
30 import org
.argeo
.util
.naming
.LdapAttrs
;
31 import org
.argeo
.util
.naming
.LdapObjs
;
32 import org
.argeo
.util
.transaction
.WorkControl
;
33 import org
.argeo
.util
.transaction
.WorkingCopyXaResource
;
34 import org
.argeo
.util
.transaction
.XAResourceProvider
;
36 public abstract class AbstractLdapDirectory
implements Directory
, XAResourceProvider
{
37 protected static final String SHARED_STATE_USERNAME
= "javax.security.auth.login.name";
38 protected static final String SHARED_STATE_PASSWORD
= "javax.security.auth.login.password";
40 protected final LdapName baseDn
;
41 protected final Hashtable
<String
, Object
> configProperties
;
42 private final Rdn userBaseRdn
, groupBaseRdn
, systemRoleBaseRdn
;
43 private final String userObjectClass
, groupObjectClass
;
44 private String memberAttributeId
= "member";
46 private final boolean readOnly
;
47 private final boolean disabled
;
48 private final String uri
;
50 private String forcedPassword
;
52 private final boolean scoped
;
54 private List
<String
> credentialAttributeIds
= Arrays
55 .asList(new String
[] { LdapAttrs
.userPassword
.name(), LdapAttrs
.authPassword
.name() });
57 private WorkControl transactionControl
;
58 private WorkingCopyXaResource
<LdapEntryWorkingCopy
> xaResource
;
60 private LdapDirectoryDao directoryDao
;
62 public AbstractLdapDirectory(URI uriArg
, Dictionary
<String
, ?
> props
, boolean scoped
) {
63 this.configProperties
= new Hashtable
<String
, Object
>();
64 for (Enumeration
<String
> keys
= props
.keys(); keys
.hasMoreElements();) {
65 String key
= keys
.nextElement();
66 configProperties
.put(key
, props
.get(key
));
68 baseDn
= toLdapName(DirectoryConf
.baseDn
.getValue(configProperties
));
72 uri
= uriArg
.toString();
73 // uri from properties is ignored
75 String uriStr
= DirectoryConf
.uri
.getValue(configProperties
);
82 forcedPassword
= DirectoryConf
.forcedPassword
.getValue(configProperties
);
84 userObjectClass
= DirectoryConf
.userObjectClass
.getValue(configProperties
);
85 groupObjectClass
= DirectoryConf
.groupObjectClass
.getValue(configProperties
);
87 String userBase
= DirectoryConf
.userBase
.getValue(configProperties
);
88 String groupBase
= DirectoryConf
.groupBase
.getValue(configProperties
);
89 String systemRoleBase
= DirectoryConf
.systemRoleBase
.getValue(configProperties
);
91 // baseDn = new LdapName(UserAdminConf.baseDn.getValue(properties));
92 userBaseRdn
= new Rdn(userBase
);
93 // userBaseDn = new LdapName(userBase + "," + baseDn);
94 groupBaseRdn
= new Rdn(groupBase
);
95 // groupBaseDn = new LdapName(groupBase + "," + baseDn);
96 systemRoleBaseRdn
= new Rdn(systemRoleBase
);
97 } catch (InvalidNameException e
) {
98 throw new IllegalArgumentException(
99 "Badly formated base DN " + DirectoryConf
.baseDn
.getValue(configProperties
), e
);
103 String readOnlyStr
= DirectoryConf
.readOnly
.getValue(configProperties
);
104 if (readOnlyStr
== null) {
105 readOnly
= readOnlyDefault(uri
);
106 configProperties
.put(DirectoryConf
.readOnly
.name(), Boolean
.toString(readOnly
));
108 readOnly
= Boolean
.parseBoolean(readOnlyStr
);
111 String disabledStr
= DirectoryConf
.disabled
.getValue(configProperties
);
112 if (disabledStr
!= null)
113 disabled
= Boolean
.parseBoolean(disabledStr
);
116 if (!getRealm().isEmpty()) {
117 // IPA multiple LDAP causes URI parsing to fail
118 // TODO manage generic redundant LDAP case
119 directoryDao
= new LdapDao(this);
121 URI u
= URI
.create(uri
);
122 if (DirectoryConf
.SCHEME_LDAP
.equals(u
.getScheme()) || DirectoryConf
.SCHEME_LDAPS
.equals(u
.getScheme())) {
123 directoryDao
= new LdapDao(this);
124 } else if (DirectoryConf
.SCHEME_FILE
.equals(u
.getScheme())) {
125 directoryDao
= new LdifDao(this);
126 } else if (DirectoryConf
.SCHEME_OS
.equals(u
.getScheme())) {
127 directoryDao
= new OsUserDirectory(this);
128 // singleUser = true;
130 throw new IllegalArgumentException("Unsupported scheme " + u
.getScheme());
133 xaResource
= new WorkingCopyXaResource
<>(directoryDao
);
140 // public abstract HierarchyUnit doGetHierarchyUnit(LdapName dn);
142 // public abstract Iterable<HierarchyUnit> doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly);
144 // protected abstract Boolean daoHasEntry(LdapName dn);
146 // protected abstract LdapEntry daoGetEntry(LdapName key) throws NameNotFoundException;
148 // protected abstract List<LdapEntry> doGetEntries(LdapName searchBase, Filter f, boolean deep);
150 // /** Returns the groups this user is a direct member of. */
151 // protected abstract List<LdapName> getDirectGroups(LdapName dn);
157 getDirectoryDao().init();
160 public void destroy() {
161 getDirectoryDao().destroy();
167 protected abstract LdapEntry
newUser(LdapName name
, Attributes attrs
);
169 protected abstract LdapEntry
newGroup(LdapName name
, Attributes attrs
);
175 public boolean isEditing() {
176 return xaResource
.wc() != null;
179 public LdapEntryWorkingCopy
getWorkingCopy() {
180 LdapEntryWorkingCopy wc
= xaResource
.wc();
186 public void checkEdit() {
187 if (xaResource
.wc() == null) {
189 transactionControl
.getWorkContext().registerXAResource(xaResource
, null);
190 } catch (Exception e
) {
191 throw new IllegalStateException("Cannot enlist " + xaResource
, e
);
197 public void setTransactionControl(WorkControl transactionControl
) {
198 this.transactionControl
= transactionControl
;
201 public XAResource
getXaResource() {
205 public boolean removeEntry(LdapName dn
) {
207 LdapEntryWorkingCopy wc
= getWorkingCopy();
208 boolean actuallyDeleted
;
209 if (getDirectoryDao().entryExists(dn
) || wc
.getNewData().containsKey(dn
)) {
210 LdapEntry user
= doGetRole(dn
);
211 wc
.getDeletedData().put(dn
, user
);
212 actuallyDeleted
= true;
213 } else {// just removing from groups (e.g. system roles)
214 actuallyDeleted
= false;
216 for (LdapName groupDn
: getDirectoryDao().getDirectGroups(dn
)) {
217 LdapEntry group
= doGetRole(groupDn
);
218 group
.getAttributes().get(getMemberAttributeId()).remove(dn
.toString());
220 return actuallyDeleted
;
227 protected LdapEntry
doGetRole(LdapName dn
) {
228 LdapEntryWorkingCopy wc
= getWorkingCopy();
231 user
= getDirectoryDao().doGetEntry(dn
);
232 } catch (NameNotFoundException e
) {
236 if (user
== null && wc
.getNewData().containsKey(dn
))
237 user
= wc
.getNewData().get(dn
);
238 else if (wc
.getDeletedData().containsKey(dn
))
244 protected void collectGroups(LdapEntry user
, List
<LdapEntry
> allRoles
) {
245 Attributes attrs
= user
.getAttributes();
246 // TODO centralize attribute name
247 Attribute memberOf
= attrs
.get(LdapAttrs
.memberOf
.name());
248 // if user belongs to this directory, we only check memberOf
249 if (memberOf
!= null && user
.getDn().startsWith(getBaseDn())) {
251 NamingEnumeration
<?
> values
= memberOf
.getAll();
252 while (values
.hasMore()) {
253 Object value
= values
.next();
254 LdapName groupDn
= new LdapName(value
.toString());
255 LdapEntry group
= doGetRole(groupDn
);
259 } catch (NamingException e
) {
260 throw new IllegalStateException("Cannot get memberOf groups for " + user
, e
);
263 for (LdapName groupDn
: getDirectoryDao().getDirectGroups(user
.getDn())) {
264 // TODO check for loops
265 LdapEntry group
= doGetRole(groupDn
);
268 collectGroups(group
, allRoles
);
278 public HierarchyUnit
getHierarchyUnit(String path
) {
279 LdapName dn
= pathToName(path
);
280 return directoryDao
.doGetHierarchyUnit(dn
);
284 public Iterable
<HierarchyUnit
> getDirectHierarchyUnits(boolean functionalOnly
) {
285 return directoryDao
.doGetDirectHierarchyUnits(baseDn
, functionalOnly
);
289 public String
getHierarchyUnitName() {
294 public HierarchyUnit
getParent() {
299 public boolean isFunctional() {
304 public Directory
getDirectory() {
313 public String
getContext() {
314 return getBaseDn().toString();
318 public String
getName() {
319 return nameToSimple(getBaseDn(), ".");
322 protected String
nameToRelativePath(LdapName dn
) {
323 LdapName name
= LdapNameUtils
.relativeName(getBaseDn(), dn
);
324 return nameToSimple(name
, "/");
327 protected String
nameToSimple(LdapName name
, String separator
) {
328 StringJoiner path
= new StringJoiner(separator
);
329 for (int i
= 0; i
< name
.size(); i
++) {
330 path
.add(name
.getRdn(i
).getValue().toString());
332 return path
.toString();
336 protected LdapName
pathToName(String path
) {
338 LdapName name
= (LdapName
) getBaseDn().clone();
339 String
[] segments
= path
.split("/");
340 Rdn parentRdn
= null;
341 // segments[0] is the directory itself
342 for (int i
= 0; i
< segments
.length
; i
++) {
343 String segment
= segments
[i
];
344 // TODO make attr names configurable ?
345 String attr
= path
.startsWith("accounts/")/* IPA */ ? LdapAttrs
.cn
.name() : LdapAttrs
.ou
.name();
346 if (parentRdn
!= null) {
347 if (getUserBaseRdn().equals(parentRdn
))
348 attr
= LdapAttrs
.uid
.name();
349 else if (getGroupBaseRdn().equals(parentRdn
))
350 attr
= LdapAttrs
.cn
.name();
351 else if (getSystemRoleBaseRdn().equals(parentRdn
))
352 attr
= LdapAttrs
.cn
.name();
354 Rdn rdn
= new Rdn(attr
, segment
);
359 } catch (InvalidNameException e
) {
360 throw new IllegalStateException("Cannot get role " + path
, e
);
368 protected static boolean hasObjectClass(Attributes attrs
, LdapObjs objectClass
) {
369 return hasObjectClass(attrs
, objectClass
.name());
372 protected static boolean hasObjectClass(Attributes attrs
, String objectClass
) {
374 Attribute attr
= attrs
.get(LdapAttrs
.objectClass
.name());
375 NamingEnumeration
<?
> en
= attr
.getAll();
376 while (en
.hasMore()) {
377 String v
= en
.next().toString();
378 if (v
.equalsIgnoreCase(objectClass
))
383 } catch (NamingException e
) {
384 throw new IllegalStateException("Cannot search for objectClass " + objectClass
, e
);
388 private static boolean readOnlyDefault(String uriStr
) {
391 /// TODO make it more generic
394 uri
= new URI(uriStr
.split(" ")[0]);
395 } catch (URISyntaxException e
) {
396 throw new IllegalArgumentException(e
);
398 if (uri
.getScheme() == null)
399 return false;// assume relative file to be writable
400 if (uri
.getScheme().equals(DirectoryConf
.SCHEME_FILE
)) {
401 File file
= new File(uri
);
403 return !file
.canWrite();
405 return !file
.getParentFile().canWrite();
406 } else if (uri
.getScheme().equals(DirectoryConf
.SCHEME_LDAP
)) {
407 if (uri
.getAuthority() != null)// assume writable if authenticated
409 } else if (uri
.getScheme().equals(DirectoryConf
.SCHEME_OS
)) {
412 return true;// read only by default
418 public LdapEntry
asLdapEntry() {
420 return directoryDao
.doGetEntry(baseDn
);
421 } catch (NameNotFoundException e
) {
422 throw new IllegalStateException("Cannot get " + baseDn
+ " entry", e
);
426 public Dictionary
<String
, Object
> getProperties() {
427 return asLdapEntry().getProperties();
434 public Optional
<String
> getRealm() {
435 Object realm
= configProperties
.get(DirectoryConf
.realm
.name());
437 return Optional
.empty();
438 return Optional
.of(realm
.toString());
441 public LdapName
getBaseDn() {
442 return (LdapName
) baseDn
.clone();
445 public boolean isReadOnly() {
449 public boolean isDisabled() {
453 public Rdn
getUserBaseRdn() {
457 public Rdn
getGroupBaseRdn() {
461 public Rdn
getSystemRoleBaseRdn() {
462 return systemRoleBaseRdn
;
465 // public Dictionary<String, Object> getConfigProperties() {
466 // return configProperties;
469 public Dictionary
<String
, Object
> cloneConfigProperties() {
470 return new Hashtable
<>(configProperties
);
473 public String
getForcedPassword() {
474 return forcedPassword
;
477 public boolean isScoped() {
481 public List
<String
> getCredentialAttributeIds() {
482 return credentialAttributeIds
;
485 public String
getUri() {
489 public LdapDirectoryDao
getDirectoryDao() {
493 /** dn can be null, in that case a default should be returned. */
494 public String
getUserObjectClass() {
495 return userObjectClass
;
498 public String
getGroupObjectClass() {
499 return groupObjectClass
;
502 public String
getMemberAttributeId() {
503 return memberAttributeId
;
511 public int hashCode() {
512 return baseDn
.hashCode();
516 public String
toString() {
517 return "Directory " + baseDn
.toString();