Polish OAuth2AuthorizationManagers

- Add OAuth2ReactiveAuthorizationManagers
- Code to interfaces
- Align error message with the same in
AuthorityAuthorizationManager
- Adjust expectations in tests to confirm an
appropriately constructed authorizaion manager
- Add JavaDoc and reference documentation

Issue gh-13654
This commit is contained in:
Josh Cummings
2023-10-02 17:25:46 -06:00
parent 2ee8f27997
commit a3227f041c
10 changed files with 399 additions and 143 deletions

View File

@@ -1,56 +0,0 @@
/*
* Copyright 2002-2023 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
*
* https://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.oauth2.core;
import org.springframework.security.authorization.AuthorityAuthorizationManager;
/**
* @author Mario Petrovski
* @since 6.2
*/
public final class OAuth2AuthorizationManagers {
private OAuth2AuthorizationManagers() {
}
public static <T> AuthorityAuthorizationManager<T> hasScope(String scope) {
verifyScope(scope);
return AuthorityAuthorizationManager.hasAuthority("SCOPE_" + scope);
}
public static <T> AuthorityAuthorizationManager<T> hasAnyScope(String... scopes) {
verifyScopes(scopes);
String[] mappedScopes = new String[scopes.length];
for (int i = 0; i < scopes.length; i++) {
mappedScopes[i] = "SCOPE_" + scopes[i];
}
return AuthorityAuthorizationManager.hasAnyAuthority(mappedScopes);
}
private static void verifyScopes(String... scopes) throws IllegalArgumentException {
for (String scope : scopes) {
verifyScope(scope);
}
}
private static void verifyScope(String scope) {
if (scope.startsWith("SCOPE_")) {
throw new IllegalArgumentException("Scope '" + scope + "' start with 'SCOPE_' prefix.");
}
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2002-2023 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
*
* https://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.oauth2.core.authorization;
import org.springframework.security.authorization.AuthorityAuthorizationManager;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
/**
* A convenience class for creating OAuth 2.0-specific {@link AuthorizationManager}s.
*
* @author Mario Petrovski
* @author Josh Cummings
* @since 6.2
* @see AuthorityAuthorizationManager
*/
public final class OAuth2AuthorizationManagers {
private OAuth2AuthorizationManagers() {
}
/**
* Create an {@link AuthorizationManager} that requires an {@link Authentication} to
* have a {@code SCOPE_scope} authority.
*
* <p>
* For example, if you call {@code hasScope("read")}, then this will require that each
* authentication have a {@link org.springframework.security.core.GrantedAuthority}
* whose value is {@code SCOPE_read}.
*
* <p>
* This would equivalent to calling
* {@code AuthorityAuthorizationManager#hasAuthority("SCOPE_read")}.
* @param scope the scope value to require
* @param <T> the secure object
* @return an {@link AuthorizationManager} that requires a {@code "SCOPE_scope"}
* authority
*/
public static <T> AuthorizationManager<T> hasScope(String scope) {
assertScope(scope);
return AuthorityAuthorizationManager.hasAuthority("SCOPE_" + scope);
}
/**
* Create an {@link AuthorizationManager} that requires an {@link Authentication} to
* have at least one authority among {@code SCOPE_scope1}, {@code SCOPE_scope2}, ...
* {@code SCOPE_scopeN}.
*
* <p>
* For example, if you call {@code hasAnyScope("read", "write")}, then this will
* require that each authentication have at least a
* {@link org.springframework.security.core.GrantedAuthority} whose value is either
* {@code SCOPE_read} or {@code SCOPE_write}.
*
* <p>
* This would equivalent to calling
* {@code AuthorityAuthorizationManager#hasAnyAuthority("SCOPE_read", "SCOPE_write")}.
* @param scopes the scope values to allow
* @param <T> the secure object
* @return an {@link AuthorizationManager} that requires at least one authority among
* {@code "SCOPE_scope1"}, {@code SCOPE_scope2}, ... {@code SCOPE_scopeN}.
*
*/
public static <T> AuthorizationManager<T> hasAnyScope(String... scopes) {
String[] mappedScopes = new String[scopes.length];
for (int i = 0; i < scopes.length; i++) {
assertScope(scopes[i]);
mappedScopes[i] = "SCOPE_" + scopes[i];
}
return AuthorityAuthorizationManager.hasAnyAuthority(mappedScopes);
}
private static void assertScope(String scope) {
Assert.isTrue(!scope.startsWith("SCOPE_"),
() -> scope + " should not start with SCOPE_ since SCOPE_"
+ " is automatically prepended when using hasScope and hasAnyScope. Consider using "
+ " AuthorityAuthorizationManager#hasAuthority or #hasAnyAuthority instead.");
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2002-2023 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
*
* https://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.oauth2.core.authorization;
import org.springframework.security.authorization.AuthorityReactiveAuthorizationManager;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
/**
* A convenience class for creating OAuth 2.0-specific {@link AuthorizationManager}s.
*
* @author Josh Cummings
* @since 6.2
* @see AuthorityReactiveAuthorizationManager
*/
public final class OAuth2ReactiveAuthorizationManagers {
private OAuth2ReactiveAuthorizationManagers() {
}
/**
* Create a {@link ReactiveAuthorizationManager} that requires an
* {@link Authentication} to have a {@code SCOPE_scope} authority.
*
* <p>
* For example, if you call {@code hasScope("read")}, then this will require that each
* authentication have a {@link org.springframework.security.core.GrantedAuthority}
* whose value is {@code SCOPE_read}.
*
* <p>
* This would equivalent to calling
* {@code AuthorityReactiveAuthorizationManager#hasAuthority("SCOPE_read")}.
* @param scope the scope value to require
* @param <T> the secure object
* @return an {@link ReactiveAuthorizationManager} that requires a
* {@code "SCOPE_scope"} authority
*/
public static <T> ReactiveAuthorizationManager<T> hasScope(String scope) {
assertScope(scope);
return AuthorityReactiveAuthorizationManager.hasAuthority("SCOPE_" + scope);
}
/**
* Create a {@link ReactiveAuthorizationManager} that requires an
* {@link Authentication} to have at least one authority among {@code SCOPE_scope1},
* {@code SCOPE_scope2}, ... {@code SCOPE_scopeN}.
*
* <p>
* For example, if you call {@code hasAnyScope("read", "write")}, then this will
* require that each authentication have at least a
* {@link org.springframework.security.core.GrantedAuthority} whose value is either
* {@code SCOPE_read} or {@code SCOPE_write}.
*
* <p>
* This would equivalent to calling
* {@code AuthorityReactiveAuthorizationManager#hasAnyAuthority("SCOPE_read", "SCOPE_write")}.
* @param scopes the scope values to allow
* @param <T> the secure object
* @return an {@link ReactiveAuthorizationManager} that requires at least one
* authority among {@code "SCOPE_scope1"}, {@code SCOPE_scope2}, ...
* {@code SCOPE_scopeN}.
*/
public static <T> ReactiveAuthorizationManager<T> hasAnyScope(String... scopes) {
String[] mappedScopes = new String[scopes.length];
for (int i = 0; i < scopes.length; i++) {
assertScope(scopes[i]);
mappedScopes[i] = "SCOPE_" + scopes[i];
}
return AuthorityReactiveAuthorizationManager.hasAnyAuthority(mappedScopes);
}
private static void assertScope(String scope) {
Assert.isTrue(!scope.startsWith("SCOPE_"),
() -> scope + " should not start with SCOPE_ since SCOPE_"
+ " is automatically prepended when using hasScope and hasAnyScope. Consider using "
+ " AuthorityReactiveAuthorizationManager#hasAuthority or #hasAnyAuthority instead.");
}
}

View File

@@ -1,63 +0,0 @@
/*
* Copyright 2002-2023 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
*
* https://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.oauth2.core;
import org.junit.jupiter.api.Test;
import org.springframework.security.authorization.AuthorityAuthorizationManager;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link OAuth2AuthorizationManagers}
*
* @author Mario Petrovski
*/
public class OAuth2AuthorizationManagersTests {
@Test
void hasScope_withInvalidScope_shouldThrowIllegalArgumentException() {
String scope = "SCOPE_invalid";
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> OAuth2AuthorizationManagers.hasScope(scope))
.withMessage("Scope 'SCOPE_invalid' start with 'SCOPE_' prefix.");
}
@Test
void hasScopes_withInvalidScope_shouldThrowIllegalArgumentException() {
String[] scopes = { "read", "write", "SCOPE_invalid" };
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> OAuth2AuthorizationManagers.hasAnyScope(scopes))
.withMessage("Scope 'SCOPE_invalid' start with 'SCOPE_' prefix.");
}
@Test
void hasScope_withValidScope_shouldPass() {
String scope = "read";
AuthorityAuthorizationManager<Object> authorizationManager = OAuth2AuthorizationManagers.hasScope(scope);
assertThat(authorizationManager).isNotNull();
}
@Test
void hasScope_withValidScopes_shouldPass() {
String[] scopes = { "read", "write" };
AuthorityAuthorizationManager<Object> authorizationManager = OAuth2AuthorizationManagers.hasAnyScope(scopes);
assertThat(authorizationManager).isNotNull();
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2002-2023 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
*
* https://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.oauth2.core.authorization;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link OAuth2AuthorizationManagers}
*
* @author Mario Petrovski
* @author Josh Cummings
*/
public class OAuth2AuthorizationManagersTests {
@Test
void hasScopeWhenInvalidScopeThenThrowIllegalArgument() {
String scope = "SCOPE_invalid";
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> OAuth2AuthorizationManagers.hasScope(scope))
.withMessageContaining("SCOPE_invalid should not start with SCOPE_");
}
@Test
void hasAnyScopeWhenInvalidScopeThenThrowIllegalArgument() {
String[] scopes = { "read", "write", "SCOPE_invalid" };
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> OAuth2AuthorizationManagers.hasAnyScope(scopes))
.withMessageContaining("SCOPE_invalid should not start with SCOPE_");
}
@Test
void hasScopeWhenValidScopeThenAuthorizationManager() {
String scope = "read";
AuthorizationManager<Object> authorizationManager = OAuth2AuthorizationManagers.hasScope(scope);
authorizationManager.verify(() -> hasScope(scope), new Object());
assertThatExceptionOfType(AccessDeniedException.class)
.isThrownBy(() -> authorizationManager.verify(() -> hasScope("wrong"), new Object()));
}
@Test
void hasAnyScopeWhenValidScopesThenAuthorizationManager() {
String[] scopes = { "read", "write" };
AuthorizationManager<Object> authorizationManager = OAuth2AuthorizationManagers.hasAnyScope(scopes);
for (String scope : scopes) {
authorizationManager.verify(() -> hasScope(scope), new Object());
}
assertThatExceptionOfType(AccessDeniedException.class)
.isThrownBy(() -> authorizationManager.verify(() -> hasScope("wrong"), new Object()));
}
Authentication hasScope(String scope) {
return new TestingAuthenticationToken("user", "pass", "SCOPE_" + scope);
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2002-2023 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
*
* https://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.oauth2.core.authorization;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link OAuth2ReactiveAuthorizationManagers}
*
* @author Josh Cummings
*/
public class OAuth2ReactiveAuthorizationManagersTests {
@Test
void hasScopeWhenInvalidScopeThenThrowIllegalArgument() {
String scope = "SCOPE_invalid";
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> OAuth2ReactiveAuthorizationManagers.hasScope(scope))
.withMessageContaining("SCOPE_invalid should not start with SCOPE_");
}
@Test
void hasAnyScopeWhenInvalidScopeThenThrowIllegalArgument() {
String[] scopes = { "read", "write", "SCOPE_invalid" };
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> OAuth2ReactiveAuthorizationManagers.hasAnyScope(scopes))
.withMessageContaining("SCOPE_invalid should not start with SCOPE_");
}
@Test
void hasScopeWhenValidScopeThenAuthorizationManager() {
String scope = "read";
ReactiveAuthorizationManager<Object> authorizationManager = OAuth2ReactiveAuthorizationManagers.hasScope(scope);
authorizationManager.verify(hasScope(scope), new Object()).block();
assertThatExceptionOfType(AccessDeniedException.class)
.isThrownBy(() -> authorizationManager.verify(hasScope("wrong"), new Object()).block());
}
@Test
void hasAnyScopeWhenValidScopesThenAuthorizationManager() {
String[] scopes = { "read", "write" };
ReactiveAuthorizationManager<Object> authorizationManager = OAuth2ReactiveAuthorizationManagers
.hasAnyScope(scopes);
for (String scope : scopes) {
authorizationManager.verify(hasScope(scope), new Object()).block();
}
assertThatExceptionOfType(AccessDeniedException.class)
.isThrownBy(() -> authorizationManager.verify(hasScope("wrong"), new Object()).block());
}
Mono<Authentication> hasScope(String scope) {
return Mono.just(new TestingAuthenticationToken("user", "pass", "SCOPE_" + scope));
}
}