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 c88aebbb..5cd8a90f 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 @@ -40,6 +40,7 @@ import org.slf4j.LoggerFactory; import biz.netcentric.cq.tools.actool.aem.AcToolCqActions; +import biz.netcentric.cq.tools.actool.configmodel.AcConfiguration; import biz.netcentric.cq.tools.actool.configmodel.AceBean; import biz.netcentric.cq.tools.actool.helper.AccessControlUtils; import biz.netcentric.cq.tools.actool.helper.RestrictionsHolder; @@ -58,7 +59,7 @@ public class AceBeanInstallerClassic extends BaseAceBeanInstaller implements Ace * * @throws RepositoryException */ protected void installAcl(Set aceBeanSetFromConfig, String path, Set principalsToRemoveAcesFor, Session session, - InstallationLogger installLog) throws RepositoryException { + InstallationLogger installLog, AcConfiguration acConfiguration) throws RepositoryException { // Remove all config contained authorizables from ACL of this path int countRemoved = AccessControlUtils.deleteAllEntriesForPrincipalsFromACL(session, @@ -72,7 +73,7 @@ protected void installAcl(Set aceBeanSetFromConfig, String path, Set aceBeanSetFromConfig, String path, Set actionMap = aceBean.getActionMap(); if (actionMap.isEmpty()) { return acl; @@ -139,8 +140,22 @@ private JackrabbitAccessControlList installActions(AceBean aceBean, Principal pr AcToolCqActions cqActions = new AcToolCqActions(session); Collection inheritedAllows = cqActions.getAllowedActions( aceBean.getJcrPathForPolicyApi(), Collections.singleton(principal)); - // this does always install new entries - cqActions.installActions(aceBean.getJcrPathForPolicyApi(), principal, actionMap, inheritedAllows); + + boolean ignoreMissingPrincipals = acConfiguration.getGlobalConfiguration().getIgnoreMissingPrincipals() != null + && acConfiguration.getGlobalConfiguration().getIgnoreMissingPrincipals().booleanValue(); + + try { + // this does always install new entries + cqActions.installActions(aceBean.getJcrPathForPolicyApi(), principal, actionMap, inheritedAllows); + } catch (Exception e) { + if (ignoreMissingPrincipals && isPrincipalNotFoundException(e)) { + LOG.warn("Ignoring missing principal '{}' for actions on path '{}' due to ignoreMissingPrincipals=true", + principal.getName(), aceBean.getJcrPath()); + return acl; // return original ACL unchanged + } else { + throw e; + } + } // since the aclist has been modified, retrieve it again final JackrabbitAccessControlList newAcl = AccessControlUtils.getAccessControlList(session, aceBean.getJcrPath()); @@ -250,5 +265,16 @@ private void extendExistingAceWithRestrictions(JackrabbitAccessControlList acces // 3. remove old entry accessControlList.removeAccessControlEntry(accessControlEntry); } + + private boolean isPrincipalNotFoundException(Exception e) { + // Check for various forms of principal not found exceptions + String message = e.getMessage(); + return message != null && ( + message.toLowerCase().contains("principal") && + (message.toLowerCase().contains("not found") || + message.toLowerCase().contains("does not exist") || + message.toLowerCase().contains("unknown")) + ); + } } 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 346d0b5d..c47f42ad 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 @@ -48,6 +48,7 @@ import org.slf4j.LoggerFactory; import biz.netcentric.cq.tools.actool.aem.AcToolCqActions; +import biz.netcentric.cq.tools.actool.configmodel.AcConfiguration; import biz.netcentric.cq.tools.actool.configmodel.AceBean; import biz.netcentric.cq.tools.actool.configmodel.Restriction; import biz.netcentric.cq.tools.actool.helper.AcHelper; @@ -69,7 +70,7 @@ public class AceBeanInstallerIncremental extends BaseAceBeanInstaller implements * * @throws RepositoryException */ protected void installAcl(Set aceBeanSetFromConfig, String path, Set principalsInConfiguration, Session session, - InstallationLogger installLog) throws RepositoryException { + InstallationLogger installLog, AcConfiguration acConfiguration) throws RepositoryException { boolean hadPendingChanges = session.hasPendingChanges(); @@ -146,7 +147,7 @@ protected void installAcl(Set aceBeanSetFromConfig, String path, Set principalsToRemoveAcesForAtThisPath = acConfiguration.getAuthorizablesConfig() .removeUnmanagedPrincipalNamesAtPath(path, principalsToRemoveAcesFor, acConfiguration.getGlobalConfiguration().getDefaultUnmanagedAcePathsRegex()); - installAcl(orderedAceBeanSetFromConfig, path, principalsToRemoveAcesForAtThisPath, session, history); + installAcl(orderedAceBeanSetFromConfig, path, principalsToRemoveAcesForAtThisPath, session, history, acConfiguration); } @@ -134,28 +134,53 @@ private Set filterReadOnlyPaths(Set paths, InstallationLogger hi * * @throws RepositoryException */ protected abstract void installAcl(Set aceBeanSetFromConfig, String path, Set authorizablesToRemoveAcesFor, - Session session, InstallationLogger history) throws RepositoryException; + Session session, InstallationLogger history, AcConfiguration acConfiguration) throws RepositoryException; protected boolean installPrivileges(AceBean aceBean, Principal principal, JackrabbitAccessControlList acl, Session session, - AccessControlManager acMgr) + AccessControlManager acMgr, AcConfiguration acConfiguration) throws RepositoryException { final Set privileges = getPrivilegeSet(aceBean.getPrivileges(), acMgr); if (!privileges.isEmpty()) { final RestrictionsHolder restrictions = getRestrictions(aceBean, session, acl); - if (!restrictions.isEmpty()) { - acl.addEntry(principal, privileges - .toArray(new Privilege[privileges.size()]), aceBean.isAllow(), - restrictions.getSingleValuedRestrictionsMap(), restrictions.getMultiValuedRestrictionsMap()); - } else { - acl.addEntry(principal, privileges - .toArray(new Privilege[privileges.size()]), aceBean.isAllow()); + + boolean ignoreMissingPrincipals = acConfiguration.getGlobalConfiguration().getIgnoreMissingPrincipals() != null + && acConfiguration.getGlobalConfiguration().getIgnoreMissingPrincipals().booleanValue(); + + try { + if (!restrictions.isEmpty()) { + acl.addEntry(principal, privileges + .toArray(new Privilege[privileges.size()]), aceBean.isAllow(), + restrictions.getSingleValuedRestrictionsMap(), restrictions.getMultiValuedRestrictionsMap()); + } else { + acl.addEntry(principal, privileges + .toArray(new Privilege[privileges.size()]), aceBean.isAllow()); + } + return true; + } catch (Exception e) { + if (ignoreMissingPrincipals && isPrincipalNotFoundException(e)) { + LOG.warn("Ignoring missing principal '{}' for path '{}' due to ignoreMissingPrincipals=true", + principal.getName(), aceBean.getJcrPath()); + return false; + } else { + throw e; + } } - return true; } return false; } + + private boolean isPrincipalNotFoundException(Exception e) { + // Check for various forms of principal not found exceptions + String message = e.getMessage(); + return message != null && ( + message.toLowerCase().contains("principal") && + (message.toLowerCase().contains("not found") || + message.toLowerCase().contains("does not exist") || + message.toLowerCase().contains("unknown")) + ); + } /** Creates a RestrictionHolder object containing 2 restriction maps being used in * {@link JackrabbitAccessControlList#addEntry(Principal, Privilege[], boolean, Map, Map)} out of the set actions on this bean. diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configmodel/GlobalConfiguration.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configmodel/GlobalConfiguration.java index ca2213b3..ad1c39b8 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configmodel/GlobalConfiguration.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configmodel/GlobalConfiguration.java @@ -38,6 +38,8 @@ public class GlobalConfiguration { public static final String KEY_ALLOW_EXTERNAL_GROUPS_IN_IS_MEMBER_OF = "allowExternalGroupsInIsMemberOf"; public static final String KEY_AUTOCREATE_TEST_USERS = "autoCreateTestUsers"; + + public static final String KEY_IGNORE_MISSING_PRINCIPALS = "ignoreMissingPrincipals"; @Deprecated public static final String KEY_KEEP_EXISTING_MEMBERSHIPS_FOR_GROUP_NAMES_REGEX = "keepExistingMembershipsForGroupNamesRegEx"; @@ -51,6 +53,8 @@ public class GlobalConfiguration { private Boolean allowCreateOfUnmanagedRelationships = null; private Boolean allowExternalGroupsInIsMemberOf = null; + + private Boolean ignoreMissingPrincipals = null; private AutoCreateTestUsersConfig autoCreateTestUsersConfig; @@ -102,6 +106,10 @@ public GlobalConfiguration(Map globalConfigMap) { setAllowExternalGroupsInIsMemberOf(Boolean.valueOf(globalConfigMap.get(KEY_ALLOW_EXTERNAL_GROUPS_IN_IS_MEMBER_OF).toString())); } + if (globalConfigMap.containsKey(KEY_IGNORE_MISSING_PRINCIPALS)) { + setIgnoreMissingPrincipals(Boolean.valueOf(globalConfigMap.get(KEY_IGNORE_MISSING_PRINCIPALS).toString())); + } + } } @@ -169,6 +177,14 @@ public void merge(GlobalConfiguration otherGlobalConfig) { } } + if (otherGlobalConfig.getIgnoreMissingPrincipals() != null) { + if (ignoreMissingPrincipals == null) { + ignoreMissingPrincipals = otherGlobalConfig.getIgnoreMissingPrincipals(); + } else { + throw new IllegalArgumentException("Duplicate config for " + KEY_IGNORE_MISSING_PRINCIPALS); + } + } + } public String getMinRequiredVersion() { @@ -235,5 +251,13 @@ public Boolean getAllowExternalGroupsInIsMemberOf() { public void setAllowExternalGroupsInIsMemberOf(Boolean allowExternalGroupsInIsMemberOf) { this.allowExternalGroupsInIsMemberOf = allowExternalGroupsInIsMemberOf; } + + public Boolean getIgnoreMissingPrincipals() { + return ignoreMissingPrincipals; + } + + public void setIgnoreMissingPrincipals(Boolean ignoreMissingPrincipals) { + this.ignoreMissingPrincipals = ignoreMissingPrincipals; + } } diff --git a/accesscontroltool-bundle/src/test/java/biz/netcentric/cq/tools/actool/aceinstaller/AceBeanInstallerIncrementalTest.java b/accesscontroltool-bundle/src/test/java/biz/netcentric/cq/tools/actool/aceinstaller/AceBeanInstallerIncrementalTest.java index b2da5e7c..14f8e333 100644 --- a/accesscontroltool-bundle/src/test/java/biz/netcentric/cq/tools/actool/aceinstaller/AceBeanInstallerIncrementalTest.java +++ b/accesscontroltool-bundle/src/test/java/biz/netcentric/cq/tools/actool/aceinstaller/AceBeanInstallerIncrementalTest.java @@ -66,7 +66,9 @@ import org.mockito.quality.Strictness; import org.mockito.stubbing.Answer; +import biz.netcentric.cq.tools.actool.configmodel.AcConfiguration; import biz.netcentric.cq.tools.actool.configmodel.AceBean; +import biz.netcentric.cq.tools.actool.configmodel.GlobalConfiguration; import biz.netcentric.cq.tools.actool.configmodel.Restriction; import biz.netcentric.cq.tools.actool.configreader.YamlConfigReader; import biz.netcentric.cq.tools.actool.history.InstallationLogger; @@ -111,6 +113,12 @@ public class AceBeanInstallerIncrementalTest { @Mock SlingRepository slingRepository; + @Mock + AcConfiguration acConfiguration; + + @Mock + GlobalConfiguration globalConfiguration; + @BeforeEach public void setup() throws RepositoryException { @@ -123,7 +131,7 @@ public void setup() throws RepositoryException { doReturn(jackrabbitAccessControlList).when(aceBeanInstallerIncremental).getAccessControlList(eq(accessControlManager), anyString()); doReturn(true).when(aceBeanInstallerIncremental).installPrivileges(any(AceBean.class), any(Principal.class), - eq(jackrabbitAccessControlList), eq(session), eq(accessControlManager)); + eq(jackrabbitAccessControlList), eq(session), eq(accessControlManager), any(AcConfiguration.class)); // default privilege is a simple privilege with the given string name doAnswer(new Answer() { @@ -145,6 +153,10 @@ public Privilege answer(InvocationOnMock invocation) { doReturn(new PrincipalImpl(FAKE_PRINCIPAL_ID)).when(aceBeanInstallerIncremental).getTestActionMapperPrincipal(); doNothing().when(aceBeanInstallerIncremental).applyCqActions(any(AceBean.class), any(Session.class), any(Principal.class)); + + // Mock configuration behavior + doReturn(globalConfiguration).when(acConfiguration).getGlobalConfiguration(); + doReturn(null).when(globalConfiguration).getIgnoreMissingPrincipals(); // default: ignore missing principals is off } @Test @@ -178,16 +190,16 @@ public void testSimplePrivilegesAcesAdditive() throws Exception { aceBeanInstallerIncremental.installAcl( asSet(bean1, bean2, bean3), testPath, - asSet(testPrincipal1, testPrincipal2, testPrincipal3), session, installLog); + asSet(testPrincipal1, testPrincipal2, testPrincipal3), session, installLog, acConfiguration); verify(jackrabbitAccessControlList, never()).removeAccessControlEntry(any(JackrabbitAccessControlEntry.class)); verify(aceBeanInstallerIncremental).installPrivileges(eq(bean1), eq(new PrincipalImpl(testPrincipal1)), - eq(jackrabbitAccessControlList), eq(session), eq(accessControlManager)); + eq(jackrabbitAccessControlList), eq(session), eq(accessControlManager), eq(acConfiguration)); verify(aceBeanInstallerIncremental).installPrivileges(eq(bean2), eq(new PrincipalImpl(testPrincipal2)), - eq(jackrabbitAccessControlList), eq(session), eq(accessControlManager)); + eq(jackrabbitAccessControlList), eq(session), eq(accessControlManager), eq(acConfiguration)); verify(aceBeanInstallerIncremental).installPrivileges(eq(bean3), eq(new PrincipalImpl(testPrincipal3)), - eq(jackrabbitAccessControlList), eq(session), eq(accessControlManager)); + eq(jackrabbitAccessControlList), eq(session), eq(accessControlManager), eq(acConfiguration)); } @@ -201,12 +213,12 @@ public void testSimplePrivilegesAcesUnchanged() throws Exception { aceBeanInstallerIncremental.installAcl( asSet(bean1, bean2, bean3), testPath, - asSet(testPrincipal1, testPrincipal2, testPrincipal3), session, installLog); + asSet(testPrincipal1, testPrincipal2, testPrincipal3), session, installLog, acConfiguration); verify(jackrabbitAccessControlList, never()).removeAccessControlEntry(any(JackrabbitAccessControlEntry.class)); verify(aceBeanInstallerIncremental, never()).installPrivileges(any(AceBean.class), any(Principal.class), - any(JackrabbitAccessControlList.class), any(Session.class), any(AccessControlManager.class)); + any(JackrabbitAccessControlList.class), any(Session.class), any(AccessControlManager.class), any(AcConfiguration.class)); } @@ -221,14 +233,14 @@ public void testSimplePrivilegesAcesRemoved() throws Exception { aceBeanInstallerIncremental.installAcl( Collections.emptySet(), testPath, - asSet(testPrincipal1, testPrincipal2, testPrincipal3), session, installLog); + asSet(testPrincipal1, testPrincipal2, testPrincipal3), session, installLog, acConfiguration); verify(jackrabbitAccessControlList).removeAccessControlEntry(ace1); verify(jackrabbitAccessControlList).removeAccessControlEntry(ace2); verify(jackrabbitAccessControlList).removeAccessControlEntry(ace3); verify(aceBeanInstallerIncremental, never()).installPrivileges(any(AceBean.class), any(Principal.class), - any(JackrabbitAccessControlList.class), any(Session.class), any(AccessControlManager.class)); + any(JackrabbitAccessControlList.class), any(Session.class), any(AccessControlManager.class), any(AcConfiguration.class)); } @@ -243,16 +255,16 @@ public void testGetPrincipalAceBeansForActionAceBeanIsCalledToResolveActions() t aceBeanInstallerIncremental.installAcl( asSet(beanWithAction1, beanWithAction2), testPath, - asSet(testPrincipal1, testPrincipal2), session, installLog); + asSet(testPrincipal1, testPrincipal2), session, installLog, acConfiguration); verify(jackrabbitAccessControlList, never()).removeAccessControlEntry(any(JackrabbitAccessControlEntry.class)); verify(aceBeanInstallerIncremental).installPrivileges(eq(bean1), eq(new PrincipalImpl(testPrincipal1)), - eq(jackrabbitAccessControlList), eq(session), eq(accessControlManager)); + eq(jackrabbitAccessControlList), eq(session), eq(accessControlManager), eq(acConfiguration)); verify(aceBeanInstallerIncremental).installPrivileges(eq(bean2), eq(new PrincipalImpl(testPrincipal2)), - eq(jackrabbitAccessControlList), eq(session), eq(accessControlManager)); + eq(jackrabbitAccessControlList), eq(session), eq(accessControlManager), eq(acConfiguration)); verify(aceBeanInstallerIncremental).installPrivileges(eq(bean2Content), eq(new PrincipalImpl(testPrincipal2)), - eq(jackrabbitAccessControlList), eq(session), eq(accessControlManager)); + eq(jackrabbitAccessControlList), eq(session), eq(accessControlManager), eq(acConfiguration)); } diff --git a/accesscontroltool-bundle/src/test/java/biz/netcentric/cq/tools/actool/configmodel/IgnoreMissingPrincipalsTest.java b/accesscontroltool-bundle/src/test/java/biz/netcentric/cq/tools/actool/configmodel/IgnoreMissingPrincipalsTest.java new file mode 100644 index 00000000..42cb1c06 --- /dev/null +++ b/accesscontroltool-bundle/src/test/java/biz/netcentric/cq/tools/actool/configmodel/IgnoreMissingPrincipalsTest.java @@ -0,0 +1,79 @@ +package biz.netcentric.cq.tools.actool.configmodel; + +/*- + * #%L + * Access Control Tool Bundle + * %% + * Copyright (C) 2015 - 2024 Cognizant Netcentric + * %% + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.InputStream; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import biz.netcentric.cq.tools.actool.configreader.YamlConfigReader; + +@ExtendWith(MockitoExtension.class) +public class IgnoreMissingPrincipalsTest { + + @Test + public void testIgnoreMissingPrincipalsConfigurationParsing() throws Exception { + + // Test creating GlobalConfiguration from Map + Map globalConfigMap = new LinkedHashMap<>(); + globalConfigMap.put("ignoreMissingPrincipals", true); + + GlobalConfiguration globalConfig = new GlobalConfiguration(globalConfigMap); + + // Verify ignoreMissingPrincipals is properly parsed + assertTrue(globalConfig.getIgnoreMissingPrincipals()); + } + + @Test + public void testIgnoreMissingPrincipalsDefaultValue() { + GlobalConfiguration globalConfig = new GlobalConfiguration(); + + // By default, ignoreMissingPrincipals should be null (not set) + assertEquals(null, globalConfig.getIgnoreMissingPrincipals()); + } + + @Test + public void testIgnoreMissingPrincipalsConfigurationMerging() { + GlobalConfiguration config1 = new GlobalConfiguration(); + config1.setIgnoreMissingPrincipals(true); + + GlobalConfiguration config2 = new GlobalConfiguration(); + + config2.merge(config1); + + assertTrue(config2.getIgnoreMissingPrincipals()); + } + + @Test + public void testIgnoreMissingPrincipalsConfigurationSetterGetter() { + GlobalConfiguration globalConfig = new GlobalConfiguration(); + + globalConfig.setIgnoreMissingPrincipals(false); + assertFalse(globalConfig.getIgnoreMissingPrincipals()); + + globalConfig.setIgnoreMissingPrincipals(true); + assertTrue(globalConfig.getIgnoreMissingPrincipals()); + } +} \ No newline at end of file