diff --git a/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-action-context.xml b/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-action-context.xml index 0c2510f0a8..6042135cda 100644 --- a/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-action-context.xml +++ b/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-action-context.xml @@ -260,7 +260,8 @@ depends-on="rmDestroyRecordsScheduledForDestructionCapability"> - + + ${rm.ghosting.enabled} diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/DestroyAction.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/DestroyAction.java index 886f927851..0835e60768 100644 --- a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/DestroyAction.java +++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/DestroyAction.java @@ -36,6 +36,7 @@ import org.alfresco.module.org_alfresco_module_rm.capability.CapabilityService; import org.alfresco.module.org_alfresco_module_rm.content.ContentDestructionComponent; 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.record.InplaceRecordService; import org.alfresco.module.org_alfresco_module_rm.version.RecordableVersionService; import org.alfresco.service.cmr.action.Action; import org.alfresco.service.cmr.repository.NodeRef; @@ -62,6 +63,9 @@ public class DestroyAction extends RMDispositionActionExecuterAbstractBase /** Recordable version service */ private RecordableVersionService recordableVersionService; + + /** Inplace record service */ + private InplaceRecordService inplaceRecordService; /** Indicates if ghosting is enabled or not */ private boolean ghostingEnabled = true; @@ -89,6 +93,14 @@ public class DestroyAction extends RMDispositionActionExecuterAbstractBase { this.recordableVersionService = recordableVersionService; } + + /** + * @param inplaceRecordService inplace record service + */ + public void setInplaceRecordService(InplaceRecordService inplaceRecordService) + { + this.inplaceRecordService = inplaceRecordService; + } /** * @param ghostingEnabled true if ghosting is enabled, false otherwise @@ -171,6 +183,9 @@ public class DestroyAction extends RMDispositionActionExecuterAbstractBase // Add the ghosted aspect getNodeService().addAspect(record, ASPECT_GHOSTED, null); + + // Hide from inplace users to give the impression of destruction + inplaceRecordService.hideRecord(record); // destroy content contentDestructionComponent.destroyContent(record); diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/record/InplaceRecordServiceImpl.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/record/InplaceRecordServiceImpl.java index 964da2d9a6..02431a8ea9 100644 --- a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/record/InplaceRecordServiceImpl.java +++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/record/InplaceRecordServiceImpl.java @@ -103,22 +103,26 @@ public class InplaceRecordServiceImpl extends ServiceBaseImpl implements Inplace { // remove the child association NodeRef originatingLocation = (NodeRef) nodeService.getProperty(nodeRef, PROP_RECORD_ORIGINATING_LOCATION); - List parentAssocs = nodeService.getParentAssocs(nodeRef); - for (ChildAssociationRef childAssociationRef : parentAssocs) + + if (originatingLocation != null) { - if (!childAssociationRef.isPrimary() && - childAssociationRef.getParentRef().equals(originatingLocation) && - !nodeService.hasAspect(childAssociationRef.getChildRef(), ASPECT_PENDING_DELETE)) + List parentAssocs = nodeService.getParentAssocs(nodeRef); + for (ChildAssociationRef childAssociationRef : parentAssocs) { - nodeService.removeChildAssociation(childAssociationRef); - break; + if (!childAssociationRef.isPrimary() && + childAssociationRef.getParentRef().equals(originatingLocation) && + !nodeService.hasAspect(childAssociationRef.getChildRef(), ASPECT_PENDING_DELETE)) + { + nodeService.removeChildAssociation(childAssociationRef); + break; + } } + + // remove the extended security from the node + // this prevents the users from continuing to see the record in searchs and other linked locations + extendedSecurityService.remove(nodeRef); } - // remove the extended security from the node - // this prevents the users from continuing to see the record in searchs and other linked locations - extendedSecurityService.remove(nodeRef); - return null; } }); diff --git a/rm-community/rm-community-repo/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/record/InplaceRecordPermissionTest.java b/rm-community/rm-community-repo/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/record/InplaceRecordPermissionTest.java index 7b5478b3ca..61db117547 100644 --- a/rm-community/rm-community-repo/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/record/InplaceRecordPermissionTest.java +++ b/rm-community/rm-community-repo/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/record/InplaceRecordPermissionTest.java @@ -34,15 +34,20 @@ import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.alfresco.module.org_alfresco_module_rm.action.impl.CutOffAction; import org.alfresco.module.org_alfresco_module_rm.action.impl.DeclareRecordAction; +import org.alfresco.module.org_alfresco_module_rm.action.impl.DestroyAction; import org.alfresco.module.org_alfresco_module_rm.capability.Capability; import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; +import org.alfresco.module.org_alfresco_module_rm.role.FilePlanRoleService; 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.bdt.BehaviourTest; import org.alfresco.repo.security.permissions.AccessDeniedException; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.security.AccessStatus; import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.util.GUID; /** * In-place record permission integration test. @@ -322,26 +327,253 @@ public class InplaceRecordPermissionTest extends BaseRMTestCase } /** - * Given a record + * Given an inplace record ready for destruction * When it is destroyed * And it's metadata is maintained - * Then the inplace users still have access to the meta-data stub + * Then the inplace users will no longer see the record */ - // TODO + public void testDestroyedRecordInplacePermissions() + { + test() + .given() + + // Given that a record is declared by a collaborator + .as(dmCollaborator) + .perform(() -> recordService.createRecord(filePlan, dmDocument)) + .expect(true) + .from(() -> recordService.isRecord(dmDocument)) + .because("Document is a record.") + + // And it is filed into the file plan + // And eligible for destruction + .asAdmin() + .perform(() -> + { + // create record category and disposition schedule + NodeRef recordCategory = filePlanService.createRecordCategory(filePlan, GUID.generate()); + utils.createBasicDispositionSchedule(recordCategory, GUID.generate(), GUID.generate(), true, true); + + // create record folder and file record + NodeRef recordFolder = recordFolderService.createRecordFolder(recordCategory, GUID.generate()); + fileFolderService.move(dmDocument, recordFolder, null); + + // cut off record + rmActionService.executeRecordsManagementAction(dmDocument, DeclareRecordAction.NAME); + utils.completeEvent(dmDocument, CommonRMTestUtils.DEFAULT_EVENT_NAME); + rmActionService.executeRecordsManagementAction(dmDocument, CutOffAction.NAME); + }) + .expect("destroy") + .from(() -> dispositionService.getNextDispositionAction(dmDocument).getName()) + .because("The next action is destroy.") + .expect(true) + .from(() -> dispositionService.isNextDispositionActionEligible(dmDocument)) + .because("The next action is eligible.") + + // When the record is destroyed + .when(() -> rmActionService.executeRecordsManagementAction(dmDocument, DestroyAction.NAME)) + + .then() + .expect(true) + .from(() -> recordService.isMetadataStub(dmDocument)) + .because("The record has been destroyed and the meta-stub remains.") + + // Then the collaborator has no permissions or capabilities + .as(dmCollaborator) + .perform(() -> + checkInPlaceAccess(dmDocument, + AccessStatus.DENIED, // read record permission + AccessStatus.DENIED, // filing permission + AccessStatus.DENIED, // view record capability + AccessStatus.DENIED, // edit non record metadata capability + AccessStatus.DENIED)) // edit record metadata capability + + // And the consumer has no permissions or capabilities + .as(dmConsumer) + .perform(() -> + checkInPlaceAccess(dmDocument, + AccessStatus.DENIED, // read record permission + AccessStatus.DENIED, // filing permission + AccessStatus.DENIED, // view record capability + AccessStatus.DENIED, // edit non record metadata capability + AccessStatus.DENIED)) // edit record metadata capability + + // And a user that is not in the site has no permissions or capabilities + .as(userName) + .perform(() -> + checkInPlaceAccess(dmDocument, + AccessStatus.DENIED, // read record permission + AccessStatus.DENIED, // filing permission + AccessStatus.DENIED, // view record capability + AccessStatus.DENIED, // edit non record metadata capability + AccessStatus.DENIED)); // edit record metadata capability + } /** - * Given an inplace user with write access - * When a role is added to the inplace writers role - * Then then they receive that additional capability on the inplace record + * Given an inplace record + * And the collaborator has view and edit non-record capability + * And doesn't have edit record capability + * When we add edit record metadata capability to the extended writer role + * Then the collaborator now has edit record metadata capability */ - // TODO + public void testAddUserToRole() + { + test() + .given() + .as(dmCollaborator) + + // Given an inplace record + .perform(() -> recordService.createRecord(filePlan, dmDocument)) + .expect(true) + .from(() -> recordService.isRecord(dmDocument)) + .because("Document is a record.") + + // And the collaborator has view and edit non-record capability + // And doesn't have edit record capability + .perform(() -> + checkInPlaceAccess(dmDocument, + AccessStatus.ALLOWED, // read record permission + AccessStatus.ALLOWED, // filing permission + AccessStatus.ALLOWED, // view record capability + AccessStatus.ALLOWED, // edit non record metadata capability + AccessStatus.DENIED)) // edit record metadata capability + .when() + .asAdmin() + + // When we add edit record metadata capability to the extended writer role + .perform(() -> filePlanRoleService.updateRole(filePlan, + FilePlanRoleService.ROLE_EXTENDED_WRITERS, + "", + Stream + .of(viewRecordsCapability, editNonRecordMetadataCapability, editRecordMetadataCapability) + .collect(Collectors.toSet()))) + + .then() + .as(dmCollaborator) + + // Then the collaborator now has edit record metadata capability + .perform(() -> + checkInPlaceAccess(dmDocument, + AccessStatus.ALLOWED, // read record permission + AccessStatus.ALLOWED, // filing permission + AccessStatus.ALLOWED, // view record capability + AccessStatus.ALLOWED, // edit non record metadata capability + AccessStatus.ALLOWED)) // edit record metadata capability + ; + } - // hide? - // TODO + /** + * Given an inplace record + * When the record is hidden + * Then the collaborator has no access to the record + * And the consumer has no access to the record + * And a user that is not in the site has no permissions or capabilities + */ + public void testNoPermissionsAfterHide() + { + test() + .given() + .as(dmCollaborator) + + // Given an inplace record + .perform(() -> recordService.createRecord(filePlan, dmDocument)) + .expect(true) + .from(() -> recordService.isRecord(dmDocument)) + .because("Document is a record.") + .when() + .asAdmin() + + // When the record is hidden + .perform(() -> inplaceRecordService.hideRecord(dmDocument)) + + .then() + + // Then the collaborator has no access to the record + .as(dmCollaborator) + .perform(() -> + checkInPlaceAccess(dmDocument, + AccessStatus.DENIED, // read record permission + AccessStatus.DENIED, // filing permission + AccessStatus.DENIED, // view record capability + AccessStatus.DENIED, // edit non record metadata capability + AccessStatus.DENIED)) // edit record metadata capability + + // And the consumer has no access to the record + .as(dmConsumer) + .perform(() -> + checkInPlaceAccess(dmDocument, + AccessStatus.DENIED, // read record permission + AccessStatus.DENIED, // filing permission + AccessStatus.DENIED, // view record capability + AccessStatus.DENIED, // edit non record metadata capability + AccessStatus.DENIED)) // edit record metadata capability + + // And a user that is not in the site has no permissions or capabilities + .as(userName) + .perform(() -> + checkInPlaceAccess(dmDocument, + AccessStatus.DENIED, // read record permission + AccessStatus.DENIED, // filing permission + AccessStatus.DENIED, // view record capability + AccessStatus.DENIED, // edit non record metadata capability + AccessStatus.DENIED)); // edit record metadata capability + ; + } - // reject? - // TODO - - // user added to group ? - // TODO + /** + * Given an inplace record + * When the record is rejected + * Then the collaborator has no access to the record + * And the consumer has no access to the record + * And a user that is not in the site has no permissions or capabilities + */ + public void testNoPermissionsAfterReject() + { + test() + .given() + .as(dmCollaborator) + + // Given an inplace record + .perform(() -> recordService.createRecord(filePlan, dmDocument)) + .expect(true) + .from(() -> recordService.isRecord(dmDocument)) + .because("Document is a record.") + .when() + .asAdmin() + + // When the record is rejected + .perform(() -> recordService.rejectRecord(dmDocument, GUID.generate())) + + .then() + + // Then the collaborator has no access to the record + .as(dmCollaborator) + .perform(() -> + checkInPlaceAccess(dmDocument, + AccessStatus.DENIED, // read record permission + AccessStatus.DENIED, // filing permission + AccessStatus.DENIED, // view record capability + AccessStatus.DENIED, // edit non record metadata capability + AccessStatus.DENIED)) // edit record metadata capability + + // And the consumer has no access to the record + .as(dmConsumer) + .perform(() -> + checkInPlaceAccess(dmDocument, + AccessStatus.DENIED, // read record permission + AccessStatus.DENIED, // filing permission + AccessStatus.DENIED, // view record capability + AccessStatus.DENIED, // edit non record metadata capability + AccessStatus.DENIED)) // edit record metadata capability + + // And a user that is not in the site has no permissions or capabilities + .as(userName) + .perform(() -> + checkInPlaceAccess(dmDocument, + AccessStatus.DENIED, // read record permission + AccessStatus.DENIED, // filing permission + AccessStatus.DENIED, // view record capability + AccessStatus.DENIED, // edit non record metadata capability + AccessStatus.DENIED)); // edit record metadata capability + ; + } }