diff --git a/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionOperations.java b/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionOperations.java index f63f131585f..8776b0c7709 100644 --- a/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionOperations.java +++ b/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionOperations.java @@ -81,6 +81,38 @@ public interface SecurityExpressionOperations { */ boolean hasAnyRole(String... roles); + /** + *

+ * Determines if the {@link #getAuthentication()} has a particular authority within + * {@link Authentication#getAuthorities()}. + *

+ *

+ * This is similar to {@link #hasAuthority(String)} except that this method implies + * that the String passed in is a scope. For example, if "read" is passed in the + * implementation may convert it to use "SCOPE_read" instead. The way in which the + * scope is converted may depend on the implementation settings. + *

+ * @param scope the authority to test (i.e. "read") + * @return true if the authority is found, else false + */ + boolean hasScope(String scope); + + /** + *

+ * Determines if the {@link #getAuthentication()} has any of the specified authorities + * within {@link Authentication#getAuthorities()}. + *

+ *

+ * This is similar to {@link #hasAnyAuthority(String...)} except that this method + * implies that the String passed in is a scope. For example, if "read" is passed in + * the implementation may convert it to use "SCOPE_read" instead. The way in which the + * scope is converted may depend on the implementation settings. + *

+ * @param scopes the authorities to test (i.e. "write", "read") + * @return true if any of the authorities is found, else false + */ + boolean hasAnyScope(String... scopes); + /** * Always grants access. * @return true diff --git a/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionRoot.java b/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionRoot.java index e0c35f7c2d3..e1fa6a97d79 100644 --- a/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionRoot.java +++ b/core/src/main/java/org/springframework/security/access/expression/SecurityExpressionRoot.java @@ -46,6 +46,8 @@ public abstract class SecurityExpressionRoot impleme private String defaultRolePrefix = "ROLE_"; + private String defaultScopePrefix = "SCOPE_"; + private final T object; private AuthorizationManagerFactory authorizationManagerFactory = new DefaultAuthorizationManagerFactory<>(); @@ -165,6 +167,24 @@ public final boolean hasAllRoles(String... roles) { return isGranted(manager); } + @Override + public final boolean hasScope(String scope) { + assertScope(scope); + return isGranted(this.authorizationManagerFactory.hasAuthority(this.defaultScopePrefix + scope)); + } + + @Override + public final boolean hasAnyScope(String... scopes) { + Assert.notNull(scopes, "scopes cannot be null"); + if (this.authorizationManagerFactory instanceof DefaultAuthorizationManagerFactory) { + for (int i = 0; i < scopes.length; i++) { + assertScope(scopes[i]); + scopes[i] = this.defaultScopePrefix + scopes[i]; + } + } + return isGranted(this.authorizationManagerFactory.hasAnyAuthority(scopes)); + } + @Override public final Authentication getAuthentication() { return this.authentication.get(); @@ -208,7 +228,8 @@ private boolean isGranted(AuthorizationManager authorizationManager) { /** * Convenience method to access {@link Authentication#getPrincipal()} from * {@link #getAuthentication()} - * @return + * @return the {@code Principal} being authenticated or the authenticated principal + * after authentication. */ public @Nullable Object getPrincipal() { return getAuthentication().getPrincipal(); @@ -304,4 +325,11 @@ public void setPermissionEvaluator(PermissionEvaluator permissionEvaluator) { this.permissionEvaluator = permissionEvaluator; } + private void assertScope(String scope) { + Assert.notNull(scope, "scope cannot be null"); + Assert.isTrue(!scope.startsWith(this.defaultScopePrefix), () -> scope + " should not start with '" + + this.defaultScopePrefix + "' since '" + this.defaultScopePrefix + + "' is automatically prepended when using hasScope and hasAnyScope. Consider using AuthorityAuthorizationManager#hasAuthority or #hasAnyAuthority instead."); + } + } diff --git a/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java index b9439ca3d60..7f7498c3196 100644 --- a/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java @@ -89,6 +89,48 @@ public void checkDoSomethingStringWhenArgIsNotGrantThenDeniedDecision() throws E assertThat(decision.isGranted()).isFalse(); } + @Test + public void checkSecuredScope() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "securedScope"); + PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager(); + AuthorizationResult decision = manager.authorize(TestAuthentication::authenticatedUser, methodInvocation); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", + "SCOPE_read"); + decision = manager.authorize(authentication, methodInvocation); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + + authentication = () -> new TestingAuthenticationToken("user", "password", "SCOPE_write"); + decision = manager.authorize(authentication, methodInvocation); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + public void checkSecuredAnyScope() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "securedAnyScope"); + PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager(); + AuthorizationResult decision = manager.authorize(TestAuthentication::authenticatedUser, methodInvocation); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", + "SCOPE_read"); + decision = manager.authorize(authentication, methodInvocation); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + + authentication = () -> new TestingAuthenticationToken("user", "password", "SCOPE_write"); + decision = manager.authorize(authentication, methodInvocation); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + @Test public void checkRequiresAdminWhenClassAnnotationsThenMethodAnnotationsTakePrecedence() throws Exception { Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER"); @@ -181,6 +223,16 @@ public void inheritedAnnotations() { } + @PreAuthorize("hasScope('write')") + public void securedScope() { + + } + + @PreAuthorize("hasAnyScope('write', 'read')") + public void securedAnyScope() { + + } + } @PreAuthorize("hasRole('USER')") diff --git a/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManagerTests.java index bb0d003e3d9..f320bb51c09 100644 --- a/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManagerTests.java @@ -90,6 +90,42 @@ public void checkDoSomethingStringWhenArgIsNotGrantThenDeniedDecision() throws E assertThat(decision.isGranted()).isFalse(); } + @Test + public void checkSecuredScope() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation( + new PreAuthorizeAuthorizationManagerTests.TestClass(), + PreAuthorizeAuthorizationManagerTests.TestClass.class, "securedScope"); + PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager(); + Mono authentication = Mono + .just(new TestingAuthenticationToken("user", "password", "SCOPE_read")); + AuthorizationResult decision = manager.authorize(authentication, methodInvocation).block(); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + + authentication = Mono.just(new TestingAuthenticationToken("user", "password", "SCOPE_write")); + decision = manager.authorize(authentication, methodInvocation).block(); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + public void checkSecuredAnyScope() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation( + new PreAuthorizeAuthorizationManagerTests.TestClass(), + PreAuthorizeAuthorizationManagerTests.TestClass.class, "securedAnyScope"); + PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager(); + Mono authentication = Mono + .just(new TestingAuthenticationToken("user", "password", "SCOPE_read")); + AuthorizationResult decision = manager.authorize(authentication, methodInvocation).block(); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + + authentication = Mono.just(new TestingAuthenticationToken("user", "password", "SCOPE_write")); + decision = manager.authorize(authentication, methodInvocation).block(); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + @Test public void checkRequiresAdminWhenClassAnnotationsThenMethodAnnotationsTakePrecedence() throws Exception { Mono authentication = Mono @@ -149,6 +185,16 @@ public void inheritedAnnotations() { } + @PreAuthorize("hasScope('write')") + public void securedScope() { + + } + + @PreAuthorize("hasAnyScope('write', 'read')") + public void securedAnyScope() { + + } + } @PreAuthorize("hasRole('USER')")