From 67f7fff0b7f32f638ab0a1e00b0b6d0b02512b5c Mon Sep 17 00:00:00 2001 From: krdabrowski <98942253+krdabrowski@users.noreply.github.com> Date: Wed, 5 Oct 2022 12:19:49 +0200 Subject: [PATCH 1/3] ACS-3525: API for manual triggering rules on a folder (#1458) * ACS-3525: API for manual triggering rules on a folder --- .../alfresco/rest/rules/CreateRulesTests.java | 2 +- .../rest/rules/ExecuteRulesTests.java | 96 +++++++++++++++ .../alfresco/rest/rules/GetRulesTests.java | 4 +- .../alfresco/rest/rules/RulesTestsUtils.java | 54 ++++++--- .../alfresco/rest/rules/UpdateRulesTests.java | 5 +- pom.xml | 2 +- .../java/org/alfresco/rest/api/Rules.java | 18 ++- .../rest/api/impl/rules/RulesImpl.java | 43 +++++-- .../rest/api/model/rules/RuleExecution.java | 111 ++++++++++++++++++ .../api/nodes/NodeRuleExecutionsRelation.java | 70 +++++++++++ .../alfresco/public-rest-context.xml | 5 + .../rest/api/impl/rules/RulesImplTest.java | 45 +++++++ 12 files changed, 419 insertions(+), 36 deletions(-) create mode 100644 packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/ExecuteRulesTests.java create mode 100644 remote-api/src/main/java/org/alfresco/rest/api/model/rules/RuleExecution.java create mode 100644 remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeRuleExecutionsRelation.java diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/CreateRulesTests.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/CreateRulesTests.java index 89690ce5ac..8b51be6bed 100644 --- a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/CreateRulesTests.java +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/CreateRulesTests.java @@ -352,7 +352,7 @@ public class CreateRulesTests extends RestTest STEP(String.format("Add a user with '%s' role in the private site's folder", userRole.toString())); UserModel userWithRole = dataUser.createRandomTestUser(); dataUser.addUserToSite(userWithRole, privateSite, userRole); - RestRuleModel ruleModel = createRuleModel("testRule", List.of(createDefaultActionModel())); + RestRuleModel ruleModel = createRuleModel("testRule", List.of(createAddAudioAspectAction())); return restClient.authenticateUser(userWithRole).withCoreAPI().usingNode(privateFolder).usingDefaultRuleSet().createSingleRule(ruleModel); } diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/ExecuteRulesTests.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/ExecuteRulesTests.java new file mode 100644 index 0000000000..c1015586bb --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/ExecuteRulesTests.java @@ -0,0 +1,96 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.rules; + +import static org.alfresco.rest.rules.RulesTestsUtils.AUDIO_ASPECT; +import static org.alfresco.rest.rules.RulesTestsUtils.createRuleExecutionRequest; +import static org.alfresco.rest.rules.RulesTestsUtils.createRuleModelWithDefaultValues; +import static org.alfresco.utility.report.log.Step.STEP; + +import org.alfresco.dataprep.CMISUtil; +import org.alfresco.rest.RestTest; +import org.alfresco.rest.model.RestNodeModel; +import org.alfresco.rest.model.RestRuleModel; +import org.alfresco.utility.Utility; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.model.UserModel; +import org.springframework.http.HttpStatus; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/** + * Tests for POST /nodes/{nodeId}/rule-executions. + */ +@Test(groups = { TestGroup.RULES}) +public class ExecuteRulesTests extends RestTest +{ + private UserModel user; + private SiteModel site; + private FolderModel ruleFolder; + private FileModel file; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + STEP("Create a user, site, folder and file in it"); + user = dataUser.createRandomTestUser(); + site = dataSite.usingUser(user).createPublicRandomSite(); + ruleFolder = dataContent.usingUser(user).usingSite(site).createFolder(); + file = dataContent.usingUser(user).usingResource(ruleFolder).createContent(CMISUtil.DocumentType.TEXT_PLAIN); + + STEP("Create one rule with add audio aspect action in the folder"); + RestRuleModel ruleModel = createRuleModelWithDefaultValues(); + restClient.authenticateUser(user).withCoreAPI().usingNode(ruleFolder).usingDefaultRuleSet().createSingleRule(ruleModel); + } + + /** + * Execute one rule with one action trying to add audio aspect to a file + */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY }) + public void executeOneRuleWithOneAction() throws InterruptedException + { + STEP("Check if file aspects don't contain Audio one"); + RestNodeModel node = restClient.authenticateUser(user).withCoreAPI().usingNode(file).getNode(); + restClient.assertStatusCodeIs(HttpStatus.OK); + node.assertThat().field("aspectNames").notContains(AUDIO_ASPECT); + + STEP("Execute rule"); + restClient.authenticateUser(user).withCoreAPI().usingNode(ruleFolder).executeRules(createRuleExecutionRequest()); + restClient.assertStatusCodeIs(HttpStatus.CREATED); + + STEP("Check if file contains Audio aspect"); + Utility.sleep(500, 10000, () -> { + RestNodeModel fileNode = restClient.authenticateUser(user).withCoreAPI().usingNode(file).getNode(); + restClient.assertStatusCodeIs(HttpStatus.OK); + fileNode.assertThat().field("aspectNames").contains(AUDIO_ASPECT); + }); + } + + // TODO add more E2Es. For more see: ACS-3620 +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/GetRulesTests.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/GetRulesTests.java index caa93a3fea..8aceca3178 100644 --- a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/GetRulesTests.java +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/GetRulesTests.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -152,7 +152,7 @@ public class GetRulesTests extends RestTest rules.getEntries().get(i).onModel() .assertThat().field("isShared").isNotNull() .assertThat().field("description").isNull() - .assertThat().field("isEnabled").is(false) + .assertThat().field("isEnabled").is(true) .assertThat().field("isInheritable").is(false) .assertThat().field("isAsynchronous").is(false) .assertThat().field("errorScript").isNull() diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/RulesTestsUtils.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/RulesTestsUtils.java index 111008c088..d81950963f 100644 --- a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/RulesTestsUtils.java +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/RulesTestsUtils.java @@ -32,6 +32,7 @@ import java.util.Map; import org.alfresco.rest.model.RestActionBodyExecTemplateModel; import org.alfresco.rest.model.RestCompositeConditionDefinitionModel; +import org.alfresco.rest.model.RestRuleExecutionBodyModel; import org.alfresco.rest.model.RestRuleModel; import org.alfresco.rest.model.RestSimpleConditionDefinitionModel; @@ -52,15 +53,22 @@ public class RulesTestsUtils static final String AND = "and"; static final String ID = "id"; static final String IS_SHARED = "isShared"; + static final String AUDIO_ASPECT = "audio:audio"; - /** - * Create a rule model filled with default values. - * - * @return The created rule model. - */ public static RestRuleModel createRuleModelWithModifiedValues() { - RestRuleModel ruleModel = createRuleModelWithDefaultValues(); + return createRuleModelWithModifiedValues(List.of(createAddAudioAspectAction())); + } + + /** + * Create a rule model filled with custom constant values. + * + * @param actions - rule's actions. + * @return The created rule model. + */ + public static RestRuleModel createRuleModelWithModifiedValues(List actions) + { + RestRuleModel ruleModel = createRuleModel(RULE_NAME_DEFAULT, actions); ruleModel.setDescription(RULE_DESCRIPTION_DEFAULT); ruleModel.setIsEnabled(RULE_ENABLED_DEFAULT); ruleModel.setIsInheritable(RULE_CASCADE_DEFAULT); @@ -74,26 +82,27 @@ public class RulesTestsUtils public static RestRuleModel createRuleModelWithDefaultValues() { - return createRuleModel(RULE_NAME_DEFAULT, List.of(createDefaultActionModel())); + return createRuleModel(RULE_NAME_DEFAULT); } public static RestRuleModel createRuleModel(String name) { - return createRuleModel(name, List.of(createDefaultActionModel())); + return createRuleModel(name, List.of(createAddAudioAspectAction())); } /** * Create a rule model. * * @param name The name for the rule. - * @param restActionModels Rule's actions. + * @param actions Rule's actions. * @return The created rule model. */ - public static RestRuleModel createRuleModel(String name, List restActionModels) + public static RestRuleModel createRuleModel(String name, List actions) { RestRuleModel ruleModel = new RestRuleModel(); + ruleModel.setIsEnabled(true); ruleModel.setName(name); - ruleModel.setActions(restActionModels); + ruleModel.setActions(actions); return ruleModel; } @@ -102,12 +111,9 @@ public class RulesTestsUtils * * @return The created action model. */ - public static RestActionBodyExecTemplateModel createDefaultActionModel() + public static RestActionBodyExecTemplateModel createAddAudioAspectAction() { - RestActionBodyExecTemplateModel restActionModel = new RestActionBodyExecTemplateModel(); - restActionModel.setActionDefinitionId("set-property-value"); - restActionModel.setParams(Map.of("aspect-name", "cm:audio")); - return restActionModel; + return createCustomActionModel("add-features", Map.of("aspect-name", AUDIO_ASPECT)); } public static RestActionBodyExecTemplateModel createCustomActionModel(String actionDefinitionId, Map params) @@ -139,7 +145,7 @@ public class RulesTestsUtils createSimpleCondition("tag", "equals", "uat") )), createCompositeCondition(INVERTED, List.of( - createSimpleCondition("aspect", "equals", "audio:audio"), + createSimpleCondition("aspect", "equals", AUDIO_ASPECT), createSimpleCondition("cm:modelVersion", "begins", "1.") )) )); @@ -182,6 +188,20 @@ public class RulesTestsUtils return createCompositeCondition(AND, inverted, null, simpleConditions); } + public static RestRuleExecutionBodyModel createRuleExecutionRequest() + { + return createRuleExecutionRequest(false, false); + } + + public static RestRuleExecutionBodyModel createRuleExecutionRequest(boolean eachSubFolderIncluded, boolean eachInheritedRuleExecuted) + { + RestRuleExecutionBodyModel ruleExecutionBody = new RestRuleExecutionBodyModel(); + ruleExecutionBody.setIsEachSubFolderIncluded(eachSubFolderIncluded); + ruleExecutionBody.setIsEachInheritedRuleExecuted(eachInheritedRuleExecuted); + + return ruleExecutionBody; + } + private static RestCompositeConditionDefinitionModel createCompositeCondition(String booleanMode, boolean inverted, List compositeConditions, List simpleConditions) { diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/UpdateRulesTests.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/UpdateRulesTests.java index 47544c6dab..5e8560de20 100644 --- a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/UpdateRulesTests.java +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/UpdateRulesTests.java @@ -34,9 +34,8 @@ import static org.alfresco.rest.rules.RulesTestsUtils.RULE_CASCADE_DEFAULT; import static org.alfresco.rest.rules.RulesTestsUtils.RULE_ENABLED_DEFAULT; import static org.alfresco.rest.rules.RulesTestsUtils.createCompositeCondition; import static org.alfresco.rest.rules.RulesTestsUtils.createCustomActionModel; -import static org.alfresco.rest.rules.RulesTestsUtils.createDefaultActionModel; +import static org.alfresco.rest.rules.RulesTestsUtils.createAddAudioAspectAction; import static org.alfresco.rest.rules.RulesTestsUtils.createRuleModel; -import static org.alfresco.rest.rules.RulesTestsUtils.createRuleModelWithDefaultValues; import static org.alfresco.rest.rules.RulesTestsUtils.createRuleModelWithModifiedValues; import static org.alfresco.rest.rules.RulesTestsUtils.createSimpleCondition; import static org.alfresco.rest.rules.RulesTestsUtils.createVariousConditions; @@ -530,7 +529,7 @@ public class UpdateRulesTests extends RestTest private RestRuleModel createAndSaveRule(String name) { - return createAndSaveRule(name, List.of(createDefaultActionModel())); + return createAndSaveRule(name, List.of(createAddAudioAspectAction())); } /** diff --git a/pom.xml b/pom.xml index 7354e089ee..2cddcf6a0f 100644 --- a/pom.xml +++ b/pom.xml @@ -123,7 +123,7 @@ 2.7.4 3.0.56 5.2.0 - 1.127 + 1.128 1.9 1.7 1.7 diff --git a/remote-api/src/main/java/org/alfresco/rest/api/Rules.java b/remote-api/src/main/java/org/alfresco/rest/api/Rules.java index 4b299ba62e..c7386f4aa1 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/Rules.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/Rules.java @@ -29,6 +29,7 @@ package org.alfresco.rest.api; import java.util.List; import org.alfresco.rest.api.model.rules.Rule; +import org.alfresco.rest.api.model.rules.RuleExecution; import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; import org.alfresco.rest.framework.resource.parameters.Paging; @@ -65,10 +66,10 @@ public interface Rules Rule getRuleById(String folderNodeId, String ruleSetId, String ruleId, List includes); /** - * Create new rules (and potentially a rule set if "_default_" is supplied). + * Create new rules (and potentially a rule set if "-default-" is supplied). * * @param folderNodeId The node id of a folder. - * @param ruleSetId The id of a rule set (or "_default_" to use/create the default rule set for the folder). + * @param ruleSetId The id of a rule set (or "-default-" to use/create the default rule set for the folder). * @param rule The definition of the rule. * @param includes The list of optional fields to include in the response. * @return The newly created rules. @@ -81,7 +82,7 @@ public interface Rules * Update a rule. * * @param folderNodeId The id of a folder. - * @param ruleSetId The id of a rule set within the folder (or "_default_" to use the default rule set for the folder). + * @param ruleSetId The id of a rule set within the folder (or "-default-" to use the default rule set for the folder). * @param ruleId The rule id. * @param rule The new version of the rule. * @param includes The list of optional fields to include in the response. @@ -94,7 +95,16 @@ public interface Rules * * @param folderNodeId - folder node ID * @param ruleSetId - rule set ID - * @param ruleId - rule ID * + * @param ruleId - rule ID */ void deleteRuleById(String folderNodeId, String ruleSetId, String ruleId); + + /** + * Execute rules for given folder node. + * + * @param folderNodeId - the ID of a folder + * @param eachSubFolderIncluded - indicates if rules should be executed also on sub-folders + * @param eachInheritedRuleExecuted - indicates if the inherited rules should be also executed + */ + RuleExecution executeRules(final String folderNodeId, final boolean eachSubFolderIncluded, final boolean eachInheritedRuleExecuted); } diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/RulesImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/RulesImpl.java index 7c9bc31193..afb0419256 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/RulesImpl.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/RulesImpl.java @@ -26,29 +26,30 @@ package org.alfresco.rest.api.impl.rules; +import java.io.Serializable; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import org.alfresco.rest.api.Nodes; +import org.alfresco.repo.action.ActionImpl; +import org.alfresco.repo.action.access.ActionAccessRestriction; +import org.alfresco.repo.action.executer.ExecuteAllRulesActionExecuter; import org.alfresco.rest.api.Rules; import org.alfresco.rest.api.model.mapper.RestModelMapper; -import org.alfresco.rest.api.model.rules.CompositeCondition; import org.alfresco.rest.api.model.rules.Rule; +import org.alfresco.rest.api.model.rules.RuleExecution; import org.alfresco.rest.api.model.rules.RuleSet; import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; -import org.alfresco.rest.api.model.rules.SimpleCondition; -import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; import org.alfresco.rest.framework.resource.parameters.ListPage; import org.alfresco.rest.framework.resource.parameters.Paging; import org.alfresco.service.Experimental; -import org.alfresco.service.cmr.action.ActionCondition; -import org.alfresco.service.cmr.action.CompositeAction; +import org.alfresco.service.cmr.action.ActionService; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.rule.RuleService; -import org.alfresco.util.collections.CollectionUtils; -import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.util.GUID; +import org.apache.commons.collections.CollectionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,6 +59,7 @@ public class RulesImpl implements Rules private static final Logger LOGGER = LoggerFactory.getLogger(RulesImpl.class); private static final String MUST_HAVE_AT_LEAST_ONE_ACTION = "A rule must have at least one action"; + private ActionService actionService; private RuleService ruleService; private NodeValidator validator; private RuleLoader ruleLoader; @@ -130,6 +132,26 @@ public class RulesImpl implements Rules ruleService.removeRule(folderNodeRef, rule); } + @Override + public RuleExecution executeRules(final String folderNodeId, final boolean eachSubFolderIncluded, final boolean eachInheritedRuleExecuted) + { + final NodeRef folderNodeRef = validator.validateFolderNode(folderNodeId, false); + final Map parameterValues = new HashMap<>(); + parameterValues.put(ExecuteAllRulesActionExecuter.PARAM_RUN_ALL_RULES_ON_CHILDREN, eachSubFolderIncluded); + parameterValues.put(ExecuteAllRulesActionExecuter.PARAM_EXECUTE_INHERITED_RULES, eachInheritedRuleExecuted); + final ActionImpl action = new ActionImpl(null, GUID.generate(), ExecuteAllRulesActionExecuter.NAME); + action.setNodeRef(folderNodeRef); + action.setParameterValues(parameterValues); + + ActionAccessRestriction.setActionContext(action, ActionAccessRestriction.V1_ACTION_CONTEXT); + actionService.executeAction(action, folderNodeRef, true, false); + + return RuleExecution.builder() + .eachSubFolderIncluded(eachSubFolderIncluded) + .eachInheritedRuleExecuted(eachInheritedRuleExecuted) + .create(); + } + private org.alfresco.service.cmr.rule.Rule mapToServiceModelAndValidateActions(Rule rule) { if (CollectionUtils.isEmpty(rule.getActions())) @@ -141,6 +163,11 @@ public class RulesImpl implements Rules return actionPermissionValidator.validateRulePermissions(serviceModelRule); } + public void setActionService(ActionService actionService) + { + this.actionService = actionService; + } + public void setRuleService(RuleService ruleService) { this.ruleService = ruleService; diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/rules/RuleExecution.java b/remote-api/src/main/java/org/alfresco/rest/api/model/rules/RuleExecution.java new file mode 100644 index 0000000000..88cffc29ed --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/rules/RuleExecution.java @@ -0,0 +1,111 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.api.model.rules; + +import java.util.Objects; + +import org.alfresco.service.Experimental; + +@Experimental +public class RuleExecution +{ + private boolean eachSubFolderIncluded; + private boolean eachInheritedRuleExecuted; + + public boolean getIsEachSubFolderIncluded() + { + return eachSubFolderIncluded; + } + + public void setIsEachSubFolderIncluded(boolean eachSubFolderIncluded) + { + this.eachSubFolderIncluded = eachSubFolderIncluded; + } + + public boolean getIsEachInheritedRuleExecuted() + { + return eachInheritedRuleExecuted; + } + + public void setIsEachInheritedRuleExecuted(boolean eachInheritedRuleExecuted) + { + this.eachInheritedRuleExecuted = eachInheritedRuleExecuted; + } + + @Override + public String toString() + { + return "RuleExecution{" + "eachSubFolderIncluded=" + eachSubFolderIncluded + ", eachInheritedRuleExecuted=" + eachInheritedRuleExecuted + '}'; + } + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + RuleExecution that = (RuleExecution) o; + return eachSubFolderIncluded == that.eachSubFolderIncluded && eachInheritedRuleExecuted == that.eachInheritedRuleExecuted; + } + + @Override + public int hashCode() + { + return Objects.hash(eachSubFolderIncluded, eachInheritedRuleExecuted); + } + + public static Builder builder() + { + return new Builder(); + } + + public static class Builder + { + private boolean eachSubFolderIncluded; + private boolean eachInheritedRuleExecuted; + + public Builder eachSubFolderIncluded(boolean eachSubFolderIncluded) + { + this.eachSubFolderIncluded = eachSubFolderIncluded; + return this; + } + + public Builder eachInheritedRuleExecuted(boolean eachInheritedRuleExecuted) + { + this.eachInheritedRuleExecuted = eachInheritedRuleExecuted; + return this; + } + + public RuleExecution create() + { + final RuleExecution ruleExecution = new RuleExecution(); + ruleExecution.setIsEachSubFolderIncluded(eachSubFolderIncluded); + ruleExecution.setIsEachInheritedRuleExecuted(eachInheritedRuleExecuted); + return ruleExecution; + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeRuleExecutionsRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeRuleExecutionsRelation.java new file mode 100644 index 0000000000..a099f31057 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeRuleExecutionsRelation.java @@ -0,0 +1,70 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.api.nodes; + +import java.util.List; + +import org.alfresco.rest.api.Rules; +import org.alfresco.rest.api.model.rules.RuleExecution; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.service.Experimental; +import org.alfresco.util.PropertyCheck; +import org.springframework.beans.factory.InitializingBean; + +@Experimental +@RelationshipResource(name = "rule-executions", entityResource = NodesEntityResource.class, title = "Executing rules") +public class NodeRuleExecutionsRelation implements RelationshipResourceAction.Create, InitializingBean +{ + private final Rules rules; + + public NodeRuleExecutionsRelation(Rules rules) + { + this.rules = rules; + } + + @Override + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "rules", this.rules); + } + + /** + * Execute rules for given folder node. + * + * @param folderNodeId - the ID of a folder + * @param ruleExecutionParameters - rule execution parameters + * @param parameters - additional request parameters + * @return execution details + */ + @Override + public List create(String folderNodeId, List ruleExecutionParameters, Parameters parameters) + { + final RuleExecution ruleExecution = ruleExecutionParameters.stream().findFirst().orElse(new RuleExecution()); + return List.of(rules.executeRules(folderNodeId, ruleExecution.getIsEachSubFolderIncluded(), ruleExecution.getIsEachInheritedRuleExecuted())); + } +} 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 23eadc7954..3ae8a3a0d8 100644 --- a/remote-api/src/main/resources/alfresco/public-rest-context.xml +++ b/remote-api/src/main/resources/alfresco/public-rest-context.xml @@ -906,6 +906,7 @@ + @@ -927,6 +928,10 @@ + + + + diff --git a/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/RulesImplTest.java b/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/RulesImplTest.java index 24811b990b..e3d6848800 100644 --- a/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/RulesImplTest.java +++ b/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/RulesImplTest.java @@ -33,6 +33,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; @@ -40,13 +41,18 @@ import static org.mockito.Mockito.mock; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.stream.IntStream; import junit.framework.TestCase; +import org.alfresco.repo.action.ActionImpl; +import org.alfresco.repo.action.access.ActionAccessRestriction; +import org.alfresco.repo.action.executer.ExecuteAllRulesActionExecuter; import org.alfresco.rest.api.Nodes; import org.alfresco.rest.api.model.mapper.RestModelMapper; import org.alfresco.rest.api.model.rules.Action; import org.alfresco.rest.api.model.rules.Rule; +import org.alfresco.rest.api.model.rules.RuleExecution; import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; @@ -54,12 +60,14 @@ import org.alfresco.rest.framework.core.exceptions.RelationshipResourceNotFoundE import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; import org.alfresco.rest.framework.resource.parameters.Paging; import org.alfresco.service.Experimental; +import org.alfresco.service.cmr.action.ActionService; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.service.cmr.rule.RuleService; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; @@ -77,10 +85,14 @@ public class RulesImplTest extends TestCase private static final NodeRef RULE_NODE_REF = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, RULE_ID); private static final Paging PAGING = Paging.DEFAULT; private static final List INCLUDE = emptyList(); + private static final boolean INCLUDE_SUB_FOLDERS = true; + private static final boolean EXECUTE_INHERITED_RULES = true; @Mock private Nodes nodesMock; @Mock + private ActionService actionServiceMock; + @Mock private RestModelMapper ruleMapper; @Mock private NodeValidator nodeValidatorMock; @@ -618,6 +630,39 @@ public class RulesImplTest extends TestCase } } + @Test + public void testExecuteRule() + { + // when + final RuleExecution actualRuleExecution = rules.executeRules(FOLDER_NODE_ID, INCLUDE_SUB_FOLDERS, EXECUTE_INHERITED_RULES); + + final RuleExecution expectedRuleExecution = RuleExecution.builder() + .eachSubFolderIncluded(INCLUDE_SUB_FOLDERS) + .eachInheritedRuleExecuted(EXECUTE_INHERITED_RULES) + .create(); + final ActionImpl expectedAction = new ActionImpl(null, null, ExecuteAllRulesActionExecuter.NAME); + expectedAction.setNodeRef(FOLDER_NODE_REF); + expectedAction.setParameterValues(Map.of( + ExecuteAllRulesActionExecuter.PARAM_RUN_ALL_RULES_ON_CHILDREN, INCLUDE_SUB_FOLDERS, + ExecuteAllRulesActionExecuter.PARAM_EXECUTE_INHERITED_RULES, EXECUTE_INHERITED_RULES, + ActionAccessRestriction.ACTION_CONTEXT_PARAM_NAME, ActionAccessRestriction.V1_ACTION_CONTEXT) + ); + final ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(ActionImpl.class); + then(nodeValidatorMock).should().validateFolderNode(FOLDER_NODE_ID, false); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + then(actionServiceMock).should().executeAction(actionCaptor.capture(), eq(FOLDER_NODE_REF), eq(true), eq(false)); + then(actionServiceMock).shouldHaveNoMoreInteractions(); + final ActionImpl actualAction = actionCaptor.getValue(); + assertThat(actualAction) + .isNotNull() + .usingRecursiveComparison().ignoringFields("id") + .isEqualTo(expectedAction); + assertThat(actualRuleExecution) + .isNotNull() + .usingRecursiveComparison() + .isEqualTo(expectedRuleExecution); + } + private static org.alfresco.service.cmr.rule.Rule createRule(final String id) { final NodeRef nodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, id); From 440f0568e396d9e6c5c72b38d42b544c307c10d5 Mon Sep 17 00:00:00 2001 From: Tom Page Date: Wed, 5 Oct 2022 14:22:05 +0100 Subject: [PATCH 2/3] ACS-3291 Permission checks when linking and unlinking to rule sets. --- .../rest/rules/RuleSetLinksTests.java | 114 +++++++++++++++++- .../rest/api/impl/rules/RuleSetsImpl.java | 4 +- .../rest/api/impl/rules/RuleSetsImplTest.java | 4 +- 3 files changed, 116 insertions(+), 6 deletions(-) diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/RuleSetLinksTests.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/RuleSetLinksTests.java index 765c9c44d5..cd38a5d134 100644 --- a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/RuleSetLinksTests.java +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/RuleSetLinksTests.java @@ -26,8 +26,15 @@ package org.alfresco.rest.rules; import static org.alfresco.rest.rules.RulesTestsUtils.createRuleModel; +import static org.alfresco.rest.rules.RulesTestsUtils.createRuleModelWithDefaultValues; +import static org.alfresco.utility.constants.UserRole.SiteConsumer; import static org.alfresco.utility.report.log.Step.STEP; -import static org.springframework.http.HttpStatus.*; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.CREATED; +import static org.springframework.http.HttpStatus.FORBIDDEN; +import static org.springframework.http.HttpStatus.NOT_FOUND; +import static org.springframework.http.HttpStatus.NO_CONTENT; +import static org.springframework.http.HttpStatus.OK; import org.alfresco.dataprep.CMISUtil; import org.alfresco.rest.RestTest; @@ -167,7 +174,6 @@ public class RuleSetLinksTests extends RestTest .get(0).onModel().assertThat().isEqualTo(expectedRuleSet); } - /** * Check we get 404 when linking to a non-existing rule set/folder. */ @@ -308,6 +314,51 @@ public class RuleSetLinksTests extends RestTest .get(0).onModel().assertThat().isEqualTo(expectedRuleSet); } + /** + * Check we get an error when trying to link to a rule set that we can't view. + */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void linkToRuleSetWithoutPermission() + { + STEP("Use admin to create a private site with a folder containing a rule."); + SiteModel privateSite = dataSite.usingAdmin().createPrivateRandomSite(); + FolderModel privateFolder = dataContent.usingAdmin().usingSite(privateSite).createFolder(); + restClient.authenticateUser(dataUser.getAdminUser()).withCoreAPI().usingNode(privateFolder).usingDefaultRuleSet() + .createSingleRule(createRuleModelWithDefaultValues()); + + STEP("Use a normal user to try to link to the rule."); + FolderModel publicFolder = dataContent.usingUser(user).usingSite(site).createFolder(); + RestRuleSetLinkModel request = new RestRuleSetLinkModel(); + request.setId(privateFolder.getNodeRef()); + restClient.authenticateUser(user).withCoreAPI().usingNode(publicFolder).createRuleLink(request); + + restClient.assertStatusCodeIs(FORBIDDEN); + } + + /** + * Check we are able to link to a rule set with only read permission. + */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void linkToRuleSetWithOnlyReadPermission() + { + STEP("Use admin to create a private site with a folder containing a rule."); + SiteModel privateSite = dataSite.usingAdmin().createPrivateRandomSite(); + FolderModel privateFolder = dataContent.usingAdmin().usingSite(privateSite).createFolder(); + restClient.authenticateUser(dataUser.getAdminUser()).withCoreAPI().usingNode(privateFolder).usingDefaultRuleSet() + .createSingleRule(createRuleModelWithDefaultValues()); + + STEP("Add the normal user as a consumer."); + dataUser.usingAdmin().addUserToSite(user, privateSite, SiteConsumer); + + STEP("Use a normal user to try to link to the rule."); + FolderModel publicFolder = dataContent.usingUser(user).usingSite(site).createFolder(); + RestRuleSetLinkModel request = new RestRuleSetLinkModel(); + request.setId(privateFolder.getNodeRef()); + restClient.authenticateUser(user).withCoreAPI().usingNode(publicFolder).createRuleLink(request); + + restClient.assertStatusCodeIs(CREATED); + } + /** * Check we can DELETE/unlink a ruleset * @@ -391,4 +442,63 @@ public class RuleSetLinksTests extends RestTest restClient.assertStatusCodeIs(NOT_FOUND) .assertLastError().containsSummary("The entity with id:"); } + + /** + * Check we cannot unlink from a rule set that we can't view. + */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void unlinkFromRuleSetWithoutPermission() + { + STEP("Use admin to create a private site with a folder containing a rule."); + SiteModel privateSite = dataSite.usingAdmin().createPrivateRandomSite(); + FolderModel privateFolder = dataContent.usingAdmin().usingSite(privateSite).createFolder(); + restClient.authenticateUser(dataUser.getAdminUser()).withCoreAPI().usingNode(privateFolder).usingDefaultRuleSet() + .createSingleRule(createRuleModelWithDefaultValues()); + + STEP("Add the user as a consumer."); + dataUser.usingAdmin().addUserToSite(user, privateSite, SiteConsumer); + + STEP("Use the consumer to create a folder with a link to the private rule set."); + FolderModel publicFolder = dataContent.usingUser(user).usingSite(site).createFolder(); + RestRuleSetLinkModel request = new RestRuleSetLinkModel(); + request.setId(privateFolder.getNodeRef()); + restClient.authenticateUser(user).withCoreAPI().usingNode(publicFolder).createRuleLink(request); + restClient.assertStatusCodeIs(CREATED); + + STEP("Remove the user from the private site."); + dataUser.usingAdmin().removeUserFromSite(user, privateSite); + + STEP("Use the user to try to unlink from the rule set."); + restClient.authenticateUser(user).withCoreAPI().usingNode(publicFolder).unlinkRuleSet("-default-"); + + restClient.assertStatusCodeIs(FORBIDDEN); + } + + /** + * Check we can unlink from a rule set if we only have read permission for it. + */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void unlinkFromRuleSetWithOnlyReadPermission() + { + STEP("Use admin to create a private site with a folder containing a rule."); + SiteModel privateSite = dataSite.usingAdmin().createPrivateRandomSite(); + FolderModel privateFolder = dataContent.usingAdmin().usingSite(privateSite).createFolder(); + restClient.authenticateUser(dataUser.getAdminUser()).withCoreAPI().usingNode(privateFolder).usingDefaultRuleSet() + .createSingleRule(createRuleModelWithDefaultValues()); + + STEP("Add the user as a consumer."); + dataUser.usingAdmin().addUserToSite(user, privateSite, SiteConsumer); + + STEP("Use the consumer to create a folder with a link to the private rule set."); + FolderModel publicFolder = dataContent.usingUser(user).usingSite(site).createFolder(); + RestRuleSetLinkModel request = new RestRuleSetLinkModel(); + request.setId(privateFolder.getNodeRef()); + restClient.authenticateUser(user).withCoreAPI().usingNode(publicFolder).createRuleLink(request); + restClient.assertStatusCodeIs(CREATED); + + STEP("Use the consumer to try to unlink from the rule set."); + restClient.authenticateUser(user).withCoreAPI().usingNode(publicFolder).unlinkRuleSet("-default-"); + + restClient.assertStatusCodeIs(NO_CONTENT); + } } diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/RuleSetsImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/RuleSetsImpl.java index 8b162290ca..61f673f648 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/RuleSetsImpl.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/RuleSetsImpl.java @@ -129,8 +129,8 @@ public class RuleSetsImpl implements RuleSets final NodeRef folderNodeRef = validator.validateFolderNode(folderNodeId,true); final boolean isRuleSetNode = validator.isRuleSetNode(linkToNodeId); final NodeRef linkToNodeRef = isRuleSetNode - ? validator.validateRuleSetNode(linkToNodeId, true) - : validator.validateFolderNode(linkToNodeId, true); + ? validator.validateRuleSetNode(linkToNodeId, false) + : validator.validateFolderNode(linkToNodeId, false); //The target node should have pre-existing rules to link to if (!ruleService.hasRules(linkToNodeRef)) { diff --git a/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/RuleSetsImplTest.java b/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/RuleSetsImplTest.java index a53dac2e87..0619f59864 100644 --- a/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/RuleSetsImplTest.java +++ b/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/RuleSetsImplTest.java @@ -98,7 +98,7 @@ public class RuleSetsImplTest extends TestCase MockitoAnnotations.openMocks(this); given(nodeValidatorMock.validateFolderNode(eq(LINK_TO_NODE_ID), anyBoolean())).willReturn(LINK_TO_NODE); - given(nodeValidatorMock.validateRuleSetNode(LINK_TO_NODE_ID,true)).willReturn(LINK_TO_NODE); + given(nodeValidatorMock.validateRuleSetNode(LINK_TO_NODE_ID,false)).willReturn(LINK_TO_NODE); given(nodeValidatorMock.validateFolderNode(eq(FOLDER_ID), anyBoolean())).willReturn(FOLDER_NODE); given(nodeValidatorMock.validateRuleSetNode(RULE_SET_ID, FOLDER_NODE)).willReturn(RULE_SET_NODE); @@ -252,7 +252,7 @@ public class RuleSetsImplTest extends TestCase then(nodeValidatorMock).should().validateFolderNode(FOLDER_ID,true); then(nodeValidatorMock).should().isRuleSetNode(LINK_TO_NODE_ID); - then(nodeValidatorMock).should().validateRuleSetNode(LINK_TO_NODE_ID,true); + then(nodeValidatorMock).should().validateRuleSetNode(LINK_TO_NODE_ID,false); then(nodeValidatorMock).shouldHaveNoMoreInteractions(); then(ruleServiceMock).should().hasRules(LINK_TO_NODE); then(ruleServiceMock).should().hasRules(FOLDER_NODE); From 68b9a0e8a6bdc59772352fe45d259952b7867de3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBurek?= Date: Wed, 5 Oct 2022 15:53:42 +0200 Subject: [PATCH 3/3] ACS-3643 Add meaningful message in the Workflow Console when deployment fails (#1467) --- .../repo/workflow/WorkflowInterpreter.java | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/repository/src/main/java/org/alfresco/repo/workflow/WorkflowInterpreter.java b/repository/src/main/java/org/alfresco/repo/workflow/WorkflowInterpreter.java index 40768cf9bc..a03fe39f4f 100644 --- a/repository/src/main/java/org/alfresco/repo/workflow/WorkflowInterpreter.java +++ b/repository/src/main/java/org/alfresco/repo/workflow/WorkflowInterpreter.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -34,6 +34,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import org.alfresco.model.ContentModel; import org.alfresco.repo.admin.BaseInterpreter; @@ -54,6 +55,7 @@ import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; import org.alfresco.service.cmr.security.PersonService; import org.alfresco.service.cmr.security.PersonService.PersonInfo; +import org.alfresco.service.cmr.workflow.FailedWorkflowDeployment; import org.alfresco.service.cmr.workflow.WorkflowDefinition; import org.alfresco.service.cmr.workflow.WorkflowDeployment; import org.alfresco.service.cmr.workflow.WorkflowException; @@ -726,10 +728,18 @@ public class WorkflowInterpreter extends BaseInterpreter { out.println(problem); } - out.println("deployed definition id: " + def.getId() + " , name: " + def.getName() + " , title: " + def.getTitle() + " , version: " + def.getVersion()); - currentDeployEngine = command[1]; - currentDeployResource = command[2]; - out.print(executeCommand("use definition " + def.getId())); + final Optional possibleDeploymentFailure = FailedWorkflowDeployment.getFailure(deployment); + if (possibleDeploymentFailure.isPresent()) + { + out.println("Failed to deploy the workflow definition."); + } + else + { + out.println("deployed definition id: " + def.getId() + " , name: " + def.getName() + " , title: " + def.getTitle() + " , version: " + def.getVersion()); + currentDeployEngine = command[1]; + currentDeployResource = command[2]; + out.print(executeCommand("use definition " + def.getId())); + } } else if (command[0].equals("redeploy"))