Introduce CMS-specific user APIs, based at this stage on OSGi UserAdmin
[lgpl/argeo-commons.git] / org.argeo.cms / src / org / argeo / cms / internal / runtime / CmsUserAdmin.java
index 9364ee4a3069bfdfcf905d41ae74ecc21172180b..e6f903d393179003f2862331a9ab131f0133de11 100644 (file)
@@ -1,19 +1,21 @@
 package org.argeo.cms.internal.runtime;
 
 import java.io.IOException;
-import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.net.URL;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.security.PrivilegedExceptionAction;
 import java.util.ArrayList;
 import java.util.Dictionary;
 import java.util.Iterator;
+import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 
-import javax.naming.ldap.LdapName;
 import javax.security.auth.Subject;
 import javax.security.auth.callback.Callback;
 import javax.security.auth.callback.CallbackHandler;
@@ -23,26 +25,18 @@ import javax.security.auth.kerberos.KerberosPrincipal;
 import javax.security.auth.login.LoginContext;
 import javax.security.auth.login.LoginException;
 
-import org.apache.commons.httpclient.auth.AuthPolicy;
-import org.apache.commons.httpclient.auth.CredentialsProvider;
-import org.apache.commons.httpclient.params.DefaultHttpParams;
-import org.apache.commons.httpclient.params.HttpMethodParams;
-import org.apache.commons.httpclient.params.HttpParams;
 import org.argeo.api.cms.CmsAuth;
 import org.argeo.api.cms.CmsConstants;
 import org.argeo.api.cms.CmsLog;
-import org.argeo.cms.internal.http.client.HttpCredentialProvider;
-import org.argeo.cms.internal.http.client.SpnegoAuthScheme;
-import org.argeo.osgi.transaction.WorkControl;
-import org.argeo.osgi.transaction.WorkTransaction;
-import org.argeo.osgi.useradmin.AbstractUserDirectory;
-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.naming.DnsBrowser;
+import org.argeo.api.cms.CmsState;
+import org.argeo.api.cms.directory.UserDirectory;
+import org.argeo.api.cms.transaction.WorkControl;
+import org.argeo.api.cms.transaction.WorkTransaction;
+import org.argeo.cms.CmsDeployProperty;
+import org.argeo.cms.dns.DnsBrowser;
+import org.argeo.cms.osgi.useradmin.AggregatingUserAdmin;
+import org.argeo.cms.osgi.useradmin.DirectoryUserAdmin;
+import org.argeo.cms.runtime.DirectoryConf;
 import org.ietf.jgss.GSSCredential;
 import org.ietf.jgss.GSSException;
 import org.ietf.jgss.GSSManager;
@@ -68,24 +62,141 @@ public class CmsUserAdmin extends AggregatingUserAdmin {
        private WorkControl transactionManager;
        private WorkTransaction userTransaction;
 
+       private CmsState cmsState;
+
        public CmsUserAdmin() {
-               super(CmsConstants.ROLES_BASEDN, CmsConstants.TOKENS_BASEDN);
+               super(CmsConstants.SYSTEM_ROLES_BASEDN, CmsConstants.TOKENS_BASEDN);
        }
 
        public void start() {
+               super.start();
+               List<Dictionary<String, Object>> configs = getUserDirectoryConfigs();
+               for (Dictionary<String, Object> config : configs) {
+                       enableUserDirectory(config);
+//                     if (userDirectory.getRealm().isPresent())
+//                             loadIpaJaasConfiguration();
+               }
+               log.debug(() -> "CMS user admin available");
        }
 
        public void stop() {
+//             for (UserDirectory userDirectory : getUserDirectories()) {
+//                     removeUserDirectory(userDirectory);
+//             }
+               super.stop();
+       }
+
+       protected List<Dictionary<String, Object>> getUserDirectoryConfigs() {
+               List<Dictionary<String, Object>> res = new ArrayList<>();
+               Path nodeBase = cmsState.getDataPath(KernelConstants.DIR_PRIVATE);
+               List<String> uris = new ArrayList<>();
+
+               // node roles
+               String nodeRolesUri = null;// getFrameworkProp(CmsConstants.ROLES_URI);
+               String baseNodeRoleDn = CmsConstants.SYSTEM_ROLES_BASEDN;
+               if (nodeRolesUri == null && nodeBase != null) {
+                       nodeRolesUri = baseNodeRoleDn + ".ldif";
+                       Path nodeRolesFile = nodeBase.resolve(nodeRolesUri);
+                       if (!Files.exists(nodeRolesFile))
+                               try {
+                                       Files.copy(CmsUserAdmin.class.getResourceAsStream(baseNodeRoleDn + ".ldif"), nodeRolesFile);
+                               } catch (IOException e) {
+                                       throw new RuntimeException("Cannot copy demo resource", e);
+                               }
+                       // nodeRolesUri = nodeRolesFile.toURI().toString();
+               }
+               if (nodeRolesUri != null)
+                       uris.add(nodeRolesUri);
+
+               // node tokens
+               String nodeTokensUri = null;// getFrameworkProp(CmsConstants.TOKENS_URI);
+               String baseNodeTokensDn = CmsConstants.TOKENS_BASEDN;
+               if (nodeTokensUri == null && nodeBase != null) {
+                       nodeTokensUri = baseNodeTokensDn + ".ldif";
+                       Path nodeTokensFile = nodeBase.resolve(nodeTokensUri);
+                       if (!Files.exists(nodeTokensFile))
+                               try {
+                                       Files.copy(CmsUserAdmin.class.getResourceAsStream(baseNodeTokensDn + ".ldif"), nodeTokensFile);
+                               } catch (IOException e) {
+                                       throw new RuntimeException("Cannot copy demo resource", e);
+                               }
+                       // nodeRolesUri = nodeRolesFile.toURI().toString();
+               }
+               if (nodeTokensUri != null)
+                       uris.add(nodeTokensUri);
+
+               // Business roles
+//             String userAdminUris = getFrameworkProp(CmsConstants.USERADMIN_URIS);
+               List<String> userAdminUris = CmsStateImpl.getDeployProperties(cmsState, CmsDeployProperty.DIRECTORY);// getFrameworkProp(CmsConstants.USERADMIN_URIS);
+               for (String userAdminUri : userAdminUris) {
+                       if (userAdminUri == null)
+                               continue;
+//                     if (!userAdminUri.trim().equals(""))
+                       uris.add(userAdminUri);
+               }
+
+               if (uris.size() == 0 && nodeBase != null) {
+                       // TODO put this somewhere else
+                       String demoBaseDn = "dc=example,dc=com";
+                       String userAdminUri = demoBaseDn + ".ldif";
+                       Path businessRolesFile = nodeBase.resolve(userAdminUri);
+                       Path systemRolesFile = nodeBase.resolve("ou=roles,ou=node.ldif");
+                       if (!Files.exists(businessRolesFile))
+                               try {
+                                       Files.copy(CmsUserAdmin.class.getResourceAsStream(demoBaseDn + ".ldif"), businessRolesFile);
+                                       if (!Files.exists(systemRolesFile))
+                                               Files.copy(CmsUserAdmin.class.getResourceAsStream("example-ou=roles,ou=node.ldif"),
+                                                               systemRolesFile);
+                               } catch (IOException e) {
+                                       throw new RuntimeException("Cannot copy demo resources", e);
+                               }
+                       // userAdminUris = businessRolesFile.toURI().toString();
+                       log.warn("## DEV Using dummy base DN " + demoBaseDn);
+                       // TODO downgrade security level
+               }
+
+               // Interprets URIs
+               for (String uri : uris) {
+                       URI u;
+                       try {
+                               u = new URI(uri);
+                               if (u.getPath() == null)
+                                       throw new IllegalArgumentException(
+                                                       "URI " + uri + " must have a path in order to determine base DN");
+                               if (u.getScheme() == null) {
+                                       if (uri.startsWith("/") || uri.startsWith("./") || uri.startsWith("../"))
+                                               u = Paths.get(uri).toRealPath().toUri();
+                                       else if (!uri.contains("/")) {
+                                               // u = KernelUtils.getOsgiInstanceUri(KernelConstants.DIR_NODE + '/' + uri);
+                                               u = new URI(uri);
+                                       } else
+                                               throw new IllegalArgumentException("Cannot interpret " + uri + " as an uri");
+                               } else if (u.getScheme().equals(DirectoryConf.SCHEME_FILE)) {
+                                       u = Paths.get(u).toRealPath().toUri();
+                               }
+                       } catch (Exception e) {
+                               throw new RuntimeException("Cannot interpret " + uri + " as an uri", e);
+                       }
+
+                       try {
+                               Dictionary<String, Object> properties = DirectoryConf.uriAsProperties(u.toString());
+                               res.add(properties);
+                       } catch (Exception e) {
+                               log.error("Cannot load user directory " + u, e);
+                       }
+               }
+
+               return res;
        }
 
        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());
-                               u = KernelUtils.getOsgiInstanceUri(KernelConstants.DIR_NODE + '/' + baseDn + ".ldif");
+                               String baseDn = (String) properties.get(DirectoryConf.baseDn.name());
+                               u = KernelUtils.getOsgiInstanceUri(KernelConstants.DIR_PRIVATE + '/' + baseDn + ".ldif");
                        } else if (realm != null) {
                                u = null;
                        } else {
@@ -96,26 +207,26 @@ public class CmsUserAdmin extends AggregatingUserAdmin {
                }
 
                // Create
-               AbstractUserDirectory userDirectory;
-               if (realm != null || UserAdminConf.SCHEME_LDAP.equals(u.getScheme())
-                               || UserAdminConf.SCHEME_LDAPS.equals(u.getScheme())) {
-                       userDirectory = new LdapUserAdmin(properties);
-               } else if (UserAdminConf.SCHEME_FILE.equals(u.getScheme())) {
-                       userDirectory = new LdifUserAdmin(u, properties);
-               } else if (UserAdminConf.SCHEME_OS.equals(u.getScheme())) {
-                       userDirectory = new OsUserDirectory(u, properties);
-                       singleUser = true;
-               } else {
-                       throw new IllegalArgumentException("Unsupported scheme " + u.getScheme());
-               }
-               LdapName baseDn = userDirectory.getBaseDn();
+               UserDirectory userDirectory = new DirectoryUserAdmin(u, properties);
+//             if (realm != null || DirectoryConf.SCHEME_LDAP.equals(u.getScheme())
+//                             || DirectoryConf.SCHEME_LDAPS.equals(u.getScheme())) {
+//                     userDirectory = new LdapUserAdmin(properties);
+//             } else if (DirectoryConf.SCHEME_FILE.equals(u.getScheme())) {
+//                     userDirectory = new LdifUserAdmin(u, properties);
+//             } else if (DirectoryConf.SCHEME_OS.equals(u.getScheme())) {
+//                     userDirectory = new OsUserDirectory(u, properties);
+//                     singleUser = true;
+//             } else {
+//                     throw new IllegalArgumentException("Unsupported scheme " + u.getScheme());
+//             }
+               String basePath = userDirectory.getBase();
 
                addUserDirectory(userDirectory);
-               if (isSystemRolesBaseDn(baseDn)) {
+               if (isSystemRolesBaseDn(basePath)) {
                        addStandardSystemRoles();
                }
                if (log.isDebugEnabled()) {
-                       log.debug("User directory " + userDirectory.getBaseDn() + (u != null ? " [" + u.getScheme() + "]" : "")
+                       log.debug("User directory " + userDirectory.getBase() + (u != null ? " [" + u.getScheme() + "]" : "")
                                        + " enabled." + (realm != null ? " " + realm + " realm." : ""));
                }
                return userDirectory;
@@ -153,13 +264,15 @@ public class CmsUserAdmin extends AggregatingUserAdmin {
                }
        }
 
-       protected void postAdd(AbstractUserDirectory userDirectory) {
+       @Override
+       protected void postAdd(UserDirectory userDirectory) {
                userDirectory.setTransactionControl(transactionManager);
 
-               Object realm = userDirectory.getProperties().get(UserAdminConf.realm.name());
-               if (realm != null) {
+               Optional<String> realm = userDirectory.getRealm();
+               if (realm.isPresent()) {
+                       loadIpaJaasConfiguration();
                        if (Files.exists(nodeKeyTab)) {
-                               String servicePrincipal = getKerberosServicePrincipal(realm.toString());
+                               String servicePrincipal = getKerberosServicePrincipal(realm.get());
                                if (servicePrincipal != null) {
                                        CallbackHandler callbackHandler = new CallbackHandler() {
                                                @Override
@@ -171,7 +284,7 @@ public class CmsUserAdmin extends AggregatingUserAdmin {
                                                }
                                        };
                                        try {
-                                               LoginContext nodeLc = new LoginContext(CmsAuth.LOGIN_CONTEXT_NODE, callbackHandler);
+                                               LoginContext nodeLc = CmsAuth.NODE.newLoginContext(callbackHandler);
                                                nodeLc.login();
                                                acceptorCredentials = logInAsAcceptor(nodeLc.getSubject(), servicePrincipal);
                                        } catch (LoginException e) {
@@ -180,22 +293,13 @@ public class CmsUserAdmin extends AggregatingUserAdmin {
                                }
                        }
 
-                       // Register client-side SPNEGO auth scheme
-                       AuthPolicy.registerAuthScheme(SpnegoAuthScheme.NAME, SpnegoAuthScheme.class);
-                       HttpParams params = DefaultHttpParams.getDefaultParams();
-                       ArrayList<String> schemes = new ArrayList<>();
-                       schemes.add(SpnegoAuthScheme.NAME);// SPNEGO preferred
-                       // schemes.add(AuthPolicy.BASIC);// incompatible with Basic
-                       params.setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, schemes);
-                       params.setParameter(CredentialsProvider.PROVIDER, new HttpCredentialProvider());
-                       params.setParameter(HttpMethodParams.COOKIE_POLICY, KernelConstants.COOKIE_POLICY_BROWSER_COMPATIBILITY);
-                       // params.setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
                }
        }
 
-       protected void preDestroy(AbstractUserDirectory userDirectory) {
-               Object realm = userDirectory.getProperties().get(UserAdminConf.realm.name());
-               if (realm != null) {
+       @Override
+       protected void preDestroy(UserDirectory userDirectory) {
+               Optional<String> realm = userDirectory.getRealm();
+               if (realm.isPresent()) {
                        if (acceptorCredentials != null) {
                                try {
                                        acceptorCredentials.dispose();
@@ -207,16 +311,30 @@ public class CmsUserAdmin extends AggregatingUserAdmin {
                }
        }
 
-       private String getKerberosServicePrincipal(String realm) {
-               String hostname;
-               try (DnsBrowser dnsBrowser = new DnsBrowser()) {
-                       InetAddress localhost = InetAddress.getLocalHost();
-                       hostname = localhost.getHostName();
+       private void loadIpaJaasConfiguration() {
+               if (CmsStateImpl.getDeployProperty(cmsState, CmsDeployProperty.JAVA_LOGIN_CONFIG) == null) {
+                       String jaasConfig = KernelConstants.JAAS_CONFIG_IPA;
+                       URL url = getClass().getClassLoader().getResource(jaasConfig);
+                       KernelUtils.setJaasConfiguration(url);
+                       log.debug("Set IPA JAAS configuration.");
+               }
+       }
+
+       protected String getKerberosServicePrincipal(String realm) {
+               if (!Files.exists(nodeKeyTab))
+                       return null;
+               List<String> dns = CmsStateImpl.getDeployProperties(cmsState, CmsDeployProperty.DNS);
+               String hostname = CmsStateImpl.getDeployProperty(cmsState, CmsDeployProperty.HOST);
+               try (DnsBrowser dnsBrowser = new DnsBrowser(dns)) {
+                       hostname = hostname != null ? hostname : InetAddress.getLocalHost().getHostName();
                        String dnsZone = hostname.substring(hostname.indexOf('.') + 1);
-                       String ipfromDns = dnsBrowser.getRecord(hostname, localhost instanceof Inet6Address ? "AAAA" : "A");
-                       boolean consistentIp = localhost.getHostAddress().equals(ipfromDns);
+                       String ipv4fromDns = dnsBrowser.getRecord(hostname, "A");
+                       String ipv6fromDns = dnsBrowser.getRecord(hostname, "AAAA");
+                       if (ipv4fromDns == null && ipv6fromDns == null)
+                               throw new IllegalStateException("hostname " + hostname + " is not registered in DNS");
+                       // boolean consistentIp = localhost.getHostAddress().equals(ipfromDns);
                        String kerberosDomain = dnsBrowser.getRecord("_kerberos." + dnsZone, "TXT");
-                       if (consistentIp && kerberosDomain != null && kerberosDomain.equals(realm) && Files.exists(nodeKeyTab)) {
+                       if (kerberosDomain != null && kerberosDomain.equals(realm)) {
                                return KernelConstants.DEFAULT_KERBEROS_SERVICE + "/" + hostname + "@" + kerberosDomain;
                        } else
                                return null;
@@ -289,8 +407,8 @@ public class CmsUserAdmin extends AggregatingUserAdmin {
                this.userTransaction = userTransaction;
        }
 
-       /*
-        * STATIC
-        */
+       public void setCmsState(CmsState cmsState) {
+               this.cmsState = cmsState;
+       }
 
 }