Decorrelate directory implementation from user admin
authorMathieu Baudier <mbaudier@argeo.org>
Wed, 22 Jun 2022 08:05:00 +0000 (10:05 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Wed, 22 Jun 2022 08:05:00 +0000 (10:05 +0200)
59 files changed:
eclipse/org.argeo.cms.e4/src/org/argeo/cms/e4/users/UserAdminWrapper.java
eclipse/org.argeo.cms.e4/src/org/argeo/cms/e4/users/handlers/NewGroup.java
eclipse/org.argeo.cms.e4/src/org/argeo/cms/e4/users/handlers/NewUser.java
org.argeo.cms/OSGI-INF/cmsUserManager.xml
org.argeo.cms/OSGI-INF/nodeUserAdmin.xml
org.argeo.cms/OSGI-INF/simpleTransactionManager.xml
org.argeo.cms/bnd.bnd
org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContent.java
org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContentProvider.java
org.argeo.cms/src/org/argeo/cms/acr/directory/HierarchyUnitContent.java
org.argeo.cms/src/org/argeo/cms/acr/directory/RoleContent.java
org.argeo.cms/src/org/argeo/cms/auth/SingleUserLoginModule.java
org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java
org.argeo.cms/src/org/argeo/cms/internal/auth/CmsUserManagerImpl.java
org.argeo.cms/src/org/argeo/cms/internal/osgi/CmsOsgiLogger.java
org.argeo.cms/src/org/argeo/cms/internal/osgi/DeployConfig.java
org.argeo.cms/src/org/argeo/cms/internal/osgi/NodeUserAdmin.java
org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsUserAdmin.java
org.argeo.cms/src/org/argeo/cms/internal/runtime/InitUtils.java
org.argeo.util/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java
org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java
org.argeo.util/src/org/argeo/osgi/useradmin/AuthenticatingUser.java
org.argeo.util/src/org/argeo/osgi/useradmin/DigestUtils.java [deleted file]
org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUser.java
org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUserWorkingCopy.java [deleted file]
org.argeo.util/src/org/argeo/osgi/useradmin/HierarchyUnit.java [deleted file]
org.argeo.util/src/org/argeo/osgi/useradmin/IpaUtils.java [deleted file]
org.argeo.util/src/org/argeo/osgi/useradmin/LdapConnection.java [deleted file]
org.argeo.util/src/org/argeo/osgi/useradmin/LdapNameUtils.java [deleted file]
org.argeo.util/src/org/argeo/osgi/useradmin/LdapUserAdmin.java
org.argeo.util/src/org/argeo/osgi/useradmin/LdifHierarchyUnit.java [deleted file]
org.argeo.util/src/org/argeo/osgi/useradmin/LdifUser.java
org.argeo.util/src/org/argeo/osgi/useradmin/LdifUserAdmin.java
org.argeo.util/src/org/argeo/osgi/useradmin/OsUserDirectory.java
org.argeo.util/src/org/argeo/osgi/useradmin/UserAdminConf.java [deleted file]
org.argeo.util/src/org/argeo/osgi/useradmin/UserDirectory.java
org.argeo.util/src/org/argeo/osgi/useradmin/WcXaResource.java [deleted file]
org.argeo.util/src/org/argeo/util/directory/Directory.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/directory/DirectoryConf.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/directory/DirectoryDigestUtils.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/directory/HierarchyUnit.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapDirectory.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapEntry.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/directory/ldap/AttributesDictionary.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/directory/ldap/AuthPassword.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/directory/ldap/IpaUtils.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/directory/ldap/LdapConnection.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/directory/ldap/LdapEntry.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/directory/ldap/LdapEntryWorkingCopy.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/directory/ldap/LdapHierarchyUnit.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/directory/ldap/LdapNameUtils.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/directory/ldap/LdifParser.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/directory/ldap/LdifWriter.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/naming/SharedSecret.java
org.argeo.util/src/org/argeo/util/naming/ldap/AttributesDictionary.java [deleted file]
org.argeo.util/src/org/argeo/util/naming/ldap/AuthPassword.java [deleted file]
org.argeo.util/src/org/argeo/util/naming/ldap/LdifParser.java [deleted file]
org.argeo.util/src/org/argeo/util/naming/ldap/LdifWriter.java [deleted file]
rap/org.argeo.cms.ui.rap/src/org/argeo/cms/web/AbstractCmsEntryPoint.java

index cd8d4d7dc79e949a0f0a3e8704fb89adee195c46..1e0fc6eb33b99558cab7ba8db364bee242d43079 100644 (file)
@@ -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) {
index d2ffa791b18d3751fb56012b3603c4c5ee2793ff..f02a26b1c29a3b465aeaf53cc796f974241f357b 100644 (file)
@@ -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<String, String> dns = getDns();
                        String bdn = baseDnCmb.getText();
                        if (EclipseUiUtils.notEmpty(bdn)) {
-                               Dictionary<String, ?> props = UserAdminConf.uriAsProperties(dns.get(bdn));
-                               String dn = LdapAttrs.cn.name() + "=" + cn + "," + UserAdminConf.groupBase.getValue(props) + "," + bdn;
+                               Dictionary<String, ?> props = DirectoryConf.uriAsProperties(dns.get(bdn));
+                               String dn = LdapAttrs.cn.name() + "=" + cn + "," + DirectoryConf.groupBase.getValue(props) + "," + bdn;
                                return dn;
                        }
                        return null;
index 07d82c749ca320adeda6a5e9289d915858c9e6fc..24f7e6250ee27c9c81c035892d7f1ced7649189b 100644 (file)
@@ -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<String, String> dns = getDns();
                        String bdn = baseDnCmb.getText();
                        if (EclipseUiUtils.notEmpty(bdn)) {
-                               Dictionary<String, ?> props = UserAdminConf.uriAsProperties(dns.get(bdn));
-                               String dn = LdapAttrs.uid.name() + "=" + uid + "," + UserAdminConf.userBase.getValue(props) + "," + bdn;
+                               Dictionary<String, ?> props = DirectoryConf.uriAsProperties(dns.get(bdn));
+                               String dn = LdapAttrs.uid.name() + "=" + uid + "," + DirectoryConf.userBase.getValue(props) + "," + bdn;
                                return dn;
                        }
                        return null;
index 524c054eccf733b1a3a295cce1d9e64dc918d7d9..2e8f868aee577f8bc51873c9e944ce07896192de 100644 (file)
@@ -5,6 +5,6 @@
       <provide interface="org.argeo.cms.CmsUserManager"/>
    </service>
    <reference bind="setUserAdmin" cardinality="1..1" interface="org.osgi.service.useradmin.UserAdmin" name="UserAdmin" policy="static"/>
-   <reference bind="setUserTransaction" cardinality="1..1" interface="org.argeo.osgi.transaction.WorkTransaction" name="UserTransaction" policy="static"/>
+   <reference bind="setUserTransaction" cardinality="1..1" interface="org.argeo.util.transaction.WorkTransaction" name="UserTransaction" policy="static"/>
    <reference bind="addUserDirectory" cardinality="0..n" interface="org.argeo.osgi.useradmin.UserDirectory" name="UserDirectory" policy="static" unbind="removeUserDirectory"/>
 </scr:component>
index eb048d9f538f318530d227b38728e4566b246c3e..cae688b0a9ec0b9ca8dd5013eea7a65a5ac8ec78 100644 (file)
@@ -5,7 +5,7 @@
    <service>
       <provide interface="org.osgi.service.cm.ManagedServiceFactory"/>
    </service>
-   <reference bind="setTransactionManager" cardinality="1..1" interface="org.argeo.osgi.transaction.WorkControl" name="WorkControl" policy="static"/>
-   <reference bind="setUserTransaction" cardinality="1..1" interface="org.argeo.osgi.transaction.WorkTransaction" name="WorkTransaction" policy="static"/>
+   <reference bind="setTransactionManager" cardinality="1..1" interface="org.argeo.util.transaction.WorkControl" name="WorkControl" policy="static"/>
+   <reference bind="setUserTransaction" cardinality="1..1" interface="org.argeo.util.transaction.WorkTransaction" name="WorkTransaction" policy="static"/>
    <reference cardinality="1..1" interface="org.argeo.api.cms.CmsState" name="CmsState" policy="static"/>
 </scr:component>
index c331aa430f16ae01ad48821618124c5d7f24e720..81997476eece13220a0333f669dfc7b1d451fa21 100644 (file)
@@ -1,8 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="Simple Transaction Manager">
-   <implementation class="org.argeo.osgi.transaction.SimpleTransactionManager"/>
+   <implementation class="org.argeo.util.transaction.SimpleTransactionManager"/>
    <service>
-      <provide interface="org.argeo.osgi.transaction.WorkControl"/>
-      <provide interface="org.argeo.osgi.transaction.WorkTransaction"/>
+      <provide interface="org.argeo.util.transaction.WorkControl"/>
+      <provide interface="org.argeo.util.transaction.WorkTransaction"/>
    </service>
 </scr:component>
index 0d85a873ffe060b87883478968646ccc3d9743aa..52987f3da8e9aa51e17f334d5124a8e99439c0b8 100644 (file)
@@ -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,\
index eef7055df4fbd43048ac6696495c61bf077c661a..e1ad96077e26bfc08925b69f8a9df6b41188f2c6 100644 (file)
@@ -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;
index bd4117ead42d759914e82622b3ad6f8fc10dcbee..f4afbdd538e7de7b12b93d0fa7bbb166873e01d3 100644 (file)
@@ -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 {
index 78bc72f5d041912a68bfa87c741ed3c7c8acc382..f6a0e3b5266d30f6f1d411e2dd6357a41a6fd644 100644 (file)
@@ -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;
        }
 
-       
 }
index bf3b319f40bd4aa5f248e2cdb637b5cca74028e4..2a22f023cb5795f40efa7545a8a83a4ef1ca6187 100644 (file)
@@ -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);
        }
 
index 08380ac5a227cd165756e9b430cec4e6fd9c5e6d..972d6a2458a74b2edaa9438b6c3acbbd5e0e69ed 100644 (file)
@@ -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;
 
index ea7d1f37082bf17f335dc27b4c02a89ffc74266c..f6832ad35af4cfc86bad5c3ab6c9ad5ec3880fd5 100644 (file)
@@ -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;
index 9b7a2ed423c1331bcf7c8d05dcd0fb0a03fe923c..7db3cdc4cfef7923fee1b407af055e85777f4ad7 100644 (file)
@@ -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<String, String> dns = getKnownBaseDns(true);
-               Dictionary<String, ?> props = UserAdminConf.uriAsProperties(dns.get(baseDn));
+               Dictionary<String, ?> 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;
index 6898c4348fe098dc9113cfb25e62d1c39c98c6d2..c167af0250cada2eaa0651e21a44990268e4d3f1 100644 (file)
@@ -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();
index 71c34932700d3ade3c9f3e0124747264fe4de11e..b8fa8a73f49438bc15bbd439c185151f08157fbf 100644 (file)
@@ -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<String> activeCns = new ArrayList<>();
                        for (int i = 0; i < userDirectoryConfigs.size(); i++) {
                                Dictionary<String, Object> 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;
index 0746d4301d5b324ac61921a56c77d720e342c767..e534d9fe36fa9c6fc92481371aaeb8a4d68dc38a 100644 (file)
@@ -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<String, ?> 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);
index 965330237c6e7226ac19f0d6db79136ad74bf2da..64b25c99e80caf71a1ad90c054f2dabba673868c 100644 (file)
@@ -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<String, ?> 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 {
index 821808017644b9dcd17089cd426809b2d73c7a66..986f7914d03a7d56edbaea56d6edb31fe5e295b4 100644 (file)
@@ -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<String, Object> properties = UserAdminConf.uriAsProperties(u.toString());
+                       Dictionary<String, Object> properties = DirectoryConf.uriAsProperties(u.toString());
                        res.add(properties);
                }
 
index 838c2ce0b15f886234136ff1275c24de880e8b48..4b13728afaffa709563dd7d89fba4556d8f0ca48 100644 (file)
@@ -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<DirectoryUserWorkingCopy>, 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<String, Object> 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<String> indexedUserProperties = Arrays
        // .asList(new String[] { LdapAttrs.uid.name(), LdapAttrs.mail.name(),
        // LdapAttrs.cn.name() });
 
-       private final boolean scoped;
-
-       private String memberAttributeId = "member";
-       private List<String> credentialAttributeIds = Arrays
-                       .asList(new String[] { LdapAttrs.userPassword.name(), LdapAttrs.authPassword.name() });
-
        // Transaction
 //     private TransactionManager transactionManager;
-       private WorkControl transactionControl;
-       private WorkingCopyXaResource<DirectoryUserWorkingCopy> xaResource = new WorkingCopyXaResource<>(this);
-
-       private String forcedPassword;
-
        AbstractUserDirectory(URI uriArg, Dictionary<String, ?> props, boolean scoped) {
-               this.scoped = scoped;
-               properties = new Hashtable<String, Object>();
-               for (Enumeration<String> 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<LdapName> getDirectGroups(LdapName dn);
 
-       protected abstract Boolean daoHasRole(LdapName dn);
-
-       protected abstract DirectoryUser daoGetRole(LdapName key) throws NameNotFoundException;
-
-       protected abstract List<DirectoryUser> doGetRoles(LdapName searchBase, Filter f, boolean deep);
-
-       protected abstract AbstractUserDirectory scope(User user);
-
-       protected abstract HierarchyUnit doGetHierarchyUnit(LdapName dn);
-
-       protected abstract Iterable<HierarchyUnit> 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<String> 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<Role> getAllRoles(DirectoryUser user) {
                List<Role> allRoles = new ArrayList<Role>();
                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<DirectoryUser> getRoles(LdapName searchBase, String filter, boolean deep) throws InvalidSyntaxException {
-               DirectoryUserWorkingCopy wc = getWorkingCopy();
+               LdapEntryWorkingCopy wc = getWorkingCopy();
                Filter f = filter != null ? FrameworkUtil.createFilter(filter) : null;
-               List<DirectoryUser> res = doGetRoles(searchBase, f, deep);
+               List<LdapEntry> searchRes = doGetEntries(searchBase, f, deep);
+               List<DirectoryUser> res = new ArrayList<>();
+               for (LdapEntry entry : searchRes)
+                       res.add((DirectoryUser) entry);
                if (wc != null) {
                        for (Iterator<DirectoryUser> 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<DirectoryUser> collectedUsers) {
                try {
                        Filter f = FrameworkUtil.createFilter("(" + key + "=" + value + ")");
-                       List<DirectoryUser> users = doGetRoles(getBaseDn(), f, true);
-                       collectedUsers.addAll(users);
+                       List<LdapEntry> 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<HierarchyUnit> getDirectHierarchyUnits(boolean functionalOnly) {
-               return doGetDirectHierarchyUnits(baseDn, functionalOnly);
+       public Iterable<? extends Role> 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<String> 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<String, Object> getProperties() {
-               return properties;
-       }
-
-       public Dictionary<String, Object> 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
         */
index ac641de97cf6a128b5c2781f9c053cba8e98a7d7..955178ce4042a3ee9a9782260bcbfcb4b164d00a 100644 (file)
@@ -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());
                }
index 01db8be9895b9f3548728f2b6d5c580f684424e4..ba1f3f753470e530fe820c7991e9d4b8483e7812 100644 (file)
@@ -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 (file)
index 55d24d9..0000000
+++ /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() {
-       }
-}
index 146b8057805d122157abe15babd275869e275cd4..c82c5a01ddbc811606acf0774dd8f244075b9cfa 100644 (file)
@@ -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 (file)
index 2aed145..0000000
+++ /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<DirectoryUser, Attributes, LdapName> {
-       @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 (file)
index 2c21342..0000000
+++ /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<HierarchyUnit> getDirectHierachyUnits(boolean functionalOnly);
-
-       boolean isFunctional();
-
-       String getContext();
-
-       List<? extends Role> getHierarchyUnitRoles(String filter, boolean deep);
-
-       UserDirectory getDirectory();
-
-//     Map<String,Object> 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 (file)
index e1c8136..0000000
+++ /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<String, Object> 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<String, Object> 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<String> 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<String, Object> 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 (file)
index 1fe7eb9..0000000
+++ /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<String, ?> properties) {
-               try {
-                       Hashtable<String, Object> connEnv = new Hashtable<String, Object>();
-                       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<SearchResult> search(LdapName searchBase, String searchFilter,
-                       SearchControls searchControls) throws NamingException {
-               NamingEnumeration<SearchResult> 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 (file)
index 7e76345..0000000
+++ /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() {
-
-       }
-}
index 879d5da04991b0113383771ee43ae8aee4ac0829..36419d9606ac188cf608224d942b03d3085295b8 100644 (file)
@@ -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<DirectoryUser> doGetRoles(LdapName searchBase, Filter f, boolean deep) {
-               ArrayList<DirectoryUser> res = new ArrayList<DirectoryUser>();
+       protected List<LdapEntry> doGetEntries(LdapName searchBase, Filter f, boolean deep) {
+               ArrayList<LdapEntry> 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<HierarchyUnit> doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) {
+       public Iterable<HierarchyUnit> doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) {
                List<HierarchyUnit> 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 (file)
index a847e49..0000000
+++ /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<HierarchyUnit> 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<HierarchyUnit> getDirectHierachyUnits(boolean functionalOnly) {
-//             List<HierarchyUnit> 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<? extends Role> 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();
-       }
-
-}
index aaac50272a02cb605ec962521fcf77f9f873a06a..6cf6725ccb065640f172a96e8d04c0825d05eff5 100644 (file)
@@ -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<String, Object> {
@@ -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<String> 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();
 
index 26d3d134c511561016333f9273458967d7446fc9..c978af4a045302789b017f7055d1d347555c8d18 100644 (file)
@@ -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<LdapName, DirectoryUser> users = new TreeMap<>();
-       private NavigableMap<LdapName, DirectoryGroup> groups = new TreeMap<>();
+       private NavigableMap<LdapName, LdapEntry> users = new TreeMap<>();
+       private NavigableMap<LdapName, LdapEntry> groups = new TreeMap<>();
 
-       private NavigableMap<LdapName, LdifHierarchyUnit> hierarchy = new TreeMap<>();
+       private NavigableMap<LdapName, LdapHierarchyUnit> hierarchy = new TreeMap<>();
 //     private List<HierarchyUnit> 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<String, Object> 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<String, Object> fromUri(String uri, String baseDn) {
                Hashtable<String, Object> res = new Hashtable<String, Object>();
-               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<DirectoryUser> doGetRoles(LdapName searchBase, Filter f, boolean deep) {
+       protected List<LdapEntry> doGetEntries(LdapName searchBase, Filter f, boolean deep) {
                Objects.requireNonNull(searchBase);
-               ArrayList<DirectoryUser> res = new ArrayList<DirectoryUser>();
+               ArrayList<LdapEntry> 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<LdapName, ? extends DirectoryUser> map, LdapName searchBase, Filter f,
-                       boolean deep, List<DirectoryUser> res) {
+       private void filterRoles(SortedMap<LdapName, ? extends LdapEntry> map, LdapName searchBase, Filter f, boolean deep,
+                       List<LdapEntry> 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<LdapName> getDirectGroups(LdapName dn) {
                List<LdapName> directGroups = new ArrayList<LdapName>();
                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<HierarchyUnit> doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) {
+       public Iterable<HierarchyUnit> doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) {
                List<HierarchyUnit> res = new ArrayList<>();
                for (LdapName n : hierarchy.keySet()) {
                        if (n.size() == searchBase.size() + 1) {
index 69c06c8483a2d73f9735f06b3700705d32907079..1f428ecbd9841702011224d4a7dc5ff5eaa51ea2 100644 (file)
@@ -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<DirectoryUser> doGetRoles(LdapName searchBase, Filter f, boolean deep) {
-               List<DirectoryUser> res = new ArrayList<>();
+       protected List<LdapEntry> doGetEntries(LdapName searchBase, Filter f, boolean deep) {
+               List<LdapEntry> 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<HierarchyUnit> doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) {
+       public Iterable<HierarchyUnit> 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 (file)
index 7fd9e18..0000000
+++ /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<String, ?> properties) {
-               Object res = getRawValue(properties);
-               if (res == null)
-                       return null;
-               return res.toString();
-       }
-
-       @SuppressWarnings("unchecked")
-       public <T> T getRawValue(Dictionary<String, ?> 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<String, ?> properties) {
-               StringBuilder query = new StringBuilder();
-
-               boolean first = true;
-//             for (Enumeration<String> 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<String, Object> uriAsProperties(String uriStr) {
-               try {
-                       Hashtable<String, Object> res = new Hashtable<String, Object>();
-                       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<String, List<String>> query = NamingUtils.queryToMap(u);
-                       for (String key : query.keySet()) {
-                               UserAdminConf ldapProp = UserAdminConf.valueOf(key);
-                               List<String> 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<String, Object> properties) {
-               String bDn = (String) properties.get(baseDn.name());
-               if (bDn == null)
-                       throw new IllegalStateException("No baseDn in " + properties);
-               return DigestUtils.sha1str(bDn);
-       }
-}
index 2c070d66d53a720561eade2f854f6ed83aa54230..05ed7cf7cef9260324307d66f6cac55c03cd5699 100644 (file)
@@ -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<String> getRealm();
-
-       Iterable<HierarchyUnit> getDirectHierarchyUnits(boolean functionalOnly);
-
-       HierarchyUnit getHierarchyUnit(String path);
+public interface UserDirectory extends Directory {
 
        HierarchyUnit getHierarchyUnit(Role role);
 
+       Iterable<? extends Role> 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 (file)
index 32bb401..0000000
+++ /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<Xid, DirectoryUserWorkingCopy> workingCopies = new HashMap<Xid, DirectoryUserWorkingCopy>();
-       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 (file)
index 0000000..b3dfa8b
--- /dev/null
@@ -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<String> getRealm();
+
+       void setTransactionControl(WorkControl transactionControl);
+
+       /*
+        * HIERARCHY
+        */
+
+       Iterable<HierarchyUnit> 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 (file)
index 0000000..c0f96ee
--- /dev/null
@@ -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<String, ?> properties) {
+               Object res = getRawValue(properties);
+               if (res == null)
+                       return null;
+               return res.toString();
+       }
+
+       @SuppressWarnings("unchecked")
+       public <T> T getRawValue(Dictionary<String, ?> 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<String, ?> properties) {
+               StringBuilder query = new StringBuilder();
+
+               boolean first = true;
+//             for (Enumeration<String> 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<String, Object> uriAsProperties(String uriStr) {
+               try {
+                       Hashtable<String, Object> res = new Hashtable<String, Object>();
+                       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<String, List<String>> query = NamingUtils.queryToMap(u);
+                       for (String key : query.keySet()) {
+                               DirectoryConf ldapProp = DirectoryConf.valueOf(key);
+                               List<String> 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<String, Object> 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 (file)
index 0000000..d07d2d2
--- /dev/null
@@ -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 (file)
index 0000000..0194ffc
--- /dev/null
@@ -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<HierarchyUnit> getDirectHierachyUnits(boolean functionalOnly);
+
+       boolean isFunctional();
+
+       String getContext();
+
+       Directory getDirectory();
+
+//     Map<String,Object> 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 (file)
index 0000000..27f9c55
--- /dev/null
@@ -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<LdapEntryWorkingCopy>, 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<String, Object> 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<String> credentialAttributeIds = Arrays
+                       .asList(new String[] { LdapAttrs.userPassword.name(), LdapAttrs.authPassword.name() });
+
+       private WorkControl transactionControl;
+       private WorkingCopyXaResource<LdapEntryWorkingCopy> xaResource = new WorkingCopyXaResource<>(this);
+
+       public AbstractLdapDirectory(URI uriArg, Dictionary<String, ?> props, boolean scoped) {
+               this.properties = new Hashtable<String, Object>();
+               for (Enumeration<String> 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<HierarchyUnit> doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly);
+
+       protected abstract Boolean daoHasEntry(LdapName dn);
+
+       protected abstract LdapEntry daoGetEntry(LdapName key) throws NameNotFoundException;
+
+       protected abstract List<LdapEntry> 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<HierarchyUnit> 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<String> 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<String, Object> getProperties() {
+               return properties;
+       }
+
+       public Dictionary<String, Object> cloneProperties() {
+               return new Hashtable<>(properties);
+       }
+
+       public String getForcedPassword() {
+               return forcedPassword;
+       }
+
+       public boolean isScoped() {
+               return scoped;
+       }
+
+       public String getMemberAttributeId() {
+               return memberAttributeId;
+       }
+
+       public List<String> 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 (file)
index 0000000..be919c0
--- /dev/null
@@ -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 (file)
index 0000000..7b0095f
--- /dev/null
@@ -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<String, Object> {
+       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<String> keys() {
+               NamingEnumeration<String> namingEnumeration = attributes.getIDs();
+               return new Enumeration<String>() {
+
+                       @Override
+                       public boolean hasMoreElements() {
+                               return namingEnumeration.hasMoreElements();
+                       }
+
+                       @Override
+                       public String nextElement() {
+                               return namingEnumeration.nextElement();
+                       }
+
+               };
+       }
+
+       @Override
+       public Enumeration<Object> elements() {
+               NamingEnumeration<String> namingEnumeration = attributes.getIDs();
+               return new Enumeration<Object>() {
+
+                       @Override
+                       public boolean hasMoreElements() {
+                               return namingEnumeration.hasMoreElements();
+                       }
+
+                       @Override
+                       public Object nextElement() {
+                               String key = namingEnumeration.nextElement();
+                               return get(key);
+                       }
+
+               };
+       }
+
+       @Override
+       /** @returns a <code>String</code> or <code>String[]</code> */
+       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 <b>content</b> of an {@link Attributes} to the provided
+        * {@link Dictionary}.
+        */
+       public static void copy(Attributes attributes, Dictionary<String, Object> dictionary) {
+               AttributesDictionary ad = new AttributesDictionary(attributes);
+               Enumeration<String> 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<String, Object> dictionary, Attributes attributes) {
+               AttributesDictionary ad = new AttributesDictionary(attributes);
+               Enumeration<String> 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 (file)
index 0000000..e10f457
--- /dev/null
@@ -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 (file)
index 0000000..861eb4f
--- /dev/null
@@ -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<String, Object> 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<String, Object> 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<String> 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<String, Object> 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 (file)
index 0000000..f783838
--- /dev/null
@@ -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<String, ?> properties) {
+               try {
+                       Hashtable<String, Object> connEnv = new Hashtable<String, Object>();
+                       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<SearchResult> search(LdapName searchBase, String searchFilter,
+                       SearchControls searchControls) throws NamingException {
+               NamingEnumeration<SearchResult> 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<?, ?, LdapName> 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 (file)
index 0000000..c145a6f
--- /dev/null
@@ -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 (file)
index 0000000..381c11b
--- /dev/null
@@ -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<LdapEntry, Attributes, LdapName> {
+       @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 (file)
index 0000000..d76c449
--- /dev/null
@@ -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<HierarchyUnit> 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<HierarchyUnit> getDirectHierachyUnits(boolean functionalOnly) {
+//             List<HierarchyUnit> 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 (file)
index 0000000..689ef23
--- /dev/null
@@ -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 (file)
index 0000000..0022943
--- /dev/null
@@ -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<LdapName, Attributes> 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<LdapName, Attributes> 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<LdapName, Attributes> read(Reader reader) throws IOException {
+               SortedMap<LdapName, Attributes> res = new TreeMap<LdapName, Attributes>();
+               try {
+                       List<String> 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 (file)
index 0000000..a10f169
--- /dev/null
@@ -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<? extends Attribute> 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<? extends Attribute> 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<LdapName, Attributes> 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<String> 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');
+               }
+       }
+}
index e38bc2f29cd8a10fd5cb69233e6c4b56fb62285e..7661d4c51c818897ebb00fc4b7b03d83eff00f6c 100644 (file)
@@ -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 (file)
index 0bbeb03..0000000
+++ /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<String, Object> {
-       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<String> keys() {
-               NamingEnumeration<String> namingEnumeration = attributes.getIDs();
-               return new Enumeration<String>() {
-
-                       @Override
-                       public boolean hasMoreElements() {
-                               return namingEnumeration.hasMoreElements();
-                       }
-
-                       @Override
-                       public String nextElement() {
-                               return namingEnumeration.nextElement();
-                       }
-
-               };
-       }
-
-       @Override
-       public Enumeration<Object> elements() {
-               NamingEnumeration<String> namingEnumeration = attributes.getIDs();
-               return new Enumeration<Object>() {
-
-                       @Override
-                       public boolean hasMoreElements() {
-                               return namingEnumeration.hasMoreElements();
-                       }
-
-                       @Override
-                       public Object nextElement() {
-                               String key = namingEnumeration.nextElement();
-                               return get(key);
-                       }
-
-               };
-       }
-
-       @Override
-       /** @returns a <code>String</code> or <code>String[]</code> */
-       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 <b>content</b> of an {@link Attributes} to the provided
-        * {@link Dictionary}.
-        */
-       public static void copy(Attributes attributes, Dictionary<String, Object> dictionary) {
-               AttributesDictionary ad = new AttributesDictionary(attributes);
-               Enumeration<String> 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<String, Object> dictionary, Attributes attributes) {
-               AttributesDictionary ad = new AttributesDictionary(attributes);
-               Enumeration<String> 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 (file)
index b116840..0000000
+++ /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 (file)
index 3c4ae0a..0000000
+++ /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<LdapName, Attributes> 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<LdapName, Attributes> 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<LdapName, Attributes> read(Reader reader) throws IOException {
-               SortedMap<LdapName, Attributes> res = new TreeMap<LdapName, Attributes>();
-               try {
-                       List<String> 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 (file)
index 3e25dcf..0000000
+++ /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<? extends Attribute> 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<? extends Attribute> 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<LdapName, Attributes> 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<String> 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');
-               }
-       }
-}
index 36ed3da7a2a7cb0aa1b98775b5dfa5605f9e65c1..a4d04568e837d1182dd29894f9b9071293b7bf3f 100644 (file)
@@ -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;