From e3ae5cd0539bd8cfd36c1b76e70af083ee86d099 Mon Sep 17 00:00:00 2001 From: Roy Wetherall Date: Thu, 8 May 2014 06:01:02 +0000 Subject: [PATCH] RM-1340: Couldn't cutoff closed folder * includes a couple of fixes from the demo prep * override of MethodSecurityInterceptor to allow us to report detailed information when an AccessDenied exception is reported as the result of a capability evaluation failure. * integration tests git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/modules/recordsmanagement/HEAD@69801 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- .../extended-repository-context.xml | 4 +- .../org_alfresco_module_rm/log4j.properties | 6 + .../rm-action-context.xml | 58 ++--- .../rm-service-context.xml | 202 ++-------------- .../rm-ui-evaluators-context.xml | 1 - .../impl/CopyMoveLinkFileToBaseAction.java | 39 +-- .../capability/AbstractCapability.java | 30 ++- .../declarative/DeclarativeCapability.java | 11 +- .../disposition/DispositionServiceImpl.java | 30 ++- .../jscript/app/JSONConversionComponent.java | 74 +++--- .../aspect/VitalRecordDefinitionAspect.java | 28 +-- .../security/RMMethodSecurityInterceptor.java | 228 ++++++++++++++++++ .../integration/IntegrationTestSuite.java | 4 +- .../integration/disposition/CutOffTest.java | 119 +++++++++ .../disposition/DispositionTestSuite.java | 38 +++ 15 files changed, 571 insertions(+), 301 deletions(-) create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/security/RMMethodSecurityInterceptor.java create mode 100644 rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/disposition/CutOffTest.java create mode 100644 rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/disposition/DispositionTestSuite.java diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/extended-repository-context.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/extended-repository-context.xml index da0c183138..1d3b2b75c8 100644 --- a/rm-server/config/alfresco/module/org_alfresco_module_rm/extended-repository-context.xml +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/extended-repository-context.xml @@ -44,7 +44,7 @@ - + @@ -246,7 +246,7 @@ - + diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/log4j.properties b/rm-server/config/alfresco/module/org_alfresco_module_rm/log4j.properties index 408f1b593c..ebe4271aef 100644 --- a/rm-server/config/alfresco/module/org_alfresco_module_rm/log4j.properties +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/log4j.properties @@ -9,6 +9,12 @@ log4j.logger.org.alfresco.module.org_alfresco_module_rm.security.RMMethodSecurit # log4j.logger.org.alfresco.module.org_alfresco_module_rm.patch=info +# +# Set to 'debug' to see details of capability failures when AccessDenied is thrown. May be +# removed to enhance performance. +# +log4j.logger.org.alfresco.module.org_alfresco_module_rm.security.RMMethodSecurityInterceptor=debug + # # RM permission debug # diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-action-context.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-action-context.xml index 63d2541b26..09d8bc62e4 100644 --- a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-action-context.xml +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-action-context.xml @@ -156,7 +156,7 @@ - + @@ -171,7 +171,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_ALLOW @@ -194,7 +194,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.ApproveRecordsScheduledForCutoff @@ -219,7 +219,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.ApproveRecordsScheduledForCutoff @@ -244,7 +244,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.Destroy @@ -292,7 +292,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.ReOpenFolders @@ -319,7 +319,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.CloseFolders @@ -346,7 +346,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.CycleVitalRecords @@ -371,7 +371,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM.Declare.0 @@ -398,7 +398,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.UndeclareRecords @@ -425,7 +425,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.ExtendRetentionPeriodOrFreeze @@ -452,7 +452,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.Unfreeze @@ -496,7 +496,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.ViewUpdateReasonsForFreeze @@ -521,7 +521,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.PlanningReviewCycles @@ -546,7 +546,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.ManuallyChangeDispositionDates @@ -572,7 +572,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_ALLOW @@ -603,7 +603,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_ALLOW @@ -634,7 +634,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.AddModifyEventDates @@ -661,7 +661,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.AddModifyEventDates @@ -687,7 +687,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.AuthorizeAllTransfers @@ -733,7 +733,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.AuthorizeNominatedTransfers @@ -786,7 +786,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.CreateModifyDestroyFileplanMetadata @@ -813,7 +813,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.RejectRecords @@ -838,7 +838,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.FileUnfiledRecords @@ -866,7 +866,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.rmCopy @@ -894,7 +894,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.rmMove @@ -922,7 +922,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.LinkToRecords @@ -1041,7 +1041,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_ALLOW @@ -1075,7 +1075,7 @@ - + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_ALLOW diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml index 81614a1195..108e1cba6f 100644 --- a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml @@ -16,7 +16,7 @@ - + @@ -77,16 +77,7 @@ - - - - - - - - - - + - - - - - - - - - - + - - - - - - - - - - + - - - - - - - - - - + - - - - - - - - - - + - - - - - - - - - - + - - - - - - - - - - + - - - - - - - - - - + - - - - - - - - - - + - - - - - - - - - - + - - - - - - - - - - + - - - - - - - - - - + - - - - - - - - - - + - - - - - - - - - - + @@ -1210,16 +1084,7 @@ - - - - - - - - - - + - - - - - - - - - - + - - - - - - - - - - + - - - - - - - - - - + RECORD - RECORD_FOLDER diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/CopyMoveLinkFileToBaseAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/CopyMoveLinkFileToBaseAction.java index 3b54d93732..6ddceeaa95 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/CopyMoveLinkFileToBaseAction.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/CopyMoveLinkFileToBaseAction.java @@ -337,27 +337,32 @@ public abstract class CopyMoveLinkFileToBaseAction extends RMActionExecuterAbstr * @param lastAsFolder true if this is the last element of the pathe being created and it should be created as a folder. ignored if targetIsUnfiledRecords is true * @return */ - private NodeRef createChild(Action action, NodeRef parent, String childName, boolean targetisUnfiledRecords, boolean lastAsFolder) + private NodeRef createChild(final Action action, final NodeRef parent, final String childName, final boolean targetisUnfiledRecords, final boolean lastAsFolder) { - NodeRef child = null; - if(targetisUnfiledRecords) + return AuthenticationUtil.runAsSystem(new RunAsWork() { - child = this.fileFolderService.create(parent, childName, RecordsManagementModel.TYPE_UNFILED_RECORD_FOLDER).getNodeRef(); - } - else if(lastAsFolder) - { - child = recordFolderService.createRecordFolder(parent, childName); - } - else - { - if(RecordsManagementModel.TYPE_RECORD_FOLDER.equals(nodeService.getType(parent))) + public NodeRef doWork() throws Exception { - throw new AlfrescoRuntimeException("Unable to execute " + action.getActionDefinitionName() + " action, because the destination path could not be created."); + NodeRef child = null; + if(targetisUnfiledRecords) + { + child = fileFolderService.create(parent, childName, RecordsManagementModel.TYPE_UNFILED_RECORD_FOLDER).getNodeRef(); + } + else if(lastAsFolder) + { + child = recordFolderService.createRecordFolder(parent, childName); + } + else + { + if(RecordsManagementModel.TYPE_RECORD_FOLDER.equals(nodeService.getType(parent))) + { + throw new AlfrescoRuntimeException("Unable to execute " + action.getActionDefinitionName() + " action, because the destination path could not be created."); + } + child = filePlanService.createRecordCategory(parent, childName); + } + return child; } - child = this.filePlanService.createRecordCategory(parent, childName); - } - return child; - + }); } /** diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/AbstractCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/AbstractCapability.java index 9332435b09..16720029c9 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/AbstractCapability.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/AbstractCapability.java @@ -21,6 +21,7 @@ package org.alfresco.module.org_alfresco_module_rm.capability; import net.sf.acegisecurity.vote.AccessDecisionVoter; import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.security.RMMethodSecurityInterceptor; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.security.AccessStatus; import org.apache.commons.lang.StringUtils; @@ -196,21 +197,24 @@ public abstract class AbstractCapability extends RMSecurityCommon { String prefix = "hasPermissionRaw" + getName(); int result = getTransactionCache(prefix, nodeRef); - if (result != NOSET_VALUE) + if (result == NOSET_VALUE) { - return result; + if (checkRmRead(nodeRef) == AccessDecisionVoter.ACCESS_DENIED) + { + result = AccessDecisionVoter.ACCESS_DENIED; + } + else + { + result = hasPermissionImpl(nodeRef); + } + + result = setTransactionCache(prefix, nodeRef, result); } - - if (checkRmRead(nodeRef) == AccessDecisionVoter.ACCESS_DENIED) - { - result = AccessDecisionVoter.ACCESS_DENIED; - } - else - { - result = hasPermissionImpl(nodeRef); - } - - return setTransactionCache(prefix, nodeRef, result); + + // Log information about evaluated capability + RMMethodSecurityInterceptor.reportCapabilityStatus(getName(), result); + + return result; } /** diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/DeclarativeCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/DeclarativeCapability.java index a9487f6419..1cce4da0b8 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/DeclarativeCapability.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/DeclarativeCapability.java @@ -29,6 +29,7 @@ import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.module.org_alfresco_module_rm.capability.AbstractCapability; import org.alfresco.module.org_alfresco_module_rm.capability.Capability; import org.alfresco.module.org_alfresco_module_rm.fileplan.FilePlanComponentKind; +import org.alfresco.module.org_alfresco_module_rm.security.RMMethodSecurityInterceptor; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.security.AccessStatus; import org.apache.commons.logging.Log; @@ -209,7 +210,12 @@ public class DeclarativeCapability extends AbstractCapability throw new AlfrescoRuntimeException("Capability condition " + conditionName + " does not exist. Check the configuration of the capability " + name + "."); } + // determine the actual value boolean actual = condition.evaluate(nodeRef); + + // report information about condition (for exception reporting) + RMMethodSecurityInterceptor.reportCapabilityCondition(getName(), condition.getName(), expected, actual); + if (expected != actual) { result = false; @@ -218,11 +224,10 @@ public class DeclarativeCapability extends AbstractCapability { logger.debug("FAIL: Condition " + condition.getName() + " failed for capability " + getName() + " on nodeRef " + nodeRef.toString()); } - + break; } } - } return result; } @@ -277,7 +282,7 @@ public class DeclarativeCapability extends AbstractCapability } /** - * @see org.alfresco.module.org_alfresco_module_rm.capability.AbstractCapability#hasPermissionImpl(org.alfresco.service.cmr.repository.NodeRef) + * @see org.alfresco.module.org_alfresco_module_rm.capability.Capability#evaluate(org.alfresco.service.cmr.repository.NodeRef) */ @Override public int evaluate(NodeRef nodeRef) diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionServiceImpl.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionServiceImpl.java index e19872d443..fa6363f8fa 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionServiceImpl.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionServiceImpl.java @@ -971,10 +971,11 @@ public class DispositionServiceImpl extends ServiceBaseImpl // apply cut off applyCutoff(nodeRef); - if (recordFolderService.isRecordFolder(nodeRef)) + // close the record folder if it isn't already closed! + if (recordFolderService.isRecordFolder(nodeRef) && + !recordFolderService.isRecordFolderClosed(nodeRef)) { - // close folder (manually since we can't normall close a folder that is cut off!! - nodeService.setProperty(nodeRef, PROP_IS_CLOSED, true); + recordFolderService.closeRecordFolder(nodeRef); } } } @@ -984,11 +985,24 @@ public class DispositionServiceImpl extends ServiceBaseImpl } } - private void applyCutoff(NodeRef nodeRef) + /** + * Helper method to apply the cut off + * + * @param nodeRef node to cut off + */ + private void applyCutoff(final NodeRef nodeRef) { - // Apply the cut off aspect and set cut off date - Map cutOffProps = new HashMap(1); - cutOffProps.put(PROP_CUT_OFF_DATE, new Date()); - nodeService.addAspect(nodeRef, ASPECT_CUT_OFF, cutOffProps); + AuthenticationUtil.runAsSystem(new RunAsWork() + { + public Void doWork() throws Exception + { + // Apply the cut off aspect and set cut off date + Map cutOffProps = new HashMap(1); + cutOffProps.put(PROP_CUT_OFF_DATE, new Date()); + nodeService.addAspect(nodeRef, ASPECT_CUT_OFF, cutOffProps); + + return null; + } + }); } } diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/JSONConversionComponent.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/JSONConversionComponent.java index ca6711f209..8eba733142 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/JSONConversionComponent.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/JSONConversionComponent.java @@ -28,6 +28,8 @@ import org.alfresco.module.org_alfresco_module_rm.fileplan.FilePlanComponentKind import org.alfresco.module.org_alfresco_module_rm.fileplan.FilePlanService; import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; import org.alfresco.module.org_alfresco_module_rm.record.RecordService; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.model.FileInfo; import org.alfresco.service.cmr.repository.ChildAssociationRef; @@ -153,47 +155,57 @@ public class JSONConversionComponent extends org.alfresco.repo.jscript.app.JSONC * @param rootJSONObject root JSON object */ @SuppressWarnings("unchecked") - private void addInfo(FileInfo nodeInfo, JSONObject rootJSONObject) + private void addInfo(final FileInfo nodeInfo, JSONObject rootJSONObject) { String itemType = (String) rootJSONObject.get("type"); - QName itemTypeQName = QName.createQName(itemType, namespaceService); - if (dictionaryService.isSubClass(itemTypeQName, ContentModel.TYPE_CONTENT)) + final QName itemTypeQName = QName.createQName(itemType, namespaceService); + + NodeRef originatingLocation = AuthenticationUtil.runAsSystem(new RunAsWork() { - NodeRef nodeRef = nodeInfo.getNodeRef(); - List parentAssocs = nodeService.getParentAssocs(nodeRef); - - NodeRef originatingLocation = null; - for (ChildAssociationRef parent : parentAssocs) - { - // FIXME: What if there is more than a secondary parent? - if (!parent.isPrimary()) + public NodeRef doWork() throws Exception + { + NodeRef originatingLocation = null; + + if (dictionaryService.isSubClass(itemTypeQName, ContentModel.TYPE_CONTENT)) { - originatingLocation = parent.getParentRef(); - - // only consider the non-RM parent otherwise we can - // run into issues with frozen or transferring records - if (!nodeService.hasAspect(originatingLocation, RecordsManagementModel.ASPECT_FILE_PLAN_COMPONENT)) + NodeRef nodeRef = nodeInfo.getNodeRef(); + List parentAssocs = nodeService.getParentAssocs(nodeRef); + + for (ChildAssociationRef parent : parentAssocs) { - // assume we have found the correct in-place location - // FIXME when we support multiple in-place locations - break; + // FIXME: What if there is more than a secondary parent? + if (!parent.isPrimary()) + { + originatingLocation = parent.getParentRef(); + + // only consider the non-RM parent otherwise we can + // run into issues with frozen or transferring records + if (!nodeService.hasAspect(originatingLocation, RecordsManagementModel.ASPECT_FILE_PLAN_COMPONENT)) + { + // assume we have found the correct in-place location + // FIXME when we support multiple in-place locations + break; + } + } } } + + return originatingLocation; } - - if (originatingLocation != null) + }); + + if (originatingLocation != null) + { + String pathSeparator = "/"; + String displayPath = PathUtil.getDisplayPath(nodeService.getPath(originatingLocation), true); + String[] displayPathElements = displayPath.split(pathSeparator); + Object[] subPath = ArrayUtils.subarray(displayPathElements, 5, displayPathElements.length); + StringBuffer originatingLocationPath = new StringBuffer(); + for (int i = 0; i < subPath.length; i++) { - String pathSeparator = "/"; - String displayPath = PathUtil.getDisplayPath(nodeService.getPath(originatingLocation), true); - String[] displayPathElements = displayPath.split(pathSeparator); - Object[] subPath = ArrayUtils.subarray(displayPathElements, 5, displayPathElements.length); - StringBuffer originatingLocationPath = new StringBuffer(); - for (int i = 0; i < subPath.length; i++) - { - originatingLocationPath.append(pathSeparator).append(subPath[i]); - } - rootJSONObject.put("originatingLocationPath", originatingLocationPath.toString()); + originatingLocationPath.append(pathSeparator).append(subPath[i]); } + rootJSONObject.put("originatingLocationPath", originatingLocationPath.toString()); } } diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/rma/aspect/VitalRecordDefinitionAspect.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/rma/aspect/VitalRecordDefinitionAspect.java index 14128ed886..6f3f93197f 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/rma/aspect/VitalRecordDefinitionAspect.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/rma/aspect/VitalRecordDefinitionAspect.java @@ -29,6 +29,7 @@ import org.alfresco.repo.policy.Behaviour.NotificationFrequency; import org.alfresco.repo.policy.annotation.Behaviour; import org.alfresco.repo.policy.annotation.BehaviourBean; import org.alfresco.repo.policy.annotation.BehaviourKind; +import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.namespace.QName; @@ -78,27 +79,26 @@ public class VitalRecordDefinitionAspect extends BaseBehaviourBean kind = BehaviourKind.CLASS, notificationFrequency = NotificationFrequency.TRANSACTION_COMMIT ) - public void onUpdateProperties(final NodeRef nodeRef, Map before, Map after) + public void onUpdateProperties(final NodeRef nodeRef, final Map before, final Map after) { - if (nodeService.exists(nodeRef) && - nodeService.hasAspect(nodeRef, ASPECT_FILE_PLAN_COMPONENT)) + AuthenticationUtil.runAsSystem(new RunAsWork() { - // check that vital record definition has been changed in the first place - Map changedProps = PropertyMap.getChangedProperties(before, after); - if (changedProps.containsKey(PROP_VITAL_RECORD_INDICATOR) || - changedProps.containsKey(PROP_REVIEW_PERIOD)) + public Void doWork() throws Exception { - filePlanAuthenticationService.runAsRmAdmin(new RunAsWork() + if (nodeService.exists(nodeRef) && + nodeService.hasAspect(nodeRef, ASPECT_FILE_PLAN_COMPONENT)) { - @Override - public Void doWork() throws Exception + // check that vital record definition has been changed in the first place + Map changedProps = PropertyMap.getChangedProperties(before, after); + if (changedProps.containsKey(PROP_VITAL_RECORD_INDICATOR) || + changedProps.containsKey(PROP_REVIEW_PERIOD)) { recordsManagementActionService.executeRecordsManagementAction(nodeRef, "broadcastVitalRecordDefinition"); - return null; - }} - ); + } + } + return null; } - } + }); } } diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/security/RMMethodSecurityInterceptor.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/security/RMMethodSecurityInterceptor.java new file mode 100644 index 0000000000..dc2bab9c5b --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/security/RMMethodSecurityInterceptor.java @@ -0,0 +1,228 @@ + +package org.alfresco.module.org_alfresco_module_rm.security; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import net.sf.acegisecurity.AccessDeniedException; +import net.sf.acegisecurity.intercept.InterceptorStatusToken; +import net.sf.acegisecurity.intercept.method.aopalliance.MethodSecurityInterceptor; +import net.sf.acegisecurity.vote.AccessDecisionVoter; + +import org.alfresco.service.cmr.security.AccessStatus; +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Records Management Method Security Interceptor. + *

+ * Provides a way to record information about the capabilities being executed and report + * when an access denied exception is thrown. + * + * @author Roy Wetherall + * @since 2.2 + */ +public class RMMethodSecurityInterceptor extends MethodSecurityInterceptor +{ + /** logger */ + protected static Log logger = LogFactory.getLog(RMMethodSecurityInterceptor.class); + + /** + * Helper class to hold capability report information + */ + private static class CapabilityReport + { + public String name; + public AccessStatus status; + public Map conditions = new HashMap(); + } + + /** + * Helper method to translate vote to access status. + * + * @param vote vote + * @return {@link AccessStatus} access status + */ + private static AccessStatus translate(int vote) + { + switch (vote) + { + case AccessDecisionVoter.ACCESS_ABSTAIN: + return AccessStatus.UNDETERMINED; + case AccessDecisionVoter.ACCESS_GRANTED: + return AccessStatus.ALLOWED; + case AccessDecisionVoter.ACCESS_DENIED: + return AccessStatus.DENIED; + default: + return AccessStatus.UNDETERMINED; + } + } + + /** + * Current capability report details. + *

+ * Used to getnerate the capability error report. + */ + private static final ThreadLocal> capabilities = new ThreadLocal>() + { + @Override + protected Map initialValue() + { + return new HashMap(); + }; + }; + + /** + * Get capability report object from the thread local, creating one for + * the given capability name if one does not already exist. + * + * @param name capability name + * @return {@link CapabilityReport} object containing information about the capability + */ + private static final CapabilityReport getCapabilityReport(String name) + { + Map map = RMMethodSecurityInterceptor.capabilities.get(); + CapabilityReport capability = map.get(name); + if (capability == null) + { + capability = new CapabilityReport(); + capability.name = name; + + map.put(name, capability); + } + return capability; + } + + /** + * Report capability status. + * + * @param name capability name + * @param status capability status + */ + public static void reportCapabilityStatus(String name, int status) + { + if (logger.isDebugEnabled()) + { + CapabilityReport capability = getCapabilityReport(name); + capability.status = translate(status);; + } + } + + /** + * Report capability condition. + * + * @param name capability name + * @param conditionName capability condition name + * @param expected expected value + * @param actual actual value + */ + public static void reportCapabilityCondition(String name, String conditionName, boolean expected, boolean actual) + { + if (logger.isDebugEnabled()) + { + CapabilityReport capability = getCapabilityReport(name); + if (expected == false) + { + conditionName = "!" + conditionName; + } + capability.conditions.put(conditionName, (expected == actual)); + } + } + + /** + * Gets the failure report for the currently recorded capabilities. + * + * @return {@link String} capability error report + */ + public String getFailureReport() + { + String result = null; + + if (logger.isDebugEnabled()) + { + Collection capabilities = RMMethodSecurityInterceptor.capabilities.get().values(); + + if (!capabilities.isEmpty()) + { + StringBuffer buffer = new StringBuffer("\n"); + for (CapabilityReport capability : capabilities) + { + buffer.append(" ").append(capability.name).append(" (").append(capability.status).append(")\n"); + if (!capability.conditions.isEmpty()) + { + for (Map.Entry entry : capability.conditions.entrySet()) + { + buffer.append(" - ").append(entry.getKey()).append(" ("); + if (entry.getValue() == true) + { + buffer.append("passed"); + } + else + { + buffer.append("failed"); + } + buffer.append(")\n"); + } + } + } + + result = buffer.toString(); + } + } + + return result; + } + + /** + * @see net.sf.acegisecurity.intercept.AbstractSecurityInterceptor#beforeInvocation(java.lang.Object) + */ + @Override + protected InterceptorStatusToken beforeInvocation(Object object) + { + InterceptorStatusToken result = null; + try + { + // clear the capability report information + RMMethodSecurityInterceptor.capabilities.remove(); + + result = super.beforeInvocation(object); + } + catch (AccessDeniedException exception) + { + String failureReport = getFailureReport(); + if (failureReport == null) + { + throw exception; + } + else + { + // rethrow with additional information + throw new AccessDeniedException(exception.getMessage() + getFailureReport(), exception); + } + } + return result; + } + + /** + * @see net.sf.acegisecurity.intercept.method.aopalliance.MethodSecurityInterceptor#invoke(org.aopalliance.intercept.MethodInvocation) + */ + @Override + public Object invoke(MethodInvocation mi) throws Throwable + { + Object result = null; + InterceptorStatusToken token = beforeInvocation(mi); + + try + { + result = mi.proceed(); + } + finally + { + result = super.afterInvocation(token, result); + } + + return result; + } +} diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/IntegrationTestSuite.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/IntegrationTestSuite.java index c3cb55c707..4d4ebc637e 100755 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/IntegrationTestSuite.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/IntegrationTestSuite.java @@ -18,6 +18,7 @@ */ package org.alfresco.module.org_alfresco_module_rm.test.integration; +import org.alfresco.module.org_alfresco_module_rm.test.integration.disposition.DispositionTestSuite; import org.alfresco.module.org_alfresco_module_rm.test.integration.dod.DoD5015TestSuite; import org.alfresco.module.org_alfresco_module_rm.test.integration.event.EventTestSuite; import org.alfresco.module.org_alfresco_module_rm.test.integration.issue.IssueTestSuite; @@ -39,7 +40,8 @@ import org.junit.runners.Suite.SuiteClasses; DoD5015TestSuite.class, IssueTestSuite.class, EventTestSuite.class, - ReportTestSuite.class + ReportTestSuite.class, + DispositionTestSuite.class }) public class IntegrationTestSuite { diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/disposition/CutOffTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/disposition/CutOffTest.java new file mode 100644 index 0000000000..ec434698ff --- /dev/null +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/disposition/CutOffTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2005-2014 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +package org.alfresco.module.org_alfresco_module_rm.test.integration.disposition; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.action.impl.CompleteEventAction; +import org.alfresco.module.org_alfresco_module_rm.action.impl.CutOffAction; +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.service.cmr.repository.NodeRef; +import org.springframework.extensions.webscripts.GUID; + +/** + * Cut off integration tests. + * + * @author Roy Wetherall + * @since 2.2 + */ +public class CutOffTest extends BaseRMTestCase +{ + /** + * given we have a record folder that is eligible for cutoff ensure that the + * record can be cut off successfully. + */ + public void testCutOffRecordFolder() + { + doBehaviourDrivenTest(new BehaviourDrivenTest() + { + NodeRef recordFolder = null; + + public void given() + { + //create record folder + recordFolder = recordFolderService.createRecordFolder(rmContainer, GUID.generate()); + + // TODO add some records + + // make eligible for cutoff + Map params = new HashMap(1); + params.put(CompleteEventAction.PARAM_EVENT_NAME, CommonRMTestUtils.DEFAULT_EVENT_NAME); + rmActionService.executeRecordsManagementAction(recordFolder, CompleteEventAction.NAME, params); + } + + public void when() + { + // complete event + rmActionService.executeRecordsManagementAction(recordFolder, CutOffAction.NAME, null); + } + + public void then() + { + // ensure the record folder is cut off + assertTrue(dispositionService.isDisposableItemCutoff(recordFolder)); + } + }); + + } + + /** + * given that we have a closed record folder eligible for cut off ensure that it can + * be cut off. + *

+ * relates to https://issues.alfresco.com/jira/browse/RM-1340 + */ + public void testCutOffClosedRecordFolder() + { + doBehaviourDrivenTest(new BehaviourDrivenTest() + { + NodeRef recordFolder = null; + + public void given() + { + //create record folder + recordFolder = recordFolderService.createRecordFolder(rmContainer, GUID.generate()); + + // TODO add some records + + // make eligible for cutoff + Map params = new HashMap(1); + params.put(CompleteEventAction.PARAM_EVENT_NAME, CommonRMTestUtils.DEFAULT_EVENT_NAME); + rmActionService.executeRecordsManagementAction(recordFolder, CompleteEventAction.NAME, params); + + // close the record folder + recordFolderService.closeRecordFolder(recordFolder); + } + + public void when() + { + // complete event + rmActionService.executeRecordsManagementAction(recordFolder, CutOffAction.NAME, null); + } + + public void then() + { + // ensure the record folder is cut off + assertTrue(dispositionService.isDisposableItemCutoff(recordFolder)); + } + }); + } +} diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/disposition/DispositionTestSuite.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/disposition/DispositionTestSuite.java new file mode 100644 index 0000000000..6d5fc61f83 --- /dev/null +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/disposition/DispositionTestSuite.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2005-2014 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +package org.alfresco.module.org_alfresco_module_rm.test.integration.disposition; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; + +/** + * Disposition integration test suite + * + * @author Roy Wetherall + * @since 2.2 + */ +@RunWith(Suite.class) +@SuiteClasses( +{ + CutOffTest.class +}) +public class DispositionTestSuite +{ +}