Skip to content

Commit 1a8494a

Browse files
Add hasScope and hasAnyScope for @PreAuthorize
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
1 parent b7fb289 commit 1a8494a

File tree

6 files changed

+208
-0
lines changed

6 files changed

+208
-0
lines changed

core/src/main/java/org/springframework/security/access/expression/SecurityExpressionOperations.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,38 @@ public interface SecurityExpressionOperations {
8181
*/
8282
boolean hasAnyRole(String... roles);
8383

84+
/**
85+
* <p>
86+
* Determines if the {@link #getAuthentication()} has a particular authority within
87+
* {@link Authentication#getAuthorities()}.
88+
* </p>
89+
* <p>
90+
* This is similar to {@link #hasAuthority(String)} except that this method implies
91+
* that the String passed in is a scope. For example, if "read" is passed in the
92+
* implementation may convert it to use "SCOPE_read" instead. The way in which the
93+
* scope is converted may depend on the implementation settings.
94+
* </p>
95+
* @param scope the authority to test (i.e. "read")
96+
* @return true if the authority is found, else false
97+
*/
98+
boolean hasScope(String scope);
99+
100+
/**
101+
* <p>
102+
* Determines if the {@link #getAuthentication()} has any of the specified authorities
103+
* within {@link Authentication#getAuthorities()}.
104+
* </p>
105+
* <p>
106+
* This is similar to {@link #hasAnyAuthority(String...)} except that this method
107+
* implies that the String passed in is a scope. For example, if "read" is passed in
108+
* the implementation may convert it to use "SCOPE_read" instead. The way in which the
109+
* scope is converted may depend on the implementation settings.
110+
* </p>
111+
* @param scopes the authorities to test (i.e. "write", "read")
112+
* @return true if any of the authorities is found, else false
113+
*/
114+
boolean hasAnyScope(String... scopes);
115+
84116
/**
85117
* Always grants access.
86118
* @return true

core/src/main/java/org/springframework/security/access/expression/SecurityExpressionRoot.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ public abstract class SecurityExpressionRoot<T extends @Nullable Object> impleme
4646

4747
private String defaultRolePrefix = "ROLE_";
4848

49+
private String defaultScopePrefix = "SCOPE_";
50+
4951
private final T object;
5052

5153
private AuthorizationManagerFactory<T> authorizationManagerFactory = new DefaultAuthorizationManagerFactory<>();
@@ -165,6 +167,25 @@ public final boolean hasAllRoles(String... roles) {
165167
return isGranted(manager);
166168
}
167169

170+
@Override
171+
public final boolean hasScope(String scope) {
172+
return isGranted(this.authorizationManagerFactory.hasScope(scope));
173+
}
174+
175+
@Override
176+
public boolean hasAnyScope(String... scopes) {
177+
if (this.authorizationManagerFactory instanceof DefaultAuthorizationManagerFactory<T>) {
178+
String scopePrefix = this.defaultScopePrefix;
179+
for (int index = 0; index < scopes.length; index++) {
180+
String scope = scopes[index];
181+
if (scope.startsWith(scopePrefix)) {
182+
scopes[index] = scope.substring(scopePrefix.length());
183+
}
184+
}
185+
}
186+
return isGranted(this.authorizationManagerFactory.hasAnyScope(scopes));
187+
}
188+
168189
@Override
169190
public final Authentication getAuthentication() {
170191
return this.authentication.get();

core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,58 @@ public static <T> AuthorityAuthorizationManager<T> hasAnyAuthority(String... aut
124124
return new AuthorityAuthorizationManager<>(authorities);
125125
}
126126

127+
/**
128+
* Create an {@link AuthorityAuthorizationManager} that requires an
129+
* {@link Authentication} to have a {@code SCOPE_scope} authority.
130+
*
131+
* <p>
132+
* For example, if you call {@code hasScope("read")}, then this will require that each
133+
* authentication have a {@link org.springframework.security.core.GrantedAuthority}
134+
* whose value is {@code SCOPE_read}.
135+
*
136+
* <p>
137+
* This would equivalent to calling
138+
* {@code AuthorityAuthorizationManager#hasAuthority("SCOPE_read")}.
139+
* @param scope the scope value to require
140+
* @param <T> the secure object
141+
* @return an {@link AuthorityAuthorizationManager} that requires a
142+
* {@code "SCOPE_scope"} authority
143+
*/
144+
public static <T> AuthorityAuthorizationManager<T> hasScope(String scope) {
145+
assertScope(scope);
146+
return hasAuthority("SCOPE_" + scope);
147+
}
148+
149+
/**
150+
* Create an {@link AuthorityAuthorizationManager} that requires an
151+
* {@link Authentication} to have at least one authority among {@code SCOPE_scope1},
152+
* {@code SCOPE_scope2}, ... {@code SCOPE_scopeN}.
153+
*
154+
* <p>
155+
* For example, if you call {@code hasAnyScope("read", "write")}, then this will
156+
* require that each authentication have at least a
157+
* {@link org.springframework.security.core.GrantedAuthority} whose value is either
158+
* {@code SCOPE_read} or {@code SCOPE_write}.
159+
*
160+
* <p>
161+
* This would equivalent to calling
162+
* {@code AuthorityAuthorizationManager#hasAnyAuthority("SCOPE_read", "SCOPE_write")}.
163+
* @param scopes the scope values to allow
164+
* @param <T> the secure object
165+
* @return an {@link AuthorityAuthorizationManager} that requires at least one
166+
* authority among {@code "SCOPE_scope1"}, {@code SCOPE_scope2}, ...
167+
* {@code SCOPE_scopeN}.
168+
*
169+
*/
170+
public static <T> AuthorityAuthorizationManager<T> hasAnyScope(String... scopes) {
171+
String[] mappedScopes = new String[scopes.length];
172+
for (int i = 0; i < scopes.length; i++) {
173+
assertScope(scopes[i]);
174+
mappedScopes[i] = "SCOPE_" + scopes[i];
175+
}
176+
return hasAnyAuthority(mappedScopes);
177+
}
178+
127179
private static String[] toNamedRolesArray(String rolePrefix, String[] roles) {
128180
String[] result = new String[roles.length];
129181
for (int i = 0; i < roles.length; i++) {
@@ -136,6 +188,14 @@ private static String[] toNamedRolesArray(String rolePrefix, String[] roles) {
136188
return result;
137189
}
138190

191+
private static void assertScope(String scope) {
192+
Assert.notNull(scope, "role cannot be null");
193+
Assert.isTrue(!scope.startsWith("SCOPE_"),
194+
() -> scope + " should not start with SCOPE_ since SCOPE_"
195+
+ " is automatically prepended when using hasScope and hasAnyScope. Consider using "
196+
+ " AuthorityReactiveAuthorizationManager#hasAuthority or #hasAnyAuthority instead.");
197+
}
198+
139199
/**
140200
* {@inheritDoc}
141201
*/

core/src/main/java/org/springframework/security/authorization/AuthorizationManagerFactory.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,28 @@ default AuthorizationManager<T> hasAllAuthorities(String... authorities) {
109109
return AllAuthoritiesAuthorizationManager.hasAllAuthorities(authorities);
110110
}
111111

112+
/**
113+
* Creates an {@link AuthorizationManager} that requires users to have the specified
114+
* scope.
115+
* @param scope the scope (automatically prepended with SCOPE_) that should be
116+
* required to allow access (i.e. write, read, etc.)
117+
* @return A new {@link AuthorizationManager} instance
118+
*/
119+
default AuthorizationManager<T> hasScope(String scope) {
120+
return AuthorityAuthorizationManager.hasScope(scope);
121+
}
122+
123+
/**
124+
* Creates an {@link AuthorizationManager} that requires users to have one of many
125+
* scopes.
126+
* @param scopes the scopes (automatically prepended with SCOPE_) that the user should
127+
* have at least one of to allow access (i.e. write, read, etc.)
128+
* @return A new {@link AuthorizationManager} instance
129+
*/
130+
default AuthorizationManager<T> hasAnyScope(String... scopes) {
131+
return AuthorityAuthorizationManager.hasAnyScope(scopes);
132+
}
133+
112134
/**
113135
* Creates an {@link AuthorizationManager} that allows any authenticated user.
114136
* @return A new {@link AuthorizationManager} instance

core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.springframework.security.core.GrantedAuthority;
3030

3131
import static org.assertj.core.api.Assertions.assertThat;
32+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
3233
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
3334

3435
/**
@@ -271,4 +272,24 @@ void hasAnyRoleWhenEmptyRolePrefixThenNoException() {
271272
AuthorityAuthorizationManager.hasAnyRole("", new String[] { "USER" });
272273
}
273274

275+
@Test
276+
void hasScopes_withInvalidScope_shouldThrowIllegalArgumentException() {
277+
String[] scopes = { "read", "write", "SCOPE_invalid" };
278+
assertThatExceptionOfType(IllegalArgumentException.class)
279+
.isThrownBy(() -> AuthorityAuthorizationManager.hasAnyScope(scopes))
280+
.withMessage("Scope 'SCOPE_invalid' start with 'SCOPE_' prefix.");
281+
}
282+
283+
@Test
284+
void hasScope_withValidScope_shouldPass() {
285+
String scope = "read";
286+
assertThat(AuthorityAuthorizationManager.hasScope(scope)).isNotNull();
287+
}
288+
289+
@Test
290+
void hasScope_withValidScopes_shouldPass() {
291+
String[] scopes = { "read", "write" };
292+
assertThat(AuthorityAuthorizationManager.hasAnyScope(scopes)).isNotNull();
293+
}
294+
274295
}

core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,48 @@ public void checkDoSomethingStringWhenArgIsNotGrantThenDeniedDecision() throws E
8989
assertThat(decision.isGranted()).isFalse();
9090
}
9191

92+
@Test
93+
public void checkSecuredScope() throws Exception {
94+
MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
95+
"securedScope");
96+
PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
97+
AuthorizationResult decision = manager.authorize(TestAuthentication::authenticatedUser, methodInvocation);
98+
assertThat(decision).isNotNull();
99+
assertThat(decision.isGranted()).isFalse();
100+
101+
Supplier<Authentication> authentication = () -> new TestingAuthenticationToken("user", "password",
102+
"SCOPE_read");
103+
decision = manager.authorize(authentication, methodInvocation);
104+
assertThat(decision).isNotNull();
105+
assertThat(decision.isGranted()).isFalse();
106+
107+
authentication = () -> new TestingAuthenticationToken("user", "password", "SCOPE_write");
108+
decision = manager.authorize(authentication, methodInvocation);
109+
assertThat(decision).isNotNull();
110+
assertThat(decision.isGranted()).isTrue();
111+
}
112+
113+
@Test
114+
public void checkSecuredAnyScope() throws Exception {
115+
MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class,
116+
"securedAnyScope");
117+
PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
118+
AuthorizationResult decision = manager.authorize(TestAuthentication::authenticatedUser, methodInvocation);
119+
assertThat(decision).isNotNull();
120+
assertThat(decision.isGranted()).isFalse();
121+
122+
Supplier<Authentication> authentication = () -> new TestingAuthenticationToken("user", "password",
123+
"SCOPE_read");
124+
decision = manager.authorize(authentication, methodInvocation);
125+
assertThat(decision).isNotNull();
126+
assertThat(decision.isGranted()).isTrue();
127+
128+
authentication = () -> new TestingAuthenticationToken("user", "password", "SCOPE_write");
129+
decision = manager.authorize(authentication, methodInvocation);
130+
assertThat(decision).isNotNull();
131+
assertThat(decision.isGranted()).isTrue();
132+
}
133+
92134
@Test
93135
public void checkRequiresAdminWhenClassAnnotationsThenMethodAnnotationsTakePrecedence() throws Exception {
94136
Supplier<Authentication> authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER");
@@ -181,6 +223,16 @@ public void inheritedAnnotations() {
181223

182224
}
183225

226+
@PreAuthorize("hasScope('write')")
227+
public void securedScope() {
228+
229+
}
230+
231+
@PreAuthorize("hasAnyScope('write', 'read')")
232+
public void securedAnyScope() {
233+
234+
}
235+
184236
}
185237

186238
@PreAuthorize("hasRole('USER')")

0 commit comments

Comments
 (0)