Monday, 29 December 2008

Spring Security with controllers - part 2

Turns out applying Spring Security on annotated controllers is working perfectly fine and what's also important, is also happy to work with mixed (old- and new-style) controllers.

First step is to add new controller class that looks like this:


@Controller
@RequestMapping(value = "/s2/*")
public class SimpleController2 {

@Autowired
private Command command;

@RequestMapping(method = RequestMethod.GET)
@Secured("administrator")
@Permissioned(action = "View", resource = "Video")
public ModelAndView doSomething2(HttpServletRequest request,
HttpServletResponse response) {
System.out.println("Doing something 2");
command.doSomething();
return new ModelAndView(new RedirectView(
"http://www.google.com/search?q=done+something"));

}
}


Then add the following into Spring context file:

<context:annotation-config />
<bean
class="org.springframework.web.servlet.mvc.annotation.
DefaultAnnotationHandlerMapping" />
<bean id="simpleController2" class="controllers.SimpleController2" />


And that's it really. URLs with /s1 should go to SimpleController and SimpleController2 will take care about /s2 - important thing is that the security interception will happen already on the controller level which you can easily observe by removing the annotations from commands - you will still get access denied error.

Tuesday, 23 December 2008

Spring Security - part 1

Probably most of us - developers - at least once in their life (and some "lucky" ones many more times than that...) spend long minutes or even hours trying to solve a problem. Usually in the end you find such a trivial mistake or misconception that it becomes almost physically painful to see how much time you lost. One of those situations became an inspiration for the first entry on this blog.

In my current project we decided to use Spring security to replace somewhat hacky authorization we use now. All controllers inherit from AbstractController, that overrides handleRequestInternal:

@Override
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
String methodName = getMethodNameResolver().getHandlerMethodName(request);
Method method = this.getClass().getMethod(methodName, HttpServletRequest.class, HttpServletResponse.class);
Permissioned annotation = method.getAnnotation(Permissioned.class);
if (annotation != null) {
String action = annotation.action();
String resource = annotation.resource();
String userName = request.getHeader("USERNAME");
if (!userService.isAllowed(userName, resource, action)) {
throw new ControllerAccessDenied("User not allowed");
}
}
return invokeNamedMethod(methodName, request, response);
} catch (NoSuchRequestHandlingMethodException ex) {
return handleNoSuchRequestHandlingMethod(ex, request, response);
}
}


It is of course not a true code taken from the project but gives you the idea of the approach. All methods on the controller that are supposed to be secured are annotated like this:

@Secured("administrator")
@Permissioned(action = "View", resource = "Video")
public ModelAndView doSomething(HttpServletRequest request, HttpServletResponse response) {
command.doSomething();
return new ModelAndView(...);
}

This is the authorization part. As for authentication, it is handled externally and at the point of reaching the controller we only care about the USERNAME header.

Implementing similar thing using spring security seemed relatively easy. The plan was to add @Secured annotation where our @Permissioned was used and then handle the logic in a voter.

First we needed to add standard stuff to web.xml:

<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>


Then we created security-context.xml with the following contents:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-2.0.4.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd">

<security:global-method-security secured-annotations="enabled" access-decision-manager-ref="accessDecisionManager" />

<bean id="accessDecisionManager" class="org.springframework.security.vote.UnanimousBased">
<property name="decisionVoters">
<list>
<ref bean="simpleVoter" />
</list>
</property>
<property name="allowIfAllAbstainDecisions" value="true" />
</bean>

<security:http entry-point-ref="preAuthenticatedProcessingFilterEntryPoint">
<security:intercept-url pattern="/*" access="IS_AUTHENTICATED_FULLY" />
</security:http>

<bean id="preAuthenticatedProcessingFilterEntryPoint" class="org.springframework.security.ui.preauth.PreAuthenticatedProcessingFilterEntryPoint" />

<bean id="requestHeaderProcessingFilter" class="org.springframework.security.ui.preauth.header.RequestHeaderPreAuthenticatedProcessingFilter">
<security:custom-filter position="PRE_AUTH_FILTER" />
<property name="principalRequestHeader" value="USERNAME" />
<property name="authenticationManager" ref="authenticationManager" />
</bean>

<bean id="preAuthenticatedAuthenticationProvider" class="org.springframework.security.providers.preauth.PreAuthenticatedAuthenticationProvider">
<security:custom-authentication-provider />
<property name="preAuthenticatedUserDetailsService" ref="userService" />
</bean>

<bean id="simpleVoter" class="security.Voter" />
<security:authentication-manager alias="authenticationManager" />
</beans>


A little explanation: security:global-method-security makes spring pick up @Secured annotations and... well.. secure annottated methods. Access decision manager contains the list of voters that will make a decision on access, security:http is the usual stuff explained on 100 different websites. The only other interesting bit is the *PreAuthenticated* thing, which allows us to ignore the authentication phase assuming that we've done it in other place. Our userService only provides UserDetails object which will be injected into security context and then used e.g. by voters.

Controllers context looked like this:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="simpleController" class="controllers.SimpleController" />
<bean id="simpleCommand" class="commands.SimpleCommand" />
<bean id="userService" class="security.UserServiceImpl" />
</beans>

Additionally we needed dispatcher-servlet.xml which contained just urls mappings:

<bean id="simpleUrlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/s1/*">simpleController</prop>
</props>
</property>
</bean>


I've put @Security annotation on controller, removed the old overridden method. Tomcat started fine but I was still allowed to enter the method even though every call should be rejected.
(An hour later...) The reason for that was simple. We annotated controllers methods. Since they are written in the 'old style', extending spring controllers, every call reaches our code through handleRequest method. Thus it becomes an internal call and obviously in Spring Aop such calls are no longer intercepted (they don't go through a proxy). The solution in this case was simply to use spring security on the service level rather than controller. However, it does not completely replace our previous solution and therefore I will investigate if there is a workaround.