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
This commit is contained in:
Roy Wetherall
2014-05-08 06:01:02 +00:00
parent 7006a49d3a
commit b32d354529
15 changed files with 571 additions and 301 deletions

View File

@@ -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<NodeRef>()
{
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;
});
}
/**

View File

@@ -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;
}
/**

View File

@@ -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)

View File

@@ -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<QName, Serializable> cutOffProps = new HashMap<QName, Serializable>(1);
cutOffProps.put(PROP_CUT_OFF_DATE, new Date());
nodeService.addAspect(nodeRef, ASPECT_CUT_OFF, cutOffProps);
AuthenticationUtil.runAsSystem(new RunAsWork<Void>()
{
public Void doWork() throws Exception
{
// Apply the cut off aspect and set cut off date
Map<QName, Serializable> cutOffProps = new HashMap<QName, Serializable>(1);
cutOffProps.put(PROP_CUT_OFF_DATE, new Date());
nodeService.addAspect(nodeRef, ASPECT_CUT_OFF, cutOffProps);
return null;
}
});
}
}

View File

@@ -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 nodeRef = nodeInfo.getNodeRef();
List<ChildAssociationRef> 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<ChildAssociationRef> 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());
}
}

View File

@@ -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<QName, Serializable> before, Map<QName, Serializable> after)
public void onUpdateProperties(final NodeRef nodeRef, final Map<QName, Serializable> before, final Map<QName, Serializable> after)
{
if (nodeService.exists(nodeRef) &&
nodeService.hasAspect(nodeRef, ASPECT_FILE_PLAN_COMPONENT))
AuthenticationUtil.runAsSystem(new RunAsWork<Void>()
{
// check that vital record definition has been changed in the first place
Map<QName, Serializable> 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<Void>()
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<QName, Serializable> 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;
}
}
});
}
}

View File

@@ -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.
* <p>
* 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<String, Boolean> conditions = new HashMap<String, Boolean>();
}
/**
* 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.
* <p>
* Used to getnerate the capability error report.
*/
private static final ThreadLocal<Map<String, CapabilityReport>> capabilities = new ThreadLocal<Map<String, CapabilityReport>>()
{
@Override
protected Map<String, CapabilityReport> initialValue()
{
return new HashMap<String, CapabilityReport>();
};
};
/**
* 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<String, CapabilityReport> 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<CapabilityReport> 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<String, Boolean> 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;
}
}