SEC-2059: Support Path Variables in Web Expressions

This commit is contained in:
Rob Winch 2015-08-20 15:14:04 -05:00
parent 5f328b1178
commit 6b05b298ff
8 changed files with 289 additions and 12 deletions

View File

@ -157,6 +157,65 @@ class InterceptUrlConfigTests extends AbstractHttpConfigTests {
}
def "SEC-2256: intercept-url supports path variables"() {
setup:
MockHttpServletRequest request = new MockHttpServletRequest(method:'GET')
MockHttpServletResponse response = new MockHttpServletResponse()
MockFilterChain chain = new MockFilterChain()
xml.http('use-expressions':true) {
'http-basic'()
'intercept-url'(pattern: '/user/{un}/**', access: "#un == authentication.name")
'intercept-url'(pattern: '/**', access: "denyAll")
}
createAppContext()
login(request, 'user', 'password')
when: 'user can access'
request.servletPath = '/user/user/abc'
springSecurityFilterChain.doFilter(request,response,chain)
then: 'The response is OK'
response.status == HttpServletResponse.SC_OK
when: 'user cannot access otheruser'
request = new MockHttpServletRequest(method:'GET', servletPath : '/user/otheruser/abc')
login(request, 'user', 'password')
chain.reset()
springSecurityFilterChain.doFilter(request,response,chain)
then: 'The response is OK'
response.status == HttpServletResponse.SC_FORBIDDEN
}
def "SEC-2256: intercept-url supports path variable type conversion"() {
setup:
MockHttpServletRequest request = new MockHttpServletRequest(method:'GET')
MockHttpServletResponse response = new MockHttpServletResponse()
MockFilterChain chain = new MockFilterChain()
xml.http('use-expressions':true) {
'http-basic'()
'intercept-url'(pattern: '/user/{un}/**', access: "@id.isOne(#un)")
'intercept-url'(pattern: '/**', access: "denyAll")
}
bean('id', Id)
createAppContext()
login(request, 'user', 'password')
when: 'can access id == 1'
request.servletPath = '/user/1/abc'
springSecurityFilterChain.doFilter(request,response,chain)
then: 'The response is OK'
response.status == HttpServletResponse.SC_OK
when: 'user cannot access 2'
request = new MockHttpServletRequest(method:'GET', servletPath : '/user/2/abc')
login(request, 'user', 'password')
chain.reset()
springSecurityFilterChain.doFilter(request,response,chain)
then: 'The response is OK'
response.status == HttpServletResponse.SC_FORBIDDEN
}
public static class Id {
public boolean isOne(int i) {
return i == 1;
}
}
def login(MockHttpServletRequest request, String username, String password) {
String toEncode = username + ':' + password
request.addHeader('Authorization','Basic ' + new String(Base64.encode(toEncode.getBytes('UTF-8'))))

View File

@ -370,8 +370,9 @@ This will give you access to the entire project history (including all releases
== What's new in Spring Security 4.1
* Meta Annotation Support
** <<test-method-meta-annotations>>
** <<method-security-meta-annotations>>
** <<test-method-meta-annotations,Test Meta Annotations>>
** <<method-security-meta-annotations,Method Security Meta Annotations>>
* <<el-access-web-path-variables,Path Variables in Web Security Expressions>>
=== What's new in Spring Security 4.0
@ -4569,7 +4570,10 @@ The base class for expression root objects is `SecurityExpressionRoot`. This pro
[[el-access-web]]
=== Web Security Expressions
To use expressions to secure individual URLs, you would first need to set the `use-expressions` attribute in the `<http>` element to `true`. Spring Security will then expect the `access` attributes of the `<intercept-url>` elements to contain Spring EL expressions. The expressions should evaluate to a boolean, defining whether access should be allowed or not. For example:
To use expressions to secure individual URLs, you would first need to set the `use-expressions` attribute in the `<http>` element to `true`.
Spring Security will then expect the `access` attributes of the `<intercept-url>` elements to contain Spring EL expressions.
The expressions should evaluate to a boolean, defining whether access should be allowed or not.
For example:
[source,xml]
----
@ -4582,9 +4586,92 @@ To use expressions to secure individual URLs, you would first need to set the `u
----
Here we have defined that the "admin" area of an application (defined by the URL pattern) should only be available to users who have the granted authority "admin" and whose IP address matches a local subnet. We've already seen the built-in `hasRole` expression in the previous section. The expression `hasIpAddress` is an additional built-in expression which is specific to web security. It is defined by the `WebSecurityExpressionRoot` class, an instance of which is used as the expression root object when evaluation web-access expressions. This object also directly exposed the `HttpServletRequest` object under the name `request` so you can invoke the request directly in an expressio
If expressions are being used, a `WebExpressionVoter` will be added to the `AccessDecisionManager` which is used by the namespace. So if you aren't using the namespace and want to use expressions, you will have to add one of these to your configuration.
Here we have defined that the "admin" area of an application (defined by the URL pattern) should only be available to users who have the granted authority "admin" and whose IP address matches a local subnet.
We've already seen the built-in `hasRole` expression in the previous section.
The expression `hasIpAddress` is an additional built-in expression which is specific to web security.
It is defined by the `WebSecurityExpressionRoot` class, an instance of which is used as the expression root object when evaluation web-access expressions.
This object also directly exposed the `HttpServletRequest` object under the name `request` so you can invoke the request directly in an expression.
If expressions are being used, a `WebExpressionVoter` will be added to the `AccessDecisionManager` which is used by the namespace.
So if you aren't using the namespace and want to use expressions, you will have to add one of these to your configuration.
[[el-access-web-beans]]
==== Referring to Beans in Web Security Expressions
If you wish to extend the expressions that are available, you can easily refer to any Spring Bean you expose.
For example, assumming you have a Bean with the name of `webSecurity` that contains the following method signature:
[source,java]
----
public class WebSecurity {
public boolean check(Authentication authentication, HttpServletRequest request) {
...
}
}
----
You could refer to the method using:
[source,xml]
----
<http>
<intercept-url pattern="/user/**"
access="@webSecurity.check(authentication,request)"/>
...
</http>
----
or in Java configuration
[source,java]
----
http
.authorizeUrls()
.antMatchers("/user/**").access("@webSecurity.check(authentication,request)")
...
----
[[el-access-web-path-variables]]
==== Path Variables in Web Security Expressions
At times it is nice to be able to refer to path variables within a URL.
For example, consider a RESTful application that looks up a user by id from the URL path in the format `/user/{userId}`.
You can easily refer to the path variable by placing it in the pattern.
For example, if you had a Bean with the name of `webSecurity` that contains the following method signature:
[source,java]
----
public class WebSecurity {
public boolean checkUserId(Authentication authentication, int id) {
...
}
}
----
You could refer to the method using:
[source,xml]
----
<http>
<intercept-url pattern="/user/{userId}/**"
access="@webSecurity.checkUserId(authentication,userId)"/>
...
</http>
----
or in Java configuration
[source,java]
----
http
.authorizeUrls()
.antMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,userId)")
...
----
In both configurations URLs that match would pass in the path variable (and convert it) into checkUserId method.
For example, if the URL were `/user/123/resource`, then the id passed in would be `123`.
=== Method Security Expressions
Method security is a bit more complicated than a simple allow or deny rule. Spring Security 3.0 introduced some new annotations in order to allow comprehensive support for the use of expressions.

View File

@ -13,6 +13,7 @@ import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
@ -53,9 +54,14 @@ public final class ExpressionBasedFilterInvocationSecurityMetadataSource extends
.getAttribute();
logger.debug("Adding web access control expression '" + expression
+ "', for " + request);
String pattern = null;
if(request instanceof AntPathRequestMatcher) {
pattern = ((AntPathRequestMatcher)request).getPattern();
}
try {
attributes.add(new WebExpressionConfigAttribute(parser
.parseExpression(expression)));
.parseExpression(expression), new PathVariableSecurityEvaluationContextPostProcessor(pattern)));
}
catch (ParseException e) {
throw new IllegalArgumentException("Failed to parse expression '"

View File

@ -0,0 +1,61 @@
/*
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.access.expression;
import java.util.Map;
import org.springframework.expression.EvaluationContext;
import org.springframework.security.web.FilterInvocation;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
/**
* Exposes URI template variables as variables on the {@link EvaluationContext}.
* For example, the pattern "/user/{username}/**" would expose a variable named
* username based on the current URI.
*
* <p>
* NOTE: This API is intentionally kept package scope as it may change in the future. It may be nice to allow users to augment expressions and queries
* </p>
*
* @author Rob Winch
* @since 4.1
*/
class PathVariableSecurityEvaluationContextPostProcessor implements SecurityEvaluationContextPostProcessor<FilterInvocation> {
private final PathMatcher matcher = new AntPathMatcher();
private final String antPattern;
/**
* Creates a new instance.
*
* @param antPattern the ant pattern that may have template variables (i.e. "/user/{username}/**)
*/
public PathVariableSecurityEvaluationContextPostProcessor(String antPattern) {
this.antPattern = antPattern;
}
public EvaluationContext postProcess(EvaluationContext context, FilterInvocation invocation) {
if(antPattern == null) {
return context;
}
Map<String, String> variables = matcher.extractUriTemplateVariables(antPattern, invocation.getRequestUrl());
for(Map.Entry<String, String> entry : variables.entrySet()) {
context.setVariable(entry.getKey(), entry.getValue());
}
return context;
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.access.expression;
import org.springframework.expression.EvaluationContext;
/**
*
/**
* Allows post processing the {@link EvaluationContext}
*
* <p>
* This API is intentionally kept package scope as it may evolve over time.
* </p>
*
* @author Rob Winch
* @since 4.1
* @param <I> the invocation to use for post processing
*/
interface SecurityEvaluationContextPostProcessor<I> {
/**
* Allows post processing of the {@link EvaluationContext}. Implementations
* may return a new instance of {@link EvaluationContext} or modify the
* {@link EvaluationContext} that was passed in.
*
* @param context
* the original {@link EvaluationContext}
* @param invocation
* the security invocation object (i.e. FilterInvocation)
* @return the upated context.
*/
EvaluationContext postProcess(EvaluationContext context, I invocation);
}

View File

@ -1,7 +1,9 @@
package org.springframework.security.web.access.expression;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.web.FilterInvocation;
/**
* Simple expression configuration attribute for use in web request authorizations.
@ -9,17 +11,23 @@ import org.springframework.security.access.ConfigAttribute;
* @author Luke Taylor
* @since 3.0
*/
class WebExpressionConfigAttribute implements ConfigAttribute {
class WebExpressionConfigAttribute implements ConfigAttribute, SecurityEvaluationContextPostProcessor<FilterInvocation> {
private final Expression authorizeExpression;
private final SecurityEvaluationContextPostProcessor<FilterInvocation> postProcessor;
public WebExpressionConfigAttribute(Expression authorizeExpression) {
public WebExpressionConfigAttribute(Expression authorizeExpression, SecurityEvaluationContextPostProcessor<FilterInvocation> postProcessor) {
this.authorizeExpression = authorizeExpression;
this.postProcessor = postProcessor;
}
Expression getAuthorizeExpression() {
return authorizeExpression;
}
public EvaluationContext postProcess(EvaluationContext context, FilterInvocation fi) {
return postProcessor.postProcess(context, fi);
}
public String getAttribute() {
return null;
}

View File

@ -1,6 +1,7 @@
package org.springframework.security.web.access.expression;
import java.util.Collection;
import java.util.Map;
import org.springframework.expression.EvaluationContext;
import org.springframework.security.access.AccessDecisionVoter;
@ -9,6 +10,7 @@ import org.springframework.security.access.expression.ExpressionUtils;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.FilterInvocation;
import org.springframework.util.AntPathMatcher;
/**
* Voter which handles web authorisation decisions.
@ -32,6 +34,7 @@ public class WebExpressionVoter implements AccessDecisionVoter<FilterInvocation>
EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication,
fi);
ctx = weca.postProcess(ctx, fi);
return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED
: ACCESS_DENIED;

View File

@ -4,11 +4,12 @@ import static org.fest.assertions.Assertions.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.*;
import org.aopalliance.intercept.MethodInvocation;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.security.access.AccessDecisionVoter;
@ -35,7 +36,7 @@ public class WebExpressionVoterTests {
public void supportsWebConfigAttributeAndFilterInvocation() throws Exception {
WebExpressionVoter voter = new WebExpressionVoter();
assertTrue(voter
.supports(new WebExpressionConfigAttribute(mock(Expression.class))));
.supports(new WebExpressionConfigAttribute(mock(Expression.class), mock(SecurityEvaluationContextPostProcessor.class))));
assertTrue(voter.supports(FilterInvocation.class));
assertFalse(voter.supports(MethodInvocation.class));
@ -54,7 +55,13 @@ public class WebExpressionVoterTests {
public void grantsAccessIfExpressionIsTrueDeniesIfFalse() {
WebExpressionVoter voter = new WebExpressionVoter();
Expression ex = mock(Expression.class);
WebExpressionConfigAttribute weca = new WebExpressionConfigAttribute(ex);
SecurityEvaluationContextPostProcessor postProcessor = mock(SecurityEvaluationContextPostProcessor.class);
when(postProcessor.postProcess(any(EvaluationContext.class), any(FilterInvocation.class))).thenAnswer(new Answer<EvaluationContext>() {
public EvaluationContext answer(InvocationOnMock invocation) throws Throwable {
return invocation.getArgumentAt(0, EvaluationContext.class);
}
});
WebExpressionConfigAttribute weca = new WebExpressionConfigAttribute(ex,postProcessor);
EvaluationContext ctx = mock(EvaluationContext.class);
SecurityExpressionHandler eh = mock(SecurityExpressionHandler.class);
FilterInvocation fi = new FilterInvocation("/path", "GET");