diff --git a/amps/ags/pom.xml b/amps/ags/pom.xml index 4cd032a00d..80cd3d6fa0 100644 --- a/amps/ags/pom.xml +++ b/amps/ags/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-community-repo-amps - 23.3.0.58-SNAPSHOT + 23.3.0.61-SNAPSHOT diff --git a/amps/ags/rm-automation/pom.xml b/amps/ags/rm-automation/pom.xml index 8fb2984e5e..2a3bd30cbd 100644 --- a/amps/ags/rm-automation/pom.xml +++ b/amps/ags/rm-automation/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-governance-services-community-parent - 23.3.0.58-SNAPSHOT + 23.3.0.61-SNAPSHOT diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/pom.xml b/amps/ags/rm-automation/rm-automation-community-rest-api/pom.xml index 8a1c88b4ee..c3c3778968 100644 --- a/amps/ags/rm-automation/rm-automation-community-rest-api/pom.xml +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-governance-services-automation-community-repo - 23.3.0.58-SNAPSHOT + 23.3.0.61-SNAPSHOT diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/core/RestAPIFactory.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/core/RestAPIFactory.java index b07ec559b5..d034690d8a 100644 --- a/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/core/RestAPIFactory.java +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/core/RestAPIFactory.java @@ -49,6 +49,7 @@ import org.alfresco.rest.rm.community.requests.gscore.api.TransferAPI; import org.alfresco.rest.rm.community.requests.gscore.api.TransferContainerAPI; import org.alfresco.rest.rm.community.requests.gscore.api.UnfiledContainerAPI; import org.alfresco.rest.rm.community.requests.gscore.api.UnfiledRecordFolderAPI; +import org.alfresco.rest.rm.community.requests.gscore.api.RetentionScheduleAPI; import org.alfresco.utility.data.DataUserAIS; import org.alfresco.utility.model.RepoTestModel; import org.alfresco.utility.model.UserModel; @@ -254,4 +255,14 @@ public class RestAPIFactory { return getGSCoreAPI(userModel).usingHoldsAPI(); } + + public RetentionScheduleAPI getRetentionScheduleAPI() + { + return getGSCoreAPI(null).usingRetentionScheduleAPI(); + } + + public RetentionScheduleAPI getRetentionScheduleAPI(UserModel userModel) + { + return getGSCoreAPI(userModel).usingRetentionScheduleAPI(); + } } diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/retentionschedule/RetentionSchedule.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/retentionschedule/RetentionSchedule.java new file mode 100644 index 0000000000..63ba5045a8 --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/retentionschedule/RetentionSchedule.java @@ -0,0 +1,49 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2024 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.rm.community.model.retentionschedule; + +import lombok.EqualsAndHashCode; +import org.alfresco.utility.model.TestModel; +import lombok.Data; + +import java.util.List; + +/** + * retention schedule + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class RetentionSchedule extends TestModel +{ + private String id ; + private String parentId; + private String authority; + private String instructions; + private boolean isRecordLevel; + private boolean isUnpublishedUpdates; + private List actions; +} diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/retentionschedule/RetentionScheduleActionDefinition.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/retentionschedule/RetentionScheduleActionDefinition.java new file mode 100644 index 0000000000..0c79e9f543 --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/retentionschedule/RetentionScheduleActionDefinition.java @@ -0,0 +1,50 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2024 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.rm.community.model.retentionschedule; + +import java.util.List; +import lombok.Data; + +/** + * retention schedule action definition + */ +@Data +public class RetentionScheduleActionDefinition +{ + private String id; + private String name; + private int periodAmount; + private String period; + private String periodProperty; + private boolean combineDispositionStepConditions; + private List events; + private boolean eligibleOnFirstCompleteEvent; + private String description; + private boolean retainRecordMetadataAfterDestruction; + private String location; + private int index; +} \ No newline at end of file diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/retentionschedule/RetentionScheduleCollection.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/retentionschedule/RetentionScheduleCollection.java new file mode 100644 index 0000000000..878a48ff09 --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/retentionschedule/RetentionScheduleCollection.java @@ -0,0 +1,32 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2024 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.rm.community.model.retentionschedule; + +import org.alfresco.rest.core.RestModels; +public class RetentionScheduleCollection extends RestModels +{ +} diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/retentionschedule/RetentionScheduleEntry.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/retentionschedule/RetentionScheduleEntry.java new file mode 100644 index 0000000000..74a95dcf19 --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/model/retentionschedule/RetentionScheduleEntry.java @@ -0,0 +1,37 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2024 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.rm.community.model.retentionschedule; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import org.alfresco.rest.core.RestModels; +@Data +public class RetentionScheduleEntry extends RestModels +{ + @JsonProperty + private RetentionSchedule entry; +} diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/requests/gscore/GSCoreAPI.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/requests/gscore/GSCoreAPI.java index d1c6a6c2c7..84333a5c73 100644 --- a/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/requests/gscore/GSCoreAPI.java +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/requests/gscore/GSCoreAPI.java @@ -47,6 +47,7 @@ import org.alfresco.rest.rm.community.requests.gscore.api.TransferAPI; import org.alfresco.rest.rm.community.requests.gscore.api.TransferContainerAPI; import org.alfresco.rest.rm.community.requests.gscore.api.UnfiledContainerAPI; import org.alfresco.rest.rm.community.requests.gscore.api.UnfiledRecordFolderAPI; +import org.alfresco.rest.rm.community.requests.gscore.api.RetentionScheduleAPI; /** * Defines the entire GS Core API @@ -193,4 +194,9 @@ public class GSCoreAPI extends RMModelRequest } public HoldsAPI usingHoldsAPI() { return new HoldsAPI(getRmRestWrapper()); } + + public RetentionScheduleAPI usingRetentionScheduleAPI() + { + return new RetentionScheduleAPI(getRmRestWrapper()); + } } diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/requests/gscore/api/RetentionScheduleAPI.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/requests/gscore/api/RetentionScheduleAPI.java new file mode 100644 index 0000000000..83a2a142d4 --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/rm/community/requests/gscore/api/RetentionScheduleAPI.java @@ -0,0 +1,125 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2024 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.rm.community.requests.gscore.api; + +import org.alfresco.rest.core.RMRestWrapper; +import org.alfresco.rest.rm.community.model.retentionschedule.RetentionSchedule; +import org.alfresco.rest.rm.community.model.retentionschedule.RetentionScheduleCollection; +import org.alfresco.rest.rm.community.requests.RMModelRequest; + +import static org.alfresco.rest.core.RestRequest.requestWithBody; +import static org.alfresco.rest.core.RestRequest.simpleRequest; +import static org.alfresco.rest.rm.community.util.ParameterCheck.mandatoryObject; +import static org.alfresco.rest.rm.community.util.ParameterCheck.mandatoryString; +import static org.alfresco.rest.rm.community.util.PojoUtility.toJson; +import static org.apache.commons.lang3.StringUtils.EMPTY; +import static org.springframework.http.HttpMethod.GET; +import static org.springframework.http.HttpMethod.POST; + +public class RetentionScheduleAPI extends RMModelRequest +{ + + /** + * @param rmRestWrapper + */ + public RetentionScheduleAPI(RMRestWrapper rmRestWrapper) + { + super(rmRestWrapper); + } + + + /** + * Creates a retention schedule. + * + * @param retentionScheduleModel The retentionSchedule model + * @param recordCategoryId The identifier of a record category + * @param parameters The URL parameters to add + * @return The created {@link RetentionSchedule} + * @throws RuntimeException for the following cases: + *
    + *
  • {@code recordCategoryId} is not a valid format or {@code recordCategoryId} is invalid
  • + *
  • authentication fails
  • + *
  • current user does not have permission to add children to {@code recordCategoryId}
  • + *
  • {@code recordCategoryId} does not exist
  • + *
  • new name clashes with an existing node in the current parent container
  • + *
+ */ + public RetentionSchedule createRetentionSchedule(RetentionSchedule retentionScheduleModel, String recordCategoryId, String parameters) + { + mandatoryString("recordCategoryId", recordCategoryId); + mandatoryObject("retentionScheduleModel", retentionScheduleModel); + + return getRmRestWrapper().processModel(RetentionSchedule.class, requestWithBody( + POST, + toJson(retentionScheduleModel), + "record-categories/{recordCategoryId}/retention-schedules", + recordCategoryId, + parameters + )); + } + + /** + * See {@link #createRetentionSchedule(RetentionSchedule, String, String)} + */ + public RetentionSchedule createRetentionSchedule(RetentionSchedule retentionScheduleModel, String recordCategoryId) + { + return createRetentionSchedule(retentionScheduleModel, recordCategoryId, EMPTY); + } + + /** + * Gets the retentionSchedule of a record category. + * + * @param recordCategoryId The identifier of a record category + * @param parameters The URL parameters to add + * @return The {@link RetentionSchedule} for the given {@code recordCategoryId} + * @throws RuntimeException for the following cases: + *
    + *
  • authentication fails
  • + *
  • current user does not have permission to read {@code recordCategoryId}
  • + *
  • {@code recordCategoryId} does not exist
  • + *
+ */ + public RetentionScheduleCollection getRetentionSchedule(String recordCategoryId, String parameters) + { + mandatoryString("recordCategoryId", recordCategoryId); + + return getRmRestWrapper().processModels(RetentionScheduleCollection.class, simpleRequest( + GET, + "record-categories/{recordCategoryId}/retention-schedules?{parameters}", + recordCategoryId, + parameters + )); + } + + /** + * See {@link #getRetentionSchedule(String, String)} + */ + public RetentionScheduleCollection getRetentionSchedule(String recordCategoryId) + { + return getRetentionSchedule(recordCategoryId, EMPTY); + } +} \ No newline at end of file diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/retentionschedule/RetentionScheduleTests.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/retentionschedule/RetentionScheduleTests.java new file mode 100644 index 0000000000..017530e385 --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/retentionschedule/RetentionScheduleTests.java @@ -0,0 +1,270 @@ +/*- + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2024 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.rm.community.retentionschedule; + +import org.alfresco.rest.rm.community.base.BaseRMRestTest; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory; +import org.alfresco.rest.rm.community.model.retentionschedule.RetentionSchedule; +import org.alfresco.rest.rm.community.model.retentionschedule.RetentionScheduleCollection; +import org.alfresco.rest.v0.RMRolesAndActionsAPI; +import org.alfresco.utility.model.UserModel; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import static org.alfresco.rest.core.v0.BaseAPI.RM_SITE_ID; +import static org.alfresco.utility.data.RandomData.getRandomAlphanumeric; +import static org.alfresco.utility.data.RandomData.getRandomName; +import static org.springframework.http.HttpStatus.CONFLICT; +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.OK; +import static org.springframework.http.HttpStatus.UNAUTHORIZED; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.AssertJUnit.assertEquals; + +/** + * This class contains the tests for the Retention Schedule CRUD V1 API + */ +public class RetentionScheduleTests extends BaseRMRestTest +{ + private RecordCategory recordCategory; + private RetentionSchedule createdRetentionSchedule; + private UserModel nonRMuser; + @Autowired + private RMRolesAndActionsAPI rmRolesAndActionsAPI; + + @BeforeClass(alwaysRun = true) + public void preconditionForRetentionScheduleTests() + { + createRMSiteIfNotExists(); + // create a non rm user + nonRMuser = dataUser.createRandomTestUser("testUser"); + //Create record category + recordCategory = createRootCategory(getRandomName("recordCategory")); + + + } + /** + *
+     * Given that a record category exists
+     * When I ask the API to create a retention schedule with a user having no rights
+     * Then it will give 403 as status code
+     * 
+ */ + @Test(priority = 1) + public void createRetentionScheduleFor403() + { + RetentionSchedule retentionSchedule = new RetentionSchedule(); + + // Create retention schedule with user having no rights + getRestAPIFactory().getRetentionScheduleAPI(nonRMuser).createRetentionSchedule(retentionSchedule, recordCategory.getId()); + + // Verify the status code + assertStatusCode(FORBIDDEN); + } + + /** + *
+     * Given that a record category does not exists
+     * When I ask the API to create a retention schedule on a category Id
+     * Then it will give 404 as a status code
+     * 
+ */ + @Test(priority = 2) + public void createRetentionScheduleFor404() + { + RetentionSchedule retentionSchedule = new RetentionSchedule(); + + //Create retention schedule with category id not exist + getRestAPIFactory().getRetentionScheduleAPI().createRetentionSchedule(retentionSchedule, getRandomAlphanumeric()); + + // Verify the status code + assertStatusCode(NOT_FOUND); + } + + /** + *
+     * Given that a record category exists
+     * When I ask the API to create a retention schedule on a category id with a user having unauthorized access
+     * Then it will give 401 as a status code
+     * 
+ */ + @Test(priority = 3) + public void createRetentionScheduleFor401() + { + RetentionSchedule retentionSchedule = new RetentionSchedule(); + + //Create retention schedule with a user with unauthorized access + createdRetentionSchedule = getRestAPIFactory().getRetentionScheduleAPI(new UserModel(getAdminUser().getUsername(), "wrongPassword")).createRetentionSchedule(retentionSchedule, recordCategory.getId()); + + // Verify the status code + assertStatusCode(UNAUTHORIZED); + } + + /** + *
+     * Given that a record category exists
+     * When I ask the API to create a retention schedule with a user having access
+     * Then it is created with a 201 status code
+     * 
+ */ + @Test(priority = 4) + public void createRetentionScheduleFor201() + { + RetentionSchedule retentionSchedule = new RetentionSchedule(); + String authority = "authority" + getRandomAlphanumeric(); + String instructions = "instructions" + getRandomAlphanumeric(); + boolean isRecordLevel = false; + retentionSchedule.setAuthority(authority); + retentionSchedule.setInstructions(instructions); + retentionSchedule.setRecordLevel(isRecordLevel); + + //Create retention schedule with a valid user + createdRetentionSchedule = getRestAPIFactory().getRetentionScheduleAPI() + .createRetentionSchedule(retentionSchedule, recordCategory.getId()); + + // Verify the status code + assertStatusCode(CREATED); + assertEquals(createdRetentionSchedule.getAuthority(), authority); + assertEquals(createdRetentionSchedule.getInstructions(), instructions); + assertFalse(createdRetentionSchedule.isRecordLevel()); + assertNotNull(createdRetentionSchedule.getId()); + } + + /** + *
+     * Given that a record category exists
+     * When I ask the API to create a retention schedule on a category id having retention schedule already
+     * Then it will give 409 as a status code
+     * 
+ */ + @Test(priority = 5) + public void createRetentionScheduleFor409() + { + RetentionSchedule retentionSchedule = new RetentionSchedule(); + //Create retention schedule on a category with already having retention schedule + getRestAPIFactory().getRetentionScheduleAPI() + .createRetentionSchedule(retentionSchedule, recordCategory.getId()); + + assertStatusCode(CONFLICT); + } + + /** + *
+     * Given that a record category exists
+     * When I ask the API to get a retention schedule on a given categoryId with a user having no rights
+     * Then it will give 403
+     * 
+ */ + @Test(priority = 6) + public void retentionScheduleWith403() + { + //Get retention schedule with user having no rights + getRestAPIFactory().getRetentionScheduleAPI(nonRMuser).getRetentionSchedule(recordCategory.getId()); + + // Verify the status code + assertStatusCode(FORBIDDEN); + } + + /** + *
+     * Given that a record category does not exists
+     * When I ask the API to get a retention schedule on a category Id
+     * Then it will give 404 as a status code
+     * 
+ */ + @Test(priority = 7) + public void retentionScheduleWith404() + { + + //Get retention schedule with category id that does not exist + getRestAPIFactory().getRetentionScheduleAPI().getRetentionSchedule(getRandomAlphanumeric()); + + // Verify the status code + assertStatusCode(NOT_FOUND); + } + + /** + *
+     * Given that a record category exists
+     * When I ask the API to get a retention schedule on a categoryId with a user having unauthorized access
+     * Then it will give 401 as a status code
+     * 
+ */ + @Test(priority = 8) + public void retentionScheduleWith401() + { + //Create retention schedule with a user with unauthorized access + getRestAPIFactory().getRetentionScheduleAPI(new UserModel(getAdminUser().getUsername(), "wrongPassword")).getRetentionSchedule(recordCategory.getId()); + + // Verify the status code + assertStatusCode(UNAUTHORIZED); + } + + /** + *
+     * Given that a record category exists
+     * When I ask the API to get a retention schedule on a categoryId with a user having access
+     * Then it will give retentionSchedule with 200 as a status code
+     * 
+ */ + @Test(priority = 9) + public void retentionScheduleWith200() + { + RetentionScheduleCollection retentionScheduleCollection = getRestAPIFactory().getRetentionScheduleAPI().getRetentionSchedule(recordCategory.getId()); + // Verify the status code + assertStatusCode(OK); + retentionScheduleCollection.getEntries().forEach(c -> + { + RetentionSchedule retentionSchedule = c.getEntry(); + String retentionScheduleId = retentionSchedule.getId(); + assertNotNull(retentionScheduleId); + logger.info("Checking retention schedule " + retentionScheduleId); + + // Find this retention schedule is created one or not + assertEquals(createdRetentionSchedule.getId(), retentionScheduleId); + assertEquals(createdRetentionSchedule.getParentId(),retentionSchedule.getParentId()); + assertEquals(createdRetentionSchedule.getAuthority(), retentionSchedule.getAuthority()); + assertEquals(createdRetentionSchedule.getInstructions(), retentionSchedule.getInstructions()); + assertEquals(createdRetentionSchedule.isRecordLevel(), retentionSchedule.isRecordLevel()); + assertEquals(createdRetentionSchedule.isUnpublishedUpdates(), retentionSchedule.isUnpublishedUpdates()); + }); + } + + @AfterClass(alwaysRun = true) + public void cleanUpRetentionScheduleTests() + { + rmRolesAndActionsAPI.deleteAllItemsInContainer(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), RM_SITE_ID, recordCategory.getName()); + deleteRecordCategory(recordCategory.getId()); + dataUser.deleteUser(nonRMuser); + } +} \ No newline at end of file diff --git a/amps/ags/rm-community/pom.xml b/amps/ags/rm-community/pom.xml index f241bcf6ea..b0163be9a1 100644 --- a/amps/ags/rm-community/pom.xml +++ b/amps/ags/rm-community/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-governance-services-community-parent - 23.3.0.58-SNAPSHOT + 23.3.0.61-SNAPSHOT diff --git a/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/bootstrap/RMDataDictionaryBootstrap.xml b/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/bootstrap/RMDataDictionaryBootstrap.xml index 38fd599a62..aa98e6cd62 100644 --- a/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/bootstrap/RMDataDictionaryBootstrap.xml +++ b/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/bootstrap/RMDataDictionaryBootstrap.xml @@ -31,6 +31,11 @@ Configuration information for the Records Management application. + + + + + diff --git a/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-public-rest-context.xml b/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-public-rest-context.xml index f1f225c569..970c73a5ee 100644 --- a/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-public-rest-context.xml +++ b/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-public-rest-context.xml @@ -147,6 +147,13 @@ + + + + + + + diff --git a/amps/ags/rm-community/rm-community-repo/pom.xml b/amps/ags/rm-community/rm-community-repo/pom.xml index ac473e89de..a7cf0395a8 100644 --- a/amps/ags/rm-community/rm-community-repo/pom.xml +++ b/amps/ags/rm-community/rm-community-repo/pom.xml @@ -8,7 +8,7 @@ org.alfresco alfresco-governance-services-community-repo-parent - 23.3.0.58-SNAPSHOT + 23.3.0.61-SNAPSHOT diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionServiceImpl.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionServiceImpl.java index 282f190f28..cf87aaf2c1 100644 --- a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionServiceImpl.java +++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionServiceImpl.java @@ -59,6 +59,9 @@ import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; import org.alfresco.repo.transaction.AlfrescoTransactionSupport; import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.NodeRef; @@ -198,7 +201,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl /** * Behavior to initialize the disposition schedule of a newly filed record. * - * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementPolicies.OnFileRecord#onFileRecord(org.alfresco.service.cmr.repository.NodeRef) + * @see RecordsManagementPolicies.OnFileRecord#onFileRecord(NodeRef) */ @Override @Behaviour(kind=BehaviourKind.CLASS, type="rma:record") @@ -216,7 +219,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl } /** - * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#refreshDispositionAction(NodeRef) + * @see DispositionService#refreshDispositionAction(NodeRef) */ @Override public void refreshDispositionAction(NodeRef nodeRef) @@ -242,7 +245,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl /** ========= Disposition Property Methods ========= */ /** - * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#registerDispositionProperty(org.alfresco.module.org_alfresco_module_rm.disposition.property.DispositionProperty) + * @see DispositionService#registerDispositionProperty(DispositionProperty) */ @Override public void registerDispositionProperty(DispositionProperty dispositionProperty) @@ -251,7 +254,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl } /** - * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#getDispositionProperties(boolean, java.lang.String) + * @see DispositionService#getDispositionProperties(boolean, String) */ @Override public Collection getDispositionProperties(boolean isRecordLevel, String dispositionAction) @@ -270,7 +273,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl } /** - * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#getDispositionProperties() + * @see DispositionService#getDispositionProperties() */ @Override public Collection getDispositionProperties() @@ -281,12 +284,11 @@ public class DispositionServiceImpl extends ServiceBaseImpl /** ========= Disposition Schedule Methods ========= */ /** - * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#getDispositionSchedule(org.alfresco.service.cmr.repository.NodeRef) + * @see DispositionService#getDispositionSchedule(NodeRef) */ @Override public DispositionSchedule getDispositionSchedule(final NodeRef nodeRef) { - DispositionSchedule ds = null; NodeRef dsNodeRef = null; if (isRecord(nodeRef)) { @@ -311,36 +313,33 @@ public class DispositionServiceImpl extends ServiceBaseImpl if (dsNextAction != null) { final NodeRef action = dsNextAction.getNextActionNodeRef(); - if (isNotTrue((Boolean)nodeService.getProperty(action, PROP_MANUALLY_SET_AS_OF))) + if (isNotTrue((Boolean)nodeService.getProperty(action, PROP_MANUALLY_SET_AS_OF)) && !dsNextAction.getWriteMode().equals(WriteMode.READ_ONLY)) { - if (!dsNextAction.getWriteMode().equals(WriteMode.READ_ONLY)) + final String dispositionActionName = dsNextAction.getNextActionName(); + final Date dispositionActionDate = dsNextAction.getNextActionDateAsOf(); + + RunAsWork runAsWork = () -> { + nodeService.setProperty(action, PROP_DISPOSITION_AS_OF, dispositionActionDate); + return null; + }; + + // if the current transaction is READ ONLY set the property on the node + // in a READ WRITE transaction + if (AlfrescoTransactionSupport.getTransactionReadState().equals(TxnReadState.TXN_READ_ONLY)) { - final String dispositionActionName = dsNextAction.getNextActionName(); - final Date dispositionActionDate = dsNextAction.getNextActionDateAsOf(); - - RunAsWork runAsWork = () -> { - nodeService.setProperty(action, PROP_DISPOSITION_AS_OF, dispositionActionDate); - return null; - }; - - // if the current transaction is READ ONLY set the property on the node - // in a READ WRITE transaction - if (AlfrescoTransactionSupport.getTransactionReadState().equals(TxnReadState.TXN_READ_ONLY)) - { - transactionService.getRetryingTransactionHelper().doInTransaction((RetryingTransactionCallback) () -> { - AuthenticationUtil.runAsSystem(runAsWork); - return null; - }, false, true); - } - else - { + transactionService.getRetryingTransactionHelper().doInTransaction((RetryingTransactionCallback) () -> { AuthenticationUtil.runAsSystem(runAsWork); - } + return null; + }, false, true); + } + else + { + AuthenticationUtil.runAsSystem(runAsWork); + } - if (dsNextAction.getWriteMode().equals(WriteMode.DATE_AND_NAME)) - { - nodeService.setProperty(action, PROP_DISPOSITION_ACTION_NAME, dispositionActionName); - } + if (dsNextAction.getWriteMode().equals(WriteMode.DATE_AND_NAME)) + { + nodeService.setProperty(action, PROP_DISPOSITION_ACTION_NAME, dispositionActionName); } } @@ -352,7 +351,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl // Get the disposition instructions for the node reference provided dsNodeRef = getDispositionScheduleImpl(nodeRef); } - + DispositionSchedule ds = null; if (dsNodeRef != null) { ds = new DispositionScheduleImpl(serviceRegistry, nodeService, dsNodeRef); @@ -382,7 +381,8 @@ public class DispositionServiceImpl extends ServiceBaseImpl } return result; } - + + @Override public DispositionSchedule getOriginDispositionSchedule(NodeRef nodeRef) { NodeRef parent = this.nodeService.getPrimaryParent(nodeRef).getParentRef(); @@ -406,7 +406,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl } /** - * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#getAssociatedDispositionSchedule(org.alfresco.service.cmr.repository.NodeRef) + * @see DispositionService#getAssociatedDispositionSchedule(NodeRef) */ @Override public DispositionSchedule getAssociatedDispositionSchedule(NodeRef nodeRef) @@ -437,7 +437,6 @@ public class DispositionServiceImpl extends ServiceBaseImpl */ private NodeRef getAssociatedDispositionScheduleImpl(NodeRef nodeRef) { - NodeRef result = null; ParameterCheck.mandatory("nodeRef", nodeRef); // Make sure we are dealing with an RM node @@ -445,6 +444,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl { throw new AlfrescoRuntimeException("Can not find the associated retention schedule for a non records management component. (nodeRef=" + nodeRef.toString() + ")"); } + NodeRef result = null; if (getInternalNodeService().hasAspect(nodeRef, ASPECT_SCHEDULED)) { List childAssocs = getInternalNodeService().getChildAssocs(nodeRef, ASSOC_DISPOSITION_SCHEDULE, RegexQNamePattern.MATCH_ALL); @@ -459,7 +459,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl } /** - * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#getAssociatedRecordsManagementContainer(org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule) + * @see DispositionService#getAssociatedRecordsManagementContainer(DispositionSchedule) */ @Override public NodeRef getAssociatedRecordsManagementContainer(DispositionSchedule dispositionSchedule) @@ -477,12 +477,9 @@ public class DispositionServiceImpl extends ServiceBaseImpl { // TODO in the future we should be able to support disposition schedule reuse, but for now just warn that // only the first disposition schedule will be considered - if (LOGGER.isWarnEnabled()) - { - LOGGER.warn("Retention schedule has more than one associated records management container. " + - "This is not currently supported so only the first container will be considered. " + - "(dispositionScheduleNodeRef=" + dispositionSchedule.getNodeRef().toString() + ")"); - } + LOGGER.atWarn().log("Retention schedule has more than one associated records management container. " + + "This is not currently supported so only the first container will be considered. " + + "(dispositionScheduleNodeRef={})", dispositionSchedule.getNodeRef()); } // Get the container reference @@ -495,7 +492,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl } /** - * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#hasDisposableItems(org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule) + * @see DispositionService#hasDisposableItems(DispositionSchedule) */ @Override public boolean hasDisposableItems(DispositionSchedule dispositionSchdule) @@ -537,19 +534,16 @@ public class DispositionServiceImpl extends ServiceBaseImpl return true; } } - else if (filePlanService.isRecordCategory(item) && getAssociatedDispositionScheduleImpl(item) == null) + else if (filePlanService.isRecordCategory(item) && getAssociatedDispositionScheduleImpl(item) == null && hasDisposableItemsImpl(isRecordLevelDisposition, item)) { - if (hasDisposableItemsImpl(isRecordLevelDisposition, item)); - { - return true; - } + return true; } } return false; } /** - * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#getDisposableItems(org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule) + * @see DispositionService#getDisposableItems(DispositionSchedule) */ @Override public List getDisposableItems(DispositionSchedule dispositionSchedule) @@ -564,7 +558,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl } /** - * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#isDisposableItem(org.alfresco.service.cmr.repository.NodeRef) + * @see DispositionService#isDisposableItem(NodeRef) */ @Override public boolean isDisposableItem(NodeRef nodeRef) @@ -604,20 +598,18 @@ public class DispositionServiceImpl extends ServiceBaseImpl } /** - * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#createDispositionSchedule(org.alfresco.service.cmr.repository.NodeRef, java.util.Map) + * @see DispositionService#createDispositionSchedule(NodeRef, Map) */ @Override public DispositionSchedule createDispositionSchedule(NodeRef nodeRef, Map props) { - NodeRef dsNodeRef = null; - // Check mandatory parameters ParameterCheck.mandatory("nodeRef", nodeRef); // Check exists if (!nodeService.exists(nodeRef)) { - throw new AlfrescoRuntimeException("Unable to create retention schedule, because node does not exist. (nodeRef=" + nodeRef.toString() + ")"); + throw new EntityNotFoundException(nodeRef.getId()); } // Check is sub-type of rm:recordCategory @@ -625,10 +617,12 @@ public class DispositionServiceImpl extends ServiceBaseImpl if (!TYPE_RECORD_CATEGORY.equals(nodeRefType) && !dictionaryService.isSubClass(nodeRefType, TYPE_RECORD_CATEGORY)) { - throw new AlfrescoRuntimeException("Unable to create retention schedule on a node that is not a records management container."); + throw new InvalidArgumentException("The given id:'" + nodeRef.getId() + "' (nodeType:" + nodeRef + + ") is not valid. Expected nodeType is:" + TYPE_RECORD_CATEGORY); } behaviourFilter.disableBehaviour(nodeRef, ASPECT_SCHEDULED); + NodeRef dsNodeRef = null; try { // Add the schedules aspect if required @@ -662,7 +656,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl else { // Error since the node already has a disposition schedule set - throw new AlfrescoRuntimeException("Unable to create retention schedule on node that already has a retention schedule."); + throw new ConstraintViolatedException("Unable to create retention schedule on node that already has a retention schedule."); } } finally @@ -686,7 +680,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl { // make sure at least a name has been defined String name = (String)actionDefinitionParams.get(PROP_DISPOSITION_ACTION_NAME); - if (name == null || name.length() == 0) + if (name == null || name.isEmpty()) { throw new IllegalArgumentException("'name' parameter is mandatory when creating a disposition action definition"); } @@ -695,10 +689,10 @@ public class DispositionServiceImpl extends ServiceBaseImpl // create the child association from the schedule to the action definition NodeRef actionNodeRef = this.nodeService.createNode(schedule.getNodeRef(), - RecordsManagementModel.ASSOC_DISPOSITION_ACTION_DEFINITIONS, + ASSOC_DISPOSITION_ACTION_DEFINITIONS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName(name)), - RecordsManagementModel.TYPE_DISPOSITION_ACTION_DEFINITION, actionDefinitionParams).getChildRef(); + TYPE_DISPOSITION_ACTION_DEFINITION, actionDefinitionParams).getChildRef(); // get the updated disposition schedule and retrieve the new action definition NodeRef scheduleParent = this.nodeService.getPrimaryParent(schedule.getNodeRef()).getParentRef(); @@ -707,7 +701,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl } /** - * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#removeDispositionActionDefinition(org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule, org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition) + * @see DispositionService#removeDispositionActionDefinition(DispositionSchedule, DispositionActionDefinition) */ @Override public void removeDispositionActionDefinition(DispositionSchedule schedule, DispositionActionDefinition actionDefinition) @@ -777,16 +771,12 @@ public class DispositionServiceImpl extends ServiceBaseImpl DispositionAction da; // check if current transaction is a READ ONLY one and if true create the node in a READ WRITE transaction - if (AlfrescoTransactionSupport.getTransactionReadState().equals(TxnReadState.TXN_READ_ONLY)) - { - da = - transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() - { - public DispositionAction execute() throws Throwable - { - return createDispositionAction(nodeRef, props); - } - }, false, true); + if (AlfrescoTransactionSupport.getTransactionReadState().equals(TxnReadState.TXN_READ_ONLY)) { + da = transactionService.getRetryingTransactionHelper().doInTransaction( + () -> createDispositionAction(nodeRef, props), + false, + true + ); } else { @@ -836,13 +826,13 @@ public class DispositionServiceImpl extends ServiceBaseImpl Period period = dispositionActionDefinition.getPeriod(); if (period != null) { - Date contextDate = null; + Date contextDate; // Get the period properties value QName periodProperty = dispositionActionDefinition.getPeriodProperty(); if (periodProperty != null) { - if (RecordsManagementModel.PROP_DISPOSITION_AS_OF.equals(periodProperty)) + if (PROP_DISPOSITION_AS_OF.equals(periodProperty)) { DispositionAction lastCompletedDispositionAction = getLastCompletedDispostionAction(nodeRef); if (lastCompletedDispositionAction != null) @@ -886,7 +876,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl } /** - * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#isNextDispositionActionEligible(org.alfresco.service.cmr.repository.NodeRef) + * @see DispositionService#isNextDispositionActionEligible(NodeRef) */ @Override public boolean isNextDispositionActionEligible(NodeRef nodeRef) @@ -940,7 +930,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl { NodeRef eventExecution = assoc.getChildRef(); Boolean isCompleteValue = (Boolean) getInternalNodeService().getProperty(eventExecution, PROP_EVENT_EXECUTION_COMPLETE); - boolean isComplete = false; + boolean isComplete; if (isCompleteValue != null) { isComplete = isCompleteValue.booleanValue(); @@ -987,7 +977,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl } /** - * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#getNextDispositionAction(org.alfresco.service.cmr.repository.NodeRef) + * @see DispositionService#getNextDispositionAction(NodeRef) */ @Override public DispositionAction getNextDispositionAction(NodeRef nodeRef) @@ -1006,7 +996,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl /** ========= Disposition Action History Methods ========= */ /** - * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#getCompletedDispositionActions(org.alfresco.service.cmr.repository.NodeRef) + * @see DispositionService#getCompletedDispositionActions(NodeRef) */ @Override public List getCompletedDispositionActions(NodeRef nodeRef) @@ -1022,7 +1012,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl } /** - * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#getLastCompletedDispostionAction(org.alfresco.service.cmr.repository.NodeRef) + * @see DispositionService#getLastCompletedDispostionAction(NodeRef) */ @Override public DispositionAction getLastCompletedDispostionAction(NodeRef nodeRef) @@ -1038,7 +1028,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl } /** - * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#isDisposableItemCutoff(NodeRef) + * @see DispositionService#isDisposableItemCutoff(NodeRef) */ @Override public boolean isDisposableItemCutoff(NodeRef nodeRef) @@ -1048,7 +1038,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl } /** - * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#updateNextDispositionAction(NodeRef) + * @see DispositionService#updateNextDispositionAction(NodeRef) */ @Override public void updateNextDispositionAction(final NodeRef nodeRef) @@ -1058,7 +1048,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl RunAsWork runAsWork = new RunAsWork() { /** - * @see org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork#doWork() + * @see RunAsWork#doWork() */ @Override public Void doWork() @@ -1077,7 +1067,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl } /** - * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#updateNextDispositionAction(NodeRef) + * @see DispositionService#updateNextDispositionAction(NodeRef) */ @Override public void updateNextDispositionAction(final NodeRef nodeRef, final DispositionSchedule dispositionSchedule) @@ -1087,7 +1077,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl RunAsWork runAsWork = new RunAsWork() { /** - * @see org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork#doWork() + * @see RunAsWork#doWork() */ @Override public Void doWork() @@ -1113,16 +1103,13 @@ public class DispositionServiceImpl extends ServiceBaseImpl } List dispositionActionDefinitions = dispositionSchedule.getDispositionActionDefinitions(); - DispositionActionDefinition currentDispositionActionDefinition = null; + DispositionActionDefinition currentDispositionActionDefinition; DispositionActionDefinition nextDispositionActionDefinition = null; - if (currentDispositionAction == null) + if (currentDispositionAction == null && !dispositionActionDefinitions.isEmpty()) { - if (!dispositionActionDefinitions.isEmpty()) - { - // The next disposition action is the first action - nextDispositionActionDefinition = dispositionActionDefinitions.get(0); - } + // The next disposition action is the first action + nextDispositionActionDefinition = dispositionActionDefinitions.get(0); } else { @@ -1167,7 +1154,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl } /** - * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#cutoffDisposableItem(NodeRef) + * @see DispositionService#cutoffDisposableItem(NodeRef) */ @Override public void cutoffDisposableItem(final NodeRef nodeRef) @@ -1205,6 +1192,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl // runAs system so that we can close a record that has already been cutoff authenticationUtil.runAsSystem(new RunAsWork() { + @Override public Void doWork() throws Exception { recordFolderService.closeRecordFolder(nodeRef); @@ -1224,6 +1212,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl } } + @Override public Date getDispositionActionDate(NodeRef record, NodeRef dispositionSchedule, String dispositionActionName) { DispositionSchedule ds = new DispositionScheduleImpl(serviceRegistry, nodeService, dispositionSchedule); @@ -1243,7 +1232,8 @@ public class DispositionServiceImpl extends ServiceBaseImpl } return null; } - + + @Override public void recalculateNextDispositionStep(NodeRef record) { List recordFolders = recordFolderService.getRecordFolders(record); @@ -1384,14 +1374,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl Date calculatedDate = (nextDispositionActionDate != null ? nextDispositionActionDate : maxDate); // We only need to update the date if the current one is too early. - if (recordDate.before(calculatedDate)) - { - return WriteMode.DATE_ONLY; - } - else - { - return WriteMode.READ_ONLY; - } + return recordDate.before(calculatedDate) ? WriteMode.DATE_ONLY : WriteMode.READ_ONLY; } /** @@ -1414,7 +1397,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl DispositionSchedule ds = new DispositionScheduleImpl(serviceRegistry, nodeService, folderDS); List dispositionActionDefinitions = ds.getDispositionActionDefinitions(); - if (dispositionActionDefinitions != null && dispositionActionDefinitions.size() > 0) + if (dispositionActionDefinitions != null && !dispositionActionDefinitions.isEmpty()) { DispositionActionDefinition firstDispositionActionDef = dispositionActionDefinitions.get(0); dispositionNodeRef = folderDS; diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/impl/ApiNodesModelFactory.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/impl/ApiNodesModelFactory.java index a48a7ae4cf..7cd3334d67 100644 --- a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/impl/ApiNodesModelFactory.java +++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/impl/ApiNodesModelFactory.java @@ -34,10 +34,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition; import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEvent; import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; import org.alfresco.rest.api.Nodes; import org.alfresco.rest.api.model.AssocChild; @@ -61,6 +64,8 @@ import org.alfresco.rm.rest.api.model.UnfiledContainer; import org.alfresco.rm.rest.api.model.UnfiledContainerChild; import org.alfresco.rm.rest.api.model.UnfiledRecordFolder; import org.alfresco.rm.rest.api.model.UnfiledRecordFolderChild; +import org.alfresco.rm.rest.api.model.RetentionSchedule; +import org.alfresco.rm.rest.api.model.RetentionScheduleActionDefinition; import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.model.FileInfo; import org.alfresco.service.cmr.repository.ChildAssociationRef; @@ -71,6 +76,8 @@ import org.alfresco.service.cmr.security.PersonService; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; +import static org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel.PROP_COMBINE_DISPOSITION_STEP_CONDITIONS; + /** * Utility class containing Alfresco and RM java services required by the API * endpoints @@ -891,4 +898,139 @@ public class ApiNodesModelFactory mapAssociations(record, info, parameters.getInclude()); return record; } -} + + /** + * Helper method that sets the information for the retention schedule type. + * @param dispositionSchedule + * @return RetentionSchedule + */ + public RetentionSchedule mapRetentionScheduleData(DispositionSchedule dispositionSchedule) + { + RetentionSchedule retentionSchedule = new RetentionSchedule(); + retentionSchedule.setId(dispositionSchedule.getNodeRef().getId()); + if(dispositionSchedule.getNodeRef() != null) { + NodeRef parent = this.nodeService.getPrimaryParent(dispositionSchedule.getNodeRef()).getParentRef(); + retentionSchedule.setParentId(parent.getId()); + } + retentionSchedule.setInstructions(dispositionSchedule.getDispositionInstructions()); + retentionSchedule.setAuthority(dispositionSchedule.getDispositionAuthority()); + retentionSchedule.setRecordLevel(dispositionSchedule.isRecordLevelDisposition()); + + boolean unpublishedUpdates = dispositionSchedule.getDispositionActionDefinitions().stream() + .map(DispositionActionDefinition::getNodeRef) + .anyMatch(actionDefNodeRef -> nodeService.hasAspect(actionDefNodeRef, RecordsManagementModel.ASPECT_UNPUBLISHED_UPDATE)); + retentionSchedule.setUnpublishedUpdates(unpublishedUpdates); + return retentionSchedule; + } + + /** + * Helper method that sets the information for the retention schedule action definition type. + * @param dispositionActionDefinition + * @return RetentionScheduleActionDefinition + */ + public RetentionScheduleActionDefinition mapRetentionScheduleActionDefData(DispositionActionDefinition dispositionActionDefinition) + { + RetentionScheduleActionDefinition retentionScheduleActionDefinition = new RetentionScheduleActionDefinition(); + // Mapping basic properties + mapRetentionActionProperties(dispositionActionDefinition, retentionScheduleActionDefinition); + // Mapping period and period amount + mapPeriodProperties(dispositionActionDefinition, retentionScheduleActionDefinition); + // Mapping events properties + mapEventsProperties(dispositionActionDefinition, retentionScheduleActionDefinition); + return retentionScheduleActionDefinition; + } + + /** + * Helper method that sets core information for the retention schedule action definition type. + * @param dispositionActionDefinition + * @param retentionScheduleActionDefinition + */ + private void mapRetentionActionProperties(DispositionActionDefinition dispositionActionDefinition, RetentionScheduleActionDefinition retentionScheduleActionDefinition) + { + retentionScheduleActionDefinition.setId(dispositionActionDefinition.getId()); + retentionScheduleActionDefinition.setName(dispositionActionDefinition.getName()); + retentionScheduleActionDefinition.setDescription(dispositionActionDefinition.getDescription()); + retentionScheduleActionDefinition.setEligibleOnFirstCompleteEvent(dispositionActionDefinition.eligibleOnFirstCompleteEvent()); + if (nodeService.getProperty(dispositionActionDefinition.getNodeRef(), PROP_COMBINE_DISPOSITION_STEP_CONDITIONS) != null) + { + retentionScheduleActionDefinition.setCombineDispositionStepConditions((Boolean) nodeService.getProperty(dispositionActionDefinition.getNodeRef(), PROP_COMBINE_DISPOSITION_STEP_CONDITIONS)); + } + retentionScheduleActionDefinition.setLocation(dispositionActionDefinition.getLocation()); + if (dispositionActionDefinition.getGhostOnDestroy() != null) + { + retentionScheduleActionDefinition.setRetainRecordMetadataAfterDestruction(dispositionActionDefinition.getGhostOnDestroy().equals("ghost")); + } + retentionScheduleActionDefinition.setIndex(dispositionActionDefinition.getIndex()); + } + + /** + * Helper method that sets the period-related information for the retention schedule action definition type. + * @param dispositionActionDefinition + * @param retentionScheduleActionDefinition + */ + private void mapPeriodProperties(DispositionActionDefinition dispositionActionDefinition, RetentionScheduleActionDefinition retentionScheduleActionDefinition) + { + if(dispositionActionDefinition.getPeriodProperty() != null) { + retentionScheduleActionDefinition.setPeriodProperty(dispositionActionDefinition.getPeriodProperty().toPrefixString(namespaceService)); + } + String period = dispositionActionDefinition.getPeriod().toString(); + if (!period.isEmpty()) + { + // In rest api we are splitting `period` property into `period` and `periodAmount`. + // so we need to split the period into two properties. + // ex. period -> 'month|10' so the split properties would be like below + // period -> 'month' + // periodAmount -> 10 + String[] periodArray = period.split("\\|"); + if (periodArray.length > 0) + { + retentionScheduleActionDefinition.setPeriod(periodArray[0]); + } + if (periodArray.length > 1) + { + try + { + retentionScheduleActionDefinition.setPeriodAmount(Integer.parseInt(periodArray[1])); + } + catch (NumberFormatException e) + { + throw new NumberFormatException("Error parsing period amount: " + e.getMessage()); + } + } + } + } + + /** + * Helper method that sets the events information for the retention schedule action definition type. + * @param dispositionActionDefinition + * @param retentionScheduleActionDefinition + */ + private void mapEventsProperties(DispositionActionDefinition dispositionActionDefinition, RetentionScheduleActionDefinition retentionScheduleActionDefinition) + { + List events = dispositionActionDefinition.getEvents(); + if (events != null && !events.isEmpty()) + { + List eventNames = events.stream() + .map(RecordsManagementEvent::getName) + .collect(Collectors.toList()); + retentionScheduleActionDefinition.setEvents(eventNames); + } + } + + /** + * Helper method that sets the optional information for the retention schedule type. + * @param retentionSchedule + * @param schedule + * @param includeParam + */ + public void mapRetentionScheduleOptionalInfo(RetentionSchedule retentionSchedule, DispositionSchedule schedule, List includeParam) + { + if (includeParam != null && !includeParam.isEmpty() && includeParam.contains("actions")) + { + List actions = schedule.getDispositionActionDefinitions().stream() + .map(this::mapRetentionScheduleActionDefData) + .collect(Collectors.toList()); + retentionSchedule.setActions(actions); + } + } +} \ No newline at end of file diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/RetentionSchedule.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/RetentionSchedule.java new file mode 100644 index 0000000000..1f698d0f33 --- /dev/null +++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/RetentionSchedule.java @@ -0,0 +1,45 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2024 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.rm.rest.api.model; + +import lombok.Data; +import java.util.List; + +/** + * retention schedule + */ +@Data +public class RetentionSchedule +{ + private String id ; + private String parentId; + private String authority; + private String instructions; + private boolean isRecordLevel; + private boolean isUnpublishedUpdates; + private List actions; +} diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/RetentionScheduleActionDefinition.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/RetentionScheduleActionDefinition.java new file mode 100644 index 0000000000..d9806e01df --- /dev/null +++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/model/RetentionScheduleActionDefinition.java @@ -0,0 +1,51 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2024 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.rm.rest.api.model; + +import java.util.List; + +import lombok.Data; + +/** + * retention schedule action definition + */ +@Data +public class RetentionScheduleActionDefinition +{ + private String id; + private String name; + private int periodAmount; + private String period; + private String periodProperty; + private boolean combineDispositionStepConditions; + private List events; + private boolean eligibleOnFirstCompleteEvent; + private String description; + private boolean retainRecordMetadataAfterDestruction; + private String location; + private int index; +} \ No newline at end of file diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/retentionschedule/RetentionScheduleRelation.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/retentionschedule/RetentionScheduleRelation.java new file mode 100644 index 0000000000..60a7a3b856 --- /dev/null +++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/retentionschedule/RetentionScheduleRelation.java @@ -0,0 +1,108 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2024 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.rm.rest.api.retentionschedule; + +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rm.rest.api.impl.ApiNodesModelFactory; +import org.alfresco.rm.rest.api.impl.FilePlanComponentsApiUtils; +import org.alfresco.rm.rest.api.model.RetentionSchedule; +import org.alfresco.rm.rest.api.recordcategories.RecordCategoriesEntityResource; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel.PROP_DISPOSITION_AUTHORITY; +import static org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel.PROP_DISPOSITION_INSTRUCTIONS; +import static org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel.PROP_RECORD_LEVEL_DISPOSITION; +import static org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel.TYPE_RECORD_CATEGORY; +import static org.alfresco.module.org_alfresco_module_rm.util.RMParameterCheck.checkNotBlank; +import static org.alfresco.util.ParameterCheck.mandatory; +import lombok.Data; + +/** + * Retention schedule relation is used perform retention schedule operation for a record category. + */ +@RelationshipResource(name = "retention-schedules", entityResource = RecordCategoriesEntityResource.class, title = "Retention Schedule") +@Data +public class RetentionScheduleRelation implements RelationshipResourceAction.Read, + RelationshipResourceAction.Create +{ + + private FilePlanComponentsApiUtils apiUtils; + private ApiNodesModelFactory nodesModelFactory; + private DispositionService dispositionService; + protected NodeService nodeService; + + @Override + @WebApiDescription(title="Create a retention schedule for the particular record category using the 'recordCategoryId'") + public List create(String recordCategoryId, List nodeInfos, Parameters parameters) + { + checkNotBlank("recordCategoryId", recordCategoryId); + mandatory("entity", nodeInfos); + mandatory("parameters", parameters); + NodeRef parentNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, recordCategoryId); + List result = new ArrayList<>(); + // Create the disposition schedule + Map dsProps = new HashMap<>(); + dsProps.put(PROP_DISPOSITION_AUTHORITY, nodeInfos.get(0).getAuthority()); + dsProps.put(PROP_DISPOSITION_INSTRUCTIONS, nodeInfos.get(0).getInstructions()); + dsProps.put(PROP_RECORD_LEVEL_DISPOSITION, nodeInfos.get(0).isRecordLevel()); + DispositionSchedule dispositionSchedule = dispositionService.createDispositionSchedule(parentNodeRef, dsProps); + RetentionSchedule retentionSchedule = nodesModelFactory.mapRetentionScheduleData(dispositionSchedule); + result.add(retentionSchedule); + return result; + } + + @Override + @WebApiDescription(title = "Return a paged list of retention schedule based on the 'recordCategoryId'") + public CollectionWithPagingInfo readAll(String recordCategoryId, Parameters parameters) + { + checkNotBlank("recordCategoryId", recordCategoryId); + mandatory("parameters", parameters); + NodeRef parentNodeRef = apiUtils.lookupAndValidateNodeType(recordCategoryId, TYPE_RECORD_CATEGORY); + DispositionSchedule schedule = dispositionService.getDispositionSchedule(parentNodeRef); + RetentionSchedule retentionSchedule = nodesModelFactory.mapRetentionScheduleData(schedule); + List retentionScheduleList = new ArrayList<>(); + nodesModelFactory.mapRetentionScheduleOptionalInfo(retentionSchedule, schedule, parameters.getInclude()); + retentionScheduleList.add(retentionSchedule); + return CollectionWithPagingInfo.asPaged(parameters.getPaging(), retentionScheduleList, false, + retentionScheduleList.size()); + } +} \ No newline at end of file diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/retentionschedule/package-info.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/retentionschedule/package-info.java new file mode 100644 index 0000000000..07e151ccb9 --- /dev/null +++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/rm/rest/api/retentionschedule/package-info.java @@ -0,0 +1,34 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2024 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 info that defines the Information Governance Retention Schedule REST API + */ +@WebApi(name="gs", scope=Api.SCOPE.PUBLIC, version=1) +package org.alfresco.rm.rest.api.retentionschedule; +import org.alfresco.rest.framework.Api; +import org.alfresco.rest.framework.WebApi; \ No newline at end of file diff --git a/amps/ags/rm-community/rm-community-repo/test/java/org/alfresco/module/org_alfresco_module_rm/test/legacy/service/DispositionServiceImplTest.java b/amps/ags/rm-community/rm-community-repo/test/java/org/alfresco/module/org_alfresco_module_rm/test/legacy/service/DispositionServiceImplTest.java index e2b7cb7b5b..f584ce59dd 100644 --- a/amps/ags/rm-community/rm-community-repo/test/java/org/alfresco/module/org_alfresco_module_rm/test/legacy/service/DispositionServiceImplTest.java +++ b/amps/ags/rm-community/rm-community-repo/test/java/org/alfresco/module/org_alfresco_module_rm/test/legacy/service/DispositionServiceImplTest.java @@ -51,6 +51,8 @@ import org.alfresco.module.org_alfresco_module_rm.job.publish.PublishExecutorReg import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; import org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMTestCase; import org.alfresco.module.org_alfresco_module_rm.test.util.CommonRMTestUtils; +import org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException; +import org.junit.Assert; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.namespace.QName; @@ -437,19 +439,12 @@ public class DispositionServiceImplTest extends BaseRMTestCase // Check the disposition schedule checkDispositionSchedule(ds, "testCreateDispositionSchedule", "testCreateDispositionSchedule", false); - } - }); - // Failure: create disposition schedule on container with existing disposition schedule - doTestInTransaction(new FailureTest - ( - "Can not create a disposition schedule on a container with an existing disposition schedule" - ) - { - @Override - public void run() - { - utils.createBasicDispositionSchedule(rmContainer); + // Failure: create disposition schedule on container with existing disposition schedule + Assert.assertThrows(ConstraintViolatedException.class, + () -> { + utils.createBasicDispositionSchedule(rmContainer); + }); } }); } @@ -492,19 +487,12 @@ public class DispositionServiceImplTest extends BaseRMTestCase // Check the disposition schedule checkDispositionSchedule(testA, "testA", "testA", false); checkDispositionSchedule(testB, "testB", "testB", false); - } - }); - // Failure: create disposition schedule on container with existing disposition schedule - doTestInTransaction(new FailureTest - ( - "Can not create a disposition schedule on container with an existing disposition schedule" - ) - { - @Override - public void run() - { - utils.createBasicDispositionSchedule(mhContainer11); + // Failure: create disposition schedule on container with existing disposition schedule + Assert.assertThrows(ConstraintViolatedException.class, + () -> { + utils.createBasicDispositionSchedule(rmContainer); + }); } }); diff --git a/amps/ags/rm-community/rm-community-repo/unit-test/java/org/alfresco/module/org_alfresco_module_rm/disposition/RetentionScheduleModelUnitTest.java b/amps/ags/rm-community/rm-community-repo/unit-test/java/org/alfresco/module/org_alfresco_module_rm/disposition/RetentionScheduleModelUnitTest.java new file mode 100644 index 0000000000..2b89472fde --- /dev/null +++ b/amps/ags/rm-community/rm-community-repo/unit-test/java/org/alfresco/module/org_alfresco_module_rm/disposition/RetentionScheduleModelUnitTest.java @@ -0,0 +1,110 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2024 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.module.org_alfresco_module_rm.disposition; + +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.test.util.BaseUnitTest; +import org.alfresco.rm.rest.api.impl.ApiNodesModelFactory; +import org.alfresco.rm.rest.api.model.RetentionSchedule; +import org.alfresco.rm.rest.api.model.RetentionScheduleActionDefinition; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.Period; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +/** + * Retention schedule model unit test + */ +public class RetentionScheduleModelUnitTest extends BaseUnitTest +{ + private static final String AUTHORITY = "authority"; + private static final String INSTRUCTIONS = "instructions"; + private static final String RETAIN_STEP = "retain"; + + @InjectMocks + private ApiNodesModelFactory apiNodesModelFactory; + + @Mock + DispositionSchedule dispositionSchedule; + + @Mock + DispositionActionDefinition dispositionActionDefinition; + + @Test + public void mapRetentionScheduleDataTest() + { + // Mock data + NodeRef nodeRef = generateNodeRef(RecordsManagementModel.TYPE_DISPOSITION_SCHEDULE, true); + ChildAssociationRef childAssociationRef = generateChildAssociationRef(filePlan, record); + when(dispositionSchedule.getDispositionAuthority()).thenReturn(AUTHORITY); + when(dispositionSchedule.getDispositionInstructions()).thenReturn(INSTRUCTIONS); + when(dispositionSchedule.getNodeRef()).thenReturn(nodeRef); + when(dispositionSchedule.isRecordLevelDisposition()).thenReturn(false); + when(mockedNodeService.getPrimaryParent(nodeRef)).thenReturn(childAssociationRef); + // Call the method + RetentionSchedule expectedResult = apiNodesModelFactory.mapRetentionScheduleData(dispositionSchedule); + assertEquals(expectedResult.getId(), dispositionSchedule.getNodeRef().getId()); + assertEquals(expectedResult.getAuthority(), dispositionSchedule.getDispositionAuthority()); + assertEquals(expectedResult.getInstructions(), dispositionSchedule.getDispositionInstructions()); + assertEquals(expectedResult.isRecordLevel(), dispositionSchedule.isRecordLevelDisposition()); + } + + @Test + public void mapRetentionScheduleActionDefDataTest() + { + // Mock data + NodeRef nodeRef = generateNodeRef(RecordsManagementModel.TYPE_DISPOSITION_SCHEDULE, true); + String period = "month|10"; + ChildAssociationRef childAssociationRef = generateChildAssociationRef(filePlan, record); + when(dispositionActionDefinition.getNodeRef()).thenReturn(nodeRef); + when(dispositionActionDefinition.getName()).thenReturn(RETAIN_STEP); + when(dispositionActionDefinition.getDescription()).thenReturn("Description"); + when(dispositionActionDefinition.getIndex()).thenReturn(1); + when(dispositionActionDefinition.getGhostOnDestroy()).thenReturn("ghost"); + when(dispositionActionDefinition.getPeriod()).thenReturn(new Period(period)); + when(dispositionActionDefinition.getLocation()).thenReturn("location"); + when(dispositionActionDefinition.getId()).thenReturn(nodeRef.getId()); + when(mockedNodeService.getPrimaryParent(nodeRef)).thenReturn(childAssociationRef); + // Call the method + RetentionScheduleActionDefinition expectedResult = apiNodesModelFactory.mapRetentionScheduleActionDefData(dispositionActionDefinition); + String resultPeriod = expectedResult.getPeriod() + "|" + expectedResult.getPeriodAmount(); + // Assertions + assertEquals(expectedResult.getId(), dispositionActionDefinition.getId()); + assertEquals(expectedResult.getName(), dispositionActionDefinition.getName()); + assertEquals(expectedResult.getDescription(), dispositionActionDefinition.getDescription()); + assertEquals(expectedResult.getIndex(), dispositionActionDefinition.getIndex()); + assertEquals(expectedResult.getLocation(), dispositionActionDefinition.getLocation()); + assertEquals(new Period(resultPeriod), dispositionActionDefinition.getPeriod()); + assertTrue(expectedResult.isRetainRecordMetadataAfterDestruction()); + } +} \ No newline at end of file diff --git a/amps/ags/rm-community/rm-community-rest-api-explorer/pom.xml b/amps/ags/rm-community/rm-community-rest-api-explorer/pom.xml index ea537b28dd..59f55ce44f 100644 --- a/amps/ags/rm-community/rm-community-rest-api-explorer/pom.xml +++ b/amps/ags/rm-community/rm-community-rest-api-explorer/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-governance-services-community-repo-parent - 23.3.0.58-SNAPSHOT + 23.3.0.61-SNAPSHOT diff --git a/amps/ags/rm-community/rm-community-rest-api-explorer/src/main/webapp/definitions/gs-core-api.yaml b/amps/ags/rm-community/rm-community-rest-api-explorer/src/main/webapp/definitions/gs-core-api.yaml index 41c728fc7e..6ab10c5079 100644 --- a/amps/ags/rm-community/rm-community-rest-api-explorer/src/main/webapp/definitions/gs-core-api.yaml +++ b/amps/ags/rm-community/rm-community-rest-api-explorer/src/main/webapp/definitions/gs-core-api.yaml @@ -40,6 +40,8 @@ tags: description: Retrieve and manage unfiled record folders - name: holds description: Retrieve and manage holds + - name: retention-schedules + description: Perform retention schedule specific operations paths: ## GS sites @@ -2634,7 +2636,194 @@ paths: description: Unexpected error schema: $ref: '#/definitions/Error' + ##retention-schedule + '/record-categories/{recordCategoryId}/retention-schedules': + post: + tags: + - retention-schedules + summary: Create a retention schedule + description: | + Create a retention schedule. + + For example, using the following JSON body will create a retention schedule: + ```JSON + { + "authority": "Retention Authority", + "instructions": "Retention Instructions", + "isRecordLevel": false + } + ``` + operationId: createRetentionSchedule + parameters: + - $ref: '#/parameters/recordCategoryIdParam' + - in: body + name: retentionNodeBodyCreate + description: | + The retention schedule information to create. + schema: + $ref: '#/definitions/RetentionNodeBodyCreate' + consumes: + - application/json + produces: + - application/json + responses: + '201': + description: Successful response + schema: + $ref: '#/definitions/RetentionScheduleResponse' + '400': + description: | + Invalid parameter: value of recordCategoryId is invalid + '401': + description: Authentication failed + '403': + description: Current user does not have permission to create retention schedule + '404': + description: recordCategoryId does not exist + '409': + description: Retention schedule already exist for the given recordCategoryId + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + get: + tags: + - retention-schedules + summary: Get the retention schedule for a record category + description: | + Get the retention schedule for a record category. + + You can use the **include** parameter (include=actions) to return additional information. + + operationId: getRetentionScheduleList + parameters: + - $ref: '#/parameters/recordCategoryIdParam' + - $ref: '#/parameters/retentionScheduleIncludeParam' + - $ref: '#/parameters/skipCountParam' + - $ref: '#/parameters/maxItemsParam' + consumes: + - application/json + produces: + - application/json + responses: + '200': + description: Successful response + schema: + $ref: '#/definitions/RetentionScheduleResponseList' + '400': + description: | + Invalid parameter: value of recordCategoryId is invalid + '401': + description: Authentication failed + '403': + description: Current user does not have permission to get retention schedule + '404': + description: recordCategoryId does not exist + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + '/retention-schedules/{retentionScheduleId}/retention-steps': + post: + tags: + - retention-schedules + summary: Create a step in the retention schedule + description: | + Create a step in the retention schedule. + + Order of steps: + * "**retain**" or "**cutoff**" should be first + * can't use "**cutoff**" after "**transfer**" or "**accession**" + * only the "**transfer**" action is allowed multiple times + * no steps are allowed after "**destroy**" + + For example, the following JSON body will create a step in the retention schedule: + ```JSON + { + "name":"accession", + "periodAmount": 2, + "period":"month", + "periodProperty":"cm:created", + "combineDispositionStepConditions": false, + "events":["versioned"], + "eligibleOnFirstCompleteEvent": true, + "description":"Step Description" + } + ``` + operationId: createRetentionScheduleAction + parameters: + - $ref: '#/parameters/retentionScheduleIdParam' + - in: body + name: nodeBodyCreate + description: | + The retention schedule steps information to create. + required: true + schema: + $ref: '#/definitions/RetentionStepNodeBodyCreate' + consumes: + - application/json + produces: + - application/json + responses: + '201': + description: Successful response + schema: + $ref: '#/definitions/RetentionStepNodeBodyResponse' + '400': + description: | + Invalid parameter: value of retentionScheduleId is invalid + Invalid parameter (e.g. event, period, periodProperty) + '401': + description: Authentication failed + '403': + description: Current user does not have permission to create retention schedule step + '404': + description: retentionScheduleId does not exist + '409': + description: | + * Invalid Step - Can't use Cut Off after Transfer or Accession + * Invalid Step - Destroy action already completed. Can't do any other Action + * Invalid Step - This step already exists. You can’t create this step [Transfer action is allowed many times] + '422': + description: Cut Off or Retain should be the first step + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + get: + tags: + - retention-schedules + summary: Get the list of steps in the retention schedule + description: | + Get the list of steps in the retention schedule. + operationId: getRetentionScheduleActionList + parameters: + - $ref: '#/parameters/retentionScheduleIdParam' + - $ref: '#/parameters/skipCountParam' + - $ref: '#/parameters/maxItemsParam' + consumes: + - application/json + produces: + - application/json + responses: + '200': + description: Successful response + schema: + $ref: '#/definitions/RetentionStepsNodeBodyResponse' + '400': + description: | + Invalid parameter: value of retentionScheduleId is invalid + '401': + description: Authentication failed + '403': + description: Current user does not have permission to get retention schedule steps + '404': + description: retentionScheduleId does not exist + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' parameters: ## File plans filePlanEntryIncludeParam: @@ -3100,6 +3289,22 @@ parameters: If true, then a name clash will cause an attempt to auto rename by finding a unique name using an integer suffix. required: false type: boolean + ## RetentionSchedule + retentionScheduleIdParam: + name: retentionScheduleId + in: path + description: + The identifier of a retention schedule. + required: true + type: string + retentionScheduleIncludeParam: + name: include + in: query + description: | + Returns additional information about the retention schedule actions. Any optional field from the response model can be requested. For example: + * actions + required: false + type: string definitions: FilePlanComponentBodyUpdate: type: object @@ -4258,6 +4463,225 @@ definitions: type: array items: $ref: '#/definitions/HoldBulkStatusEntry' + RetentionNodeBodyCreate: + type: object + properties: + authority: + type: string + description: | + Authority name for the retention schedule. + instructions: + type: string + description: | + Required instructions for the retention schedule. + isRecordLevel: + type: boolean + default: false + description: | + This field is used to specify whether the retention schedule needs to be applied in the folder level or record level. + True will cause the the retention schedule to apply to records and false will cause the retention schedule to apply to record folders. + This cannot be changed once items start being managed by the retention schedule. + RetentionScheduleResponse: + type: object + properties: + id: + type: string + parentId: + type: string + authority: + type: string + instructions: + type: string + isRecordLevel: + type: boolean + unpublishedUpdates: + type: boolean + RetentionScheduleResponseList: + type: object + properties: + list: + type: object + properties: + pagination: + $ref: '#/definitions/Pagination' + entries: + type: array + items: + $ref: '#/definitions/FullRetentionScheduleResponse' + FullRetentionScheduleResponse: + type: object + properties: + id: + type: string + parentId: + type: string + authority: + type: string + instructions: + type: string + isRecordLevel: + type: boolean + unpublishedUpdates: + type: boolean + actions: + type: array + items: + $ref: '#/definitions/Actions' + Actions: + type: object + properties: + id: + type: string + name: + type: string + periodAmount: + type: integer + period: + type: string + periodProperty: + type: string + combineDispositionStepConditions: + type: boolean + default: false + eligibleOnFirstCompleteEvent: + type: boolean + default: true + description: + type: string + retainRecordMetadataAfterDestruction: + type: boolean + location: + type: string + events: + type: array + items: + type: string + index: + type: integer + RetentionStepNodeBodyCreate: + type: object + properties: + name: + type: string + description: | + The valid names are: + * retain + * cutoff + * accession + * transfer + * destroy + periodAmount: + type: integer + description: | + This property is only applicable for the following period values. + * day + * month + * quarter + * week + * duration + * year + period: + type: string + description: | + Valid values for the period. + * day = Day + * fmend = End Of Financial Month + * fqend = End Of Financial Quarter + * fyend = End Of Financial Year + * immediately = Immediately + * monthend = End Of Month + * quarterend = End Of Quarter + * yearend = End Of Year + * month = Month + * none = None + * quarter = Quarter + * week = Week + * duration = XML Duration + * year = Year + + If you provide XML Duration for the period value, you need to specify a time interval using XML syntax. + The syntax should take the form of: + P = Period (required) + nY = Number of years + nM = Number of months + nD = Number of days + T = Start time of a time section (required if specifying hours, minutes, or seconds) + nH = Number of hours + nM = Number of minutes + nS = Number of seconds + For example, ‘P2M10D’ represents two months and ten days. + periodProperty: + type: string + default: cm:created + description: | + Valid values for the periodProperty property + * cm:created = Created Date (defult value) + * rma:cutOffDate = Cut Off Date + * rma:dispositionAsOf = Retention Action + combineDispositionStepConditions: + type: boolean + description: | + This property is only valid for **accession** step. + This is used to specify whether to combine the period condition and events for the step execution or only consider one of them. + For example: + **periodCondition**: After a period of 2 months + **eventsCondition**: Case Closed event + This flag can be used to consider only (**periodCondition** or **eventsCondition**) or both of them at once. + events: + type: array + items: + type: string + description: | + Valid values for the events property + * case_closed = Case Closed + * abolished = Abolished + * re_designated = Redesignated + * no_longer_needed = No Longer Needed + * superseded = Superseded + * versioned = Versioned + * study_complete = Study Complete + * training_complete = Training Complete + * related_record_trasfered_inactive_storage = Related Record Transferred to Inactive Storage + * obsolete = Obsolete + * all_allowances_granted_are_terminated = All Allowances Granted are Terminated + * WGI_action_complete = WGI Action Complete + * separation = Separation + * case_complete = Case Complete + * declassification_review = Declassification Review + eligibleOnFirstCompleteEvent: + type: boolean + description: | + * false = When all events have happened + * true = Whichever event is earlier + description: + type: string + description: | + This property is used to provide the step description. + retainRecordMetadataAfterDestruction: + type: boolean + description: | + This property is used to retain the metadata after record destruction. + location: + type: string + description: | + This property is only valid for transfer step + RetentionStepNodeBodyResponse: + type: object + properties: + actions: + $ref: '#/definitions/Actions' + RetentionStepsNodeBodyResponse: + type: object + properties: + list: + type: object + properties: + pagination: + $ref: '#/definitions/Pagination' + entries: + type: array + items: + $ref: '#/definitions/RetentionStepNodeBodyResponse' ## RequestBodyFile: type: object diff --git a/amps/pom.xml b/amps/pom.xml index 03cb9a4617..cde64b28e3 100644 --- a/amps/pom.xml +++ b/amps/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-community-repo - 23.3.0.58-SNAPSHOT + 23.3.0.61-SNAPSHOT diff --git a/amps/share-services/pom.xml b/amps/share-services/pom.xml index ccd28bf2db..31e39694f5 100644 --- a/amps/share-services/pom.xml +++ b/amps/share-services/pom.xml @@ -8,7 +8,7 @@ org.alfresco alfresco-community-repo-amps - 23.3.0.58-SNAPSHOT + 23.3.0.61-SNAPSHOT diff --git a/core/pom.xml b/core/pom.xml index c7d0428605..22c55b58ce 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-community-repo - 23.3.0.58-SNAPSHOT + 23.3.0.61-SNAPSHOT diff --git a/data-model/pom.xml b/data-model/pom.xml index b5e884dc48..b703d5918f 100644 --- a/data-model/pom.xml +++ b/data-model/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-community-repo - 23.3.0.58-SNAPSHOT + 23.3.0.61-SNAPSHOT diff --git a/mmt/pom.xml b/mmt/pom.xml index 203a25b4d3..f940ea4664 100644 --- a/mmt/pom.xml +++ b/mmt/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-community-repo - 23.3.0.58-SNAPSHOT + 23.3.0.61-SNAPSHOT diff --git a/packaging/distribution/pom.xml b/packaging/distribution/pom.xml index c815b26189..211c0d904e 100644 --- a/packaging/distribution/pom.xml +++ b/packaging/distribution/pom.xml @@ -9,6 +9,6 @@ org.alfresco alfresco-community-repo-packaging - 23.3.0.58-SNAPSHOT + 23.3.0.61-SNAPSHOT diff --git a/packaging/docker-alfresco/pom.xml b/packaging/docker-alfresco/pom.xml index 3b868bce08..3affd65da1 100644 --- a/packaging/docker-alfresco/pom.xml +++ b/packaging/docker-alfresco/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-community-repo-packaging - 23.3.0.58-SNAPSHOT + 23.3.0.61-SNAPSHOT diff --git a/packaging/pom.xml b/packaging/pom.xml index b5de6e9f0d..6b6b298451 100644 --- a/packaging/pom.xml +++ b/packaging/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-community-repo - 23.3.0.58-SNAPSHOT + 23.3.0.61-SNAPSHOT diff --git a/packaging/tests/pom.xml b/packaging/tests/pom.xml index dadcc89c80..5d585a9dd8 100644 --- a/packaging/tests/pom.xml +++ b/packaging/tests/pom.xml @@ -6,7 +6,7 @@ org.alfresco alfresco-community-repo-packaging - 23.3.0.58-SNAPSHOT + 23.3.0.61-SNAPSHOT diff --git a/packaging/tests/tas-cmis/pom.xml b/packaging/tests/tas-cmis/pom.xml index 0a63121a52..3f8800ea26 100644 --- a/packaging/tests/tas-cmis/pom.xml +++ b/packaging/tests/tas-cmis/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-community-repo-tests - 23.3.0.58-SNAPSHOT + 23.3.0.61-SNAPSHOT diff --git a/packaging/tests/tas-email/pom.xml b/packaging/tests/tas-email/pom.xml index 995fa9abcc..1080873d7c 100644 --- a/packaging/tests/tas-email/pom.xml +++ b/packaging/tests/tas-email/pom.xml @@ -9,7 +9,7 @@ org.alfresco alfresco-community-repo-tests - 23.3.0.58-SNAPSHOT + 23.3.0.61-SNAPSHOT diff --git a/packaging/tests/tas-integration/pom.xml b/packaging/tests/tas-integration/pom.xml index 58a8f0d567..e6b323086b 100644 --- a/packaging/tests/tas-integration/pom.xml +++ b/packaging/tests/tas-integration/pom.xml @@ -9,7 +9,7 @@ org.alfresco alfresco-community-repo-tests - 23.3.0.58-SNAPSHOT + 23.3.0.61-SNAPSHOT diff --git a/packaging/tests/tas-restapi/pom.xml b/packaging/tests/tas-restapi/pom.xml index f351f59f64..5a72760007 100644 --- a/packaging/tests/tas-restapi/pom.xml +++ b/packaging/tests/tas-restapi/pom.xml @@ -8,7 +8,7 @@ org.alfresco alfresco-community-repo-tests - 23.3.0.58-SNAPSHOT + 23.3.0.61-SNAPSHOT diff --git a/packaging/tests/tas-webdav/pom.xml b/packaging/tests/tas-webdav/pom.xml index 5192b8fa43..6bebe1a61f 100644 --- a/packaging/tests/tas-webdav/pom.xml +++ b/packaging/tests/tas-webdav/pom.xml @@ -9,7 +9,7 @@ org.alfresco alfresco-community-repo-tests - 23.3.0.58-SNAPSHOT + 23.3.0.61-SNAPSHOT diff --git a/packaging/war/pom.xml b/packaging/war/pom.xml index d990f883bd..44817efdce 100644 --- a/packaging/war/pom.xml +++ b/packaging/war/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-community-repo-packaging - 23.3.0.58-SNAPSHOT + 23.3.0.61-SNAPSHOT diff --git a/pom.xml b/pom.xml index 8b961fe41d..84646c808e 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 alfresco-community-repo - 23.3.0.58-SNAPSHOT + 23.3.0.61-SNAPSHOT pom Alfresco Community Repo Parent diff --git a/remote-api/pom.xml b/remote-api/pom.xml index 0150864b82..842546833b 100644 --- a/remote-api/pom.xml +++ b/remote-api/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-community-repo - 23.3.0.58-SNAPSHOT + 23.3.0.61-SNAPSHOT diff --git a/repository/pom.xml b/repository/pom.xml index 7a0ac1881c..24b4bf1ab2 100644 --- a/repository/pom.xml +++ b/repository/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-community-repo - 23.3.0.58-SNAPSHOT + 23.3.0.61-SNAPSHOT diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceConfig.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceConfig.java index c8b4b4be1d..6dbc91e2d0 100644 --- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceConfig.java +++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceConfig.java @@ -4,30 +4,35 @@ * %% * Copyright (C) 2005 - 2024 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 + * 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.repo.security.authentication.identityservice; +import java.util.Objects; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.lang3.StringUtils; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.web.util.UriComponentsBuilder; /** @@ -35,6 +40,7 @@ import org.springframework.web.util.UriComponentsBuilder; * * @author Gavin Cornwell */ +@SuppressWarnings("PMD.ExcessivePublicCount") public class IdentityServiceConfig { private static final String REALMS = "realms"; @@ -62,6 +68,7 @@ public class IdentityServiceConfig private String principalAttribute; private boolean clientIdValidationDisabled; private String adminConsoleRedirectPath; + private String signatureAlgorithms; /** * @@ -306,4 +313,18 @@ public class IdentityServiceConfig { this.adminConsoleRedirectPath = adminConsoleRedirectPath; } + + public Set getSignatureAlgorithms() + { + return Stream.of(signatureAlgorithms.split(",")) + .map(String::trim) + .map(SignatureAlgorithm::from) + .filter(Objects::nonNull) + .collect(Collectors.toUnmodifiableSet()); + } + + public void setSignatureAlgorithms(String signatureAlgorithms) + { + this.signatureAlgorithms = signatureAlgorithms; + } } diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBean.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBean.java index 54ef842e99..80dd2b9c91 100644 --- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBean.java +++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBean.java @@ -58,10 +58,13 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; +import com.nimbusds.jose.JOSEObjectType; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.jwk.source.DefaultJWKSetCache; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.jwk.source.RemoteJWKSet; +import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier; import com.nimbusds.jose.proc.JWSVerificationKeySelector; import com.nimbusds.jose.proc.SecurityContext; import com.nimbusds.jose.util.ResourceRetriever; @@ -129,6 +132,9 @@ import org.springframework.web.util.UriComponentsBuilder; public class IdentityServiceFacadeFactoryBean implements FactoryBean { private static final Log LOGGER = LogFactory.getLog(IdentityServiceFacadeFactoryBean.class); + + private static final JOSEObjectType AT_JWT = new JOSEObjectType("at+jwt"); + private boolean enabled; private SpringBasedIdentityServiceFacadeFactory factory; @@ -554,12 +560,20 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean signatureAlgorithms; JwtDecoderProvider(IdentityServiceConfig config) { this.config = requireNonNull(config); + this.signatureAlgorithms = ofNullable(config.getSignatureAlgorithms()) + .filter(not(Set::isEmpty)) + .orElseGet(() -> { + LOGGER.warn("Unable to find any valid signature algorithms in the configuration. " + + "Using the default signature algorithm: " + DEFAULT_SIGNATURE_ALGORITHM.getName() + "."); + return Set.of(DEFAULT_SIGNATURE_ALGORITHM); + }); } public JwtDecoder createJwtDecoder(RestOperations rest, ProviderDetails providerDetails) @@ -587,13 +601,13 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean( - JWSAlgorithm.parse(SIGNATURE_ALGORITHM.getName()), + signatureAlgorithms.stream() + .map(signatureAlgorithm -> JWSAlgorithm.parse(signatureAlgorithm.getName())) + .collect(Collectors.toSet()), cachingJWKSource)); + jwtProcessor.setJWSTypeVerifier(new CustomJOSEObjectTypeVerifier(JOSEObjectType.JWT, AT_JWT)); } private OAuth2TokenValidator createJwtTokenValidator(ProviderDetails providerDetails) @@ -759,7 +776,6 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean + { + public CustomJOSEObjectTypeVerifier(JOSEObjectType... allowedTypes) + { + super(Set.of(allowedTypes)); + } + + @Override + public void verify(JOSEObjectType type, SecurityContext context) throws BadJOSEException + { + super.verify(type, context); + } + } + private static boolean isDefined(String value) { return value != null && !value.isBlank(); } - } diff --git a/repository/src/main/resources/alfresco/bootstrap/customMessagesSpace.xml b/repository/src/main/resources/alfresco/bootstrap/customMessagesSpace.xml index c5099f4c11..7194d46ca8 100644 --- a/repository/src/main/resources/alfresco/bootstrap/customMessagesSpace.xml +++ b/repository/src/main/resources/alfresco/bootstrap/customMessagesSpace.xml @@ -4,6 +4,8 @@ view:childName="app:messages"> + + ${spaces.messages.description} diff --git a/repository/src/main/resources/alfresco/bootstrap/customModelsSpace.acp b/repository/src/main/resources/alfresco/bootstrap/customModelsSpace.acp index f57119b8b3..969d192357 100644 Binary files a/repository/src/main/resources/alfresco/bootstrap/customModelsSpace.acp and b/repository/src/main/resources/alfresco/bootstrap/customModelsSpace.acp differ diff --git a/repository/src/main/resources/alfresco/bootstrap/customWebClientExtensionSpace.xml b/repository/src/main/resources/alfresco/bootstrap/customWebClientExtensionSpace.xml index 18df478072..aeb23a48eb 100644 --- a/repository/src/main/resources/alfresco/bootstrap/customWebClientExtensionSpace.xml +++ b/repository/src/main/resources/alfresco/bootstrap/customWebClientExtensionSpace.xml @@ -4,6 +4,8 @@ view:childName="app:webclient_extension"> + + ${spaces.web.client.extension.description} diff --git a/repository/src/main/resources/alfresco/bootstrap/customWorkflowDefsSpace.acp b/repository/src/main/resources/alfresco/bootstrap/customWorkflowDefsSpace.acp index af60348470..9d85df1bdd 100644 Binary files a/repository/src/main/resources/alfresco/bootstrap/customWorkflowDefsSpace.acp and b/repository/src/main/resources/alfresco/bootstrap/customWorkflowDefsSpace.acp differ diff --git a/repository/src/main/resources/alfresco/bootstrap/imapSpaces.acp b/repository/src/main/resources/alfresco/bootstrap/imapSpaces.acp index 5be874baa4..f0f98ac134 100644 Binary files a/repository/src/main/resources/alfresco/bootstrap/imapSpaces.acp and b/repository/src/main/resources/alfresco/bootstrap/imapSpaces.acp differ diff --git a/repository/src/main/resources/alfresco/bootstrap/renderingActionSpace.xml b/repository/src/main/resources/alfresco/bootstrap/renderingActionSpace.xml index d2b6fe9563..fe25e02702 100644 --- a/repository/src/main/resources/alfresco/bootstrap/renderingActionSpace.xml +++ b/repository/src/main/resources/alfresco/bootstrap/renderingActionSpace.xml @@ -12,6 +12,10 @@ ${spaces.rendition.rendering_actions.name} ${spaces.rendition.rendering_actions.description} + + + + diff --git a/repository/src/main/resources/alfresco/bootstrap/replicationActionSpace.xml b/repository/src/main/resources/alfresco/bootstrap/replicationActionSpace.xml index 25c2a0dfef..c37eb279a5 100644 --- a/repository/src/main/resources/alfresco/bootstrap/replicationActionSpace.xml +++ b/repository/src/main/resources/alfresco/bootstrap/replicationActionSpace.xml @@ -12,6 +12,10 @@ ${spaces.replication.replication_actions.name} ${spaces.replication.replication_actions.description} + + + + diff --git a/repository/src/main/resources/alfresco/bootstrap/scheduledActionsFolder.xml b/repository/src/main/resources/alfresco/bootstrap/scheduledActionsFolder.xml index 38c9ecfdca..00133289b9 100644 --- a/repository/src/main/resources/alfresco/bootstrap/scheduledActionsFolder.xml +++ b/repository/src/main/resources/alfresco/bootstrap/scheduledActionsFolder.xml @@ -11,5 +11,9 @@ ${spaces.actions.scheduled_actions.name} ${spaces.actions.scheduled_actions.description} + + + + diff --git a/repository/src/main/resources/alfresco/bootstrap/solrFacetsRootFolder.xml b/repository/src/main/resources/alfresco/bootstrap/solrFacetsRootFolder.xml index 4b80d008e2..206befe90b 100644 --- a/repository/src/main/resources/alfresco/bootstrap/solrFacetsRootFolder.xml +++ b/repository/src/main/resources/alfresco/bootstrap/solrFacetsRootFolder.xml @@ -14,5 +14,9 @@ ${spaces.solr_facets.root.description} + + + + diff --git a/repository/src/main/resources/alfresco/bootstrap/spaces.xml b/repository/src/main/resources/alfresco/bootstrap/spaces.xml index c1faaec15f..744ab0ad4b 100644 --- a/repository/src/main/resources/alfresco/bootstrap/spaces.xml +++ b/repository/src/main/resources/alfresco/bootstrap/spaces.xml @@ -1,7 +1,7 @@ + xmlns:emailserver="http://www.alfresco.org/model/emailserver/1.0" xmlns:sys="http://www.alfresco.org/model/system/1.0"> @@ -30,6 +30,10 @@ space-icon-default ${spaces.dictionary.name} ${spaces.dictionary.description} + + + + @@ -37,6 +41,10 @@ space-icon-default ${spaces.templates.name} ${spaces.templates.description} + + + + @@ -45,6 +53,10 @@ space-icon-default ${spaces.templates.content.name} ${spaces.templates.content.description} + + + + @@ -53,6 +65,10 @@ space-icon-default ${spaces.templates.email.name} ${spaces.templates.email.description} + + + + @@ -79,6 +95,10 @@ space-icon-default ${spaces.templates.rss.name} ${spaces.templates.rss.description} + + + + @@ -93,6 +113,10 @@ space-icon-default ${spaces.savedsearches.name} ${spaces.savedsearches.description} + + + + @@ -100,6 +124,10 @@ space-icon-default ${spaces.scripts.name} ${spaces.scripts.description} + + + + @@ -107,6 +135,10 @@ space-icon-default ${spaces.nodeTemplatesSpace.name} ${spaces.nodeTemplatesSpace.description} + + + + @@ -120,6 +152,10 @@ space-icon-default ${spaces.smartfoldertemplates.name} ${spaces.smartfoldertemplates.description} + + + + @@ -133,6 +169,10 @@ space-icon-default ${spaces.smartdownloads.name} ${spaces.smartdownloads.description} + + + + diff --git a/repository/src/main/resources/alfresco/bootstrap/transferSpaces.xml b/repository/src/main/resources/alfresco/bootstrap/transferSpaces.xml index 07cd98dc6c..7a9c6dc200 100644 --- a/repository/src/main/resources/alfresco/bootstrap/transferSpaces.xml +++ b/repository/src/main/resources/alfresco/bootstrap/transferSpaces.xml @@ -4,10 +4,13 @@ xmlns:view="http://www.alfresco.org/view/repository/1.0" xmlns:cm="http://www.alfresco.org/model/content/1.0" xmlns:app="http://www.alfresco.org/model/application/1.0" - xmlns:trx="http://www.alfresco.org/model/transfer/1.0"> + xmlns:trx="http://www.alfresco.org/model/transfer/1.0" + xmlns:sys="http://www.alfresco.org/model/system/1.0"> + + ${spaces.transfers.description} diff --git a/repository/src/main/resources/alfresco/bootstrap/webScripts.xml b/repository/src/main/resources/alfresco/bootstrap/webScripts.xml index 064480500f..201598caac 100644 --- a/repository/src/main/resources/alfresco/bootstrap/webScripts.xml +++ b/repository/src/main/resources/alfresco/bootstrap/webScripts.xml @@ -3,6 +3,8 @@ + + ${webscripts.url_addressable_web_services} diff --git a/repository/src/main/resources/alfresco/bootstrap/webScriptsExtensions.xml b/repository/src/main/resources/alfresco/bootstrap/webScriptsExtensions.xml index 7e9884ff62..1586d3f24e 100644 --- a/repository/src/main/resources/alfresco/bootstrap/webScriptsExtensions.xml +++ b/repository/src/main/resources/alfresco/bootstrap/webScriptsExtensions.xml @@ -3,6 +3,8 @@ + + ${webscriptsextentions.customized_web_scripts} diff --git a/repository/src/main/resources/alfresco/model-specific-services-context.xml b/repository/src/main/resources/alfresco/model-specific-services-context.xml index 7bd52210b4..069357f4fa 100644 --- a/repository/src/main/resources/alfresco/model-specific-services-context.xml +++ b/repository/src/main/resources/alfresco/model-specific-services-context.xml @@ -68,14 +68,6 @@ /${spaces.company_home.childname} - /${spaces.company_home.childname}/${spaces.dictionary.childname} - /${spaces.company_home.childname}/${spaces.dictionary.childname}/${spaces.templates.childname} - /${spaces.company_home.childname}/${spaces.dictionary.childname}/${spaces.templates.content.childname} - /${spaces.company_home.childname}/${spaces.dictionary.childname}/${spaces.templates.email.childname} - /${spaces.company_home.childname}/${spaces.dictionary.childname}/${spaces.templates.rss.childname} - /${spaces.company_home.childname}/${spaces.dictionary.childname}/${spaces.scripts.childname} - /${spaces.company_home.childname}/${spaces.dictionary.childname}/${spaces.webscripts.childname} - /${spaces.company_home.childname}/${spaces.dictionary.childname}/${spaces.extension_webscripts.childname} diff --git a/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml b/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml index 2e7a2da573..748baf8cec 100644 --- a/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml +++ b/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml @@ -155,6 +155,9 @@ ${identity-service.admin-console.redirect-path} + + ${identity-service.signature-algorithms:RS256,PS256} + diff --git a/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication.properties b/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication.properties index 4a26aa1d94..7357b01644 100644 --- a/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication.properties +++ b/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication.properties @@ -11,4 +11,5 @@ identity-service.realm=alfresco identity-service.resource=alfresco identity-service.credentials.secret= identity-service.public-client=true -identity-service.admin-console.redirect-path=/alfresco/s/admin/admin-communitysummary \ No newline at end of file +identity-service.admin-console.redirect-path=/alfresco/s/admin/admin-communitysummary +identity-service.signature-algorithms=RS256,PS256 \ No newline at end of file diff --git a/repository/src/test/java/org/alfresco/AppContext01TestSuite.java b/repository/src/test/java/org/alfresco/AppContext01TestSuite.java index c7f78a960c..147626480a 100644 --- a/repository/src/test/java/org/alfresco/AppContext01TestSuite.java +++ b/repository/src/test/java/org/alfresco/AppContext01TestSuite.java @@ -77,8 +77,7 @@ import org.junit.runners.Suite; org.alfresco.repo.activities.feed.cleanup.FeedCleanerTestCaseSensitivity.class, org.alfresco.repo.activities.SiteActivityTestCaseInsensitivity.class, org.alfresco.repo.admin.registry.RegistryServiceImplTest.class, - org.alfresco.repo.bootstrap.DataDictionaryFolderTest.class, - org.alfresco.repo.action.executer.NodeSizeActionExecuterTest.class + org.alfresco.repo.bootstrap.DataDictionaryFolderTest.class }) public class AppContext01TestSuite { diff --git a/repository/src/test/java/org/alfresco/repo/bootstrap/DataDictionaryFolderTest.java b/repository/src/test/java/org/alfresco/repo/bootstrap/DataDictionaryFolderTest.java new file mode 100644 index 0000000000..009fca7639 --- /dev/null +++ b/repository/src/test/java/org/alfresco/repo/bootstrap/DataDictionaryFolderTest.java @@ -0,0 +1,124 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2024 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.repo.bootstrap; + + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.ServiceRegistry; +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.namespace.QName; +import org.alfresco.util.BaseSpringTest; +import org.alfresco.util.test.junitrules.ApplicationContextInit; +import org.alfresco.util.test.junitrules.WellKnownNodes; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; + +import java.util.List; + +public class DataDictionaryFolderTest extends BaseSpringTest +{ + @ClassRule + private static final ApplicationContextInit APP_CONTEXT_INIT = new ApplicationContextInit(); + + private static final String DATA_DICTIONARY = "Data Dictionary"; + + @Rule + private WellKnownNodes wellKnownNodes = new WellKnownNodes(APP_CONTEXT_INIT); + + private NodeService nodeService; + + @Before + public void before() + { + ServiceRegistry serviceRegistry = (ServiceRegistry) this.applicationContext.getBean("ServiceRegistry"); + this.nodeService = serviceRegistry.getNodeService(); + } + + @Test + public void testDataDictionaryFolderIsUndeletable() + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + // get the company_home + NodeRef companyHomeRef = wellKnownNodes.getCompanyHome(); + // get the Data Dictionary + NodeRef dataDictionaryRef = nodeService.getChildByName(companyHomeRef, ContentModel.ASSOC_CONTAINS, DATA_DICTIONARY); + assertTrue(nodeService.hasAspect(dataDictionaryRef, ContentModel.ASPECT_UNDELETABLE)); + + List chilAssocsList = nodeService.getChildAssocs(dataDictionaryRef); + + chilAssocsList.stream() + .map(ChildAssociationRef::getChildRef) + .forEach(childNodeRef -> { + assertTrue(nodeService.hasAspect(childNodeRef, ContentModel.ASPECT_UNDELETABLE)); + try + { + nodeService.deleteNode(childNodeRef); + } + catch (Exception ex) + { + assertTrue(ex.getMessage().contains("deletion is not allowed")); + } + }); + } + + @Test + public void testDataDictionaryFolderIsUnmovable() + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + // get the company_home + NodeRef companyHomeRef = wellKnownNodes.getCompanyHome(); + // get the Data Dictionary + NodeRef dataDictionaryRef = nodeService.getChildByName(companyHomeRef, ContentModel.ASSOC_CONTAINS, DATA_DICTIONARY); + assertTrue(nodeService.hasAspect(dataDictionaryRef, ContentModel.ASPECT_UNMOVABLE)); + + List chilAssocsList = nodeService.getChildAssocs(dataDictionaryRef); + + chilAssocsList.stream() + .map(ChildAssociationRef::getChildRef) + .forEach(childNodeRef -> { + assertTrue(nodeService.hasAspect(childNodeRef, ContentModel.ASPECT_UNMOVABLE)); + NodeRef folderRef = nodeService.createNode( + companyHomeRef, + ContentModel.ASSOC_CONTAINS, + QName.createQName("testDeleteAndRestore-folder2-" + System.currentTimeMillis()), + ContentModel.TYPE_FOLDER + ).getChildRef(); + try + { + nodeService.moveNode(childNodeRef, folderRef, ContentModel.ASSOC_CONTAINS, ContentModel.ASSOC_CONTAINS); + } + catch (Exception ex) + { + assertTrue(ex.getMessage().contains("move is not allowed")); + } + }); + } +} \ No newline at end of file diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBeanTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBeanTest.java index 646e887119..ef0bf2201e 100644 --- a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBeanTest.java +++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBeanTest.java @@ -25,53 +25,156 @@ */ package org.alfresco.repo.security.authentication.identityservice; +import static com.nimbusds.jose.HeaderParameterNames.KEY_ID; + import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; +import com.nimbusds.jose.Algorithm; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JOSEObjectType; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.gen.RSAKeyGenerator; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; import com.nimbusds.openid.connect.sdk.claims.PersonClaims; import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.JwtAudienceValidator; import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.JwtDecoderProvider; import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.JwtIssuerValidator; import org.junit.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.jwt.BadJwtException; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.web.client.RestOperations; public class IdentityServiceFacadeFactoryBeanTest { private static final String EXPECTED_ISSUER = "expected-issuer"; private static final String EXPECTED_AUDIENCE = "expected-audience"; + public final IdentityServiceConfig config = mock(IdentityServiceConfig.class); + public final RestOperations restOperations = mock(RestOperations.class); + public final ResponseEntity responseEntity = mock(ResponseEntity.class); + public final ProviderDetails providerDetails = mock(ProviderDetails.class); @Test public void shouldCreateJwtDecoderWithoutIDSWhenPublicKeyIsProvided() { - final IdentityServiceConfig config = mock(IdentityServiceConfig.class); - when(config.getRealmKey()).thenReturn("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAve3MabX/rp3LbE7/zNqKxuid8WT7y4qSXsNaiPvl/OVbNWW/cu5td1VndItYhH6/gL7Z5W/r4MOeTlz/fOdXfjrRJou2f3UiPQwLV9RdOH3oS4/BUe+sviD8Q3eRfWBWWz3yw8f2YNtD4bMztIMMjqthvwdEEb9S9jbxxD0o71Bsrz/FwPi7HhSDA+Z/p01Hct8m4wx13ZlKRd4YjyC12FBmi9MSgsrFuWzyQHhHTeBDoALpfuiut3rhVxUtFmVTpy6p9vil7C5J5pok4MXPH0dJCyDNQz05ww5+fD+tfksIEpFeokRpN226F+P21oQVFUWwYIaXaFlG/hfvwmnlfQIDAQAB"); + when(config.getRealmKey()).thenReturn( + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAve3MabX/rp3LbE7/zNqKxuid8WT7y4qSXsNaiPvl/OVbNWW/cu5td1VndItYhH6/gL7Z5W/r4MOeTlz/fOdXfjrRJou2f3UiPQwLV9RdOH3oS4/BUe+sviD8Q3eRfWBWWz3yw8f2YNtD4bMztIMMjqthvwdEEb9S9jbxxD0o71Bsrz/FwPi7HhSDA+Z/p01Hct8m4wx13ZlKRd4YjyC12FBmi9MSgsrFuWzyQHhHTeBDoALpfuiut3rhVxUtFmVTpy6p9vil7C5J5pok4MXPH0dJCyDNQz05ww5+fD+tfksIEpFeokRpN226F+P21oQVFUWwYIaXaFlG/hfvwmnlfQIDAQAB"); when(config.isClientIdValidationDisabled()).thenReturn(true); - - final ProviderDetails providerDetails = mock(ProviderDetails.class); when(providerDetails.getIssuerUri()).thenReturn("https://my.issuer"); final JwtDecoderProvider provider = new JwtDecoderProvider(config); final JwtDecoder decoder = provider.createJwtDecoder(null, providerDetails); - final Jwt decodedToken = decoder.decode("eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjIxNDc0ODM2NDcsImp0aSI6IjEyMzQiLCJpc3MiOiJodHRwczovL215Lmlzc3VlciIsInN1YiI6ImFiYzEyMyIsInR5cCI6IkJlYXJlciIsInByZWZlcnJlZF91c2VybmFtZSI6InBpb3RyZWsifQ.k_KaOrLLh3QsT8mKphkcz2vKpulgxp92UoEDccpHJ1mxE3Pa3gFXPKTj4goUBKXieGPZRMvBDhfWNxMvRYZPiQr2NXJKapkh0bTd0qoaSWz9ICe9Nu3eg7_VA_nwUVPz_35wwmrxgVk0_kpUYQN_VtaO7ZgFE2sJzFjbkVls5aqfAMnEjEgQl837hqZvmlW2ZRWebtxXfQxAjtp0gcTg-xtAHKIINYo_1_uAtt_H9L8KqFaioxrVAEDDIlcKnb-Ks3Y62CrZauaGUJeN_aNj2gdOpdkhvCw79yJyZSGZ7okjGbidCNSAf7Bo2Y6h3dP1Gga7kRmD648ftZESrNvbyg"); + final Jwt decodedToken = decoder.decode( + "eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjIxNDc0ODM2NDcsImp0aSI6IjEyMzQiLCJpc3MiOiJodHRwczovL215Lmlzc3VlciIsInN1YiI6ImFiYzEyMyIsInR5cCI6IkJlYXJlciIsInByZWZlcnJlZF91c2VybmFtZSI6InBpb3RyZWsifQ.k_KaOrLLh3QsT8mKphkcz2vKpulgxp92UoEDccpHJ1mxE3Pa3gFXPKTj4goUBKXieGPZRMvBDhfWNxMvRYZPiQr2NXJKapkh0bTd0qoaSWz9ICe9Nu3eg7_VA_nwUVPz_35wwmrxgVk0_kpUYQN_VtaO7ZgFE2sJzFjbkVls5aqfAMnEjEgQl837hqZvmlW2ZRWebtxXfQxAjtp0gcTg-xtAHKIINYo_1_uAtt_H9L8KqFaioxrVAEDDIlcKnb-Ks3Y62CrZauaGUJeN_aNj2gdOpdkhvCw79yJyZSGZ7okjGbidCNSAf7Bo2Y6h3dP1Gga7kRmD648ftZESrNvbyg"); assertThat(decodedToken).isNotNull(); final Map claims = decodedToken.getClaims(); assertThat(claims).isNotNull() - .isNotEmpty() - .containsEntry(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME, "piotrek"); + .isNotEmpty() + .containsEntry(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME, "piotrek"); + } + + @Test + public void shouldAcceptAndDecodeAtJwtToken() throws JOSEException + { + when(config.isClientIdValidationDisabled()).thenReturn(true); + when(config.getSignatureAlgorithms()).thenReturn(Set.of(SignatureAlgorithm.PS256, SignatureAlgorithm.ES512)); + when(restOperations.exchange(any(), any(Class.class))).thenReturn(responseEntity); + + final RSAKey rsaKey = getRsaKey(); + final RSAKey rsaPublicJWK = rsaKey.toPublicJWK(); + when(responseEntity.getStatusCode()).thenReturn(HttpStatus.OK); + when(responseEntity.getBody()).thenReturn(String.format("{\"keys\": [%s]}", rsaPublicJWK.toJSONString())); + + final SignedJWT signedJWT = getSignedJWT(rsaKey, "at+jwt", "userA", "https://my.issuer"); + signedJWT.sign(new RSASSASigner(rsaKey)); + + when(providerDetails.getIssuerUri()).thenReturn("https://my.issuer"); + when(providerDetails.getJwkSetUri()).thenReturn("https://my.jwkSetUri"); + + final JwtDecoderProvider provider = new JwtDecoderProvider(config); + + final JwtDecoder decoder = provider.createJwtDecoder(restOperations, providerDetails); + final Jwt decodedToken = decoder.decode(signedJWT.serialize()); + assertThat(decodedToken).isNotNull(); + + final Map claims = decodedToken.getClaims(); + assertThat(claims).isNotNull() + .isNotEmpty() + .containsEntry(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME, "userA"); + } + + @Test + public void shouldFailWithNotMatchingAlgorithm() throws JOSEException + { + when(config.isClientIdValidationDisabled()).thenReturn(true); + when(config.getSignatureAlgorithms()).thenReturn(Set.of(SignatureAlgorithm.RS256)); + + when(restOperations.exchange(any(), any(Class.class))).thenReturn(responseEntity); + + final RSAKey rsaKey = getRsaKey(); + final RSAKey rsaPublicJWK = rsaKey.toPublicJWK(); + when(responseEntity.getStatusCode()).thenReturn(HttpStatus.OK); + when(responseEntity.getBody()).thenReturn(String.format("{\"keys\": [%s]}", rsaPublicJWK.toJSONString())); + + final SignedJWT signedJWT = getSignedJWT(rsaKey, "at+jwt", "userA", "https://my.issuer"); + signedJWT.sign(new RSASSASigner(rsaKey)); + + when(providerDetails.getIssuerUri()).thenReturn("https://my.issuer"); + when(providerDetails.getJwkSetUri()).thenReturn("https://my.jwkSetUri"); + + final JwtDecoderProvider provider = new JwtDecoderProvider(config); + + final JwtDecoder decoder = provider.createJwtDecoder(restOperations, providerDetails); + assertThrows(BadJwtException.class, () -> decoder.decode(signedJWT.serialize())); + } + + @Test + public void shouldFailWithNotAllowedJOSEHeaderTyp() throws JOSEException + { + when(config.isClientIdValidationDisabled()).thenReturn(true); + when(config.getSignatureAlgorithms()).thenReturn(Set.of(SignatureAlgorithm.PS256)); + when(restOperations.exchange(any(), any(Class.class))).thenReturn(responseEntity); + + final RSAKey rsaKey = getRsaKey(); + final RSAKey rsaPublicJWK = rsaKey.toPublicJWK(); + when(responseEntity.getStatusCode()).thenReturn(HttpStatus.OK); + when(responseEntity.getBody()).thenReturn(String.format("{\"keys\": [%s]}", rsaPublicJWK.toJSONString())); + + final SignedJWT signedJWT = getSignedJWT(rsaKey, "not-allowed-type", "userA", "https://my.issuer"); + signedJWT.sign(new RSASSASigner(rsaKey)); + + when(providerDetails.getIssuerUri()).thenReturn("https://my.issuer"); + when(providerDetails.getJwkSetUri()).thenReturn("https://my.jwkSetUri"); + + final JwtDecoderProvider provider = new JwtDecoderProvider(config); + + final JwtDecoder decoder = provider.createJwtDecoder(restOperations, providerDetails); + assertThrows(BadJwtException.class, () -> decoder.decode(signedJWT.serialize())); } @Test @@ -79,7 +182,8 @@ public class IdentityServiceFacadeFactoryBeanTest { final JwtIssuerValidator issuerValidator = new JwtIssuerValidator(EXPECTED_ISSUER); - final OAuth2TokenValidatorResult validationResult = issuerValidator.validate(tokenWithIssuer("different-issuer")); + final OAuth2TokenValidatorResult validationResult = issuerValidator.validate( + tokenWithIssuer("different-issuer")); assertThat(validationResult).isNotNull(); assertThat(validationResult.hasErrors()).isTrue(); assertThat(validationResult.getErrors()).hasSize(1); @@ -164,15 +268,35 @@ public class IdentityServiceFacadeFactoryBeanTest final JwtAudienceValidator audienceValidator = new JwtAudienceValidator(EXPECTED_AUDIENCE); final Jwt token = Jwt.withTokenValue(UUID.randomUUID().toString()) - .claim("aud", EXPECTED_AUDIENCE) - .header("JUST", "FOR TESTING") - .build(); + .claim("aud", EXPECTED_AUDIENCE) + .header("JUST", "FOR TESTING") + .build(); final OAuth2TokenValidatorResult validationResult = audienceValidator.validate(token); assertThat(validationResult).isNotNull(); assertThat(validationResult.hasErrors()).isFalse(); assertThat(validationResult.getErrors()).isEmpty(); } + private static RSAKey getRsaKey() throws JOSEException + { + return new RSAKeyGenerator(2048) + .keyUse(KeyUse.SIGNATURE) + .algorithm(new Algorithm("PS256")) + .keyID(KEY_ID) + .generate(); + } + + private static SignedJWT getSignedJWT(RSAKey rsaKey, String type, String usernameClaim, String issuer) + { + final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() + .issuer(issuer) + .claim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME, usernameClaim) + .build(); + return new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.PS256) + .type(new JOSEObjectType(type)) + .keyID(rsaKey.getKeyID()).build(), claimsSet); + } + private Jwt tokenWithIssuer(String issuer) { return Jwt.withTokenValue(UUID.randomUUID().toString())