Merge branch 'master' of github.com:Alfresco/alfresco-community-repo into feature/MNT-24127-EndpointToCalculateFolderSize

# Conflicts:
#	repository/src/test/java/org/alfresco/AppContext01TestSuite.java
This commit is contained in:
mohit-singh4
2024-06-27 12:03:37 +05:30
64 changed files with 2026 additions and 189 deletions

View File

@@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.alfresco</groupId> <groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-amps</artifactId> <artifactId>alfresco-community-repo-amps</artifactId>
<version>23.3.0.58-SNAPSHOT</version> <version>23.3.0.61-SNAPSHOT</version>
</parent> </parent>
<modules> <modules>

View File

@@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.alfresco</groupId> <groupId>org.alfresco</groupId>
<artifactId>alfresco-governance-services-community-parent</artifactId> <artifactId>alfresco-governance-services-community-parent</artifactId>
<version>23.3.0.58-SNAPSHOT</version> <version>23.3.0.61-SNAPSHOT</version>
</parent> </parent>
<modules> <modules>

View File

@@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.alfresco</groupId> <groupId>org.alfresco</groupId>
<artifactId>alfresco-governance-services-automation-community-repo</artifactId> <artifactId>alfresco-governance-services-automation-community-repo</artifactId>
<version>23.3.0.58-SNAPSHOT</version> <version>23.3.0.61-SNAPSHOT</version>
</parent> </parent>
<build> <build>

View File

@@ -49,6 +49,7 @@ import org.alfresco.rest.rm.community.requests.gscore.api.TransferAPI;
import org.alfresco.rest.rm.community.requests.gscore.api.TransferContainerAPI; import org.alfresco.rest.rm.community.requests.gscore.api.TransferContainerAPI;
import org.alfresco.rest.rm.community.requests.gscore.api.UnfiledContainerAPI; 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.UnfiledRecordFolderAPI;
import org.alfresco.rest.rm.community.requests.gscore.api.RetentionScheduleAPI;
import org.alfresco.utility.data.DataUserAIS; import org.alfresco.utility.data.DataUserAIS;
import org.alfresco.utility.model.RepoTestModel; import org.alfresco.utility.model.RepoTestModel;
import org.alfresco.utility.model.UserModel; import org.alfresco.utility.model.UserModel;
@@ -254,4 +255,14 @@ public class RestAPIFactory
{ {
return getGSCoreAPI(userModel).usingHoldsAPI(); return getGSCoreAPI(userModel).usingHoldsAPI();
} }
public RetentionScheduleAPI getRetentionScheduleAPI()
{
return getGSCoreAPI(null).usingRetentionScheduleAPI();
}
public RetentionScheduleAPI getRetentionScheduleAPI(UserModel userModel)
{
return getGSCoreAPI(userModel).usingRetentionScheduleAPI();
}
} }

View File

@@ -0,0 +1,49 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.rm.community.model.retentionschedule;
import lombok.EqualsAndHashCode;
import org.alfresco.utility.model.TestModel;
import lombok.Data;
import java.util.List;
/**
* retention schedule
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class RetentionSchedule extends TestModel
{
private String id ;
private String parentId;
private String authority;
private String instructions;
private boolean isRecordLevel;
private boolean isUnpublishedUpdates;
private List<RetentionScheduleActionDefinition> actions;
}

View File

@@ -0,0 +1,50 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.rm.community.model.retentionschedule;
import java.util.List;
import lombok.Data;
/**
* retention schedule action definition
*/
@Data
public class RetentionScheduleActionDefinition
{
private String id;
private String name;
private int periodAmount;
private String period;
private String periodProperty;
private boolean combineDispositionStepConditions;
private List<String> events;
private boolean eligibleOnFirstCompleteEvent;
private String description;
private boolean retainRecordMetadataAfterDestruction;
private String location;
private int index;
}

View File

@@ -0,0 +1,32 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.rm.community.model.retentionschedule;
import org.alfresco.rest.core.RestModels;
public class RetentionScheduleCollection extends RestModels<RetentionScheduleEntry, RetentionScheduleCollection>
{
}

View File

@@ -0,0 +1,37 @@
/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.rm.community.model.retentionschedule;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import org.alfresco.rest.core.RestModels;
@Data
public class RetentionScheduleEntry extends RestModels<RetentionSchedule, RetentionScheduleEntry>
{
@JsonProperty
private RetentionSchedule entry;
}

View File

@@ -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.TransferContainerAPI;
import org.alfresco.rest.rm.community.requests.gscore.api.UnfiledContainerAPI; 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.UnfiledRecordFolderAPI;
import org.alfresco.rest.rm.community.requests.gscore.api.RetentionScheduleAPI;
/** /**
* Defines the entire GS Core API * Defines the entire GS Core API
@@ -193,4 +194,9 @@ public class GSCoreAPI extends RMModelRequest
} }
public HoldsAPI usingHoldsAPI() { return new HoldsAPI(getRmRestWrapper()); } public HoldsAPI usingHoldsAPI() { return new HoldsAPI(getRmRestWrapper()); }
public RetentionScheduleAPI usingRetentionScheduleAPI()
{
return new RetentionScheduleAPI(getRmRestWrapper());
}
} }

View File

@@ -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 <http://www.gnu.org/licenses/>.
* #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:
* <ul>
* <li>{@code recordCategoryId} is not a valid format or {@code recordCategoryId} is invalid</li>
* <li>authentication fails</li>
* <li>current user does not have permission to add children to {@code recordCategoryId}</li>
* <li>{@code recordCategoryId} does not exist</li>
* <li>new name clashes with an existing node in the current parent container</li>
* </ul>
*/
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:
* <ul>
* <li>authentication fails</li>
* <li>current user does not have permission to read {@code recordCategoryId}</li>
* <li>{@code recordCategoryId} does not exist</li>
*</ul>
*/
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);
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
* #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"));
}
/**
* <pre>
* 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
* </pre>
*/
@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);
}
/**
* <pre>
* 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
* </pre>
*/
@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);
}
/**
* <pre>
* 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
* </pre>
*/
@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);
}
/**
* <pre>
* 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
* </pre>
*/
@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());
}
/**
* <pre>
* 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
* </pre>
*/
@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);
}
/**
* <pre>
* 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
* </pre>
*/
@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);
}
/**
* <pre>
* 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
* </pre>
*/
@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);
}
/**
* <pre>
* 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
* </pre>
*/
@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);
}
/**
* <pre>
* 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
* </pre>
*/
@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);
}
}

View File

@@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.alfresco</groupId> <groupId>org.alfresco</groupId>
<artifactId>alfresco-governance-services-community-parent</artifactId> <artifactId>alfresco-governance-services-community-parent</artifactId>
<version>23.3.0.58-SNAPSHOT</version> <version>23.3.0.61-SNAPSHOT</version>
</parent> </parent>
<modules> <modules>

View File

@@ -31,6 +31,11 @@
<cm:description>Configuration information for the Records Management application.</cm:description> <cm:description>Configuration information for the Records Management application.</cm:description>
</view:properties> </view:properties>
<view:aspects>
<sys:undeletable/>
<sys:unmovable/>
</view:aspects>
<view:associations> <view:associations>
<cm:contains> <cm:contains>

View File

@@ -147,6 +147,13 @@
<property name="transactionService" ref="transactionService" /> <property name="transactionService" ref="transactionService" />
</bean> </bean>
<bean class="org.alfresco.rm.rest.api.retentionschedule.RetentionScheduleRelation">
<property name="apiUtils" ref="apiUtils" />
<property name="nodesModelFactory" ref="nodesModelFactory" />
<property name="dispositionService" ref="DispositionService"/>
<property name="nodeService" ref="NodeService"/>
</bean>
<bean class="org.alfresco.rm.rest.api.recordfolders.RecordFolderEntityResource"> <bean class="org.alfresco.rm.rest.api.recordfolders.RecordFolderEntityResource">
<property name="apiUtils" ref="apiUtils" /> <property name="apiUtils" ref="apiUtils" />
<property name="fileFolderService" ref="FileFolderService" /> <property name="fileFolderService" ref="FileFolderService" />

View File

@@ -8,7 +8,7 @@
<parent> <parent>
<groupId>org.alfresco</groupId> <groupId>org.alfresco</groupId>
<artifactId>alfresco-governance-services-community-repo-parent</artifactId> <artifactId>alfresco-governance-services-community-repo-parent</artifactId>
<version>23.3.0.58-SNAPSHOT</version> <version>23.3.0.61-SNAPSHOT</version>
</parent> </parent>
<properties> <properties>

View File

@@ -59,6 +59,9 @@ import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport; import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState; import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; 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.dictionary.DictionaryService;
import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.NodeRef; 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. * 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 @Override
@Behaviour(kind=BehaviourKind.CLASS, type="rma:record") @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 @Override
public void refreshDispositionAction(NodeRef nodeRef) public void refreshDispositionAction(NodeRef nodeRef)
@@ -242,7 +245,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl
/** ========= Disposition Property Methods ========= */ /** ========= 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 @Override
public void registerDispositionProperty(DispositionProperty dispositionProperty) 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 @Override
public Collection<DispositionProperty> getDispositionProperties(boolean isRecordLevel, String dispositionAction) public Collection<DispositionProperty> 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 @Override
public Collection<DispositionProperty> getDispositionProperties() public Collection<DispositionProperty> getDispositionProperties()
@@ -281,12 +284,11 @@ public class DispositionServiceImpl extends ServiceBaseImpl
/** ========= Disposition Schedule Methods ========= */ /** ========= 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 @Override
public DispositionSchedule getDispositionSchedule(final NodeRef nodeRef) public DispositionSchedule getDispositionSchedule(final NodeRef nodeRef)
{ {
DispositionSchedule ds = null;
NodeRef dsNodeRef = null; NodeRef dsNodeRef = null;
if (isRecord(nodeRef)) if (isRecord(nodeRef))
{ {
@@ -311,9 +313,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl
if (dsNextAction != null) if (dsNextAction != null)
{ {
final NodeRef action = dsNextAction.getNextActionNodeRef(); 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 String dispositionActionName = dsNextAction.getNextActionName();
final Date dispositionActionDate = dsNextAction.getNextActionDateAsOf(); final Date dispositionActionDate = dsNextAction.getNextActionDateAsOf();
@@ -342,7 +342,6 @@ public class DispositionServiceImpl extends ServiceBaseImpl
nodeService.setProperty(action, PROP_DISPOSITION_ACTION_NAME, dispositionActionName); nodeService.setProperty(action, PROP_DISPOSITION_ACTION_NAME, dispositionActionName);
} }
} }
}
dsNodeRef = dsNextAction.getDispositionNodeRef(); dsNodeRef = dsNextAction.getDispositionNodeRef();
} }
@@ -352,7 +351,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl
// Get the disposition instructions for the node reference provided // Get the disposition instructions for the node reference provided
dsNodeRef = getDispositionScheduleImpl(nodeRef); dsNodeRef = getDispositionScheduleImpl(nodeRef);
} }
DispositionSchedule ds = null;
if (dsNodeRef != null) if (dsNodeRef != null)
{ {
ds = new DispositionScheduleImpl(serviceRegistry, nodeService, dsNodeRef); ds = new DispositionScheduleImpl(serviceRegistry, nodeService, dsNodeRef);
@@ -383,6 +382,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl
return result; return result;
} }
@Override
public DispositionSchedule getOriginDispositionSchedule(NodeRef nodeRef) public DispositionSchedule getOriginDispositionSchedule(NodeRef nodeRef)
{ {
NodeRef parent = this.nodeService.getPrimaryParent(nodeRef).getParentRef(); 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 @Override
public DispositionSchedule getAssociatedDispositionSchedule(NodeRef nodeRef) public DispositionSchedule getAssociatedDispositionSchedule(NodeRef nodeRef)
@@ -437,7 +437,6 @@ public class DispositionServiceImpl extends ServiceBaseImpl
*/ */
private NodeRef getAssociatedDispositionScheduleImpl(NodeRef nodeRef) private NodeRef getAssociatedDispositionScheduleImpl(NodeRef nodeRef)
{ {
NodeRef result = null;
ParameterCheck.mandatory("nodeRef", nodeRef); ParameterCheck.mandatory("nodeRef", nodeRef);
// Make sure we are dealing with an RM node // 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() + ")"); 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)) if (getInternalNodeService().hasAspect(nodeRef, ASPECT_SCHEDULED))
{ {
List<ChildAssociationRef> childAssocs = getInternalNodeService().getChildAssocs(nodeRef, ASSOC_DISPOSITION_SCHEDULE, RegexQNamePattern.MATCH_ALL); List<ChildAssociationRef> 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 @Override
public NodeRef getAssociatedRecordsManagementContainer(DispositionSchedule dispositionSchedule) 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 // 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 // only the first disposition schedule will be considered
if (LOGGER.isWarnEnabled()) LOGGER.atWarn().log("Retention schedule has more than one associated records management container. " +
{
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. " + "This is not currently supported so only the first container will be considered. " +
"(dispositionScheduleNodeRef=" + dispositionSchedule.getNodeRef().toString() + ")"); "(dispositionScheduleNodeRef={})", dispositionSchedule.getNodeRef());
}
} }
// Get the container reference // 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 @Override
public boolean hasDisposableItems(DispositionSchedule dispositionSchdule) public boolean hasDisposableItems(DispositionSchedule dispositionSchdule)
@@ -537,19 +534,16 @@ public class DispositionServiceImpl extends ServiceBaseImpl
return true; 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; 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 @Override
public List<NodeRef> getDisposableItems(DispositionSchedule dispositionSchedule) public List<NodeRef> 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 @Override
public boolean isDisposableItem(NodeRef nodeRef) 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 @Override
public DispositionSchedule createDispositionSchedule(NodeRef nodeRef, Map<QName, Serializable> props) public DispositionSchedule createDispositionSchedule(NodeRef nodeRef, Map<QName, Serializable> props)
{ {
NodeRef dsNodeRef = null;
// Check mandatory parameters // Check mandatory parameters
ParameterCheck.mandatory("nodeRef", nodeRef); ParameterCheck.mandatory("nodeRef", nodeRef);
// Check exists // Check exists
if (!nodeService.exists(nodeRef)) 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 // Check is sub-type of rm:recordCategory
@@ -625,10 +617,12 @@ public class DispositionServiceImpl extends ServiceBaseImpl
if (!TYPE_RECORD_CATEGORY.equals(nodeRefType) && if (!TYPE_RECORD_CATEGORY.equals(nodeRefType) &&
!dictionaryService.isSubClass(nodeRefType, TYPE_RECORD_CATEGORY)) !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); behaviourFilter.disableBehaviour(nodeRef, ASPECT_SCHEDULED);
NodeRef dsNodeRef = null;
try try
{ {
// Add the schedules aspect if required // Add the schedules aspect if required
@@ -662,7 +656,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl
else else
{ {
// Error since the node already has a disposition schedule set // 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 finally
@@ -686,7 +680,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl
{ {
// make sure at least a name has been defined // make sure at least a name has been defined
String name = (String)actionDefinitionParams.get(PROP_DISPOSITION_ACTION_NAME); 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"); 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 // create the child association from the schedule to the action definition
NodeRef actionNodeRef = this.nodeService.createNode(schedule.getNodeRef(), 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.createQName(NamespaceService.CONTENT_MODEL_1_0_URI,
QName.createValidLocalName(name)), 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 // get the updated disposition schedule and retrieve the new action definition
NodeRef scheduleParent = this.nodeService.getPrimaryParent(schedule.getNodeRef()).getParentRef(); 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 @Override
public void removeDispositionActionDefinition(DispositionSchedule schedule, DispositionActionDefinition actionDefinition) public void removeDispositionActionDefinition(DispositionSchedule schedule, DispositionActionDefinition actionDefinition)
@@ -777,16 +771,12 @@ public class DispositionServiceImpl extends ServiceBaseImpl
DispositionAction da; DispositionAction da;
// check if current transaction is a READ ONLY one and if true create the node in a READ WRITE transaction // 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)) if (AlfrescoTransactionSupport.getTransactionReadState().equals(TxnReadState.TXN_READ_ONLY)) {
{ da = transactionService.getRetryingTransactionHelper().doInTransaction(
da = () -> createDispositionAction(nodeRef, props),
transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback<DispositionAction>() false,
{ true
public DispositionAction execute() throws Throwable );
{
return createDispositionAction(nodeRef, props);
}
}, false, true);
} }
else else
{ {
@@ -836,13 +826,13 @@ public class DispositionServiceImpl extends ServiceBaseImpl
Period period = dispositionActionDefinition.getPeriod(); Period period = dispositionActionDefinition.getPeriod();
if (period != null) if (period != null)
{ {
Date contextDate = null; Date contextDate;
// Get the period properties value // Get the period properties value
QName periodProperty = dispositionActionDefinition.getPeriodProperty(); QName periodProperty = dispositionActionDefinition.getPeriodProperty();
if (periodProperty != null) if (periodProperty != null)
{ {
if (RecordsManagementModel.PROP_DISPOSITION_AS_OF.equals(periodProperty)) if (PROP_DISPOSITION_AS_OF.equals(periodProperty))
{ {
DispositionAction lastCompletedDispositionAction = getLastCompletedDispostionAction(nodeRef); DispositionAction lastCompletedDispositionAction = getLastCompletedDispostionAction(nodeRef);
if (lastCompletedDispositionAction != null) 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 @Override
public boolean isNextDispositionActionEligible(NodeRef nodeRef) public boolean isNextDispositionActionEligible(NodeRef nodeRef)
@@ -940,7 +930,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl
{ {
NodeRef eventExecution = assoc.getChildRef(); NodeRef eventExecution = assoc.getChildRef();
Boolean isCompleteValue = (Boolean) getInternalNodeService().getProperty(eventExecution, PROP_EVENT_EXECUTION_COMPLETE); Boolean isCompleteValue = (Boolean) getInternalNodeService().getProperty(eventExecution, PROP_EVENT_EXECUTION_COMPLETE);
boolean isComplete = false; boolean isComplete;
if (isCompleteValue != null) if (isCompleteValue != null)
{ {
isComplete = isCompleteValue.booleanValue(); 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 @Override
public DispositionAction getNextDispositionAction(NodeRef nodeRef) public DispositionAction getNextDispositionAction(NodeRef nodeRef)
@@ -1006,7 +996,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl
/** ========= Disposition Action History Methods ========= */ /** ========= 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 @Override
public List<DispositionAction> getCompletedDispositionActions(NodeRef nodeRef) public List<DispositionAction> 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 @Override
public DispositionAction getLastCompletedDispostionAction(NodeRef nodeRef) 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 @Override
public boolean isDisposableItemCutoff(NodeRef nodeRef) 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 @Override
public void updateNextDispositionAction(final NodeRef nodeRef) public void updateNextDispositionAction(final NodeRef nodeRef)
@@ -1058,7 +1048,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl
RunAsWork<Void> runAsWork = new RunAsWork<Void>() RunAsWork<Void> runAsWork = new RunAsWork<Void>()
{ {
/** /**
* @see org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork#doWork() * @see RunAsWork#doWork()
*/ */
@Override @Override
public Void doWork() 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 @Override
public void updateNextDispositionAction(final NodeRef nodeRef, final DispositionSchedule dispositionSchedule) public void updateNextDispositionAction(final NodeRef nodeRef, final DispositionSchedule dispositionSchedule)
@@ -1087,7 +1077,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl
RunAsWork<Void> runAsWork = new RunAsWork<Void>() RunAsWork<Void> runAsWork = new RunAsWork<Void>()
{ {
/** /**
* @see org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork#doWork() * @see RunAsWork#doWork()
*/ */
@Override @Override
public Void doWork() public Void doWork()
@@ -1113,17 +1103,14 @@ public class DispositionServiceImpl extends ServiceBaseImpl
} }
List<DispositionActionDefinition> dispositionActionDefinitions = dispositionSchedule.getDispositionActionDefinitions(); List<DispositionActionDefinition> dispositionActionDefinitions = dispositionSchedule.getDispositionActionDefinitions();
DispositionActionDefinition currentDispositionActionDefinition = null; DispositionActionDefinition currentDispositionActionDefinition;
DispositionActionDefinition nextDispositionActionDefinition = null; DispositionActionDefinition nextDispositionActionDefinition = null;
if (currentDispositionAction == null) if (currentDispositionAction == null && !dispositionActionDefinitions.isEmpty())
{
if (!dispositionActionDefinitions.isEmpty())
{ {
// The next disposition action is the first action // The next disposition action is the first action
nextDispositionActionDefinition = dispositionActionDefinitions.get(0); nextDispositionActionDefinition = dispositionActionDefinitions.get(0);
} }
}
else else
{ {
// Get the current action // Get the current action
@@ -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 @Override
public void cutoffDisposableItem(final NodeRef nodeRef) 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 // runAs system so that we can close a record that has already been cutoff
authenticationUtil.runAsSystem(new RunAsWork<Void>() authenticationUtil.runAsSystem(new RunAsWork<Void>()
{ {
@Override
public Void doWork() throws Exception public Void doWork() throws Exception
{ {
recordFolderService.closeRecordFolder(nodeRef); recordFolderService.closeRecordFolder(nodeRef);
@@ -1224,6 +1212,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl
} }
} }
@Override
public Date getDispositionActionDate(NodeRef record, NodeRef dispositionSchedule, String dispositionActionName) public Date getDispositionActionDate(NodeRef record, NodeRef dispositionSchedule, String dispositionActionName)
{ {
DispositionSchedule ds = new DispositionScheduleImpl(serviceRegistry, nodeService, dispositionSchedule); DispositionSchedule ds = new DispositionScheduleImpl(serviceRegistry, nodeService, dispositionSchedule);
@@ -1244,6 +1233,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl
return null; return null;
} }
@Override
public void recalculateNextDispositionStep(NodeRef record) public void recalculateNextDispositionStep(NodeRef record)
{ {
List<NodeRef> recordFolders = recordFolderService.getRecordFolders(record); List<NodeRef> recordFolders = recordFolderService.getRecordFolders(record);
@@ -1384,14 +1374,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl
Date calculatedDate = (nextDispositionActionDate != null ? nextDispositionActionDate : maxDate); Date calculatedDate = (nextDispositionActionDate != null ? nextDispositionActionDate : maxDate);
// We only need to update the date if the current one is too early. // We only need to update the date if the current one is too early.
if (recordDate.before(calculatedDate)) return recordDate.before(calculatedDate) ? WriteMode.DATE_ONLY : WriteMode.READ_ONLY;
{
return WriteMode.DATE_ONLY;
}
else
{
return WriteMode.READ_ONLY;
}
} }
/** /**
@@ -1414,7 +1397,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl
DispositionSchedule ds = new DispositionScheduleImpl(serviceRegistry, nodeService, folderDS); DispositionSchedule ds = new DispositionScheduleImpl(serviceRegistry, nodeService, folderDS);
List<DispositionActionDefinition> dispositionActionDefinitions = ds.getDispositionActionDefinitions(); List<DispositionActionDefinition> dispositionActionDefinitions = ds.getDispositionActionDefinitions();
if (dispositionActionDefinitions != null && dispositionActionDefinitions.size() > 0) if (dispositionActionDefinitions != null && !dispositionActionDefinitions.isEmpty())
{ {
DispositionActionDefinition firstDispositionActionDef = dispositionActionDefinitions.get(0); DispositionActionDefinition firstDispositionActionDef = dispositionActionDefinitions.get(0);
dispositionNodeRef = folderDS; dispositionNodeRef = folderDS;

View File

@@ -34,10 +34,13 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import org.alfresco.model.ContentModel; 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.DispositionSchedule;
import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; 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.module.org_alfresco_module_rm.model.RecordsManagementModel;
import org.alfresco.rest.api.Nodes; import org.alfresco.rest.api.Nodes;
import org.alfresco.rest.api.model.AssocChild; 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.UnfiledContainerChild;
import org.alfresco.rm.rest.api.model.UnfiledRecordFolder; import org.alfresco.rm.rest.api.model.UnfiledRecordFolder;
import org.alfresco.rm.rest.api.model.UnfiledRecordFolderChild; 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.ServiceRegistry;
import org.alfresco.service.cmr.model.FileInfo; import org.alfresco.service.cmr.model.FileInfo;
import org.alfresco.service.cmr.repository.ChildAssociationRef; 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.NamespaceService;
import org.alfresco.service.namespace.QName; 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 * Utility class containing Alfresco and RM java services required by the API
* endpoints * endpoints
@@ -891,4 +898,139 @@ public class ApiNodesModelFactory
mapAssociations(record, info, parameters.getInclude()); mapAssociations(record, info, parameters.getInclude());
return record; 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<RecordsManagementEvent> events = dispositionActionDefinition.getEvents();
if (events != null && !events.isEmpty())
{
List<String> 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<String> includeParam)
{
if (includeParam != null && !includeParam.isEmpty() && includeParam.contains("actions"))
{
List<RetentionScheduleActionDefinition> actions = schedule.getDispositionActionDefinitions().stream()
.map(this::mapRetentionScheduleActionDefData)
.collect(Collectors.toList());
retentionSchedule.setActions(actions);
}
}
} }

View File

@@ -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 <http://www.gnu.org/licenses/>.
* #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<RetentionScheduleActionDefinition> actions;
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
* #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<String> events;
private boolean eligibleOnFirstCompleteEvent;
private String description;
private boolean retainRecordMetadataAfterDestruction;
private String location;
private int index;
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
* #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<RetentionSchedule>,
RelationshipResourceAction.Create<RetentionSchedule>
{
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<RetentionSchedule> create(String recordCategoryId, List<RetentionSchedule> nodeInfos, Parameters parameters)
{
checkNotBlank("recordCategoryId", recordCategoryId);
mandatory("entity", nodeInfos);
mandatory("parameters", parameters);
NodeRef parentNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, recordCategoryId);
List<RetentionSchedule> result = new ArrayList<>();
// Create the disposition schedule
Map<QName, Serializable> 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<RetentionSchedule> 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<RetentionSchedule> retentionScheduleList = new ArrayList<>();
nodesModelFactory.mapRetentionScheduleOptionalInfo(retentionSchedule, schedule, parameters.getInclude());
retentionScheduleList.add(retentionSchedule);
return CollectionWithPagingInfo.asPaged(parameters.getPaging(), retentionScheduleList, false,
retentionScheduleList.size());
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
* #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;

View File

@@ -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.model.RecordsManagementModel;
import org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMTestCase; 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.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.cmr.repository.NodeRef;
import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.QName;
@@ -437,19 +439,12 @@ public class DispositionServiceImplTest extends BaseRMTestCase
// Check the disposition schedule // Check the disposition schedule
checkDispositionSchedule(ds, "testCreateDispositionSchedule", "testCreateDispositionSchedule", false); checkDispositionSchedule(ds, "testCreateDispositionSchedule", "testCreateDispositionSchedule", false);
}
});
// Failure: create disposition schedule on container with existing disposition schedule // Failure: create disposition schedule on container with existing disposition schedule
doTestInTransaction(new FailureTest Assert.assertThrows(ConstraintViolatedException.class,
( () -> {
"Can not create a disposition schedule on a container with an existing disposition schedule"
)
{
@Override
public void run()
{
utils.createBasicDispositionSchedule(rmContainer); utils.createBasicDispositionSchedule(rmContainer);
});
} }
}); });
} }
@@ -492,19 +487,12 @@ public class DispositionServiceImplTest extends BaseRMTestCase
// Check the disposition schedule // Check the disposition schedule
checkDispositionSchedule(testA, "testA", "testA", false); checkDispositionSchedule(testA, "testA", "testA", false);
checkDispositionSchedule(testB, "testB", "testB", false); checkDispositionSchedule(testB, "testB", "testB", false);
}
});
// Failure: create disposition schedule on container with existing disposition schedule // Failure: create disposition schedule on container with existing disposition schedule
doTestInTransaction(new FailureTest Assert.assertThrows(ConstraintViolatedException.class,
( () -> {
"Can not create a disposition schedule on container with an existing disposition schedule" utils.createBasicDispositionSchedule(rmContainer);
) });
{
@Override
public void run()
{
utils.createBasicDispositionSchedule(mhContainer11);
} }
}); });

View File

@@ -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 <http://www.gnu.org/licenses/>.
* #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());
}
}

View File

@@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.alfresco</groupId> <groupId>org.alfresco</groupId>
<artifactId>alfresco-governance-services-community-repo-parent</artifactId> <artifactId>alfresco-governance-services-community-repo-parent</artifactId>
<version>23.3.0.58-SNAPSHOT</version> <version>23.3.0.61-SNAPSHOT</version>
</parent> </parent>
<build> <build>

View File

@@ -40,6 +40,8 @@ tags:
description: Retrieve and manage unfiled record folders description: Retrieve and manage unfiled record folders
- name: holds - name: holds
description: Retrieve and manage holds description: Retrieve and manage holds
- name: retention-schedules
description: Perform retention schedule specific operations
paths: paths:
## GS sites ## GS sites
@@ -2634,7 +2636,194 @@ paths:
description: Unexpected error description: Unexpected error
schema: schema:
$ref: '#/definitions/Error' $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 cant 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: parameters:
## File plans ## File plans
filePlanEntryIncludeParam: 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. If true, then a name clash will cause an attempt to auto rename by finding a unique name using an integer suffix.
required: false required: false
type: boolean 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: definitions:
FilePlanComponentBodyUpdate: FilePlanComponentBodyUpdate:
type: object type: object
@@ -4258,6 +4463,225 @@ definitions:
type: array type: array
items: items:
$ref: '#/definitions/HoldBulkStatusEntry' $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: RequestBodyFile:
type: object type: object

View File

@@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.alfresco</groupId> <groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId> <artifactId>alfresco-community-repo</artifactId>
<version>23.3.0.58-SNAPSHOT</version> <version>23.3.0.61-SNAPSHOT</version>
</parent> </parent>
<modules> <modules>

View File

@@ -8,7 +8,7 @@
<parent> <parent>
<groupId>org.alfresco</groupId> <groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-amps</artifactId> <artifactId>alfresco-community-repo-amps</artifactId>
<version>23.3.0.58-SNAPSHOT</version> <version>23.3.0.61-SNAPSHOT</version>
</parent> </parent>
<properties> <properties>

View File

@@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.alfresco</groupId> <groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId> <artifactId>alfresco-community-repo</artifactId>
<version>23.3.0.58-SNAPSHOT</version> <version>23.3.0.61-SNAPSHOT</version>
</parent> </parent>
<dependencies> <dependencies>

View File

@@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.alfresco</groupId> <groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId> <artifactId>alfresco-community-repo</artifactId>
<version>23.3.0.58-SNAPSHOT</version> <version>23.3.0.61-SNAPSHOT</version>
</parent> </parent>
<properties> <properties>

View File

@@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.alfresco</groupId> <groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId> <artifactId>alfresco-community-repo</artifactId>
<version>23.3.0.58-SNAPSHOT</version> <version>23.3.0.61-SNAPSHOT</version>
</parent> </parent>
<dependencies> <dependencies>

View File

@@ -9,6 +9,6 @@
<parent> <parent>
<groupId>org.alfresco</groupId> <groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-packaging</artifactId> <artifactId>alfresco-community-repo-packaging</artifactId>
<version>23.3.0.58-SNAPSHOT</version> <version>23.3.0.61-SNAPSHOT</version>
</parent> </parent>
</project> </project>

View File

@@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.alfresco</groupId> <groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-packaging</artifactId> <artifactId>alfresco-community-repo-packaging</artifactId>
<version>23.3.0.58-SNAPSHOT</version> <version>23.3.0.61-SNAPSHOT</version>
</parent> </parent>
<properties> <properties>

View File

@@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.alfresco</groupId> <groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId> <artifactId>alfresco-community-repo</artifactId>
<version>23.3.0.58-SNAPSHOT</version> <version>23.3.0.61-SNAPSHOT</version>
</parent> </parent>
<modules> <modules>

View File

@@ -6,7 +6,7 @@
<parent> <parent>
<groupId>org.alfresco</groupId> <groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-packaging</artifactId> <artifactId>alfresco-community-repo-packaging</artifactId>
<version>23.3.0.58-SNAPSHOT</version> <version>23.3.0.61-SNAPSHOT</version>
</parent> </parent>
<modules> <modules>

View File

@@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.alfresco</groupId> <groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-tests</artifactId> <artifactId>alfresco-community-repo-tests</artifactId>
<version>23.3.0.58-SNAPSHOT</version> <version>23.3.0.61-SNAPSHOT</version>
</parent> </parent>
<organization> <organization>

View File

@@ -9,7 +9,7 @@
<parent> <parent>
<groupId>org.alfresco</groupId> <groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-tests</artifactId> <artifactId>alfresco-community-repo-tests</artifactId>
<version>23.3.0.58-SNAPSHOT</version> <version>23.3.0.61-SNAPSHOT</version>
</parent> </parent>
<developers> <developers>

View File

@@ -9,7 +9,7 @@
<parent> <parent>
<groupId>org.alfresco</groupId> <groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-tests</artifactId> <artifactId>alfresco-community-repo-tests</artifactId>
<version>23.3.0.58-SNAPSHOT</version> <version>23.3.0.61-SNAPSHOT</version>
</parent> </parent>
<developers> <developers>

View File

@@ -8,7 +8,7 @@
<parent> <parent>
<groupId>org.alfresco</groupId> <groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-tests</artifactId> <artifactId>alfresco-community-repo-tests</artifactId>
<version>23.3.0.58-SNAPSHOT</version> <version>23.3.0.61-SNAPSHOT</version>
</parent> </parent>
<properties> <properties>

View File

@@ -9,7 +9,7 @@
<parent> <parent>
<groupId>org.alfresco</groupId> <groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-tests</artifactId> <artifactId>alfresco-community-repo-tests</artifactId>
<version>23.3.0.58-SNAPSHOT</version> <version>23.3.0.61-SNAPSHOT</version>
</parent> </parent>
<developers> <developers>

View File

@@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.alfresco</groupId> <groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-packaging</artifactId> <artifactId>alfresco-community-repo-packaging</artifactId>
<version>23.3.0.58-SNAPSHOT</version> <version>23.3.0.61-SNAPSHOT</version>
</parent> </parent>
<properties> <properties>

View File

@@ -2,7 +2,7 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>alfresco-community-repo</artifactId> <artifactId>alfresco-community-repo</artifactId>
<version>23.3.0.58-SNAPSHOT</version> <version>23.3.0.61-SNAPSHOT</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<name>Alfresco Community Repo Parent</name> <name>Alfresco Community Repo Parent</name>

View File

@@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.alfresco</groupId> <groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId> <artifactId>alfresco-community-repo</artifactId>
<version>23.3.0.58-SNAPSHOT</version> <version>23.3.0.61-SNAPSHOT</version>
</parent> </parent>
<dependencies> <dependencies>

View File

@@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.alfresco</groupId> <groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId> <artifactId>alfresco-community-repo</artifactId>
<version>23.3.0.58-SNAPSHOT</version> <version>23.3.0.61-SNAPSHOT</version>
</parent> </parent>
<dependencies> <dependencies>

View File

@@ -25,9 +25,14 @@
*/ */
package org.alfresco.repo.security.authentication.identityservice; package org.alfresco.repo.security.authentication.identityservice;
import java.util.Objects;
import java.util.Optional; 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.apache.commons.lang3.StringUtils;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
/** /**
@@ -35,6 +40,7 @@ import org.springframework.web.util.UriComponentsBuilder;
* *
* @author Gavin Cornwell * @author Gavin Cornwell
*/ */
@SuppressWarnings("PMD.ExcessivePublicCount")
public class IdentityServiceConfig public class IdentityServiceConfig
{ {
private static final String REALMS = "realms"; private static final String REALMS = "realms";
@@ -62,6 +68,7 @@ public class IdentityServiceConfig
private String principalAttribute; private String principalAttribute;
private boolean clientIdValidationDisabled; private boolean clientIdValidationDisabled;
private String adminConsoleRedirectPath; private String adminConsoleRedirectPath;
private String signatureAlgorithms;
/** /**
* *
@@ -306,4 +313,18 @@ public class IdentityServiceConfig
{ {
this.adminConsoleRedirectPath = adminConsoleRedirectPath; this.adminConsoleRedirectPath = adminConsoleRedirectPath;
} }
public Set<SignatureAlgorithm> 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;
}
} }

View File

@@ -58,10 +58,13 @@ import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.source.DefaultJWKSetCache; import com.nimbusds.jose.jwk.source.DefaultJWKSetCache;
import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.jwk.source.RemoteJWKSet; 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.JWSVerificationKeySelector;
import com.nimbusds.jose.proc.SecurityContext; import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jose.util.ResourceRetriever; import com.nimbusds.jose.util.ResourceRetriever;
@@ -129,6 +132,9 @@ import org.springframework.web.util.UriComponentsBuilder;
public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentityServiceFacade> public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentityServiceFacade>
{ {
private static final Log LOGGER = LogFactory.getLog(IdentityServiceFacadeFactoryBean.class); private static final Log LOGGER = LogFactory.getLog(IdentityServiceFacadeFactoryBean.class);
private static final JOSEObjectType AT_JWT = new JOSEObjectType("at+jwt");
private boolean enabled; private boolean enabled;
private SpringBasedIdentityServiceFacadeFactory factory; private SpringBasedIdentityServiceFacadeFactory factory;
@@ -554,12 +560,20 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
static class JwtDecoderProvider static class JwtDecoderProvider
{ {
private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.RS256; private static final SignatureAlgorithm DEFAULT_SIGNATURE_ALGORITHM = SignatureAlgorithm.RS256;
private final IdentityServiceConfig config; private final IdentityServiceConfig config;
private final Set<SignatureAlgorithm> signatureAlgorithms;
JwtDecoderProvider(IdentityServiceConfig config) JwtDecoderProvider(IdentityServiceConfig config)
{ {
this.config = requireNonNull(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) public JwtDecoder createJwtDecoder(RestOperations rest, ProviderDetails providerDetails)
@@ -587,13 +601,13 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
{ {
final RSAPublicKey publicKey = parsePublicKey(config.getRealmKey()); final RSAPublicKey publicKey = parsePublicKey(config.getRealmKey());
return NimbusJwtDecoder.withPublicKey(publicKey) return NimbusJwtDecoder.withPublicKey(publicKey)
.signatureAlgorithm(SIGNATURE_ALGORITHM) .signatureAlgorithm(DEFAULT_SIGNATURE_ALGORITHM)
.build(); .build();
} }
final String jwkSetUri = requireValidJwkSetUri(providerDetails); final String jwkSetUri = requireValidJwkSetUri(providerDetails);
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri) final NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder decoderBuilder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri);
.jwsAlgorithm(SIGNATURE_ALGORITHM) signatureAlgorithms.forEach(decoderBuilder::jwsAlgorithm);
return decoderBuilder
.restOperations(rest) .restOperations(rest)
.jwtProcessorCustomizer(this::reconfigureJWKSCache) .jwtProcessorCustomizer(this::reconfigureJWKSCache)
.build(); .build();
@@ -633,8 +647,11 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
resourceRetriever.get(), cache); resourceRetriever.get(), cache);
jwtProcessor.setJWSKeySelector(new JWSVerificationKeySelector<>( jwtProcessor.setJWSKeySelector(new JWSVerificationKeySelector<>(
JWSAlgorithm.parse(SIGNATURE_ALGORITHM.getName()), signatureAlgorithms.stream()
.map(signatureAlgorithm -> JWSAlgorithm.parse(signatureAlgorithm.getName()))
.collect(Collectors.toSet()),
cachingJWKSource)); cachingJWKSource));
jwtProcessor.setJWSTypeVerifier(new CustomJOSEObjectTypeVerifier(JOSEObjectType.JWT, AT_JWT));
} }
private OAuth2TokenValidator<Jwt> createJwtTokenValidator(ProviderDetails providerDetails) private OAuth2TokenValidator<Jwt> createJwtTokenValidator(ProviderDetails providerDetails)
@@ -759,7 +776,6 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
} }
} }
static class CustomClientHttpRequestFactory extends HttpComponentsClientHttpRequestFactory static class CustomClientHttpRequestFactory extends HttpComponentsClientHttpRequestFactory
{ {
CustomClientHttpRequestFactory(HttpClient httpClient) CustomClientHttpRequestFactory(HttpClient httpClient)
@@ -781,9 +797,22 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
} }
} }
static class CustomJOSEObjectTypeVerifier extends DefaultJOSEObjectTypeVerifier<SecurityContext>
{
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) private static boolean isDefined(String value)
{ {
return value != null && !value.isBlank(); return value != null && !value.isBlank();
} }
} }

View File

@@ -4,6 +4,8 @@
view:childName="app:messages"> view:childName="app:messages">
<view:aspects> <view:aspects>
<app:uifacets></app:uifacets> <app:uifacets></app:uifacets>
<sys:undeletable/>
<sys:unmovable/>
</view:aspects> </view:aspects>
<view:properties> <view:properties>
<cm:description>${spaces.messages.description}</cm:description> <cm:description>${spaces.messages.description}</cm:description>

View File

@@ -4,6 +4,8 @@
view:childName="app:webclient_extension"> view:childName="app:webclient_extension">
<view:aspects> <view:aspects>
<app:uifacets></app:uifacets> <app:uifacets></app:uifacets>
<sys:undeletable/>
<sys:unmovable/>
</view:aspects> </view:aspects>
<view:properties> <view:properties>
<cm:description>${spaces.web.client.extension.description}</cm:description> <cm:description>${spaces.web.client.extension.description}</cm:description>

View File

@@ -12,6 +12,10 @@
<cm:title>${spaces.rendition.rendering_actions.name}</cm:title> <cm:title>${spaces.rendition.rendering_actions.name}</cm:title>
<cm:description>${spaces.rendition.rendering_actions.description}</cm:description> <cm:description>${spaces.rendition.rendering_actions.description}</cm:description>
</view:properties> </view:properties>
<view:aspects>
<sys:undeletable/>
<sys:unmovable/>
</view:aspects>
</cm:folder> </cm:folder>
</view:view> </view:view>

View File

@@ -12,6 +12,10 @@
<cm:title>${spaces.replication.replication_actions.name}</cm:title> <cm:title>${spaces.replication.replication_actions.name}</cm:title>
<cm:description>${spaces.replication.replication_actions.description}</cm:description> <cm:description>${spaces.replication.replication_actions.description}</cm:description>
</view:properties> </view:properties>
<view:aspects>
<sys:undeletable/>
<sys:unmovable/>
</view:aspects>
</cm:folder> </cm:folder>
</view:view> </view:view>

View File

@@ -11,5 +11,9 @@
<cm:title>${spaces.actions.scheduled_actions.name}</cm:title> <cm:title>${spaces.actions.scheduled_actions.name}</cm:title>
<cm:description>${spaces.actions.scheduled_actions.description}</cm:description> <cm:description>${spaces.actions.scheduled_actions.description}</cm:description>
</view:properties> </view:properties>
<view:aspects>
<sys:undeletable/>
<sys:unmovable/>
</view:aspects>
</cm:folder> </cm:folder>
</view:view> </view:view>

View File

@@ -14,5 +14,9 @@
<cm:description>${spaces.solr_facets.root.description} <cm:description>${spaces.solr_facets.root.description}
</cm:description> </cm:description>
</view:properties> </view:properties>
<view:aspects>
<sys:undeletable/>
<sys:unmovable/>
</view:aspects>
</srft:facets> </srft:facets>
</view:view> </view:view>

View File

@@ -1,7 +1,7 @@
<view:view xmlns:view="http://www.alfresco.org/view/repository/1.0" <view:view xmlns:view="http://www.alfresco.org/view/repository/1.0"
xmlns:cm="http://www.alfresco.org/model/content/1.0" xmlns:cm="http://www.alfresco.org/model/content/1.0"
xmlns:app="http://www.alfresco.org/model/application/1.0" xmlns:app="http://www.alfresco.org/model/application/1.0"
xmlns:emailserver="http://www.alfresco.org/model/emailserver/1.0"> xmlns:emailserver="http://www.alfresco.org/model/emailserver/1.0" xmlns:sys="http://www.alfresco.org/model/system/1.0">
<!-- NOTE: all replaced properties referenced from repository.properties file must also be <!-- NOTE: all replaced properties referenced from repository.properties file must also be
mapped in the import-export-context.xml spacesStoreImporter/configuration section --> mapped in the import-export-context.xml spacesStoreImporter/configuration section -->
@@ -30,6 +30,10 @@
<app:icon>space-icon-default</app:icon> <app:icon>space-icon-default</app:icon>
<cm:title>${spaces.dictionary.name}</cm:title> <cm:title>${spaces.dictionary.name}</cm:title>
<cm:description>${spaces.dictionary.description}</cm:description> <cm:description>${spaces.dictionary.description}</cm:description>
<view:aspects>
<sys:undeletable/>
<sys:unmovable/>
</view:aspects>
<cm:contains> <cm:contains>
<cm:folder view:childName="${spaces.templates.childname}"> <cm:folder view:childName="${spaces.templates.childname}">
<app:uifacets/> <app:uifacets/>
@@ -37,6 +41,10 @@
<app:icon>space-icon-default</app:icon> <app:icon>space-icon-default</app:icon>
<cm:title>${spaces.templates.name}</cm:title> <cm:title>${spaces.templates.name}</cm:title>
<cm:description>${spaces.templates.description}</cm:description> <cm:description>${spaces.templates.description}</cm:description>
<view:aspects>
<sys:undeletable/>
<sys:unmovable/>
</view:aspects>
</cm:folder> </cm:folder>
<cm:folder <cm:folder
view:childName="${spaces.templates.content.childname}"> view:childName="${spaces.templates.content.childname}">
@@ -45,6 +53,10 @@
<app:icon>space-icon-default</app:icon> <app:icon>space-icon-default</app:icon>
<cm:title>${spaces.templates.content.name}</cm:title> <cm:title>${spaces.templates.content.name}</cm:title>
<cm:description>${spaces.templates.content.description}</cm:description> <cm:description>${spaces.templates.content.description}</cm:description>
<view:aspects>
<sys:undeletable/>
<sys:unmovable/>
</view:aspects>
</cm:folder> </cm:folder>
<cm:folder <cm:folder
view:childName="${spaces.templates.email.childname}"> view:childName="${spaces.templates.email.childname}">
@@ -53,6 +65,10 @@
<app:icon>space-icon-default</app:icon> <app:icon>space-icon-default</app:icon>
<cm:title>${spaces.templates.email.name}</cm:title> <cm:title>${spaces.templates.email.name}</cm:title>
<cm:description>${spaces.templates.email.description}</cm:description> <cm:description>${spaces.templates.email.description}</cm:description>
<view:aspects>
<sys:undeletable/>
<sys:unmovable/>
</view:aspects>
<cm:contains> <cm:contains>
<cm:folder <cm:folder
view:childName="${spaces.templates.email.invite.childname}"> view:childName="${spaces.templates.email.invite.childname}">
@@ -79,6 +95,10 @@
<app:icon>space-icon-default</app:icon> <app:icon>space-icon-default</app:icon>
<cm:title>${spaces.templates.rss.name}</cm:title> <cm:title>${spaces.templates.rss.name}</cm:title>
<cm:description>${spaces.templates.rss.description}</cm:description> <cm:description>${spaces.templates.rss.description}</cm:description>
<view:aspects>
<sys:undeletable/>
<sys:unmovable/>
</view:aspects>
</cm:folder> </cm:folder>
<cm:folder <cm:folder
view:childName="${spaces.savedsearches.childname}"> view:childName="${spaces.savedsearches.childname}">
@@ -93,6 +113,10 @@
<app:icon>space-icon-default</app:icon> <app:icon>space-icon-default</app:icon>
<cm:title>${spaces.savedsearches.name}</cm:title> <cm:title>${spaces.savedsearches.name}</cm:title>
<cm:description>${spaces.savedsearches.description}</cm:description> <cm:description>${spaces.savedsearches.description}</cm:description>
<view:aspects>
<sys:undeletable/>
<sys:unmovable/>
</view:aspects>
</cm:folder> </cm:folder>
<cm:folder view:childName="${spaces.scripts.childname}"> <cm:folder view:childName="${spaces.scripts.childname}">
<app:uifacets/> <app:uifacets/>
@@ -100,6 +124,10 @@
<app:icon>space-icon-default</app:icon> <app:icon>space-icon-default</app:icon>
<cm:title>${spaces.scripts.name}</cm:title> <cm:title>${spaces.scripts.name}</cm:title>
<cm:description>${spaces.scripts.description}</cm:description> <cm:description>${spaces.scripts.description}</cm:description>
<view:aspects>
<sys:undeletable/>
<sys:unmovable/>
</view:aspects>
</cm:folder> </cm:folder>
<cm:folder view:childName="${spaces.nodetemplates.childname}"> <cm:folder view:childName="${spaces.nodetemplates.childname}">
<app:uifacets/> <app:uifacets/>
@@ -107,6 +135,10 @@
<app:icon>space-icon-default</app:icon> <app:icon>space-icon-default</app:icon>
<cm:title>${spaces.nodeTemplatesSpace.name}</cm:title> <cm:title>${spaces.nodeTemplatesSpace.name}</cm:title>
<cm:description>${spaces.nodeTemplatesSpace.description}</cm:description> <cm:description>${spaces.nodeTemplatesSpace.description}</cm:description>
<view:aspects>
<sys:undeletable/>
<sys:unmovable/>
</view:aspects>
</cm:folder> </cm:folder>
<cm:folder view:childName="${spaces.smartfolders.childname}"> <cm:folder view:childName="${spaces.smartfolders.childname}">
<view:acl view:inherit="false"> <view:acl view:inherit="false">
@@ -120,6 +152,10 @@
<app:icon>space-icon-default</app:icon> <app:icon>space-icon-default</app:icon>
<cm:title>${spaces.smartfoldertemplates.name}</cm:title> <cm:title>${spaces.smartfoldertemplates.name}</cm:title>
<cm:description>${spaces.smartfoldertemplates.description}</cm:description> <cm:description>${spaces.smartfoldertemplates.description}</cm:description>
<view:aspects>
<sys:undeletable/>
<sys:unmovable/>
</view:aspects>
</cm:folder> </cm:folder>
<cm:folder view:childName="${spaces.smartdownloads.childname}"> <cm:folder view:childName="${spaces.smartdownloads.childname}">
<view:acl view:inherit="false"> <view:acl view:inherit="false">
@@ -133,6 +169,10 @@
<app:icon>space-icon-default</app:icon> <app:icon>space-icon-default</app:icon>
<cm:title>${spaces.smartdownloads.name}</cm:title> <cm:title>${spaces.smartdownloads.name}</cm:title>
<cm:description>${spaces.smartdownloads.description}</cm:description> <cm:description>${spaces.smartdownloads.description}</cm:description>
<view:aspects>
<sys:undeletable/>
<sys:unmovable/>
</view:aspects>
</cm:folder> </cm:folder>
</cm:contains> </cm:contains>
</cm:folder> </cm:folder>

View File

@@ -4,10 +4,13 @@
xmlns:view="http://www.alfresco.org/view/repository/1.0" xmlns:view="http://www.alfresco.org/view/repository/1.0"
xmlns:cm="http://www.alfresco.org/model/content/1.0" xmlns:cm="http://www.alfresco.org/model/content/1.0"
xmlns:app="http://www.alfresco.org/model/application/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">
<cm:folder view:childName="${spaces.transfers.childname}"> <cm:folder view:childName="${spaces.transfers.childname}">
<view:aspects> <view:aspects>
<sys:undeletable/>
<sys:unmovable/>
</view:aspects> </view:aspects>
<view:properties> <view:properties>
<cm:description>${spaces.transfers.description}</cm:description> <cm:description>${spaces.transfers.description}</cm:description>

View File

@@ -3,6 +3,8 @@
<cm:folder xmlns:alf="http://www.alfresco.org" xmlns:d="http://www.alfresco.org/model/dictionary/1.0" xmlns:view="http://www.alfresco.org/view/repository/1.0" xmlns:act="http://www.alfresco.org/model/action/1.0" xmlns:wf="http://www.alfresco.org/model/workflow/1.0" xmlns:app="http://www.alfresco.org/model/application/1.0" xmlns:ver="http://www.alfresco.org/model/versionstore/1.0" xmlns:usr="http://www.alfresco.org/model/user/1.0" xmlns:cm="http://www.alfresco.org/model/content/1.0" xmlns:sys="http://www.alfresco.org/model/system/1.0" xmlns:rule="http://www.alfresco.org/model/rule/1.0" xmlns:fm="http://www.alfresco.org/model/forum/1.0" xmlns:bpm="http://www.alfresco.org/model/bpm/1.0" xmlns:custom="custom.model" xmlns="" view:childName="cm:webscripts"> <cm:folder xmlns:alf="http://www.alfresco.org" xmlns:d="http://www.alfresco.org/model/dictionary/1.0" xmlns:view="http://www.alfresco.org/view/repository/1.0" xmlns:act="http://www.alfresco.org/model/action/1.0" xmlns:wf="http://www.alfresco.org/model/workflow/1.0" xmlns:app="http://www.alfresco.org/model/application/1.0" xmlns:ver="http://www.alfresco.org/model/versionstore/1.0" xmlns:usr="http://www.alfresco.org/model/user/1.0" xmlns:cm="http://www.alfresco.org/model/content/1.0" xmlns:sys="http://www.alfresco.org/model/system/1.0" xmlns:rule="http://www.alfresco.org/model/rule/1.0" xmlns:fm="http://www.alfresco.org/model/forum/1.0" xmlns:bpm="http://www.alfresco.org/model/bpm/1.0" xmlns:custom="custom.model" xmlns="" view:childName="cm:webscripts">
<view:aspects> <view:aspects>
<app:uifacets></app:uifacets> <app:uifacets></app:uifacets>
<sys:undeletable/>
<sys:unmovable/>
</view:aspects> </view:aspects>
<view:properties> <view:properties>
<cm:description>${webscripts.url_addressable_web_services}</cm:description> <cm:description>${webscripts.url_addressable_web_services}</cm:description>

View File

@@ -3,6 +3,8 @@
<cm:folder xmlns:alf="http://www.alfresco.org" xmlns:d="http://www.alfresco.org/model/dictionary/1.0" xmlns:view="http://www.alfresco.org/view/repository/1.0" xmlns:act="http://www.alfresco.org/model/action/1.0" xmlns:wf="http://www.alfresco.org/model/workflow/1.0" xmlns:app="http://www.alfresco.org/model/application/1.0" xmlns:ver="http://www.alfresco.org/model/versionstore/1.0" xmlns:usr="http://www.alfresco.org/model/user/1.0" xmlns:cm="http://www.alfresco.org/model/content/1.0" xmlns:sys="http://www.alfresco.org/model/system/1.0" xmlns:rule="http://www.alfresco.org/model/rule/1.0" xmlns:fm="http://www.alfresco.org/model/forum/1.0" xmlns:bpm="http://www.alfresco.org/model/bpm/1.0" xmlns:custom="custom.model" xmlns="" view:childName="cm:extensionwebscripts"> <cm:folder xmlns:alf="http://www.alfresco.org" xmlns:d="http://www.alfresco.org/model/dictionary/1.0" xmlns:view="http://www.alfresco.org/view/repository/1.0" xmlns:act="http://www.alfresco.org/model/action/1.0" xmlns:wf="http://www.alfresco.org/model/workflow/1.0" xmlns:app="http://www.alfresco.org/model/application/1.0" xmlns:ver="http://www.alfresco.org/model/versionstore/1.0" xmlns:usr="http://www.alfresco.org/model/user/1.0" xmlns:cm="http://www.alfresco.org/model/content/1.0" xmlns:sys="http://www.alfresco.org/model/system/1.0" xmlns:rule="http://www.alfresco.org/model/rule/1.0" xmlns:fm="http://www.alfresco.org/model/forum/1.0" xmlns:bpm="http://www.alfresco.org/model/bpm/1.0" xmlns:custom="custom.model" xmlns="" view:childName="cm:extensionwebscripts">
<view:aspects> <view:aspects>
<app:uifacets></app:uifacets> <app:uifacets></app:uifacets>
<sys:undeletable/>
<sys:unmovable/>
</view:aspects> </view:aspects>
<view:properties> <view:properties>
<cm:description>${webscriptsextentions.customized_web_scripts}</cm:description> <cm:description>${webscriptsextentions.customized_web_scripts}</cm:description>

View File

@@ -68,14 +68,6 @@
<property name="systemPaths"> <property name="systemPaths">
<list> <list>
<value>/${spaces.company_home.childname}</value> <value>/${spaces.company_home.childname}</value>
<value>/${spaces.company_home.childname}/${spaces.dictionary.childname}</value>
<value>/${spaces.company_home.childname}/${spaces.dictionary.childname}/${spaces.templates.childname}</value>
<value>/${spaces.company_home.childname}/${spaces.dictionary.childname}/${spaces.templates.content.childname}</value>
<value>/${spaces.company_home.childname}/${spaces.dictionary.childname}/${spaces.templates.email.childname}</value>
<value>/${spaces.company_home.childname}/${spaces.dictionary.childname}/${spaces.templates.rss.childname}</value>
<value>/${spaces.company_home.childname}/${spaces.dictionary.childname}/${spaces.scripts.childname}</value>
<value>/${spaces.company_home.childname}/${spaces.dictionary.childname}/${spaces.webscripts.childname}</value>
<value>/${spaces.company_home.childname}/${spaces.dictionary.childname}/${spaces.extension_webscripts.childname}</value>
</list> </list>
</property> </property>
</bean> </bean>

View File

@@ -155,6 +155,9 @@
<property name="adminConsoleRedirectPath"> <property name="adminConsoleRedirectPath">
<value>${identity-service.admin-console.redirect-path}</value> <value>${identity-service.admin-console.redirect-path}</value>
</property> </property>
<property name="signatureAlgorithms">
<value>${identity-service.signature-algorithms:RS256,PS256}</value>
</property>
</bean> </bean>
<!-- Enable control over mapping between request and user ID --> <!-- Enable control over mapping between request and user ID -->

View File

@@ -12,3 +12,4 @@ identity-service.resource=alfresco
identity-service.credentials.secret= identity-service.credentials.secret=
identity-service.public-client=true identity-service.public-client=true
identity-service.admin-console.redirect-path=/alfresco/s/admin/admin-communitysummary identity-service.admin-console.redirect-path=/alfresco/s/admin/admin-communitysummary
identity-service.signature-algorithms=RS256,PS256

View File

@@ -77,8 +77,7 @@ import org.junit.runners.Suite;
org.alfresco.repo.activities.feed.cleanup.FeedCleanerTestCaseSensitivity.class, org.alfresco.repo.activities.feed.cleanup.FeedCleanerTestCaseSensitivity.class,
org.alfresco.repo.activities.SiteActivityTestCaseInsensitivity.class, org.alfresco.repo.activities.SiteActivityTestCaseInsensitivity.class,
org.alfresco.repo.admin.registry.RegistryServiceImplTest.class, org.alfresco.repo.admin.registry.RegistryServiceImplTest.class,
org.alfresco.repo.bootstrap.DataDictionaryFolderTest.class, org.alfresco.repo.bootstrap.DataDictionaryFolderTest.class
org.alfresco.repo.action.executer.NodeSizeActionExecuterTest.class
}) })
public class AppContext01TestSuite public class AppContext01TestSuite
{ {

View File

@@ -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 <http://www.gnu.org/licenses/>.
* #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<ChildAssociationRef> 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<ChildAssociationRef> 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"));
}
});
}
}

View File

@@ -25,47 +25,71 @@
*/ */
package org.alfresco.repo.security.authentication.identityservice; 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.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.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.UUID; 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 com.nimbusds.openid.connect.sdk.claims.PersonClaims;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.JwtAudienceValidator; 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.JwtDecoderProvider;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.JwtIssuerValidator; import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.JwtIssuerValidator;
import org.junit.Test; 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.client.registration.ClientRegistration.ProviderDetails;
import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; 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.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.web.client.RestOperations;
public class IdentityServiceFacadeFactoryBeanTest public class IdentityServiceFacadeFactoryBeanTest
{ {
private static final String EXPECTED_ISSUER = "expected-issuer"; private static final String EXPECTED_ISSUER = "expected-issuer";
private static final String EXPECTED_AUDIENCE = "expected-audience"; 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 @Test
public void shouldCreateJwtDecoderWithoutIDSWhenPublicKeyIsProvided() public void shouldCreateJwtDecoderWithoutIDSWhenPublicKeyIsProvided()
{ {
final IdentityServiceConfig config = mock(IdentityServiceConfig.class); when(config.getRealmKey()).thenReturn(
when(config.getRealmKey()).thenReturn("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAve3MabX/rp3LbE7/zNqKxuid8WT7y4qSXsNaiPvl/OVbNWW/cu5td1VndItYhH6/gL7Z5W/r4MOeTlz/fOdXfjrRJou2f3UiPQwLV9RdOH3oS4/BUe+sviD8Q3eRfWBWWz3yw8f2YNtD4bMztIMMjqthvwdEEb9S9jbxxD0o71Bsrz/FwPi7HhSDA+Z/p01Hct8m4wx13ZlKRd4YjyC12FBmi9MSgsrFuWzyQHhHTeBDoALpfuiut3rhVxUtFmVTpy6p9vil7C5J5pok4MXPH0dJCyDNQz05ww5+fD+tfksIEpFeokRpN226F+P21oQVFUWwYIaXaFlG/hfvwmnlfQIDAQAB"); "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAve3MabX/rp3LbE7/zNqKxuid8WT7y4qSXsNaiPvl/OVbNWW/cu5td1VndItYhH6/gL7Z5W/r4MOeTlz/fOdXfjrRJou2f3UiPQwLV9RdOH3oS4/BUe+sviD8Q3eRfWBWWz3yw8f2YNtD4bMztIMMjqthvwdEEb9S9jbxxD0o71Bsrz/FwPi7HhSDA+Z/p01Hct8m4wx13ZlKRd4YjyC12FBmi9MSgsrFuWzyQHhHTeBDoALpfuiut3rhVxUtFmVTpy6p9vil7C5J5pok4MXPH0dJCyDNQz05ww5+fD+tfksIEpFeokRpN226F+P21oQVFUWwYIaXaFlG/hfvwmnlfQIDAQAB");
when(config.isClientIdValidationDisabled()).thenReturn(true); when(config.isClientIdValidationDisabled()).thenReturn(true);
final ProviderDetails providerDetails = mock(ProviderDetails.class);
when(providerDetails.getIssuerUri()).thenReturn("https://my.issuer"); when(providerDetails.getIssuerUri()).thenReturn("https://my.issuer");
final JwtDecoderProvider provider = new JwtDecoderProvider(config); final JwtDecoderProvider provider = new JwtDecoderProvider(config);
final JwtDecoder decoder = provider.createJwtDecoder(null, providerDetails); 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(); assertThat(decodedToken).isNotNull();
final Map<String, Object> claims = decodedToken.getClaims(); final Map<String, Object> claims = decodedToken.getClaims();
@@ -74,12 +98,92 @@ public class IdentityServiceFacadeFactoryBeanTest
.containsEntry(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME, "piotrek"); .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<String, Object> 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 @Test
public void shouldFailWithNotMatchingIssuerURIs() public void shouldFailWithNotMatchingIssuerURIs()
{ {
final JwtIssuerValidator issuerValidator = new JwtIssuerValidator(EXPECTED_ISSUER); 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).isNotNull();
assertThat(validationResult.hasErrors()).isTrue(); assertThat(validationResult.hasErrors()).isTrue();
assertThat(validationResult.getErrors()).hasSize(1); assertThat(validationResult.getErrors()).hasSize(1);
@@ -173,6 +277,26 @@ public class IdentityServiceFacadeFactoryBeanTest
assertThat(validationResult.getErrors()).isEmpty(); 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) private Jwt tokenWithIssuer(String issuer)
{ {
return Jwt.withTokenValue(UUID.randomUUID().toString()) return Jwt.withTokenValue(UUID.randomUUID().toString())