Make data admin log-in more robust and easier to use.
authorMathieu Baudier <mbaudier@argeo.org>
Sun, 8 Sep 2019 06:06:04 +0000 (08:06 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Sun, 8 Sep 2019 06:06:04 +0000 (08:06 +0200)
org.argeo.cms/src/org/argeo/cms/auth/DataAdminLoginModule.java [deleted file]
org.argeo.cms/src/org/argeo/cms/auth/IdentLoginModule.java
org.argeo.cms/src/org/argeo/cms/internal/kernel/HomeRepository.java
org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeAuthorization.java
org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeHttp.java
org.argeo.cms/src/org/argeo/cms/internal/kernel/jaas-ipa.cfg
org.argeo.cms/src/org/argeo/cms/internal/kernel/jaas.cfg
org.argeo.jcr/src/org/argeo/jcr/JcrRepositoryWrapper.java
org.argeo.node.api/src/org/argeo/node/DataAdminLoginModule.java [new file with mode: 0644]
org.argeo.node.api/src/org/argeo/node/NodeUtils.java

diff --git a/org.argeo.cms/src/org/argeo/cms/auth/DataAdminLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/DataAdminLoginModule.java
deleted file mode 100644 (file)
index 50a8788..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-package org.argeo.cms.auth;
-
-import java.util.Map;
-
-import javax.security.auth.Subject;
-import javax.security.auth.callback.CallbackHandler;
-import javax.security.auth.login.LoginException;
-import javax.security.auth.spi.LoginModule;
-
-import org.argeo.node.security.DataAdminPrincipal;
-
-/** Logs a system process as data admin */
-public class DataAdminLoginModule implements LoginModule {
-       private Subject subject;
-
-       @Override
-       public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState,
-                       Map<String, ?> options) {
-               this.subject = subject;
-       }
-
-       @Override
-       public boolean login() throws LoginException {
-               // TODO check permission?
-               return true;
-       }
-
-       @Override
-       public boolean commit() throws LoginException {
-               subject.getPrincipals().add(new DataAdminPrincipal());
-               return true;
-       }
-
-       @Override
-       public boolean abort() throws LoginException {
-               return true;
-       }
-
-       @Override
-       public boolean logout() throws LoginException {
-               subject.getPrincipals().removeAll(subject.getPrincipals(DataAdminPrincipal.class));
-               return true;
-       }
-}
index b831097ccd3d034729c16eab2026414077c9250f..ff741d8a05464588a27d33782b3bab852e77d968 100644 (file)
@@ -16,17 +16,17 @@ import org.apache.commons.logging.LogFactory;
 import org.argeo.cms.internal.kernel.Activator;
 import org.argeo.ident.IdentClient;
 
+/** Use an ident service to identify. */
 public class IdentLoginModule implements LoginModule {
        private final static Log log = LogFactory.getLog(IdentLoginModule.class);
 
-       private Subject subject = null;
        private CallbackHandler callbackHandler = null;
        private Map<String, Object> sharedState = null;
 
+       @SuppressWarnings("unchecked")
        @Override
        public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState,
                        Map<String, ?> options) {
-               this.subject = subject;
                this.callbackHandler = callbackHandler;
                this.sharedState = (Map<String, Object>) sharedState;
        }
index fd432d11e4c85304f688e5ce550a4411f1480e6c..e5164b6c3fc6424e7dc14e234c25a33c8f3021fd 100644 (file)
@@ -71,7 +71,7 @@ class HomeRepository extends JcrRepositoryWrapper implements KernelConstants {
        }
 
        @Override
-       protected void processNewSession(Session session) {
+       protected void processNewSession(Session session, String workspaceName) {
                String username = session.getUserID();
                if (username == null || username.toString().equals(""))
                        return;
@@ -82,7 +82,7 @@ class HomeRepository extends JcrRepositoryWrapper implements KernelConstants {
                        return;
                Session adminSession = KernelUtils.openAdminSession(getRepository(), session.getWorkspace().getName());
                try {
-                       syncJcr(adminSession, username);
+                       syncJcr(adminSession, username, workspaceName);
                        checkedUsers.add(username);
                } finally {
                        JcrUtils.logoutQuietly(adminSession);
@@ -109,28 +109,35 @@ class HomeRepository extends JcrRepositoryWrapper implements KernelConstants {
                }
        }
 
-       private void syncJcr(Session session, String username) {
+       protected synchronized void syncJcr(Session adminSession, String username, String workspaceName) {
+               // only in the default workspace
+               if (workspaceName != null)
+                       return;
+               // skip system users
+               if (username.endsWith(NodeConstants.ROLES_BASEDN))
+                       return;
+
                try {
-                       Node userHome = NodeUtils.getUserHome(session, username);
+                       Node userHome = NodeUtils.getUserHome(adminSession, username);
                        if (userHome == null) {
                                String homePath = generateUserPath(username);
-                               if (session.itemExists(homePath))// duplicate user id
-                                       userHome = session.getNode(homePath).getParent().addNode(JcrUtils.lastPathElement(homePath));
+                               if (adminSession.itemExists(homePath))// duplicate user id
+                                       userHome = adminSession.getNode(homePath).getParent().addNode(JcrUtils.lastPathElement(homePath));
                                else
-                                       userHome = JcrUtils.mkdirs(session, homePath);
+                                       userHome = JcrUtils.mkdirs(adminSession, homePath);
                                // userHome = JcrUtils.mkfolders(session, homePath);
                                userHome.addMixin(NodeTypes.NODE_USER_HOME);
                                userHome.addMixin(NodeType.MIX_CREATED);
                                userHome.setProperty(NodeNames.LDAP_UID, username);
-                               session.save();
+                               adminSession.save();
 
-                               JcrUtils.clearAccessControList(session, homePath, username);
-                               JcrUtils.addPrivilege(session, homePath, username, Privilege.JCR_ALL);
+                               JcrUtils.clearAccessControList(adminSession, homePath, username);
+                               JcrUtils.addPrivilege(adminSession, homePath, username, Privilege.JCR_ALL);
                        }
-                       if (session.hasPendingChanges())
-                               session.save();
+                       if (adminSession.hasPendingChanges())
+                               adminSession.save();
                } catch (RepositoryException e) {
-                       JcrUtils.discardQuietly(session);
+                       JcrUtils.discardQuietly(adminSession);
                        throw new CmsException("Cannot sync node security model for " + username, e);
                }
        }
index 416f3bf756fa8cf108b124e3a54ca3537869bdc4..ddb9730e628e105955cf0b2d9c1239b190aaa7ad 100644 (file)
@@ -10,6 +10,7 @@ import javax.security.auth.x500.X500Principal;
 
 import org.osgi.service.useradmin.Authorization;
 
+@Deprecated
 class NodeAuthorization implements Authorization {
        private final String name;
        private final String displayName;
index 4b700d4bc42f9e5ee4802939183460d62831bdbc..e9219849523eaf0c5bb23cbf21bdd3ad76239ddc 100644 (file)
@@ -18,7 +18,6 @@ import org.apache.jackrabbit.webdav.simple.SimpleWebdavServlet;
 import org.argeo.cms.CmsException;
 import org.argeo.cms.internal.http.CmsSessionProvider;
 import org.argeo.cms.internal.http.DataHttpContext;
-import org.argeo.cms.internal.http.HtmlServlet;
 import org.argeo.cms.internal.http.HttpUtils;
 import org.argeo.cms.internal.http.LinkServlet;
 import org.argeo.cms.internal.http.PrivateHttpContext;
index 018c1bf9ca947f1376b045e9e94484fda498376e..1d43afd9af8fe59b87f33b0f46cbc3b0035f1c25 100644 (file)
@@ -11,7 +11,7 @@ ANONYMOUS {
 };
 
 DATA_ADMIN {
-    org.argeo.cms.auth.DataAdminLoginModule requisite;
+    org.argeo.node.DataAdminLoginModule requisite;
 };
 
 NODE {
@@ -19,7 +19,7 @@ NODE {
      keyTab="${osgi.instance.area}node/krb5.keytab" 
      useKeyTab=true
      storeKey=true;
-    org.argeo.cms.auth.DataAdminLoginModule requisite;
+    org.argeo.node.DataAdminLoginModule requisite;
 };
 
 KEYRING {
index 9b3f0114b9a853b96276038081cf3729b5af6906..e54277a3c73fcb1b9dbcc2a4fe5c1908db9f3ad1 100644 (file)
@@ -10,11 +10,11 @@ ANONYMOUS {
 };
 
 DATA_ADMIN {
-    org.argeo.cms.auth.DataAdminLoginModule requisite;
+    org.argeo.node.DataAdminLoginModule requisite;
 };
 
 NODE {
-    org.argeo.cms.auth.DataAdminLoginModule requisite;
+    org.argeo.node.DataAdminLoginModule requisite;
 };
 
 KEYRING {
index 1e08c18ac7aeb43a4c989bb035dc61eb1657c926..d4bf4381ed1efd88149ea1bb00c44df680f51164 100644 (file)
@@ -106,7 +106,7 @@ public abstract class JcrRepositoryWrapper implements Repository {
                        else
                                throw e;
                }
-               processNewSession(session);
+               processNewSession(session, workspaceName);
                return session;
        }
 
@@ -123,7 +123,7 @@ public abstract class JcrRepositoryWrapper implements Repository {
        }
 
        /** Called after a session has been created, does nothing by default. */
-       protected void processNewSession(Session session) {
+       protected void processNewSession(Session session, String workspaceName) {
        }
 
        /** Wraps access to the repository, making sure it is available. */
@@ -138,8 +138,8 @@ public abstract class JcrRepositoryWrapper implements Repository {
        }
 
        /**
-        * Logs in to the default workspace, creates the required workspace, logs
-        * out, logs in to the required workspace.
+        * Logs in to the default workspace, creates the required workspace, logs out,
+        * logs in to the required workspace.
         */
        protected Session createWorkspaceAndLogsIn(Credentials credentials, String workspaceName)
                        throws RepositoryException {
diff --git a/org.argeo.node.api/src/org/argeo/node/DataAdminLoginModule.java b/org.argeo.node.api/src/org/argeo/node/DataAdminLoginModule.java
new file mode 100644 (file)
index 0000000..3074748
--- /dev/null
@@ -0,0 +1,48 @@
+package org.argeo.node;
+
+import java.util.Map;
+
+import javax.security.auth.AuthPermission;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.login.LoginException;
+import javax.security.auth.spi.LoginModule;
+
+import org.argeo.node.security.DataAdminPrincipal;
+
+/**
+ * Log-in a system process as data admin. Protection is via
+ * {@link AuthPermission} on this login module, so if it can be accessed it will
+ * always succeed.
+ */
+public class DataAdminLoginModule implements LoginModule {
+       private Subject subject;
+
+       @Override
+       public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState,
+                       Map<String, ?> options) {
+               this.subject = subject;
+       }
+
+       @Override
+       public boolean login() throws LoginException {
+               return true;
+       }
+
+       @Override
+       public boolean commit() throws LoginException {
+               subject.getPrincipals().add(new DataAdminPrincipal());
+               return true;
+       }
+
+       @Override
+       public boolean abort() throws LoginException {
+               return true;
+       }
+
+       @Override
+       public boolean logout() throws LoginException {
+               subject.getPrincipals().removeAll(subject.getPrincipals(DataAdminPrincipal.class));
+               return true;
+       }
+}
index afb64bd7a4a8df6327ef2673d1fc4209382c4af8..9b9e854b32ff362f608cd9247635e6829f0141e4 100644 (file)
@@ -15,6 +15,7 @@
  */
 package org.argeo.node;
 
+import java.security.PrivilegedAction;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -31,13 +32,17 @@ import javax.jcr.query.qom.DynamicOperand;
 import javax.jcr.query.qom.QueryObjectModelFactory;
 import javax.jcr.query.qom.Selector;
 import javax.jcr.query.qom.StaticOperand;
+import javax.security.auth.AuthPermission;
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
 
 /** Utilities related to Argeo model in JCR */
 public class NodeUtils {
        /**
         * Wraps the call to the repository factory based on parameter
-        * {@link NodeConstants#CN} in order to simplify it and
-        * protect against future API changes.
+        * {@link NodeConstants#CN} in order to simplify it and protect against future
+        * API changes.
         */
        public static Repository getRepositoryByAlias(RepositoryFactory repositoryFactory, String alias) {
                try {
@@ -52,8 +57,8 @@ public class NodeUtils {
 
        /**
         * Wraps the call to the repository factory based on parameter
-        * {@link NodeConstants#LABELED_URI} in order to simplify it and
-        * protect against future API changes.
+        * {@link NodeConstants#LABELED_URI} in order to simplify it and protect against
+        * future API changes.
         */
        public static Repository getRepositoryByUri(RepositoryFactory repositoryFactory, String uri) {
                return getRepositoryByUri(repositoryFactory, uri, null);
@@ -61,8 +66,8 @@ public class NodeUtils {
 
        /**
         * Wraps the call to the repository factory based on parameter
-        * {@link NodeConstants#LABELED_URI} in order to simplify it and
-        * protect against future API changes.
+        * {@link NodeConstants#LABELED_URI} in order to simplify it and protect against
+        * future API changes.
         */
        public static Repository getRepositoryByUri(RepositoryFactory repositoryFactory, String uri, String alias) {
                try {
@@ -76,18 +81,13 @@ public class NodeUtils {
                }
        }
 
-       private NodeUtils() {
-       }
-
        /**
         * Returns the home node of the user or null if none was found.
         * 
-        * @param session
-        *            the session to use in order to perform the search, this can be
-        *            a session with a different user ID than the one searched,
-        *            typically when a system or admin session is used.
-        * @param username
-        *            the username of the user
+        * @param session  the session to use in order to perform the search, this can
+        *                 be a session with a different user ID than the one searched,
+        *                 typically when a system or admin session is used.
+        * @param username the username of the user
         */
        public static Node getUserHome(Session session, String username) {
                try {
@@ -106,12 +106,10 @@ public class NodeUtils {
        /**
         * Returns the home node of the user or null if none was found.
         * 
-        * @param session
-        *            the session to use in order to perform the search, this can be
-        *            a session with a different user ID than the one searched,
-        *            typically when a system or admin session is used.
-        * @param cn
-        *            the name of the group
+        * @param session the session to use in order to perform the search, this can be
+        *                a session with a different user ID than the one searched,
+        *                typically when a system or admin session is used.
+        * @param cn      the name of the group
         */
        public static Node getGroupHome(Session session, String cn) {
                try {
@@ -131,8 +129,7 @@ public class NodeUtils {
         * Queries one single node.
         * 
         * @return one single node or null if none was found
-        * @throws ArgeoJcrException
-        *             if more than one node was found
+        * @throws ArgeoJcrException if more than one node was found
         */
        private static Node querySingleNode(Query query) {
                NodeIterator nodeIterator;
@@ -159,10 +156,50 @@ public class NodeUtils {
                return getUserHome(session, userID);
        }
 
+       /**
+        * Translate the path to this node into a path containing the name of the
+        * repository and the name of the workspace.
+        */
        public static String getDataPath(String cn, Node node) throws RepositoryException {
                assert node != null;
                StringBuilder buf = new StringBuilder(NodeConstants.PATH_DATA);
                return buf.append('/').append(cn).append('/').append(node.getSession().getWorkspace().getName())
                                .append(node.getPath()).toString();
        }
+
+       /**
+        * Open a JCR session with full read/write rights on the data, as
+        * {@link NodeConstants#ROLE_USER_ADMIN}, using the
+        * {@link NodeConstants#LOGIN_CONTEXT_DATA_ADMIN} login context. For security
+        * hardened deployement, use {@link AuthPermission} on this login context.
+        */
+       public static Session openDataAdminSession(Repository repository, String workspaceName) {
+               ClassLoader currentCl = Thread.currentThread().getContextClassLoader();
+               LoginContext loginContext;
+               try {
+                       loginContext = new LoginContext(NodeConstants.LOGIN_CONTEXT_DATA_ADMIN);
+                       loginContext.login();
+               } catch (LoginException e1) {
+                       throw new RuntimeException("Could not login as data admin", e1);
+               } finally {
+                       Thread.currentThread().setContextClassLoader(currentCl);
+               }
+               return Subject.doAs(loginContext.getSubject(), new PrivilegedAction<Session>() {
+
+                       @Override
+                       public Session run() {
+                               try {
+                                       return repository.login(workspaceName);
+                               } catch (RepositoryException e) {
+                                       throw new RuntimeException("Cannot open data admin session", e);
+                               }
+                       }
+
+               });
+       }
+
+       /** Singleton. */
+       private NodeUtils() {
+       }
+
 }