Skip to content

Commit c4766e6

Browse files
evgeniychebanjzheaux
authored andcommitted
Add AuthorizationManager that uses ExpressionHandler
Closes gh-11105
1 parent 2b47944 commit c4766e6

File tree

6 files changed

+471
-3
lines changed

6 files changed

+471
-3
lines changed

config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java

+134
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,13 @@
3737
import org.springframework.security.config.test.SpringTestContextExtension;
3838
import org.springframework.security.core.authority.SimpleGrantedAuthority;
3939
import org.springframework.security.web.SecurityFilterChain;
40+
import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager;
4041
import org.springframework.security.web.access.intercept.AuthorizationFilter;
4142
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
4243
import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager;
4344
import org.springframework.test.web.servlet.MockMvc;
4445
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
46+
import org.springframework.test.web.servlet.request.RequestPostProcessor;
4547
import org.springframework.web.bind.annotation.GetMapping;
4648
import org.springframework.web.bind.annotation.PostMapping;
4749
import org.springframework.web.bind.annotation.RestController;
@@ -395,6 +397,90 @@ public void getWhenAnyRequestAuthenticatedConfiguredAndUserLoggedInThenRespondsW
395397
this.mvc.perform(requestWithUser).andExpect(status().isOk());
396398
}
397399

400+
@Test
401+
public void getWhenExpressionHasRoleUserConfiguredAndRoleIsUserThenRespondsWithOk() throws Exception {
402+
this.spring.register(ExpressionRoleUserConfig.class, BasicController.class).autowire();
403+
// @formatter:off
404+
MockHttpServletRequestBuilder requestWithUser = get("/")
405+
.with(user("user")
406+
.roles("USER"));
407+
// @formatter:on
408+
this.mvc.perform(requestWithUser).andExpect(status().isOk());
409+
}
410+
411+
@Test
412+
public void getWhenExpressionHasRoleUserConfiguredAndRoleIsAdminThenRespondsWithForbidden() throws Exception {
413+
this.spring.register(ExpressionRoleUserConfig.class, BasicController.class).autowire();
414+
// @formatter:off
415+
MockHttpServletRequestBuilder requestWithAdmin = get("/")
416+
.with(user("user")
417+
.roles("ADMIN"));
418+
// @formatter:on
419+
this.mvc.perform(requestWithAdmin).andExpect(status().isForbidden());
420+
}
421+
422+
@Test
423+
public void getWhenExpressionRoleUserOrAdminConfiguredAndRoleIsUserThenRespondsWithOk() throws Exception {
424+
this.spring.register(ExpressionRoleUserOrAdminConfig.class, BasicController.class).autowire();
425+
// @formatter:off
426+
MockHttpServletRequestBuilder requestWithUser = get("/")
427+
.with(user("user")
428+
.roles("USER"));
429+
// @formatter:on
430+
this.mvc.perform(requestWithUser).andExpect(status().isOk());
431+
}
432+
433+
@Test
434+
public void getWhenExpressionRoleUserOrAdminConfiguredAndRoleIsAdminThenRespondsWithOk() throws Exception {
435+
this.spring.register(ExpressionRoleUserOrAdminConfig.class, BasicController.class).autowire();
436+
// @formatter:off
437+
MockHttpServletRequestBuilder requestWithAdmin = get("/")
438+
.with(user("user")
439+
.roles("ADMIN"));
440+
// @formatter:on
441+
this.mvc.perform(requestWithAdmin).andExpect(status().isOk());
442+
}
443+
444+
@Test
445+
public void getWhenExpressionRoleUserOrAdminConfiguredAndRoleIsOtherThenRespondsWithForbidden() throws Exception {
446+
this.spring.register(ExpressionRoleUserOrAdminConfig.class, BasicController.class).autowire();
447+
// @formatter:off
448+
MockHttpServletRequestBuilder requestWithRoleOther = get("/")
449+
.with(user("user")
450+
.roles("OTHER"));
451+
// @formatter:on
452+
this.mvc.perform(requestWithRoleOther).andExpect(status().isForbidden());
453+
}
454+
455+
@Test
456+
public void getWhenExpressionHasIpAddressLocalhostConfiguredIpAddressIsLocalhostThenRespondsWithOk()
457+
throws Exception {
458+
this.spring.register(ExpressionIpAddressLocalhostConfig.class, BasicController.class).autowire();
459+
// @formatter:off
460+
MockHttpServletRequestBuilder requestFromLocalhost = get("/")
461+
.with(remoteAddress("127.0.0.1"));
462+
// @formatter:on
463+
this.mvc.perform(requestFromLocalhost).andExpect(status().isOk());
464+
}
465+
466+
@Test
467+
public void getWhenExpressionHasIpAddressLocalhostConfiguredIpAddressIsOtherThenRespondsWithForbidden()
468+
throws Exception {
469+
this.spring.register(ExpressionIpAddressLocalhostConfig.class, BasicController.class).autowire();
470+
// @formatter:off
471+
MockHttpServletRequestBuilder requestFromOtherHost = get("/")
472+
.with(remoteAddress("192.168.0.1"));
473+
// @formatter:on
474+
this.mvc.perform(requestFromOtherHost).andExpect(status().isForbidden());
475+
}
476+
477+
private static RequestPostProcessor remoteAddress(String remoteAddress) {
478+
return (request) -> {
479+
request.setRemoteAddr(remoteAddress);
480+
return request;
481+
};
482+
}
483+
398484
@EnableWebSecurity
399485
static class NoRequestsConfig {
400486

@@ -713,6 +799,54 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
713799

714800
}
715801

802+
@EnableWebSecurity
803+
static class ExpressionRoleUserConfig {
804+
805+
@Bean
806+
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
807+
// @formatter:off
808+
return http
809+
.authorizeHttpRequests((requests) -> requests
810+
.anyRequest().access(new WebExpressionAuthorizationManager("hasRole('USER')"))
811+
)
812+
.build();
813+
// @formatter:on
814+
}
815+
816+
}
817+
818+
@EnableWebSecurity
819+
static class ExpressionRoleUserOrAdminConfig {
820+
821+
@Bean
822+
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
823+
// @formatter:off
824+
return http
825+
.authorizeHttpRequests((requests) -> requests
826+
.anyRequest().access(new WebExpressionAuthorizationManager("hasRole('USER') or hasRole('ADMIN')"))
827+
)
828+
.build();
829+
// @formatter:on
830+
}
831+
832+
}
833+
834+
@EnableWebSecurity
835+
static class ExpressionIpAddressLocalhostConfig {
836+
837+
@Bean
838+
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
839+
// @formatter:off
840+
return http
841+
.authorizeHttpRequests((requests) -> requests
842+
.anyRequest().access(new WebExpressionAuthorizationManager("hasIpAddress('127.0.0.1')"))
843+
)
844+
.build();
845+
// @formatter:on
846+
}
847+
848+
}
849+
716850
@Configuration
717851
static class AuthorizationEventPublisherConfig {
718852

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright 2002-2022 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+
* https://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+
17+
package org.springframework.security.web.access.expression;
18+
19+
import org.springframework.security.access.expression.AbstractSecurityExpressionHandler;
20+
import org.springframework.security.access.expression.SecurityExpressionHandler;
21+
import org.springframework.security.access.expression.SecurityExpressionOperations;
22+
import org.springframework.security.authentication.AuthenticationTrustResolver;
23+
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
24+
import org.springframework.security.core.Authentication;
25+
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
26+
import org.springframework.util.Assert;
27+
28+
/**
29+
* A {@link SecurityExpressionHandler} that uses a {@link RequestAuthorizationContext} to
30+
* create a {@link WebSecurityExpressionRoot}.
31+
*
32+
* @author Evgeniy Cheban
33+
* @since 5.8
34+
*/
35+
public class DefaultHttpSecurityExpressionHandler extends AbstractSecurityExpressionHandler<RequestAuthorizationContext>
36+
implements SecurityExpressionHandler<RequestAuthorizationContext> {
37+
38+
private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
39+
40+
private String defaultRolePrefix = "ROLE_";
41+
42+
@Override
43+
protected SecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication,
44+
RequestAuthorizationContext context) {
45+
WebSecurityExpressionRoot root = new WebSecurityExpressionRoot(authentication, context.getRequest());
46+
root.setRoleHierarchy(getRoleHierarchy());
47+
root.setPermissionEvaluator(getPermissionEvaluator());
48+
root.setTrustResolver(this.trustResolver);
49+
root.setDefaultRolePrefix(this.defaultRolePrefix);
50+
return root;
51+
}
52+
53+
/**
54+
* Sets the {@link AuthenticationTrustResolver} to be used. The default is
55+
* {@link AuthenticationTrustResolverImpl}.
56+
* @param trustResolver the {@link AuthenticationTrustResolver} to use
57+
*/
58+
public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
59+
Assert.notNull(trustResolver, "trustResolver cannot be null");
60+
this.trustResolver = trustResolver;
61+
}
62+
63+
/**
64+
* Sets the default prefix to be added to
65+
* {@link org.springframework.security.access.expression.SecurityExpressionRoot#hasAnyRole(String...)}
66+
* or
67+
* {@link org.springframework.security.access.expression.SecurityExpressionRoot#hasRole(String)}.
68+
* For example, if hasRole("ADMIN") or hasRole("ROLE_ADMIN") is passed in, then the
69+
* role ROLE_ADMIN will be used when the defaultRolePrefix is "ROLE_" (default).
70+
* @param defaultRolePrefix the default prefix to add to roles. The default is
71+
* "ROLE_".
72+
*/
73+
public void setDefaultRolePrefix(String defaultRolePrefix) {
74+
Assert.hasText(defaultRolePrefix, "defaultRolePrefix cannot be empty");
75+
this.defaultRolePrefix = defaultRolePrefix;
76+
}
77+
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2002-2022 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+
* https://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+
17+
package org.springframework.security.web.access.expression;
18+
19+
import org.springframework.expression.Expression;
20+
import org.springframework.security.authorization.AuthorizationDecision;
21+
22+
/**
23+
* An expression-based {@link AuthorizationDecision}.
24+
*
25+
* @author Evgeniy Cheban
26+
* @since 5.8
27+
*/
28+
public final class ExpressionAuthorizationDecision extends AuthorizationDecision {
29+
30+
private final Expression expression;
31+
32+
/**
33+
* Creates an instance.
34+
* @param granted the decision to use
35+
* @param expression the {@link Expression} to use
36+
*/
37+
public ExpressionAuthorizationDecision(boolean granted, Expression expression) {
38+
super(granted);
39+
this.expression = expression;
40+
}
41+
42+
/**
43+
* Returns the {@link Expression}.
44+
* @return the {@link Expression} to use
45+
*/
46+
public Expression getExpression() {
47+
return this.expression;
48+
}
49+
50+
@Override
51+
public String toString() {
52+
return "ExpressionAuthorizationDecision[granted=" + isGranted() + ", expression='" + this.expression + "']";
53+
}
54+
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright 2002-2022 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+
* https://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+
17+
package org.springframework.security.web.access.expression;
18+
19+
import java.util.function.Supplier;
20+
21+
import org.springframework.expression.EvaluationContext;
22+
import org.springframework.expression.Expression;
23+
import org.springframework.security.access.expression.ExpressionUtils;
24+
import org.springframework.security.access.expression.SecurityExpressionHandler;
25+
import org.springframework.security.authorization.AuthorizationDecision;
26+
import org.springframework.security.authorization.AuthorizationManager;
27+
import org.springframework.security.core.Authentication;
28+
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
29+
import org.springframework.util.Assert;
30+
31+
/**
32+
* An expression-based {@link AuthorizationManager} that determines the access by
33+
* evaluating the provided expression.
34+
*
35+
* @author Evgeniy Cheban
36+
* @since 5.8
37+
*/
38+
public final class WebExpressionAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
39+
40+
private SecurityExpressionHandler<RequestAuthorizationContext> expressionHandler = new DefaultHttpSecurityExpressionHandler();
41+
42+
private Expression expression;
43+
44+
/**
45+
* Creates an instance.
46+
* @param expressionString the raw expression string to parse
47+
*/
48+
public WebExpressionAuthorizationManager(String expressionString) {
49+
Assert.hasText(expressionString, "expressionString cannot be empty");
50+
this.expression = this.expressionHandler.getExpressionParser().parseExpression(expressionString);
51+
}
52+
53+
/**
54+
* Sets the {@link SecurityExpressionHandler} to be used. The default is
55+
* {@link DefaultHttpSecurityExpressionHandler}.
56+
* @param expressionHandler the {@link SecurityExpressionHandler} to use
57+
*/
58+
public void setExpressionHandler(SecurityExpressionHandler<RequestAuthorizationContext> expressionHandler) {
59+
Assert.notNull(expressionHandler, "expressionHandler cannot be null");
60+
this.expressionHandler = expressionHandler;
61+
this.expression = expressionHandler.getExpressionParser()
62+
.parseExpression(this.expression.getExpressionString());
63+
}
64+
65+
/**
66+
* Determines the access by evaluating the provided expression.
67+
* @param authentication the {@link Supplier} of the {@link Authentication} to check
68+
* @param context the {@link RequestAuthorizationContext} to check
69+
* @return an {@link ExpressionAuthorizationDecision} based on the evaluated
70+
* expression
71+
*/
72+
@Override
73+
public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext context) {
74+
EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication.get(), context);
75+
boolean granted = ExpressionUtils.evaluateAsBoolean(this.expression, ctx);
76+
return new ExpressionAuthorizationDecision(granted, this.expression);
77+
}
78+
79+
@Override
80+
public String toString() {
81+
return "WebExpressionAuthorizationManager[expression='" + this.expression + "']";
82+
}
83+
84+
}

0 commit comments

Comments
 (0)