From: Mathieu Baudier Date: Wed, 22 Jun 2022 08:05:00 +0000 (+0200) Subject: Decorrelate directory implementation from user admin X-Git-Tag: v2.3.10~172 X-Git-Url: https://git.argeo.org/?a=commitdiff_plain;h=e2ffdf6872592aa22d0de2b0ec69ee4eca698c45;p=lgpl%2Fargeo-commons.git Decorrelate directory implementation from user admin --- diff --git a/eclipse/org.argeo.cms.e4/src/org/argeo/cms/e4/users/UserAdminWrapper.java b/eclipse/org.argeo.cms.e4/src/org/argeo/cms/e4/users/UserAdminWrapper.java index cd8d4d7dc..1e0fc6eb3 100644 --- a/eclipse/org.argeo.cms.e4/src/org/argeo/cms/e4/users/UserAdminWrapper.java +++ b/eclipse/org.argeo.cms.e4/src/org/argeo/cms/e4/users/UserAdminWrapper.java @@ -10,8 +10,8 @@ import java.util.Map; import org.argeo.api.cms.CmsConstants; import org.argeo.cms.CmsException; -import org.argeo.osgi.useradmin.UserAdminConf; import org.argeo.osgi.useradmin.UserDirectory; +import org.argeo.util.directory.DirectoryConf; import org.argeo.util.transaction.WorkTransaction; import org.osgi.service.useradmin.UserAdmin; import org.osgi.service.useradmin.UserAdminEvent; @@ -99,7 +99,7 @@ public class UserAdminWrapper { continue; if (baseDn.equalsIgnoreCase(CmsConstants.TOKENS_BASEDN)) continue; - dns.put(baseDn, UserAdminConf.propertiesAsUri(userDirectories.get(userDirectory)).toString()); + dns.put(baseDn, DirectoryConf.propertiesAsUri(userDirectories.get(userDirectory)).toString()); } // for (String uri : uris) { diff --git a/eclipse/org.argeo.cms.e4/src/org/argeo/cms/e4/users/handlers/NewGroup.java b/eclipse/org.argeo.cms.e4/src/org/argeo/cms/e4/users/handlers/NewGroup.java index d2ffa791b..f02a26b1c 100644 --- a/eclipse/org.argeo.cms.e4/src/org/argeo/cms/e4/users/handlers/NewGroup.java +++ b/eclipse/org.argeo.cms.e4/src/org/argeo/cms/e4/users/handlers/NewGroup.java @@ -9,7 +9,7 @@ import org.argeo.cms.CmsException; import org.argeo.cms.e4.users.UserAdminWrapper; import org.argeo.eclipse.ui.EclipseUiUtils; import org.argeo.eclipse.ui.dialogs.ErrorFeedback; -import org.argeo.osgi.useradmin.UserAdminConf; +import org.argeo.util.directory.DirectoryConf; import org.argeo.util.naming.LdapAttrs; import org.eclipse.e4.core.di.annotations.Execute; import org.eclipse.jface.wizard.Wizard; @@ -179,8 +179,8 @@ public class NewGroup { Map dns = getDns(); String bdn = baseDnCmb.getText(); if (EclipseUiUtils.notEmpty(bdn)) { - Dictionary props = UserAdminConf.uriAsProperties(dns.get(bdn)); - String dn = LdapAttrs.cn.name() + "=" + cn + "," + UserAdminConf.groupBase.getValue(props) + "," + bdn; + Dictionary props = DirectoryConf.uriAsProperties(dns.get(bdn)); + String dn = LdapAttrs.cn.name() + "=" + cn + "," + DirectoryConf.groupBase.getValue(props) + "," + bdn; return dn; } return null; diff --git a/eclipse/org.argeo.cms.e4/src/org/argeo/cms/e4/users/handlers/NewUser.java b/eclipse/org.argeo.cms.e4/src/org/argeo/cms/e4/users/handlers/NewUser.java index 07d82c749..24f7e6250 100644 --- a/eclipse/org.argeo.cms.e4/src/org/argeo/cms/e4/users/handlers/NewUser.java +++ b/eclipse/org.argeo.cms.e4/src/org/argeo/cms/e4/users/handlers/NewUser.java @@ -15,7 +15,7 @@ import org.argeo.cms.e4.users.UiAdminUtils; import org.argeo.cms.e4.users.UserAdminWrapper; import org.argeo.eclipse.ui.EclipseUiUtils; import org.argeo.eclipse.ui.dialogs.ErrorFeedback; -import org.argeo.osgi.useradmin.UserAdminConf; +import org.argeo.util.directory.DirectoryConf; import org.argeo.util.naming.LdapAttrs; import org.eclipse.e4.core.di.annotations.Execute; import org.eclipse.jface.wizard.Wizard; @@ -241,8 +241,8 @@ public class NewUser { Map dns = getDns(); String bdn = baseDnCmb.getText(); if (EclipseUiUtils.notEmpty(bdn)) { - Dictionary props = UserAdminConf.uriAsProperties(dns.get(bdn)); - String dn = LdapAttrs.uid.name() + "=" + uid + "," + UserAdminConf.userBase.getValue(props) + "," + bdn; + Dictionary props = DirectoryConf.uriAsProperties(dns.get(bdn)); + String dn = LdapAttrs.uid.name() + "=" + uid + "," + DirectoryConf.userBase.getValue(props) + "," + bdn; return dn; } return null; diff --git a/org.argeo.cms/OSGI-INF/cmsUserManager.xml b/org.argeo.cms/OSGI-INF/cmsUserManager.xml index 524c054ec..2e8f868ae 100644 --- a/org.argeo.cms/OSGI-INF/cmsUserManager.xml +++ b/org.argeo.cms/OSGI-INF/cmsUserManager.xml @@ -5,6 +5,6 @@ - + diff --git a/org.argeo.cms/OSGI-INF/nodeUserAdmin.xml b/org.argeo.cms/OSGI-INF/nodeUserAdmin.xml index eb048d9f5..cae688b0a 100644 --- a/org.argeo.cms/OSGI-INF/nodeUserAdmin.xml +++ b/org.argeo.cms/OSGI-INF/nodeUserAdmin.xml @@ -5,7 +5,7 @@ - - + + diff --git a/org.argeo.cms/OSGI-INF/simpleTransactionManager.xml b/org.argeo.cms/OSGI-INF/simpleTransactionManager.xml index c331aa430..81997476e 100644 --- a/org.argeo.cms/OSGI-INF/simpleTransactionManager.xml +++ b/org.argeo.cms/OSGI-INF/simpleTransactionManager.xml @@ -1,8 +1,8 @@ - + - - + + diff --git a/org.argeo.cms/bnd.bnd b/org.argeo.cms/bnd.bnd index 0d85a873f..52987f3da 100644 --- a/org.argeo.cms/bnd.bnd +++ b/org.argeo.cms/bnd.bnd @@ -1,7 +1,6 @@ Bundle-Activator: org.argeo.cms.internal.osgi.CmsActivator Import-Package: \ -org.argeo.osgi.transaction, \ org.apache.commons.httpclient.cookie;resolution:=optional,\ !com.sun.security.jgss,\ org.osgi.*;version=0.0.0,\ diff --git a/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContent.java b/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContent.java index eef7055df..e1ad96077 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContent.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContent.java @@ -11,14 +11,14 @@ import org.argeo.api.acr.ContentName; import org.argeo.api.acr.spi.ContentProvider; import org.argeo.api.acr.spi.ProvidedSession; import org.argeo.cms.acr.AbstractContent; -import org.argeo.osgi.useradmin.HierarchyUnit; -import org.argeo.osgi.useradmin.UserDirectory; +import org.argeo.util.directory.Directory; +import org.argeo.util.directory.HierarchyUnit; class DirectoryContent extends AbstractContent { - private UserDirectory directory; + private Directory directory; private DirectoryContentProvider provider; - public DirectoryContent(ProvidedSession session, DirectoryContentProvider provider, UserDirectory directory) { + public DirectoryContent(ProvidedSession session, DirectoryContentProvider provider, Directory directory) { super(session); this.provider = provider; this.directory = directory; diff --git a/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContentProvider.java b/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContentProvider.java index bd4117ead..f4afbdd53 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContentProvider.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContentProvider.java @@ -17,8 +17,8 @@ import org.argeo.api.acr.spi.ProvidedSession; import org.argeo.cms.CmsUserManager; import org.argeo.cms.acr.AbstractContent; import org.argeo.cms.acr.ContentUtils; -import org.argeo.osgi.useradmin.HierarchyUnit; import org.argeo.osgi.useradmin.UserDirectory; +import org.argeo.util.directory.HierarchyUnit; import org.osgi.service.useradmin.User; public class DirectoryContentProvider implements ContentProvider { diff --git a/org.argeo.cms/src/org/argeo/cms/acr/directory/HierarchyUnitContent.java b/org.argeo.cms/src/org/argeo/cms/acr/directory/HierarchyUnitContent.java index 78bc72f5d..f6a0e3b52 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/directory/HierarchyUnitContent.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/directory/HierarchyUnitContent.java @@ -13,7 +13,8 @@ import org.argeo.api.acr.CrName; import org.argeo.api.acr.spi.ContentProvider; import org.argeo.api.acr.spi.ProvidedSession; import org.argeo.cms.acr.AbstractContent; -import org.argeo.osgi.useradmin.HierarchyUnit; +import org.argeo.osgi.useradmin.UserDirectory; +import org.argeo.util.directory.HierarchyUnit; import org.osgi.service.useradmin.Role; class HierarchyUnitContent extends AbstractContent { @@ -59,7 +60,8 @@ class HierarchyUnitContent extends AbstractContent { for (HierarchyUnit hu : hierarchyUnit.getDirectHierachyUnits(false)) lst.add(new HierarchyUnitContent(getSession(), provider, hu)); - for (Role role : hierarchyUnit.getHierarchyUnitRoles(null, false)) + for (Role role : ((UserDirectory) hierarchyUnit.getDirectory()).getHierarchyUnitRoles(hierarchyUnit, null, + false)) lst.add(new RoleContent(getSession(), provider, this, role)); return lst.iterator(); } @@ -82,5 +84,4 @@ class HierarchyUnitContent extends AbstractContent { return hierarchyUnit; } - } diff --git a/org.argeo.cms/src/org/argeo/cms/acr/directory/RoleContent.java b/org.argeo.cms/src/org/argeo/cms/acr/directory/RoleContent.java index bf3b319f4..2a22f023c 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/directory/RoleContent.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/directory/RoleContent.java @@ -17,6 +17,7 @@ import org.argeo.api.acr.NamespaceUtils; import org.argeo.api.acr.spi.ContentProvider; import org.argeo.api.acr.spi.ProvidedSession; import org.argeo.cms.acr.AbstractContent; +import org.argeo.osgi.useradmin.UserDirectory; import org.argeo.util.naming.LdapAttrs; import org.argeo.util.naming.LdapObjs; import org.osgi.service.useradmin.Role; @@ -43,7 +44,7 @@ class RoleContent extends AbstractContent { @Override public QName getName() { - String name = parent.getHierarchyUnit().getDirectory().getRoleSimpleName(role); + String name = ((UserDirectory) parent.getHierarchyUnit().getDirectory()).getRoleSimpleName(role); return new ContentName(name); } diff --git a/org.argeo.cms/src/org/argeo/cms/auth/SingleUserLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/SingleUserLoginModule.java index 08380ac5a..972d6a245 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/SingleUserLoginModule.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/SingleUserLoginModule.java @@ -14,8 +14,8 @@ import javax.security.auth.spi.LoginModule; import javax.security.auth.x500.X500Principal; import org.argeo.api.cms.CmsLog; -import org.argeo.osgi.useradmin.IpaUtils; import org.argeo.osgi.useradmin.OsUserUtils; +import org.argeo.util.directory.ldap.IpaUtils; import org.argeo.util.naming.LdapAttrs; import org.osgi.service.useradmin.Authorization; diff --git a/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java index ea7d1f370..f6832ad35 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java @@ -30,8 +30,8 @@ import org.argeo.cms.internal.osgi.NodeUserAdmin; import org.argeo.cms.internal.runtime.CmsContextImpl; import org.argeo.cms.security.CryptoKeyring; import org.argeo.osgi.useradmin.AuthenticatingUser; -import org.argeo.osgi.useradmin.IpaUtils; import org.argeo.osgi.useradmin.TokenUtils; +import org.argeo.util.directory.ldap.IpaUtils; import org.argeo.util.naming.LdapAttrs; import org.osgi.framework.BundleContext; import org.osgi.framework.FrameworkUtil; 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 9b7a2ed42..7db3cdc4c 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,8 +32,8 @@ import org.argeo.cms.CmsUserManager; import org.argeo.cms.auth.CurrentUser; import org.argeo.cms.auth.UserAdminUtils; import org.argeo.osgi.useradmin.TokenUtils; -import org.argeo.osgi.useradmin.UserAdminConf; import org.argeo.osgi.useradmin.UserDirectory; +import org.argeo.util.directory.DirectoryConf; import org.argeo.util.naming.LdapAttrs; import org.argeo.util.naming.NamingUtils; import org.argeo.util.naming.SharedSecret; @@ -240,7 +240,7 @@ public class CmsUserManagerImpl implements CmsUserManager { continue; if (baseDn.equalsIgnoreCase(CmsConstants.TOKENS_BASEDN)) continue; - dns.put(baseDn, UserAdminConf.propertiesAsUri(userDirectories.get(userDirectory)).toString()); + dns.put(baseDn, DirectoryConf.propertiesAsUri(userDirectories.get(userDirectory)).toString()); } return dns; @@ -254,12 +254,12 @@ public class CmsUserManagerImpl implements CmsUserManager { public String buildDistinguishedName(String localId, String baseDn, int type) { Map dns = getKnownBaseDns(true); - Dictionary props = UserAdminConf.uriAsProperties(dns.get(baseDn)); + Dictionary props = DirectoryConf.uriAsProperties(dns.get(baseDn)); String dn = null; if (Role.GROUP == type) - dn = LdapAttrs.cn.name() + "=" + localId + "," + UserAdminConf.groupBase.getValue(props) + "," + baseDn; + dn = LdapAttrs.cn.name() + "=" + localId + "," + DirectoryConf.groupBase.getValue(props) + "," + baseDn; else if (Role.USER == type) - dn = LdapAttrs.uid.name() + "=" + localId + "," + UserAdminConf.userBase.getValue(props) + "," + baseDn; + dn = LdapAttrs.uid.name() + "=" + localId + "," + DirectoryConf.userBase.getValue(props) + "," + baseDn; else throw new IllegalStateException("Unknown role type. " + "Cannot deduce dn for " + localId); return dn; diff --git a/org.argeo.cms/src/org/argeo/cms/internal/osgi/CmsOsgiLogger.java b/org.argeo.cms/src/org/argeo/cms/internal/osgi/CmsOsgiLogger.java index 6898c4348..c167af025 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/osgi/CmsOsgiLogger.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/osgi/CmsOsgiLogger.java @@ -28,7 +28,7 @@ import org.argeo.cms.ArgeoLogger; import org.argeo.cms.CmsException; import org.argeo.cms.auth.CurrentUser; import org.argeo.cms.internal.runtime.KernelConstants; -import org.argeo.osgi.useradmin.UserAdminConf; +import org.argeo.util.directory.DirectoryConf; import org.osgi.framework.Bundle; import org.osgi.framework.Constants; import org.osgi.framework.ServiceReference; @@ -218,9 +218,9 @@ public class CmsOsgiLogger implements ArgeoLogger, LogListener { sb.append(" " + KernelConstants.CONTEXT_NAME_PROP + ": " + contextName); // user directories - Object baseDn = sr.getProperty(UserAdminConf.baseDn.name()); + Object baseDn = sr.getProperty(DirectoryConf.baseDn.name()); if (baseDn != null) - sb.append(" " + UserAdminConf.baseDn.name() + ": " + baseDn); + sb.append(" " + DirectoryConf.baseDn.name() + ": " + baseDn); } return sb.toString(); diff --git a/org.argeo.cms/src/org/argeo/cms/internal/osgi/DeployConfig.java b/org.argeo.cms/src/org/argeo/cms/internal/osgi/DeployConfig.java index 71c349327..b8fa8a73f 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/osgi/DeployConfig.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/osgi/DeployConfig.java @@ -26,10 +26,10 @@ import org.argeo.api.cms.CmsLog; import org.argeo.cms.internal.runtime.InitUtils; import org.argeo.cms.internal.runtime.KernelConstants; import org.argeo.cms.internal.runtime.KernelUtils; -import org.argeo.osgi.useradmin.UserAdminConf; -import org.argeo.util.naming.ldap.AttributesDictionary; -import org.argeo.util.naming.ldap.LdifParser; -import org.argeo.util.naming.ldap.LdifWriter; +import org.argeo.util.directory.DirectoryConf; +import org.argeo.util.directory.ldap.AttributesDictionary; +import org.argeo.util.directory.ldap.LdifParser; +import org.argeo.util.directory.ldap.LdifWriter; import org.osgi.framework.InvalidSyntaxException; import org.osgi.service.cm.Configuration; import org.osgi.service.cm.ConfigurationAdmin; @@ -75,12 +75,12 @@ public class DeployConfig { List activeCns = new ArrayList<>(); for (int i = 0; i < userDirectoryConfigs.size(); i++) { Dictionary userDirectoryConfig = userDirectoryConfigs.get(i); - String baseDn = (String) userDirectoryConfig.get(UserAdminConf.baseDn.name()); + String baseDn = (String) userDirectoryConfig.get(DirectoryConf.baseDn.name()); String cn; if (CmsConstants.ROLES_BASEDN.equals(baseDn)) cn = ROLES; else - cn = UserAdminConf.baseDnHash(userDirectoryConfig); + cn = DirectoryConf.baseDnHash(userDirectoryConfig); activeCns.add(cn); userDirectoryConfig.put(CmsConstants.CN, cn); putFactoryDeployConfig(CmsConstants.NODE_USER_ADMIN_PID, userDirectoryConfig); @@ -93,7 +93,7 @@ public class DeployConfig { Attributes attrs = deployConfigs.get(name); String cn = name.getRdn(name.size() - 1).getValue().toString(); if (!activeCns.contains(cn)) { - attrs.put(UserAdminConf.disabled.name(), "true"); + attrs.put(DirectoryConf.disabled.name(), "true"); } // } catch (Exception e) { // throw new CmsException("Cannot disable user directory " + name, e); @@ -206,7 +206,7 @@ public class DeployConfig { // service factory definition } } else { - Attribute disabled = deployConfig.get(UserAdminConf.disabled.name()); + Attribute disabled = deployConfig.get(DirectoryConf.disabled.name()); if (disabled != null) continue deployConfigs; // service factory service @@ -378,7 +378,7 @@ public class DeployConfig { boolean hasDomain = false; for (Configuration config : configs) { - Object realm = config.getProperties().get(UserAdminConf.realm.name()); + Object realm = config.getProperties().get(DirectoryConf.realm.name()); if (realm != null) { log.debug("Found realm: " + realm); hasDomain = true; diff --git a/org.argeo.cms/src/org/argeo/cms/internal/osgi/NodeUserAdmin.java b/org.argeo.cms/src/org/argeo/cms/internal/osgi/NodeUserAdmin.java index 0746d4301..e534d9fe3 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/osgi/NodeUserAdmin.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/osgi/NodeUserAdmin.java @@ -9,8 +9,8 @@ import org.argeo.api.cms.CmsConstants; import org.argeo.api.cms.CmsLog; import org.argeo.cms.internal.runtime.CmsUserAdmin; import org.argeo.cms.internal.runtime.KernelConstants; -import org.argeo.osgi.useradmin.UserAdminConf; import org.argeo.osgi.useradmin.UserDirectory; +import org.argeo.util.directory.DirectoryConf; import org.osgi.framework.Constants; import org.osgi.service.cm.ConfigurationException; import org.osgi.service.cm.ManagedServiceFactory; @@ -29,7 +29,7 @@ public class NodeUserAdmin extends CmsUserAdmin implements ManagedServiceFactory @Override public void updated(String pid, Dictionary properties) throws ConfigurationException { - String basePath = (String) properties.get(UserAdminConf.baseDn.name()); + String basePath = (String) properties.get(DirectoryConf.baseDn.name()); // FIXME make updates more robust if (pidToBaseDn.containsValue(basePath)) { @@ -44,7 +44,7 @@ public class NodeUserAdmin extends CmsUserAdmin implements ManagedServiceFactory regProps.put(Constants.SERVICE_PID, pid); if (isSystemRolesBaseDn(basePath)) regProps.put(Constants.SERVICE_RANKING, Integer.MAX_VALUE); - regProps.put(UserAdminConf.baseDn.name(), basePath); + regProps.put(DirectoryConf.baseDn.name(), basePath); CmsActivator.getBundleContext().registerService(UserDirectory.class, userDirectory, regProps); pidToBaseDn.put(pid, basePath); 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 965330237..64b25c99e 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 @@ -37,8 +37,8 @@ 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.UserAdminConf; import org.argeo.osgi.useradmin.UserDirectory; +import org.argeo.util.directory.DirectoryConf; import org.argeo.util.naming.dns.DnsBrowser; import org.argeo.util.transaction.WorkControl; import org.argeo.util.transaction.WorkTransaction; @@ -78,12 +78,12 @@ public class CmsUserAdmin extends AggregatingUserAdmin { } public UserDirectory enableUserDirectory(Dictionary properties) { - String uri = (String) properties.get(UserAdminConf.uri.name()); - Object realm = properties.get(UserAdminConf.realm.name()); + String uri = (String) properties.get(DirectoryConf.uri.name()); + Object realm = properties.get(DirectoryConf.realm.name()); URI u; try { if (uri == null) { - String baseDn = (String) properties.get(UserAdminConf.baseDn.name()); + String baseDn = (String) properties.get(DirectoryConf.baseDn.name()); u = KernelUtils.getOsgiInstanceUri(KernelConstants.DIR_NODE + '/' + baseDn + ".ldif"); } else if (realm != null) { u = null; @@ -96,12 +96,12 @@ public class CmsUserAdmin extends AggregatingUserAdmin { // Create UserDirectory userDirectory; - if (realm != null || UserAdminConf.SCHEME_LDAP.equals(u.getScheme()) - || UserAdminConf.SCHEME_LDAPS.equals(u.getScheme())) { + if (realm != null || DirectoryConf.SCHEME_LDAP.equals(u.getScheme()) + || DirectoryConf.SCHEME_LDAPS.equals(u.getScheme())) { userDirectory = new LdapUserAdmin(properties); - } else if (UserAdminConf.SCHEME_FILE.equals(u.getScheme())) { + } else if (DirectoryConf.SCHEME_FILE.equals(u.getScheme())) { userDirectory = new LdifUserAdmin(u, properties); - } else if (UserAdminConf.SCHEME_OS.equals(u.getScheme())) { + } else if (DirectoryConf.SCHEME_OS.equals(u.getScheme())) { userDirectory = new OsUserDirectory(u, properties); singleUser = true; } else { diff --git a/org.argeo.cms/src/org/argeo/cms/internal/runtime/InitUtils.java b/org.argeo.cms/src/org/argeo/cms/internal/runtime/InitUtils.java index 821808017..986f7914d 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/runtime/InitUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/runtime/InitUtils.java @@ -24,7 +24,7 @@ import org.apache.commons.io.FileUtils; import org.argeo.api.cms.CmsConstants; import org.argeo.api.cms.CmsLog; import org.argeo.cms.internal.http.InternalHttpConstants; -import org.argeo.osgi.useradmin.UserAdminConf; +import org.argeo.util.directory.DirectoryConf; /** * Interprets framework properties in order to generate the initial deploy @@ -201,13 +201,13 @@ public class InitUtils { u = new URI(uri); } else throw new IllegalArgumentException("Cannot interpret " + uri + " as an uri"); - } else if (u.getScheme().equals(UserAdminConf.SCHEME_FILE)) { + } else if (u.getScheme().equals(DirectoryConf.SCHEME_FILE)) { u = new File(u).getCanonicalFile().toURI(); } } catch (Exception e) { throw new RuntimeException("Cannot interpret " + uri + " as an uri", e); } - Dictionary properties = UserAdminConf.uriAsProperties(u.toString()); + Dictionary properties = DirectoryConf.uriAsProperties(u.toString()); res.add(properties); } diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java b/org.argeo.util/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java index 838c2ce0b..4b13728af 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java @@ -7,18 +7,11 @@ 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.io.File; import java.net.URI; -import java.net.URISyntaxException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Dictionary; -import java.util.Enumeration; -import java.util.Hashtable; import java.util.Iterator; import java.util.List; -import java.util.Optional; -import java.util.StringJoiner; import javax.naming.InvalidNameException; import javax.naming.NameNotFoundException; @@ -30,14 +23,14 @@ import javax.naming.directory.BasicAttribute; import javax.naming.directory.BasicAttributes; import javax.naming.ldap.LdapName; import javax.naming.ldap.Rdn; -import javax.transaction.xa.XAResource; +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.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; import org.osgi.framework.FrameworkUtil; import org.osgi.framework.InvalidSyntaxException; @@ -47,112 +40,28 @@ import org.osgi.service.useradmin.User; import org.osgi.service.useradmin.UserAdmin; /** Base class for a {@link UserDirectory}. */ -abstract class AbstractUserDirectory - implements UserAdmin, UserDirectory, WorkingCopyProcessor, XAResourceProvider { - static final String SHARED_STATE_USERNAME = "javax.security.auth.login.name"; - static final String SHARED_STATE_PASSWORD = "javax.security.auth.login.password"; - - private final Hashtable properties; - private final LdapName baseDn; - // private final LdapName userBaseDn, groupBaseDn; - private final Rdn userBaseRdn, groupBaseRdn, systemRoleBaseRdn; - private final String userObjectClass, groupObjectClass; - - private final boolean readOnly; - private final boolean disabled; - private final String uri; +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() }); - private final boolean scoped; - - private String memberAttributeId = "member"; - private List credentialAttributeIds = Arrays - .asList(new String[] { LdapAttrs.userPassword.name(), LdapAttrs.authPassword.name() }); - // Transaction // private TransactionManager transactionManager; - private WorkControl transactionControl; - private WorkingCopyXaResource xaResource = new WorkingCopyXaResource<>(this); - - private String forcedPassword; - AbstractUserDirectory(URI uriArg, Dictionary props, boolean scoped) { - this.scoped = scoped; - properties = new Hashtable(); - for (Enumeration keys = props.keys(); keys.hasMoreElements();) { - String key = keys.nextElement(); - properties.put(key, props.get(key)); - } - - if (uriArg != null) { - uri = uriArg.toString(); - // uri from properties is ignored - } else { - String uriStr = UserAdminConf.uri.getValue(properties); - if (uriStr == null) - uri = null; - else - uri = uriStr; - } - - forcedPassword = UserAdminConf.forcedPassword.getValue(properties); - - userObjectClass = UserAdminConf.userObjectClass.getValue(properties); - String userBase = UserAdminConf.userBase.getValue(properties); - groupObjectClass = UserAdminConf.groupObjectClass.getValue(properties); - String groupBase = UserAdminConf.groupBase.getValue(properties); - String systemRoleBase = UserAdminConf.systemRoleBase.getValue(properties); - try { - baseDn = new LdapName(UserAdminConf.baseDn.getValue(properties)); - userBaseRdn = new Rdn(userBase); -// userBaseDn = new LdapName(userBase + "," + baseDn); - groupBaseRdn = new Rdn(groupBase); -// groupBaseDn = new LdapName(groupBase + "," + baseDn); - systemRoleBaseRdn = new Rdn(systemRoleBase); - } catch (InvalidNameException e) { - throw new IllegalArgumentException("Badly formated base DN " + UserAdminConf.baseDn.getValue(properties), - e); - } - - // read only - String readOnlyStr = UserAdminConf.readOnly.getValue(properties); - if (readOnlyStr == null) { - readOnly = readOnlyDefault(uri); - properties.put(UserAdminConf.readOnly.name(), Boolean.toString(readOnly)); - } else - readOnly = Boolean.parseBoolean(readOnlyStr); - - // disabled - String disabledStr = UserAdminConf.disabled.getValue(properties); - if (disabledStr != null) - disabled = Boolean.parseBoolean(disabledStr); - else - disabled = false; + 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); - protected abstract Boolean daoHasRole(LdapName dn); - - protected abstract DirectoryUser daoGetRole(LdapName key) throws NameNotFoundException; - - protected abstract List doGetRoles(LdapName searchBase, Filter f, boolean deep); - - protected abstract AbstractUserDirectory scope(User user); - - protected abstract HierarchyUnit doGetHierarchyUnit(LdapName dn); - - protected abstract Iterable doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly); - /* * INITIALIZATION */ @@ -165,20 +74,6 @@ abstract class AbstractUserDirectory } - /* - * PATHS - */ - - @Override - public String getContext() { - return getBaseDn().toString(); - } - - @Override - public String getName() { - return nameToSimple(getBaseDn(), "."); - } - @Override public String getRolePath(Role role) { return nameToRelativePath(((DirectoryUser) role).getDn()); @@ -191,86 +86,11 @@ abstract class AbstractUserDirectory return name; } - protected String nameToRelativePath(LdapName dn) { - LdapName name = LdapNameUtils.relativeName(getBaseDn(), dn); - return nameToSimple(name, "/"); - } - - protected String nameToSimple(LdapName name, String separator) { - StringJoiner path = new StringJoiner(separator); - for (int i = 0; i < name.size(); i++) { - path.add(name.getRdn(i).getValue().toString()); - } - return path.toString(); - - } - - protected LdapName pathToName(String path) { - try { - LdapName name = (LdapName) getBaseDn().clone(); - String[] segments = path.split("/"); - Rdn parentRdn = null; - for (String segment : segments) { - // TODO make attr names configurable ? - String attr = LdapAttrs.ou.name(); - if (parentRdn != null) { - if (getUserBaseRdn().equals(parentRdn)) - attr = LdapAttrs.uid.name(); - else if (getGroupBaseRdn().equals(parentRdn)) - attr = LdapAttrs.cn.name(); - else if (getSystemRoleBaseRdn().equals(parentRdn)) - attr = LdapAttrs.cn.name(); - } - Rdn rdn = new Rdn(attr, segment); - name.add(rdn); - parentRdn = rdn; - } - return name; - } catch (InvalidNameException e) { - throw new IllegalStateException("Cannot get role " + path, e); - } - - } - @Override public Role getRoleByPath(String path) { return doGetRole(pathToName(path)); } - @Override - public Optional getRealm() { - Object realm = getProperties().get(UserAdminConf.realm.name()); - if (realm == null) - return Optional.empty(); - return Optional.of(realm.toString()); - } - - /* - * EDITION - */ - - protected boolean isEditing() { - return xaResource.wc() != null; - } - - protected DirectoryUserWorkingCopy getWorkingCopy() { - DirectoryUserWorkingCopy wc = xaResource.wc(); - if (wc == null) - return null; - return wc; - } - - protected void checkEdit() { - if (xaResource.wc() == null) { - try { - transactionControl.getWorkContext().registerXAResource(xaResource, null); - } catch (Exception e) { - throw new IllegalStateException("Cannot enlist " + xaResource, e); - } - } else { - } - } - protected List getAllRoles(DirectoryUser user) { List allRoles = new ArrayList(); if (user != null) { @@ -322,16 +142,16 @@ abstract class AbstractUserDirectory } protected DirectoryUser doGetRole(LdapName dn) { - DirectoryUserWorkingCopy wc = getWorkingCopy(); + LdapEntryWorkingCopy wc = getWorkingCopy(); DirectoryUser user; try { - user = daoGetRole(dn); + user = (DirectoryUser) daoGetEntry(dn); } catch (NameNotFoundException e) { user = null; } if (wc != null) { if (user == null && wc.getNewData().containsKey(dn)) - user = wc.getNewData().get(dn); + user = (DirectoryUser) wc.getNewData().get(dn); else if (wc.getDeletedData().containsKey(dn)) user = null; } @@ -345,17 +165,21 @@ abstract class AbstractUserDirectory } List getRoles(LdapName searchBase, String filter, boolean deep) throws InvalidSyntaxException { - DirectoryUserWorkingCopy wc = getWorkingCopy(); + LdapEntryWorkingCopy wc = getWorkingCopy(); Filter f = filter != null ? FrameworkUtil.createFilter(filter) : null; - List res = doGetRoles(searchBase, f, deep); + 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 = it.next(); + DirectoryUser user = (DirectoryUser) it.next(); LdapName dn = user.getDn(); if (wc.getDeletedData().containsKey(dn)) it.remove(); } - for (DirectoryUser user : wc.getNewData().values()) { + for (LdapEntry ldapEntry : wc.getNewData().values()) { + DirectoryUser user = (DirectoryUser) ldapEntry; if (f == null || f.match(user.getProperties())) res.add(user); } @@ -402,8 +226,9 @@ abstract class AbstractUserDirectory protected void doGetUser(String key, String value, List collectedUsers) { try { Filter f = FrameworkUtil.createFilter("(" + key + "=" + value + ")"); - List users = doGetRoles(getBaseDn(), f, true); - collectedUsers.addAll(users); + 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); } @@ -415,7 +240,7 @@ abstract class AbstractUserDirectory return new LdifAuthorization(user, getAllRoles((DirectoryUser) user)); } else { // bind - AbstractUserDirectory scopedUserAdmin = scope(user); + AbstractUserDirectory scopedUserAdmin = (AbstractUserDirectory) scope(user); try { DirectoryUser directoryUser = (DirectoryUser) scopedUserAdmin.getRole(user.getName()); if (directoryUser == null) @@ -432,9 +257,9 @@ abstract class AbstractUserDirectory @Override public Role createRole(String name, int type) { checkEdit(); - DirectoryUserWorkingCopy wc = getWorkingCopy(); + LdapEntryWorkingCopy wc = getWorkingCopy(); LdapName dn = toLdapName(name); - if ((daoHasRole(dn) && !wc.getDeletedData().containsKey(dn)) || wc.getNewData().containsKey(dn)) + 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()); @@ -484,10 +309,10 @@ abstract class AbstractUserDirectory @Override public boolean removeRole(String name) { checkEdit(); - DirectoryUserWorkingCopy wc = getWorkingCopy(); + LdapEntryWorkingCopy wc = getWorkingCopy(); LdapName dn = toLdapName(name); boolean actuallyDeleted; - if (daoHasRole(dn) || wc.getNewData().containsKey(dn)) { + if (daoHasEntry(dn) || wc.getNewData().containsKey(dn)) { DirectoryUser user = (DirectoryUser) getRole(name); wc.getDeletedData().put(dn, user); actuallyDeleted = true; @@ -501,23 +326,9 @@ abstract class AbstractUserDirectory return actuallyDeleted; } - /* - * TRANSACTION - */ - @Override - public DirectoryUserWorkingCopy newWorkingCopy() { - return new DirectoryUserWorkingCopy(); - } - /* * HIERARCHY */ - @Override - public HierarchyUnit getHierarchyUnit(String path) { - LdapName dn = pathToName(path); - return doGetHierarchyUnit(dn); - } - @Override public HierarchyUnit getHierarchyUnit(Role role) { LdapName dn = LdapNameUtils.toLdapName(role.getName()); @@ -529,8 +340,13 @@ abstract class AbstractUserDirectory } @Override - public Iterable getDirectHierarchyUnits(boolean functionalOnly) { - return doGetDirectHierarchyUnits(baseDn, functionalOnly); + 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); + } } /* @@ -552,70 +368,7 @@ abstract class AbstractUserDirectory } - private boolean hasObjectClass(Attributes attrs, LdapObjs 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())) - return true; - - } - return false; - } catch (NamingException e) { - throw new IllegalStateException("Cannot search for objectClass " + objectClass.name(), e); - } - } - // GETTERS - protected String getMemberAttributeId() { - return memberAttributeId; - } - - protected List getCredentialAttributeIds() { - return credentialAttributeIds; - } - - protected String getUri() { - return uri; - } - - private static boolean readOnlyDefault(String uriStr) { - if (uriStr == null) - return true; - /// TODO make it more generic - URI uri; - try { - uri = new URI(uriStr.split(" ")[0]); - } catch (URISyntaxException e) { - throw new IllegalArgumentException(e); - } - if (uri.getScheme() == null) - return false;// assume relative file to be writable - if (uri.getScheme().equals(UserAdminConf.SCHEME_FILE)) { - File file = new File(uri); - if (file.exists()) - return !file.canWrite(); - else - return !file.getParentFile().canWrite(); - } else if (uri.getScheme().equals(UserAdminConf.SCHEME_LDAP)) { - if (uri.getAuthority() != null)// assume writable if authenticated - return false; - } else if (uri.getScheme().equals(UserAdminConf.SCHEME_OS)) { - return true; - } - return true;// read only by default - } - - public boolean isReadOnly() { - return readOnly; - } - - public boolean isDisabled() { - return disabled; - } - protected UserAdmin getExternalRoles() { return externalRoles; } @@ -624,50 +377,13 @@ abstract class AbstractUserDirectory Rdn technicalRdn = LdapNameUtils.getParentRdn(dn); if (getGroupBaseRdn().equals(technicalRdn) || getSystemRoleBaseRdn().equals(technicalRdn)) return Role.GROUP; - else if (userBaseRdn.equals(technicalRdn)) + else if (getUserBaseRdn().equals(technicalRdn)) return Role.USER; else throw new IllegalArgumentException( "Cannot dind role type, " + technicalRdn + " is not a technical RDN for " + dn); } - /** dn can be null, in that case a default should be returned. */ - public String getUserObjectClass() { - return userObjectClass; - } - - Rdn getUserBaseRdn() { - return userBaseRdn; - } - - protected String newUserObjectClass(LdapName dn) { - return getUserObjectClass(); - } - - public String getGroupObjectClass() { - return groupObjectClass; - } - - Rdn getGroupBaseRdn() { - return groupBaseRdn; - } - - Rdn getSystemRoleBaseRdn() { - return systemRoleBaseRdn; - } - - LdapName getBaseDn() { - return (LdapName) baseDn.clone(); - } - - public Dictionary getProperties() { - return properties; - } - - public Dictionary cloneProperties() { - return new Hashtable<>(properties); - } - public void setExternalRoles(UserAdmin externalRoles) { this.externalRoles = externalRoles; } @@ -676,32 +392,6 @@ abstract class AbstractUserDirectory // this.transactionManager = transactionManager; // } - public String getForcedPassword() { - return forcedPassword; - } - - public void setTransactionControl(WorkControl transactionControl) { - this.transactionControl = transactionControl; - } - - public XAResource getXaResource() { - return xaResource; - } - - public boolean isScoped() { - return scoped; - } - - @Override - public int hashCode() { - return baseDn.hashCode(); - } - - @Override - public String toString() { - return "User Directory " + baseDn.toString(); - } - /* * STATIC UTILITIES */ 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 ac641de97..955178ce4 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java @@ -117,7 +117,7 @@ public class AggregatingUserAdmin implements UserAdmin { if (user instanceof DirectoryUser) { userAdminToUse = userReferentialOfThisUser; } else if (user instanceof AuthenticatingUser) { - userAdminToUse = userReferentialOfThisUser.scope(user); + userAdminToUse = (AbstractUserDirectory) userReferentialOfThisUser.scope(user); } else { throw new IllegalArgumentException("Unsupported user type " + user.getClass()); } diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/AuthenticatingUser.java b/org.argeo.util/src/org/argeo/osgi/useradmin/AuthenticatingUser.java index 01db8be98..ba1f3f753 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/AuthenticatingUser.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/AuthenticatingUser.java @@ -5,6 +5,7 @@ import java.util.Hashtable; import javax.naming.ldap.LdapName; +import org.argeo.util.directory.DirectoryDigestUtils; import org.osgi.service.useradmin.User; /** @@ -38,7 +39,7 @@ public class AuthenticatingUser implements User { this.name = name; credentials = new Hashtable<>(); credentials.put(SHARED_STATE_NAME, name); - byte[] pwd = DigestUtils.charsToBytes(password); + byte[] pwd = DirectoryDigestUtils.charsToBytes(password); credentials.put(SHARED_STATE_PWD, pwd); } diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/DigestUtils.java b/org.argeo.util/src/org/argeo/osgi/useradmin/DigestUtils.java deleted file mode 100644 index 55d24d994..000000000 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/DigestUtils.java +++ /dev/null @@ -1,113 +0,0 @@ -package org.argeo.osgi.useradmin; - -import java.math.BigInteger; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.KeySpec; -import java.util.Arrays; - -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.PBEKeySpec; - -/** Utilities around digests, mostly those related to passwords. */ -class DigestUtils { - final static String PASSWORD_SCHEME_SHA = "SHA"; - final static String PASSWORD_SCHEME_PBKDF2_SHA256 = "PBKDF2_SHA256"; - - static byte[] sha1(byte[] bytes) { - try { - MessageDigest digest = MessageDigest.getInstance("SHA1"); - digest.update(bytes); - byte[] checksum = digest.digest(); - return checksum; - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("Cannot SHA1 digest", e); - } - } - - static byte[] toPasswordScheme(String passwordScheme, char[] password, byte[] salt, Integer iterations, - Integer keyLength) { - try { - if (PASSWORD_SCHEME_SHA.equals(passwordScheme)) { - MessageDigest digest = MessageDigest.getInstance("SHA1"); - byte[] bytes = charsToBytes(password); - digest.update(bytes); - return digest.digest(); - } else if (PASSWORD_SCHEME_PBKDF2_SHA256.equals(passwordScheme)) { - KeySpec spec = new PBEKeySpec(password, salt, iterations, keyLength); - - SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); - final int ITERATION_LENGTH = 4; - byte[] key = f.generateSecret(spec).getEncoded(); - byte[] result = new byte[ITERATION_LENGTH + salt.length + key.length]; - byte iterationsArr[] = new BigInteger(iterations.toString()).toByteArray(); - if (iterationsArr.length < ITERATION_LENGTH) { - Arrays.fill(result, 0, ITERATION_LENGTH - iterationsArr.length, (byte) 0); - System.arraycopy(iterationsArr, 0, result, ITERATION_LENGTH - iterationsArr.length, - iterationsArr.length); - } else { - System.arraycopy(iterationsArr, 0, result, 0, ITERATION_LENGTH); - } - System.arraycopy(salt, 0, result, ITERATION_LENGTH, salt.length); - System.arraycopy(key, 0, result, ITERATION_LENGTH + salt.length, key.length); - return result; - } else { - throw new UnsupportedOperationException("Unkown password scheme " + passwordScheme); - } - } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { - throw new IllegalStateException("Cannot digest", e); - } - } - - static char[] bytesToChars(Object obj) { - if (obj instanceof char[]) - return (char[]) obj; - if (!(obj instanceof byte[])) - throw new IllegalArgumentException(obj.getClass() + " is not a byte array"); - ByteBuffer fromBuffer = ByteBuffer.wrap((byte[]) obj); - CharBuffer toBuffer = StandardCharsets.UTF_8.decode(fromBuffer); - char[] res = Arrays.copyOfRange(toBuffer.array(), toBuffer.position(), toBuffer.limit()); - // Arrays.fill(fromBuffer.array(), (byte) 0); // clear sensitive data - // Arrays.fill((byte[]) obj, (byte) 0); // clear sensitive data - // Arrays.fill(toBuffer.array(), '\u0000'); // clear sensitive data - return res; - } - - static byte[] charsToBytes(char[] chars) { - CharBuffer charBuffer = CharBuffer.wrap(chars); - ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer); - byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit()); - // Arrays.fill(charBuffer.array(), '\u0000'); // clear sensitive data - // Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data - return bytes; - } - - static String sha1str(String str) { - byte[] hash = sha1(str.getBytes(StandardCharsets.UTF_8)); - return encodeHexString(hash); - } - - final private static char[] hexArray = "0123456789abcdef".toCharArray(); - - /** - * From - * http://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to - * -a-hex-string-in-java - */ - public static String encodeHexString(byte[] bytes) { - char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = hexArray[v >>> 4]; - hexChars[j * 2 + 1] = hexArray[v & 0x0F]; - } - return new String(hexChars); - } - - private DigestUtils() { - } -} 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 146b80578..c82c5a01d 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUser.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUser.java @@ -1,15 +1,8 @@ package org.argeo.osgi.useradmin; -import javax.naming.directory.Attributes; -import javax.naming.ldap.LdapName; - +import org.argeo.util.directory.ldap.LdapEntry; import org.osgi.service.useradmin.User; /** A user in a user directory. */ -interface DirectoryUser extends User { - LdapName getDn(); - - Attributes getAttributes(); - - void publishAttributes(Attributes modifiedAttributes); +interface DirectoryUser extends User, LdapEntry { } diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUserWorkingCopy.java b/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUserWorkingCopy.java deleted file mode 100644 index 2aed145c7..000000000 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUserWorkingCopy.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.argeo.osgi.useradmin; - -import javax.naming.directory.Attributes; -import javax.naming.ldap.LdapName; - -import org.argeo.util.transaction.AbstractWorkingCopy; - -/** Working copy for a user directory being edited. */ -class DirectoryUserWorkingCopy extends AbstractWorkingCopy { - @Override - protected LdapName getId(DirectoryUser user) { - return user.getDn(); - } - - @Override - protected Attributes cloneAttributes(DirectoryUser user) { - return (Attributes) user.getAttributes().clone(); - } -} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/HierarchyUnit.java b/org.argeo.util/src/org/argeo/osgi/useradmin/HierarchyUnit.java deleted file mode 100644 index 2c21342e3..000000000 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/HierarchyUnit.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.argeo.osgi.useradmin; - -import java.util.List; - -import org.osgi.service.useradmin.Role; - -/** A unit within the high-level organisational structure of a directory. */ -public interface HierarchyUnit { - String getHierarchyUnitName(); - - HierarchyUnit getParent(); - - Iterable getDirectHierachyUnits(boolean functionalOnly); - - boolean isFunctional(); - - String getContext(); - - List getHierarchyUnitRoles(String filter, boolean deep); - - UserDirectory getDirectory(); - -// Map getHierarchyProperties(); -} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/IpaUtils.java b/org.argeo.util/src/org/argeo/osgi/useradmin/IpaUtils.java deleted file mode 100644 index e1c8136f5..000000000 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/IpaUtils.java +++ /dev/null @@ -1,139 +0,0 @@ -package org.argeo.osgi.useradmin; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Dictionary; -import java.util.Hashtable; -import java.util.List; - -import javax.naming.InvalidNameException; -import javax.naming.NamingException; -import javax.naming.ldap.LdapName; - -import org.argeo.util.naming.LdapAttrs; -import org.argeo.util.naming.dns.DnsBrowser; - -/** Free IPA specific conventions. */ -public class IpaUtils { - public final static String IPA_USER_BASE = "cn=users,cn=accounts"; - public final static String IPA_GROUP_BASE = "cn=groups,cn=accounts"; - public final static String IPA_ROLE_BASE = "cn=roles,cn=accounts"; - public final static String IPA_SERVICE_BASE = "cn=services,cn=accounts"; - - private final static String KRB_PRINCIPAL_NAME = LdapAttrs.krbPrincipalName.name().toLowerCase(); - - public final static String IPA_USER_DIRECTORY_CONFIG = UserAdminConf.userBase + "=" + IPA_USER_BASE + "&" - + UserAdminConf.groupBase + "=" + IPA_GROUP_BASE + "&" + UserAdminConf.readOnly + "=true"; - - @Deprecated - static String domainToUserDirectoryConfigPath(String realm) { - return domainToBaseDn(realm) + "?" + IPA_USER_DIRECTORY_CONFIG + "&" + UserAdminConf.realm.name() + "=" + realm; - } - - public static void addIpaConfig(String realm, Dictionary properties) { - properties.put(UserAdminConf.baseDn.name(), domainToBaseDn(realm)); - properties.put(UserAdminConf.realm.name(), realm); - properties.put(UserAdminConf.userBase.name(), IPA_USER_BASE); - properties.put(UserAdminConf.groupBase.name(), IPA_GROUP_BASE); - properties.put(UserAdminConf.systemRoleBase.name(), IPA_ROLE_BASE); - properties.put(UserAdminConf.readOnly.name(), Boolean.TRUE.toString()); - } - - public static String domainToBaseDn(String domain) { - String[] dcs = domain.split("\\."); - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < dcs.length; i++) { - if (i != 0) - sb.append(','); - String dc = dcs[i]; - sb.append(LdapAttrs.dc.name()).append('=').append(dc.toLowerCase()); - } - return sb.toString(); - } - - public static LdapName kerberosToDn(String kerberosName) { - String[] kname = kerberosName.split("@"); - String username = kname[0]; - String baseDn = domainToBaseDn(kname[1]); - String dn; - if (!username.contains("/")) - dn = LdapAttrs.uid + "=" + username + "," + IPA_USER_BASE + "," + baseDn; - else - dn = KRB_PRINCIPAL_NAME + "=" + kerberosName + "," + IPA_SERVICE_BASE + "," + baseDn; - try { - return new LdapName(dn); - } catch (InvalidNameException e) { - throw new IllegalArgumentException("Badly formatted name for " + kerberosName + ": " + dn); - } - } - - private IpaUtils() { - - } - - public static String kerberosDomainFromDns() { - String kerberosDomain; - try (DnsBrowser dnsBrowser = new DnsBrowser()) { - InetAddress localhost = InetAddress.getLocalHost(); - String hostname = localhost.getHostName(); - String dnsZone = hostname.substring(hostname.indexOf('.') + 1); - kerberosDomain = dnsBrowser.getRecord("_kerberos." + dnsZone, "TXT"); - return kerberosDomain; - } catch (NamingException | IOException e) { - throw new IllegalStateException("Cannot determine Kerberos domain from DNS", e); - } - - } - - public static Dictionary convertIpaUri(URI uri) { - String path = uri.getPath(); - String kerberosRealm; - if (path == null || path.length() <= 1) { - kerberosRealm = kerberosDomainFromDns(); - } else { - kerberosRealm = path.substring(1); - } - - if (kerberosRealm == null) - throw new IllegalStateException("No Kerberos domain available for " + uri); - // TODO intergrate CA certificate in truststore - // String schemeToUse = SCHEME_LDAPS; - String schemeToUse = UserAdminConf.SCHEME_LDAP; - List ldapHosts; - String ldapHostsStr = uri.getHost(); - if (ldapHostsStr == null || ldapHostsStr.trim().equals("")) { - try (DnsBrowser dnsBrowser = new DnsBrowser()) { - ldapHosts = dnsBrowser.getSrvRecordsAsHosts("_ldap._tcp." + kerberosRealm.toLowerCase(), - schemeToUse.equals(UserAdminConf.SCHEME_LDAP) ? true : false); - if (ldapHosts == null || ldapHosts.size() == 0) { - throw new IllegalStateException("Cannot configure LDAP for IPA " + uri); - } else { - ldapHostsStr = ldapHosts.get(0); - } - } catch (NamingException | IOException e) { - throw new IllegalStateException("Cannot convert IPA uri " + uri, e); - } - } else { - ldapHosts = new ArrayList<>(); - ldapHosts.add(ldapHostsStr); - } - - StringBuilder uriStr = new StringBuilder(); - try { - for (String host : ldapHosts) { - URI convertedUri = new URI(schemeToUse + "://" + host + "/"); - uriStr.append(convertedUri).append(' '); - } - } catch (URISyntaxException e) { - throw new IllegalStateException("Cannot convert IPA uri " + uri, e); - } - - Hashtable res = new Hashtable<>(); - res.put(UserAdminConf.uri.name(), uriStr.toString()); - addIpaConfig(kerberosRealm, res); - return res; - } -} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/LdapConnection.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdapConnection.java deleted file mode 100644 index 1fe7eb9df..000000000 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/LdapConnection.java +++ /dev/null @@ -1,147 +0,0 @@ -package org.argeo.osgi.useradmin; - -import java.util.Dictionary; -import java.util.Hashtable; - -import javax.naming.CommunicationException; -import javax.naming.Context; -import javax.naming.NameNotFoundException; -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attributes; -import javax.naming.directory.DirContext; -import javax.naming.directory.SearchControls; -import javax.naming.directory.SearchResult; -import javax.naming.ldap.InitialLdapContext; -import javax.naming.ldap.LdapName; - -import org.argeo.util.naming.LdapAttrs; - -/** A synchronized wrapper for a single {@link InitialLdapContext}. */ -// TODO implement multiple contexts and connection pooling. -class LdapConnection { - private InitialLdapContext initialLdapContext = null; - - LdapConnection(String url, Dictionary properties) { - try { - Hashtable connEnv = new Hashtable(); - connEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); - connEnv.put(Context.PROVIDER_URL, url); - connEnv.put("java.naming.ldap.attributes.binary", LdapAttrs.userPassword.name()); - // use pooling in order to avoid connection timeout -// connEnv.put("com.sun.jndi.ldap.connect.pool", "true"); -// connEnv.put("com.sun.jndi.ldap.connect.pool.timeout", 300000); - - initialLdapContext = new InitialLdapContext(connEnv, null); - // StartTlsResponse tls = (StartTlsResponse) ctx - // .extendedOperation(new StartTlsRequest()); - // tls.negotiate(); - Object securityAuthentication = properties.get(Context.SECURITY_AUTHENTICATION); - if (securityAuthentication != null) - initialLdapContext.addToEnvironment(Context.SECURITY_AUTHENTICATION, securityAuthentication); - else - initialLdapContext.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple"); - Object principal = properties.get(Context.SECURITY_PRINCIPAL); - if (principal != null) { - initialLdapContext.addToEnvironment(Context.SECURITY_PRINCIPAL, principal.toString()); - Object creds = properties.get(Context.SECURITY_CREDENTIALS); - if (creds != null) { - initialLdapContext.addToEnvironment(Context.SECURITY_CREDENTIALS, creds.toString()); - } - } - } catch (NamingException e) { - throw new IllegalStateException("Cannot connect to LDAP", e); - } - - } - - public void init() { - - } - - public void destroy() { - try { - // tls.close(); - initialLdapContext.close(); - initialLdapContext = null; - } catch (NamingException e) { - e.printStackTrace(); - } - } - - protected InitialLdapContext getLdapContext() { - return initialLdapContext; - } - - protected void reconnect() throws NamingException { - initialLdapContext.reconnect(initialLdapContext.getConnectControls()); - } - - public synchronized NamingEnumeration search(LdapName searchBase, String searchFilter, - SearchControls searchControls) throws NamingException { - NamingEnumeration results; - try { - results = getLdapContext().search(searchBase, searchFilter, searchControls); - } catch (CommunicationException e) { - reconnect(); - results = getLdapContext().search(searchBase, searchFilter, searchControls); - } - return results; - } - - public synchronized Attributes getAttributes(LdapName name) throws NamingException { - try { - return getLdapContext().getAttributes(name); - } catch (CommunicationException e) { - reconnect(); - return getLdapContext().getAttributes(name); - } - } - - synchronized void prepareChanges(DirectoryUserWorkingCopy wc) throws NamingException { - // make sure connection will work - reconnect(); - - // delete - for (LdapName dn : wc.getDeletedData().keySet()) { - if (!entryExists(dn)) - throw new IllegalStateException("User to delete no found " + dn); - } - // add - for (LdapName dn : wc.getNewData().keySet()) { - if (entryExists(dn)) - throw new IllegalStateException("User to create found " + dn); - } - // modify - for (LdapName dn : wc.getModifiedData().keySet()) { - if (!wc.getNewData().containsKey(dn) && !entryExists(dn)) - throw new IllegalStateException("User to modify not found " + dn); - } - - } - - protected boolean entryExists(LdapName dn) throws NamingException { - try { - return getAttributes(dn).size() != 0; - } catch (NameNotFoundException e) { - return false; - } - } - - synchronized void commitChanges(DirectoryUserWorkingCopy wc) throws NamingException { - // delete - for (LdapName dn : wc.getDeletedData().keySet()) { - getLdapContext().destroySubcontext(dn); - } - // add - for (LdapName dn : wc.getNewData().keySet()) { - DirectoryUser user = wc.getNewData().get(dn); - getLdapContext().createSubcontext(dn, user.getAttributes()); - } - // modify - for (LdapName dn : wc.getModifiedData().keySet()) { - Attributes modifiedAttrs = wc.getModifiedData().get(dn); - getLdapContext().modifyAttributes(dn, DirContext.REPLACE_ATTRIBUTE, modifiedAttrs); - } - } -} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/LdapNameUtils.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdapNameUtils.java deleted file mode 100644 index 7e763456a..000000000 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/LdapNameUtils.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.argeo.osgi.useradmin; - -import javax.naming.InvalidNameException; -import javax.naming.ldap.LdapName; -import javax.naming.ldap.Rdn; - -/** Utilities to simplify using {@link LdapName}. */ -class LdapNameUtils { - - static LdapName relativeName(LdapName prefix, LdapName dn) { - try { - if (!dn.startsWith(prefix)) - throw new IllegalArgumentException("Prefix " + prefix + " not consistent with " + dn); - LdapName res = (LdapName) dn.clone(); - for (int i = 0; i < prefix.size(); i++) { - res.remove(0); - } - return res; - } catch (InvalidNameException e) { - throw new IllegalStateException("Cannot find realtive name", e); - } - } - - static LdapName getParent(LdapName dn) { - try { - LdapName parent = (LdapName) dn.clone(); - parent.remove(parent.size() - 1); - return parent; - } catch (InvalidNameException e) { - throw new IllegalArgumentException("Cannot get parent of " + dn, e); - } - } - - static Rdn getParentRdn(LdapName dn) { - if (dn.size() < 2) - throw new IllegalArgumentException(dn + " has no parent"); - Rdn parentRdn = dn.getRdn(dn.size() - 2); - return parentRdn; - } - - static LdapName toLdapName(String distinguishedName) { - try { - return new LdapName(distinguishedName); - } catch (InvalidNameException e) { - throw new IllegalArgumentException("Cannot parse " + distinguishedName + " as LDAP name", e); - } - } - - static Rdn getLastRdn(LdapName dn) { - return dn.getRdn(dn.size() - 1); - } - - static String getLastRdnAsString(LdapName dn) { - return getLastRdn(dn).toString(); - } - - static String getLastRdnValue(LdapName dn) { - return getLastRdn(dn).getValue().toString(); - } - - /** singleton */ - private LdapNameUtils() { - - } -} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/LdapUserAdmin.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdapUserAdmin.java index 879d5da04..36419d960 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/LdapUserAdmin.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/LdapUserAdmin.java @@ -19,6 +19,12 @@ 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; @@ -52,7 +58,7 @@ public class LdapUserAdmin extends AbstractUserDirectory { Object pwdCred = credentials.get(SHARED_STATE_PASSWORD); byte[] pwd = (byte[]) pwdCred; if (pwd != null) { - char[] password = DigestUtils.bytesToChars(pwd); + char[] password = DirectoryDigestUtils.bytesToChars(pwd); properties.put(Context.SECURITY_CREDENTIALS, new String(password)); } else { properties.put(Context.SECURITY_AUTHENTICATION, "GSSAPI"); @@ -65,16 +71,16 @@ public class LdapUserAdmin extends AbstractUserDirectory { // } @Override - protected Boolean daoHasRole(LdapName dn) { + protected Boolean daoHasEntry(LdapName dn) { try { - return daoGetRole(dn) != null; + return daoGetEntry(dn) != null; } catch (NameNotFoundException e) { return false; } } @Override - protected DirectoryUser daoGetRole(LdapName name) throws NameNotFoundException { + protected DirectoryUser daoGetEntry(LdapName name) throws NameNotFoundException { try { Attributes attrs = ldapConnection.getAttributes(name); if (attrs.size() == 0) @@ -96,8 +102,8 @@ public class LdapUserAdmin extends AbstractUserDirectory { } @Override - protected List doGetRoles(LdapName searchBase, Filter f, boolean deep) { - ArrayList res = new ArrayList(); + protected List doGetEntries(LdapName searchBase, Filter f, boolean deep) { + ArrayList res = new ArrayList<>(); try { String searchFilter = f != null ? f.toString() : "(|(" + objectClass + "=" + getUserObjectClass() + ")(" + objectClass + "=" @@ -165,7 +171,7 @@ public class LdapUserAdmin extends AbstractUserDirectory { } @Override - public void prepare(DirectoryUserWorkingCopy wc) { + public void prepare(LdapEntryWorkingCopy wc) { try { ldapConnection.prepareChanges(wc); } catch (NamingException e) { @@ -174,7 +180,7 @@ public class LdapUserAdmin extends AbstractUserDirectory { } @Override - public void commit(DirectoryUserWorkingCopy wc) { + public void commit(LdapEntryWorkingCopy wc) { try { ldapConnection.commitChanges(wc); } catch (NamingException e) { @@ -183,7 +189,7 @@ public class LdapUserAdmin extends AbstractUserDirectory { } @Override - public void rollback(DirectoryUserWorkingCopy wc) { + public void rollback(LdapEntryWorkingCopy wc) { // prepare not impacting } @@ -192,7 +198,7 @@ public class LdapUserAdmin extends AbstractUserDirectory { */ @Override - protected Iterable doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) { + public Iterable doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) { List res = new ArrayList<>(); try { String searchFilter = "(|(" + objectClass + "=" + LdapObjs.organizationalUnit.name() + ")(" + objectClass @@ -207,7 +213,7 @@ public class LdapUserAdmin extends AbstractUserDirectory { SearchResult searchResult = (SearchResult) results.nextElement(); LdapName dn = toDn(searchBase, searchResult); Attributes attrs = searchResult.getAttributes(); - LdifHierarchyUnit hierarchyUnit = new LdifHierarchyUnit(this, dn, attrs); + LdapHierarchyUnit hierarchyUnit = new LdapHierarchyUnit(this, dn, attrs); if (functionalOnly) { if (hierarchyUnit.isFunctional()) res.add(hierarchyUnit); @@ -222,10 +228,10 @@ public class LdapUserAdmin extends AbstractUserDirectory { } @Override - protected HierarchyUnit doGetHierarchyUnit(LdapName dn) { + public HierarchyUnit doGetHierarchyUnit(LdapName dn) { try { Attributes attrs = ldapConnection.getAttributes(dn); - return new LdifHierarchyUnit(this, dn, attrs); + 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/LdifHierarchyUnit.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifHierarchyUnit.java deleted file mode 100644 index a847e49ae..000000000 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifHierarchyUnit.java +++ /dev/null @@ -1,108 +0,0 @@ -package org.argeo.osgi.useradmin; - -import java.util.List; -import java.util.Objects; - -import javax.naming.directory.Attributes; -import javax.naming.ldap.LdapName; -import javax.naming.ldap.Rdn; - -import org.osgi.framework.InvalidSyntaxException; -import org.osgi.service.useradmin.Role; - -/** LDIF/LDAP based implementation of {@link HierarchyUnit}. */ -class LdifHierarchyUnit implements HierarchyUnit { - private final AbstractUserDirectory directory; - - private final LdapName dn; - private final boolean functional; - private final Attributes attributes; - -// HierarchyUnit parent; -// List children = new ArrayList<>(); - - LdifHierarchyUnit(AbstractUserDirectory directory, LdapName dn, Attributes attributes) { - Objects.requireNonNull(directory); - Objects.requireNonNull(dn); - - this.directory = directory; - this.dn = dn; - this.attributes = attributes; - - Rdn rdn = LdapNameUtils.getLastRdn(dn); - functional = !(directory.getUserBaseRdn().equals(rdn) || directory.getGroupBaseRdn().equals(rdn) - || directory.getSystemRoleBaseRdn().equals(rdn)); - } - - @Override - public HierarchyUnit getParent() { - return directory.doGetHierarchyUnit(LdapNameUtils.getParent(dn)); - } - - @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); - } - - @Override - public boolean isFunctional() { - return functional; - } - - @Override - public String getHierarchyUnitName() { - String name = LdapNameUtils.getLastRdnValue(dn); - // TODO check ou, o, etc. - return name; - } - - public Attributes getAttributes() { - return attributes; - } - - @Override - public String getContext() { - return dn.toString(); - } - - @Override - public List getHierarchyUnitRoles(String filter, boolean deep) { - try { - return directory.getRoles(dn, filter, deep); - } catch (InvalidSyntaxException e) { - throw new IllegalArgumentException("Cannot filter " + filter + " " + dn, e); - } - } - - @Override - public UserDirectory getDirectory() { - return directory; - } - - @Override - public int hashCode() { - return dn.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof LdifHierarchyUnit)) - return false; - return ((LdifHierarchyUnit) obj).dn.equals(dn); - } - - @Override - public String toString() { - return "Hierarchy Unit " + dn.toString(); - } - -} 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 aaac50272..6cf6725cc 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUser.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUser.java @@ -21,40 +21,28 @@ import javax.naming.directory.Attributes; import javax.naming.directory.BasicAttribute; import javax.naming.ldap.LdapName; +import org.argeo.util.directory.DirectoryDigestUtils; import org.argeo.util.directory.Person; +import org.argeo.util.directory.ldap.AbstractLdapEntry; +import org.argeo.util.directory.ldap.AuthPassword; import org.argeo.util.naming.LdapAttrs; import org.argeo.util.naming.LdapObjs; import org.argeo.util.naming.SharedSecret; -import org.argeo.util.naming.ldap.AuthPassword; /** Directory user implementation */ -abstract class LdifUser implements DirectoryUser { - private final AbstractUserDirectory userAdmin; - - private final LdapName dn; - - private final boolean frozen; - private Attributes publishedAttributes; - +abstract class LdifUser extends AbstractLdapEntry implements DirectoryUser { private final AttributeDictionary properties; private final AttributeDictionary credentials; LdifUser(AbstractUserDirectory userAdmin, LdapName dn, Attributes attributes) { - this(userAdmin, dn, attributes, false); - } - - private LdifUser(AbstractUserDirectory userAdmin, LdapName dn, Attributes attributes, boolean frozen) { - this.userAdmin = userAdmin; - this.dn = dn; - this.publishedAttributes = attributes; + super(userAdmin, dn, attributes); properties = new AttributeDictionary(false); credentials = new AttributeDictionary(true); - this.frozen = frozen; } @Override public String getName() { - return dn.toString(); + return getDn().toString(); } @Override @@ -78,9 +66,10 @@ abstract class LdifUser implements DirectoryUser { // TODO check other sources (like PKCS12) // String pwd = new String((char[]) value); // authPassword (RFC 312 https://tools.ietf.org/html/rfc3112) - char[] password = DigestUtils.bytesToChars(value); + char[] password = DirectoryDigestUtils.bytesToChars(value); - if (userAdmin.getForcedPassword() != null && userAdmin.getForcedPassword().equals(new String(password))) + if (getDirectory().getForcedPassword() != null + && getDirectory().getForcedPassword().equals(new String(password))) return true; AuthPassword authPassword = AuthPassword.matchAuthValue(getAttributes(), password); @@ -104,7 +93,7 @@ abstract class LdifUser implements DirectoryUser { // Regular password // byte[] hashedPassword = hash(password, DigestUtils.PASSWORD_SCHEME_PBKDF2_SHA256); - if (hasCredential(LdapAttrs.userPassword.name(), DigestUtils.charsToBytes(password))) + if (hasCredential(LdapAttrs.userPassword.name(), DirectoryDigestUtils.charsToBytes(password))) return true; return false; } @@ -125,11 +114,12 @@ abstract class LdifUser implements DirectoryUser { passwordScheme = storedBase64.substring(1, index); String storedValueBase64 = storedBase64.substring(index + 1); byte[] storedValueBytes = Base64.getDecoder().decode(storedValueBase64); - char[] passwordValue = DigestUtils.bytesToChars((byte[]) value); + char[] passwordValue = DirectoryDigestUtils.bytesToChars((byte[]) value); byte[] valueBytes; - if (DigestUtils.PASSWORD_SCHEME_SHA.equals(passwordScheme)) { - valueBytes = DigestUtils.toPasswordScheme(passwordScheme, passwordValue, null, null, null); - } else if (DigestUtils.PASSWORD_SCHEME_PBKDF2_SHA256.equals(passwordScheme)) { + if (DirectoryDigestUtils.PASSWORD_SCHEME_SHA.equals(passwordScheme)) { + valueBytes = DirectoryDigestUtils.toPasswordScheme(passwordScheme, passwordValue, null, null, + null); + } else if (DirectoryDigestUtils.PASSWORD_SCHEME_PBKDF2_SHA256.equals(passwordScheme)) { // see https://www.thesubtlety.com/post/a-389-ds-pbkdf2-password-checker/ byte[] iterationsArr = Arrays.copyOfRange(storedValueBytes, 0, 4); BigInteger iterations = new BigInteger(iterationsArr); @@ -138,7 +128,7 @@ abstract class LdifUser implements DirectoryUser { byte[] keyArr = Arrays.copyOfRange(storedValueBytes, iterationsArr.length + salt.length, storedValueBytes.length); int keyLengthBits = keyArr.length * 8; - valueBytes = DigestUtils.toPasswordScheme(passwordScheme, passwordValue, salt, + valueBytes = DirectoryDigestUtils.toPasswordScheme(passwordScheme, passwordValue, salt, iterations.intValue(), keyLengthBits); } else { throw new UnsupportedOperationException("Unknown password scheme " + passwordScheme); @@ -155,8 +145,8 @@ abstract class LdifUser implements DirectoryUser { /** Hash the password */ byte[] sha1hash(char[] password) { - byte[] hashedPassword = ("{SHA}" - + Base64.getEncoder().encodeToString(DigestUtils.sha1(DigestUtils.charsToBytes(password)))) + byte[] hashedPassword = ("{SHA}" + Base64.getEncoder() + .encodeToString(DirectoryDigestUtils.sha1(DirectoryDigestUtils.charsToBytes(password)))) .getBytes(StandardCharsets.UTF_8); return hashedPassword; } @@ -170,71 +160,8 @@ abstract class LdifUser implements DirectoryUser { // return hashedPassword; // } - @Override - public LdapName getDn() { - return dn; - } - - @Override - public synchronized Attributes getAttributes() { - return isEditing() ? getModifiedAttributes() : publishedAttributes; - } - - /** Should only be called from working copy thread. */ - private synchronized Attributes getModifiedAttributes() { - assert getWc() != null; - return getWc().getModifiedData().get(getDn()); - } - - protected synchronized boolean isEditing() { - return getWc() != null && getModifiedAttributes() != null; - } - - private synchronized DirectoryUserWorkingCopy getWc() { - return userAdmin.getWorkingCopy(); - } - - protected synchronized void startEditing() { - if (frozen) - throw new IllegalStateException("Cannot edit frozen view"); - if (getUserAdmin().isReadOnly()) - throw new IllegalStateException("User directory is read-only"); - assert getModifiedAttributes() == null; - getWc().startEditing(this); - // modifiedAttributes = (Attributes) publishedAttributes.clone(); - } - - public synchronized void publishAttributes(Attributes modifiedAttributes) { - publishedAttributes = modifiedAttributes; - } - -// public DirectoryUser getPublished() { -// return new LdifUser(userAdmin, dn, publishedAttributes, true); -// } - - @Override - public int hashCode() { - return dn.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj instanceof LdifUser) { - LdifUser that = (LdifUser) obj; - return this.dn.equals(that.dn); - } - return false; - } - - @Override - public String toString() { - return dn.toString(); - } - protected AbstractUserDirectory getUserAdmin() { - return userAdmin; + return (AbstractUserDirectory) getDirectory(); } private class AttributeDictionary extends Dictionary { @@ -243,7 +170,7 @@ abstract class LdifUser implements DirectoryUser { private final Boolean includeFilter; public AttributeDictionary(Boolean credentials) { - this.attrFilter = userAdmin.getCredentialAttributeIds(); + this.attrFilter = getDirectory().getCredentialAttributeIds(); this.includeFilter = credentials; try { NamingEnumeration ids = getAttributes().getIDs(); @@ -322,10 +249,10 @@ abstract class LdifUser implements DirectoryUser { continue attrs; if (first == null) first = v; - if (v.equalsIgnoreCase(userAdmin.getUserObjectClass())) - return userAdmin.getUserObjectClass(); - else if (v.equalsIgnoreCase(userAdmin.getGroupObjectClass())) - return userAdmin.getGroupObjectClass(); + if (v.equalsIgnoreCase(getDirectory().getUserObjectClass())) + return getDirectory().getUserObjectClass(); + else if (v.equalsIgnoreCase(getDirectory().getGroupObjectClass())) + return getDirectory().getGroupObjectClass(); } if (first != null) return first; @@ -350,7 +277,7 @@ abstract class LdifUser implements DirectoryUser { public Object put(String key, Object value) { if (key == null) { // TODO persist to other sources (like PKCS12) - char[] password = DigestUtils.bytesToChars(value); + char[] password = DirectoryDigestUtils.bytesToChars(value); byte[] hashedPassword = sha1hash(password); return put(LdapAttrs.userPassword.name(), hashedPassword); } @@ -358,7 +285,7 @@ abstract class LdifUser implements DirectoryUser { return put(LdapAttrs.authPassword.name(), value); } - userAdmin.checkEdit(); + getDirectory().checkEdit(); if (!isEditing()) startEditing(); @@ -390,7 +317,7 @@ abstract class LdifUser implements DirectoryUser { @Override public Object remove(Object key) { - userAdmin.checkEdit(); + getDirectory().checkEdit(); if (!isEditing()) startEditing(); diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUserAdmin.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUserAdmin.java index 26d3d134c..c978af4a0 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUserAdmin.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUserAdmin.java @@ -28,19 +28,25 @@ 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.argeo.util.naming.ldap.LdifParser; -import org.argeo.util.naming.ldap.LdifWriter; 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 users = new TreeMap<>(); + private NavigableMap groups = new TreeMap<>(); - private NavigableMap hierarchy = new TreeMap<>(); + private NavigableMap hierarchy = new TreeMap<>(); // private List rootHierarchyUnits = new ArrayList<>(); public LdifUserAdmin(String uri, String baseDn) { @@ -68,7 +74,7 @@ public class LdifUserAdmin extends AbstractUserDirectory { Object pwdCred = credentials.get(SHARED_STATE_PASSWORD); byte[] pwd = (byte[]) pwdCred; if (pwd != null) { - char[] password = DigestUtils.bytesToChars(pwd); + char[] password = DirectoryDigestUtils.bytesToChars(pwd); User directoryUser = (User) getRole(username); if (!directoryUser.hasCredential(null, password)) throw new IllegalStateException("Invalid credentials"); @@ -76,7 +82,7 @@ public class LdifUserAdmin extends AbstractUserDirectory { throw new IllegalStateException("Password is required"); } Dictionary properties = cloneProperties(); - properties.put(UserAdminConf.readOnly.name(), "true"); + properties.put(DirectoryConf.readOnly.name(), "true"); LdifUserAdmin scopedUserAdmin = new LdifUserAdmin(properties, true); scopedUserAdmin.groups = Collections.unmodifiableNavigableMap(groups); scopedUserAdmin.users = Collections.unmodifiableNavigableMap(users); @@ -85,8 +91,8 @@ public class LdifUserAdmin extends AbstractUserDirectory { private static Dictionary fromUri(String uri, String baseDn) { Hashtable res = new Hashtable(); - res.put(UserAdminConf.uri.name(), uri); - res.put(UserAdminConf.baseDn.name(), baseDn); + res.put(DirectoryConf.uri.name(), uri); + res.put(DirectoryConf.baseDn.name(), baseDn); return res; } @@ -172,7 +178,7 @@ public class LdifUserAdmin extends AbstractUserDirectory { // if (getUserBase().equalsIgnoreCase(name) || getGroupBase().equalsIgnoreCase(name)) // break objectClasses; // skip // TODO skip if it does not contain groups or users - hierarchy.put(key, new LdifHierarchyUnit(this, key, attributes)); + hierarchy.put(key, new LdapHierarchyUnit(this, key, attributes)); break objectClasses; } } @@ -208,23 +214,23 @@ public class LdifUserAdmin extends AbstractUserDirectory { */ @Override - protected DirectoryUser daoGetRole(LdapName key) throws NameNotFoundException { + protected DirectoryUser daoGetEntry(LdapName key) throws NameNotFoundException { if (groups.containsKey(key)) - return groups.get(key); + return (DirectoryUser) groups.get(key); if (users.containsKey(key)) - return users.get(key); + return (DirectoryUser) users.get(key); throw new NameNotFoundException(key + " not persisted"); } @Override - protected Boolean daoHasRole(LdapName dn) { + protected Boolean daoHasEntry(LdapName dn) { return users.containsKey(dn) || groups.containsKey(dn); } @Override - protected List doGetRoles(LdapName searchBase, Filter f, boolean deep) { + protected List doGetEntries(LdapName searchBase, Filter f, boolean deep) { Objects.requireNonNull(searchBase); - ArrayList res = new ArrayList(); + ArrayList res = new ArrayList<>(); if (f == null && deep && getBaseDn().equals(searchBase)) { res.addAll(users.values()); res.addAll(groups.values()); @@ -235,18 +241,20 @@ public class LdifUserAdmin extends AbstractUserDirectory { return res; } - private void filterRoles(SortedMap map, LdapName searchBase, Filter f, - boolean deep, List res) { + private void filterRoles(SortedMap map, LdapName searchBase, Filter f, boolean deep, + List res) { // TODO reduce map with search base ? - roles: for (DirectoryUser user : map.values()) { + 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(user.getProperties())) - res.add(user); + else { + if (f.match(((DirectoryUser) user).getProperties())) + res.add(user); + } } } @@ -256,7 +264,12 @@ public class LdifUserAdmin extends AbstractUserDirectory { protected List getDirectGroups(LdapName dn) { List directGroups = new ArrayList(); for (LdapName name : groups.keySet()) { - DirectoryGroup group = groups.get(name); + 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()); } @@ -264,7 +277,7 @@ public class LdifUserAdmin extends AbstractUserDirectory { } @Override - public void prepare(DirectoryUserWorkingCopy wc) { + public void prepare(LdapEntryWorkingCopy wc) { // delete for (LdapName dn : wc.getDeletedData().keySet()) { if (users.containsKey(dn)) @@ -276,7 +289,7 @@ public class LdifUserAdmin extends AbstractUserDirectory { } // add for (LdapName dn : wc.getNewData().keySet()) { - DirectoryUser user = wc.getNewData().get(dn); + 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()) @@ -290,23 +303,24 @@ public class LdifUserAdmin extends AbstractUserDirectory { for (LdapName dn : wc.getModifiedData().keySet()) { Attributes modifiedAttrs = wc.getModifiedData().get(dn); DirectoryUser user; - if (users.containsKey(dn)) - user = users.get(dn); - else if (groups.containsKey(dn)) - user = groups.get(dn); - else + 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(DirectoryUserWorkingCopy wc) { + public void commit(LdapEntryWorkingCopy wc) { save(); } @Override - public void rollback(DirectoryUserWorkingCopy wc) { + public void rollback(LdapEntryWorkingCopy wc) { init(); } @@ -324,12 +338,12 @@ public class LdifUserAdmin extends AbstractUserDirectory { // return rootHierarchyUnits.get(i); // } @Override - protected HierarchyUnit doGetHierarchyUnit(LdapName dn) { + public HierarchyUnit doGetHierarchyUnit(LdapName dn) { return hierarchy.get(dn); } @Override - protected Iterable doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) { + public Iterable doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) { List res = new ArrayList<>(); for (LdapName n : hierarchy.keySet()) { if (n.size() == searchBase.size() + 1) { 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 69c06c848..1f428ecbd 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserDirectory.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserDirectory.java @@ -11,6 +11,9 @@ import javax.naming.directory.Attributes; import javax.naming.directory.BasicAttributes; import javax.naming.ldap.LdapName; +import org.argeo.util.directory.HierarchyUnit; +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; @@ -40,12 +43,12 @@ public class OsUserDirectory extends AbstractUserDirectory { } @Override - protected Boolean daoHasRole(LdapName dn) { + protected Boolean daoHasEntry(LdapName dn) { return osUserDn.equals(dn); } @Override - protected DirectoryUser daoGetRole(LdapName key) throws NameNotFoundException { + protected DirectoryUser daoGetEntry(LdapName key) throws NameNotFoundException { if (osUserDn.equals(key)) return osUser; else @@ -53,8 +56,8 @@ public class OsUserDirectory extends AbstractUserDirectory { } @Override - protected List doGetRoles(LdapName searchBase, Filter f, boolean deep) { - List res = new ArrayList<>(); + protected List doGetEntries(LdapName searchBase, Filter f, boolean deep) { + List res = new ArrayList<>(); if (f == null || f.match(osUser.getProperties())) res.add(osUser); return res; @@ -66,26 +69,25 @@ public class OsUserDirectory extends AbstractUserDirectory { } @Override - protected HierarchyUnit doGetHierarchyUnit(LdapName dn) { + public HierarchyUnit doGetHierarchyUnit(LdapName dn) { return null; } @Override - protected Iterable doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) { + public Iterable doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) { return new ArrayList<>(); } - public void prepare(DirectoryUserWorkingCopy wc) { + public void prepare(LdapEntryWorkingCopy wc) { } - public void commit(DirectoryUserWorkingCopy wc) { + public void commit(LdapEntryWorkingCopy wc) { } - public void rollback(DirectoryUserWorkingCopy wc) { + public void rollback(LdapEntryWorkingCopy wc) { } - } diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/UserAdminConf.java b/org.argeo.util/src/org/argeo/osgi/useradmin/UserAdminConf.java deleted file mode 100644 index 7fd9e1895..000000000 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/UserAdminConf.java +++ /dev/null @@ -1,246 +0,0 @@ -package org.argeo.osgi.useradmin; - -import java.net.InetAddress; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.UnknownHostException; -import java.util.Dictionary; -import java.util.Hashtable; -import java.util.List; -import java.util.Map; - -import javax.naming.Context; -import javax.naming.InvalidNameException; -import javax.naming.ldap.LdapName; - -import org.argeo.util.naming.NamingUtils; - -/** Properties used to configure user admins. */ -public enum UserAdminConf { - /** Base DN (cannot be configured externally) */ - baseDn("dc=example,dc=com"), - - /** URI of the underlying resource (cannot be configured externally) */ - uri("ldap://localhost:10389"), - - /** User objectClass */ - userObjectClass("inetOrgPerson"), - - /** Relative base DN for users */ - userBase("ou=People"), - - /** Groups objectClass */ - groupObjectClass("groupOfNames"), - - /** Relative base DN for users */ - groupBase("ou=Groups"), - - /** Relative base DN for users */ - systemRoleBase("ou=Roles"), - - /** Read-only source */ - readOnly(null), - - /** Disabled source */ - disabled(null), - - /** Authentication realm */ - realm(null), - - /** Override all passwords with this value (typically for testing purposes) */ - forcedPassword(null); - - public final static String FACTORY_PID = "org.argeo.osgi.useradmin.config"; - - public final static String SCHEME_LDAP = "ldap"; - public final static String SCHEME_LDAPS = "ldaps"; - public final static String SCHEME_FILE = "file"; - public final static String SCHEME_OS = "os"; - public final static String SCHEME_IPA = "ipa"; - - /** The default value. */ - private Object def; - - UserAdminConf(Object def) { - this.def = def; - } - - public Object getDefault() { - return def; - } - - /** - * For use as Java property. - * - * @deprecated use {@link #name()} instead - */ - @Deprecated - public String property() { - return name(); - } - - public String getValue(Dictionary properties) { - Object res = getRawValue(properties); - if (res == null) - return null; - return res.toString(); - } - - @SuppressWarnings("unchecked") - public T getRawValue(Dictionary properties) { - Object res = properties.get(name()); - if (res == null) - res = getDefault(); - return (T) res; - } - - /** @deprecated use {@link #valueOf(String)} instead */ - @Deprecated - public static UserAdminConf local(String property) { - return UserAdminConf.valueOf(property); - } - - /** Hides host and credentials. */ - public static URI propertiesAsUri(Dictionary properties) { - StringBuilder query = new StringBuilder(); - - boolean first = true; -// for (Enumeration keys = properties.keys(); keys.hasMoreElements();) { -// String key = keys.nextElement(); -// // TODO clarify which keys are relevant (list only the enum?) -// if (!key.equals("service.factoryPid") && !key.equals("cn") && !key.equals("dn") -// && !key.equals(Constants.SERVICE_PID) && !key.startsWith("java") && !key.equals(baseDn.name()) -// && !key.equals(uri.name()) && !key.equals(Constants.OBJECTCLASS) -// && !key.equals(Constants.SERVICE_ID) && !key.equals("bundle.id")) { -// if (first) -// first = false; -// else -// query.append('&'); -// query.append(valueOf(key).name()); -// query.append('=').append(properties.get(key).toString()); -// } -// } - - keys: for (UserAdminConf key : UserAdminConf.values()) { - if (key.equals(baseDn) || key.equals(uri)) - continue keys; - Object value = properties.get(key.name()); - if (value == null) - continue keys; - if (first) - first = false; - else - query.append('&'); - query.append(key.name()); - query.append('=').append(value.toString()); - - } - - Object bDnObj = properties.get(baseDn.name()); - String bDn = bDnObj != null ? bDnObj.toString() : null; - try { - return new URI(null, null, bDn != null ? '/' + bDn : null, query.length() != 0 ? query.toString() : null, - null); - } catch (URISyntaxException e) { - throw new IllegalArgumentException("Cannot create URI from properties", e); - } - } - - public static Dictionary uriAsProperties(String uriStr) { - try { - Hashtable res = new Hashtable(); - URI u = new URI(uriStr); - String scheme = u.getScheme(); - if (scheme != null && scheme.equals(SCHEME_IPA)) { - return IpaUtils.convertIpaUri(u); -// scheme = u.getScheme(); - } - String path = u.getPath(); - // base DN - String bDn = path.substring(path.lastIndexOf('/') + 1, path.length()); - if (bDn.equals("") && SCHEME_OS.equals(scheme)) { - bDn = getBaseDnFromHostname(); - } - - if (bDn.endsWith(".ldif")) - bDn = bDn.substring(0, bDn.length() - ".ldif".length()); - - // Normalize base DN as LDAP name - bDn = new LdapName(bDn).toString(); - - String principal = null; - String credentials = null; - if (scheme != null) - if (scheme.equals(SCHEME_LDAP) || scheme.equals(SCHEME_LDAPS)) { - // TODO additional checks - if (u.getUserInfo() != null) { - String[] userInfo = u.getUserInfo().split(":"); - principal = userInfo.length > 0 ? userInfo[0] : null; - credentials = userInfo.length > 1 ? userInfo[1] : null; - } - } else if (scheme.equals(SCHEME_FILE)) { - } else if (scheme.equals(SCHEME_IPA)) { - } else if (scheme.equals(SCHEME_OS)) { - } else - throw new IllegalArgumentException("Unsupported scheme " + scheme); - Map> query = NamingUtils.queryToMap(u); - for (String key : query.keySet()) { - UserAdminConf ldapProp = UserAdminConf.valueOf(key); - List values = query.get(key); - if (values.size() == 1) { - res.put(ldapProp.name(), values.get(0)); - } else { - throw new IllegalArgumentException("Only single values are supported"); - } - } - res.put(baseDn.name(), bDn); - if (SCHEME_OS.equals(scheme)) - res.put(readOnly.name(), "true"); - if (principal != null) - res.put(Context.SECURITY_PRINCIPAL, principal); - if (credentials != null) - res.put(Context.SECURITY_CREDENTIALS, credentials); - if (scheme != null) {// relative URIs are dealt with externally - if (SCHEME_OS.equals(scheme)) { - res.put(uri.name(), SCHEME_OS + ":///"); - } else { - URI bareUri = new URI(scheme, null, u.getHost(), u.getPort(), - scheme.equals(SCHEME_FILE) ? u.getPath() : null, null, null); - res.put(uri.name(), bareUri.toString()); - } - } - return res; - } catch (URISyntaxException | InvalidNameException e) { - throw new IllegalArgumentException("Cannot convert " + uri + " to properties", e); - } - } - - private static String getBaseDnFromHostname() { - String hostname; - try { - hostname = InetAddress.getLocalHost().getHostName(); - } catch (UnknownHostException e) { - hostname = "localhost.localdomain"; - } - int dotIdx = hostname.indexOf('.'); - if (dotIdx >= 0) { - String domain = hostname.substring(dotIdx + 1, hostname.length()); - String bDn = ("." + domain).replaceAll("\\.", ",dc="); - bDn = bDn.substring(1, bDn.length()); - return bDn; - } else { - return "dc=" + hostname; - } - } - - /** - * Hash the base DN in order to have a deterministic string to be used as a cn - * for the underlying user directory. - */ - public static String baseDnHash(Dictionary properties) { - String bDn = (String) properties.get(baseDn.name()); - if (bDn == null) - throw new IllegalStateException("No baseDn in " + properties); - return DigestUtils.sha1str(bDn); - } -} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/UserDirectory.java b/org.argeo.util/src/org/argeo/osgi/useradmin/UserDirectory.java index 2c070d66d..05ed7cf7c 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/UserDirectory.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/UserDirectory.java @@ -1,52 +1,19 @@ package org.argeo.osgi.useradmin; -import java.util.Optional; - -import org.argeo.util.transaction.WorkControl; +import org.argeo.util.directory.Directory; +import org.argeo.util.directory.HierarchyUnit; import org.osgi.service.useradmin.Role; /** Information about a user directory. */ -public interface UserDirectory { - /** - * The base of the hierarchy defined by this directory. This could typically be - * an LDAP base DN. - */ - String getContext(); - - String getName(); - -// /** The base DN of all entries in this user directory */ -// LdapName getBaseDn(); - -// /** The related {@link XAResource} */ -// XAResource getXaResource(); - - boolean isReadOnly(); - - boolean isDisabled(); - - String getUserObjectClass(); - -// String getUserBase(); - - String getGroupObjectClass(); - -// String getGroupBase(); - - Optional getRealm(); - - Iterable getDirectHierarchyUnits(boolean functionalOnly); - - HierarchyUnit getHierarchyUnit(String path); +public interface UserDirectory extends Directory { HierarchyUnit getHierarchyUnit(Role role); + Iterable getHierarchyUnitRoles(HierarchyUnit hierarchyUnit, String filter, boolean deep); + String getRolePath(Role role); String getRoleSimpleName(Role role); Role getRoleByPath(String path); - - @Deprecated - void setTransactionControl(WorkControl transactionControl); } diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/WcXaResource.java b/org.argeo.util/src/org/argeo/osgi/useradmin/WcXaResource.java deleted file mode 100644 index 32bb401bc..000000000 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/WcXaResource.java +++ /dev/null @@ -1,135 +0,0 @@ -package org.argeo.osgi.useradmin; - -import java.util.HashMap; -import java.util.Map; - -import javax.transaction.xa.XAException; -import javax.transaction.xa.XAResource; -import javax.transaction.xa.Xid; - -/** {@link XAResource} for a user directory being edited. */ -class WcXaResource implements XAResource { - private final AbstractUserDirectory userDirectory; - - private Map workingCopies = new HashMap(); - private Xid editingXid = null; - private int transactionTimeout = 0; - - public WcXaResource(AbstractUserDirectory userDirectory) { - this.userDirectory = userDirectory; - } - - @Override - public synchronized void start(Xid xid, int flags) throws XAException { - if (editingXid != null) - throw new IllegalStateException("Already editing " + editingXid); - DirectoryUserWorkingCopy wc = workingCopies.put(xid, new DirectoryUserWorkingCopy()); - if (wc != null) - throw new IllegalStateException("There is already a working copy for " + xid); - this.editingXid = xid; - } - - @Override - public void end(Xid xid, int flags) throws XAException { - checkXid(xid); - } - - private DirectoryUserWorkingCopy wc(Xid xid) { - return workingCopies.get(xid); - } - - synchronized DirectoryUserWorkingCopy wc() { - if (editingXid == null) - return null; - DirectoryUserWorkingCopy wc = workingCopies.get(editingXid); - if (wc == null) - throw new IllegalStateException("No working copy found for " + editingXid); - return wc; - } - - private synchronized void cleanUp(Xid xid) { - wc(xid).cleanUp(); - workingCopies.remove(xid); - editingXid = null; - } - - @Override - public int prepare(Xid xid) throws XAException { - checkXid(xid); - DirectoryUserWorkingCopy wc = wc(xid); - if (wc.noModifications()) - return XA_RDONLY; - try { - userDirectory.prepare(wc); - } catch (Exception e) { - e.printStackTrace(); - throw new XAException(XAException.XAER_RMERR); - } - return XA_OK; - } - - @Override - public void commit(Xid xid, boolean onePhase) throws XAException { - try { - checkXid(xid); - DirectoryUserWorkingCopy wc = wc(xid); - if (wc.noModifications()) - return; - if (onePhase) - userDirectory.prepare(wc); - userDirectory.commit(wc); - } catch (Exception e) { - e.printStackTrace(); - throw new XAException(XAException.XAER_RMERR); - } finally { - cleanUp(xid); - } - } - - @Override - public void rollback(Xid xid) throws XAException { - try { - checkXid(xid); - userDirectory.rollback(wc(xid)); - } catch (Exception e) { - e.printStackTrace(); - throw new XAException(XAException.XAER_RMERR); - } finally { - cleanUp(xid); - } - } - - @Override - public void forget(Xid xid) throws XAException { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isSameRM(XAResource xares) throws XAException { - return xares == this; - } - - @Override - public Xid[] recover(int flag) throws XAException { - return new Xid[0]; - } - - @Override - public int getTransactionTimeout() throws XAException { - return transactionTimeout; - } - - @Override - public boolean setTransactionTimeout(int seconds) throws XAException { - transactionTimeout = seconds; - return true; - } - - private void checkXid(Xid xid) throws XAException { - if (xid == null) - throw new XAException(XAException.XAER_OUTSIDE); - if (!xid.equals(xid)) - throw new XAException(XAException.XAER_NOTA); - } - -} diff --git a/org.argeo.util/src/org/argeo/util/directory/Directory.java b/org.argeo.util/src/org/argeo/util/directory/Directory.java new file mode 100644 index 000000000..b3dfa8b05 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/directory/Directory.java @@ -0,0 +1,36 @@ +package org.argeo.util.directory; + +import java.util.Optional; + +import org.argeo.util.transaction.WorkControl; + +public interface Directory { + /** + * The base of the hierarchy defined by this directory. This could typically be + * an LDAP base DN. + */ + String getContext(); + + String getName(); + + boolean isReadOnly(); + + boolean isDisabled(); + + String getUserObjectClass(); + + String getGroupObjectClass(); + + Optional getRealm(); + + void setTransactionControl(WorkControl transactionControl); + + /* + * HIERARCHY + */ + + Iterable getDirectHierarchyUnits(boolean functionalOnly); + + HierarchyUnit getHierarchyUnit(String path); + +} diff --git a/org.argeo.util/src/org/argeo/util/directory/DirectoryConf.java b/org.argeo.util/src/org/argeo/util/directory/DirectoryConf.java new file mode 100644 index 000000000..c0f96ee75 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/directory/DirectoryConf.java @@ -0,0 +1,246 @@ +package org.argeo.util.directory; + +import java.net.InetAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.UnknownHostException; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; + +import org.argeo.util.directory.ldap.IpaUtils; +import org.argeo.util.naming.NamingUtils; + +/** Properties used to configure user admins. */ +public enum DirectoryConf { + /** Base DN (cannot be configured externally) */ + baseDn("dc=example,dc=com"), + + /** URI of the underlying resource (cannot be configured externally) */ + uri("ldap://localhost:10389"), + + /** User objectClass */ + userObjectClass("inetOrgPerson"), + + /** Relative base DN for users */ + userBase("ou=People"), + + /** Groups objectClass */ + groupObjectClass("groupOfNames"), + + /** Relative base DN for users */ + groupBase("ou=Groups"), + + /** Relative base DN for users */ + systemRoleBase("ou=Roles"), + + /** Read-only source */ + readOnly(null), + + /** Disabled source */ + disabled(null), + + /** Authentication realm */ + realm(null), + + /** Override all passwords with this value (typically for testing purposes) */ + forcedPassword(null); + + public final static String FACTORY_PID = "org.argeo.osgi.useradmin.config"; + + public final static String SCHEME_LDAP = "ldap"; + public final static String SCHEME_LDAPS = "ldaps"; + public final static String SCHEME_FILE = "file"; + public final static String SCHEME_OS = "os"; + public final static String SCHEME_IPA = "ipa"; + + private final static String SECURITY_PRINCIPAL = "java.naming.security.principal"; + private final static String SECURITY_CREDENTIALS = "java.naming.security.credentials"; + + /** The default value. */ + private Object def; + + DirectoryConf(Object def) { + this.def = def; + } + + public Object getDefault() { + return def; + } + + /** + * For use as Java property. + * + * @deprecated use {@link #name()} instead + */ + @Deprecated + public String property() { + return name(); + } + + public String getValue(Dictionary properties) { + Object res = getRawValue(properties); + if (res == null) + return null; + return res.toString(); + } + + @SuppressWarnings("unchecked") + public T getRawValue(Dictionary properties) { + Object res = properties.get(name()); + if (res == null) + res = getDefault(); + return (T) res; + } + + /** @deprecated use {@link #valueOf(String)} instead */ + @Deprecated + public static DirectoryConf local(String property) { + return DirectoryConf.valueOf(property); + } + + /** Hides host and credentials. */ + public static URI propertiesAsUri(Dictionary properties) { + StringBuilder query = new StringBuilder(); + + boolean first = true; +// for (Enumeration keys = properties.keys(); keys.hasMoreElements();) { +// String key = keys.nextElement(); +// // TODO clarify which keys are relevant (list only the enum?) +// if (!key.equals("service.factoryPid") && !key.equals("cn") && !key.equals("dn") +// && !key.equals(Constants.SERVICE_PID) && !key.startsWith("java") && !key.equals(baseDn.name()) +// && !key.equals(uri.name()) && !key.equals(Constants.OBJECTCLASS) +// && !key.equals(Constants.SERVICE_ID) && !key.equals("bundle.id")) { +// if (first) +// first = false; +// else +// query.append('&'); +// query.append(valueOf(key).name()); +// query.append('=').append(properties.get(key).toString()); +// } +// } + + keys: for (DirectoryConf key : DirectoryConf.values()) { + if (key.equals(baseDn) || key.equals(uri)) + continue keys; + Object value = properties.get(key.name()); + if (value == null) + continue keys; + if (first) + first = false; + else + query.append('&'); + query.append(key.name()); + query.append('=').append(value.toString()); + + } + + Object bDnObj = properties.get(baseDn.name()); + String bDn = bDnObj != null ? bDnObj.toString() : null; + try { + return new URI(null, null, bDn != null ? '/' + bDn : null, query.length() != 0 ? query.toString() : null, + null); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Cannot create URI from properties", e); + } + } + + public static Dictionary uriAsProperties(String uriStr) { + try { + Hashtable res = new Hashtable(); + URI u = new URI(uriStr); + String scheme = u.getScheme(); + if (scheme != null && scheme.equals(SCHEME_IPA)) { + return IpaUtils.convertIpaUri(u); +// scheme = u.getScheme(); + } + String path = u.getPath(); + // base DN + String bDn = path.substring(path.lastIndexOf('/') + 1, path.length()); + if (bDn.equals("") && SCHEME_OS.equals(scheme)) { + bDn = getBaseDnFromHostname(); + } + + if (bDn.endsWith(".ldif")) + bDn = bDn.substring(0, bDn.length() - ".ldif".length()); + + // Normalize base DN as LDAP name +// bDn = new LdapName(bDn).toString(); + + String principal = null; + String credentials = null; + if (scheme != null) + if (scheme.equals(SCHEME_LDAP) || scheme.equals(SCHEME_LDAPS)) { + // TODO additional checks + if (u.getUserInfo() != null) { + String[] userInfo = u.getUserInfo().split(":"); + principal = userInfo.length > 0 ? userInfo[0] : null; + credentials = userInfo.length > 1 ? userInfo[1] : null; + } + } else if (scheme.equals(SCHEME_FILE)) { + } else if (scheme.equals(SCHEME_IPA)) { + } else if (scheme.equals(SCHEME_OS)) { + } else + throw new IllegalArgumentException("Unsupported scheme " + scheme); + Map> query = NamingUtils.queryToMap(u); + for (String key : query.keySet()) { + DirectoryConf ldapProp = DirectoryConf.valueOf(key); + List values = query.get(key); + if (values.size() == 1) { + res.put(ldapProp.name(), values.get(0)); + } else { + throw new IllegalArgumentException("Only single values are supported"); + } + } + res.put(baseDn.name(), bDn); + if (SCHEME_OS.equals(scheme)) + res.put(readOnly.name(), "true"); + if (principal != null) + res.put(SECURITY_PRINCIPAL, principal); + if (credentials != null) + res.put(SECURITY_CREDENTIALS, credentials); + if (scheme != null) {// relative URIs are dealt with externally + if (SCHEME_OS.equals(scheme)) { + res.put(uri.name(), SCHEME_OS + ":///"); + } else { + URI bareUri = new URI(scheme, null, u.getHost(), u.getPort(), + scheme.equals(SCHEME_FILE) ? u.getPath() : null, null, null); + res.put(uri.name(), bareUri.toString()); + } + } + return res; + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Cannot convert " + uri + " to properties", e); + } + } + + private static String getBaseDnFromHostname() { + String hostname; + try { + hostname = InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + hostname = "localhost.localdomain"; + } + int dotIdx = hostname.indexOf('.'); + if (dotIdx >= 0) { + String domain = hostname.substring(dotIdx + 1, hostname.length()); + String bDn = ("." + domain).replaceAll("\\.", ",dc="); + bDn = bDn.substring(1, bDn.length()); + return bDn; + } else { + return "dc=" + hostname; + } + } + + /** + * Hash the base DN in order to have a deterministic string to be used as a cn + * for the underlying user directory. + */ + public static String baseDnHash(Dictionary properties) { + String bDn = (String) properties.get(baseDn.name()); + if (bDn == null) + throw new IllegalStateException("No baseDn in " + properties); + return DirectoryDigestUtils.sha1str(bDn); + } +} diff --git a/org.argeo.util/src/org/argeo/util/directory/DirectoryDigestUtils.java b/org.argeo.util/src/org/argeo/util/directory/DirectoryDigestUtils.java new file mode 100644 index 000000000..d07d2d2ed --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/directory/DirectoryDigestUtils.java @@ -0,0 +1,114 @@ +package org.argeo.util.directory; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.Arrays; + +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; + +/** Utilities around digests, mostly those related to passwords. */ +public class DirectoryDigestUtils { + public final static String PASSWORD_SCHEME_SHA = "SHA"; + public final static String PASSWORD_SCHEME_PBKDF2_SHA256 = "PBKDF2_SHA256"; + + public static byte[] sha1(byte[] bytes) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA1"); + digest.update(bytes); + byte[] checksum = digest.digest(); + return checksum; + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Cannot SHA1 digest", e); + } + } + + public static byte[] toPasswordScheme(String passwordScheme, char[] password, byte[] salt, Integer iterations, + Integer keyLength) { + try { + if (PASSWORD_SCHEME_SHA.equals(passwordScheme)) { + MessageDigest digest = MessageDigest.getInstance("SHA1"); + byte[] bytes = charsToBytes(password); + digest.update(bytes); + return digest.digest(); + } else if (PASSWORD_SCHEME_PBKDF2_SHA256.equals(passwordScheme)) { + KeySpec spec = new PBEKeySpec(password, salt, iterations, keyLength); + + SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + final int ITERATION_LENGTH = 4; + byte[] key = f.generateSecret(spec).getEncoded(); + byte[] result = new byte[ITERATION_LENGTH + salt.length + key.length]; + byte iterationsArr[] = new BigInteger(iterations.toString()).toByteArray(); + if (iterationsArr.length < ITERATION_LENGTH) { + Arrays.fill(result, 0, ITERATION_LENGTH - iterationsArr.length, (byte) 0); + System.arraycopy(iterationsArr, 0, result, ITERATION_LENGTH - iterationsArr.length, + iterationsArr.length); + } else { + System.arraycopy(iterationsArr, 0, result, 0, ITERATION_LENGTH); + } + System.arraycopy(salt, 0, result, ITERATION_LENGTH, salt.length); + System.arraycopy(key, 0, result, ITERATION_LENGTH + salt.length, key.length); + return result; + } else { + throw new UnsupportedOperationException("Unkown password scheme " + passwordScheme); + } + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new IllegalStateException("Cannot digest", e); + } + } + + public static char[] bytesToChars(Object obj) { + if (obj instanceof char[]) + return (char[]) obj; + if (!(obj instanceof byte[])) + throw new IllegalArgumentException(obj.getClass() + " is not a byte array"); + ByteBuffer fromBuffer = ByteBuffer.wrap((byte[]) obj); + CharBuffer toBuffer = StandardCharsets.UTF_8.decode(fromBuffer); + char[] res = Arrays.copyOfRange(toBuffer.array(), toBuffer.position(), toBuffer.limit()); + // Arrays.fill(fromBuffer.array(), (byte) 0); // clear sensitive data + // Arrays.fill((byte[]) obj, (byte) 0); // clear sensitive data + // Arrays.fill(toBuffer.array(), '\u0000'); // clear sensitive data + return res; + } + + public static byte[] charsToBytes(char[] chars) { + CharBuffer charBuffer = CharBuffer.wrap(chars); + ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer); + byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit()); + // Arrays.fill(charBuffer.array(), '\u0000'); // clear sensitive data + // Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data + return bytes; + } + + public static String sha1str(String str) { + byte[] hash = sha1(str.getBytes(StandardCharsets.UTF_8)); + return encodeHexString(hash); + } + + final private static char[] hexArray = "0123456789abcdef".toCharArray(); + + /** + * From + * http://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to + * -a-hex-string-in-java + */ + public static String encodeHexString(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + + /** singleton */ + private DirectoryDigestUtils() { + } +} diff --git a/org.argeo.util/src/org/argeo/util/directory/HierarchyUnit.java b/org.argeo.util/src/org/argeo/util/directory/HierarchyUnit.java new file mode 100644 index 000000000..0194ffc89 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/directory/HierarchyUnit.java @@ -0,0 +1,18 @@ +package org.argeo.util.directory; + +/** A unit within the high-level organisational structure of a directory. */ +public interface HierarchyUnit { + String getHierarchyUnitName(); + + HierarchyUnit getParent(); + + Iterable getDirectHierachyUnits(boolean functionalOnly); + + boolean isFunctional(); + + String getContext(); + + Directory getDirectory(); + +// Map getHierarchyProperties(); +} 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 new file mode 100644 index 000000000..27f9c55e3 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapDirectory.java @@ -0,0 +1,376 @@ +package org.argeo.util.directory.ldap; + +import static org.argeo.util.directory.ldap.LdapNameUtils.toLdapName; + +import java.io.File; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.List; +import java.util.Optional; +import java.util.StringJoiner; + +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.ldap.LdapName; +import javax.naming.ldap.Rdn; +import javax.transaction.xa.XAResource; + +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 { + protected static final String SHARED_STATE_USERNAME = "javax.security.auth.login.name"; + protected static final String SHARED_STATE_PASSWORD = "javax.security.auth.login.password"; + + protected final LdapName baseDn; + protected final Hashtable properties; + private final Rdn userBaseRdn, groupBaseRdn, systemRoleBaseRdn; + private final String userObjectClass, groupObjectClass; + + private final boolean readOnly; + private final boolean disabled; + private final String uri; + + private String forcedPassword; + + 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); + + public AbstractLdapDirectory(URI uriArg, Dictionary props, boolean scoped) { + this.properties = new Hashtable(); + for (Enumeration keys = props.keys(); keys.hasMoreElements();) { + String key = keys.nextElement(); + properties.put(key, props.get(key)); + } + baseDn = toLdapName(DirectoryConf.baseDn.getValue(properties)); + this.scoped = scoped; + + if (uriArg != null) { + uri = uriArg.toString(); + // uri from properties is ignored + } else { + String uriStr = DirectoryConf.uri.getValue(properties); + if (uriStr == null) + uri = null; + else + uri = uriStr; + } + + forcedPassword = DirectoryConf.forcedPassword.getValue(properties); + + userObjectClass = DirectoryConf.userObjectClass.getValue(properties); + String userBase = DirectoryConf.userBase.getValue(properties); + groupObjectClass = DirectoryConf.groupObjectClass.getValue(properties); + String groupBase = DirectoryConf.groupBase.getValue(properties); + String systemRoleBase = DirectoryConf.systemRoleBase.getValue(properties); + try { +// baseDn = new LdapName(UserAdminConf.baseDn.getValue(properties)); + userBaseRdn = new Rdn(userBase); +// userBaseDn = new LdapName(userBase + "," + baseDn); + groupBaseRdn = new Rdn(groupBase); +// groupBaseDn = new LdapName(groupBase + "," + baseDn); + systemRoleBaseRdn = new Rdn(systemRoleBase); + } catch (InvalidNameException e) { + throw new IllegalArgumentException("Badly formated base DN " + DirectoryConf.baseDn.getValue(properties), + e); + } + + // read only + String readOnlyStr = DirectoryConf.readOnly.getValue(properties); + if (readOnlyStr == null) { + readOnly = readOnlyDefault(uri); + properties.put(DirectoryConf.readOnly.name(), Boolean.toString(readOnly)); + } else + readOnly = Boolean.parseBoolean(readOnlyStr); + + // disabled + String disabledStr = DirectoryConf.disabled.getValue(properties); + if (disabledStr != null) + disabled = Boolean.parseBoolean(disabledStr); + else + disabled = false; + } + + /* + * 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); + + /* + * EDITION + */ + + public boolean isEditing() { + return xaResource.wc() != null; + } + + public LdapEntryWorkingCopy getWorkingCopy() { + LdapEntryWorkingCopy wc = xaResource.wc(); + if (wc == null) + return null; + return wc; + } + + public void checkEdit() { + if (xaResource.wc() == null) { + try { + transactionControl.getWorkContext().registerXAResource(xaResource, null); + } catch (Exception e) { + throw new IllegalStateException("Cannot enlist " + xaResource, e); + } + } else { + } + } + + public void setTransactionControl(WorkControl transactionControl) { + this.transactionControl = transactionControl; + } + + public XAResource getXaResource() { + return xaResource; + } + + @Override + public LdapEntryWorkingCopy newWorkingCopy() { + return new LdapEntryWorkingCopy(); + } + + /* + * HIERARCHY + */ + @Override + public HierarchyUnit getHierarchyUnit(String path) { + LdapName dn = pathToName(path); + return doGetHierarchyUnit(dn); + } + + @Override + public Iterable getDirectHierarchyUnits(boolean functionalOnly) { + return doGetDirectHierarchyUnits(baseDn, functionalOnly); + } + + /* + * PATHS + */ + + @Override + public String getContext() { + return getBaseDn().toString(); + } + + @Override + public String getName() { + return nameToSimple(getBaseDn(), "."); + } + + protected String nameToRelativePath(LdapName dn) { + LdapName name = LdapNameUtils.relativeName(getBaseDn(), dn); + return nameToSimple(name, "/"); + } + + protected String nameToSimple(LdapName name, String separator) { + StringJoiner path = new StringJoiner(separator); + for (int i = 0; i < name.size(); i++) { + path.add(name.getRdn(i).getValue().toString()); + } + return path.toString(); + + } + + protected LdapName pathToName(String path) { + try { + LdapName name = (LdapName) getBaseDn().clone(); + String[] segments = path.split("/"); + Rdn parentRdn = null; + for (String segment : segments) { + // TODO make attr names configurable ? + String attr = LdapAttrs.ou.name(); + if (parentRdn != null) { + if (getUserBaseRdn().equals(parentRdn)) + attr = LdapAttrs.uid.name(); + else if (getGroupBaseRdn().equals(parentRdn)) + attr = LdapAttrs.cn.name(); + else if (getSystemRoleBaseRdn().equals(parentRdn)) + attr = LdapAttrs.cn.name(); + } + Rdn rdn = new Rdn(attr, segment); + name.add(rdn); + parentRdn = rdn; + } + return name; + } catch (InvalidNameException e) { + throw new IllegalStateException("Cannot get role " + path, e); + } + + } + + /* + * UTILITIES + */ + + protected static boolean hasObjectClass(Attributes attrs, LdapObjs 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())) + return true; + + } + return false; + } catch (NamingException e) { + throw new IllegalStateException("Cannot search for objectClass " + objectClass.name(), e); + } + } + + private static boolean readOnlyDefault(String uriStr) { + if (uriStr == null) + return true; + /// TODO make it more generic + URI uri; + try { + uri = new URI(uriStr.split(" ")[0]); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + if (uri.getScheme() == null) + return false;// assume relative file to be writable + if (uri.getScheme().equals(DirectoryConf.SCHEME_FILE)) { + File file = new File(uri); + if (file.exists()) + return !file.canWrite(); + else + return !file.getParentFile().canWrite(); + } else if (uri.getScheme().equals(DirectoryConf.SCHEME_LDAP)) { + if (uri.getAuthority() != null)// assume writable if authenticated + return false; + } else if (uri.getScheme().equals(DirectoryConf.SCHEME_OS)) { + return true; + } + return true;// read only by default + } + + /* + * ACCESSORS + */ + @Override + public Optional getRealm() { + Object realm = getProperties().get(DirectoryConf.realm.name()); + if (realm == null) + return Optional.empty(); + return Optional.of(realm.toString()); + } + + protected LdapName getBaseDn() { + return (LdapName) baseDn.clone(); + } + + public boolean isReadOnly() { + return readOnly; + } + + public boolean isDisabled() { + 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; + } + + public Rdn getSystemRoleBaseRdn() { + return systemRoleBaseRdn; + } + + public Dictionary getProperties() { + return properties; + } + + public Dictionary cloneProperties() { + return new Hashtable<>(properties); + } + + public String getForcedPassword() { + return forcedPassword; + } + + public boolean isScoped() { + return scoped; + } + + public String getMemberAttributeId() { + return memberAttributeId; + } + + public List getCredentialAttributeIds() { + return credentialAttributeIds; + } + + protected String getUri() { + return uri; + } + + /* + * OBJECT METHODS + */ + + @Override + public int hashCode() { + return baseDn.hashCode(); + } + + @Override + public String toString() { + return "Directory " + baseDn.toString(); + } + +} 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 new file mode 100644 index 000000000..be919c020 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapEntry.java @@ -0,0 +1,81 @@ +package org.argeo.util.directory.ldap; + +import javax.naming.directory.Attributes; +import javax.naming.ldap.LdapName; + +public abstract class AbstractLdapEntry implements LdapEntry { + private final AbstractLdapDirectory directory; + + private final LdapName dn; + + private Attributes publishedAttributes; + + protected AbstractLdapEntry(AbstractLdapDirectory userAdmin, LdapName dn, Attributes attributes) { + this.directory = userAdmin; + this.dn = dn; + this.publishedAttributes = attributes; + } + + @Override + public LdapName getDn() { + return dn; + } + + public synchronized Attributes getAttributes() { + return isEditing() ? getModifiedAttributes() : publishedAttributes; + } + + /** Should only be called from working copy thread. */ + protected synchronized Attributes getModifiedAttributes() { + assert getWc() != null; + return getWc().getModifiedData().get(getDn()); + } + + protected synchronized boolean isEditing() { + return getWc() != null && getModifiedAttributes() != null; + } + + private synchronized LdapEntryWorkingCopy getWc() { + return directory.getWorkingCopy(); + } + + protected synchronized void startEditing() { +// if (frozen) +// throw new IllegalStateException("Cannot edit frozen view"); + if (directory.isReadOnly()) + throw new IllegalStateException("User directory is read-only"); + assert getModifiedAttributes() == null; + getWc().startEditing(this); + // modifiedAttributes = (Attributes) publishedAttributes.clone(); + } + + public synchronized void publishAttributes(Attributes modifiedAttributes) { + publishedAttributes = modifiedAttributes; + } + + protected AbstractLdapDirectory getDirectory() { + return directory; + } + + @Override + public int hashCode() { + return dn.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj instanceof LdapEntry) { + LdapEntry that = (LdapEntry) obj; + return this.dn.equals(that.getDn()); + } + return false; + } + + @Override + public String toString() { + return dn.toString(); + } + +} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/AttributesDictionary.java b/org.argeo.util/src/org/argeo/util/directory/ldap/AttributesDictionary.java new file mode 100644 index 000000000..7b0095fbe --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/AttributesDictionary.java @@ -0,0 +1,171 @@ +package org.argeo.util.directory.ldap; + +import java.util.Dictionary; +import java.util.Enumeration; + +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.BasicAttribute; + +public class AttributesDictionary extends Dictionary { + private final Attributes attributes; + + /** The provided attributes is wrapped, not copied. */ + public AttributesDictionary(Attributes attributes) { + if (attributes == null) + throw new IllegalArgumentException("Attributes cannot be null"); + this.attributes = attributes; + } + + @Override + public int size() { + return attributes.size(); + } + + @Override + public boolean isEmpty() { + return attributes.size() == 0; + } + + @Override + public Enumeration keys() { + NamingEnumeration namingEnumeration = attributes.getIDs(); + return new Enumeration() { + + @Override + public boolean hasMoreElements() { + return namingEnumeration.hasMoreElements(); + } + + @Override + public String nextElement() { + return namingEnumeration.nextElement(); + } + + }; + } + + @Override + public Enumeration elements() { + NamingEnumeration namingEnumeration = attributes.getIDs(); + return new Enumeration() { + + @Override + public boolean hasMoreElements() { + return namingEnumeration.hasMoreElements(); + } + + @Override + public Object nextElement() { + String key = namingEnumeration.nextElement(); + return get(key); + } + + }; + } + + @Override + /** @returns a String or String[] */ + public Object get(Object key) { + try { + if (key == null) + throw new IllegalArgumentException("Key cannot be null"); + Attribute attr = attributes.get(key.toString()); + if (attr == null) + return null; + if (attr.size() == 0) + throw new IllegalStateException("There must be at least one value"); + else if (attr.size() == 1) { + return attr.get().toString(); + } else {// multiple + String[] res = new String[attr.size()]; + for (int i = 0; i < attr.size(); i++) { + Object value = attr.get(); + if (value == null) + throw new RuntimeException("Values cannot be null"); + res[i] = attr.get(i).toString(); + } + return res; + } + } catch (NamingException e) { + throw new RuntimeException("Cannot get value for " + key, e); + } + } + + @Override + public Object put(String key, Object value) { + if (key == null) + throw new IllegalArgumentException("Key cannot be null"); + if (value == null) + throw new IllegalArgumentException("Value cannot be null"); + + Object oldValue = get(key); + Attribute attr = attributes.get(key); + if (attr == null) { + attr = new BasicAttribute(key); + attributes.put(attr); + } + + if (value instanceof String[]) { + String[] values = (String[]) value; + // clean additional values + for (int i = values.length; i < attr.size(); i++) + attr.remove(i); + // set values + for (int i = 0; i < values.length; i++) { + attr.set(i, values[i]); + } + } else { + if (attr.size() > 1) + throw new IllegalArgumentException("Attribute " + key + " is multi-valued"); + if (attr.size() == 1) { + try { + if (!attr.get(0).equals(value)) + attr.set(0, value.toString()); + } catch (NamingException e) { + throw new RuntimeException("Cannot check existing value", e); + } + } else { + attr.add(value.toString()); + } + } + return oldValue; + } + + @Override + public Object remove(Object key) { + if (key == null) + throw new IllegalArgumentException("Key cannot be null"); + Object oldValue = get(key); + if (oldValue == null) + return null; + return attributes.remove(key.toString()); + } + + /** + * Copy the content of an {@link Attributes} to the provided + * {@link Dictionary}. + */ + public static void copy(Attributes attributes, Dictionary dictionary) { + AttributesDictionary ad = new AttributesDictionary(attributes); + Enumeration keys = ad.keys(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + dictionary.put(key, ad.get(key)); + } + } + + /** + * Copy a {@link Dictionary} into an {@link Attributes}. + */ + public static void copy(Dictionary dictionary, Attributes attributes) { + AttributesDictionary ad = new AttributesDictionary(attributes); + Enumeration keys = dictionary.keys(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + ad.put(key, dictionary.get(key)); + } + } +} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/AuthPassword.java b/org.argeo.util/src/org/argeo/util/directory/ldap/AuthPassword.java new file mode 100644 index 000000000..e10f45756 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/AuthPassword.java @@ -0,0 +1,140 @@ +package org.argeo.util.directory.ldap; + +import java.io.IOException; +import java.util.Arrays; +import java.util.StringTokenizer; + +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; + +import org.argeo.util.naming.LdapAttrs; + +/** LDAP authPassword field according to RFC 3112 */ +public class AuthPassword implements CallbackHandler { + private final String authScheme; + private final String authInfo; + private final String authValue; + + public AuthPassword(String value) { + StringTokenizer st = new StringTokenizer(value, "$"); + // TODO make it more robust, deal with bad formatting + this.authScheme = st.nextToken().trim(); + this.authInfo = st.nextToken().trim(); + this.authValue = st.nextToken().trim(); + + String expectedAuthScheme = getExpectedAuthScheme(); + if (expectedAuthScheme != null && !authScheme.equals(expectedAuthScheme)) + throw new IllegalArgumentException( + "Auth scheme " + authScheme + " is not compatible with " + expectedAuthScheme); + } + + protected AuthPassword(String authInfo, String authValue) { + this.authScheme = getExpectedAuthScheme(); + if (authScheme == null) + throw new IllegalArgumentException("Expected auth scheme cannot be null"); + this.authInfo = authInfo; + this.authValue = authValue; + } + + protected AuthPassword(AuthPassword authPassword) { + this.authScheme = authPassword.getAuthScheme(); + this.authInfo = authPassword.getAuthInfo(); + this.authValue = authPassword.getAuthValue(); + } + + protected String getExpectedAuthScheme() { + return null; + } + + protected boolean matchAuthValue(Object object) { + return authValue.equals(object.toString()); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof AuthPassword)) + return false; + AuthPassword authPassword = (AuthPassword) obj; + return authScheme.equals(authPassword.authScheme) && authInfo.equals(authPassword.authInfo) + && authValue.equals(authValue); + } + + public boolean keyEquals(AuthPassword authPassword) { + return authScheme.equals(authPassword.authScheme) && authInfo.equals(authPassword.authInfo); + } + + @Override + public int hashCode() { + return authValue.hashCode(); + } + + @Override + public String toString() { + return toAuthPassword(); + } + + public final String toAuthPassword() { + return getAuthScheme() + '$' + authInfo + '$' + authValue; + } + + public String getAuthScheme() { + return authScheme; + } + + public String getAuthInfo() { + return authInfo; + } + + public String getAuthValue() { + return authValue; + } + + public static AuthPassword matchAuthValue(Attributes attributes, char[] value) { + try { + Attribute authPassword = attributes.get(LdapAttrs.authPassword.name()); + if (authPassword != null) { + NamingEnumeration values = authPassword.getAll(); + while (values.hasMore()) { + Object val = values.next(); + AuthPassword token = new AuthPassword(val.toString()); + String auth; + if (Arrays.binarySearch(value, '$') >= 0) { + auth = token.authInfo + '$' + token.authValue; + } else { + auth = token.authValue; + } + if (Arrays.equals(auth.toCharArray(), value)) + return token; + // if (token.matchAuthValue(value)) + // return token; + } + } + return null; + } catch (NamingException e) { + throw new IllegalStateException("Cannot check attribute", e); + } + } + + public static boolean remove(Attributes attributes, AuthPassword value) { + Attribute authPassword = attributes.get(LdapAttrs.authPassword.name()); + return authPassword.remove(value.toAuthPassword()); + } + + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof NameCallback) + ((NameCallback) callback).setName(toAuthPassword()); + else if (callback instanceof PasswordCallback) + ((PasswordCallback) callback).setPassword(getAuthValue().toCharArray()); + } + } + +} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/IpaUtils.java b/org.argeo.util/src/org/argeo/util/directory/ldap/IpaUtils.java new file mode 100644 index 000000000..861eb4f1f --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/IpaUtils.java @@ -0,0 +1,140 @@ +package org.argeo.util.directory.ldap; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; + +import javax.naming.InvalidNameException; +import javax.naming.NamingException; +import javax.naming.ldap.LdapName; + +import org.argeo.util.directory.DirectoryConf; +import org.argeo.util.naming.LdapAttrs; +import org.argeo.util.naming.dns.DnsBrowser; + +/** Free IPA specific conventions. */ +public class IpaUtils { + public final static String IPA_USER_BASE = "cn=users,cn=accounts"; + public final static String IPA_GROUP_BASE = "cn=groups,cn=accounts"; + public final static String IPA_ROLE_BASE = "cn=roles,cn=accounts"; + public final static String IPA_SERVICE_BASE = "cn=services,cn=accounts"; + + private final static String KRB_PRINCIPAL_NAME = LdapAttrs.krbPrincipalName.name().toLowerCase(); + + public final static String IPA_USER_DIRECTORY_CONFIG = DirectoryConf.userBase + "=" + IPA_USER_BASE + "&" + + DirectoryConf.groupBase + "=" + IPA_GROUP_BASE + "&" + DirectoryConf.readOnly + "=true"; + + @Deprecated + static String domainToUserDirectoryConfigPath(String realm) { + return domainToBaseDn(realm) + "?" + IPA_USER_DIRECTORY_CONFIG + "&" + DirectoryConf.realm.name() + "=" + realm; + } + + public static void addIpaConfig(String realm, Dictionary properties) { + properties.put(DirectoryConf.baseDn.name(), domainToBaseDn(realm)); + properties.put(DirectoryConf.realm.name(), realm); + properties.put(DirectoryConf.userBase.name(), IPA_USER_BASE); + properties.put(DirectoryConf.groupBase.name(), IPA_GROUP_BASE); + properties.put(DirectoryConf.systemRoleBase.name(), IPA_ROLE_BASE); + properties.put(DirectoryConf.readOnly.name(), Boolean.TRUE.toString()); + } + + public static String domainToBaseDn(String domain) { + String[] dcs = domain.split("\\."); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < dcs.length; i++) { + if (i != 0) + sb.append(','); + String dc = dcs[i]; + sb.append(LdapAttrs.dc.name()).append('=').append(dc.toLowerCase()); + } + return sb.toString(); + } + + public static LdapName kerberosToDn(String kerberosName) { + String[] kname = kerberosName.split("@"); + String username = kname[0]; + String baseDn = domainToBaseDn(kname[1]); + String dn; + if (!username.contains("/")) + dn = LdapAttrs.uid + "=" + username + "," + IPA_USER_BASE + "," + baseDn; + else + dn = KRB_PRINCIPAL_NAME + "=" + kerberosName + "," + IPA_SERVICE_BASE + "," + baseDn; + try { + return new LdapName(dn); + } catch (InvalidNameException e) { + throw new IllegalArgumentException("Badly formatted name for " + kerberosName + ": " + dn); + } + } + + private IpaUtils() { + + } + + public static String kerberosDomainFromDns() { + String kerberosDomain; + try (DnsBrowser dnsBrowser = new DnsBrowser()) { + InetAddress localhost = InetAddress.getLocalHost(); + String hostname = localhost.getHostName(); + String dnsZone = hostname.substring(hostname.indexOf('.') + 1); + kerberosDomain = dnsBrowser.getRecord("_kerberos." + dnsZone, "TXT"); + return kerberosDomain; + } catch (NamingException | IOException e) { + throw new IllegalStateException("Cannot determine Kerberos domain from DNS", e); + } + + } + + public static Dictionary convertIpaUri(URI uri) { + String path = uri.getPath(); + String kerberosRealm; + if (path == null || path.length() <= 1) { + kerberosRealm = kerberosDomainFromDns(); + } else { + kerberosRealm = path.substring(1); + } + + if (kerberosRealm == null) + throw new IllegalStateException("No Kerberos domain available for " + uri); + // TODO intergrate CA certificate in truststore + // String schemeToUse = SCHEME_LDAPS; + String schemeToUse = DirectoryConf.SCHEME_LDAP; + List ldapHosts; + String ldapHostsStr = uri.getHost(); + if (ldapHostsStr == null || ldapHostsStr.trim().equals("")) { + try (DnsBrowser dnsBrowser = new DnsBrowser()) { + ldapHosts = dnsBrowser.getSrvRecordsAsHosts("_ldap._tcp." + kerberosRealm.toLowerCase(), + schemeToUse.equals(DirectoryConf.SCHEME_LDAP) ? true : false); + if (ldapHosts == null || ldapHosts.size() == 0) { + throw new IllegalStateException("Cannot configure LDAP for IPA " + uri); + } else { + ldapHostsStr = ldapHosts.get(0); + } + } catch (NamingException | IOException e) { + throw new IllegalStateException("Cannot convert IPA uri " + uri, e); + } + } else { + ldapHosts = new ArrayList<>(); + ldapHosts.add(ldapHostsStr); + } + + StringBuilder uriStr = new StringBuilder(); + try { + for (String host : ldapHosts) { + URI convertedUri = new URI(schemeToUse + "://" + host + "/"); + uriStr.append(convertedUri).append(' '); + } + } catch (URISyntaxException e) { + throw new IllegalStateException("Cannot convert IPA uri " + uri, e); + } + + Hashtable res = new Hashtable<>(); + res.put(DirectoryConf.uri.name(), uriStr.toString()); + addIpaConfig(kerberosRealm, res); + return res; + } +} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapConnection.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapConnection.java new file mode 100644 index 000000000..f7838381d --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapConnection.java @@ -0,0 +1,148 @@ +package org.argeo.util.directory.ldap; + +import java.util.Dictionary; +import java.util.Hashtable; + +import javax.naming.CommunicationException; +import javax.naming.Context; +import javax.naming.NameNotFoundException; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; +import javax.naming.ldap.InitialLdapContext; +import javax.naming.ldap.LdapName; + +import org.argeo.util.naming.LdapAttrs; +import org.argeo.util.transaction.WorkingCopy; + +/** A synchronized wrapper for a single {@link InitialLdapContext}. */ +// TODO implement multiple contexts and connection pooling. +public class LdapConnection { + private InitialLdapContext initialLdapContext = null; + + public LdapConnection(String url, Dictionary properties) { + try { + Hashtable connEnv = new Hashtable(); + connEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + connEnv.put(Context.PROVIDER_URL, url); + connEnv.put("java.naming.ldap.attributes.binary", LdapAttrs.userPassword.name()); + // use pooling in order to avoid connection timeout +// connEnv.put("com.sun.jndi.ldap.connect.pool", "true"); +// connEnv.put("com.sun.jndi.ldap.connect.pool.timeout", 300000); + + initialLdapContext = new InitialLdapContext(connEnv, null); + // StartTlsResponse tls = (StartTlsResponse) ctx + // .extendedOperation(new StartTlsRequest()); + // tls.negotiate(); + Object securityAuthentication = properties.get(Context.SECURITY_AUTHENTICATION); + if (securityAuthentication != null) + initialLdapContext.addToEnvironment(Context.SECURITY_AUTHENTICATION, securityAuthentication); + else + initialLdapContext.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple"); + Object principal = properties.get(Context.SECURITY_PRINCIPAL); + if (principal != null) { + initialLdapContext.addToEnvironment(Context.SECURITY_PRINCIPAL, principal.toString()); + Object creds = properties.get(Context.SECURITY_CREDENTIALS); + if (creds != null) { + initialLdapContext.addToEnvironment(Context.SECURITY_CREDENTIALS, creds.toString()); + } + } + } catch (NamingException e) { + throw new IllegalStateException("Cannot connect to LDAP", e); + } + + } + + public void init() { + + } + + public void destroy() { + try { + // tls.close(); + initialLdapContext.close(); + initialLdapContext = null; + } catch (NamingException e) { + e.printStackTrace(); + } + } + + protected InitialLdapContext getLdapContext() { + return initialLdapContext; + } + + protected void reconnect() throws NamingException { + initialLdapContext.reconnect(initialLdapContext.getConnectControls()); + } + + public synchronized NamingEnumeration search(LdapName searchBase, String searchFilter, + SearchControls searchControls) throws NamingException { + NamingEnumeration results; + try { + results = getLdapContext().search(searchBase, searchFilter, searchControls); + } catch (CommunicationException e) { + reconnect(); + results = getLdapContext().search(searchBase, searchFilter, searchControls); + } + return results; + } + + public synchronized Attributes getAttributes(LdapName name) throws NamingException { + try { + return getLdapContext().getAttributes(name); + } catch (CommunicationException e) { + reconnect(); + return getLdapContext().getAttributes(name); + } + } + + public synchronized void prepareChanges(WorkingCopy wc) throws NamingException { + // make sure connection will work + reconnect(); + + // delete + for (LdapName dn : wc.getDeletedData().keySet()) { + if (!entryExists(dn)) + throw new IllegalStateException("User to delete no found " + dn); + } + // add + for (LdapName dn : wc.getNewData().keySet()) { + if (entryExists(dn)) + throw new IllegalStateException("User to create found " + dn); + } + // modify + for (LdapName dn : wc.getModifiedData().keySet()) { + if (!wc.getNewData().containsKey(dn) && !entryExists(dn)) + throw new IllegalStateException("User to modify not found " + dn); + } + + } + + protected boolean entryExists(LdapName dn) throws NamingException { + try { + return getAttributes(dn).size() != 0; + } catch (NameNotFoundException e) { + return false; + } + } + + public synchronized void commitChanges(LdapEntryWorkingCopy wc) throws NamingException { + // delete + for (LdapName dn : wc.getDeletedData().keySet()) { + getLdapContext().destroySubcontext(dn); + } + // add + for (LdapName dn : wc.getNewData().keySet()) { + LdapEntry user = wc.getNewData().get(dn); + getLdapContext().createSubcontext(dn, user.getAttributes()); + } + // modify + for (LdapName dn : wc.getModifiedData().keySet()) { + Attributes modifiedAttrs = wc.getModifiedData().get(dn); + getLdapContext().modifyAttributes(dn, DirContext.REPLACE_ATTRIBUTE, modifiedAttrs); + } + } +} 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 new file mode 100644 index 000000000..c145a6f0a --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapEntry.java @@ -0,0 +1,13 @@ +package org.argeo.util.directory.ldap; + +import javax.naming.directory.Attributes; +import javax.naming.ldap.LdapName; + +public interface LdapEntry { + LdapName getDn(); + + Attributes getAttributes(); + + void publishAttributes(Attributes modifiedAttributes); + +} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapEntryWorkingCopy.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapEntryWorkingCopy.java new file mode 100644 index 000000000..381c11b2f --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapEntryWorkingCopy.java @@ -0,0 +1,19 @@ +package org.argeo.util.directory.ldap; + +import javax.naming.directory.Attributes; +import javax.naming.ldap.LdapName; + +import org.argeo.util.transaction.AbstractWorkingCopy; + +/** Working copy for a user directory being edited. */ +public class LdapEntryWorkingCopy extends AbstractWorkingCopy { + @Override + protected LdapName getId(LdapEntry entry) { + return entry.getDn(); + } + + @Override + protected Attributes cloneAttributes(LdapEntry entry) { + return (Attributes) entry.getAttributes().clone(); + } +} 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 new file mode 100644 index 000000000..d76c449b0 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapHierarchyUnit.java @@ -0,0 +1,98 @@ +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; + 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; + + Rdn rdn = LdapNameUtils.getLastRdn(dn); + functional = !(directory.getUserBaseRdn().equals(rdn) || directory.getGroupBaseRdn().equals(rdn) + || directory.getSystemRoleBaseRdn().equals(rdn)); + } + + @Override + public HierarchyUnit getParent() { + return directory.doGetHierarchyUnit(LdapNameUtils.getParent(dn)); + } + + @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); + } + + @Override + public boolean isFunctional() { + return functional; + } + + @Override + public String getHierarchyUnitName() { + String name = LdapNameUtils.getLastRdnValue(dn); + // 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); + } + + @Override + public String toString() { + return "Hierarchy Unit " + dn.toString(); + } + +} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapNameUtils.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapNameUtils.java new file mode 100644 index 000000000..689ef2329 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapNameUtils.java @@ -0,0 +1,65 @@ +package org.argeo.util.directory.ldap; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; + +/** Utilities to simplify using {@link LdapName}. */ +public class LdapNameUtils { + + public static LdapName relativeName(LdapName prefix, LdapName dn) { + try { + if (!dn.startsWith(prefix)) + throw new IllegalArgumentException("Prefix " + prefix + " not consistent with " + dn); + LdapName res = (LdapName) dn.clone(); + for (int i = 0; i < prefix.size(); i++) { + res.remove(0); + } + return res; + } catch (InvalidNameException e) { + throw new IllegalStateException("Cannot find realtive name", e); + } + } + + public static LdapName getParent(LdapName dn) { + try { + LdapName parent = (LdapName) dn.clone(); + parent.remove(parent.size() - 1); + return parent; + } catch (InvalidNameException e) { + throw new IllegalArgumentException("Cannot get parent of " + dn, e); + } + } + + public static Rdn getParentRdn(LdapName dn) { + if (dn.size() < 2) + throw new IllegalArgumentException(dn + " has no parent"); + Rdn parentRdn = dn.getRdn(dn.size() - 2); + return parentRdn; + } + + public static LdapName toLdapName(String distinguishedName) { + try { + return new LdapName(distinguishedName); + } catch (InvalidNameException e) { + throw new IllegalArgumentException("Cannot parse " + distinguishedName + " as LDAP name", e); + } + } + + public static Rdn getLastRdn(LdapName dn) { + return dn.getRdn(dn.size() - 1); + } + + public static String getLastRdnAsString(LdapName dn) { + return getLastRdn(dn).toString(); + } + + public static String getLastRdnValue(LdapName dn) { + return getLastRdn(dn).getValue().toString(); + } + + /** singleton */ + private LdapNameUtils() { + + } +} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdifParser.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdifParser.java new file mode 100644 index 000000000..0022943e1 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/LdifParser.java @@ -0,0 +1,161 @@ +package org.argeo.util.directory.ldap; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; + +import javax.naming.InvalidNameException; +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.naming.LdapAttrs; + +/** Basic LDIF parser. */ +public class LdifParser { + private final static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + + protected Attributes addAttributes(SortedMap res, int lineNumber, LdapName currentDn, + Attributes currentAttributes) { + try { + Rdn nameRdn = currentDn.getRdn(currentDn.size() - 1); + Attribute nameAttr = currentAttributes.get(nameRdn.getType()); + if (nameAttr == null) + currentAttributes.put(nameRdn.getType(), nameRdn.getValue()); + else if (!nameAttr.get().equals(nameRdn.getValue())) + throw new IllegalStateException( + "Attribute " + nameAttr.getID() + "=" + nameAttr.get() + " not consistent with DN " + currentDn + + " (shortly before line " + lineNumber + " in LDIF file)"); + Attributes previous = res.put(currentDn, currentAttributes); + return previous; + } catch (NamingException e) { + throw new IllegalStateException("Cannot add " + currentDn, e); + } + } + + /** With UTF-8 charset */ + public SortedMap read(InputStream in) throws IOException { + try (Reader reader = new InputStreamReader(in, DEFAULT_CHARSET)) { + return read(reader); + } finally { + try { + in.close(); + } catch (IOException e) { + // silent + } + } + } + + /** Will close the reader. */ + public SortedMap read(Reader reader) throws IOException { + SortedMap res = new TreeMap(); + try { + List lines = new ArrayList<>(); + try (BufferedReader br = new BufferedReader(reader)) { + String line; + while ((line = br.readLine()) != null) { + lines.add(line); + } + } + if (lines.size() == 0) + return res; + // add an empty new line since the last line is not checked + if (!lines.get(lines.size() - 1).equals("")) + lines.add(""); + + LdapName currentDn = null; + Attributes currentAttributes = null; + StringBuilder currentEntry = new StringBuilder(); + + readLines: for (int lineNumber = 0; lineNumber < lines.size(); lineNumber++) { + String line = lines.get(lineNumber); + boolean isLastLine = false; + if (lineNumber == lines.size() - 1) + isLastLine = true; + if (line.startsWith(" ")) { + currentEntry.append(line.substring(1)); + if (!isLastLine) + continue readLines; + } + + if (currentEntry.length() != 0 || isLastLine) { + // read previous attribute + StringBuilder attrId = new StringBuilder(8); + boolean isBase64 = false; + readAttrId: for (int i = 0; i < currentEntry.length(); i++) { + char c = currentEntry.charAt(i); + if (c == ':') { + if (i + 1 < currentEntry.length() && currentEntry.charAt(i + 1) == ':') + isBase64 = true; + currentEntry.delete(0, i + (isBase64 ? 2 : 1)); + break readAttrId; + } else { + attrId.append(c); + } + } + + String attributeId = attrId.toString(); + // TODO should we really trim the end of the string as well? + String cleanValueStr = currentEntry.toString().trim(); + Object attributeValue = isBase64 ? Base64.getDecoder().decode(cleanValueStr) : cleanValueStr; + + // manage DN attributes + if (attributeId.equals(LdapAttrs.DN) || isLastLine) { + if (currentDn != null) { + // + // ADD + // + Attributes previous = addAttributes(res, lineNumber, currentDn, currentAttributes); + if (previous != null) { +// log.warn("There was already an entry with DN " + currentDn +// + ", which has been discarded by a subsequent one."); + } + } + + if (attributeId.equals(LdapAttrs.DN)) + try { + currentDn = new LdapName(attributeValue.toString()); + currentAttributes = new BasicAttributes(true); + } catch (InvalidNameException e) { +// log.error(attributeValue + " not a valid DN, skipping the entry."); + currentDn = null; + currentAttributes = null; + } + } + + // store attribute + if (currentAttributes != null) { + Attribute attribute = currentAttributes.get(attributeId); + if (attribute == null) { + attribute = new BasicAttribute(attributeId); + currentAttributes.put(attribute); + } + attribute.add(attributeValue); + } + currentEntry = new StringBuilder(); + } + currentEntry.append(line); + } + } finally { + try { + reader.close(); + } catch (IOException e) { + // silent + } + } + return res; + } +} \ No newline at end of file diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdifWriter.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdifWriter.java new file mode 100644 index 000000000..a10f16938 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/LdifWriter.java @@ -0,0 +1,104 @@ +package org.argeo.util.directory.ldap; + +import static org.argeo.util.naming.LdapAttrs.DN; +import static org.argeo.util.naming.LdapAttrs.member; +import static org.argeo.util.naming.LdapAttrs.objectClass; +import static org.argeo.util.naming.LdapAttrs.uniqueMember; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; + +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; + +/** Basic LDIF writer */ +public class LdifWriter { + private final static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + private final Writer writer; + + /** Writer must be closed by caller */ + public LdifWriter(Writer writer) { + this.writer = writer; + } + + /** Stream must be closed by caller */ + public LdifWriter(OutputStream out) { + this(new OutputStreamWriter(out, DEFAULT_CHARSET)); + } + + public void writeEntry(LdapName name, Attributes attributes) throws IOException { + try { + // check consistency + Rdn nameRdn = name.getRdn(name.size() - 1); + Attribute nameAttr = attributes.get(nameRdn.getType()); + if (!nameAttr.get().equals(nameRdn.getValue())) + throw new IllegalArgumentException( + "Attribute " + nameAttr.getID() + "=" + nameAttr.get() + " not consistent with DN " + name); + + writer.append(DN + ": ").append(name.toString()).append('\n'); + Attribute objectClassAttr = attributes.get(objectClass.name()); + if (objectClassAttr != null) + writeAttribute(objectClassAttr); + attributes: for (NamingEnumeration attrs = attributes.getAll(); attrs.hasMore();) { + Attribute attribute = attrs.next(); + if (attribute.getID().equals(DN) || attribute.getID().equals(objectClass.name())) + continue attributes;// skip DN attribute + if (attribute.getID().equals(member.name()) || attribute.getID().equals(uniqueMember.name())) + continue attributes;// skip member and uniqueMember attributes, so that they are always written last + writeAttribute(attribute); + } + // write member and uniqueMember attributes last + for (NamingEnumeration attrs = attributes.getAll(); attrs.hasMore();) { + Attribute attribute = attrs.next(); + if (attribute.getID().equals(member.name()) || attribute.getID().equals(uniqueMember.name())) + writeMemberAttribute(attribute); + } + writer.append('\n'); + writer.flush(); + } catch (NamingException e) { + throw new IllegalStateException("Cannot write LDIF", e); + } + } + + public void write(Map entries) throws IOException { + for (LdapName dn : entries.keySet()) + writeEntry(dn, entries.get(dn)); + } + + protected void writeAttribute(Attribute attribute) throws NamingException, IOException { + for (NamingEnumeration attrValues = attribute.getAll(); attrValues.hasMore();) { + Object value = attrValues.next(); + if (value instanceof byte[]) { + String encoded = Base64.getEncoder().encodeToString((byte[]) value); + writer.append(attribute.getID()).append(":: ").append(encoded).append('\n'); + } else { + writer.append(attribute.getID()).append(": ").append(value.toString()).append('\n'); + } + } + } + + protected void writeMemberAttribute(Attribute attribute) throws NamingException, IOException { + // Note: duplicate entries will be swallowed + SortedSet values = new TreeSet<>(); + for (NamingEnumeration attrValues = attribute.getAll(); attrValues.hasMore();) { + String value = attrValues.next().toString(); + values.add(value); + } + + for (String value : values) { + writer.append(attribute.getID()).append(": ").append(value).append('\n'); + } + } +} diff --git a/org.argeo.util/src/org/argeo/util/naming/SharedSecret.java b/org.argeo.util/src/org/argeo/util/naming/SharedSecret.java index e38bc2f29..7661d4c51 100644 --- a/org.argeo.util/src/org/argeo/util/naming/SharedSecret.java +++ b/org.argeo.util/src/org/argeo/util/naming/SharedSecret.java @@ -4,7 +4,7 @@ import java.time.Instant; import java.time.ZoneOffset; import java.time.ZonedDateTime; -import org.argeo.util.naming.ldap.AuthPassword; +import org.argeo.util.directory.ldap.AuthPassword; public class SharedSecret extends AuthPassword { public final static String X_SHARED_SECRET = "X-SharedSecret"; diff --git a/org.argeo.util/src/org/argeo/util/naming/ldap/AttributesDictionary.java b/org.argeo.util/src/org/argeo/util/naming/ldap/AttributesDictionary.java deleted file mode 100644 index 0bbeb03fe..000000000 --- a/org.argeo.util/src/org/argeo/util/naming/ldap/AttributesDictionary.java +++ /dev/null @@ -1,171 +0,0 @@ -package org.argeo.util.naming.ldap; - -import java.util.Dictionary; -import java.util.Enumeration; - -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.directory.BasicAttribute; - -public class AttributesDictionary extends Dictionary { - private final Attributes attributes; - - /** The provided attributes is wrapped, not copied. */ - public AttributesDictionary(Attributes attributes) { - if (attributes == null) - throw new IllegalArgumentException("Attributes cannot be null"); - this.attributes = attributes; - } - - @Override - public int size() { - return attributes.size(); - } - - @Override - public boolean isEmpty() { - return attributes.size() == 0; - } - - @Override - public Enumeration keys() { - NamingEnumeration namingEnumeration = attributes.getIDs(); - return new Enumeration() { - - @Override - public boolean hasMoreElements() { - return namingEnumeration.hasMoreElements(); - } - - @Override - public String nextElement() { - return namingEnumeration.nextElement(); - } - - }; - } - - @Override - public Enumeration elements() { - NamingEnumeration namingEnumeration = attributes.getIDs(); - return new Enumeration() { - - @Override - public boolean hasMoreElements() { - return namingEnumeration.hasMoreElements(); - } - - @Override - public Object nextElement() { - String key = namingEnumeration.nextElement(); - return get(key); - } - - }; - } - - @Override - /** @returns a String or String[] */ - public Object get(Object key) { - try { - if (key == null) - throw new IllegalArgumentException("Key cannot be null"); - Attribute attr = attributes.get(key.toString()); - if (attr == null) - return null; - if (attr.size() == 0) - throw new IllegalStateException("There must be at least one value"); - else if (attr.size() == 1) { - return attr.get().toString(); - } else {// multiple - String[] res = new String[attr.size()]; - for (int i = 0; i < attr.size(); i++) { - Object value = attr.get(); - if (value == null) - throw new RuntimeException("Values cannot be null"); - res[i] = attr.get(i).toString(); - } - return res; - } - } catch (NamingException e) { - throw new RuntimeException("Cannot get value for " + key, e); - } - } - - @Override - public Object put(String key, Object value) { - if (key == null) - throw new IllegalArgumentException("Key cannot be null"); - if (value == null) - throw new IllegalArgumentException("Value cannot be null"); - - Object oldValue = get(key); - Attribute attr = attributes.get(key); - if (attr == null) { - attr = new BasicAttribute(key); - attributes.put(attr); - } - - if (value instanceof String[]) { - String[] values = (String[]) value; - // clean additional values - for (int i = values.length; i < attr.size(); i++) - attr.remove(i); - // set values - for (int i = 0; i < values.length; i++) { - attr.set(i, values[i]); - } - } else { - if (attr.size() > 1) - throw new IllegalArgumentException("Attribute " + key + " is multi-valued"); - if (attr.size() == 1) { - try { - if (!attr.get(0).equals(value)) - attr.set(0, value.toString()); - } catch (NamingException e) { - throw new RuntimeException("Cannot check existing value", e); - } - } else { - attr.add(value.toString()); - } - } - return oldValue; - } - - @Override - public Object remove(Object key) { - if (key == null) - throw new IllegalArgumentException("Key cannot be null"); - Object oldValue = get(key); - if (oldValue == null) - return null; - return attributes.remove(key.toString()); - } - - /** - * Copy the content of an {@link Attributes} to the provided - * {@link Dictionary}. - */ - public static void copy(Attributes attributes, Dictionary dictionary) { - AttributesDictionary ad = new AttributesDictionary(attributes); - Enumeration keys = ad.keys(); - while (keys.hasMoreElements()) { - String key = keys.nextElement(); - dictionary.put(key, ad.get(key)); - } - } - - /** - * Copy a {@link Dictionary} into an {@link Attributes}. - */ - public static void copy(Dictionary dictionary, Attributes attributes) { - AttributesDictionary ad = new AttributesDictionary(attributes); - Enumeration keys = dictionary.keys(); - while (keys.hasMoreElements()) { - String key = keys.nextElement(); - ad.put(key, dictionary.get(key)); - } - } -} diff --git a/org.argeo.util/src/org/argeo/util/naming/ldap/AuthPassword.java b/org.argeo.util/src/org/argeo/util/naming/ldap/AuthPassword.java deleted file mode 100644 index b11684020..000000000 --- a/org.argeo.util/src/org/argeo/util/naming/ldap/AuthPassword.java +++ /dev/null @@ -1,140 +0,0 @@ -package org.argeo.util.naming.ldap; - -import java.io.IOException; -import java.util.Arrays; -import java.util.StringTokenizer; - -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.security.auth.callback.Callback; -import javax.security.auth.callback.CallbackHandler; -import javax.security.auth.callback.NameCallback; -import javax.security.auth.callback.PasswordCallback; -import javax.security.auth.callback.UnsupportedCallbackException; - -import org.argeo.util.naming.LdapAttrs; - -/** LDAP authPassword field according to RFC 3112 */ -public class AuthPassword implements CallbackHandler { - private final String authScheme; - private final String authInfo; - private final String authValue; - - public AuthPassword(String value) { - StringTokenizer st = new StringTokenizer(value, "$"); - // TODO make it more robust, deal with bad formatting - this.authScheme = st.nextToken().trim(); - this.authInfo = st.nextToken().trim(); - this.authValue = st.nextToken().trim(); - - String expectedAuthScheme = getExpectedAuthScheme(); - if (expectedAuthScheme != null && !authScheme.equals(expectedAuthScheme)) - throw new IllegalArgumentException( - "Auth scheme " + authScheme + " is not compatible with " + expectedAuthScheme); - } - - protected AuthPassword(String authInfo, String authValue) { - this.authScheme = getExpectedAuthScheme(); - if (authScheme == null) - throw new IllegalArgumentException("Expected auth scheme cannot be null"); - this.authInfo = authInfo; - this.authValue = authValue; - } - - protected AuthPassword(AuthPassword authPassword) { - this.authScheme = authPassword.getAuthScheme(); - this.authInfo = authPassword.getAuthInfo(); - this.authValue = authPassword.getAuthValue(); - } - - protected String getExpectedAuthScheme() { - return null; - } - - protected boolean matchAuthValue(Object object) { - return authValue.equals(object.toString()); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof AuthPassword)) - return false; - AuthPassword authPassword = (AuthPassword) obj; - return authScheme.equals(authPassword.authScheme) && authInfo.equals(authPassword.authInfo) - && authValue.equals(authValue); - } - - public boolean keyEquals(AuthPassword authPassword) { - return authScheme.equals(authPassword.authScheme) && authInfo.equals(authPassword.authInfo); - } - - @Override - public int hashCode() { - return authValue.hashCode(); - } - - @Override - public String toString() { - return toAuthPassword(); - } - - public final String toAuthPassword() { - return getAuthScheme() + '$' + authInfo + '$' + authValue; - } - - public String getAuthScheme() { - return authScheme; - } - - public String getAuthInfo() { - return authInfo; - } - - public String getAuthValue() { - return authValue; - } - - public static AuthPassword matchAuthValue(Attributes attributes, char[] value) { - try { - Attribute authPassword = attributes.get(LdapAttrs.authPassword.name()); - if (authPassword != null) { - NamingEnumeration values = authPassword.getAll(); - while (values.hasMore()) { - Object val = values.next(); - AuthPassword token = new AuthPassword(val.toString()); - String auth; - if (Arrays.binarySearch(value, '$') >= 0) { - auth = token.authInfo + '$' + token.authValue; - } else { - auth = token.authValue; - } - if (Arrays.equals(auth.toCharArray(), value)) - return token; - // if (token.matchAuthValue(value)) - // return token; - } - } - return null; - } catch (NamingException e) { - throw new IllegalStateException("Cannot check attribute", e); - } - } - - public static boolean remove(Attributes attributes, AuthPassword value) { - Attribute authPassword = attributes.get(LdapAttrs.authPassword.name()); - return authPassword.remove(value.toAuthPassword()); - } - - @Override - public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { - for (Callback callback : callbacks) { - if (callback instanceof NameCallback) - ((NameCallback) callback).setName(toAuthPassword()); - else if (callback instanceof PasswordCallback) - ((PasswordCallback) callback).setPassword(getAuthValue().toCharArray()); - } - } - -} diff --git a/org.argeo.util/src/org/argeo/util/naming/ldap/LdifParser.java b/org.argeo.util/src/org/argeo/util/naming/ldap/LdifParser.java deleted file mode 100644 index 3c4ae0a85..000000000 --- a/org.argeo.util/src/org/argeo/util/naming/ldap/LdifParser.java +++ /dev/null @@ -1,161 +0,0 @@ -package org.argeo.util.naming.ldap; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Base64; -import java.util.List; -import java.util.SortedMap; -import java.util.TreeMap; - -import javax.naming.InvalidNameException; -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.naming.LdapAttrs; - -/** Basic LDIF parser. */ -public class LdifParser { - private final static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; - - protected Attributes addAttributes(SortedMap res, int lineNumber, LdapName currentDn, - Attributes currentAttributes) { - try { - Rdn nameRdn = currentDn.getRdn(currentDn.size() - 1); - Attribute nameAttr = currentAttributes.get(nameRdn.getType()); - if (nameAttr == null) - currentAttributes.put(nameRdn.getType(), nameRdn.getValue()); - else if (!nameAttr.get().equals(nameRdn.getValue())) - throw new IllegalStateException( - "Attribute " + nameAttr.getID() + "=" + nameAttr.get() + " not consistent with DN " + currentDn - + " (shortly before line " + lineNumber + " in LDIF file)"); - Attributes previous = res.put(currentDn, currentAttributes); - return previous; - } catch (NamingException e) { - throw new IllegalStateException("Cannot add " + currentDn, e); - } - } - - /** With UTF-8 charset */ - public SortedMap read(InputStream in) throws IOException { - try (Reader reader = new InputStreamReader(in, DEFAULT_CHARSET)) { - return read(reader); - } finally { - try { - in.close(); - } catch (IOException e) { - // silent - } - } - } - - /** Will close the reader. */ - public SortedMap read(Reader reader) throws IOException { - SortedMap res = new TreeMap(); - try { - List lines = new ArrayList<>(); - try (BufferedReader br = new BufferedReader(reader)) { - String line; - while ((line = br.readLine()) != null) { - lines.add(line); - } - } - if (lines.size() == 0) - return res; - // add an empty new line since the last line is not checked - if (!lines.get(lines.size() - 1).equals("")) - lines.add(""); - - LdapName currentDn = null; - Attributes currentAttributes = null; - StringBuilder currentEntry = new StringBuilder(); - - readLines: for (int lineNumber = 0; lineNumber < lines.size(); lineNumber++) { - String line = lines.get(lineNumber); - boolean isLastLine = false; - if (lineNumber == lines.size() - 1) - isLastLine = true; - if (line.startsWith(" ")) { - currentEntry.append(line.substring(1)); - if (!isLastLine) - continue readLines; - } - - if (currentEntry.length() != 0 || isLastLine) { - // read previous attribute - StringBuilder attrId = new StringBuilder(8); - boolean isBase64 = false; - readAttrId: for (int i = 0; i < currentEntry.length(); i++) { - char c = currentEntry.charAt(i); - if (c == ':') { - if (i + 1 < currentEntry.length() && currentEntry.charAt(i + 1) == ':') - isBase64 = true; - currentEntry.delete(0, i + (isBase64 ? 2 : 1)); - break readAttrId; - } else { - attrId.append(c); - } - } - - String attributeId = attrId.toString(); - // TODO should we really trim the end of the string as well? - String cleanValueStr = currentEntry.toString().trim(); - Object attributeValue = isBase64 ? Base64.getDecoder().decode(cleanValueStr) : cleanValueStr; - - // manage DN attributes - if (attributeId.equals(LdapAttrs.DN) || isLastLine) { - if (currentDn != null) { - // - // ADD - // - Attributes previous = addAttributes(res, lineNumber, currentDn, currentAttributes); - if (previous != null) { -// log.warn("There was already an entry with DN " + currentDn -// + ", which has been discarded by a subsequent one."); - } - } - - if (attributeId.equals(LdapAttrs.DN)) - try { - currentDn = new LdapName(attributeValue.toString()); - currentAttributes = new BasicAttributes(true); - } catch (InvalidNameException e) { -// log.error(attributeValue + " not a valid DN, skipping the entry."); - currentDn = null; - currentAttributes = null; - } - } - - // store attribute - if (currentAttributes != null) { - Attribute attribute = currentAttributes.get(attributeId); - if (attribute == null) { - attribute = new BasicAttribute(attributeId); - currentAttributes.put(attribute); - } - attribute.add(attributeValue); - } - currentEntry = new StringBuilder(); - } - currentEntry.append(line); - } - } finally { - try { - reader.close(); - } catch (IOException e) { - // silent - } - } - return res; - } -} \ No newline at end of file diff --git a/org.argeo.util/src/org/argeo/util/naming/ldap/LdifWriter.java b/org.argeo.util/src/org/argeo/util/naming/ldap/LdifWriter.java deleted file mode 100644 index 3e25dcfcb..000000000 --- a/org.argeo.util/src/org/argeo/util/naming/ldap/LdifWriter.java +++ /dev/null @@ -1,104 +0,0 @@ -package org.argeo.util.naming.ldap; - -import static org.argeo.util.naming.LdapAttrs.DN; -import static org.argeo.util.naming.LdapAttrs.member; -import static org.argeo.util.naming.LdapAttrs.objectClass; -import static org.argeo.util.naming.LdapAttrs.uniqueMember; - -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.Writer; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.Map; -import java.util.SortedSet; -import java.util.TreeSet; - -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.ldap.LdapName; -import javax.naming.ldap.Rdn; - -/** Basic LDIF writer */ -public class LdifWriter { - private final static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; - private final Writer writer; - - /** Writer must be closed by caller */ - public LdifWriter(Writer writer) { - this.writer = writer; - } - - /** Stream must be closed by caller */ - public LdifWriter(OutputStream out) { - this(new OutputStreamWriter(out, DEFAULT_CHARSET)); - } - - public void writeEntry(LdapName name, Attributes attributes) throws IOException { - try { - // check consistency - Rdn nameRdn = name.getRdn(name.size() - 1); - Attribute nameAttr = attributes.get(nameRdn.getType()); - if (!nameAttr.get().equals(nameRdn.getValue())) - throw new IllegalArgumentException( - "Attribute " + nameAttr.getID() + "=" + nameAttr.get() + " not consistent with DN " + name); - - writer.append(DN + ": ").append(name.toString()).append('\n'); - Attribute objectClassAttr = attributes.get(objectClass.name()); - if (objectClassAttr != null) - writeAttribute(objectClassAttr); - attributes: for (NamingEnumeration attrs = attributes.getAll(); attrs.hasMore();) { - Attribute attribute = attrs.next(); - if (attribute.getID().equals(DN) || attribute.getID().equals(objectClass.name())) - continue attributes;// skip DN attribute - if (attribute.getID().equals(member.name()) || attribute.getID().equals(uniqueMember.name())) - continue attributes;// skip member and uniqueMember attributes, so that they are always written last - writeAttribute(attribute); - } - // write member and uniqueMember attributes last - for (NamingEnumeration attrs = attributes.getAll(); attrs.hasMore();) { - Attribute attribute = attrs.next(); - if (attribute.getID().equals(member.name()) || attribute.getID().equals(uniqueMember.name())) - writeMemberAttribute(attribute); - } - writer.append('\n'); - writer.flush(); - } catch (NamingException e) { - throw new IllegalStateException("Cannot write LDIF", e); - } - } - - public void write(Map entries) throws IOException { - for (LdapName dn : entries.keySet()) - writeEntry(dn, entries.get(dn)); - } - - protected void writeAttribute(Attribute attribute) throws NamingException, IOException { - for (NamingEnumeration attrValues = attribute.getAll(); attrValues.hasMore();) { - Object value = attrValues.next(); - if (value instanceof byte[]) { - String encoded = Base64.getEncoder().encodeToString((byte[]) value); - writer.append(attribute.getID()).append(":: ").append(encoded).append('\n'); - } else { - writer.append(attribute.getID()).append(": ").append(value.toString()).append('\n'); - } - } - } - - protected void writeMemberAttribute(Attribute attribute) throws NamingException, IOException { - // Note: duplicate entries will be swallowed - SortedSet values = new TreeSet<>(); - for (NamingEnumeration attrValues = attribute.getAll(); attrValues.hasMore();) { - String value = attrValues.next().toString(); - values.add(value); - } - - for (String value : values) { - writer.append(attribute.getID()).append(": ").append(value).append('\n'); - } - } -} diff --git a/rap/org.argeo.cms.ui.rap/src/org/argeo/cms/web/AbstractCmsEntryPoint.java b/rap/org.argeo.cms.ui.rap/src/org/argeo/cms/web/AbstractCmsEntryPoint.java index 36ed3da7a..a4d04568e 100644 --- a/rap/org.argeo.cms.ui.rap/src/org/argeo/cms/web/AbstractCmsEntryPoint.java +++ b/rap/org.argeo.cms.ui.rap/src/org/argeo/cms/web/AbstractCmsEntryPoint.java @@ -34,8 +34,8 @@ import org.argeo.cms.swt.CmsStyles; import org.argeo.cms.swt.CmsSwtUtils; import org.argeo.eclipse.ui.specific.UiContext; import org.argeo.jcr.JcrUtils; +import org.argeo.util.directory.ldap.AuthPassword; import org.argeo.util.naming.SharedSecret; -import org.argeo.util.naming.ldap.AuthPassword; import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.application.AbstractEntryPoint; import org.eclipse.rap.rwt.client.WebClient;