From b6cad136dfd4589bc2a8f48ec9168732517f451b Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Sat, 22 Feb 2020 12:56:52 +0100 Subject: [PATCH] Remove node data model, home areas based on workspaces instead. --- .../argeo/cms/ui/jcr/NodeLabelProvider.java | 11 +- .../org/argeo/cms/tabular/JcrTabularTest.java | 2 +- .../src/org/argeo/cms/auth/CmsSession.java | 36 ++++-- .../src/org/argeo/cms/auth/CmsSessionId.java | 6 + .../cms/internal/auth/CmsSessionImpl.java | 4 + .../cms/internal/http/CmsSessionProvider.java | 53 +++------ .../cms/internal/kernel/HomeRepository.java | 102 +++++++++++----- org.argeo.ext.jackrabbit/pom.xml | 9 +- .../ArgeoAccessControlProvider.java | 12 +- .../jackrabbit/ArgeoSecurityManager.java | 43 ++++++- .../security/JackrabbitSecurityUtils.java | 80 +++++++++++++ org.argeo.jcr/src/org/argeo/jcr/JcrUtils.java | 17 ++- org.argeo.node.api/bnd.bnd | 2 +- .../src/org/argeo/node/NodeConstants.java | 3 + .../src/org/argeo/node/NodeNames.java | 1 + .../src/org/argeo/node/NodeTypes.java | 1 + .../src/org/argeo/node/NodeUtils.java | 110 ++++++++++++++---- .../src/org/argeo/node/ldap.cnd | 1 + .../src/org/argeo/node/node.cnd | 1 - 19 files changed, 376 insertions(+), 118 deletions(-) create mode 100644 org.argeo.jcr/src/org/argeo/jackrabbit/security/JackrabbitSecurityUtils.java create mode 100644 org.argeo.node.api/src/org/argeo/node/ldap.cnd diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/NodeLabelProvider.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/NodeLabelProvider.java index 765f3201a..0cd451600 100644 --- a/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/NodeLabelProvider.java +++ b/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/NodeLabelProvider.java @@ -17,6 +17,7 @@ package org.argeo.cms.ui.jcr; import javax.jcr.NamespaceException; import javax.jcr.Node; +import javax.jcr.Property; import javax.jcr.RepositoryException; import javax.jcr.nodetype.NodeType; @@ -28,8 +29,6 @@ import org.argeo.cms.ui.jcr.model.RepositoryElem; import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem; import org.argeo.cms.ui.jcr.model.WorkspaceElem; import org.argeo.eclipse.ui.EclipseUiException; -import org.argeo.naming.LdapAttrs; -import org.argeo.node.NodeTypes; import org.eclipse.jface.viewers.ColumnLabelProvider; import org.eclipse.swt.graphics.Image; @@ -110,9 +109,13 @@ public class NodeLabelProvider extends ColumnLabelProvider { else if (node.getPrimaryNodeType().isNodeType(NodeType.NT_RESOURCE)) return JcrImages.BINARY; try { - // optimizes - if (node.hasProperty(LdapAttrs.uid.property()) && node.isNodeType(NodeTypes.NODE_USER_HOME)) + // TODO check workspace type? + if (node.getDepth() == 1 && node.hasProperty(Property.JCR_ID)) return JcrImages.HOME; + + // optimizes +// if (node.hasProperty(LdapAttrs.uid.property()) && node.isNodeType(NodeTypes.NODE_USER_HOME)) +// return JcrImages.HOME; } catch (NamespaceException e) { // node namespace is not registered in this repo } diff --git a/org.argeo.cms/ext/test/org/argeo/cms/tabular/JcrTabularTest.java b/org.argeo.cms/ext/test/org/argeo/cms/tabular/JcrTabularTest.java index 9201a14fc..44ef29d6a 100644 --- a/org.argeo.cms/ext/test/org/argeo/cms/tabular/JcrTabularTest.java +++ b/org.argeo.cms/ext/test/org/argeo/cms/tabular/JcrTabularTest.java @@ -37,7 +37,7 @@ public class JcrTabularTest extends AbstractJackrabbitTestCase { public void testWriteReadCsv() throws Exception { // session().setNamespacePrefix("argeo", ArgeoNames.ARGEO_NAMESPACE); - InputStreamReader reader = new InputStreamReader(getClass().getResourceAsStream("/org/argeo/node/node.cnd")); + InputStreamReader reader = new InputStreamReader(getClass().getResourceAsStream("/org/argeo/node/ldap.cnd")); CndImporter.registerNodeTypes(reader, session()); reader.close(); reader = new InputStreamReader(getClass().getResourceAsStream("/org/argeo/cms/argeo.cnd")); diff --git a/org.argeo.cms/src/org/argeo/cms/auth/CmsSession.java b/org.argeo.cms/src/org/argeo/cms/auth/CmsSession.java index 078543020..b9798006b 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/CmsSession.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/CmsSession.java @@ -1,12 +1,17 @@ package org.argeo.cms.auth; import java.time.ZonedDateTime; +import java.util.Collection; import java.util.Locale; import java.util.UUID; import javax.naming.ldap.LdapName; +import javax.security.auth.Subject; import org.argeo.naming.LdapAttrs; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; import org.osgi.service.useradmin.Authorization; /** An authenticated user session. */ @@ -15,8 +20,6 @@ public interface CmsSession { final static String SESSION_UUID = LdapAttrs.entryUUID.name(); final static String SESSION_LOCAL_ID = LdapAttrs.uniqueIdentifier.name(); - // public String getId(); - UUID getUuid(); LdapName getUserDn(); @@ -35,12 +38,25 @@ public interface CmsSession { boolean isValid(); - // public Session getDataSession(String cn, String workspace, Repository - // repository); - // - // public void releaseDataSession(String cn, Session session); - - // public void addHttpSession(HttpServletRequest request); - - // public void cleanUp(); + /** @return The {@link CmsSession} for this {@link Subject} or null. */ + static CmsSession getCmsSession(BundleContext bc, Subject subject) { + if (subject.getPrivateCredentials(CmsSessionId.class).isEmpty()) + return null; + CmsSessionId cmsSessionId = subject.getPrivateCredentials(CmsSessionId.class).iterator().next(); + String uuid = cmsSessionId.getUuid().toString(); + Collection> sr; + try { + sr = bc.getServiceReferences(CmsSession.class, "(" + CmsSession.SESSION_UUID + "=" + uuid + ")"); + } catch (InvalidSyntaxException e) { + throw new IllegalArgumentException("Cannot get CMS session for uuid " + uuid, e); + } + ServiceReference cmsSessionRef; + if (sr.size() == 1) { + cmsSessionRef = sr.iterator().next(); + return bc.getService(cmsSessionRef); + } else if (sr.size() == 0) { + return null; + } else + throw new IllegalStateException(sr.size() + " CMS sessions registered for " + uuid); + } } diff --git a/org.argeo.cms/src/org/argeo/cms/auth/CmsSessionId.java b/org.argeo.cms/src/org/argeo/cms/auth/CmsSessionId.java index 875328974..b71eb4fc7 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/CmsSessionId.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/CmsSessionId.java @@ -2,8 +2,14 @@ package org.argeo.cms.auth; import java.util.UUID; +import javax.security.auth.Subject; + import org.argeo.cms.CmsException; +/** + * The ID of a {@link CmsSession}, which must be available in the private + * credentials of an authenticated {@link Subject}. + */ public class CmsSessionId { private final UUID uuid; diff --git a/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsSessionImpl.java b/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsSessionImpl.java index 010000f61..ce38cf0ee 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsSessionImpl.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsSessionImpl.java @@ -128,6 +128,10 @@ public class CmsSessionImpl implements CmsSession { return getSubject().getPrivateCredentials(SecretKey.class); } + public Session newDataSession(String cn, String workspace, Repository repository) { + return login(repository, workspace); + } + public synchronized Session getDataSession(String cn, String workspace, Repository repository) { // FIXME make it more robust if (workspace == null) diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/CmsSessionProvider.java b/org.argeo.cms/src/org/argeo/cms/internal/http/CmsSessionProvider.java index a1ddcb0ba..14f311f37 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/http/CmsSessionProvider.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/http/CmsSessionProvider.java @@ -1,7 +1,6 @@ package org.argeo.cms.internal.http; import java.io.Serializable; -import java.util.LinkedHashMap; import javax.jcr.Repository; import javax.jcr.RepositoryException; @@ -26,7 +25,7 @@ public class CmsSessionProvider implements SessionProvider, Serializable { private final String alias; - private LinkedHashMap cmsSessions = new LinkedHashMap<>(); +// private LinkedHashMap cmsSessions = new LinkedHashMap<>(); public CmsSessionProvider(String alias) { this.alias = alias; @@ -35,47 +34,27 @@ public class CmsSessionProvider implements SessionProvider, Serializable { public Session getSession(HttpServletRequest request, Repository rep, String workspace) throws javax.jcr.LoginException, ServletException, RepositoryException { + // a client is scanning parent URLs. + if (workspace == null) + return null; + CmsSessionImpl cmsSession = WebCmsSessionImpl.getCmsSession(request); - // if (cmsSession == null) - // return anonymousSession(request, rep, workspace); if (log.isTraceEnabled()) { log.trace("Get JCR session from " + cmsSession); } - Session session = cmsSession.getDataSession(alias, workspace, rep); - cmsSessions.put(session, cmsSession); + Session session = cmsSession.newDataSession(alias, workspace, rep); +// cmsSessions.put(session, cmsSession); return session; } - // private synchronized Session anonymousSession(HttpServletRequest request, - // Repository repository, String workspace) { - // // TODO rather log in here as anonymous? - // LoginContext lc = (LoginContext) - // request.getAttribute(NodeConstants.LOGIN_CONTEXT_ANONYMOUS); - // if (lc == null) - // throw new CmsException("No login context available"); - // // optimize - // Session session; - // try { - // session = Subject.doAs(lc.getSubject(), new - // PrivilegedExceptionAction() { - // @Override - // public Session run() throws Exception { - // return repository.login(workspace); - // } - // }); - // } catch (Exception e) { - // throw new CmsException("Cannot log in to JCR", e); - // } - // return session; - // } - - public synchronized void releaseSession(Session session) { - if (cmsSessions.containsKey(session)) { - CmsSessionImpl cmsSession = cmsSessions.get(session); - cmsSession.releaseDataSession(alias, session); - } else { - log.warn("JCR session " + session + " not found in CMS session list. Logging it out..."); - JcrUtils.logoutQuietly(session); - } + public void releaseSession(Session session) { + JcrUtils.logoutQuietly(session); +// if (cmsSessions.containsKey(session)) { +// CmsSessionImpl cmsSession = cmsSessions.get(session); +// cmsSession.releaseDataSession(alias, session); +// } else { +// log.warn("JCR session " + session + " not found in CMS session list. Logging it out..."); +// JcrUtils.logoutQuietly(session); +// } } } diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/HomeRepository.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/HomeRepository.java index 77d901d07..cc7005be3 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/kernel/HomeRepository.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/kernel/HomeRepository.java @@ -9,6 +9,7 @@ import javax.jcr.Credentials; import javax.jcr.LoginException; import javax.jcr.NoSuchWorkspaceException; import javax.jcr.Node; +import javax.jcr.Property; import javax.jcr.Repository; import javax.jcr.RepositoryException; import javax.jcr.Session; @@ -20,11 +21,10 @@ import javax.security.auth.Subject; import javax.security.auth.login.LoginContext; import org.argeo.cms.CmsException; +import org.argeo.jackrabbit.security.JackrabbitSecurityUtils; import org.argeo.jcr.JcrRepositoryWrapper; import org.argeo.jcr.JcrUtils; import org.argeo.node.NodeConstants; -import org.argeo.node.NodeNames; -import org.argeo.node.NodeTypes; import org.argeo.node.NodeUtils; /** @@ -33,15 +33,17 @@ import org.argeo.node.NodeUtils; class HomeRepository extends JcrRepositoryWrapper implements KernelConstants { /** The home base path. */ - private String homeBasePath = KernelConstants.DEFAULT_HOME_BASE_PATH; - private String usersBasePath = KernelConstants.DEFAULT_USERS_BASE_PATH; - private String groupsBasePath = KernelConstants.DEFAULT_GROUPS_BASE_PATH; +// private String homeBasePath = KernelConstants.DEFAULT_HOME_BASE_PATH; +// private String usersBasePath = KernelConstants.DEFAULT_USERS_BASE_PATH; +// private String groupsBasePath = KernelConstants.DEFAULT_GROUPS_BASE_PATH; private Set checkedUsers = new HashSet(); private SimpleDateFormat usersDatePath = new SimpleDateFormat("YYYY/MM"); private String defaultHomeWorkspace = NodeConstants.HOME; + private String defaultGroupsWorkspace = NodeConstants.GROUPS; + private String defaultGuestsWorkspace = NodeConstants.GUESTS; private final boolean remote; public HomeRepository(Repository repository, boolean remote) { @@ -60,16 +62,8 @@ class HomeRepository extends JcrRepositoryWrapper implements KernelConstants { @Override public Void run() { - Session adminSession = null; - try { - adminSession = JcrUtils.loginOrCreateWorkspace(getRepository(defaultHomeWorkspace), - defaultHomeWorkspace); - initJcr(adminSession); - } catch (RepositoryException e) { - throw new CmsException("Cannot init JCR home", e); - } finally { - JcrUtils.logoutQuietly(adminSession); - } + loginOrCreateWorkspace(defaultHomeWorkspace); + loginOrCreateWorkspace(defaultGroupsWorkspace); return null; } @@ -77,6 +71,20 @@ class HomeRepository extends JcrRepositoryWrapper implements KernelConstants { } } + private void loginOrCreateWorkspace(String workspace) { + Session adminSession = null; + try { + adminSession = JcrUtils.loginOrCreateWorkspace(getRepository(workspace), workspace); +// JcrUtils.addPrivilege(adminSession, "/", NodeConstants.ROLE_USER, Privilege.JCR_READ); + +// initJcr(adminSession); + } catch (RepositoryException e) { + throw new CmsException("Cannot init JCR home", e); + } finally { + JcrUtils.logoutQuietly(adminSession); + } + } + @Override public Session login(Credentials credentials, String workspaceName) throws LoginException, NoSuchWorkspaceException, RepositoryException { @@ -92,6 +100,16 @@ class HomeRepository extends JcrRepositoryWrapper implements KernelConstants { return defaultHomeWorkspace; } + protected String getGroupsWorkspace() { + // TODO base on JAAS Subject metadata + return defaultGroupsWorkspace; + } + + protected String getGuestsWorkspace() { + // TODO base on JAAS Subject metadata + return defaultGuestsWorkspace; + } + @Override protected void processNewSession(Session session, String workspaceName) { String username = session.getUserID(); @@ -122,7 +140,7 @@ class HomeRepository extends JcrRepositoryWrapper implements KernelConstants { private void initJcr(Session adminSession) { try { // JcrUtils.mkdirs(adminSession, homeBasePath); - JcrUtils.mkdirs(adminSession, groupsBasePath); +// JcrUtils.mkdirs(adminSession, groupsBasePath); adminSession.save(); // JcrUtils.addPrivilege(adminSession, homeBasePath, NodeConstants.ROLE_USER_ADMIN, Privilege.JCR_READ); @@ -146,19 +164,23 @@ class HomeRepository extends JcrRepositoryWrapper implements KernelConstants { try { Node userHome = NodeUtils.getUserHome(adminSession, username); if (userHome == null) { - String homePath = generateUserPath(username); - if (adminSession.itemExists(homePath))// duplicate user id - userHome = adminSession.getNode(homePath).getParent().addNode(JcrUtils.lastPathElement(homePath)); - else - userHome = JcrUtils.mkdirs(adminSession, homePath); - // userHome = JcrUtils.mkfolders(session, homePath); - userHome.addMixin(NodeTypes.NODE_USER_HOME); +// String homePath = generateUserPath(username); + String userId = extractUserId(username); +// if (adminSession.itemExists(homePath))// duplicate user id +// userHome = adminSession.getNode(homePath).getParent().addNode(JcrUtils.lastPathElement(homePath)); +// else +// userHome = JcrUtils.mkdirs(adminSession, homePath); + userHome = adminSession.getRootNode().addNode(userId); +// userHome.addMixin(NodeTypes.NODE_USER_HOME); userHome.addMixin(NodeType.MIX_CREATED); - userHome.setProperty(NodeNames.LDAP_UID, username); + userHome.setProperty(Property.JCR_ID, username); +// userHome.setProperty(NodeNames.LDAP_UID, username); adminSession.save(); - JcrUtils.clearAccessControList(adminSession, homePath, username); - JcrUtils.addPrivilege(adminSession, homePath, username, Privilege.JCR_ALL); + JcrUtils.clearAccessControList(adminSession, userHome.getPath(), username); + JcrUtils.addPrivilege(adminSession, userHome.getPath(), username, Privilege.JCR_ALL); +// JackrabbitSecurityUtils.denyPrivilege(adminSession, userHome.getPath(), NodeConstants.ROLE_USER, +// Privilege.JCR_READ); } if (adminSession.hasPendingChanges()) adminSession.save(); @@ -186,8 +208,26 @@ class HomeRepository extends JcrRepositoryWrapper implements KernelConstants { // } } + private String extractUserId(String username) { + LdapName dn; + try { + dn = new LdapName(username); + } catch (InvalidNameException e) { + throw new CmsException("Invalid name " + username, e); + } + String userId = dn.getRdn(dn.size() - 1).getValue().toString(); + return userId; +// int atIndex = userId.indexOf('@'); +// if (atIndex < 0) { +// return homeBasePath+'/' + userId; +// } else { +// return usersBasePath + '/' + usersDatePath.format(new Date()) + '/' + userId; +// } + } + public void createWorkgroup(LdapName dn) { - Session adminSession = KernelUtils.openAdminSession(this); + String groupsWorkspace = getGroupsWorkspace(); + Session adminSession = KernelUtils.openAdminSession(getRepository(groupsWorkspace), groupsWorkspace); String cn = dn.getRdn(dn.size() - 1).getValue().toString(); Node newWorkgroup = NodeUtils.getGroupHome(adminSession, cn); if (newWorkgroup != null) { @@ -198,10 +238,12 @@ class HomeRepository extends JcrRepositoryWrapper implements KernelConstants { // TODO enhance transformation of cn to a valid node name // String relPath = cn.replaceAll("[^a-zA-Z0-9]", "_"); String relPath = JcrUtils.replaceInvalidChars(cn); - newWorkgroup = JcrUtils.mkdirs(adminSession.getNode(groupsBasePath), relPath, NodeType.NT_UNSTRUCTURED); - newWorkgroup.addMixin(NodeTypes.NODE_GROUP_HOME); + newWorkgroup = adminSession.getRootNode().addNode(relPath, NodeType.NT_UNSTRUCTURED); +// newWorkgroup = JcrUtils.mkdirs(adminSession.getNode(groupsBasePath), relPath, NodeType.NT_UNSTRUCTURED); +// newWorkgroup.addMixin(NodeTypes.NODE_GROUP_HOME); newWorkgroup.addMixin(NodeType.MIX_CREATED); - newWorkgroup.setProperty(NodeNames.LDAP_CN, cn); + newWorkgroup.setProperty(Property.JCR_ID, dn.toString()); +// newWorkgroup.setProperty(NodeNames.LDAP_CN, cn); adminSession.save(); JcrUtils.addPrivilege(adminSession, newWorkgroup.getPath(), dn.toString(), Privilege.JCR_ALL); adminSession.save(); diff --git a/org.argeo.ext.jackrabbit/pom.xml b/org.argeo.ext.jackrabbit/pom.xml index 0f2d2d8f7..14f8f7327 100644 --- a/org.argeo.ext.jackrabbit/pom.xml +++ b/org.argeo.ext.jackrabbit/pom.xml @@ -1,5 +1,7 @@ - + 4.0.0 org.argeo.commons @@ -15,6 +17,11 @@ org.argeo.node.api 2.1.86-SNAPSHOT + + org.argeo.commons + org.argeo.cms + 2.1.86-SNAPSHOT + diff --git a/org.argeo.ext.jackrabbit/src/org/argeo/security/jackrabbit/ArgeoAccessControlProvider.java b/org.argeo.ext.jackrabbit/src/org/argeo/security/jackrabbit/ArgeoAccessControlProvider.java index cd0cf86f2..bffe531a1 100644 --- a/org.argeo.ext.jackrabbit/src/org/argeo/security/jackrabbit/ArgeoAccessControlProvider.java +++ b/org.argeo.ext.jackrabbit/src/org/argeo/security/jackrabbit/ArgeoAccessControlProvider.java @@ -1,6 +1,8 @@ package org.argeo.security.jackrabbit; +import java.security.Principal; import java.util.Map; +import java.util.Set; import javax.jcr.RepositoryException; import javax.jcr.Session; @@ -12,11 +14,17 @@ public class ArgeoAccessControlProvider extends ACLProvider { @SuppressWarnings({ "rawtypes", "unchecked" }) @Override - public void init(Session systemSession, Map configuration) - throws RepositoryException { + public void init(Session systemSession, Map configuration) throws RepositoryException { if (!configuration.containsKey(PARAM_ALLOW_UNKNOWN_PRINCIPALS)) configuration.put(PARAM_ALLOW_UNKNOWN_PRINCIPALS, "true"); + if (!configuration.containsKey(PARAM_OMIT_DEFAULT_PERMISSIONS)) + configuration.put(PARAM_OMIT_DEFAULT_PERMISSIONS, "true"); super.init(systemSession, configuration); } + @Override + public boolean canAccessRoot(Set principals) throws RepositoryException { + return super.canAccessRoot(principals); + } + } diff --git a/org.argeo.ext.jackrabbit/src/org/argeo/security/jackrabbit/ArgeoSecurityManager.java b/org.argeo.ext.jackrabbit/src/org/argeo/security/jackrabbit/ArgeoSecurityManager.java index 15199c0ce..36401596f 100644 --- a/org.argeo.ext.jackrabbit/src/org/argeo/security/jackrabbit/ArgeoSecurityManager.java +++ b/org.argeo.ext.jackrabbit/src/org/argeo/security/jackrabbit/ArgeoSecurityManager.java @@ -16,13 +16,18 @@ package org.argeo.security.jackrabbit; import java.security.Principal; +import java.util.HashSet; +import java.util.Properties; import java.util.Set; +import javax.jcr.Repository; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.security.auth.Subject; import javax.security.auth.x500.X500Principal; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.apache.jackrabbit.api.security.user.UserManager; import org.apache.jackrabbit.core.DefaultSecurityManager; import org.apache.jackrabbit.core.security.AMContext; @@ -31,12 +36,26 @@ import org.apache.jackrabbit.core.security.SecurityConstants; import org.apache.jackrabbit.core.security.SystemPrincipal; import org.apache.jackrabbit.core.security.authorization.WorkspaceAccessManager; import org.apache.jackrabbit.core.security.principal.AdminPrincipal; +import org.apache.jackrabbit.core.security.principal.PrincipalProvider; +import org.argeo.cms.auth.CmsSession; import org.argeo.node.NodeConstants; import org.argeo.node.security.AnonymousPrincipal; import org.argeo.node.security.DataAdminPrincipal; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; /** Customises Jackrabbit security. */ public class ArgeoSecurityManager extends DefaultSecurityManager { + private final static Log log = LogFactory.getLog(ArgeoSecurityManager.class); + + private BundleContext cmsBundleContext = null; + + public ArgeoSecurityManager() { + if (FrameworkUtil.getBundle(CmsSession.class) != null) { + cmsBundleContext = FrameworkUtil.getBundle(CmsSession.class).getBundleContext(); + } + } + @Override public AccessManager getAccessManager(Session session, AMContext amContext) throws RepositoryException { synchronized (getSystemSession()) { @@ -51,6 +70,11 @@ public class ArgeoSecurityManager extends DefaultSecurityManager { } } + @Override + protected PrincipalProvider createDefaultPrincipalProvider(Properties[] moduleConfig) throws RepositoryException { + return super.createDefaultPrincipalProvider(moduleConfig); + } + /** Called once when the session is created */ @Override public String getUserID(Subject subject, String workspaceName) throws RepositoryException { @@ -59,6 +83,13 @@ public class ArgeoSecurityManager extends DefaultSecurityManager { boolean isJackrabbitSystem = !subject.getPrincipals(SystemPrincipal.class).isEmpty(); Set userPrincipal = subject.getPrincipals(X500Principal.class); boolean isRegularUser = !userPrincipal.isEmpty(); + CmsSession cmsSession = null; + if (cmsBundleContext != null) { + cmsSession = CmsSession.getCmsSession(cmsBundleContext, subject); + if (log.isTraceEnabled()) + log.trace("Opening JCR session for CMS session " + cmsSession); + } + if (isAnonymous) { if (isDataAdmin || isJackrabbitSystem || isRegularUser) throw new IllegalStateException("Inconsistent " + subject); @@ -96,7 +127,10 @@ public class ArgeoSecurityManager extends DefaultSecurityManager { @Override protected WorkspaceAccessManager createDefaultWorkspaceAccessManager() { WorkspaceAccessManager wam = super.createDefaultWorkspaceAccessManager(); - return new ArgeoWorkspaceAccessManagerImpl(wam); + ArgeoWorkspaceAccessManagerImpl workspaceAccessManager = new ArgeoWorkspaceAccessManagerImpl(wam); + if (log.isTraceEnabled()) + log.trace("Created workspace access manager"); + return workspaceAccessManager; } private class ArgeoWorkspaceAccessManagerImpl implements SecurityConstants, WorkspaceAccessManager { @@ -109,6 +143,10 @@ public class ArgeoSecurityManager extends DefaultSecurityManager { public void init(Session systemSession) throws RepositoryException { wam.init(systemSession); + Repository repository = systemSession.getRepository(); + if (log.isTraceEnabled()) + log.trace("Initialised workspace access manager on repository " + repository + + ", systemSession workspace: " + systemSession.getWorkspace().getName()); } public void close() throws RepositoryException { @@ -116,7 +154,10 @@ public class ArgeoSecurityManager extends DefaultSecurityManager { public boolean grants(Set principals, String workspaceName) throws RepositoryException { // TODO: implements finer access to workspaces + if (log.isTraceEnabled()) + log.trace("Grants " + new HashSet<>(principals) + " access to workspace '" + workspaceName + "'"); return true; + // return wam.grants(principals, workspaceName); } } diff --git a/org.argeo.jcr/src/org/argeo/jackrabbit/security/JackrabbitSecurityUtils.java b/org.argeo.jcr/src/org/argeo/jackrabbit/security/JackrabbitSecurityUtils.java new file mode 100644 index 000000000..a75c79541 --- /dev/null +++ b/org.argeo.jcr/src/org/argeo/jackrabbit/security/JackrabbitSecurityUtils.java @@ -0,0 +1,80 @@ +package org.argeo.jackrabbit.security; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.security.Privilege; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlManager; +import org.argeo.jcr.JcrUtils; + +/** Utilities around Jackrabbit security extensions. */ +public class JackrabbitSecurityUtils { + private final static Log log = LogFactory.getLog(JackrabbitSecurityUtils.class); + + /** + * Convenience method for denying a single privilege to a principal (user or + * role), typically jcr:all + */ + public synchronized static void denyPrivilege(Session session, String path, String principal, String privilege) + throws RepositoryException { + List privileges = new ArrayList(); + privileges.add(session.getAccessControlManager().privilegeFromName(privilege)); + denyPrivileges(session, path, () -> principal, privileges); + } + + /** + * Deny privileges on a path to a {@link Principal}. The path must already + * exist. Session is saved. Synchronized to prevent concurrent modifications of + * the same node. + */ + public synchronized static Boolean denyPrivileges(Session session, String path, Principal principal, + List privs) throws RepositoryException { + // make sure the session is in line with the persisted state + session.refresh(false); + JackrabbitAccessControlManager acm = (JackrabbitAccessControlManager) session.getAccessControlManager(); + JackrabbitAccessControlList acl = (JackrabbitAccessControlList) JcrUtils.getAccessControlList(acm, path); + +// accessControlEntries: for (AccessControlEntry ace : acl.getAccessControlEntries()) { +// Principal currentPrincipal = ace.getPrincipal(); +// if (currentPrincipal.getName().equals(principal.getName())) { +// Privilege[] currentPrivileges = ace.getPrivileges(); +// if (currentPrivileges.length != privs.size()) +// break accessControlEntries; +// for (int i = 0; i < currentPrivileges.length; i++) { +// Privilege currP = currentPrivileges[i]; +// Privilege p = privs.get(i); +// if (!currP.getName().equals(p.getName())) { +// break accessControlEntries; +// } +// } +// return false; +// } +// } + + Privilege[] privileges = privs.toArray(new Privilege[privs.size()]); + acl.addEntry(principal, privileges, false); + acm.setPolicy(path, acl); + if (log.isDebugEnabled()) { + StringBuffer privBuf = new StringBuffer(); + for (Privilege priv : privs) + privBuf.append(priv.getName()); + log.debug("Denied privileges " + privBuf + " to " + principal.getName() + " on " + path + " in '" + + session.getWorkspace().getName() + "'"); + } + session.refresh(true); + session.save(); + return true; + } + + /** Singleton. */ + private JackrabbitSecurityUtils() { + + } +} diff --git a/org.argeo.jcr/src/org/argeo/jcr/JcrUtils.java b/org.argeo.jcr/src/org/argeo/jcr/JcrUtils.java index d78ccae8d..416d035f8 100644 --- a/org.argeo.jcr/src/org/argeo/jcr/JcrUtils.java +++ b/org.argeo.jcr/src/org/argeo/jcr/JcrUtils.java @@ -1308,23 +1308,30 @@ public class JcrUtils { return true; } - /** Gets access control list for this path, throws exception if not found */ + /** + * Gets the first available access control list for this path, throws exception + * if not found + */ public synchronized static AccessControlList getAccessControlList(AccessControlManager acm, String path) throws RepositoryException { // search for an access control list AccessControlList acl = null; AccessControlPolicyIterator policyIterator = acm.getApplicablePolicies(path); - if (policyIterator.hasNext()) { + applicablePolicies: if (policyIterator.hasNext()) { while (policyIterator.hasNext()) { AccessControlPolicy acp = policyIterator.nextAccessControlPolicy(); - if (acp instanceof AccessControlList) + if (acp instanceof AccessControlList) { acl = ((AccessControlList) acp); + break applicablePolicies; + } } } else { AccessControlPolicy[] existingPolicies = acm.getPolicies(path); - for (AccessControlPolicy acp : existingPolicies) { - if (acp instanceof AccessControlList) + existingPolicies: for (AccessControlPolicy acp : existingPolicies) { + if (acp instanceof AccessControlList) { acl = ((AccessControlList) acp); + break existingPolicies; + } } } if (acl != null) diff --git a/org.argeo.node.api/bnd.bnd b/org.argeo.node.api/bnd.bnd index 52a2bdd57..5b235f666 100644 --- a/org.argeo.node.api/bnd.bnd +++ b/org.argeo.node.api/bnd.bnd @@ -1 +1 @@ -Provide-Capability: cms.datamodel;name=node;cnd=/org/argeo/node/node.cnd \ No newline at end of file +Provide-Capability: cms.datamodel;name=node;cnd=/org/argeo/node/ldap.cnd;abstract=true diff --git a/org.argeo.node.api/src/org/argeo/node/NodeConstants.java b/org.argeo.node.api/src/org/argeo/node/NodeConstants.java index 31029d991..067d8c317 100644 --- a/org.argeo.node.api/src/org/argeo/node/NodeConstants.java +++ b/org.argeo.node.api/src/org/argeo/node/NodeConstants.java @@ -24,6 +24,9 @@ public interface NodeConstants { */ String NODE = "node"; String HOME = "home"; + String GROUPS = "groups"; + String GUESTS = "guests"; + String PUBLIC = "public"; /* * BASE DNs diff --git a/org.argeo.node.api/src/org/argeo/node/NodeNames.java b/org.argeo.node.api/src/org/argeo/node/NodeNames.java index 05b86ffdc..7ded1115d 100644 --- a/org.argeo.node.api/src/org/argeo/node/NodeNames.java +++ b/org.argeo.node.api/src/org/argeo/node/NodeNames.java @@ -16,6 +16,7 @@ package org.argeo.node; /** JCR types in the http://www.argeo.org/node namespace */ +@Deprecated public interface NodeNames { String LDAP_UID = "ldap:"+NodeConstants.UID; String LDAP_CN = "ldap:"+NodeConstants.CN; diff --git a/org.argeo.node.api/src/org/argeo/node/NodeTypes.java b/org.argeo.node.api/src/org/argeo/node/NodeTypes.java index bfb55e175..891f54659 100644 --- a/org.argeo.node.api/src/org/argeo/node/NodeTypes.java +++ b/org.argeo.node.api/src/org/argeo/node/NodeTypes.java @@ -16,6 +16,7 @@ package org.argeo.node; /** JCR types in the http://www.argeo.org/node namespace */ +@Deprecated public interface NodeTypes { String NODE_USER_HOME = "node:userHome"; String NODE_GROUP_HOME = "node:groupHome"; diff --git a/org.argeo.node.api/src/org/argeo/node/NodeUtils.java b/org.argeo.node.api/src/org/argeo/node/NodeUtils.java index 375e916d5..378ec3322 100644 --- a/org.argeo.node.api/src/org/argeo/node/NodeUtils.java +++ b/org.argeo.node.api/src/org/argeo/node/NodeUtils.java @@ -28,11 +28,8 @@ import javax.jcr.RepositoryFactory; import javax.jcr.Session; import javax.jcr.query.Query; import javax.jcr.query.QueryResult; -import javax.jcr.query.qom.Constraint; -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.naming.InvalidNameException; +import javax.naming.ldap.LdapName; import javax.security.auth.AuthPermission; import javax.security.auth.Subject; import javax.security.auth.login.LoginContext; @@ -91,39 +88,102 @@ public class NodeUtils { * @param username the username of the user */ public static Node getUserHome(Session session, String username) { +// try { +// QueryObjectModelFactory qomf = session.getWorkspace().getQueryManager().getQOMFactory(); +// Selector sel = qomf.selector(NodeTypes.NODE_USER_HOME, "sel"); +// DynamicOperand dop = qomf.propertyValue(sel.getSelectorName(), NodeNames.LDAP_UID); +// StaticOperand sop = qomf.literal(session.getValueFactory().createValue(username)); +// Constraint constraint = qomf.comparison(dop, QueryObjectModelFactory.JCR_OPERATOR_EQUAL_TO, sop); +// Query query = qomf.createQuery(sel, constraint, null, null); +// return querySingleNode(query); +// } catch (RepositoryException e) { +// throw new RuntimeException("Cannot find home for user " + username, e); +// } + try { - QueryObjectModelFactory qomf = session.getWorkspace().getQueryManager().getQOMFactory(); - Selector sel = qomf.selector(NodeTypes.NODE_USER_HOME, "sel"); - DynamicOperand dop = qomf.propertyValue(sel.getSelectorName(), NodeNames.LDAP_UID); - StaticOperand sop = qomf.literal(session.getValueFactory().createValue(username)); - Constraint constraint = qomf.comparison(dop, QueryObjectModelFactory.JCR_OPERATOR_EQUAL_TO, sop); - Query query = qomf.createQuery(sel, constraint, null, null); - return querySingleNode(query); + checkUserWorkspace(session, username); + String homePath = getHomePath(username); + if (session.itemExists(homePath)) + return session.getNode(homePath); + // legacy + homePath = "/home/" + username; + if (session.itemExists(homePath)) + return session.getNode(homePath); + return null; } catch (RepositoryException e) { throw new RuntimeException("Cannot find home for user " + username, e); } } + private static String getHomePath(String username) { + LdapName dn; + try { + dn = new LdapName(username); + } catch (InvalidNameException e) { + throw new IllegalArgumentException("Invalid name " + username, e); + } + String userId = dn.getRdn(dn.size() - 1).getValue().toString(); + return '/' + userId; + } + + private static void checkUserWorkspace(Session session, String username) { + String workspaceName = session.getWorkspace().getName(); + if (!NodeConstants.HOME.equals(workspaceName)) + throw new IllegalArgumentException(workspaceName + " is not the home workspace for user " + username); + } + /** * 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 groupname the name of the group */ - public static Node getGroupHome(Session session, String cn) { + public static Node getGroupHome(Session session, String groupname) { +// try { +// QueryObjectModelFactory qomf = session.getWorkspace().getQueryManager().getQOMFactory(); +// Selector sel = qomf.selector(NodeTypes.NODE_GROUP_HOME, "sel"); +// DynamicOperand dop = qomf.propertyValue(sel.getSelectorName(), NodeNames.LDAP_CN); +// StaticOperand sop = qomf.literal(session.getValueFactory().createValue(cn)); +// Constraint constraint = qomf.comparison(dop, QueryObjectModelFactory.JCR_OPERATOR_EQUAL_TO, sop); +// Query query = qomf.createQuery(sel, constraint, null, null); +// return querySingleNode(query); +// } catch (RepositoryException e) { +// throw new RuntimeException("Cannot find home for group " + cn, e); +// } + try { - QueryObjectModelFactory qomf = session.getWorkspace().getQueryManager().getQOMFactory(); - Selector sel = qomf.selector(NodeTypes.NODE_GROUP_HOME, "sel"); - DynamicOperand dop = qomf.propertyValue(sel.getSelectorName(), NodeNames.LDAP_CN); - StaticOperand sop = qomf.literal(session.getValueFactory().createValue(cn)); - Constraint constraint = qomf.comparison(dop, QueryObjectModelFactory.JCR_OPERATOR_EQUAL_TO, sop); - Query query = qomf.createQuery(sel, constraint, null, null); - return querySingleNode(query); + checkGroupWorkspace(session, groupname); + String homePath = getGroupPath(groupname); + if (session.itemExists(homePath)) + return session.getNode(homePath); + // legacy + homePath = "/groups/" + groupname; + if (session.itemExists(homePath)) + return session.getNode(homePath); + return null; } catch (RepositoryException e) { - throw new RuntimeException("Cannot find home for user " + cn, e); + throw new RuntimeException("Cannot find home for group " + groupname, e); + } + + } + + private static String getGroupPath(String groupname) { + String cn; + try { + LdapName dn = new LdapName(groupname); + cn = dn.getRdn(dn.size() - 1).getValue().toString(); + } catch (InvalidNameException e) { + cn = groupname; } + return '/' + cn; + } + + private static void checkGroupWorkspace(Session session, String groupname) { + String workspaceName = session.getWorkspace().getName(); + if (!NodeConstants.GROUPS.equals(workspaceName)) + throw new IllegalArgumentException(workspaceName + " is not the group workspace for group " + groupname); } /** diff --git a/org.argeo.node.api/src/org/argeo/node/ldap.cnd b/org.argeo.node.api/src/org/argeo/node/ldap.cnd new file mode 100644 index 000000000..a2306c60e --- /dev/null +++ b/org.argeo.node.api/src/org/argeo/node/ldap.cnd @@ -0,0 +1 @@ + diff --git a/org.argeo.node.api/src/org/argeo/node/node.cnd b/org.argeo.node.api/src/org/argeo/node/node.cnd index 1a6dec5ef..d8a26b64e 100644 --- a/org.argeo.node.api/src/org/argeo/node/node.cnd +++ b/org.argeo.node.api/src/org/argeo/node/node.cnd @@ -1,4 +1,3 @@ - [node:userHome] -- 2.30.2