From 4012bedd8870634488b307d2233590fb1226e5d4 Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Wed, 17 Aug 2022 14:08:58 +0200 Subject: [PATCH] Improve Directory framework. --- .../src/org/argeo/cms/CmsUserManager.java | 49 ++++++------ .../cms/internal/auth/CmsUserManagerImpl.java | 80 +++++++++++++------ .../src/org/argeo/util/LangUtils.java | 14 ++++ .../org/argeo/util/directory/Directory.java | 11 ++- .../directory/ldap/AbstractLdapDirectory.java | 38 +++++---- .../util/directory/ldap/DefaultLdapEntry.java | 54 ++++++++----- .../argeo/util/directory/ldap/LdapDao.java | 2 + 7 files changed, 163 insertions(+), 85 deletions(-) diff --git a/org.argeo.cms/src/org/argeo/cms/CmsUserManager.java b/org.argeo.cms/src/org/argeo/cms/CmsUserManager.java index 9af578c96..040138878 100644 --- a/org.argeo.cms/src/org/argeo/cms/CmsUserManager.java +++ b/org.argeo.cms/src/org/argeo/cms/CmsUserManager.java @@ -8,6 +8,7 @@ import java.util.Set; import javax.security.auth.Subject; import org.argeo.osgi.useradmin.UserDirectory; +import org.argeo.util.directory.HierarchyUnit; import org.osgi.framework.InvalidSyntaxException; import org.osgi.service.useradmin.Role; import org.osgi.service.useradmin.User; @@ -17,52 +18,59 @@ import org.osgi.service.useradmin.User; * the userAdmin. */ public interface CmsUserManager { - public Map getKnownBaseDns(boolean onlyWritable); - public Set getUserDirectories(); - + Map getKnownBaseDns(boolean onlyWritable); + + Set getUserDirectories(); + // CurrentUser /** Returns the e-mail of the current logged in user */ - public String getMyMail(); + String getMyMail(); // Other users /** Returns a {@link User} given a username */ - public User getUser(String username); + User getUser(String username); /** Can be a group or a user */ - public String getUserDisplayName(String dn); + String getUserDisplayName(String dn); /** Can be a group or a user */ - public String getUserMail(String dn); + String getUserMail(String dn); /** Lists all roles of the given user */ - public String[] getUserRoles(String dn); + String[] getUserRoles(String dn); /** Checks if the passed user belongs to the passed role */ - public boolean isUserInRole(String userDn, String roleDn); + boolean isUserInRole(String userDn, String roleDn); // Search /** Returns a filtered list of roles */ - public Role[] getRoles(String filter) throws InvalidSyntaxException; + Role[] getRoles(String filter) throws InvalidSyntaxException; /** Recursively lists users in a given group. */ - public Set listUsersInGroup(String groupDn, String filter); + Set listUsersInGroup(String groupDn, String filter); /** Search among groups including system roles and users if needed */ - public List listGroups(String filter, boolean includeUsers, boolean includeSystemRoles); + List listGroups(String filter, boolean includeUsers, boolean includeSystemRoles); + + /* + * EDITION + */ + /** Creates a new user.*/ + User createUser(String username, Map properties, Map credentials); /* MISCELLANEOUS */ /** Returns the dn of a role given its local ID */ - public String buildDefaultDN(String localId, int type); + String buildDefaultDN(String localId, int type); /** Exposes the main default domain name for this instance */ - public String getDefaultDomainName(); + String getDefaultDomainName(); /** * Search for a {@link User} (might also be a group) whose uid or cn is equals * to localId within the various user repositories defined in the current * context. */ - public User getUserFromLocalId(String localId); + User getUserFromLocalId(String localId); void changeOwnPassword(char[] oldPassword, char[] newPassword); @@ -80,14 +88,9 @@ public interface CmsUserManager { void expireAuthToken(String token); void expireAuthTokens(Subject subject); - - UserDirectory getDirectory(Role role); -// User createUserFromPerson(Node person); + UserDirectory getDirectory(Role role); -// @Deprecated -// public UserAdmin getUserAdmin(); -// -// @Deprecated -// public UserTransaction getUserTransaction(); + /** Create a new hierarchy unit. Does nothing if it already exists. */ + HierarchyUnit createHierarchyUnit(UserDirectory directory, String path); } \ No newline at end of file diff --git a/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsUserManagerImpl.java b/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsUserManagerImpl.java index dfb0a56a2..36ae6e6d6 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsUserManagerImpl.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsUserManagerImpl.java @@ -32,6 +32,7 @@ import org.argeo.osgi.useradmin.AggregatingUserAdmin; import org.argeo.osgi.useradmin.TokenUtils; import org.argeo.osgi.useradmin.UserDirectory; import org.argeo.util.directory.DirectoryConf; +import org.argeo.util.directory.HierarchyUnit; import org.argeo.util.directory.ldap.SharedSecret; import org.argeo.util.naming.LdapAttrs; import org.argeo.util.naming.NamingUtils; @@ -62,6 +63,9 @@ public class CmsUserManagerImpl implements CmsUserManager { // private Map serviceProperties; private WorkTransaction userTransaction; + private final String[] knownProps = { LdapAttrs.cn.name(), LdapAttrs.sn.name(), LdapAttrs.givenName.name(), + LdapAttrs.uid.name() }; + // private Map> userDirectories = Collections // .synchronizedMap(new LinkedHashMap<>()); @@ -124,9 +128,6 @@ public class CmsUserManagerImpl implements CmsUserManager { return false; } - private final String[] knownProps = { LdapAttrs.cn.name(), LdapAttrs.sn.name(), LdapAttrs.givenName.name(), - LdapAttrs.uid.name() }; - public Set listUsersInGroup(String groupDn, String filter) { Group group = (Group) userAdmin.getRole(groupDn); if (group == null) @@ -205,6 +206,37 @@ public class CmsUserManagerImpl implements CmsUserManager { return buildDistinguishedName(localId, getDefaultDomainName(), type); } + /* + * EDITION + */ + @Override + public User createUser(String username, Map properties, Map credentials) { + try { + userTransaction.begin(); + User user = (User) userAdmin.createRole(username, Role.USER); + if (properties != null) { + for (String key : properties.keySet()) + user.getProperties().put(key, properties.get(key)); + } + if (credentials != null) { + for (String key : credentials.keySet()) + user.getCredentials().put(key, credentials.get(key)); + } + userTransaction.commit(); + return user; + } catch (Exception e) { + try { + userTransaction.rollback(); + } catch (Exception e1) { + log.error("Could not roll back", e1); + } + if (e instanceof RuntimeException) + throw (RuntimeException) e; + else + throw new RuntimeException("Cannot create user " + username, e); + } + } + @Override public String getDefaultDomainName() { Map dns = getKnownBaseDns(true); @@ -215,27 +247,6 @@ public class CmsUserManagerImpl implements CmsUserManager { + dns.keySet().toString() + ". Unable to chose a default one."); } -// public Map getKnownBaseDns(boolean onlyWritable) { -// Map dns = new HashMap(); -// String[] propertyKeys = serviceProperties.keySet().toArray(new String[serviceProperties.size()]); -// for (String uri : propertyKeys) { -// if (!uri.startsWith("/")) -// continue; -// Dictionary props = UserAdminConf.uriAsProperties(uri); -// String readOnly = UserAdminConf.readOnly.getValue(props); -// String baseDn = UserAdminConf.baseDn.getValue(props); -// -// if (onlyWritable && "true".equals(readOnly)) -// continue; -// if (baseDn.equalsIgnoreCase(NodeConstants.ROLES_BASEDN)) -// continue; -// if (baseDn.equalsIgnoreCase(NodeConstants.TOKENS_BASEDN)) -// continue; -// dns.put(baseDn, uri); -// } -// return dns; -// } - public Map getKnownBaseDns(boolean onlyWritable) { Map dns = new HashMap(); for (UserDirectory userDirectory : userDirectories) { @@ -462,6 +473,27 @@ public class CmsUserManagerImpl implements CmsUserManager { return possible.lastEntry().getValue(); } + public HierarchyUnit createHierarchyUnit(UserDirectory directory, String path) { + HierarchyUnit hi = directory.getHierarchyUnit(path); + if (hi != null) + return hi; + try { + userTransaction.begin(); + HierarchyUnit hierarchyUnit = directory.createHierarchyUnit(path); + userTransaction.commit(); + return hierarchyUnit; + } catch (Exception e1) { + try { + if (!userTransaction.isNoTransactionStatus()) + userTransaction.rollback(); + } catch (Exception e2) { + if (log.isTraceEnabled()) + log.trace("Cannot rollback transaction", e2); + } + throw new RuntimeException("Cannot create hierarchy unit " + path + " in directory " + directory, e1); + } + } + // public User createUserFromPerson(Node person) { // String email = JcrUtils.get(person, LdapAttrs.mail.property()); // String dn = buildDefaultDN(email, Role.USER); diff --git a/org.argeo.util/src/org/argeo/util/LangUtils.java b/org.argeo.util/src/org/argeo/util/LangUtils.java index 1177d4f53..1aee28c03 100644 --- a/org.argeo.util/src/org/argeo/util/LangUtils.java +++ b/org.argeo.util/src/org/argeo/util/LangUtils.java @@ -94,6 +94,20 @@ public class LangUtils { return res; } + /** Converts a {@link Dictionary} to a {@link Map}. */ + public static Map dictToMap(Dictionary properties) { + if (properties == null) { + return null; + } + Map res = new HashMap<>(properties.size()); + Enumeration keys = properties.keys(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + res.put(key, properties.get(key)); + } + return res; + } + /** * Get a string property from this map, expecting to find it, or * null if not found. 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 351a608bc..11e8e6285 100644 --- a/org.argeo.util/src/org/argeo/util/directory/Directory.java +++ b/org.argeo.util/src/org/argeo/util/directory/Directory.java @@ -5,6 +5,7 @@ import java.util.Optional; import org.argeo.util.transaction.WorkControl; +/** An information directory (typicylly LDAP). */ public interface Directory extends HierarchyUnit { /** * The base of the hierarchy defined by this directory. This could typically be @@ -14,25 +15,33 @@ public interface Directory extends HierarchyUnit { String getName(); + /** Whether this directory is read only. */ boolean isReadOnly(); + /** Whether this directory is disabled. */ boolean isDisabled(); + /** The realm (typically Kerberos) of this directory. */ Optional getRealm(); + /** Sets the transaction control used by this directory when editing. */ void setTransactionControl(WorkControl transactionControl); /* * METADATA */ + /** Metadata of this directory. */ public Dictionary getProperties(); /* * HIERARCHY */ - + /** The first level of hierarchy units. */ Iterable getDirectHierarchyUnits(boolean functionalOnly); + /** The hierarchy unit at this path. */ HierarchyUnit getHierarchyUnit(String path); + /** Create a new hierarchy unit. */ + HierarchyUnit createHierarchyUnit(String path); } 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 36047d53e..27c2b9531 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 @@ -34,6 +34,7 @@ import org.argeo.util.transaction.WorkControl; import org.argeo.util.transaction.WorkingCopyXaResource; import org.argeo.util.transaction.XAResourceProvider; +/** A {@link Directory} based either on LDAP or LDIF. */ 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"; @@ -146,23 +147,7 @@ public abstract class AbstractLdapDirectory implements Directory, XAResourceProv } /* - * ABSTRACT METHODS - */ - -// 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 + * INITIALISATION */ public void init() { @@ -327,6 +312,25 @@ public abstract class AbstractLdapDirectory implements Directory, XAResourceProv return this; } + @Override + public HierarchyUnit createHierarchyUnit(String path) { + checkEdit(); + LdapEntryWorkingCopy wc = getWorkingCopy(); + LdapName dn = pathToName(path); + if ((getDirectoryDao().entryExists(dn) && !wc.getDeletedData().containsKey(dn)) + || wc.getNewData().containsKey(dn)) + throw new IllegalArgumentException("Already a hierarchy unit " + path); + BasicAttributes attrs = new BasicAttributes(true); + attrs.put(LdapAttrs.objectClass.name(), LdapObjs.organizationalUnit.name()); + Rdn nameRdn = dn.getRdn(dn.size() - 1); + // TODO deal with multiple attr RDN + attrs.put(nameRdn.getType(), nameRdn.getValue()); + wc.getModifiedData().put(dn, attrs); + LdapHierarchyUnit newHierarchyUnit = new LdapHierarchyUnit(this, dn, attrs); + wc.getNewData().put(dn, newHierarchyUnit); + return newHierarchyUnit; + } + /* * PATHS */ diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/DefaultLdapEntry.java b/org.argeo.util/src/org/argeo/util/directory/ldap/DefaultLdapEntry.java index 7166710f6..14a7eb87c 100644 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/DefaultLdapEntry.java +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/DefaultLdapEntry.java @@ -119,7 +119,7 @@ public class DefaultLdapEntry implements LdapEntry { public Dictionary getCredentials() { if (credentials == null) - credentials = new AttributeDictionary(false); + credentials = new AttributeDictionary(true); return credentials; } @@ -374,29 +374,43 @@ public class DefaultLdapEntry implements LdapEntry { @Override public Object put(String key, Object value) { - if (key == null) { - // TODO persist to other sources (like PKCS12) - char[] password = DirectoryDigestUtils.bytesToChars(value); - byte[] hashedPassword = sha1hash(password); - return put(LdapAttrs.userPassword.name(), hashedPassword); - } - if (key.startsWith("X-")) { - return put(LdapAttrs.authPassword.name(), value); - } + try { + if (key == null) { + // TODO persist to other sources (like PKCS12) + char[] password = DirectoryDigestUtils.bytesToChars(value); + byte[] hashedPassword = sha1hash(password); + return put(LdapAttrs.userPassword.name(), hashedPassword); + } + if (key.startsWith("X-")) { + return put(LdapAttrs.authPassword.name(), value); + } + if (key.equals(LdapAttrs.objectClasses.name())) { + Attribute attribute = new BasicAttribute(LdapAttrs.objectClass.name()); + String[] objectClasses = value.toString().split("\n"); + for (String objectClass : objectClasses) { + if (objectClass.trim().equals("")) + continue; + attribute.add(objectClass); + } + Attribute previousAttribute = getModifiedAttributes().put(attribute); + if (previousAttribute != null) + return previousAttribute.get(); + else + return null; + } - getDirectory().checkEdit(); - if (!isEditing()) - startEditing(); + getDirectory().checkEdit(); + if (!isEditing()) + startEditing(); - if (!(value instanceof String || value instanceof byte[])) - throw new IllegalArgumentException("Value must be String or byte[]"); + if (!(value instanceof String || value instanceof byte[])) + throw new IllegalArgumentException("Value must be String or byte[]"); - if (includeFilter && !attrFilter.contains(key)) - throw new IllegalArgumentException("Key " + key + " not included"); - else if (!includeFilter && attrFilter.contains(key)) - throw new IllegalArgumentException("Key " + key + " excluded"); + if (includeFilter && !attrFilter.contains(key)) + throw new IllegalArgumentException("Key " + key + " not included"); + else if (!includeFilter && attrFilter.contains(key)) + throw new IllegalArgumentException("Key " + key + " excluded"); - try { Attribute attribute = getModifiedAttributes().get(key.toString()); // if (attribute == null) // block unit tests attribute = new BasicAttribute(key.toString()); 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 index fac7dd1ac..b1c0c9849 100644 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapDao.java +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapDao.java @@ -278,6 +278,8 @@ public class LdapDao extends AbstractLdapDirectoryDao { throw new IllegalArgumentException(dn + " does not start with base DN " + getDirectory().getBaseDn()); Attributes attrs = ldapConnection.getAttributes(dn); return new LdapHierarchyUnit(getDirectory(), dn, attrs); + } catch (NameNotFoundException e) { + return null; } catch (NamingException e) { throw new IllegalStateException("Cannot get hierarchy unit " + dn, e); } -- 2.30.2