From c4e183ce5a0ef5f718f02f7da5364c926b224c32 Mon Sep 17 00:00:00 2001 From: Konrad Windszus Date: Thu, 27 Feb 2025 10:12:29 +0100 Subject: [PATCH] Support principal-based authorization WIP Open issues: Combination of resource and principal based ACEs for same authorizable Usage in YAML This closes #369 --- accesscontroltool-bundle/pom.xml | 1 - .../aceinstaller/AceBeanInstallerClassic.java | 4 +- .../AceBeanInstallerIncremental.java | 10 +-- .../aceinstaller/BaseAceBeanInstaller.java | 36 ++++++++- .../cq/tools/actool/aem/AcToolCqActions.java | 2 +- .../cq/tools/actool/configmodel/AceBean.java | 11 +++ .../actool/configreader/YamlConfigReader.java | 25 +++--- .../actool/helper/AccessControlUtils.java | 78 ++++++++++++------- docs/Configuration.md | 1 + pom.xml | 9 ++- 10 files changed, 122 insertions(+), 55 deletions(-) diff --git a/accesscontroltool-bundle/pom.xml b/accesscontroltool-bundle/pom.xml index 828f7dd7e..c47a7efd7 100644 --- a/accesscontroltool-bundle/pom.xml +++ b/accesscontroltool-bundle/pom.xml @@ -214,7 +214,6 @@ com.adobe.aem uber-jar - apis provided diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/aceinstaller/AceBeanInstallerClassic.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/aceinstaller/AceBeanInstallerClassic.java index e52f5b954..b0b8dff34 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/aceinstaller/AceBeanInstallerClassic.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/aceinstaller/AceBeanInstallerClassic.java @@ -91,7 +91,7 @@ private void installAce(AceBean aceBean, final Session session, Principal princi final AccessControlManager acMgr = session.getAccessControlManager(); - JackrabbitAccessControlList acl = AccessControlUtils.getModifiableAcl(acMgr, aceBean.getJcrPathForPolicyApi()); + JackrabbitAccessControlList acl = getAccessControlList(acMgr, aceBean); if (acl == null) { installLog.addMessage(LOG, "Skipped installing privileges/actions for non existing path: " + aceBean.getJcrPath()); return; @@ -139,7 +139,7 @@ private JackrabbitAccessControlList installActions(AceBean aceBean, Principal pr Collection inheritedAllows = cqActions.getAllowedActions( aceBean.getJcrPathForPolicyApi(), Collections.singleton(principal)); // this does always install new entries - cqActions.installActions(aceBean.getJcrPathForPolicyApi(), principal, actionMap, inheritedAllows); + cqActions.installActions(aceBean.isPrincipalBased(), aceBean.getJcrPathForPolicyApi(), principal, actionMap, inheritedAllows); // since the aclist has been modified, retrieve it again final JackrabbitAccessControlList newAcl = AccessControlUtils.getAccessControlList(session, aceBean.getJcrPath()); diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/aceinstaller/AceBeanInstallerIncremental.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/aceinstaller/AceBeanInstallerIncremental.java index 8ace16749..3d3e1fc0a 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/aceinstaller/AceBeanInstallerIncremental.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/aceinstaller/AceBeanInstallerIncremental.java @@ -205,12 +205,6 @@ private Set filterInitialContentOnlyNodes(Set aceBeanSetFromCo return aceBeanSetNoInitialContentOnlyNodes; } - // to be overwritten in JUnit Test - protected JackrabbitAccessControlList getAccessControlList(AccessControlManager acMgr, String path) throws RepositoryException { - JackrabbitAccessControlList acl = AccessControlUtils.getModifiableAcl(acMgr, path); - return acl; - } - private Set transformActionsIntoPrivileges(Set aceBeanSetFromConfig, Session session, InstallationLogger installLog) throws RepositoryException { @@ -289,7 +283,7 @@ Set getPrincipalAceBeansForActionAceBean(AceBean origAceBean, Session s Principal testActionMapperPrincipal = getTestActionMapperPrincipal(); applyCqActions(origAceBean, session, testActionMapperPrincipal); - JackrabbitAccessControlList newAcl = getAccessControlList(session.getAccessControlManager(), origAceBean.getJcrPathForPolicyApi()); + JackrabbitAccessControlList newAcl = getAccessControlList(session.getAccessControlManager(), origAceBean); boolean isFirst = true; for (AccessControlEntry newAce : newAcl.getAccessControlEntries()) { @@ -371,7 +365,7 @@ void applyCqActions(AceBean origAceBean, Session session, Principal principal) t Collection inheritedAllows = cqActions.getAllowedActions(origAceBean.getJcrPathForPolicyApi(), Collections.singleton(principal)); // this does always install new entries - cqActions.installActions(origAceBean.getJcrPath(), principal, origAceBean.getActionMap(), inheritedAllows); + cqActions.installActions(origAceBean.isPrincipalBased(), origAceBean.getJcrPath(), principal, origAceBean.getActionMap(), inheritedAllows); } diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/aceinstaller/BaseAceBeanInstaller.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/aceinstaller/BaseAceBeanInstaller.java index d39d629d8..67499f8ca 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/aceinstaller/BaseAceBeanInstaller.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/aceinstaller/BaseAceBeanInstaller.java @@ -34,6 +34,8 @@ import org.apache.commons.lang3.time.StopWatch; import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlManager; +import org.apache.jackrabbit.oak.spi.security.principal.PrincipalImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -76,7 +78,7 @@ public void installPathBasedACEs( .get(path); // Set which holds the AceBeans of the current path in configuration // check if the path even exists - final boolean pathExits = AccessControlUtils.getModifiableAcl(session.getAccessControlManager(), path) != null; + final boolean pathExits = getAccessControlList(getModifiableAcl(session.getAccessControlManager(), ace path) != null; if (!pathExits) { if (!ContentHelper.createInitialContent(session, history, path, aceBeanSetFromConfig)) { history.addVerboseMessage(LOG, "Skipped installing privileges/actions for non existing path: " + path); @@ -171,11 +173,15 @@ protected RestrictionsHolder getRestrictions(AceBean aceBean, Session session, J final Collection supportedRestrictionNames = Arrays.asList(acl.getRestrictionNames()); + List restrictions = aceBean.getRestrictions(); + if (aceBean.isPrincipalBased()) { + // special restriction for principal based ACEs (https://jackrabbit.apache.org/oak/docs/security/authorization/principalbased.html) + restrictions.add(new Restriction("rep:nodePath", aceBean.getJcrPath())); + } if (aceBean.getRestrictions().isEmpty()) { return RestrictionsHolder.empty(); } - List restrictions = aceBean.getRestrictions(); for (Restriction restriction : restrictions) { if (!supportedRestrictionNames.contains(restriction.getName())) { throw new IllegalStateException( @@ -210,4 +216,30 @@ public Set getPrivilegeSet(String[] privNames, AccessControlManager a return privileges; } + protected JackrabbitAccessControlList getAccessControlList(AccessControlManager acMgr, AceBean aceBean) throws RepositoryException { + return getAccessControlList(acMgr, aceBean.isPrincipalBased(), aceBean.getPrincipalName(), aceBean.getJcrPathForPolicyApi()); + } + + protected JackrabbitAccessControlList getAccessControlList(AccessControlManager acMgr, boolean isPrincipalBased, String principal, String path) throws RepositoryException { + if (isPrincipalBased) { + return getPrincipalBasedAccessControlList(acMgr, new PrincipalImpl(principal)); + } else { + return getResourceBasedAccessControlList(acMgr, path); + } + } + + // to be overwritten in JUnit Test + protected JackrabbitAccessControlList getResourceBasedAccessControlList(AccessControlManager acMgr, String path) throws RepositoryException { + JackrabbitAccessControlList acl = AccessControlUtils.getModifiableAcl(acMgr, path); + return acl; + } + + protected JackrabbitAccessControlList getPrincipalBasedAccessControlList(AccessControlManager acMgr, Principal principal) throws RepositoryException { + if (acMgr instanceof JackrabbitAccessControlManager) { + return getPrincipalBasedAccessControlList((JackrabbitAccessControlManager) acMgr, principal); + } else { + throw new RepositoryException("AccessControlManager is not a JackrabbitAccessControlManager"); + } + } + } diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/aem/AcToolCqActions.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/aem/AcToolCqActions.java index 3491e80d6..d603684cb 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/aem/AcToolCqActions.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/aem/AcToolCqActions.java @@ -118,7 +118,7 @@ public Collection getAllowedActions(String nodePath, Set prin return granted; } - public void installActions(String nodePath, Principal principal, Map actionMap, Collection inheritedAllows) + public void installActions(boolean isPrincipalBased, String nodePath, Principal principal, Map actionMap, Collection inheritedAllows) throws RepositoryException { if (actionMap.isEmpty()) { return; diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configmodel/AceBean.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configmodel/AceBean.java index 00c195e25..bccfdaa8b 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configmodel/AceBean.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configmodel/AceBean.java @@ -47,12 +47,14 @@ public class AceBean implements AcDumpElement { private List restrictions = new ArrayList(); private boolean keepOrder = false; // default is to reorder denies before allows + private boolean isPrincipalBased; private String initialContent; // config source (only for logging) private String configSource; + public static final String RESTRICTION_NAME_GLOB = "rep:glob"; @Override @@ -68,6 +70,7 @@ public AceBean clone() { clone.setRestrictions(new ArrayList(restrictions)); clone.setInitialContent(initialContent); clone.setKeepOrder(keepOrder); + clone.setIsPrincipalBased(isPrincipalBased); clone.setConfigSource(configSource +" (cloned)"); return clone; @@ -362,4 +365,12 @@ public boolean isInitialContentOnlyConfig() { && StringUtils.isBlank(actionsStringFromConfig); } + public boolean isPrincipalBased() { + return isPrincipalBased; + } + + public void setIsPrincipalBased(boolean isPrincipalBased) { + this.isPrincipalBased = isPrincipalBased; + } + } diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/YamlConfigReader.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/YamlConfigReader.java index c900a1a7c..77117fd47 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/YamlConfigReader.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/YamlConfigReader.java @@ -68,6 +68,7 @@ public class YamlConfigReader implements ConfigReader { protected static final String ACE_CONFIG_PROPERTY_PATH = "path"; protected static final String ACE_CONFIG_PROPERTY_KEEP_ORDER = "keepOrder"; protected static final String ACE_CONFIG_INITIAL_CONTENT = "initialContent"; + protected static final String ACE_CONFIG_PROPERTY_IS_PRINCIPAL_BASED = "isPrincipalBased"; private static final String GROUP_CONFIG_PROPERTY_IS_MEMBER_OF = "isMemberOf"; private static final String GROUP_CONFIG_PROPERTY_MEMBER_OF_LEGACY = "memberOf"; @@ -344,29 +345,29 @@ protected AuthorizableConfigBean getNewAuthorizableConfigBean() { return new AuthorizableConfigBean(); } - protected void setupAceBean(final String authorizableId, final Map currentAceDefinition, final AceBean aclBean, String sourceFile) { + protected void setupAceBean(final String authorizableId, final Map currentAceDefinition, final AceBean aceBean, String sourceFile) { - aclBean.setAuthorizableId(authorizableId); - aclBean.setPrincipalName(authorizableId); // to ensure it is set, later corrected if necessary in + aceBean.setAuthorizableId(authorizableId); + aceBean.setPrincipalName(authorizableId); // to ensure it is set, later corrected if necessary in // AcConfiguration.ensureAceBeansHaveCorrectPrincipalNameSet() String jcrPath = getMapValueAsString(currentAceDefinition, ACE_CONFIG_PROPERTY_PATH).trim(); // remove trailing slashes (but retain simple slashes) jcrPath = (!jcrPath.equals("/") && jcrPath.endsWith("/")) ? StringUtils.removeEnd(jcrPath, "/") : jcrPath; - aclBean.setJcrPath(jcrPath); + aceBean.setJcrPath(jcrPath); - aclBean.setPrivilegesString(getMapValueAsString(currentAceDefinition, ACE_CONFIG_PROPERTY_PRIVILEGES)); - aclBean.setPermission(getMapValueAsString(currentAceDefinition, ACE_CONFIG_PROPERTY_PERMISSION)); + aceBean.setPrivilegesString(getMapValueAsString(currentAceDefinition, ACE_CONFIG_PROPERTY_PRIVILEGES)); + aceBean.setPermission(getMapValueAsString(currentAceDefinition, ACE_CONFIG_PROPERTY_PERMISSION)); - aclBean.setRestrictions(currentAceDefinition.get(ACE_CONFIG_PROPERTY_RESTRICTIONS),(String) currentAceDefinition.get(ACE_CONFIG_PROPERTY_GLOB)); - aclBean.setActions(parseActionsString(getMapValueAsString(currentAceDefinition, ACE_CONFIG_PROPERTY_ACTIONS))); + aceBean.setRestrictions(currentAceDefinition.get(ACE_CONFIG_PROPERTY_RESTRICTIONS),(String) currentAceDefinition.get(ACE_CONFIG_PROPERTY_GLOB)); + aceBean.setActions(parseActionsString(getMapValueAsString(currentAceDefinition, ACE_CONFIG_PROPERTY_ACTIONS))); - aclBean.setKeepOrder(Boolean.valueOf(getMapValueAsString(currentAceDefinition, ACE_CONFIG_PROPERTY_KEEP_ORDER))); + aceBean.setKeepOrder(Boolean.valueOf(getMapValueAsString(currentAceDefinition, ACE_CONFIG_PROPERTY_KEEP_ORDER))); String initialContent = getMapValueAsString(currentAceDefinition, ACE_CONFIG_INITIAL_CONTENT); - aclBean.setInitialContent(initialContent); - - aclBean.setConfigSource(sourceFile); + aceBean.setInitialContent(initialContent); + aceBean.setConfigSource(sourceFile); + aceBean.setIsPrincipalBased(Boolean.parseBoolean(getMapValueAsString(currentAceDefinition, ACE_CONFIG_PROPERTY_IS_PRINCIPAL_BASED))); } public static String[] parseActionsString(final String actionsStringFromConfig) { diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/helper/AccessControlUtils.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/helper/AccessControlUtils.java index f47a0831d..96f4a29dd 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/helper/AccessControlUtils.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/helper/AccessControlUtils.java @@ -15,6 +15,8 @@ import java.security.Principal; import java.util.HashSet; +import java.util.Iterator; +import java.util.Optional; import java.util.Set; import javax.jcr.AccessDeniedException; @@ -29,12 +31,14 @@ import javax.jcr.security.AccessControlPolicyIterator; import javax.jcr.security.Privilege; +import org.apache.commons.collections4.IteratorUtils; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.jackrabbit.api.JackrabbitSession; import org.apache.jackrabbit.api.security.JackrabbitAccessControlEntry; import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; import org.apache.jackrabbit.api.security.JackrabbitAccessControlManager; +import org.apache.jackrabbit.api.security.authorization.PrincipalAccessControlList; import org.apache.jackrabbit.api.security.user.UserManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -205,44 +209,64 @@ public static int deleteAllEntriesForPrincipalsFromACL(final Session session, return countRemoved; } - /** Retrieves JackrabbitAccessControlList for path. + /** Retrieves (resource-based) {@link JackrabbitAccessControlList} for path. * * @param acMgr * @param path - * @return + * @return JackrabbitAccessControlList or {@code null} if the path does not exist. * @throws RepositoryException * @throws AccessDeniedException */ - public static JackrabbitAccessControlList getModifiableAcl( - AccessControlManager acMgr, String path) throws RepositoryException, AccessDeniedException { - - if (StringUtils.isBlank(path)) { - path = null; // repository level permission - } - - AccessControlPolicy[] existing = null; + public static JackrabbitAccessControlList getModifiableAcl(final + AccessControlManager acMgr, final String path) throws RepositoryException { + final String normalizedPath = StringUtils.stripToNull(path); try { - existing = acMgr.getPolicies(path); + AccessControlPolicy[] existingPolicies = acMgr.getPolicies(normalizedPath); + Optional firstMatchingPolicyOfType = getFirstMatchingPolicyOfType(existingPolicies, JackrabbitAccessControlList.class); + if (!firstMatchingPolicyOfType.isPresent()) { + AccessControlPolicyIterator applicablePoliciesIterator = acMgr.getApplicablePolicies(normalizedPath); + return getFirstMatchingPolicyOfType((Iterator)applicablePoliciesIterator, JackrabbitAccessControlList.class) + .orElseThrow(() -> new AccessControlException("No modifiable ACL at " + normalizedPath)); + } else { + return firstMatchingPolicyOfType.get(); + } } catch (final PathNotFoundException e) { - LOG.debug("No node could be found under: {}. Application of ACL for that node cancelled!", path); + LOG.debug("No node could be found under: {}. Application of ACL for that node cancelled!", normalizedPath); } - if (existing != null) { - for (final AccessControlPolicy p : existing) { - if (p instanceof JackrabbitAccessControlList) { - return ((JackrabbitAccessControlList) p); - } - } + return null; + } - final AccessControlPolicyIterator it = acMgr.getApplicablePolicies(path); - while (it.hasNext()) { - final AccessControlPolicy p = it.nextAccessControlPolicy(); - if (p instanceof JackrabbitAccessControlList) { - return ((JackrabbitAccessControlList) p); - } - } + /** Retrieves (principal-based) {@link PrincipalAccessControlList} for principal. + * + * @param acMgr + * @param principal principal for which to retrieve the ACL + * @return PrincipalAccessControlList (never {@code null}) + * @throws RepositoryException + */ + public static PrincipalAccessControlList getPrincipalBasedModifiableAcl(final JackrabbitAccessControlManager acMgr, final Principal principal) + throws RepositoryException { + AccessControlPolicy[] existingPolicies = acMgr.getPolicies(principal); + Optional firstMatchingPolicyOfType = getFirstMatchingPolicyOfType(existingPolicies, PrincipalAccessControlList.class); + if (!firstMatchingPolicyOfType.isPresent()) { + AccessControlPolicy[] applicablePolicies = acMgr.getApplicablePolicies(principal); + return getFirstMatchingPolicyOfType(applicablePolicies, PrincipalAccessControlList.class) + .orElseThrow(() -> new AccessControlException("No principal ACL for " + principal.getName())); + } else { + return firstMatchingPolicyOfType.get(); + } + } + + static Optional getFirstMatchingPolicyOfType(AccessControlPolicy[] policies, Class policyType) { + return getFirstMatchingPolicyOfType(IteratorUtils.arrayIterator(policies), policyType); + } - throw new AccessControlException("No modifiable ACL at " + path); + static Optional getFirstMatchingPolicyOfType(Iterator policiesIterator, Class policyType) { + while (policiesIterator.hasNext()) { + AccessControlPolicy policy = policiesIterator.next(); + if (policyType.isInstance(policy)) { + return Optional.of(policyType.cast(policy)); + } } - return null; + return Optional.empty(); } /** Returns user manager for session disabling autoSave if applicable. diff --git a/docs/Configuration.md b/docs/Configuration.md index b9fb460f5..ca01e8fe3 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -207,6 +207,7 @@ repGlob |A [repGlob expression](https://jackrabbit.apache.org/oak/docs/security/ restrictions|An associative array of restriction entries. Each entry uses the restriction name as key (e.g. `rep:glob`) and a literal as value. Values for multi-valued restrictions (like e.g. `rep:ntNames`) are also given as YAML string literals with commas separating each value (not using YAML arrays, in line with how isMemberOf is configured). Arbitrary restrictions are supported as long as they are supported by the underlying repository on which the installation takes place (validated before installation starts). For an overview of built-in restrictions in different Oak versions see: [Oak Restriction Management](https://jackrabbit.apache.org/oak/docs/security/authorization/restriction.html#Built-in_Restrictions). Additional restrictions are provided by [Sling Oak Restrictions](https://sling.apache.org/documentation/bundles/sling-oak-restrictions.html) and [AEM Advanced Restrictions](https://github.com/IBM/aem-advanced-restrictions). Available from version 1.9.0.| no initialContent | Allows to specify [enhanced docview xml](https://jackrabbit.apache.org/filevault/docview.html) to create the path if it does not exist. The namespaces for jcr, sling and cq are added automatically if not provided to keep xml short. Initial content must only be specified exactly once per path (this is validated). If paths without permissions should be created, it is possible to provide only a path/initialContent tuple. See also [Providing Initial Content](AdvancedFeatures.md#providing-initial-content) | no keepOrder | Defaults to false - use carefully only for special use cases as described in [Use Manual ACL Ordering](AdvancedFeatures.md#use-manual-acl-ordering) | no +isPrincipalBased | Defaults to false, set to true to use [Principal-based access control list](https://jackrabbit.apache.org/oak/docs/security/authorization/principalbased.html). The first ACE entry below a principal determines this for all following entries belonging to the same principal. This is only supported in AEMaaCS and AEM 6.6. Several limitations apply when using principal-based ACLs. Every new data entry starts with a "-". The rules are sorted so that deny rules are always on top of allow rules. ACEs that are not part of the config file will not be reordered and stay on very top. This way allow rules are evaluated first, then deny rules and finally any system rules. diff --git a/pom.xml b/pom.xml index f79fac163..2c84162a1 100644 --- a/pom.xml +++ b/pom.xml @@ -106,14 +106,19 @@ - + io.wcm.maven io.wcm.maven.aem-dependencies - 6.4.0.0004 + 6.5.9.0000 pom import + + org.osgi + org.osgi.service.url + 1.0.1 + org.yaml