diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/GetRuleSetsTests.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/GetRuleSetsTests.java index 77e5855894..fe037be6b2 100644 --- a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/GetRuleSetsTests.java +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/GetRuleSetsTests.java @@ -25,17 +25,26 @@ */ package org.alfresco.rest.rules; +import static org.alfresco.rest.requests.RuleSettings.IS_INHERITANCE_ENABLED; import static org.alfresco.rest.rules.RulesTestsUtils.createRuleModel; +import static org.alfresco.rest.rules.RulesTestsUtils.createRuleModelWithModifiedValues; import static org.alfresco.utility.report.log.Step.STEP; import static org.junit.Assert.assertTrue; import static org.springframework.http.HttpStatus.NOT_FOUND; import static org.springframework.http.HttpStatus.OK; +import java.util.List; + +import com.google.common.collect.ImmutableMap; + import org.alfresco.rest.RestTest; import org.alfresco.rest.model.RestRuleModel; +import org.alfresco.rest.model.RestRuleSetLinkModel; import org.alfresco.rest.model.RestRuleSetModel; import org.alfresco.rest.model.RestRuleSetModelsCollection; import org.alfresco.rest.model.RestRuleSettingsModel; +import org.alfresco.rest.requests.coreAPI.RestCoreAPI; +import org.alfresco.utility.constants.UserRole; import org.alfresco.utility.model.FolderModel; import org.alfresco.utility.model.SiteModel; import org.alfresco.utility.model.TestGroup; @@ -248,4 +257,78 @@ public class GetRuleSetsTests extends RestTest ruleSet.assertThat().field("owningFolder").is(ruleFolder.getNodeRef()) .assertThat().field("id").is(ruleSetId); } + + /** + * Check we can find out the id of any folders that inherit a rule set. + *

+ * The test checks several different situations: + *

+     *   folder --[owns]-> rule set
+     *   +- publicFolder --[inherits]-> rule set (user has access)
+     *   +- privateFolder --[inherits]-> rule set (user does not have access)
+     *      +- publicGrandchild --[inherits]-> rule set (user has access again)
+     *   +- nonInheritingFolder (inheritance should be prevented)
+     *      +- linkingFolder --[links]-> rule set (not inherited)
+     *         +- descendantFolder --[inherits]-> rule set (inherited via link)
+     * 
+ */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void getRuleSetAndInheritedBy() + { + STEP("Create a site owned by admin and add user as a contributor"); + SiteModel siteModel = dataSite.usingAdmin().createPrivateRandomSite(); + dataUser.addUserToSite(user, siteModel, UserRole.SiteContributor); + + STEP("Create the folder structure"); + FolderModel folder = dataContent.usingUser(user).usingSite(siteModel).createFolder(); + FolderModel publicFolder = dataContent.usingUser(user).usingResource(folder).createFolder(); + FolderModel privateFolder = dataContent.usingAdmin().usingResource(folder).createFolder(); + dataContent.usingAdmin().usingResource(privateFolder).setInheritPermissions(false); + // Create the grandchild with user and use admin to move it under the private folder. + FolderModel publicGrandchild = dataContent.usingUser(user).usingSite(siteModel).createFolder(); + coreAPIForAdmin().usingActions().executeAction("move", publicGrandchild, ImmutableMap.of("destination-folder", "workspace://SpacesStore/" + privateFolder.getNodeRef())); + // Create the non-inheriting folder. + FolderModel nonInheritingFolder = dataContent.usingUser(user).usingResource(folder).createFolder(); + RestRuleSettingsModel nonInheriting = new RestRuleSettingsModel(); + nonInheriting.setKey(IS_INHERITANCE_ENABLED); + nonInheriting.setValue(false); + coreAPIForUser().usingNode(nonInheritingFolder).usingIsInheritanceEnabledRuleSetting().updateSetting(nonInheriting); + // Create a child that will link to the rule and a child of that to inherit via the link. + FolderModel linkingFolder = dataContent.usingUser(user).usingResource(nonInheritingFolder).createFolder(); + FolderModel descendantFolder = dataContent.usingUser(user).usingResource(linkingFolder).createFolder(); + + STEP("Create an inheritable rule in the folder and get the rule set id."); + RestRuleModel ruleModel = createRuleModelWithModifiedValues(); + coreAPIForUser().usingNode(folder).usingDefaultRuleSet().createSingleRule(ruleModel); + RestRuleSetModelsCollection ruleSets = coreAPIForUser().usingNode(folder).getListOfRuleSets(); + String ruleSetId = ruleSets.getEntries().get(0).onModel().getId(); + + STEP("Create the link to the rule from the linking folder"); + RestRuleSetLinkModel ruleSetLink = new RestRuleSetLinkModel(); + ruleSetLink.setId(folder.getNodeRef()); + coreAPIForUser().usingNode(linkingFolder).createRuleLink(ruleSetLink); + + STEP("Remove the user from the site"); + dataUser.removeUserFromSite(user, siteModel); + + STEP("Get the rule set and inheriting folders"); + RestRuleSetModel ruleSet = coreAPIForUser().usingNode(folder) + .include("inheritedBy") + .getRuleSet(ruleSetId); + + restClient.assertStatusCodeIs(OK); + List expectedInheritors = List.of(publicFolder.getNodeRef(), descendantFolder.getNodeRef(), publicGrandchild.getNodeRef()); + ruleSet.assertThat().field("inheritedBy").is(expectedInheritors) + .assertThat().field("id").is(ruleSetId); + } + + private RestCoreAPI coreAPIForUser() + { + return restClient.authenticateUser(user).withCoreAPI(); + } + + private RestCoreAPI coreAPIForAdmin() + { + return restClient.authenticateUser(dataUser.getAdminUser()).withCoreAPI(); + } } diff --git a/pom.xml b/pom.xml index 6f1520f5cc..8f5e42a9b2 100644 --- a/pom.xml +++ b/pom.xml @@ -123,7 +123,7 @@ 2.7.4 3.0.49 5.1.1 - 1.115 + 1.117 1.32 1.9 1.7 diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/RuleSetLoader.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/RuleSetLoader.java index 864462f5d0..9800de58b3 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/RuleSetLoader.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/RuleSetLoader.java @@ -36,6 +36,7 @@ import org.alfresco.service.Experimental; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.rule.RuleService; /** Responsible for converting a NodeRef into a {@link RuleSet} object. */ @Experimental @@ -43,7 +44,10 @@ public class RuleSetLoader { protected static final String OWNING_FOLDER = "owningFolder"; protected static final String INCLUSION_TYPE = "inclusionType"; + protected static final String INHERITED_BY = "inheritedBy"; + public static final int MAX_INHERITED_BY_SIZE = 100; private NodeService nodeService; + private RuleService ruleService; /** * Load a rule set for the given node ref. @@ -79,12 +83,26 @@ public class RuleSetLoader ruleSet.setInclusionType(linked ? LINKED : INHERITED); } } + if (includes.contains(INHERITED_BY)) + { + ruleSet.setInheritedBy(loadInheritedBy(ruleSetNodeRef)); + } } return ruleSet; } + private List loadInheritedBy(NodeRef ruleSetNodeRef) + { + return ruleService.getFoldersInheritingRuleSet(ruleSetNodeRef, MAX_INHERITED_BY_SIZE); + } + public void setNodeService(NodeService nodeService) { this.nodeService = nodeService; } + + public void setRuleService(RuleService ruleService) + { + this.ruleService = ruleService; + } } diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/rules/RuleSet.java b/remote-api/src/main/java/org/alfresco/rest/api/model/rules/RuleSet.java index 7cddaa45cc..e2719bd4ed 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/model/rules/RuleSet.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/rules/RuleSet.java @@ -26,6 +26,7 @@ package org.alfresco.rest.api.model.rules; +import java.util.List; import java.util.Objects; import java.util.StringJoiner; @@ -40,6 +41,7 @@ public class RuleSet private String id; private NodeRef owningFolder; private InclusionType inclusionType; + private List inheritedBy; public static RuleSet of(String id) { @@ -86,6 +88,16 @@ public class RuleSet this.inclusionType = inclusionType; } + public List getInheritedBy() + { + return inheritedBy; + } + + public void setInheritedBy(List inheritedBy) + { + this.inheritedBy = inheritedBy; + } + @Override public String toString() { @@ -94,6 +106,7 @@ public class RuleSet .add("id='" + id + "'") .add("owningFolder='" + owningFolder + "'") .add("inclusionType='" + inclusionType + "'") + .add("inheritedBy='" + inheritedBy + "'") .toString() + '}'; } @@ -108,13 +121,14 @@ public class RuleSet RuleSet ruleSet = (RuleSet) o; return Objects.equals(id, ruleSet.id) && Objects.equals(owningFolder, ruleSet.owningFolder) - && inclusionType == ruleSet.inclusionType; + && inclusionType == ruleSet.inclusionType + && Objects.equals(inheritedBy, ruleSet.inheritedBy); } @Override public int hashCode() { - return Objects.hash(id, owningFolder, inclusionType); + return Objects.hash(id, owningFolder, inclusionType, inheritedBy); } public static Builder builder() @@ -127,6 +141,7 @@ public class RuleSet private String id; private NodeRef owningFolder; private InclusionType inclusionType; + private List inheritedBy; public Builder id(String id) { @@ -146,12 +161,19 @@ public class RuleSet return this; } + public Builder inheritedBy(List inheritedBy) + { + this.inheritedBy = inheritedBy; + return this; + } + public RuleSet create() { final RuleSet ruleSet = new RuleSet(); ruleSet.setId(id); ruleSet.setOwningFolder(owningFolder); ruleSet.setInclusionType(inclusionType); + ruleSet.setInheritedBy(inheritedBy); return ruleSet; } } diff --git a/remote-api/src/main/resources/alfresco/public-rest-context.xml b/remote-api/src/main/resources/alfresco/public-rest-context.xml index a4436bc7c0..e4c38b46db 100644 --- a/remote-api/src/main/resources/alfresco/public-rest-context.xml +++ b/remote-api/src/main/resources/alfresco/public-rest-context.xml @@ -863,6 +863,7 @@ + diff --git a/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/RuleSetLoaderTest.java b/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/RuleSetLoaderTest.java index 29a0fd92c5..f30cef0bf3 100644 --- a/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/RuleSetLoaderTest.java +++ b/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/RuleSetLoaderTest.java @@ -26,11 +26,14 @@ package org.alfresco.rest.api.impl.rules; import static org.alfresco.rest.api.impl.rules.RuleSetLoader.INCLUSION_TYPE; +import static org.alfresco.rest.api.impl.rules.RuleSetLoader.INHERITED_BY; import static org.alfresco.rest.api.impl.rules.RuleSetLoader.OWNING_FOLDER; import static org.alfresco.rest.api.model.rules.InclusionType.INHERITED; import static org.alfresco.rest.api.model.rules.InclusionType.LINKED; import static org.alfresco.rest.api.model.rules.InclusionType.OWNED; import static org.alfresco.service.cmr.repository.StoreRef.STORE_REF_WORKSPACE_SPACESSTORE; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import java.util.List; @@ -41,6 +44,7 @@ import org.alfresco.service.Experimental; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.rule.RuleService; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -66,6 +70,8 @@ public class RuleSetLoaderTest extends TestCase @Mock private NodeService nodeServiceMock; @Mock + private RuleService ruleServiceMock; + @Mock private ChildAssociationRef ruleSetAssociationMock; @Mock private ChildAssociationRef linkAssociationMock; @@ -79,6 +85,8 @@ public class RuleSetLoaderTest extends TestCase given(linkAssociationMock.getParentRef()).willReturn(LINKING_FOLDER); given(nodeServiceMock.getParentAssocs(RULE_SET_NODE)).willReturn(List.of(ruleSetAssociationMock, linkAssociationMock)); + + given(ruleServiceMock.getFoldersInheritingRuleSet(eq(RULE_SET_NODE), anyInt())).willReturn(List.of(INHERITING_FOLDER)); } @Test @@ -130,4 +138,14 @@ public class RuleSetLoaderTest extends TestCase RuleSet expected = RuleSet.builder().id(RULE_SET_ID).inclusionType(INHERITED).create(); assertEquals(expected, actual); } + + @Test + public void testLoadRuleSet_inheritedBy() + { + // Call the method under test. + RuleSet actual = ruleSetLoader.loadRuleSet(RULE_SET_NODE, INHERITING_FOLDER, List.of(INHERITED_BY)); + + RuleSet expected = RuleSet.builder().id(RULE_SET_ID).inheritedBy(List.of(INHERITING_FOLDER)).create(); + assertEquals(expected, actual); + } } diff --git a/repository/src/main/java/org/alfresco/repo/rule/RuleServiceImpl.java b/repository/src/main/java/org/alfresco/repo/rule/RuleServiceImpl.java index fa92c02204..db591fbbf7 100644 --- a/repository/src/main/java/org/alfresco/repo/rule/RuleServiceImpl.java +++ b/repository/src/main/java/org/alfresco/repo/rule/RuleServiceImpl.java @@ -25,14 +25,19 @@ */ package org.alfresco.repo.rule; +import static org.alfresco.model.ContentModel.ASSOC_CONTAINS; import static org.alfresco.repo.rule.RuleModel.ASPECT_IGNORE_INHERITED_RULES; import static org.alfresco.repo.rule.RuleModel.ASSOC_RULE_FOLDER; +import static org.alfresco.service.cmr.security.AccessStatus.ALLOWED; +import static org.alfresco.service.namespace.RegexQNamePattern.MATCH_ALL; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; +import java.util.Deque; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -70,7 +75,6 @@ import org.alfresco.service.cmr.rule.Rule; import org.alfresco.service.cmr.rule.RuleService; import org.alfresco.service.cmr.rule.RuleServiceException; import org.alfresco.service.cmr.rule.RuleType; -import org.alfresco.service.cmr.security.AccessStatus; import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.QNamePattern; @@ -520,7 +524,7 @@ public class RuleServiceImpl // https://issues.alfresco.com/browse/ETWOTWO-438 if (!runtimeNodeService.hasAspect(nodeRef, RuleModel.ASPECT_RULES) || - permissionService.hasPermission(nodeRef, PermissionService.READ) != AccessStatus.ALLOWED) + permissionService.hasPermission(nodeRef, PermissionService.READ) != ALLOWED) { // Doesn't have the aspect or the user doesn't have access return Collections.emptyList(); @@ -538,7 +542,7 @@ public class RuleServiceImpl { // Get the rules for this node List ruleChildAssocRefs = - this.runtimeNodeService.getChildAssocs(ruleFolder, RegexQNamePattern.MATCH_ALL, ASSOC_NAME_RULES_REGEX); + this.runtimeNodeService.getChildAssocs(ruleFolder, MATCH_ALL, ASSOC_NAME_RULES_REGEX); for (ChildAssociationRef ruleChildAssocRef : ruleChildAssocRefs) { // Create the rule and add to the list @@ -567,7 +571,7 @@ public class RuleServiceImpl { // Get the rules for this node List ruleChildAssocRefs = - this.runtimeNodeService.getChildAssocs(ruleFolder, RegexQNamePattern.MATCH_ALL, ASSOC_NAME_RULES_REGEX); + this.runtimeNodeService.getChildAssocs(ruleFolder, MATCH_ALL, ASSOC_NAME_RULES_REGEX); ruleCount = ruleChildAssocRefs.size(); } @@ -647,6 +651,44 @@ public class RuleServiceImpl return returnList; } + /** {@inheritDoc} */ + @Override + @Experimental + public List getFoldersInheritingRuleSet(NodeRef ruleSet, int maxFoldersToReturn) + { + // Seed stack with all folders owning or linking to the rule set. + Deque stack = new LinkedList<>(); + for (ChildAssociationRef parentAssociation : runtimeNodeService.getParentAssocs(ruleSet)) + { + stack.add(parentAssociation.getParentRef()); + } + // Process child folders to find all that inherit the rules. + List inheritors = new ArrayList<>(); + while (!stack.isEmpty() && inheritors.size() < maxFoldersToReturn) + { + NodeRef folder = stack.pop(); + runtimeNodeService.getChildAssocs(folder, ASSOC_CONTAINS, MATCH_ALL).stream().map(ChildAssociationRef::getChildRef).forEach(childNode -> { + QName childType = runtimeNodeService.getType(childNode); + if (dictionaryService.isSubClass(childType, ContentModel.TYPE_FOLDER) + && !runtimeNodeService.hasAspect(childNode, ASPECT_IGNORE_INHERITED_RULES)) + { + stack.add(childNode); + // Only return nodes that the user has permission to view. + if (permissionService.hasReadPermission(childNode) == ALLOWED) + { + inheritors.add(childNode); + if (inheritors.size() == maxFoldersToReturn) + { + // Return once we've hit the limit. + return; + } + } + } + }); + } + return inheritors; + } + /** * Gets the inherited rules for a given node reference * @@ -798,7 +840,7 @@ public class RuleServiceImpl { checkForLinkedRules(nodeRef); - if (this.permissionService.hasPermission(nodeRef, PermissionService.CHANGE_PERMISSIONS) != AccessStatus.ALLOWED) + if (this.permissionService.hasPermission(nodeRef, PermissionService.CHANGE_PERMISSIONS) != ALLOWED) { throw new RuleServiceException("Insufficient permissions to save a rule."); } @@ -823,7 +865,7 @@ public class RuleServiceImpl // Create the action node ruleNodeRef = this.nodeService.createNode( getSavedRuleFolderRef(nodeRef), - ContentModel.ASSOC_CONTAINS, + ASSOC_CONTAINS, QName.createQName(RuleModel.RULE_MODEL_URI, ASSOC_NAME_RULES_PREFIX + GUID.generate()), RuleModel.TYPE_RULE).getChildRef(); @@ -866,7 +908,7 @@ public class RuleServiceImpl NodeRef ruleFolder = getSavedRuleFolderRef(nodeRef); if (ruleFolder != null) { - List assocs = this.runtimeNodeService.getChildAssocs(ruleFolder, RegexQNamePattern.MATCH_ALL, ASSOC_NAME_RULES_REGEX); + List assocs = this.runtimeNodeService.getChildAssocs(ruleFolder, MATCH_ALL, ASSOC_NAME_RULES_REGEX); List orderedAssocs = new ArrayList(assocs.size()); ChildAssociationRef movedAssoc = null; for (ChildAssociationRef assoc : assocs) @@ -948,7 +990,7 @@ public class RuleServiceImpl { checkForLinkedRules(nodeRef); - if (permissionService.hasPermission(nodeRef, PermissionService.CHANGE_PERMISSIONS) == AccessStatus.ALLOWED) + if (permissionService.hasPermission(nodeRef, PermissionService.CHANGE_PERMISSIONS) == ALLOWED) { if (nodeService.exists(nodeRef) && nodeService.hasAspect(nodeRef, RuleModel.ASPECT_RULES)) { @@ -1011,7 +1053,7 @@ public class RuleServiceImpl { checkForLinkedRules(nodeRef); - if (this.permissionService.hasPermission(nodeRef, PermissionService.CHANGE_PERMISSIONS) == AccessStatus.ALLOWED) + if (this.permissionService.hasPermission(nodeRef, PermissionService.CHANGE_PERMISSIONS) == ALLOWED) { if (this.nodeService.exists(nodeRef) == true && this.nodeService.hasAspect(nodeRef, RuleModel.ASPECT_RULES) == true) @@ -1021,7 +1063,7 @@ public class RuleServiceImpl { List ruleChildAssocs = this.nodeService.getChildAssocs( folder, - RegexQNamePattern.MATCH_ALL, ASSOC_NAME_RULES_REGEX); + MATCH_ALL, ASSOC_NAME_RULES_REGEX); for (ChildAssociationRef ruleChildAssoc : ruleChildAssocs) { this.nodeService.removeChild(folder, ruleChildAssoc.getChildRef()); @@ -1371,7 +1413,7 @@ public class RuleServiceImpl { boolean result = true; if (this.nodeService.exists(actionedUponNodeRef) - && this.permissionService.hasPermission(actionedUponNodeRef, PermissionService.READ).equals(AccessStatus.ALLOWED)) + && this.permissionService.hasPermission(actionedUponNodeRef, PermissionService.READ).equals(ALLOWED)) { NodeRef copiedFrom = copyService.getOriginal(actionedUponNodeRef); if (logger.isDebugEnabled() == true) diff --git a/repository/src/main/java/org/alfresco/service/cmr/rule/RuleService.java b/repository/src/main/java/org/alfresco/service/cmr/rule/RuleService.java index ed42df7e25..3b03eb329b 100644 --- a/repository/src/main/java/org/alfresco/service/cmr/rule/RuleService.java +++ b/repository/src/main/java/org/alfresco/service/cmr/rule/RuleService.java @@ -227,6 +227,18 @@ public interface RuleService @Experimental List getNodesSupplyingRuleSets(NodeRef nodeRef); + /** + * Get a list of folders inheriting the specified rule set. + * + * @param ruleSet The rule set node. + * @param maxFoldersToReturn A limit on the number of folders to return (since otherwise this could traverse a very large proportion of + * the repository. + * @return The list of the specified + */ + @Auditable (parameters = { "ruleSet", "maxFoldersToReturn" }) + @Experimental + List getFoldersInheritingRuleSet(NodeRef ruleSet, int maxFoldersToReturn); + /** * Get the rule given its node reference * diff --git a/repository/src/test/java/org/alfresco/repo/rule/RuleServiceImplUnitTest.java b/repository/src/test/java/org/alfresco/repo/rule/RuleServiceImplUnitTest.java index 11f7caebc2..8c085abaea 100644 --- a/repository/src/test/java/org/alfresco/repo/rule/RuleServiceImplUnitTest.java +++ b/repository/src/test/java/org/alfresco/repo/rule/RuleServiceImplUnitTest.java @@ -25,17 +25,21 @@ */ package org.alfresco.repo.rule; +import static java.util.Collections.emptyList; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; import static org.alfresco.model.ContentModel.ASSOC_CONTAINS; import static org.alfresco.model.ContentModel.ASSOC_MEMBER; +import static org.alfresco.model.ContentModel.TYPE_CONTENT; +import static org.alfresco.model.ContentModel.TYPE_FOLDER; import static org.alfresco.repo.rule.RuleModel.ASPECT_IGNORE_INHERITED_RULES; import static org.alfresco.repo.rule.RuleModel.ASSOC_ACTION; import static org.alfresco.repo.rule.RuleModel.ASSOC_RULE_FOLDER; import static org.alfresco.repo.rule.RuleModel.TYPE_RULE; import static org.alfresco.service.cmr.security.AccessStatus.ALLOWED; import static org.alfresco.service.cmr.security.AccessStatus.DENIED; +import static org.alfresco.service.namespace.RegexQNamePattern.MATCH_ALL; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.junit.Assert.assertEquals; @@ -52,18 +56,19 @@ import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.openMocks; import java.io.Serializable; -import java.util.Collections; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.IntStream; -import org.alfresco.model.ContentModel; import org.alfresco.repo.action.RuntimeActionService; import org.alfresco.repo.cache.SimpleCache; import org.alfresco.service.cmr.action.Action; import org.alfresco.service.cmr.action.ActionServiceException; +import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; @@ -98,6 +103,8 @@ public class RuleServiceImplUnitTest @Mock private RuntimeActionService runtimeActionService; @Mock + private DictionaryService dictionaryService; + @Mock private Rule mockRule; @Mock private Action mockAction; @@ -106,6 +113,10 @@ public class RuleServiceImplUnitTest public void setUp() { openMocks(this); + + when(dictionaryService.isSubClass(TYPE_FOLDER, TYPE_FOLDER)).thenReturn(true); + when(dictionaryService.isSubClass(TYPE_CONTENT, TYPE_FOLDER)).thenReturn(false); + when(permissionService.hasReadPermission(any())).thenReturn(ALLOWED); } @Test @@ -220,7 +231,7 @@ public class RuleServiceImplUnitTest @Test public void testGetRuleSetNode_emptyAssociation() { - given(runtimeNodeService.getChildAssocs(any(), any(), any())).willReturn(Collections.emptyList()); + given(runtimeNodeService.getChildAssocs(any(), any(), any())).willReturn(emptyList()); // when final NodeRef actualNode = ruleService.getRuleSetNode(FOLDER_NODE); @@ -264,7 +275,7 @@ public class RuleServiceImplUnitTest @Test public void testIsRuleSetAssociatedWithFolder_emptyAssociation() { - given(runtimeNodeService.getParentAssocs(any(), any(), any())).willReturn(Collections.emptyList()); + given(runtimeNodeService.getParentAssocs(any(), any(), any())).willReturn(emptyList()); // when boolean associated = ruleService.isRuleSetAssociatedWithFolder(RULE_SET_NODE, FOLDER_NODE); @@ -343,7 +354,7 @@ public class RuleServiceImplUnitTest @Test public void testIsRuleAssociatedWithRuleSet_emptyAssociation() { - given(runtimeNodeService.getParentAssocs(any())).willReturn(Collections.emptyList()); + given(runtimeNodeService.getParentAssocs(any())).willReturn(emptyList()); // when boolean associated = ruleService.isRuleAssociatedWithRuleSet(RULE_NODE, RULE_SET_NODE); @@ -456,7 +467,7 @@ public class RuleServiceImplUnitTest { Map nodes = createParentChildHierarchy("A,B", "B,C", "C,D", "D,E"); // Replace the B,C association with a user group membership association. - ChildAssociationRef memberAssoc = new ChildAssociationRef(ASSOC_MEMBER, nodes.get("B"), ContentModel.TYPE_FOLDER, nodes.get("C")); + ChildAssociationRef memberAssoc = new ChildAssociationRef(ASSOC_MEMBER, nodes.get("B"), TYPE_FOLDER, nodes.get("C")); given(runtimeNodeService.getParentAssocs(nodes.get("C"))).willReturn(List.of(memberAssoc)); List actual = ruleService.getNodesSupplyingRuleSets(nodes.get("E")); @@ -570,10 +581,137 @@ public class RuleServiceImplUnitTest .filter(assoc -> assoc.endsWith(nodeName)) .map(assoc -> assoc.split(",")[0]) .map(nodeRefMap::get) - .map(parentRef -> new ChildAssociationRef(ASSOC_CONTAINS, parentRef, ContentModel.TYPE_FOLDER, nodeRef)) + .map(parentRef -> new ChildAssociationRef(ASSOC_CONTAINS, parentRef, TYPE_FOLDER, nodeRef)) .collect(toList()); given(runtimeNodeService.getParentAssocs(nodeRef)).willReturn(parentAssocs); }); return nodeRefMap; } + + /** Check that getFoldersInheritingRuleSet returns a child folder. */ + @Test + public void testGetFoldersInheritingRuleSet() + { + NodeRef parent = new NodeRef("parent://node/"); + NodeRef ruleSetNode = new NodeRef("rule://set/"); + ChildAssociationRef ruleSetAssociation = mock(ChildAssociationRef.class); + given(runtimeNodeService.getParentAssocs(ruleSetNode)).willReturn(List.of(ruleSetAssociation)); + given(ruleSetAssociation.getParentRef()).willReturn(parent); + NodeRef child = new NodeRef("child://node/"); + ChildAssociationRef childAssocMock = mock(ChildAssociationRef.class); + given(runtimeNodeService.getChildAssocs(parent, ASSOC_CONTAINS, MATCH_ALL)).willReturn(List.of(childAssocMock)); + given(childAssocMock.getChildRef()).willReturn(child); + given(runtimeNodeService.getType(child)).willReturn(TYPE_FOLDER); + + List actual = ruleService.getFoldersInheritingRuleSet(ruleSetNode, 100); + + assertEquals("Unexpected list of inheriting folders.", List.of(child), actual); + } + + /** Check that getFoldersInheritingRuleSet omits a child folder if IGNORE_INHERITED_RULES is applied. */ + @Test + public void testGetFoldersInheritingRuleSet_ignoreInheritedRules() + { + NodeRef parent = new NodeRef("parent://node/"); + NodeRef ruleSetNode = new NodeRef("rule://set/"); + ChildAssociationRef ruleSetAssociation = mock(ChildAssociationRef.class); + given(runtimeNodeService.getParentAssocs(ruleSetNode)).willReturn(List.of(ruleSetAssociation)); + given(ruleSetAssociation.getParentRef()).willReturn(parent); + NodeRef child = new NodeRef("child://node/"); + ChildAssociationRef childAssocMock = mock(ChildAssociationRef.class); + given(runtimeNodeService.getChildAssocs(parent, ASSOC_CONTAINS, MATCH_ALL)).willReturn(List.of(childAssocMock)); + given(childAssocMock.getChildRef()).willReturn(child); + given(runtimeNodeService.getType(child)).willReturn(TYPE_FOLDER); + given(runtimeNodeService.hasAspect(child, ASPECT_IGNORE_INHERITED_RULES)).willReturn(true); + + List actual = ruleService.getFoldersInheritingRuleSet(ruleSetNode, 100); + + assertEquals("Unexpected list of inheriting folders.", emptyList(), actual); + } + + /** Check that getFoldersInheritingRuleSet only returns at most the requested number of folders. */ + @Test + public void testGetFoldersExceedsLimit() + { + NodeRef root = new NodeRef("root://node/"); + NodeRef ruleSetNode = new NodeRef("rule://set/"); + ChildAssociationRef ruleSetAssociation = mock(ChildAssociationRef.class); + given(runtimeNodeService.getParentAssocs(ruleSetNode)).willReturn(List.of(ruleSetAssociation)); + given(ruleSetAssociation.getParentRef()).willReturn(root); + // Create a chain of ancestors starting from the root that is 10 folders deep. + List nodeChain = new ArrayList<>(); + nodeChain.add(root); + IntStream.range(0, 10).forEach(index -> { + NodeRef parent = nodeChain.get(nodeChain.size() - 1); + NodeRef child = new NodeRef("chain://node/" + index); + nodeChain.add(child); + ChildAssociationRef childAssocMock = mock(ChildAssociationRef.class); + given(runtimeNodeService.getChildAssocs(parent, ASSOC_CONTAINS, MATCH_ALL)).willReturn(List.of(childAssocMock)); + given(childAssocMock.getChildRef()).willReturn(child); + given(runtimeNodeService.getType(child)).willReturn(TYPE_FOLDER); + }); + + // Request at most 9 folders inheriting the rule. + List actual = ruleService.getFoldersInheritingRuleSet(ruleSetNode, 9); + + // Check we don't get the root node or the final descendant folder. + assertEquals("Unexpected list of inheriting folders.", nodeChain.subList(1, 10), actual); + } + + /** Check that getFoldersInheritingRuleSet doesn't include documents. */ + @Test + public void testGetFoldersInheritingRuleSet_ignoreFiles() + { + NodeRef parent = new NodeRef("parent://node/"); + NodeRef ruleSetNode = new NodeRef("rule://set/"); + ChildAssociationRef ruleSetAssociation = mock(ChildAssociationRef.class); + given(runtimeNodeService.getParentAssocs(ruleSetNode)).willReturn(List.of(ruleSetAssociation)); + given(ruleSetAssociation.getParentRef()).willReturn(parent); + NodeRef childDocument = new NodeRef("child://document/"); + ChildAssociationRef childAssocMock = mock(ChildAssociationRef.class); + given(runtimeNodeService.getChildAssocs(parent, ASSOC_CONTAINS, MATCH_ALL)).willReturn(List.of(childAssocMock)); + given(childAssocMock.getChildRef()).willReturn(childDocument); + given(runtimeNodeService.getType(childDocument)).willReturn(TYPE_CONTENT); + + List actual = ruleService.getFoldersInheritingRuleSet(ruleSetNode, 100); + + assertEquals("Unexpected list of inheriting folders.", emptyList(), actual); + } + + /** + * Check that getFoldersInheritingRuleSet does not include folders that the user doesn't have access to. + *

+ * This test uses a chain of three folders: + *

+     *     grandparent - owns the rule set
+     *     parent - user does not have read access
+     *     child - user _does_ have read access
+     * 
+ */ + @Test + public void testGetFoldersInheritingRuleSet_omitFoldersWithoutReadPermission() + { + NodeRef grandparent = new NodeRef("grandparent://node/"); + NodeRef ruleSetNode = new NodeRef("rule://set/"); + ChildAssociationRef ruleSetAssociation = mock(ChildAssociationRef.class); + given(runtimeNodeService.getParentAssocs(ruleSetNode)).willReturn(List.of(ruleSetAssociation)); + given(ruleSetAssociation.getParentRef()).willReturn(grandparent); + NodeRef parent = new NodeRef("parent://node/"); + ChildAssociationRef parentAssocMock = mock(ChildAssociationRef.class); + given(runtimeNodeService.getChildAssocs(grandparent, ASSOC_CONTAINS, MATCH_ALL)).willReturn(List.of(parentAssocMock)); + given(parentAssocMock.getChildRef()).willReturn(parent); + given(runtimeNodeService.getType(parent)).willReturn(TYPE_FOLDER); + NodeRef child = new NodeRef("child://node/"); + ChildAssociationRef childAssocMock = mock(ChildAssociationRef.class); + given(runtimeNodeService.getChildAssocs(grandparent, ASSOC_CONTAINS, MATCH_ALL)).willReturn(List.of(childAssocMock)); + given(childAssocMock.getChildRef()).willReturn(child); + given(runtimeNodeService.getType(child)).willReturn(TYPE_FOLDER); + + // The current user doesn't have permission to view the parent node. + given(permissionService.hasReadPermission(parent)).willReturn(DENIED); + + List actual = ruleService.getFoldersInheritingRuleSet(ruleSetNode, 100); + + assertEquals("Unexpected list of inheriting folders.", List.of(child), actual); + } }