Spring Security offers an authentication replacement feature, often referred to as Run-As, that can replace the current user's authentication (and thus permissions) during a single secured object invocation. Using this feature makes sense when a backend system invoked during request processing requires different privileges than the current application.
For example, an application might want to expose a financial transaction log to the currently logged in user, but the backend system that provides it only permits this action to the members of a special "auditor" role. The application can not simply assign this role to the user as that would potentially permit them to execute other restricted actions. Instead, the user can be given this right exclusively for viewing their transaction log.
1 2 3 4 5 6 7 8 9 |
< bean id = "runAsManager" class = "org.springframework.security.access.intercept.RunAsManagerImpl" > < property name = "key" value = "my_run_as_key" /> </ bean > < bean id = "runAsAuthenticationProvider" class = "org.springframework.security.access.intercept.RunAsImplAuthenticationProvider" > < property name = "key" value = "my_run_as_key" /> </ bean > |
If an instance is registered, RunAsManager will be invoked by AbstractSecurityInterceptor for every intercepted object invocation for which the user has already been given access. If RunAsManager returns a token, this token will be used be used instead of the original one for the duration of the invocation, thus granting the user different privileges. There are two key points here. In order for the authentication replacement feature to do anything, the call has to actually be secured (and thus intercepted), and the user has to already have been granted access.
To register a RunAsManager instance with the method security interceptor, something similar to the following is needed:1 |
< global-method-security secured-annotations = "enabled" run-as-manager-ref = "runAsManager" /> |
The default implementation of RunAsManager (RunAsManagerImpl) will inspect the secured object's configuration and if it finds any attributes prefixed with RUN_AS_, it will create a token identical to the original, with the addition of one new GrantedAuthorty per RUN_AS_ attribute found. The new GrantedAuthority will be a role (prefixed by ROLE_ by default) named like the found attribute without the RUN_AS_ prefix.
So, if a user with a role ROLE_REGISTERED_USER invokes a method annotated with @Secured({"ROLE_REGISTERED_USER","RUN_AS_AUDITOR"}), e.g.
1 2 3 4 5 6 7 8 9 10 11 12 |
@Controller public class TransactionLogController { @Secured ({ "ROLE_REGISTERED_USER" , "RUN_AS_AUDITOR" }) //Authorities needed for method access and authorities added by RunAsManager prefixed with RUN_AS_ @RequestMapping (value = "/transactions" , method = RequestMethod.GET) //Spring MVC configuration. Not related to security @ResponseBody //Spring MVC configuration. Not related to security public List<Transaction> getTransactionLog(...) { ... //Invoke something in the backend requiring ROLE_AUDITOR { ... //User does not have ROLE_AUDITOR here } |
1 2 3 4 |
< authentication-manager alias = "authenticationManager" > < authentication-provider ref = "runAsAuthenticationProvider" /> ... other authentication-providers used by the application ... </ authentication-manager > |
Still, this setting will not work for methods secured by @PreAuthorize and @PostAuthorize annotations as their configuration attributes are differently evaluated (they are SpEL expressions and not a simple list or required authorities like with @Secured) and will not be recognized by RunAsManagerImpl. For this scenario to work, a custom RunAsManager implementation is required, as, at least at the time of writing, no applicable implementation is provided by Spring.
A custom RunAsManager implementation for use with @PreAuthorize/@PostAuthorize
A convenient implementation relying on a custom annotation is provided below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public class AnnotationDrivenRunAsManager extends RunAsManagerImpl { @Override public Authentication buildRunAs(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) { if (!(object instanceof ReflectiveMethodInvocation) || ((ReflectiveMethodInvocation)object).getMethod().getAnnotation(RunAsRole. class ) == null ) { return super .buildRunAs(authentication, object, attributes); } String roleName = ((ReflectiveMethodInvocation)object).getMethod().getAnnotation(RunAsRole. class ).value(); if (roleName == null || roleName.isEmpty()) { return null ; } GrantedAuthority runAsAuthority = new SimpleGrantedAuthority(roleName); List<GrantedAuthority> newAuthorities = new ArrayList<GrantedAuthority>(); // Add existing authorities newAuthorities.addAll(authentication.getAuthorities()); // Add the new run-as authority newAuthorities.add(runAsAuthority); return new RunAsUserToken(getKey(), authentication.getPrincipal(), authentication.getCredentials(), newAuthorities, authentication.getClass()); } } |
1 2 3 4 5 |
@Retention (RetentionPolicy.RUNTIME) @Target (ElementType.METHOD) public @interface RunAsRole { String value(); } |
1 2 3 4 |
< bean id = "runAsManager" class = "org.springframework.security.access.intercept.RunAsManagerImpl" > < property name = "key" value = "my_run_as_key" /> </ bean > |
1 2 3 |
< global-method-security pre-post-annotations = "enabled" run-as-manager-ref = "runAsManager" > < expression-handler ref = "expressionHandler" /> </ global-method-security > |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Controller public class TransactionLogController { @PreAuthorize ( "hasRole('ROLE_REGISTERED_USER')" ) //Authority needed to access the method @RunAsRole ( "ROLE_AUDITOR" ) //Authority added by RunAsManager @RequestMapping (value = "/transactions" , method = RequestMethod.GET) //Spring MVC configuration. Not related to security @ResponseBody //Spring MVC configuration. Not related to security public List<Transaction> getTransactionLog(...) { ... //Invoke something in the backend requiring ROLE_AUDITOR { ... //User does not have ROLE_AUDITOR here } |