/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2016 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.capability.declarative;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
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.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.transaction.TransactionalResourceHelper;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.security.AccessStatus;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import net.sf.acegisecurity.vote.AccessDecisionVoter;
/**
* Declarative capability implementation.
*
* @author Roy Wetherall
*/
public class DeclarativeCapability extends AbstractCapability
{
/** Logger */
protected static final Log LOGGER = LogFactory.getLog(DeclarativeCapability.class);
/** Required permissions */
protected List<String> permissions;
/** Map of conditions and expected evaluation result */
protected Map<String, Boolean> conditions;
/** List of file plan component kinds one of which must be satisfied */
protected List<String> kinds;
/** Capability to be evaluated against the target node reference */
protected Capability targetCapability;
/** Indicates whether to return an undetermined result */
protected boolean isUndetermined = false;
/**
* @param permissions permissions
*/
public void setPermissions(List<String> permissions)
{
this.permissions = permissions;
}
/**
* @param conditions conditions and expected values
*/
public void setConditions(Map<String, Boolean> conditions)
{
this.conditions = conditions;
}
/**
* @return {@link Map}<String, Boolean> conditions and expected values
*/
public Map<String, Boolean> getConditions()
{
return conditions;
}
/**
* @param kinds list of file plan component kinds
*/
public void setKinds(List<String> kinds)
{
this.kinds = kinds;
}
/**
* @return {@link List}<@link String> list of expected file plan component kinds
*/
public List<String> getKinds()
{
return kinds;
}
/**
* Helper method to set a single kind.
*
* @param kind file plan component kind
*/
public void setKind(String kind)
{
this.kinds = Collections.singletonList(kind);
}
/**
* Sets whether the capability will return an undetermined result when evaluating permissions
* for a single node reference or not. The default is to return grant.
*
* @param isUndetermined true if undetermined result, false otherwise
*/
public void setUndetermined(boolean isUndetermined)
{
this.isUndetermined = isUndetermined;
}
/**
* @return
*/
public boolean isUndetermined()
{
return isUndetermined;
}
/**
* Helper @see #setPermissions(List)
*
* @param permission permission
*/
public void setPermission(String permission)
{
List<String> permissions = new ArrayList<String>(1);
permissions.add(permission);
this.permissions = permissions;
}
/**
* @param targetCapability target capability
*/
public void setTargetCapability(Capability targetCapability)
{
this.targetCapability = targetCapability;
}
/**
* Check the permissions passed.
*
* @param nodeRef node reference
* @return boolean true if the permissions are present, false otherwise
*/
protected boolean checkPermissionsImpl(NodeRef nodeRef, String ... permissions)
{
boolean result = true;
NodeRef filePlan = getFilePlanService().getFilePlan(nodeRef);
if(filePlan == null)
{
return result;
}
for (String permission : permissions)
{
if (permissionService.hasPermission(filePlan, permission) != AccessStatus.ALLOWED)
{
result = false;
break;
}
}
return result;
}
/**
* Checks the permissions required for the capability.
*
* @param nodeRef
* @return
*/
protected boolean checkPermissions(NodeRef nodeRef)
{
boolean result = true;
if (permissions != null && !permissions.isEmpty())
{
result = checkPermissionsImpl(nodeRef, (String[])permissions.toArray(new String[permissions.size()]));
}
return result;
}
/**
* Checks the passed conditions.
*
* @param nodeRef
* @return
*/
protected boolean checkConditions(NodeRef nodeRef, Map<String, Boolean> conditions)
{
boolean result = true;
if (conditions != null)
{
for (Map.Entry<String, Boolean> entry : conditions.entrySet())
{
boolean expected = entry.getValue().booleanValue();
String conditionName = entry.getKey();
CapabilityCondition condition = (CapabilityCondition)applicationContext.getBean(conditionName);
if (condition == null)
{
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;
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("FAIL: Condition " + condition.getName() + " failed for capability " + getName() + " on nodeRef " + nodeRef.toString());
}
break;
}
}
}
return result;
}
/**
* Checks the set conditions.
*
* @param nodeRef node reference
* @return boolean true if conditions satisfied, false otherwise
*/
protected boolean checkConditions(NodeRef nodeRef)
{
return checkConditions(nodeRef, conditions);
}
/**
* Checks that the node ref is of the expected kind
*
* @param nodeRef
* @return
*/
protected boolean checkKinds(NodeRef nodeRef)
{
boolean result = false;
FilePlanComponentKind actualKind = getFilePlanService().getFilePlanComponentKind(nodeRef);
if (actualKind != null)
{
if (kinds != null && !kinds.isEmpty())
{
// need to check the actual file plan kind is in the list specified
for (String kindString : kinds)
{
FilePlanComponentKind kind = FilePlanComponentKind.valueOf(kindString);
if (actualKind.equals(kind))
{
result = true;
break;
}
}
}
else
{
// we don't have any specific kinds to check, so we pass since we have a file
// plan component in our hands
result = true;
}
}
return result;
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.capability.Capability#evaluate(org.alfresco.service.cmr.repository.NodeRef)
*/
@Override
public int evaluate(NodeRef nodeRef)
{
int result = AccessDecisionVoter.ACCESS_ABSTAIN;
// check transaction cache
Map<String, Integer> map = TransactionalResourceHelper.getMap("rm.declarativeCapability");
String key = getName() + "|" + nodeRef.toString() + "|" + AuthenticationUtil.getRunAsUser();
if (map.containsKey(key))
{
result = map.get(key);
}
else
{
// Check we are dealing with a file plan component
if (getFilePlanService().isFilePlanComponent(nodeRef))
{
// Check the kind of the object, the permissions and the conditions
if (checkKinds(nodeRef) && checkPermissions(nodeRef) && checkConditions(nodeRef))
{
// Opportunity for child implementations to extend
result = evaluateImpl(nodeRef);
}
else
{
result = AccessDecisionVoter.ACCESS_DENIED;
}
}
// Last chance for child implementations to veto/change the result
result = onEvaluate(nodeRef, result);
// log access denied to help with debug
if (LOGGER.isDebugEnabled() && AccessDecisionVoter.ACCESS_DENIED == result)
{
LOGGER.debug("Capability " + getName() + " returned an Access Denied result during evaluation of node " + nodeRef.toString());
}
map.put(key, result);
}
return result;
}
@Override
public int evaluate(NodeRef source, NodeRef target)
{
int result = evaluate(source);
if (targetCapability != null && result != AccessDecisionVoter.ACCESS_DENIED)
{
result = targetCapability.evaluate(target);
}
return result;
}
/**
* Default implementation. Given extending classes a hook point for further checks.
*
* @param nodeRef node reference
* @return
*/
protected int evaluateImpl(NodeRef nodeRef)
{
int result = AccessDecisionVoter.ACCESS_GRANTED;
if (isUndetermined)
{
result = AccessDecisionVoter.ACCESS_ABSTAIN;
}
return result;
}
/**
* Default implementation.
*
* Called before evaluate completes. The result returned overwrites the already discovered result.
* Provides a hook point for child implementations that wish to veto the result.
*
* @param nodeRef
* @param result
* @return
*/
protected int onEvaluate(NodeRef nodeRef, int result)
{
return result;
}
}