Skip to content

Commit ef27402

Browse files
rwinchDave Syer
authored andcommitted
Created OAuth2ExpressionParser that automatically wraps a Spring Expression
... with a method that provides meaningful errors. This allows Spring Security OAuth to check for errors and throw Excepions when an error occurs while ensuring that the exception is only thrown when necessary. For example, if an expression has an or statment we do not want to throw the Exception if only part of the expression is false. Fixes spring-atticgh-41, Fixes spring-atticgh-118
1 parent 5819960 commit ef27402

File tree

8 files changed

+288
-105
lines changed

8 files changed

+288
-105
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2002-2011 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.security.oauth2.provider.expression;
17+
18+
import org.springframework.expression.Expression;
19+
import org.springframework.expression.ExpressionParser;
20+
import org.springframework.expression.ParseException;
21+
import org.springframework.expression.ParserContext;
22+
import org.springframework.util.Assert;
23+
24+
/**
25+
* <p>
26+
* A custom {@link ExpressionParser} that automatically wraps SpEL expression with
27+
* {@link OAuth2SecurityExpressionMethods#throwOnError(boolean)}. This makes it simple for users to specify an
28+
* expression and then have it verified (providing errors) after the result of the expression is known.
29+
* </p>
30+
* <p>
31+
* Note: The implication is that all expressions that are parsed must return a boolean result. This expectation is
32+
* already true since Spring Security expects the result to be a boolean.
33+
* </p>
34+
*
35+
* @author Rob Winch
36+
*
37+
*/
38+
final class OAuth2ExpressionParser implements ExpressionParser {
39+
private final ExpressionParser delegate;
40+
41+
public OAuth2ExpressionParser(ExpressionParser delegate) {
42+
Assert.notNull(delegate, "delegate cannot be null");
43+
this.delegate = delegate;
44+
}
45+
46+
public Expression parseExpression(String expressionString) throws ParseException {
47+
return delegate.parseExpression(wrapExpression(expressionString));
48+
}
49+
50+
public Expression parseExpression(String expressionString, ParserContext context) throws ParseException {
51+
return delegate.parseExpression(wrapExpression(expressionString), context);
52+
}
53+
54+
private String wrapExpression(String expressionString) {
55+
return "#oauth2.throwOnError(" + expressionString + ")";
56+
}
57+
}
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,36 @@
11
package org.springframework.security.oauth2.provider.expression;
22

33
import org.aopalliance.intercept.MethodInvocation;
4+
import org.springframework.expression.ExpressionParser;
45
import org.springframework.expression.spel.support.StandardEvaluationContext;
56
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
67
import org.springframework.security.core.Authentication;
78

89
/**
10+
* <p>
911
* A security expression handler that can handle default method security expressions plus the set provided by
1012
* {@link OAuth2SecurityExpressionMethods} using the variable oauth2 to access the methods. For example, the expression
1113
* <code>#oauth2.clientHasRole('ROLE_ADMIN')</code> would invoke {@link OAuth2SecurityExpressionMethods#clientHasRole}
12-
*
14+
* </p>
15+
* <p>
16+
* By default the {@link OAuth2ExpressionParser} is used. If this is undesirable one can inject their own
17+
* {@link ExpressionParser} using {@link #setExpressionParser(ExpressionParser)}.
18+
* </p>
19+
*
1320
* @author Dave Syer
1421
* @author Rob Winch
22+
* @see OAuth2ExpressionParser
1523
*/
1624
public class OAuth2MethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler {
17-
18-
private boolean throwExceptionOnInvalidScope = true;
1925

20-
/**
21-
* Flag to determine the behaviour on access denied if the reason is . If set then we throw an
22-
* {@link InvalidScopeException} instead of returning true. This is unconventional for an access decision because it
23-
* vetos the other voters in the chain, but it enables us to pass a message to the caller with information about the
24-
* required scope.
25-
*
26-
* @param throwException the flag to set (default true)
27-
*/
28-
public void setThrowExceptionOnInvalidScope(boolean throwException) {
29-
this.throwExceptionOnInvalidScope = throwException;
26+
public OAuth2MethodSecurityExpressionHandler() {
27+
setExpressionParser(new OAuth2ExpressionParser(getExpressionParser()));
3028
}
3129

3230
@Override
3331
public StandardEvaluationContext createEvaluationContextInternal(Authentication authentication, MethodInvocation mi) {
3432
StandardEvaluationContext ec = super.createEvaluationContextInternal(authentication, mi);
35-
ec.setVariable("oauth2", new OAuth2SecurityExpressionMethods(authentication, throwExceptionOnInvalidScope));
33+
ec.setVariable("oauth2", new OAuth2SecurityExpressionMethods(authentication));
3634
return ec;
3735
}
3836
}

spring-security-oauth2/src/main/java/org/springframework/security/oauth2/provider/expression/OAuth2SecurityExpressionMethods.java

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -37,25 +37,33 @@ public class OAuth2SecurityExpressionMethods {
3737

3838
private Set<String> missingScopes = new LinkedHashSet<String>();
3939

40-
private boolean throwExceptionOnInvalidScope;
41-
42-
public OAuth2SecurityExpressionMethods(Authentication authentication, boolean throwExceptionOnInvalidScope) {
40+
public OAuth2SecurityExpressionMethods(Authentication authentication) {
4341
this.authentication = authentication;
44-
this.throwExceptionOnInvalidScope = throwExceptionOnInvalidScope;
4542
}
4643

4744
/**
48-
* Check if any scope decisions have been denied in the current context and throw an exception if so. Example usage:
45+
* Check if any scope decisions have been denied in the current context and throw an exception if so. This method
46+
* automatically wraps any expressions when using {@link OAuth2MethodSecurityExpressionHandler} or
47+
* {@link OAuth2WebSecurityExpressionHandler}.
48+
*
49+
* OAuth2Example usage:
50+
*
51+
* <pre>
52+
* access = &quot;#oauth2.hasScope('read') or (#oauth2.hasScope('other') and hasRole('ROLE_USER'))&quot;
53+
* </pre>
54+
*
55+
* Will automatically be wrapped to ensure that explicit errors are propagated rather than a generic error when
56+
* returning false:
4957
*
5058
* <pre>
51-
* access = &quot;#oauth2.sufficientScope(#oauth2.hasScope('read') or (#oauth2.hasScope('other') and hasRole('ROLE_USER')))&quot;
59+
* access = &quot;#oauth2.throwOnError(#oauth2.hasScope('read') or (#oauth2.hasScope('other') and hasRole('ROLE_USER'))&quot;
5260
* </pre>
5361
*
5462
* @param decision the existing access decision
5563
* @return true if the OAuth2 token has one of these scopes
5664
* @throws InsufficientScopeException if the scope is invalid and we the flag is set to throw the exception
5765
*/
58-
public boolean sufficientScope(boolean decision) {
66+
public boolean throwOnError(boolean decision) {
5967
if (!decision && !missingScopes.isEmpty()) {
6068
throw new InsufficientScopeException("Insufficient scope for this resource", missingScopes);
6169
}
@@ -103,10 +111,8 @@ public boolean hasScope(String scope) {
103111
*/
104112
public boolean hasAnyScope(String... scopes) {
105113
boolean result = OAuth2ExpressionUtils.hasAnyScope(authentication, scopes);
106-
if (!result && throwExceptionOnInvalidScope) {
114+
if (!result) {
107115
missingScopes.addAll(Arrays.asList(scopes));
108-
Throwable failure = new InsufficientScopeException("Insufficient scope for this resource", missingScopes);
109-
throw new AccessDeniedException(failure.getMessage(), failure);
110116
}
111117
return result;
112118
}
@@ -142,10 +148,8 @@ public boolean hasScopeMatching(String scopeRegex) {
142148
public boolean hasAnyScopeMatching(String... scopesRegex) {
143149

144150
boolean result = OAuth2ExpressionUtils.hasAnyScopeMatching(authentication, scopesRegex);
145-
if (!result && throwExceptionOnInvalidScope) {
151+
if (!result) {
146152
missingScopes.addAll(Arrays.asList(scopesRegex));
147-
Throwable failure = new InsufficientScopeException("Insufficient scope for this resource", missingScopes);
148-
throw new AccessDeniedException(failure.getMessage(), failure);
149153
}
150154
return result;
151155
}
@@ -185,13 +189,4 @@ public boolean isUser() {
185189
public boolean isClient() {
186190
return OAuth2ExpressionUtils.isOAuthClientAuth(authentication);
187191
}
188-
189-
/**
190-
* A flag to indicate that an exception should be thrown if a scope decision is negative.
191-
*
192-
* @param throwExceptionOnInvalidScope flag value (default true)
193-
*/
194-
public void setThrowExceptionOnInvalidScope(boolean throwExceptionOnInvalidScope) {
195-
this.throwExceptionOnInvalidScope = throwExceptionOnInvalidScope;
196-
}
197192
}

spring-security-oauth2/src/main/java/org/springframework/security/oauth2/provider/expression/OAuth2WebSecurityExpressionHandler.java

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,40 +12,38 @@
1212
*/
1313
package org.springframework.security.oauth2.provider.expression;
1414

15+
import org.springframework.expression.ExpressionParser;
1516
import org.springframework.expression.spel.support.StandardEvaluationContext;
1617
import org.springframework.security.core.Authentication;
1718
import org.springframework.security.web.FilterInvocation;
1819
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
1920

2021
/**
22+
* <p>
2123
* A security expression handler that can handle default web security expressions plus the set provided by
2224
* {@link OAuth2SecurityExpressionMethods} using the variable oauth2 to access the methods. For example, the expression
2325
* <code>#oauth2.clientHasRole('ROLE_ADMIN')</code> would invoke {@link OAuth2SecurityExpressionMethods#clientHasRole}.
26+
* </p>
27+
* <p>
28+
* By default the {@link OAuth2ExpressionParser} is used. If this is undesirable one can inject their own
29+
* {@link ExpressionParser} using {@link #setExpressionParser(ExpressionParser)}.
30+
* </p>
2431
*
2532
* @author Dave Syer
2633
* @author Rob Winch
2734
*
35+
* @see OAuth2ExpressionParser
2836
*/
2937
public class OAuth2WebSecurityExpressionHandler extends DefaultWebSecurityExpressionHandler {
30-
private boolean throwExceptionOnInvalidScope = true;
31-
32-
/**
33-
* Flag to determine the behaviour on access denied if the reason is . If set then we throw an
34-
* {@link InvalidScopeException} instead of returning true. This is unconventional for an access decision because it
35-
* vetos the other voters in the chain, but it enables us to pass a message to the caller with information about the
36-
* required scope.
37-
*
38-
* @param throwException the flag to set (default true)
39-
*/
40-
public void setThrowExceptionOnInvalidScope(boolean throwException) {
41-
this.throwExceptionOnInvalidScope = throwException;
38+
public OAuth2WebSecurityExpressionHandler() {
39+
setExpressionParser(new OAuth2ExpressionParser(getExpressionParser()));
4240
}
43-
41+
4442
@Override
4543
protected StandardEvaluationContext createEvaluationContextInternal(Authentication authentication,
4644
FilterInvocation invocation) {
4745
StandardEvaluationContext ec = super.createEvaluationContextInternal(authentication, invocation);
48-
ec.setVariable("oauth2", new OAuth2SecurityExpressionMethods(authentication, throwExceptionOnInvalidScope));
46+
ec.setVariable("oauth2", new OAuth2SecurityExpressionMethods(authentication));
4947
return ec;
5048
}
5149
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package org.springframework.security.oauth2.provider.expression;
2+
3+
import static org.mockito.Mockito.verify;
4+
5+
import org.junit.Before;
6+
import org.junit.Test;
7+
import org.junit.runner.RunWith;
8+
import org.mockito.Mock;
9+
import org.mockito.runners.MockitoJUnitRunner;
10+
import org.springframework.expression.ExpressionParser;
11+
import org.springframework.expression.ParserContext;
12+
13+
/**
14+
*
15+
* @author Rob Winch
16+
*
17+
*/
18+
@RunWith(MockitoJUnitRunner.class)
19+
public class OAuth2ExpressionParserTests {
20+
@Mock
21+
private ExpressionParser delegate;
22+
@Mock
23+
private ParserContext parserContext;
24+
25+
private final String expressionString = "ORIGIONAL";
26+
27+
private final String wrappedExpression = "#oauth2.throwOnError(" + expressionString + ")";
28+
29+
private OAuth2ExpressionParser parser;
30+
31+
@Before
32+
public void setUp() {
33+
parser = new OAuth2ExpressionParser(delegate);
34+
}
35+
36+
@Test(expected = IllegalArgumentException.class)
37+
public void constructorNull() {
38+
new OAuth2ExpressionParser(null);
39+
}
40+
41+
@Test
42+
public void parseExpression() {
43+
parser.parseExpression(expressionString);
44+
verify(delegate).parseExpression(wrappedExpression);
45+
}
46+
47+
@Test
48+
public void parseExpressionWithContext() {
49+
parser.parseExpression(expressionString, parserContext);
50+
verify(delegate).parseExpression(wrappedExpression, parserContext);
51+
}
52+
}

0 commit comments

Comments
 (0)