Sunday, January 3, 2010

Spring Security – Part 3 - Role based Method Invocation

In the previous part we looked how to secure the URL calls. Now let us look into how to secure the method invocation. Let us again have a properties file (methods.properties) that contains the method names and the roles that can invoke the methods. For example,

com.test.service.MyService.delete*=ROLE_USER,ROLE_MANAGER
com.test.service.MyService.createAccount=ROLE_ADMIN

You can also have this mapping defined in database or LDAP server. For simplicity we will have the mapping defined in a properties file.

Let us now define interface (IMyService) and implementation (MyService)

public interface IMyService {
public void deleteUserPreferences (String userName);
public void deleteUserLogs(String userName);
public void createAccount(String userName);
}

Now based on the above configurations ROLE_USER, ROLE_MANAGER would only be able to invoke the methods deleteUserPreferences and deleteUserLogs. ROLE_ADMIN would only be able to invoke method createAccount.

Now you can write MyService implementing the above interface.

Method security in enforced using a MethodSecurityInterceptor, which secures
MethodInvocations. The interceptor uses a MethodDefinitionSource instance to obtain the configuration attributes that apply to a particular method invocation. Since we would be reading the methods names and the corresponding roles from properties file, we would either –

1. Define our own implementation of MethodSecurityMetadataSource and implement all methods.

2. Or we can write a class extending MapBasedMethodSecurityMetadataSource. MapBasedMethodDefinitionSource is used to store configuration attributes keyed by method names (which can be wildcarded) and will be used internally when the attributes are defined in the application context using the <intercept-methods> or <protect-point> elements.

We will go ahead with point 2 as we just need to read the properties file, create Map<String, List<ConfigAttribute>> and call MapBasedMethodDefinitionSource’s constructor passing this map.

So, let us first define a bean PropertyHolder that would read the properties file and create Map<String, List<ConfigAttribute>>.

<bean id="propHolder" class="com.test.common.PropertyHolder">
<property name="methodProperties">
<util:properties location="classpath:methods.properties" />
</property>
</bean>

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.util.StringUtils;

public class PropertyHolder {
private Properties methodProperties;

public void setMethodProperties(Properties methodProperties) {
this.methodProperties = methodProperties;
}

public Properties getMethodProperties() {
return methodProperties;
}

public Map<String, List<ConfigAttribute>> getMethodMap() {
Map<String, List<ConfigAttribute>> methodMap = new HashMap<String, List<ConfigAttribute>>();
for (Iterator iter = methodProperties.keySet().iterator(); iter
.hasNext();) {
String name = (String) iter.next();
String value = methodProperties.getProperty(name);

String[] tokens = StringUtils
.commaDelimitedListToStringArray(value);
List<ConfigAttribute> attributes = new ArrayList<ConfigAttribute>(
tokens.length);

for (String token : tokens) {
attributes.add(new SecurityConfig(token));
}

methodMap.put(name, attributes);
}

return methodMap;
}
}

Now let us configure a MethodSecurityIterceptor in our application context

<bean id="methodSecurityInterceptor" class="org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="securityMetadataSource" ref="mySecurityMetadataSource"/>
</bean>

We have already configured authenticationManager and accessDecisionManager in previous parts.

Let us now configure mySecurityMetadataSource.

<bean id=" mySecurityMetadataSource" class="com.test.common.MySecurityMetadataSource">
<constructor-arg name="propHolder" ref="propHolder"/>
</bean>


Now let us write the bean MySecurityMetadataSource that extends MapBasedMethodSecurityMetadataSource.

Import org.springframework.security.access.method.MapBasedMethodSecurityMetadataSource;

public class MySecurityMetadataSource extends
MapBasedMethodSecurityMetadataSource {

public MyMethodSecurityFilterNew(PropertyHolder propHolder) {
super(propHolder.getMethodMap());
}
}

Now define the beans MyService in application context that we want to secure and define another bean org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator to create a proxy for MyService object so that an authorization check may be applied for every invocation on the object.

<bean id=”myService” class=”com.test.service.MyService” />

<bean id="autoProxyCreator" class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="interceptorNames">
<list>
<value>methodSecurityInterceptor</value>
</list>
</property>
<property name="beanNames">
<list>
<value> myService</value>
</list>
</property>
</bean>

4 comments:

yen said...

I had refer your blog on method protection and followed accordingly.
The configuration is working, but it is redirecting browser to error page rather than access denied page.
Below are some extract from logs:
It look to me that the 'chain' still processed normally after the decision voter voted.

Am I missing out something?

[DEBUG] 12:06:37(AbstractSecurityInterceptor.java:beforeInvocation:191)
Secure object: ReflectiveMethodInvocation: public com.mcom.mes.dao.support.ResultPackage com.mcom.mes.service.impl.ContactManagerImpl.saveOrUpdateContact(com.mcom.mes.entity.Contact,boolean,java.util.List); target is of class [com.mcom.mes.service.impl.ContactManagerImpl]; Attributes: [ROLE_SUPERVISOR]

[DEBUG] 12:06:37(AbstractSecurityInterceptor.java:authenticateIfRequired:292)
Previously Authenticated: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@4364cc1d: Principal: com.mcom.mes.security.AccountUserDetails@1f60800: Username: test; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_Member; Password: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@fffe9938: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: A9AF5F5739D0407E2DA24C2BCEE37346; Granted Authorities: ROLE_Member

[DEBUG] 12:06:37(AffirmativeBased.java:decide:53)
Voter: org.springframework.security.access.vote.RoleVoter@d90727, returned: -1

[DEBUG] 12:06:37(AffirmativeBased.java:decide:53)
Voter: org.springframework.security.access.vote.AuthenticatedVoter@64a871, returned: 0

[DEBUG] 12:06:37(ExceptionTranslationFilter.java:doFilter:100)
Chain processed normally

[DEBUG] 12:06:37(SecurityContextPersistenceFilter.java:doFilter:89)
SecurityContextHolder now cleared, as request processing completed

yen said...

sorry, I forgot to mention that I do not get a AccessDeniedException, probably that's why the chain proceed normally. But I do not understand why it didn't raise the AccessDeniedException when the voters voted.

yen said...

After googled a while, found a similar problem in this URL: http://forum.springsource.org/archive/index.php/t-34086.html

So, the cause of problem was because I'm using Struts2, and the AccessDeniedException that was thrown has been hidden away by the global exception mapping I defined in struts.xml. So, it has wrap up the AccessDeniedException instead of throwing it to browser.

Unknown said...

Do you also have an example for spring acl?