From: Mathieu Baudier Date: Wed, 22 Jun 2022 11:13:19 +0000 (+0200) Subject: Separate LDIF and LDAP DAOs X-Git-Tag: v2.3.10~171 X-Git-Url: https://git.argeo.org/?a=commitdiff_plain;h=dc27b57704278684e72efcaf72b01c5b91df39f8;p=lgpl%2Fargeo-commons.git Separate LDIF and LDAP DAOs --- diff --git a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsUserAdmin.java b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsUserAdmin.java index 64b25c99e..64e32b16a 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsUserAdmin.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsUserAdmin.java @@ -33,10 +33,8 @@ import org.argeo.api.cms.CmsConstants; import org.argeo.api.cms.CmsLog; import org.argeo.cms.internal.http.client.HttpCredentialProvider; import org.argeo.cms.internal.http.client.SpnegoAuthScheme; +import org.argeo.osgi.useradmin.DirectoryUserAdmin; import org.argeo.osgi.useradmin.AggregatingUserAdmin; -import org.argeo.osgi.useradmin.LdapUserAdmin; -import org.argeo.osgi.useradmin.LdifUserAdmin; -import org.argeo.osgi.useradmin.OsUserDirectory; import org.argeo.osgi.useradmin.UserDirectory; import org.argeo.util.directory.DirectoryConf; import org.argeo.util.naming.dns.DnsBrowser; @@ -95,18 +93,18 @@ public class CmsUserAdmin extends AggregatingUserAdmin { } // Create - UserDirectory userDirectory; - if (realm != null || DirectoryConf.SCHEME_LDAP.equals(u.getScheme()) - || DirectoryConf.SCHEME_LDAPS.equals(u.getScheme())) { - userDirectory = new LdapUserAdmin(properties); - } else if (DirectoryConf.SCHEME_FILE.equals(u.getScheme())) { - userDirectory = new LdifUserAdmin(u, properties); - } else if (DirectoryConf.SCHEME_OS.equals(u.getScheme())) { - userDirectory = new OsUserDirectory(u, properties); - singleUser = true; - } else { - throw new IllegalArgumentException("Unsupported scheme " + u.getScheme()); - } + UserDirectory userDirectory = new DirectoryUserAdmin(u, properties); +// if (realm != null || DirectoryConf.SCHEME_LDAP.equals(u.getScheme()) +// || DirectoryConf.SCHEME_LDAPS.equals(u.getScheme())) { +// userDirectory = new LdapUserAdmin(properties); +// } else if (DirectoryConf.SCHEME_FILE.equals(u.getScheme())) { +// userDirectory = new LdifUserAdmin(u, properties); +// } else if (DirectoryConf.SCHEME_OS.equals(u.getScheme())) { +// userDirectory = new OsUserDirectory(u, properties); +// singleUser = true; +// } else { +// throw new IllegalArgumentException("Unsupported scheme " + u.getScheme()); +// } String basePath = userDirectory.getContext(); addUserDirectory(userDirectory); diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java b/org.argeo.util/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java deleted file mode 100644 index 4b13728af..000000000 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java +++ /dev/null @@ -1,405 +0,0 @@ -package org.argeo.osgi.useradmin; - -import static org.argeo.util.naming.LdapAttrs.objectClass; -import static org.argeo.util.naming.LdapObjs.extensibleObject; -import static org.argeo.util.naming.LdapObjs.inetOrgPerson; -import static org.argeo.util.naming.LdapObjs.organizationalPerson; -import static org.argeo.util.naming.LdapObjs.person; -import static org.argeo.util.naming.LdapObjs.top; - -import java.net.URI; -import java.util.ArrayList; -import java.util.Dictionary; -import java.util.Iterator; -import java.util.List; - -import javax.naming.InvalidNameException; -import javax.naming.NameNotFoundException; -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.directory.BasicAttribute; -import javax.naming.directory.BasicAttributes; -import javax.naming.ldap.LdapName; -import javax.naming.ldap.Rdn; - -import org.argeo.util.directory.HierarchyUnit; -import org.argeo.util.directory.ldap.AbstractLdapDirectory; -import org.argeo.util.directory.ldap.LdapEntry; -import org.argeo.util.directory.ldap.LdapEntryWorkingCopy; -import org.argeo.util.directory.ldap.LdapNameUtils; -import org.argeo.util.naming.LdapAttrs; -import org.argeo.util.naming.LdapObjs; -import org.osgi.framework.Filter; -import org.osgi.framework.FrameworkUtil; -import org.osgi.framework.InvalidSyntaxException; -import org.osgi.service.useradmin.Authorization; -import org.osgi.service.useradmin.Role; -import org.osgi.service.useradmin.User; -import org.osgi.service.useradmin.UserAdmin; - -/** Base class for a {@link UserDirectory}. */ -abstract class AbstractUserDirectory extends AbstractLdapDirectory implements UserAdmin, UserDirectory { - - private UserAdmin externalRoles; - // private List indexedUserProperties = Arrays - // .asList(new String[] { LdapAttrs.uid.name(), LdapAttrs.mail.name(), - // LdapAttrs.cn.name() }); - - // Transaction -// private TransactionManager transactionManager; - AbstractUserDirectory(URI uriArg, Dictionary props, boolean scoped) { - super(uriArg, props, scoped); - } - - /* - * ABSTRACT METHODS - */ - - protected abstract AbstractLdapDirectory scope(User user); - - /** Returns the groups this user is a direct member of. */ - protected abstract List getDirectGroups(LdapName dn); - - /* - * INITIALIZATION - */ - - public void init() { - - } - - public void destroy() { - - } - - @Override - public String getRolePath(Role role) { - return nameToRelativePath(((DirectoryUser) role).getDn()); - } - - @Override - public String getRoleSimpleName(Role role) { - LdapName dn = LdapNameUtils.toLdapName(role.getName()); - String name = LdapNameUtils.getLastRdnValue(dn); - return name; - } - - @Override - public Role getRoleByPath(String path) { - return doGetRole(pathToName(path)); - } - - protected List getAllRoles(DirectoryUser user) { - List allRoles = new ArrayList(); - if (user != null) { - collectRoles(user, allRoles); - allRoles.add(user); - } else - collectAnonymousRoles(allRoles); - return allRoles; - } - - private void collectRoles(DirectoryUser user, List allRoles) { - Attributes attrs = user.getAttributes(); - // TODO centralize attribute name - Attribute memberOf = attrs.get(LdapAttrs.memberOf.name()); - // if user belongs to this directory, we only check meberOf - if (memberOf != null && user.getDn().startsWith(getBaseDn())) { - try { - NamingEnumeration values = memberOf.getAll(); - while (values.hasMore()) { - Object value = values.next(); - LdapName groupDn = new LdapName(value.toString()); - DirectoryUser group = doGetRole(groupDn); - if (group != null) - allRoles.add(group); - } - } catch (NamingException e) { - throw new IllegalStateException("Cannot get memberOf groups for " + user, e); - } - } else { - for (LdapName groupDn : getDirectGroups(user.getDn())) { - // TODO check for loops - DirectoryUser group = doGetRole(groupDn); - if (group != null) { - allRoles.add(group); - collectRoles(group, allRoles); - } - } - } - } - - private void collectAnonymousRoles(List allRoles) { - // TODO gather anonymous roles - } - - // USER ADMIN - @Override - public Role getRole(String name) { - return doGetRole(toLdapName(name)); - } - - protected DirectoryUser doGetRole(LdapName dn) { - LdapEntryWorkingCopy wc = getWorkingCopy(); - DirectoryUser user; - try { - user = (DirectoryUser) daoGetEntry(dn); - } catch (NameNotFoundException e) { - user = null; - } - if (wc != null) { - if (user == null && wc.getNewData().containsKey(dn)) - user = (DirectoryUser) wc.getNewData().get(dn); - else if (wc.getDeletedData().containsKey(dn)) - user = null; - } - return user; - } - - @Override - public Role[] getRoles(String filter) throws InvalidSyntaxException { - List res = getRoles(getBaseDn(), filter, true); - return res.toArray(new Role[res.size()]); - } - - List getRoles(LdapName searchBase, String filter, boolean deep) throws InvalidSyntaxException { - LdapEntryWorkingCopy wc = getWorkingCopy(); - Filter f = filter != null ? FrameworkUtil.createFilter(filter) : null; - List searchRes = doGetEntries(searchBase, f, deep); - List res = new ArrayList<>(); - for (LdapEntry entry : searchRes) - res.add((DirectoryUser) entry); - if (wc != null) { - for (Iterator it = res.iterator(); it.hasNext();) { - DirectoryUser user = (DirectoryUser) it.next(); - LdapName dn = user.getDn(); - if (wc.getDeletedData().containsKey(dn)) - it.remove(); - } - for (LdapEntry ldapEntry : wc.getNewData().values()) { - DirectoryUser user = (DirectoryUser) ldapEntry; - if (f == null || f.match(user.getProperties())) - res.add(user); - } - // no need to check modified users, - // since doGetRoles was already based on the modified attributes - } - - // if non deep we also search users and groups -// if (!deep) { -// try { -// if (!(searchBase.endsWith(new LdapName(getUserBase())) -// || searchBase.endsWith(new LdapName(getGroupBase())))) { -// LdapName usersBase = (LdapName) ((LdapName) searchBase.clone()).add(getUserBase()); -// res.addAll(getRoles(usersBase, filter, false)); -// LdapName groupsBase = (LdapName) ((LdapName) searchBase.clone()).add(getGroupBase()); -// res.addAll(getRoles(groupsBase, filter, false)); -// } -// } catch (InvalidNameException e) { -// throw new IllegalStateException("Cannot search users and groups", e); -// } -// } - return res; - } - - @Override - public User getUser(String key, String value) { - // TODO check value null or empty - List collectedUsers = new ArrayList(); - if (key != null) { - doGetUser(key, value, collectedUsers); - } else { - throw new IllegalArgumentException("Key cannot be null"); - } - - if (collectedUsers.size() == 1) { - return collectedUsers.get(0); - } else if (collectedUsers.size() > 1) { - // log.warn(collectedUsers.size() + " users for " + (key != null ? key + "=" : - // "") + value); - } - return null; - } - - protected void doGetUser(String key, String value, List collectedUsers) { - try { - Filter f = FrameworkUtil.createFilter("(" + key + "=" + value + ")"); - List users = doGetEntries(getBaseDn(), f, true); - for (LdapEntry entry : users) - collectedUsers.add((DirectoryUser) entry); - } catch (InvalidSyntaxException e) { - throw new IllegalArgumentException("Cannot get user with " + key + "=" + value, e); - } - } - - @Override - public Authorization getAuthorization(User user) { - if (user == null || user instanceof DirectoryUser) { - return new LdifAuthorization(user, getAllRoles((DirectoryUser) user)); - } else { - // bind - AbstractUserDirectory scopedUserAdmin = (AbstractUserDirectory) scope(user); - try { - DirectoryUser directoryUser = (DirectoryUser) scopedUserAdmin.getRole(user.getName()); - if (directoryUser == null) - throw new IllegalStateException("No scoped user found for " + user); - LdifAuthorization authorization = new LdifAuthorization(directoryUser, - scopedUserAdmin.getAllRoles(directoryUser)); - return authorization; - } finally { - scopedUserAdmin.destroy(); - } - } - } - - @Override - public Role createRole(String name, int type) { - checkEdit(); - LdapEntryWorkingCopy wc = getWorkingCopy(); - LdapName dn = toLdapName(name); - if ((daoHasEntry(dn) && !wc.getDeletedData().containsKey(dn)) || wc.getNewData().containsKey(dn)) - throw new IllegalArgumentException("Already a role " + name); - BasicAttributes attrs = new BasicAttributes(true); - // attrs.put(LdifName.dn.name(), dn.toString()); - Rdn nameRdn = dn.getRdn(dn.size() - 1); - // TODO deal with multiple attr RDN - attrs.put(nameRdn.getType(), nameRdn.getValue()); - if (wc.getDeletedData().containsKey(dn)) { - wc.getDeletedData().remove(dn); - wc.getModifiedData().put(dn, attrs); - return getRole(name); - } else { - wc.getModifiedData().put(dn, attrs); - DirectoryUser newRole = newRole(dn, type, attrs); - wc.getNewData().put(dn, newRole); - return newRole; - } - } - - protected DirectoryUser newRole(LdapName dn, int type, Attributes attrs) { - DirectoryUser newRole; - BasicAttribute objClass = new BasicAttribute(objectClass.name()); - if (type == Role.USER) { - String userObjClass = newUserObjectClass(dn); - objClass.add(userObjClass); - if (inetOrgPerson.name().equals(userObjClass)) { - objClass.add(organizationalPerson.name()); - objClass.add(person.name()); - } else if (organizationalPerson.name().equals(userObjClass)) { - objClass.add(person.name()); - } - objClass.add(top.name()); - objClass.add(extensibleObject.name()); - attrs.put(objClass); - newRole = newUser(dn, attrs); - } else if (type == Role.GROUP) { - String groupObjClass = getGroupObjectClass(); - objClass.add(groupObjClass); - // objClass.add(LdifName.extensibleObject.name()); - objClass.add(top.name()); - attrs.put(objClass); - newRole = newGroup(dn, attrs); - } else - throw new IllegalArgumentException("Unsupported type " + type); - return newRole; - } - - @Override - public boolean removeRole(String name) { - checkEdit(); - LdapEntryWorkingCopy wc = getWorkingCopy(); - LdapName dn = toLdapName(name); - boolean actuallyDeleted; - if (daoHasEntry(dn) || wc.getNewData().containsKey(dn)) { - DirectoryUser user = (DirectoryUser) getRole(name); - wc.getDeletedData().put(dn, user); - actuallyDeleted = true; - } else {// just removing from groups (e.g. system roles) - actuallyDeleted = false; - } - for (LdapName groupDn : getDirectGroups(dn)) { - DirectoryUser group = doGetRole(groupDn); - group.getAttributes().get(getMemberAttributeId()).remove(dn.toString()); - } - return actuallyDeleted; - } - - /* - * HIERARCHY - */ - @Override - public HierarchyUnit getHierarchyUnit(Role role) { - LdapName dn = LdapNameUtils.toLdapName(role.getName()); - LdapName huDn = LdapNameUtils.getParent(dn); - HierarchyUnit hierarchyUnit = doGetHierarchyUnit(huDn); - if (hierarchyUnit == null) - throw new IllegalStateException("No hierarchy unit found for " + role); - return hierarchyUnit; - } - - @Override - public Iterable getHierarchyUnitRoles(HierarchyUnit hierarchyUnit, String filter, boolean deep) { - LdapName dn = LdapNameUtils.toLdapName(hierarchyUnit.getContext()); - try { - return getRoles(dn, filter, deep); - } catch (InvalidSyntaxException e) { - throw new IllegalArgumentException("Cannot filter " + filter + " " + dn, e); - } - } - - /* - * ROLES CREATION - */ - protected DirectoryUser newUser(LdapName name, Attributes attrs) { - // TODO support devices, applications, etc. - return new LdifUser.LdifPerson(this, name, attrs); - } - - protected DirectoryGroup newGroup(LdapName name, Attributes attrs) { - if (LdapNameUtils.getParentRdn(name).equals(getSystemRoleBaseRdn())) - return new LdifGroup.LdifSystemPermissions(this, name, attrs); - - if (hasObjectClass(attrs, LdapObjs.organization)) - return new LdifGroup.LdifOrganization(this, name, attrs); - else - return new LdifGroup.LdifFunctionalGroup(this, name, attrs); - - } - - // GETTERS - protected UserAdmin getExternalRoles() { - return externalRoles; - } - - protected int roleType(LdapName dn) { - Rdn technicalRdn = LdapNameUtils.getParentRdn(dn); - if (getGroupBaseRdn().equals(technicalRdn) || getSystemRoleBaseRdn().equals(technicalRdn)) - return Role.GROUP; - else if (getUserBaseRdn().equals(technicalRdn)) - return Role.USER; - else - throw new IllegalArgumentException( - "Cannot dind role type, " + technicalRdn + " is not a technical RDN for " + dn); - } - - public void setExternalRoles(UserAdmin externalRoles) { - this.externalRoles = externalRoles; - } - -// public void setTransactionManager(TransactionManager transactionManager) { -// this.transactionManager = transactionManager; -// } - - /* - * STATIC UTILITIES - */ - static LdapName toLdapName(String name) { - try { - return new LdapName(name); - } catch (InvalidNameException e) { - throw new IllegalArgumentException(name + " is not an LDAP name", e); - } - } -} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java b/org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java index 955178ce4..3857b08d0 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java @@ -1,6 +1,6 @@ package org.argeo.osgi.useradmin; -import static org.argeo.osgi.useradmin.AbstractUserDirectory.toLdapName; +import static org.argeo.osgi.useradmin.DirectoryUserAdmin.toLdapName; import java.util.ArrayList; import java.util.Arrays; @@ -30,9 +30,9 @@ public class AggregatingUserAdmin implements UserAdmin { private final LdapName tokensBaseDn; // DAOs - private AbstractUserDirectory systemRoles = null; - private AbstractUserDirectory tokens = null; - private Map businessRoles = new HashMap(); + private DirectoryUserAdmin systemRoles = null; + private DirectoryUserAdmin tokens = null; + private Map businessRoles = new HashMap(); // TODO rather use an empty constructor and an init method public AggregatingUserAdmin(String systemRolesBaseDn, String tokensBaseDn) { @@ -91,7 +91,7 @@ public class AggregatingUserAdmin implements UserAdmin { if (user == null) {// anonymous return systemRoles.getAuthorization(null); } - AbstractUserDirectory userReferentialOfThisUser = findUserAdmin(user.getName()); + DirectoryUserAdmin userReferentialOfThisUser = findUserAdmin(user.getName()); Authorization rawAuthorization = userReferentialOfThisUser.getAuthorization(user); String usernameToUse; String displayNameToUse; @@ -113,11 +113,11 @@ public class AggregatingUserAdmin implements UserAdmin { } // gather roles from other referentials - final AbstractUserDirectory userAdminToUse;// possibly scoped when authenticating + final DirectoryUserAdmin userAdminToUse;// possibly scoped when authenticating if (user instanceof DirectoryUser) { userAdminToUse = userReferentialOfThisUser; } else if (user instanceof AuthenticatingUser) { - userAdminToUse = (AbstractUserDirectory) userReferentialOfThisUser.scope(user); + userAdminToUse = (DirectoryUserAdmin) userReferentialOfThisUser.scope(user); } else { throw new IllegalArgumentException("Unsupported user type " + user.getClass()); } @@ -157,9 +157,9 @@ public class AggregatingUserAdmin implements UserAdmin { // USER ADMIN AGGREGATOR // protected void addUserDirectory(UserDirectory ud) { - if (!(ud instanceof AbstractUserDirectory)) - throw new IllegalArgumentException("Only " + AbstractUserDirectory.class.getName() + " is supported"); - AbstractUserDirectory userDirectory = (AbstractUserDirectory) ud; + if (!(ud instanceof DirectoryUserAdmin)) + throw new IllegalArgumentException("Only " + DirectoryUserAdmin.class.getName() + " is supported"); + DirectoryUserAdmin userDirectory = (DirectoryUserAdmin) ud; String basePath = userDirectory.getContext(); if (isSystemRolesBaseDn(basePath)) { this.systemRoles = userDirectory; @@ -181,7 +181,7 @@ public class AggregatingUserAdmin implements UserAdmin { protected void postAdd(UserDirectory userDirectory) { } - private AbstractUserDirectory findUserAdmin(String name) { + private DirectoryUserAdmin findUserAdmin(String name) { try { return findUserAdmin(new LdapName(name)); } catch (InvalidNameException e) { @@ -189,14 +189,14 @@ public class AggregatingUserAdmin implements UserAdmin { } } - private AbstractUserDirectory findUserAdmin(LdapName name) { + private DirectoryUserAdmin findUserAdmin(LdapName name) { if (name.startsWith(systemRolesBaseDn)) return systemRoles; if (tokensBaseDn != null && name.startsWith(tokensBaseDn)) return tokens; - List res = new ArrayList<>(1); + List res = new ArrayList<>(1); userDirectories: for (LdapName baseDn : businessRoles.keySet()) { - AbstractUserDirectory userDirectory = businessRoles.get(baseDn); + DirectoryUserAdmin userDirectory = businessRoles.get(baseDn); if (name.startsWith(baseDn)) { if (userDirectory.isDisabled()) continue userDirectories; @@ -240,7 +240,7 @@ public class AggregatingUserAdmin implements UserAdmin { public void destroy() { for (LdapName name : businessRoles.keySet()) { - AbstractUserDirectory userDirectory = businessRoles.get(name); + DirectoryUserAdmin userDirectory = businessRoles.get(name); destroy(userDirectory); } businessRoles.clear(); @@ -249,7 +249,7 @@ public class AggregatingUserAdmin implements UserAdmin { systemRoles = null; } - private void destroy(AbstractUserDirectory userDirectory) { + private void destroy(DirectoryUserAdmin userDirectory) { preDestroy(userDirectory); userDirectory.destroy(); } @@ -260,7 +260,7 @@ public class AggregatingUserAdmin implements UserAdmin { LdapName baseDn = toLdapName(basePath); if (!businessRoles.containsKey(baseDn)) throw new IllegalStateException("No user directory registered for " + baseDn); - AbstractUserDirectory userDirectory = businessRoles.remove(baseDn); + DirectoryUserAdmin userDirectory = businessRoles.remove(baseDn); destroy(userDirectory); } diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryGroup.java b/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryGroup.java index 7f8046313..1d58a2dae 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryGroup.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryGroup.java @@ -1,12 +1,8 @@ package org.argeo.osgi.useradmin; -import java.util.List; - -import javax.naming.ldap.LdapName; - import org.osgi.service.useradmin.Group; /** A group in a user directroy. */ interface DirectoryGroup extends Group, DirectoryUser { - List getMemberNames(); +// List getMemberNames(); } diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUser.java b/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUser.java index c82c5a01d..18b28a288 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUser.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUser.java @@ -1,8 +1,7 @@ package org.argeo.osgi.useradmin; -import org.argeo.util.directory.ldap.LdapEntry; import org.osgi.service.useradmin.User; /** A user in a user directory. */ -interface DirectoryUser extends User, LdapEntry { +interface DirectoryUser extends User { } diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUserAdmin.java b/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUserAdmin.java new file mode 100644 index 000000000..9f6d62d7a --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUserAdmin.java @@ -0,0 +1,420 @@ +package org.argeo.osgi.useradmin; + +import static org.argeo.util.naming.LdapAttrs.objectClass; +import static org.argeo.util.naming.LdapObjs.extensibleObject; +import static org.argeo.util.naming.LdapObjs.inetOrgPerson; +import static org.argeo.util.naming.LdapObjs.organizationalPerson; +import static org.argeo.util.naming.LdapObjs.person; +import static org.argeo.util.naming.LdapObjs.top; + +import java.net.URI; +import java.nio.channels.UnsupportedAddressTypeException; +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.Iterator; +import java.util.List; + +import javax.naming.Context; +import javax.naming.InvalidNameException; +import javax.naming.directory.Attributes; +import javax.naming.directory.BasicAttribute; +import javax.naming.directory.BasicAttributes; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; + +import org.argeo.util.directory.DirectoryConf; +import org.argeo.util.directory.DirectoryDigestUtils; +import org.argeo.util.directory.HierarchyUnit; +import org.argeo.util.directory.ldap.AbstractLdapDirectory; +import org.argeo.util.directory.ldap.LdapEntry; +import org.argeo.util.directory.ldap.LdapEntryWorkingCopy; +import org.argeo.util.directory.ldap.LdapNameUtils; +import org.argeo.util.directory.ldap.LdifDao; +import org.argeo.util.naming.LdapObjs; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.service.useradmin.Authorization; +import org.osgi.service.useradmin.Role; +import org.osgi.service.useradmin.User; +import org.osgi.service.useradmin.UserAdmin; + +/** Base class for a {@link UserDirectory}. */ +public class DirectoryUserAdmin extends AbstractLdapDirectory implements UserAdmin, UserDirectory { + + private UserAdmin externalRoles; + // private List indexedUserProperties = Arrays + // .asList(new String[] { LdapAttrs.uid.name(), LdapAttrs.mail.name(), + // LdapAttrs.cn.name() }); + + // Transaction +// private TransactionManager transactionManager; + public DirectoryUserAdmin(URI uriArg, Dictionary props) { + this(uriArg, props, false); + } + + public DirectoryUserAdmin(URI uriArg, Dictionary props, boolean scoped) { + super(uriArg, props, scoped); + } + + public DirectoryUserAdmin(Dictionary props) { + this(null, props); + } + + /* + * ABSTRACT METHODS + */ + + protected AbstractLdapDirectory scope(User user) { + throw new UnsupportedAddressTypeException(); + } + + protected DirectoryUserAdmin scopeLdap(User user) { + Dictionary credentials = user.getCredentials(); + String username = (String) credentials.get(SHARED_STATE_USERNAME); + if (username == null) + username = user.getName(); + Dictionary properties = cloneProperties(); + properties.put(Context.SECURITY_PRINCIPAL, username.toString()); + Object pwdCred = credentials.get(SHARED_STATE_PASSWORD); + byte[] pwd = (byte[]) pwdCred; + if (pwd != null) { + char[] password = DirectoryDigestUtils.bytesToChars(pwd); + properties.put(Context.SECURITY_CREDENTIALS, new String(password)); + } else { + properties.put(Context.SECURITY_AUTHENTICATION, "GSSAPI"); + } + return new DirectoryUserAdmin(null, properties, true); + } + + protected DirectoryUserAdmin scopeLdif(User user) { + Dictionary credentials = user.getCredentials(); + String username = (String) credentials.get(SHARED_STATE_USERNAME); + if (username == null) + username = user.getName(); + Object pwdCred = credentials.get(SHARED_STATE_PASSWORD); + byte[] pwd = (byte[]) pwdCred; + if (pwd != null) { + char[] password = DirectoryDigestUtils.bytesToChars(pwd); + User directoryUser = (User) getRole(username); + if (!directoryUser.hasCredential(null, password)) + throw new IllegalStateException("Invalid credentials"); + } else { + throw new IllegalStateException("Password is required"); + } + Dictionary properties = cloneProperties(); + properties.put(DirectoryConf.readOnly.name(), "true"); + DirectoryUserAdmin scopedUserAdmin = new DirectoryUserAdmin(null, properties, true); +// scopedUserAdmin.groups = Collections.unmodifiableNavigableMap(groups); +// scopedUserAdmin.users = Collections.unmodifiableNavigableMap(users); + // FIXME do it better + ((LdifDao) getDirectoryDao()).scope((LdifDao) scopedUserAdmin.getDirectoryDao()); + return scopedUserAdmin; + } + + @Override + public String getRolePath(Role role) { + return nameToRelativePath(LdapNameUtils.toLdapName(role.getName())); + } + + @Override + public String getRoleSimpleName(Role role) { + LdapName dn = LdapNameUtils.toLdapName(role.getName()); + String name = LdapNameUtils.getLastRdnValue(dn); + return name; + } + + @Override + public Role getRoleByPath(String path) { + return (Role) doGetRole(pathToName(path)); + } + + protected List getAllRoles(DirectoryUser user) { + List allRoles = new ArrayList(); + if (user != null) { + collectRoles(user, allRoles); + allRoles.add(user); + } else + collectAnonymousRoles(allRoles); + return allRoles; + } + + private void collectRoles(DirectoryUser user, List allRoles) { + List allEntries = new ArrayList<>(); + LdapEntry entry = (LdapEntry) user; + collectGroups(entry, allEntries); + for (LdapEntry e : allEntries) { + allRoles.add((Role) e); + } +// Attributes attrs = user.getAttributes(); +// // TODO centralize attribute name +// Attribute memberOf = attrs.get(LdapAttrs.memberOf.name()); +// // if user belongs to this directory, we only check memberOf +// if (memberOf != null && user.getDn().startsWith(getBaseDn())) { +// try { +// NamingEnumeration values = memberOf.getAll(); +// while (values.hasMore()) { +// Object value = values.next(); +// LdapName groupDn = new LdapName(value.toString()); +// DirectoryUser group = doGetRole(groupDn); +// if (group != null) +// allRoles.add(group); +// } +// } catch (NamingException e) { +// throw new IllegalStateException("Cannot get memberOf groups for " + user, e); +// } +// } else { +// for (LdapName groupDn : getDirectoryDao().getDirectGroups(user.getDn())) { +// // TODO check for loops +// DirectoryUser group = doGetRole(groupDn); +// if (group != null) { +// allRoles.add(group); +// collectRoles(group, allRoles); +// } +// } +// } + } + + private void collectAnonymousRoles(List allRoles) { + // TODO gather anonymous roles + } + + // USER ADMIN + @Override + public Role getRole(String name) { + return (Role) doGetRole(toLdapName(name)); + } + + @Override + public Role[] getRoles(String filter) throws InvalidSyntaxException { + List res = getRoles(getBaseDn(), filter, true); + return res.toArray(new Role[res.size()]); + } + + List getRoles(LdapName searchBase, String filter, boolean deep) throws InvalidSyntaxException { + LdapEntryWorkingCopy wc = getWorkingCopy(); +// Filter f = filter != null ? FrameworkUtil.createFilter(filter) : null; + List searchRes = getDirectoryDao().doGetEntries(searchBase, filter, deep); + List res = new ArrayList<>(); + for (LdapEntry entry : searchRes) + res.add((DirectoryUser) entry); + if (wc != null) { + for (Iterator it = res.iterator(); it.hasNext();) { + DirectoryUser user = (DirectoryUser) it.next(); + LdapName dn = LdapNameUtils.toLdapName(user.getName()); + if (wc.getDeletedData().containsKey(dn)) + it.remove(); + } + Filter f = filter != null ? FrameworkUtil.createFilter(filter) : null; + for (LdapEntry ldapEntry : wc.getNewData().values()) { + DirectoryUser user = (DirectoryUser) ldapEntry; + if (f == null || f.match(user.getProperties())) + res.add(user); + } + // no need to check modified users, + // since doGetRoles was already based on the modified attributes + } + + // if non deep we also search users and groups +// if (!deep) { +// try { +// if (!(searchBase.endsWith(new LdapName(getUserBase())) +// || searchBase.endsWith(new LdapName(getGroupBase())))) { +// LdapName usersBase = (LdapName) ((LdapName) searchBase.clone()).add(getUserBase()); +// res.addAll(getRoles(usersBase, filter, false)); +// LdapName groupsBase = (LdapName) ((LdapName) searchBase.clone()).add(getGroupBase()); +// res.addAll(getRoles(groupsBase, filter, false)); +// } +// } catch (InvalidNameException e) { +// throw new IllegalStateException("Cannot search users and groups", e); +// } +// } + return res; + } + + @Override + public User getUser(String key, String value) { + // TODO check value null or empty + List collectedUsers = new ArrayList(); + if (key != null) { + doGetUser(key, value, collectedUsers); + } else { + throw new IllegalArgumentException("Key cannot be null"); + } + + if (collectedUsers.size() == 1) { + return collectedUsers.get(0); + } else if (collectedUsers.size() > 1) { + // log.warn(collectedUsers.size() + " users for " + (key != null ? key + "=" : + // "") + value); + } + return null; + } + + protected void doGetUser(String key, String value, List collectedUsers) { + String f = "(" + key + "=" + value + ")"; + List users = getDirectoryDao().doGetEntries(getBaseDn(), f, true); + for (LdapEntry entry : users) + collectedUsers.add((DirectoryUser) entry); + } + + @Override + public Authorization getAuthorization(User user) { + if (user == null || user instanceof DirectoryUser) { + return new LdifAuthorization(user, getAllRoles((DirectoryUser) user)); + } else { + // bind + DirectoryUserAdmin scopedUserAdmin = (DirectoryUserAdmin) scope(user); + try { + DirectoryUser directoryUser = (DirectoryUser) scopedUserAdmin.getRole(user.getName()); + if (directoryUser == null) + throw new IllegalStateException("No scoped user found for " + user); + LdifAuthorization authorization = new LdifAuthorization(directoryUser, + scopedUserAdmin.getAllRoles(directoryUser)); + return authorization; + } finally { + scopedUserAdmin.destroy(); + } + } + } + + @Override + public Role createRole(String name, int type) { + checkEdit(); + LdapEntryWorkingCopy wc = getWorkingCopy(); + LdapName dn = toLdapName(name); + if ((getDirectoryDao().daoHasEntry(dn) && !wc.getDeletedData().containsKey(dn)) + || wc.getNewData().containsKey(dn)) + throw new IllegalArgumentException("Already a role " + name); + BasicAttributes attrs = new BasicAttributes(true); + // attrs.put(LdifName.dn.name(), dn.toString()); + Rdn nameRdn = dn.getRdn(dn.size() - 1); + // TODO deal with multiple attr RDN + attrs.put(nameRdn.getType(), nameRdn.getValue()); + if (wc.getDeletedData().containsKey(dn)) { + wc.getDeletedData().remove(dn); + wc.getModifiedData().put(dn, attrs); + return getRole(name); + } else { + wc.getModifiedData().put(dn, attrs); + LdapEntry newRole = newRole(dn, type, attrs); + wc.getNewData().put(dn, newRole); + return (Role) newRole; + } + } + + protected LdapEntry newRole(LdapName dn, int type, Attributes attrs) { + LdapEntry newRole; + BasicAttribute objClass = new BasicAttribute(objectClass.name()); + if (type == Role.USER) { + String userObjClass = getUserObjectClass(); + objClass.add(userObjClass); + if (inetOrgPerson.name().equals(userObjClass)) { + objClass.add(organizationalPerson.name()); + objClass.add(person.name()); + } else if (organizationalPerson.name().equals(userObjClass)) { + objClass.add(person.name()); + } + objClass.add(top.name()); + objClass.add(extensibleObject.name()); + attrs.put(objClass); + newRole = newUser(dn, attrs); + } else if (type == Role.GROUP) { + String groupObjClass = getGroupObjectClass(); + objClass.add(groupObjClass); + // objClass.add(LdifName.extensibleObject.name()); + objClass.add(top.name()); + attrs.put(objClass); + newRole = newGroup(dn, attrs); + } else + throw new IllegalArgumentException("Unsupported type " + type); + return newRole; + } + + @Override + public boolean removeRole(String name) { + return removeEntry(LdapNameUtils.toLdapName(name)); +// checkEdit(); +// LdapEntryWorkingCopy wc = getWorkingCopy(); +// LdapName dn = toLdapName(name); +// boolean actuallyDeleted; +// if (getDirectoryDao().daoHasEntry(dn) || wc.getNewData().containsKey(dn)) { +// DirectoryUser user = (DirectoryUser) getRole(name); +// wc.getDeletedData().put(dn, user); +// actuallyDeleted = true; +// } else {// just removing from groups (e.g. system roles) +// actuallyDeleted = false; +// } +// for (LdapName groupDn : getDirectoryDao().getDirectGroups(dn)) { +// LdapEntry group = doGetRole(groupDn); +// group.getAttributes().get(getMemberAttributeId()).remove(dn.toString()); +// } +// return actuallyDeleted; + } + + /* + * HIERARCHY + */ + @Override + public HierarchyUnit getHierarchyUnit(Role role) { + LdapName dn = LdapNameUtils.toLdapName(role.getName()); + LdapName huDn = LdapNameUtils.getParent(dn); + HierarchyUnit hierarchyUnit = getDirectoryDao().doGetHierarchyUnit(huDn); + if (hierarchyUnit == null) + throw new IllegalStateException("No hierarchy unit found for " + role); + return hierarchyUnit; + } + + @Override + public Iterable getHierarchyUnitRoles(HierarchyUnit hierarchyUnit, String filter, boolean deep) { + LdapName dn = LdapNameUtils.toLdapName(hierarchyUnit.getContext()); + try { + return getRoles(dn, filter, deep); + } catch (InvalidSyntaxException e) { + throw new IllegalArgumentException("Cannot filter " + filter + " " + dn, e); + } + } + + /* + * ROLES CREATION + */ + protected LdapEntry newUser(LdapName name, Attributes attrs) { + // TODO support devices, applications, etc. + return new LdifUser.LdifPerson(this, name, attrs); + } + + protected LdapEntry newGroup(LdapName name, Attributes attrs) { + if (LdapNameUtils.getParentRdn(name).equals(getSystemRoleBaseRdn())) + return new LdifGroup.LdifSystemPermissions(this, name, attrs); + + if (hasObjectClass(attrs, LdapObjs.organization)) + return new LdifGroup.LdifOrganization(this, name, attrs); + else + return new LdifGroup.LdifFunctionalGroup(this, name, attrs); + + } + + // GETTERS + protected UserAdmin getExternalRoles() { + return externalRoles; + } + + public void setExternalRoles(UserAdmin externalRoles) { + this.externalRoles = externalRoles; + } + +// public void setTransactionManager(TransactionManager transactionManager) { +// this.transactionManager = transactionManager; +// } + + /* + * STATIC UTILITIES + */ + static LdapName toLdapName(String name) { + try { + return new LdapName(name); + } catch (InvalidNameException e) { + throw new IllegalArgumentException(name + " is not an LDAP name", e); + } + } +} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/LdapUserAdmin.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdapUserAdmin.java deleted file mode 100644 index 36419d960..000000000 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/LdapUserAdmin.java +++ /dev/null @@ -1,240 +0,0 @@ -package org.argeo.osgi.useradmin; - -import static org.argeo.util.naming.LdapAttrs.objectClass; - -import java.util.ArrayList; -import java.util.Dictionary; -import java.util.List; - -import javax.naming.AuthenticationNotSupportedException; -import javax.naming.Binding; -import javax.naming.Context; -import javax.naming.InvalidNameException; -import javax.naming.NameNotFoundException; -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.directory.SearchControls; -import javax.naming.directory.SearchResult; -import javax.naming.ldap.LdapName; - -import org.argeo.util.directory.DirectoryDigestUtils; -import org.argeo.util.directory.HierarchyUnit; -import org.argeo.util.directory.ldap.LdapConnection; -import org.argeo.util.directory.ldap.LdapEntry; -import org.argeo.util.directory.ldap.LdapEntryWorkingCopy; -import org.argeo.util.directory.ldap.LdapHierarchyUnit; -import org.argeo.util.naming.LdapObjs; -import org.osgi.framework.Filter; -import org.osgi.service.useradmin.Role; -import org.osgi.service.useradmin.User; - -/** A user admin based on a LDAP server. */ -public class LdapUserAdmin extends AbstractUserDirectory { - private LdapConnection ldapConnection; - - public LdapUserAdmin(Dictionary properties) { - this(properties, false); - } - - public LdapUserAdmin(Dictionary properties, boolean scoped) { - super(null, properties, scoped); - ldapConnection = new LdapConnection(getUri().toString(), properties); - } - - public void destroy() { - ldapConnection.destroy(); - } - - @Override - protected AbstractUserDirectory scope(User user) { - Dictionary credentials = user.getCredentials(); - String username = (String) credentials.get(SHARED_STATE_USERNAME); - if (username == null) - username = user.getName(); - Dictionary properties = cloneProperties(); - properties.put(Context.SECURITY_PRINCIPAL, username.toString()); - Object pwdCred = credentials.get(SHARED_STATE_PASSWORD); - byte[] pwd = (byte[]) pwdCred; - if (pwd != null) { - char[] password = DirectoryDigestUtils.bytesToChars(pwd); - properties.put(Context.SECURITY_CREDENTIALS, new String(password)); - } else { - properties.put(Context.SECURITY_AUTHENTICATION, "GSSAPI"); - } - return new LdapUserAdmin(properties, true); - } - -// protected InitialLdapContext getLdapContext() { -// return initialLdapContext; -// } - - @Override - protected Boolean daoHasEntry(LdapName dn) { - try { - return daoGetEntry(dn) != null; - } catch (NameNotFoundException e) { - return false; - } - } - - @Override - protected DirectoryUser daoGetEntry(LdapName name) throws NameNotFoundException { - try { - Attributes attrs = ldapConnection.getAttributes(name); - if (attrs.size() == 0) - return null; - int roleType = roleType(name); - DirectoryUser res; - if (roleType == Role.GROUP) - res = newGroup(name, attrs); - else if (roleType == Role.USER) - res = newUser(name, attrs); - else - throw new IllegalArgumentException("Unsupported LDAP type for " + name); - return res; - } catch (NameNotFoundException e) { - throw e; - } catch (NamingException e) { - return null; - } - } - - @Override - protected List doGetEntries(LdapName searchBase, Filter f, boolean deep) { - ArrayList res = new ArrayList<>(); - try { - String searchFilter = f != null ? f.toString() - : "(|(" + objectClass + "=" + getUserObjectClass() + ")(" + objectClass + "=" - + getGroupObjectClass() + "))"; - SearchControls searchControls = new SearchControls(); - // FIXME make one level consistent with deep - searchControls.setSearchScope(deep ? SearchControls.SUBTREE_SCOPE : SearchControls.ONELEVEL_SCOPE); - - // LdapName searchBase = getBaseDn(); - NamingEnumeration results = ldapConnection.search(searchBase, searchFilter, searchControls); - - results: while (results.hasMoreElements()) { - SearchResult searchResult = results.next(); - Attributes attrs = searchResult.getAttributes(); - Attribute objectClassAttr = attrs.get(objectClass.name()); - LdapName dn = toDn(searchBase, searchResult); - DirectoryUser role; - if (objectClassAttr.contains(getGroupObjectClass()) - || objectClassAttr.contains(getGroupObjectClass().toLowerCase())) - role = newGroup(dn, attrs); - else if (objectClassAttr.contains(getUserObjectClass()) - || objectClassAttr.contains(getUserObjectClass().toLowerCase())) - role = newUser(dn, attrs); - else { -// log.warn("Unsupported LDAP type for " + searchResult.getName()); - continue results; - } - res.add(role); - } - return res; - } catch (AuthenticationNotSupportedException e) { - // ignore (typically an unsupported anonymous bind) - // TODO better logging - return res; - } catch (NamingException e) { - throw new IllegalStateException("Cannot get roles for filter " + f, e); - } - } - - private LdapName toDn(LdapName baseDn, Binding binding) throws InvalidNameException { - return new LdapName(binding.isRelative() ? binding.getName() + "," + baseDn : binding.getName()); - } - - @Override - protected List getDirectGroups(LdapName dn) { - List directGroups = new ArrayList(); - try { - String searchFilter = "(&(" + objectClass + "=" + getGroupObjectClass() + ")(" + getMemberAttributeId() - + "=" + dn + "))"; - - SearchControls searchControls = new SearchControls(); - searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); - - LdapName searchBase = getBaseDn(); - NamingEnumeration results = ldapConnection.search(searchBase, searchFilter, searchControls); - - while (results.hasMoreElements()) { - SearchResult searchResult = (SearchResult) results.nextElement(); - directGroups.add(toDn(searchBase, searchResult)); - } - return directGroups; - } catch (NamingException e) { - throw new IllegalStateException("Cannot populate direct members of " + dn, e); - } - } - - @Override - public void prepare(LdapEntryWorkingCopy wc) { - try { - ldapConnection.prepareChanges(wc); - } catch (NamingException e) { - throw new IllegalStateException("Cannot prepare LDAP", e); - } - } - - @Override - public void commit(LdapEntryWorkingCopy wc) { - try { - ldapConnection.commitChanges(wc); - } catch (NamingException e) { - throw new IllegalStateException("Cannot commit LDAP", e); - } - } - - @Override - public void rollback(LdapEntryWorkingCopy wc) { - // prepare not impacting - } - - /* - * HIERARCHY - */ - - @Override - public Iterable doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) { - List res = new ArrayList<>(); - try { - String searchFilter = "(|(" + objectClass + "=" + LdapObjs.organizationalUnit.name() + ")(" + objectClass - + "=" + LdapObjs.organization.name() + "))"; - - SearchControls searchControls = new SearchControls(); - searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE); - - NamingEnumeration results = ldapConnection.search(searchBase, searchFilter, searchControls); - - while (results.hasMoreElements()) { - SearchResult searchResult = (SearchResult) results.nextElement(); - LdapName dn = toDn(searchBase, searchResult); - Attributes attrs = searchResult.getAttributes(); - LdapHierarchyUnit hierarchyUnit = new LdapHierarchyUnit(this, dn, attrs); - if (functionalOnly) { - if (hierarchyUnit.isFunctional()) - res.add(hierarchyUnit); - } else { - res.add(hierarchyUnit); - } - } - return res; - } catch (NamingException e) { - throw new IllegalStateException("Cannot get direct hierarchy units ", e); - } - } - - @Override - public HierarchyUnit doGetHierarchyUnit(LdapName dn) { - try { - Attributes attrs = ldapConnection.getAttributes(dn); - return new LdapHierarchyUnit(this, dn, attrs); - } catch (NamingException e) { - throw new IllegalStateException("Cannot get hierarchy unit " + dn, e); - } - } - -} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifGroup.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifGroup.java index 72b08a8c3..7aad15a8c 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifGroup.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifGroup.java @@ -4,8 +4,6 @@ import java.util.ArrayList; import java.util.List; import javax.naming.InvalidNameException; -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; import javax.naming.directory.Attribute; import javax.naming.directory.Attributes; import javax.naming.ldap.LdapName; @@ -13,13 +11,14 @@ import javax.naming.ldap.LdapName; import org.argeo.util.directory.FunctionalGroup; import org.argeo.util.directory.Organization; import org.argeo.util.directory.SystemPermissions; +import org.argeo.util.directory.ldap.AbstractLdapDirectory; import org.osgi.service.useradmin.Role; /** Directory group implementation */ abstract class LdifGroup extends LdifUser implements DirectoryGroup { private final String memberAttributeId; - LdifGroup(AbstractUserDirectory userAdmin, LdapName dn, Attributes attributes) { + LdifGroup(AbstractLdapDirectory userAdmin, LdapName dn, Attributes attributes) { super(userAdmin, dn, attributes); memberAttributeId = userAdmin.getMemberAttributeId(); } @@ -74,7 +73,7 @@ abstract class LdifGroup extends LdifUser implements DirectoryGroup { @Override public Role[] getMembers() { List directMembers = new ArrayList(); - for (LdapName ldapName : getMemberNames()) { + for (LdapName ldapName : getReferences(memberAttributeId)) { Role role = findRole(ldapName); if (role == null) { throw new IllegalStateException("Role " + ldapName + " not found."); @@ -98,23 +97,23 @@ abstract class LdifGroup extends LdifUser implements DirectoryGroup { return role; } - @Override - public List getMemberNames() { - Attribute memberAttribute = getAttributes().get(memberAttributeId); - if (memberAttribute == null) - return new ArrayList(); - try { - List roles = new ArrayList(); - NamingEnumeration values = memberAttribute.getAll(); - while (values.hasMore()) { - LdapName dn = new LdapName(values.next().toString()); - roles.add(dn); - } - return roles; - } catch (NamingException e) { - throw new IllegalStateException("Cannot get members", e); - } - } +// @Override +// public List getMemberNames() { +// Attribute memberAttribute = getAttributes().get(memberAttributeId); +// if (memberAttribute == null) +// return new ArrayList(); +// try { +// List roles = new ArrayList(); +// NamingEnumeration values = memberAttribute.getAll(); +// while (values.hasMore()) { +// LdapName dn = new LdapName(values.next().toString()); +// roles.add(dn); +// } +// return roles; +// } catch (NamingException e) { +// throw new IllegalStateException("Cannot get members", e); +// } +// } @Override public Role[] getRequiredMembers() { @@ -131,7 +130,7 @@ abstract class LdifGroup extends LdifUser implements DirectoryGroup { */ static class LdifFunctionalGroup extends LdifGroup implements FunctionalGroup { - public LdifFunctionalGroup(AbstractUserDirectory userAdmin, LdapName dn, Attributes attributes) { + public LdifFunctionalGroup(DirectoryUserAdmin userAdmin, LdapName dn, Attributes attributes) { super(userAdmin, dn, attributes); } @@ -139,7 +138,7 @@ abstract class LdifGroup extends LdifUser implements DirectoryGroup { static class LdifOrganization extends LdifGroup implements Organization { - public LdifOrganization(AbstractUserDirectory userAdmin, LdapName dn, Attributes attributes) { + public LdifOrganization(DirectoryUserAdmin userAdmin, LdapName dn, Attributes attributes) { super(userAdmin, dn, attributes); } @@ -147,7 +146,7 @@ abstract class LdifGroup extends LdifUser implements DirectoryGroup { static class LdifSystemPermissions extends LdifGroup implements SystemPermissions { - public LdifSystemPermissions(AbstractUserDirectory userAdmin, LdapName dn, Attributes attributes) { + public LdifSystemPermissions(DirectoryUserAdmin userAdmin, LdapName dn, Attributes attributes) { super(userAdmin, dn, attributes); } diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUser.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUser.java index 6cf6725cc..cceb6e461 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUser.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUser.java @@ -23,6 +23,7 @@ import javax.naming.ldap.LdapName; import org.argeo.util.directory.DirectoryDigestUtils; import org.argeo.util.directory.Person; +import org.argeo.util.directory.ldap.AbstractLdapDirectory; import org.argeo.util.directory.ldap.AbstractLdapEntry; import org.argeo.util.directory.ldap.AuthPassword; import org.argeo.util.naming.LdapAttrs; @@ -34,7 +35,7 @@ abstract class LdifUser extends AbstractLdapEntry implements DirectoryUser { private final AttributeDictionary properties; private final AttributeDictionary credentials; - LdifUser(AbstractUserDirectory userAdmin, LdapName dn, Attributes attributes) { + LdifUser(AbstractLdapDirectory userAdmin, LdapName dn, Attributes attributes) { super(userAdmin, dn, attributes); properties = new AttributeDictionary(false); credentials = new AttributeDictionary(true); @@ -160,8 +161,8 @@ abstract class LdifUser extends AbstractLdapEntry implements DirectoryUser { // return hashedPassword; // } - protected AbstractUserDirectory getUserAdmin() { - return (AbstractUserDirectory) getDirectory(); + protected DirectoryUserAdmin getUserAdmin() { + return (DirectoryUserAdmin) getDirectory(); } private class AttributeDictionary extends Dictionary { @@ -357,7 +358,7 @@ abstract class LdifUser extends AbstractLdapEntry implements DirectoryUser { static class LdifPerson extends LdifUser implements Person { - public LdifPerson(AbstractUserDirectory userAdmin, LdapName dn, Attributes attributes) { + public LdifPerson(DirectoryUserAdmin userAdmin, LdapName dn, Attributes attributes) { super(userAdmin, dn, attributes); } diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUserAdmin.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUserAdmin.java deleted file mode 100644 index c978af4a0..000000000 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUserAdmin.java +++ /dev/null @@ -1,379 +0,0 @@ -package org.argeo.osgi.useradmin; - -import static org.argeo.util.naming.LdapAttrs.objectClass; -import static org.argeo.util.naming.LdapObjs.inetOrgPerson; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Dictionary; -import java.util.HashSet; -import java.util.Hashtable; -import java.util.List; -import java.util.NavigableMap; -import java.util.Objects; -import java.util.Set; -import java.util.SortedMap; -import java.util.TreeMap; - -import javax.naming.NameNotFoundException; -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attributes; -import javax.naming.ldap.LdapName; - -import org.argeo.util.directory.DirectoryConf; -import org.argeo.util.directory.DirectoryDigestUtils; -import org.argeo.util.directory.HierarchyUnit; -import org.argeo.util.directory.ldap.LdapEntry; -import org.argeo.util.directory.ldap.LdapEntryWorkingCopy; -import org.argeo.util.directory.ldap.LdapHierarchyUnit; -import org.argeo.util.directory.ldap.LdifParser; -import org.argeo.util.directory.ldap.LdifWriter; -import org.argeo.util.naming.LdapObjs; -import org.osgi.framework.Filter; -import org.osgi.service.useradmin.Role; -import org.osgi.service.useradmin.User; - -/** A user admin based on a LDIF files. */ -public class LdifUserAdmin extends AbstractUserDirectory { - private NavigableMap users = new TreeMap<>(); - private NavigableMap groups = new TreeMap<>(); - - private NavigableMap hierarchy = new TreeMap<>(); -// private List rootHierarchyUnits = new ArrayList<>(); - - public LdifUserAdmin(String uri, String baseDn) { - this(fromUri(uri, baseDn), false); - } - - public LdifUserAdmin(Dictionary properties) { - this(properties, false); - } - - protected LdifUserAdmin(Dictionary properties, boolean scoped) { - super(null, properties, scoped); - } - - public LdifUserAdmin(URI uri, Dictionary properties) { - super(uri, properties, false); - } - - @Override - protected AbstractUserDirectory scope(User user) { - Dictionary credentials = user.getCredentials(); - String username = (String) credentials.get(SHARED_STATE_USERNAME); - if (username == null) - username = user.getName(); - Object pwdCred = credentials.get(SHARED_STATE_PASSWORD); - byte[] pwd = (byte[]) pwdCred; - if (pwd != null) { - char[] password = DirectoryDigestUtils.bytesToChars(pwd); - User directoryUser = (User) getRole(username); - if (!directoryUser.hasCredential(null, password)) - throw new IllegalStateException("Invalid credentials"); - } else { - throw new IllegalStateException("Password is required"); - } - Dictionary properties = cloneProperties(); - properties.put(DirectoryConf.readOnly.name(), "true"); - LdifUserAdmin scopedUserAdmin = new LdifUserAdmin(properties, true); - scopedUserAdmin.groups = Collections.unmodifiableNavigableMap(groups); - scopedUserAdmin.users = Collections.unmodifiableNavigableMap(users); - return scopedUserAdmin; - } - - private static Dictionary fromUri(String uri, String baseDn) { - Hashtable res = new Hashtable(); - res.put(DirectoryConf.uri.name(), uri); - res.put(DirectoryConf.baseDn.name(), baseDn); - return res; - } - - public void init() { - - try { - URI u = new URI(getUri()); - if (u.getScheme().equals("file")) { - File file = new File(u); - if (!file.exists()) - return; - } - load(u.toURL().openStream()); - } catch (IOException | URISyntaxException e) { - throw new IllegalStateException("Cannot open URL " + getUri(), e); - } - } - - public void save() { - if (getUri() == null) - throw new IllegalStateException("Cannot save LDIF user admin: no URI is set"); - if (isReadOnly()) - throw new IllegalStateException("Cannot save LDIF user admin: " + getUri() + " is read-only"); - try (FileOutputStream out = new FileOutputStream(new File(new URI(getUri())))) { - save(out); - } catch (IOException | URISyntaxException e) { - throw new IllegalStateException("Cannot save user admin to " + getUri(), e); - } - } - - public void save(OutputStream out) throws IOException { - try { - LdifWriter ldifWriter = new LdifWriter(out); - for (LdapName name : hierarchy.keySet()) - ldifWriter.writeEntry(name, hierarchy.get(name).getAttributes()); - for (LdapName name : groups.keySet()) - ldifWriter.writeEntry(name, groups.get(name).getAttributes()); - for (LdapName name : users.keySet()) - ldifWriter.writeEntry(name, users.get(name).getAttributes()); - } finally { - out.close(); - } - } - - protected void load(InputStream in) { - try { - users.clear(); - groups.clear(); - hierarchy.clear(); - - LdifParser ldifParser = new LdifParser(); - SortedMap allEntries = ldifParser.read(in); - for (LdapName key : allEntries.keySet()) { - Attributes attributes = allEntries.get(key); - // check for inconsistency - Set lowerCase = new HashSet(); - NamingEnumeration ids = attributes.getIDs(); - while (ids.hasMoreElements()) { - String id = ids.nextElement().toLowerCase(); - if (lowerCase.contains(id)) - throw new IllegalStateException(key + " has duplicate id " + id); - lowerCase.add(id); - } - - // analyse object classes - NamingEnumeration objectClasses = attributes.get(objectClass.name()).getAll(); - // System.out.println(key); - objectClasses: while (objectClasses.hasMore()) { - String objectClass = objectClasses.next().toString(); - // System.out.println(" " + objectClass); - if (objectClass.toLowerCase().equals(inetOrgPerson.name().toLowerCase())) { - users.put(key, newUser(key, attributes)); - break objectClasses; - } else if (objectClass.toLowerCase().equals(getGroupObjectClass().toLowerCase())) { - groups.put(key, newGroup(key, attributes)); - break objectClasses; -// } else if (objectClass.equalsIgnoreCase(LdapObjs.organization.name())) { -// // we only consider organizations which are not groups -// hierarchy.put(key, new LdifHierarchyUnit(this, key, HierarchyUnit.ORGANIZATION, attributes)); -// break objectClasses; - } else if (objectClass.equalsIgnoreCase(LdapObjs.organizationalUnit.name())) { -// String name = key.getRdn(key.size() - 1).toString(); -// if (getUserBase().equalsIgnoreCase(name) || getGroupBase().equalsIgnoreCase(name)) -// break objectClasses; // skip - // TODO skip if it does not contain groups or users - hierarchy.put(key, new LdapHierarchyUnit(this, key, attributes)); - break objectClasses; - } - } - } - - // link hierarchy -// hierachyUnits: for (LdapName dn : hierarchy.keySet()) { -// LdifHierarchyUnit unit = hierarchy.get(dn); -// LdapName parentDn = (LdapName) dn.getPrefix(dn.size() - 1); -// LdifHierarchyUnit parent = hierarchy.get(parentDn); -// if (parent == null) { -// rootHierarchyUnits.add(unit); -// unit.parent = null; -// continue hierachyUnits; -// } -// parent.children.add(unit); -// unit.parent = parent; -// } - } catch (NamingException | IOException e) { - throw new IllegalStateException("Cannot load user admin service from LDIF", e); - } - } - - public void destroy() { - if (users == null || groups == null) - throw new IllegalStateException("User directory " + getBaseDn() + " is already destroyed"); - users = null; - groups = null; - } - - /* - * USER ADMIN - */ - - @Override - protected DirectoryUser daoGetEntry(LdapName key) throws NameNotFoundException { - if (groups.containsKey(key)) - return (DirectoryUser) groups.get(key); - if (users.containsKey(key)) - return (DirectoryUser) users.get(key); - throw new NameNotFoundException(key + " not persisted"); - } - - @Override - protected Boolean daoHasEntry(LdapName dn) { - return users.containsKey(dn) || groups.containsKey(dn); - } - - @Override - protected List doGetEntries(LdapName searchBase, Filter f, boolean deep) { - Objects.requireNonNull(searchBase); - ArrayList res = new ArrayList<>(); - if (f == null && deep && getBaseDn().equals(searchBase)) { - res.addAll(users.values()); - res.addAll(groups.values()); - } else { - filterRoles(users, searchBase, f, deep, res); - filterRoles(groups, searchBase, f, deep, res); - } - return res; - } - - private void filterRoles(SortedMap map, LdapName searchBase, Filter f, boolean deep, - List res) { - // TODO reduce map with search base ? - roles: for (LdapEntry user : map.values()) { - LdapName dn = user.getDn(); - if (dn.startsWith(searchBase)) { - if (!deep && dn.size() != (searchBase.size() + 1)) - continue roles; - if (f == null) - res.add(user); - else { - if (f.match(((DirectoryUser) user).getProperties())) - res.add(user); - } - } - } - - } - - @Override - protected List getDirectGroups(LdapName dn) { - List directGroups = new ArrayList(); - for (LdapName name : groups.keySet()) { - DirectoryGroup group; - try { - group = (DirectoryGroup) daoGetEntry(name); - } catch (NameNotFoundException e) { - throw new IllegalArgumentException("Group " + dn + " not found", e); - } - if (group.getMemberNames().contains(dn)) - directGroups.add(group.getDn()); - } - return directGroups; - } - - @Override - public void prepare(LdapEntryWorkingCopy wc) { - // delete - for (LdapName dn : wc.getDeletedData().keySet()) { - if (users.containsKey(dn)) - users.remove(dn); - else if (groups.containsKey(dn)) - groups.remove(dn); - else - throw new IllegalStateException("User to delete not found " + dn); - } - // add - for (LdapName dn : wc.getNewData().keySet()) { - DirectoryUser user = (DirectoryUser) wc.getNewData().get(dn); - if (users.containsKey(dn) || groups.containsKey(dn)) - throw new IllegalStateException("User to create found " + dn); - else if (Role.USER == user.getType()) - users.put(dn, user); - else if (Role.GROUP == user.getType()) - groups.put(dn, (DirectoryGroup) user); - else - throw new IllegalStateException("Unsupported role type " + user.getType() + " for new user " + dn); - } - // modify - for (LdapName dn : wc.getModifiedData().keySet()) { - Attributes modifiedAttrs = wc.getModifiedData().get(dn); - DirectoryUser user; - try { - user = daoGetEntry(dn); - } catch (NameNotFoundException e) { - throw new IllegalStateException("User to modify no found " + dn, e); - } - if (user == null) - throw new IllegalStateException("User to modify no found " + dn); - user.publishAttributes(modifiedAttrs); - } - } - - @Override - public void commit(LdapEntryWorkingCopy wc) { - save(); - } - - @Override - public void rollback(LdapEntryWorkingCopy wc) { - init(); - } - - /* - * HIERARCHY - */ - -// @Override -// public int getHierarchyChildCount() { -// return rootHierarchyUnits.size(); -// } -// -// @Override -// public HierarchyUnit getHierarchyChild(int i) { -// return rootHierarchyUnits.get(i); -// } - @Override - public HierarchyUnit doGetHierarchyUnit(LdapName dn) { - return hierarchy.get(dn); - } - - @Override - public Iterable doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) { - List res = new ArrayList<>(); - for (LdapName n : hierarchy.keySet()) { - if (n.size() == searchBase.size() + 1) { - if (n.startsWith(searchBase)) { - HierarchyUnit hu = hierarchy.get(n); - if (functionalOnly) { - if (hu.isFunctional()) - res.add(hu); - } else { - res.add(hu); - } - } - } - } - return res; - } - -// @Override -// public Iterable getDirectHierarchyUnits(boolean functionalOnly) { -// if (functionalOnly) { -// List res = new ArrayList<>(); -// for (HierarchyUnit hu : rootHierarchyUnits) { -// if (hu.isFunctional()) -// res.add(hu); -// } -// return res; -// -// } else { -// return rootHierarchyUnits; -// } -// } - -} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserDirectory.java b/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserDirectory.java index 1f428ecbd..1adc7e0df 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserDirectory.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserDirectory.java @@ -1,8 +1,6 @@ package org.argeo.osgi.useradmin; -import java.net.URI; import java.util.ArrayList; -import java.util.Dictionary; import java.util.List; import javax.naming.NameNotFoundException; @@ -12,23 +10,23 @@ import javax.naming.directory.BasicAttributes; import javax.naming.ldap.LdapName; import org.argeo.util.directory.HierarchyUnit; +import org.argeo.util.directory.ldap.AbstractLdapDirectory; +import org.argeo.util.directory.ldap.AbstractLdapDirectoryDao; import org.argeo.util.directory.ldap.LdapEntry; import org.argeo.util.directory.ldap.LdapEntryWorkingCopy; import org.argeo.util.naming.LdapAttrs; -import org.osgi.framework.Filter; -import org.osgi.service.useradmin.User; /** Pseudo user directory to be used when logging in as OS user. */ -public class OsUserDirectory extends AbstractUserDirectory { +public class OsUserDirectory extends AbstractLdapDirectoryDao { private final String osUsername = System.getProperty("user.name"); private final LdapName osUserDn; - private final DirectoryUser osUser; + private final LdapEntry osUser; - public OsUserDirectory(URI uriArg, Dictionary props) { - super(uriArg, props, false); + public OsUserDirectory(AbstractLdapDirectory directory) { + super(directory); try { - osUserDn = new LdapName( - LdapAttrs.uid.name() + "=" + osUsername + "," + getUserBaseRdn() + "," + getBaseDn()); + osUserDn = new LdapName(LdapAttrs.uid.name() + "=" + osUsername + "," + directory.getUserBaseRdn() + "," + + directory.getBaseDn()); Attributes attributes = new BasicAttributes(); attributes.put(LdapAttrs.uid.name(), osUsername); osUser = newUser(osUserDn, attributes); @@ -38,17 +36,17 @@ public class OsUserDirectory extends AbstractUserDirectory { } @Override - protected List getDirectGroups(LdapName dn) { + public List getDirectGroups(LdapName dn) { return new ArrayList<>(); } @Override - protected Boolean daoHasEntry(LdapName dn) { + public Boolean daoHasEntry(LdapName dn) { return osUserDn.equals(dn); } @Override - protected DirectoryUser daoGetEntry(LdapName key) throws NameNotFoundException { + public LdapEntry daoGetEntry(LdapName key) throws NameNotFoundException { if (osUserDn.equals(key)) return osUser; else @@ -56,18 +54,13 @@ public class OsUserDirectory extends AbstractUserDirectory { } @Override - protected List doGetEntries(LdapName searchBase, Filter f, boolean deep) { + public List doGetEntries(LdapName searchBase, String f, boolean deep) { List res = new ArrayList<>(); - if (f == null || f.match(osUser.getProperties())) - res.add(osUser); +// if (f == null || f.match(osUser.getProperties())) + res.add(osUser); return res; } - @Override - protected AbstractUserDirectory scope(User user) { - throw new UnsupportedOperationException(); - } - @Override public HierarchyUnit doGetHierarchyUnit(LdapName dn) { return null; @@ -90,4 +83,17 @@ public class OsUserDirectory extends AbstractUserDirectory { } + @Override + public void init() { + // TODO Auto-generated method stub + + } + + @Override + public void destroy() { + // TODO Auto-generated method stub + + } + + } diff --git a/org.argeo.util/src/org/argeo/util/directory/Directory.java b/org.argeo.util/src/org/argeo/util/directory/Directory.java index b3dfa8b05..05808908d 100644 --- a/org.argeo.util/src/org/argeo/util/directory/Directory.java +++ b/org.argeo.util/src/org/argeo/util/directory/Directory.java @@ -17,10 +17,6 @@ public interface Directory { boolean isDisabled(); - String getUserObjectClass(); - - String getGroupObjectClass(); - Optional getRealm(); void setTransactionControl(WorkControl transactionControl); diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapDirectory.java b/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapDirectory.java index 27f9c55e3..d8e8e7d21 100644 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapDirectory.java +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapDirectory.java @@ -23,19 +23,17 @@ import javax.naming.ldap.LdapName; import javax.naming.ldap.Rdn; import javax.transaction.xa.XAResource; +import org.argeo.osgi.useradmin.OsUserDirectory; import org.argeo.util.directory.Directory; import org.argeo.util.directory.DirectoryConf; import org.argeo.util.directory.HierarchyUnit; import org.argeo.util.naming.LdapAttrs; import org.argeo.util.naming.LdapObjs; import org.argeo.util.transaction.WorkControl; -import org.argeo.util.transaction.WorkingCopyProcessor; import org.argeo.util.transaction.WorkingCopyXaResource; import org.argeo.util.transaction.XAResourceProvider; -import org.osgi.framework.Filter; -public abstract class AbstractLdapDirectory - implements Directory, WorkingCopyProcessor, XAResourceProvider { +public abstract class AbstractLdapDirectory implements Directory, XAResourceProvider { protected static final String SHARED_STATE_USERNAME = "javax.security.auth.login.name"; protected static final String SHARED_STATE_PASSWORD = "javax.security.auth.login.password"; @@ -43,6 +41,7 @@ public abstract class AbstractLdapDirectory protected final Hashtable properties; private final Rdn userBaseRdn, groupBaseRdn, systemRoleBaseRdn; private final String userObjectClass, groupObjectClass; + private String memberAttributeId = "member"; private final boolean readOnly; private final boolean disabled; @@ -52,12 +51,13 @@ public abstract class AbstractLdapDirectory private final boolean scoped; - private String memberAttributeId = "member"; private List credentialAttributeIds = Arrays .asList(new String[] { LdapAttrs.userPassword.name(), LdapAttrs.authPassword.name() }); private WorkControl transactionControl; - private WorkingCopyXaResource xaResource = new WorkingCopyXaResource<>(this); + private WorkingCopyXaResource xaResource; + + private LdapDirectoryDao directoryDao; public AbstractLdapDirectory(URI uriArg, Dictionary props, boolean scoped) { this.properties = new Hashtable(); @@ -82,8 +82,9 @@ public abstract class AbstractLdapDirectory forcedPassword = DirectoryConf.forcedPassword.getValue(properties); userObjectClass = DirectoryConf.userObjectClass.getValue(properties); - String userBase = DirectoryConf.userBase.getValue(properties); groupObjectClass = DirectoryConf.groupObjectClass.getValue(properties); + + String userBase = DirectoryConf.userBase.getValue(properties); String groupBase = DirectoryConf.groupBase.getValue(properties); String systemRoleBase = DirectoryConf.systemRoleBase.getValue(properties); try { @@ -112,21 +113,56 @@ public abstract class AbstractLdapDirectory disabled = Boolean.parseBoolean(disabledStr); else disabled = false; + + URI u = URI.create(uri); + if (!getRealm().isEmpty() || DirectoryConf.SCHEME_LDAP.equals(u.getScheme()) + || DirectoryConf.SCHEME_LDAPS.equals(u.getScheme())) { + directoryDao = new LdapDao(this); + } else if (DirectoryConf.SCHEME_FILE.equals(u.getScheme())) { + directoryDao = new LdifDao(this); + } else if (DirectoryConf.SCHEME_OS.equals(u.getScheme())) { + directoryDao = new OsUserDirectory(this); + // singleUser = true; + } else { + throw new IllegalArgumentException("Unsupported scheme " + u.getScheme()); + } + xaResource = new WorkingCopyXaResource<>(directoryDao); } /* * ABSTRACT METHODS */ - public abstract HierarchyUnit doGetHierarchyUnit(LdapName dn); +// public abstract HierarchyUnit doGetHierarchyUnit(LdapName dn); +// +// public abstract Iterable doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly); +// +// protected abstract Boolean daoHasEntry(LdapName dn); +// +// protected abstract LdapEntry daoGetEntry(LdapName key) throws NameNotFoundException; +// +// protected abstract List doGetEntries(LdapName searchBase, Filter f, boolean deep); +// +// /** Returns the groups this user is a direct member of. */ +// protected abstract List getDirectGroups(LdapName dn); + /* + * INITIALIZATION + */ - public abstract Iterable doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly); + public void init() { + getDirectoryDao().init(); + } - protected abstract Boolean daoHasEntry(LdapName dn); + public void destroy() { + getDirectoryDao().destroy(); + } - protected abstract LdapEntry daoGetEntry(LdapName key) throws NameNotFoundException; + /* + * CREATION + */ + protected abstract LdapEntry newUser(LdapName name, Attributes attrs); - protected abstract List doGetEntries(LdapName searchBase, Filter f, boolean deep); + protected abstract LdapEntry newGroup(LdapName name, Attributes attrs); /* * EDITION @@ -162,9 +198,73 @@ public abstract class AbstractLdapDirectory return xaResource; } - @Override - public LdapEntryWorkingCopy newWorkingCopy() { - return new LdapEntryWorkingCopy(); + public boolean removeEntry(LdapName dn) { + checkEdit(); + LdapEntryWorkingCopy wc = getWorkingCopy(); + boolean actuallyDeleted; + if (getDirectoryDao().daoHasEntry(dn) || wc.getNewData().containsKey(dn)) { + LdapEntry user = doGetRole(dn); + wc.getDeletedData().put(dn, user); + actuallyDeleted = true; + } else {// just removing from groups (e.g. system roles) + actuallyDeleted = false; + } + for (LdapName groupDn : getDirectoryDao().getDirectGroups(dn)) { + LdapEntry group = doGetRole(groupDn); + group.getAttributes().get(getMemberAttributeId()).remove(dn.toString()); + } + return actuallyDeleted; + } + + /* + * RETRIEVAL + */ + + protected LdapEntry doGetRole(LdapName dn) { + LdapEntryWorkingCopy wc = getWorkingCopy(); + LdapEntry user; + try { + user = getDirectoryDao().daoGetEntry(dn); + } catch (NameNotFoundException e) { + user = null; + } + if (wc != null) { + if (user == null && wc.getNewData().containsKey(dn)) + user = wc.getNewData().get(dn); + else if (wc.getDeletedData().containsKey(dn)) + user = null; + } + return user; + } + + protected void collectGroups(LdapEntry user, List allRoles) { + Attributes attrs = user.getAttributes(); + // TODO centralize attribute name + Attribute memberOf = attrs.get(LdapAttrs.memberOf.name()); + // if user belongs to this directory, we only check memberOf + if (memberOf != null && user.getDn().startsWith(getBaseDn())) { + try { + NamingEnumeration values = memberOf.getAll(); + while (values.hasMore()) { + Object value = values.next(); + LdapName groupDn = new LdapName(value.toString()); + LdapEntry group = doGetRole(groupDn); + if (group != null) + allRoles.add(group); + } + } catch (NamingException e) { + throw new IllegalStateException("Cannot get memberOf groups for " + user, e); + } + } else { + for (LdapName groupDn : getDirectoryDao().getDirectGroups(user.getDn())) { + // TODO check for loops + LdapEntry group = doGetRole(groupDn); + if (group != null) { + allRoles.add(group); + collectGroups(group, allRoles); + } + } + } } /* @@ -173,12 +273,12 @@ public abstract class AbstractLdapDirectory @Override public HierarchyUnit getHierarchyUnit(String path) { LdapName dn = pathToName(path); - return doGetHierarchyUnit(dn); + return directoryDao.doGetHierarchyUnit(dn); } @Override public Iterable getDirectHierarchyUnits(boolean functionalOnly) { - return doGetDirectHierarchyUnits(baseDn, functionalOnly); + return directoryDao.doGetDirectHierarchyUnits(baseDn, functionalOnly); } /* @@ -239,20 +339,23 @@ public abstract class AbstractLdapDirectory /* * UTILITIES */ - protected static boolean hasObjectClass(Attributes attrs, LdapObjs objectClass) { + return hasObjectClass(attrs, objectClass.name()); + } + + protected static boolean hasObjectClass(Attributes attrs, String objectClass) { try { Attribute attr = attrs.get(LdapAttrs.objectClass.name()); NamingEnumeration en = attr.getAll(); while (en.hasMore()) { String v = en.next().toString(); - if (v.equalsIgnoreCase(objectClass.name())) + if (v.equalsIgnoreCase(objectClass)) return true; } return false; } catch (NamingException e) { - throw new IllegalStateException("Cannot search for objectClass " + objectClass.name(), e); + throw new IllegalStateException("Cannot search for objectClass " + objectClass, e); } } @@ -294,7 +397,7 @@ public abstract class AbstractLdapDirectory return Optional.of(realm.toString()); } - protected LdapName getBaseDn() { + public LdapName getBaseDn() { return (LdapName) baseDn.clone(); } @@ -306,23 +409,10 @@ public abstract class AbstractLdapDirectory return disabled; } - /** dn can be null, in that case a default should be returned. */ - public String getUserObjectClass() { - return userObjectClass; - } - public Rdn getUserBaseRdn() { return userBaseRdn; } - protected String newUserObjectClass(LdapName dn) { - return getUserObjectClass(); - } - - public String getGroupObjectClass() { - return groupObjectClass; - } - public Rdn getGroupBaseRdn() { return groupBaseRdn; } @@ -347,18 +437,31 @@ public abstract class AbstractLdapDirectory return scoped; } - public String getMemberAttributeId() { - return memberAttributeId; - } - public List getCredentialAttributeIds() { return credentialAttributeIds; } - protected String getUri() { + public String getUri() { return uri; } + public LdapDirectoryDao getDirectoryDao() { + return directoryDao; + } + + /** dn can be null, in that case a default should be returned. */ + public String getUserObjectClass() { + return userObjectClass; + } + + public String getGroupObjectClass() { + return groupObjectClass; + } + + public String getMemberAttributeId() { + return memberAttributeId; + } + /* * OBJECT METHODS */ diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapDirectoryDao.java b/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapDirectoryDao.java new file mode 100644 index 000000000..8c87170fe --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapDirectoryDao.java @@ -0,0 +1,34 @@ +package org.argeo.util.directory.ldap; + +import javax.naming.directory.Attributes; +import javax.naming.ldap.LdapName; + +public abstract class AbstractLdapDirectoryDao implements LdapDirectoryDao { + + private AbstractLdapDirectory directory; + + public AbstractLdapDirectoryDao(AbstractLdapDirectory directory) { + this.directory = directory; + + } + + public AbstractLdapDirectory getDirectory() { + return directory; + } + + @Override + public LdapEntryWorkingCopy newWorkingCopy() { + return new LdapEntryWorkingCopy(); + } + + @Override + public LdapEntry newUser(LdapName name, Attributes attrs) { + return getDirectory().newUser(name, attrs); + } + + @Override + public LdapEntry newGroup(LdapName name, Attributes attrs) { + return getDirectory().newGroup(name, attrs); + } + +} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapEntry.java b/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapEntry.java index be919c020..25f233a01 100644 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapEntry.java +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapEntry.java @@ -1,8 +1,16 @@ package org.argeo.util.directory.ldap; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; import javax.naming.directory.Attributes; import javax.naming.ldap.LdapName; +/** An entry in an LDAP (or LDIF) directory. */ public abstract class AbstractLdapEntry implements LdapEntry { private final AbstractLdapDirectory directory; @@ -10,8 +18,10 @@ public abstract class AbstractLdapEntry implements LdapEntry { private Attributes publishedAttributes; - protected AbstractLdapEntry(AbstractLdapDirectory userAdmin, LdapName dn, Attributes attributes) { - this.directory = userAdmin; + protected AbstractLdapEntry(AbstractLdapDirectory directory, LdapName dn, Attributes attributes) { + Objects.requireNonNull(directory); + Objects.requireNonNull(dn); + this.directory = directory; this.dn = dn; this.publishedAttributes = attributes; } @@ -24,6 +34,25 @@ public abstract class AbstractLdapEntry implements LdapEntry { public synchronized Attributes getAttributes() { return isEditing() ? getModifiedAttributes() : publishedAttributes; } + + @Override + public List getReferences(String attributeId){ + Attribute memberAttribute = getAttributes().get(attributeId); + if (memberAttribute == null) + return new ArrayList(); + try { + List roles = new ArrayList(); + NamingEnumeration values = memberAttribute.getAll(); + while (values.hasMore()) { + LdapName dn = new LdapName(values.next().toString()); + roles.add(dn); + } + return roles; + } catch (NamingException e) { + throw new IllegalStateException("Cannot get members", e); + } + + } /** Should only be called from working copy thread. */ protected synchronized Attributes getModifiedAttributes() { @@ -53,10 +82,14 @@ public abstract class AbstractLdapEntry implements LdapEntry { publishedAttributes = modifiedAttributes; } - protected AbstractLdapDirectory getDirectory() { + public AbstractLdapDirectory getDirectory() { return directory; } + public LdapDirectoryDao getDirectoryDao() { + return directory.getDirectoryDao(); + } + @Override public int hashCode() { return dn.hashCode(); diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapDao.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapDao.java new file mode 100644 index 000000000..a2d9e7fc3 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapDao.java @@ -0,0 +1,247 @@ +package org.argeo.util.directory.ldap; + +import static org.argeo.util.naming.LdapAttrs.objectClass; + +import java.util.ArrayList; +import java.util.List; + +import javax.naming.AuthenticationNotSupportedException; +import javax.naming.Binding; +import javax.naming.InvalidNameException; +import javax.naming.NameNotFoundException; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; + +import org.argeo.util.directory.HierarchyUnit; +import org.argeo.util.naming.LdapObjs; + +/** A user admin based on a LDAP server. */ +public class LdapDao extends AbstractLdapDirectoryDao { + private LdapConnection ldapConnection; + +// public LdapUserAdmin(Dictionary properties) { +// this(properties, false); +// } + + public LdapDao(AbstractLdapDirectory directory) { + super(directory); + } + + @Override + public void init() { + ldapConnection = new LdapConnection(getDirectory().getUri().toString(), getDirectory().getProperties()); + } + + public void destroy() { + ldapConnection.destroy(); + } + +// @Override +// protected AbstractUserDirectory scope(User user) { +// Dictionary credentials = user.getCredentials(); +// String username = (String) credentials.get(SHARED_STATE_USERNAME); +// if (username == null) +// username = user.getName(); +// Dictionary properties = cloneProperties(); +// properties.put(Context.SECURITY_PRINCIPAL, username.toString()); +// Object pwdCred = credentials.get(SHARED_STATE_PASSWORD); +// byte[] pwd = (byte[]) pwdCred; +// if (pwd != null) { +// char[] password = DirectoryDigestUtils.bytesToChars(pwd); +// properties.put(Context.SECURITY_CREDENTIALS, new String(password)); +// } else { +// properties.put(Context.SECURITY_AUTHENTICATION, "GSSAPI"); +// } +// return new LdapUserAdmin(properties, true); +// } + +// protected InitialLdapContext getLdapContext() { +// return initialLdapContext; +// } + + @Override + public Boolean daoHasEntry(LdapName dn) { + try { + return daoGetEntry(dn) != null; + } catch (NameNotFoundException e) { + return false; + } + } + + @Override + public LdapEntry daoGetEntry(LdapName name) throws NameNotFoundException { + try { + Attributes attrs = ldapConnection.getAttributes(name); + if (attrs.size() == 0) + return null; +// int roleType = roleType(name); + LdapEntry res; + if (isGroup(name)) + res = newGroup(name, attrs); + else + res = newUser(name, attrs); +// else +// throw new IllegalArgumentException("Unsupported LDAP type for " + name); + return res; + } catch (NameNotFoundException e) { + throw e; + } catch (NamingException e) { + return null; + } + } + + protected boolean isGroup(LdapName dn) { + Rdn technicalRdn = LdapNameUtils.getParentRdn(dn); + if (getDirectory().getGroupBaseRdn().equals(technicalRdn) + || getDirectory().getSystemRoleBaseRdn().equals(technicalRdn)) + return true; + else if (getDirectory().getUserBaseRdn().equals(technicalRdn)) + return false; + else + throw new IllegalArgumentException( + "Cannot dind role type, " + technicalRdn + " is not a technical RDN for " + dn); + } + + @Override + public List doGetEntries(LdapName searchBase, String f, boolean deep) { + ArrayList res = new ArrayList<>(); + try { + String searchFilter = f != null ? f.toString() + : "(|(" + objectClass + "=" + getDirectory().getUserObjectClass() + ")(" + objectClass + "=" + + getDirectory().getGroupObjectClass() + "))"; + SearchControls searchControls = new SearchControls(); + // FIXME make one level consistent with deep + searchControls.setSearchScope(deep ? SearchControls.SUBTREE_SCOPE : SearchControls.ONELEVEL_SCOPE); + + // LdapName searchBase = getBaseDn(); + NamingEnumeration results = ldapConnection.search(searchBase, searchFilter, searchControls); + + results: while (results.hasMoreElements()) { + SearchResult searchResult = results.next(); + Attributes attrs = searchResult.getAttributes(); + Attribute objectClassAttr = attrs.get(objectClass.name()); + LdapName dn = toDn(searchBase, searchResult); + LdapEntry role; + if (objectClassAttr.contains(getDirectory().getGroupObjectClass()) + || objectClassAttr.contains(getDirectory().getGroupObjectClass().toLowerCase())) + role = newGroup(dn, attrs); + else if (objectClassAttr.contains(getDirectory().getUserObjectClass()) + || objectClassAttr.contains(getDirectory().getUserObjectClass().toLowerCase())) + role = newUser(dn, attrs); + else { +// log.warn("Unsupported LDAP type for " + searchResult.getName()); + continue results; + } + res.add(role); + } + return res; + } catch (AuthenticationNotSupportedException e) { + // ignore (typically an unsupported anonymous bind) + // TODO better logging + return res; + } catch (NamingException e) { + throw new IllegalStateException("Cannot get roles for filter " + f, e); + } + } + + private LdapName toDn(LdapName baseDn, Binding binding) throws InvalidNameException { + return new LdapName(binding.isRelative() ? binding.getName() + "," + baseDn : binding.getName()); + } + + @Override + public List getDirectGroups(LdapName dn) { + List directGroups = new ArrayList(); + try { + String searchFilter = "(&(" + objectClass + "=" + getDirectory().getGroupObjectClass() + ")(" + + getDirectory().getMemberAttributeId() + "=" + dn + "))"; + + SearchControls searchControls = new SearchControls(); + searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); + + LdapName searchBase = getDirectory().getBaseDn(); + NamingEnumeration results = ldapConnection.search(searchBase, searchFilter, searchControls); + + while (results.hasMoreElements()) { + SearchResult searchResult = (SearchResult) results.nextElement(); + directGroups.add(toDn(searchBase, searchResult)); + } + return directGroups; + } catch (NamingException e) { + throw new IllegalStateException("Cannot populate direct members of " + dn, e); + } + } + + @Override + public void prepare(LdapEntryWorkingCopy wc) { + try { + ldapConnection.prepareChanges(wc); + } catch (NamingException e) { + throw new IllegalStateException("Cannot prepare LDAP", e); + } + } + + @Override + public void commit(LdapEntryWorkingCopy wc) { + try { + ldapConnection.commitChanges(wc); + } catch (NamingException e) { + throw new IllegalStateException("Cannot commit LDAP", e); + } + } + + @Override + public void rollback(LdapEntryWorkingCopy wc) { + // prepare not impacting + } + + /* + * HIERARCHY + */ + + @Override + public Iterable doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) { + List res = new ArrayList<>(); + try { + String searchFilter = "(|(" + objectClass + "=" + LdapObjs.organizationalUnit.name() + ")(" + objectClass + + "=" + LdapObjs.organization.name() + "))"; + + SearchControls searchControls = new SearchControls(); + searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE); + + NamingEnumeration results = ldapConnection.search(searchBase, searchFilter, searchControls); + + while (results.hasMoreElements()) { + SearchResult searchResult = (SearchResult) results.nextElement(); + LdapName dn = toDn(searchBase, searchResult); + Attributes attrs = searchResult.getAttributes(); + LdapHierarchyUnit hierarchyUnit = new LdapHierarchyUnit(getDirectory(), dn, attrs); + if (functionalOnly) { + if (hierarchyUnit.isFunctional()) + res.add(hierarchyUnit); + } else { + res.add(hierarchyUnit); + } + } + return res; + } catch (NamingException e) { + throw new IllegalStateException("Cannot get direct hierarchy units ", e); + } + } + + @Override + public HierarchyUnit doGetHierarchyUnit(LdapName dn) { + try { + Attributes attrs = ldapConnection.getAttributes(dn); + return new LdapHierarchyUnit(getDirectory(), dn, attrs); + } catch (NamingException e) { + throw new IllegalStateException("Cannot get hierarchy unit " + dn, e); + } + } + +} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapDirectoryDao.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapDirectoryDao.java new file mode 100644 index 000000000..3a0f4e6e0 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapDirectoryDao.java @@ -0,0 +1,32 @@ +package org.argeo.util.directory.ldap; + +import java.util.List; + +import javax.naming.NameNotFoundException; +import javax.naming.directory.Attributes; +import javax.naming.ldap.LdapName; + +import org.argeo.util.directory.HierarchyUnit; +import org.argeo.util.transaction.WorkingCopyProcessor; + +public interface LdapDirectoryDao extends WorkingCopyProcessor { + Boolean daoHasEntry(LdapName dn); + + LdapEntry daoGetEntry(LdapName name) throws NameNotFoundException; + + List doGetEntries(LdapName searchBase, String filter, boolean deep); + + List getDirectGroups(LdapName dn); + + Iterable doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly); + + HierarchyUnit doGetHierarchyUnit(LdapName dn); + + LdapEntry newUser(LdapName name, Attributes attrs); + + LdapEntry newGroup(LdapName name, Attributes attrs); + + void init(); + + void destroy(); +} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapEntry.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapEntry.java index c145a6f0a..3fa23e5f1 100644 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapEntry.java +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapEntry.java @@ -1,5 +1,7 @@ package org.argeo.util.directory.ldap; +import java.util.List; + import javax.naming.directory.Attributes; import javax.naming.ldap.LdapName; @@ -10,4 +12,5 @@ public interface LdapEntry { void publishAttributes(Attributes modifiedAttributes); + public List getReferences(String attributeId); } diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapHierarchyUnit.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapHierarchyUnit.java index d76c449b0..5cfca3192 100644 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapHierarchyUnit.java +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapHierarchyUnit.java @@ -1,32 +1,17 @@ package org.argeo.util.directory.ldap; -import java.util.Objects; - import javax.naming.directory.Attributes; import javax.naming.ldap.LdapName; import javax.naming.ldap.Rdn; -import org.argeo.util.directory.Directory; import org.argeo.util.directory.HierarchyUnit; /** LDIF/LDAP based implementation of {@link HierarchyUnit}. */ -public class LdapHierarchyUnit implements HierarchyUnit { - private final AbstractLdapDirectory directory; - - private final LdapName dn; +public class LdapHierarchyUnit extends AbstractLdapEntry implements HierarchyUnit { private final boolean functional; - private final Attributes attributes; - -// HierarchyUnit parent; -// List children = new ArrayList<>(); public LdapHierarchyUnit(AbstractLdapDirectory directory, LdapName dn, Attributes attributes) { - Objects.requireNonNull(directory); - Objects.requireNonNull(dn); - - this.directory = directory; - this.dn = dn; - this.attributes = attributes; + super(directory, dn, attributes); Rdn rdn = LdapNameUtils.getLastRdn(dn); functional = !(directory.getUserBaseRdn().equals(rdn) || directory.getGroupBaseRdn().equals(rdn) @@ -35,21 +20,12 @@ public class LdapHierarchyUnit implements HierarchyUnit { @Override public HierarchyUnit getParent() { - return directory.doGetHierarchyUnit(LdapNameUtils.getParent(dn)); + return getDirectoryDao().doGetHierarchyUnit(LdapNameUtils.getParent(getDn())); } @Override public Iterable getDirectHierachyUnits(boolean functionalOnly) { -// List res = new ArrayList<>(); -// if (functionalOnly) -// for (HierarchyUnit hu : children) { -// if (hu.isFunctional()) -// res.add(hu); -// } -// else -// res.addAll(children); -// return Collections.unmodifiableList(res); - return directory.doGetDirectHierarchyUnits(dn, functionalOnly); + return getDirectoryDao().doGetDirectHierarchyUnits(getDn(), functionalOnly); } @Override @@ -59,40 +35,19 @@ public class LdapHierarchyUnit implements HierarchyUnit { @Override public String getHierarchyUnitName() { - String name = LdapNameUtils.getLastRdnValue(dn); + String name = LdapNameUtils.getLastRdnValue(getDn()); // TODO check ou, o, etc. return name; } - public Attributes getAttributes() { - return attributes; - } - @Override public String getContext() { - return dn.toString(); - } - - @Override - public Directory getDirectory() { - return directory; - } - - @Override - public int hashCode() { - return dn.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof LdapHierarchyUnit)) - return false; - return ((LdapHierarchyUnit) obj).dn.equals(dn); + return getDn().toString(); } @Override public String toString() { - return "Hierarchy Unit " + dn.toString(); + return "Hierarchy Unit " + getDn().toString(); } } diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdifDao.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdifDao.java new file mode 100644 index 000000000..c805d1272 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/LdifDao.java @@ -0,0 +1,408 @@ +package org.argeo.util.directory.ldap; + +import static org.argeo.util.naming.LdapAttrs.objectClass; +import static org.argeo.util.naming.LdapObjs.inetOrgPerson; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Dictionary; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.List; +import java.util.NavigableMap; +import java.util.Objects; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; + +import javax.naming.NameNotFoundException; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attributes; +import javax.naming.ldap.LdapName; + +import org.argeo.util.directory.DirectoryConf; +import org.argeo.util.directory.HierarchyUnit; +import org.argeo.util.naming.LdapObjs; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.service.useradmin.Role; + +/** A user admin based on a LDIF files. */ +public class LdifDao extends AbstractLdapDirectoryDao { +// private NavigableMap users = new TreeMap<>(); +// private NavigableMap groups = new TreeMap<>(); + private NavigableMap entries = new TreeMap<>(); + + private NavigableMap hierarchy = new TreeMap<>(); +// private List rootHierarchyUnits = new ArrayList<>(); + +// public LdifUserAdmin(String uri, String baseDn) { +// this(fromUri(uri, baseDn), false); +// } + + public LdifDao(AbstractLdapDirectory directory) { + super(directory); + } + +// protected LdifUserAdmin(Hashtable properties, boolean scoped) { +// super( properties, scoped); +// } + +// public LdifUserAdmin(URI uri, Dictionary properties) { +// super(uri, properties, false); +// } + +// @Override +// protected AbstractUserDirectory scope(User user) { +// Dictionary credentials = user.getCredentials(); +// String username = (String) credentials.get(SHARED_STATE_USERNAME); +// if (username == null) +// username = user.getName(); +// Object pwdCred = credentials.get(SHARED_STATE_PASSWORD); +// byte[] pwd = (byte[]) pwdCred; +// if (pwd != null) { +// char[] password = DirectoryDigestUtils.bytesToChars(pwd); +// User directoryUser = (User) getRole(username); +// if (!directoryUser.hasCredential(null, password)) +// throw new IllegalStateException("Invalid credentials"); +// } else { +// throw new IllegalStateException("Password is required"); +// } +// Dictionary properties = cloneProperties(); +// properties.put(DirectoryConf.readOnly.name(), "true"); +// LdifUserAdmin scopedUserAdmin = new LdifUserAdmin(properties, true); +//// scopedUserAdmin.groups = Collections.unmodifiableNavigableMap(groups); +//// scopedUserAdmin.users = Collections.unmodifiableNavigableMap(users); +// scopedUserAdmin.entries = Collections.unmodifiableNavigableMap(entries); +// return scopedUserAdmin; +// } + + private static Dictionary fromUri(String uri, String baseDn) { + Hashtable res = new Hashtable(); + res.put(DirectoryConf.uri.name(), uri); + res.put(DirectoryConf.baseDn.name(), baseDn); + return res; + } + + public void init() { + + try { + URI u = new URI(getDirectory().getUri()); + if (u.getScheme().equals("file")) { + File file = new File(u); + if (!file.exists()) + return; + } + load(u.toURL().openStream()); + } catch (IOException | URISyntaxException e) { + throw new IllegalStateException("Cannot open URL " + getDirectory().getUri(), e); + } + } + + public void save() { + if (getDirectory().getUri() == null) + throw new IllegalStateException("Cannot save LDIF user admin: no URI is set"); + if (getDirectory().isReadOnly()) + throw new IllegalStateException( + "Cannot save LDIF user admin: " + getDirectory().getUri() + " is read-only"); + try (FileOutputStream out = new FileOutputStream(new File(new URI(getDirectory().getUri())))) { + save(out); + } catch (IOException | URISyntaxException e) { + throw new IllegalStateException("Cannot save user admin to " + getDirectory().getUri(), e); + } + } + + public void save(OutputStream out) throws IOException { + try { + LdifWriter ldifWriter = new LdifWriter(out); + for (LdapName name : hierarchy.keySet()) + ldifWriter.writeEntry(name, hierarchy.get(name).getAttributes()); +// for (LdapName name : groups.keySet()) +// ldifWriter.writeEntry(name, groups.get(name).getAttributes()); +// for (LdapName name : users.keySet()) +// ldifWriter.writeEntry(name, users.get(name).getAttributes()); + for (LdapName name : entries.keySet()) + ldifWriter.writeEntry(name, entries.get(name).getAttributes()); + } finally { + out.close(); + } + } + + public void load(InputStream in) { + try { +// users.clear(); +// groups.clear(); + entries.clear(); + hierarchy.clear(); + + LdifParser ldifParser = new LdifParser(); + SortedMap allEntries = ldifParser.read(in); + for (LdapName key : allEntries.keySet()) { + Attributes attributes = allEntries.get(key); + // check for inconsistency + Set lowerCase = new HashSet(); + NamingEnumeration ids = attributes.getIDs(); + while (ids.hasMoreElements()) { + String id = ids.nextElement().toLowerCase(); + if (lowerCase.contains(id)) + throw new IllegalStateException(key + " has duplicate id " + id); + lowerCase.add(id); + } + + // analyse object classes + NamingEnumeration objectClasses = attributes.get(objectClass.name()).getAll(); + // System.out.println(key); + objectClasses: while (objectClasses.hasMore()) { + String objectClass = objectClasses.next().toString(); + // System.out.println(" " + objectClass); + if (objectClass.toLowerCase().equals(inetOrgPerson.name().toLowerCase())) { + entries.put(key, newUser(key, attributes)); + break objectClasses; + } else if (objectClass.toLowerCase().equals(getDirectory().getGroupObjectClass().toLowerCase())) { + entries.put(key, newGroup(key, attributes)); + break objectClasses; +// } else if (objectClass.equalsIgnoreCase(LdapObjs.organization.name())) { +// // we only consider organizations which are not groups +// hierarchy.put(key, new LdifHierarchyUnit(this, key, HierarchyUnit.ORGANIZATION, attributes)); +// break objectClasses; + } else if (objectClass.equalsIgnoreCase(LdapObjs.organizationalUnit.name())) { +// String name = key.getRdn(key.size() - 1).toStrindirectoryDaog(); +// if (getUserBase().equalsIgnoreCase(name) || getGroupBase().equalsIgnoreCase(name)) +// break objectClasses; // skip + // TODO skip if it does not contain groups or users + hierarchy.put(key, new LdapHierarchyUnit(getDirectory(), key, attributes)); + break objectClasses; + } + } + } + + // link hierarchy +// hierachyUnits: for (LdapName dn : hierarchy.keySet()) { +// LdifHierarchyUnit unit = hierarchy.get(dn); +// LdapName parentDn = (LdapName) dn.getPrefix(dn.size() - 1); +// LdifHierarchyUnit parent = hierarchy.get(parentDn); +// if (parent == null) { +// rootHierarchyUnits.add(unit); +// unit.parent = null; +// continue hierachyUnits; +// } +// parent.children.add(unit); +// unit.parent = parent; +// } + } catch (NamingException | IOException e) { + throw new IllegalStateException("Cannot load user admin service from LDIF", e); + } + } + + public void destroy() { +// if (users == null || groups == null) + if (entries == null) + throw new IllegalStateException("User directory " + getDirectory().getBaseDn() + " is already destroyed"); +// users = null; +// groups = null; + entries = null; + } + + /* + * USER ADMIN + */ + + @Override + public LdapEntry daoGetEntry(LdapName key) throws NameNotFoundException { +// if (groups.containsKey(key)) +// return groups.get(key); +// if (users.containsKey(key)) +// return users.get(key); + if (entries.containsKey(key)) + return entries.get(key); + throw new NameNotFoundException(key + " not persisted"); + } + + @Override + public Boolean daoHasEntry(LdapName dn) { + return entries.containsKey(dn);// || groups.containsKey(dn); + } + + @Override + public List doGetEntries(LdapName searchBase, String f, boolean deep) { + Objects.requireNonNull(searchBase); + ArrayList res = new ArrayList<>(); + if (f == null && deep && getDirectory().getBaseDn().equals(searchBase)) { +// res.addAll(users.values()); +// res.addAll(groups.values()); + res.addAll(entries.values()); + } else { +// filterRoles(users, searchBase, f, deep, res); +// filterRoles(groups, searchBase, f, deep, res); + filterRoles(entries, searchBase, f, deep, res); + } + return res; + } + + private void filterRoles(SortedMap map, LdapName searchBase, String f, boolean deep, + List res) { + // FIXME get rid of OSGi references + try { + // TODO reduce map with search base ? + Filter filter = f != null ? FrameworkUtil.createFilter(f) : null; + roles: for (LdapEntry user : map.values()) { + LdapName dn = user.getDn(); + if (dn.startsWith(searchBase)) { + if (!deep && dn.size() != (searchBase.size() + 1)) + continue roles; + if (filter == null) + res.add(user); + else { + if (user instanceof Role) { + if (filter.match(((Role) user).getProperties())) + res.add(user); + } + } + } + } + } catch (InvalidSyntaxException e) { + throw new IllegalArgumentException("Cannot create filter " + f, e); + } + + } + + @Override + public List getDirectGroups(LdapName dn) { + List directGroups = new ArrayList(); + entries: for (LdapName name : entries.keySet()) { + LdapEntry group; + try { + LdapEntry entry = daoGetEntry(name); + if (AbstractLdapDirectory.hasObjectClass(entry.getAttributes(), getDirectory().getGroupObjectClass())) { + group = entry; + } else { + continue entries; + } + } catch (NameNotFoundException e) { + throw new IllegalArgumentException("Group " + dn + " not found", e); + } + if (group.getReferences(getDirectory().getMemberAttributeId()).contains(dn)) { + directGroups.add(group.getDn()); + } + } + return directGroups; + } + + @Override + public void prepare(LdapEntryWorkingCopy wc) { + // delete + for (LdapName dn : wc.getDeletedData().keySet()) { + if (entries.containsKey(dn)) + entries.remove(dn); +// if (users.containsKey(dn)) +// users.remove(dn); +// else if (groups.containsKey(dn)) +// groups.remove(dn); + else + throw new IllegalStateException("User to delete not found " + dn); + } + // add + for (LdapName dn : wc.getNewData().keySet()) { + LdapEntry user = (LdapEntry) wc.getNewData().get(dn); +// if (users.containsKey(dn) || groups.containsKey(dn)) + if (entries.containsKey(dn)) + throw new IllegalStateException("User to create found " + dn); + entries.put(dn, user); +// else if (Role.USER == user.getType()) +// users.put(dn, user); +// else if (Role.GROUP == user.getType()) +// groups.put(dn, (DirectoryGroup) user); +// else +// throw new IllegalStateException("Unsupported role type " + user.getType() + " for new user " + dn); + } + // modify + for (LdapName dn : wc.getModifiedData().keySet()) { + Attributes modifiedAttrs = wc.getModifiedData().get(dn); + LdapEntry user; + try { + user = daoGetEntry(dn); + } catch (NameNotFoundException e) { + throw new IllegalStateException("User to modify no found " + dn, e); + } + if (user == null) + throw new IllegalStateException("User to modify no found " + dn); + user.publishAttributes(modifiedAttrs); + } + } + + @Override + public void commit(LdapEntryWorkingCopy wc) { + save(); + } + + @Override + public void rollback(LdapEntryWorkingCopy wc) { + init(); + } + + /* + * HIERARCHY + */ + +// @Override +// public int getHierarchyChildCount() { +// return rootHierarchyUnits.size(); +// } +// +// @Override +// public HierarchyUnit getHierarchyChild(int i) { +// return rootHierarchyUnits.get(i); +// } + @Override + public HierarchyUnit doGetHierarchyUnit(LdapName dn) { + return hierarchy.get(dn); + } + + @Override + public Iterable doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) { + List res = new ArrayList<>(); + for (LdapName n : hierarchy.keySet()) { + if (n.size() == searchBase.size() + 1) { + if (n.startsWith(searchBase)) { + HierarchyUnit hu = hierarchy.get(n); + if (functionalOnly) { + if (hu.isFunctional()) + res.add(hu); + } else { + res.add(hu); + } + } + } + } + return res; + } + + public void scope(LdifDao scoped) { + scoped.entries = Collections.unmodifiableNavigableMap(entries); + } + +// @Override +// public Iterable getDirectHierarchyUnits(boolean functionalOnly) { +// if (functionalOnly) { +// List res = new ArrayList<>(); +// for (HierarchyUnit hu : rootHierarchyUnits) { +// if (hu.isFunctional()) +// res.add(hu); +// } +// return res; +// +// } else { +// return rootHierarchyUnits; +// } +// } + +}