/*
* #%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.security;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.alfresco.repo.security.permissions.impl.acegi.MethodSecurityInterceptor;
import org.alfresco.service.cmr.security.AccessStatus;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import net.sf.acegisecurity.AccessDeniedException;
import net.sf.acegisecurity.intercept.InterceptorStatusToken;
import net.sf.acegisecurity.vote.AccessDecisionVoter;
/**
* 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 final 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 generate 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>();
};
};
/**
* Indicates whether this is an RM security check or not
*/
private static final ThreadLocal<Boolean> IS_RM_SECURITY_CHECK = new ThreadLocal<Boolean>()
{
protected Boolean initialValue() {return false;};
};
/**
* Messages to display in error report.
*/
private static final ThreadLocal<List<String>> MESSAGES = new ThreadLocal<List<String>>()
{
protected List<String> initialValue() {return new ArrayList<String>();};
};
/**
* 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 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;
}
/**
* Indicates whether this is a RM security check or not
*
* @param newValue true if RM security check, false otherwise
*/
public static void isRMSecurityChecked(boolean newValue)
{
if (LOGGER.isDebugEnabled())
{
RMMethodSecurityInterceptor.IS_RM_SECURITY_CHECK.set(newValue);
}
}
/**
* Add a message to be displayed in the error report.
*
* @param message error message
*/
public static void addMessage(String message)
{
if (LOGGER.isDebugEnabled())
{
List<String> messages = RMMethodSecurityInterceptor.MESSAGES.get();
messages.add(message);
}
}
public static void addMessage(String message, Object ... params)
{
if (LOGGER.isDebugEnabled())
{
addMessage(MessageFormat.format(message, params));
}
}
/**
* 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)
{
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())
{
StringBuilder buffer = new StringBuilder("\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())
{
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();
RMMethodSecurityInterceptor.IS_RM_SECURITY_CHECK.remove();
RMMethodSecurityInterceptor.MESSAGES.remove();
// before invocation (where method security check takes place)
result = super.beforeInvocation(object);
}
catch (AccessDeniedException exception)
{
if (LOGGER.isDebugEnabled())
{
MethodInvocation mi = (MethodInvocation)object;
StringBuilder methodDetails = new StringBuilder("\n");
if (RMMethodSecurityInterceptor.IS_RM_SECURITY_CHECK.get())
{
methodDetails.append("RM method security check was performed.\n");
}
else
{
methodDetails.append("Standard DM method security check was performed.\n");
}
boolean first = true;
methodDetails.append("Failed on method: ").append(mi.getMethod().getName()).append("(");
for (Object arg : mi.getArguments())
{
if (first)
{
first = false;
}
else
{
methodDetails.append(", ");
}
if (arg != null)
{
methodDetails.append(arg.toString());
}
else
{
methodDetails.append("null");
}
}
methodDetails.append(")\n");
List<String> messages = RMMethodSecurityInterceptor.MESSAGES.get();
for (String message : messages)
{
methodDetails.append(message).append("\n");
}
String failureReport = getFailureReport();
if (failureReport == null)
{
// rethrow with additional information
throw new AccessDeniedException(exception.getMessage() + methodDetails, exception);
}
else
{
// rethrow with additional information
throw new AccessDeniedException(exception.getMessage() + methodDetails + getFailureReport(), exception);
}
}
else
{
throw 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;
}
}