+{
+ @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())