Skip to content

Commit b38bbf6

Browse files
authored
Scan Spring Security @Secured annotation (#2055)
* Scan Spring Security `@Secured` annotation Signed-off-by: Michael Edgar <[email protected]> * Update tests for `@Secured`/`@RolesAllowed` on Spring methods Signed-off-by: Michael Edgar <[email protected]> --------- Signed-off-by: Michael Edgar <[email protected]>
1 parent 8767c70 commit b38bbf6

File tree

11 files changed

+137
-38
lines changed

11 files changed

+137
-38
lines changed

core/src/main/java/io/smallrye/openapi/runtime/scanner/processor/JavaSecurityProcessor.java

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
import java.util.ArrayList;
44
import java.util.Arrays;
55
import java.util.LinkedHashMap;
6+
import java.util.LinkedHashSet;
67
import java.util.List;
78
import java.util.Map;
9+
import java.util.Set;
10+
import java.util.function.Supplier;
811
import java.util.stream.Collectors;
912
import java.util.stream.Stream;
1013

@@ -29,7 +32,9 @@
2932
public class JavaSecurityProcessor {
3033

3134
public void addRolesAllowedToScopes(String[] roles) {
32-
resourceRolesAllowed = roles;
35+
if (roles != null) {
36+
resourceRolesAllowed.addAll(Arrays.asList(roles));
37+
}
3338
addScopes(roles);
3439
}
3540

@@ -38,13 +43,18 @@ public void addDeclaredRolesToScopes(String[] roles) {
3843
}
3944

4045
public void processSecurityRoles(MethodInfo method, Operation operation) {
41-
processSecurityRolesForMethodOperation(method, operation);
46+
processSecurityRolesForMethodOperation(method, operation,
47+
() -> context.annotations().getAnnotationValue(method, SecurityConstants.ROLES_ALLOWED));
48+
}
49+
50+
public void processSecurityRoles(MethodInfo method, Operation operation, Supplier<String[]> roleSupplier) {
51+
processSecurityRolesForMethodOperation(method, operation, roleSupplier);
4252
}
4353

4454
private final AnnotationScannerContext context;
4555
private String currentSecurityScheme;
4656
private List<OAuthFlow> currentFlows;
47-
private String[] resourceRolesAllowed;
57+
private final Set<String> resourceRolesAllowed = new LinkedHashSet<>();
4858

4959
public JavaSecurityProcessor(AnnotationScannerContext context) {
5060
this.context = context;
@@ -53,7 +63,7 @@ public JavaSecurityProcessor(AnnotationScannerContext context) {
5363
public void initialize(OpenAPI openApi) {
5464
currentSecurityScheme = null;
5565
currentFlows = null;
56-
resourceRolesAllowed = null;
66+
resourceRolesAllowed.clear();
5767
checkSecurityScheme(openApi);
5868
}
5969

@@ -96,21 +106,22 @@ private void addScopes(String[] roles) {
96106
* @param method the current JAX-RS method
97107
* @param operation the OpenAPI Operation
98108
*/
99-
private void processSecurityRolesForMethodOperation(MethodInfo method, Operation operation) {
109+
private void processSecurityRolesForMethodOperation(MethodInfo method, Operation operation,
110+
Supplier<String[]> roleSupplier) {
100111
if (this.currentSecurityScheme != null) {
101-
String[] rolesAllowed = context.annotations().getAnnotationValue(method, SecurityConstants.ROLES_ALLOWED);
112+
String[] rolesAllowed = roleSupplier.get();
102113

103114
if (rolesAllowed != null) {
104115
addScopes(rolesAllowed);
105116
addRolesAllowed(operation, rolesAllowed);
106-
} else if (this.resourceRolesAllowed != null) {
117+
} else if (!this.resourceRolesAllowed.isEmpty()) {
107118
boolean denyAll = context.annotations().getAnnotation(method, SecurityConstants.DENY_ALL) != null;
108119
boolean permitAll = context.annotations().getAnnotation(method, SecurityConstants.PERMIT_ALL) != null;
109120

110121
if (denyAll) {
111122
addRolesAllowed(operation, new String[0]);
112123
} else if (!permitAll) {
113-
addRolesAllowed(operation, this.resourceRolesAllowed);
124+
addRolesAllowed(operation, this.resourceRolesAllowed.toArray(String[]::new));
114125
}
115126
}
116127
}

extension-spring/pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
<properties>
1616
<version.spring>6.1.13</version.spring>
17+
<version.spring-security>6.3.4</version.spring-security>
1718
</properties>
1819

1920
<dependencyManagement>
@@ -63,6 +64,12 @@
6364
<artifactId>spring-webmvc</artifactId>
6465
<scope>test</scope>
6566
</dependency>
67+
<dependency>
68+
<groupId>org.springframework.security</groupId>
69+
<artifactId>spring-security-core</artifactId>
70+
<version>${version.spring-security}</version>
71+
<scope>test</scope>
72+
</dependency>
6673
<dependency>
6774
<groupId>javax.servlet</groupId>
6875
<artifactId>javax.servlet-api</artifactId>

extension-spring/src/main/java/io/smallrye/openapi/spring/SpringAnnotationScanner.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
import java.util.Collection;
66
import java.util.HashSet;
77
import java.util.List;
8+
import java.util.Objects;
89
import java.util.Optional;
910
import java.util.Set;
1011
import java.util.function.Function;
12+
import java.util.stream.Stream;
1113

1214
import org.eclipse.microprofile.openapi.OASFactory;
1315
import org.eclipse.microprofile.openapi.models.OpenAPI;
@@ -24,13 +26,16 @@
2426
import org.jboss.jandex.MethodParameterInfo;
2527
import org.jboss.jandex.Type;
2628

29+
import io.smallrye.openapi.api.constants.SecurityConstants;
2730
import io.smallrye.openapi.api.util.ListUtil;
2831
import io.smallrye.openapi.api.util.MergeUtil;
2932
import io.smallrye.openapi.runtime.scanner.AnnotationScannerExtension;
3033
import io.smallrye.openapi.runtime.scanner.ResourceParameters;
3134
import io.smallrye.openapi.runtime.scanner.dataobject.TypeResolver;
35+
import io.smallrye.openapi.runtime.scanner.processor.JavaSecurityProcessor;
3236
import io.smallrye.openapi.runtime.scanner.spi.AbstractAnnotationScanner;
3337
import io.smallrye.openapi.runtime.scanner.spi.AnnotationScannerContext;
38+
import io.smallrye.openapi.runtime.util.Annotations;
3439
import io.smallrye.openapi.runtime.util.ModelUtil;
3540

3641
/**
@@ -211,6 +216,15 @@ private OpenAPI processControllerClass(ClassInfo controllerClass) {
211216
return openApi;
212217
}
213218

219+
@Override
220+
public void processJavaSecurity(AnnotationScannerContext context, ClassInfo resourceClass, OpenAPI openApi) {
221+
super.processJavaSecurity(context, resourceClass, openApi);
222+
JavaSecurityProcessor securityProcessor = context.getJavaSecurityProcessor();
223+
securityProcessor
224+
.addRolesAllowedToScopes(
225+
context.annotations().getAnnotationValue(resourceClass, SpringConstants.SECURED));
226+
}
227+
214228
/**
215229
* Process the Spring controller Operation methods
216230
*
@@ -334,7 +348,7 @@ private void processControllerMethod(final ClassInfo resourceClass,
334348
processExtensions(context, method, operation);
335349

336350
// Process Security Roles
337-
context.getJavaSecurityProcessor().processSecurityRoles(method, operation);
351+
context.getJavaSecurityProcessor().processSecurityRoles(method, operation, () -> getDeclaredRoles(method));
338352

339353
// Now set the operation on the PathItem as appropriate based on the Http method type
340354
pathItem.setOperation(methodType, operation);
@@ -357,6 +371,21 @@ private void processControllerMethod(final ClassInfo resourceClass,
357371
}
358372
}
359373

374+
private String[] getDeclaredRoles(MethodInfo method) {
375+
Annotations annotations = context.annotations();
376+
String[] rolesAllowed = annotations.getAnnotationValue(method, SecurityConstants.ROLES_ALLOWED);
377+
String[] securedRoles = annotations.getAnnotationValue(method, SpringConstants.SECURED);
378+
379+
if (rolesAllowed == null && securedRoles == null) {
380+
return null; // NOSONAR
381+
}
382+
383+
return Stream.of(rolesAllowed, securedRoles)
384+
.filter(Objects::nonNull)
385+
.flatMap(Arrays::stream)
386+
.toArray(String[]::new);
387+
}
388+
360389
private ResourceParameters getResourceParameters(final ClassInfo resourceClass,
361390
final MethodInfo method) {
362391
Function<AnnotationInstance, Parameter> reader = t -> context.io().parameterIO().read(t);

extension-spring/src/main/java/io/smallrye/openapi/spring/SpringConstants.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
*/
1717
public class SpringConstants {
1818

19+
static final DotName SECURED = DotName.createSimple("org.springframework.security.access.annotation.Secured");
20+
1921
static final DotName REST_CONTROLLER = DotName.createSimple("org.springframework.web.bind.annotation.RestController");
2022
static final DotName REQUEST_MAPPING = DotName.createSimple("org.springframework.web.bind.annotation.RequestMapping");
2123
static final DotName GET_MAPPING = DotName.createSimple("org.springframework.web.bind.annotation.GetMapping");

extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingDeleteController.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package test.io.smallrye.openapi.runtime.scanner.resources;
22

3+
import org.eclipse.microprofile.openapi.annotations.enums.SecuritySchemeType;
34
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
5+
import org.eclipse.microprofile.openapi.annotations.security.SecurityScheme;
46
import org.springframework.http.MediaType;
57
import org.springframework.http.ResponseEntity;
8+
import org.springframework.security.access.annotation.Secured;
69
import org.springframework.web.bind.annotation.DeleteMapping;
710
import org.springframework.web.bind.annotation.PathVariable;
811
import org.springframework.web.bind.annotation.RequestMapping;
@@ -17,6 +20,8 @@
1720
*/
1821
@RestController
1922
@RequestMapping(value = "/greeting", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
23+
@Secured({ "roles:removal" })
24+
@SecurityScheme(securitySchemeName = "oauth", type = SecuritySchemeType.OAUTH2)
2025
public class GreetingDeleteController {
2126

2227
// 1) Basic path var test
@@ -28,7 +33,7 @@ public void greet(@PathVariable(name = "id") String id) {
2833
// 2) ResponseEntity without a type specified
2934
@DeleteMapping("/greetWithResponse/{id}")
3035
@APIResponse(responseCode = "204", description = "No Content")
31-
public ResponseEntity greetWithResponse(@PathVariable(name = "id") String id) {
36+
public ResponseEntity<Void> greetWithResponse(@PathVariable(name = "id") String id) {
3237
return ResponseEntity.noContent().build();
3338
}
3439

extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingDeleteControllerAlt.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package test.io.smallrye.openapi.runtime.scanner.resources;
22

3+
import jakarta.annotation.security.RolesAllowed;
4+
5+
import org.eclipse.microprofile.openapi.annotations.enums.SecuritySchemeType;
36
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
7+
import org.eclipse.microprofile.openapi.annotations.security.SecurityScheme;
48
import org.springframework.http.MediaType;
59
import org.springframework.http.ResponseEntity;
610
import org.springframework.web.bind.annotation.PathVariable;
@@ -17,6 +21,8 @@
1721
*/
1822
@RestController
1923
@RequestMapping(value = "/greeting", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
24+
@RolesAllowed({ "roles:removal" })
25+
@SecurityScheme(securitySchemeName = "oauth", type = SecuritySchemeType.OAUTH2)
2026
public class GreetingDeleteControllerAlt {
2127

2228
// 1) Basic path var test
@@ -28,7 +34,7 @@ public void greet(@PathVariable(name = "id") String id) {
2834
// 2) ResponseEntity without a type specified
2935
@RequestMapping(value = "/greetWithResponse/{id}", method = RequestMethod.DELETE)
3036
@APIResponse(responseCode = "204", description = "No Content")
31-
public ResponseEntity greetWithResponse(@PathVariable(name = "id") String id) {
37+
public ResponseEntity<Void> greetWithResponse(@PathVariable(name = "id") String id) {
3238
return ResponseEntity.noContent().build();
3339
}
3440

extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingGetController.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
import java.util.List;
55
import java.util.Optional;
66

7+
import org.eclipse.microprofile.openapi.annotations.enums.SecuritySchemeType;
78
import org.eclipse.microprofile.openapi.annotations.media.Content;
89
import org.eclipse.microprofile.openapi.annotations.media.Schema;
910
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
11+
import org.eclipse.microprofile.openapi.annotations.security.SecurityScheme;
1012
import org.springframework.http.MediaType;
1113
import org.springframework.http.ResponseEntity;
14+
import org.springframework.security.access.annotation.Secured;
1215
import org.springframework.web.bind.annotation.GetMapping;
1316
import org.springframework.web.bind.annotation.PathVariable;
1417
import org.springframework.web.bind.annotation.RequestMapping;
@@ -26,6 +29,7 @@
2629
*/
2730
@RestController
2831
@RequestMapping(value = "/greeting", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
32+
@SecurityScheme(securitySchemeName = "oauth", type = SecuritySchemeType.OAUTH2)
2933
public class GreetingGetController {
3034

3135
// 1) Basic path var test
@@ -48,11 +52,13 @@ public Optional<Greeting> helloOptional(@PathVariable(name = "name") String name
4852

4953
// 4) Basic request param test
5054
@GetMapping(path = "/helloRequestParam")
55+
@Secured({ "roles:retrieval-by-query" })
5156
public Greeting helloRequestParam(@RequestParam(value = "name", required = false) String name) {
5257
return new Greeting("Hello " + name);
5358
}
5459

5560
// 5) ResponseEntity without a type specified
61+
@SuppressWarnings("rawtypes")
5662
@GetMapping("/helloPathVariableWithResponse/{name}")
5763
@APIResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(ref = "#/components/schemas/Greeting")))
5864
public ResponseEntity helloPathVariableWithResponse(@PathVariable(name = "name") String name) {

extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingGetControllerAlt.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44
import java.util.List;
55
import java.util.Optional;
66

7+
import jakarta.annotation.security.RolesAllowed;
8+
9+
import org.eclipse.microprofile.openapi.annotations.enums.SecuritySchemeType;
710
import org.eclipse.microprofile.openapi.annotations.media.Content;
811
import org.eclipse.microprofile.openapi.annotations.media.Schema;
912
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
13+
import org.eclipse.microprofile.openapi.annotations.security.SecurityScheme;
1014
import org.springframework.http.MediaType;
1115
import org.springframework.http.ResponseEntity;
1216
import org.springframework.web.bind.annotation.PathVariable;
@@ -28,6 +32,7 @@
2832
*/
2933
@RestController
3034
@RequestMapping(value = "/greeting", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
35+
@SecurityScheme(securitySchemeName = "oauth", type = SecuritySchemeType.OAUTH2)
3136
public class GreetingGetControllerAlt {
3237

3338
// 1) Basic path var test
@@ -50,11 +55,13 @@ public Optional<Greeting> helloOptional(@PathVariable(name = "name") String name
5055

5156
// 4) Basic request param test
5257
@RequestMapping(value = "/helloRequestParam", method = RequestMethod.GET)
58+
@RolesAllowed({ "roles:retrieval-by-query" })
5359
public Greeting helloRequestParam(@RequestParam(value = "name", required = false) String name) {
5460
return new Greeting("Hello " + name);
5561
}
5662

5763
// 5) ResponseEntity without a type specified
64+
@SuppressWarnings("rawtypes")
5865
@RequestMapping(value = "/helloPathVariableWithResponse/{name}", method = RequestMethod.GET)
5966
@APIResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(ref = "#/components/schemas/Greeting")))
6067
public ResponseEntity helloPathVariableWithResponse(@PathVariable(name = "name") String name) {

extension-spring/src/test/java/test/io/smallrye/openapi/runtime/scanner/resources/GreetingGetControllerAlt2.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
import java.util.List;
55
import java.util.Optional;
66

7+
import org.eclipse.microprofile.openapi.annotations.enums.SecuritySchemeType;
78
import org.eclipse.microprofile.openapi.annotations.media.Content;
89
import org.eclipse.microprofile.openapi.annotations.media.Schema;
910
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
11+
import org.eclipse.microprofile.openapi.annotations.security.SecurityScheme;
1012
import org.springframework.http.MediaType;
1113
import org.springframework.http.ResponseEntity;
14+
import org.springframework.security.access.annotation.Secured;
1215
import org.springframework.web.bind.annotation.PathVariable;
1316
import org.springframework.web.bind.annotation.RequestMapping;
1417
import org.springframework.web.bind.annotation.RequestMethod;
@@ -28,6 +31,7 @@
2831
*/
2932
@RestController
3033
@RequestMapping(path = "/greeting", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
34+
@SecurityScheme(securitySchemeName = "oauth", type = SecuritySchemeType.OAUTH2)
3135
public class GreetingGetControllerAlt2 {
3236

3337
// 1) Basic path var test
@@ -50,11 +54,13 @@ public Optional<Greeting> helloOptional(@PathVariable(name = "name") String name
5054

5155
// 4) Basic request param test
5256
@RequestMapping(path = "/helloRequestParam", method = RequestMethod.GET)
57+
@Secured({ "roles:retrieval-by-query" })
5358
public Greeting helloRequestParam(@RequestParam(value = "name", required = false) String name) {
5459
return new Greeting("Hello " + name);
5560
}
5661

5762
// 5) ResponseEntity without a type specified
63+
@SuppressWarnings("rawtypes")
5864
@RequestMapping(path = "/helloPathVariableWithResponse/{name}", method = RequestMethod.GET)
5965
@APIResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(ref = "#/components/schemas/Greeting")))
6066
public ResponseEntity helloPathVariableWithResponse(@PathVariable(name = "name") String name) {

0 commit comments

Comments
 (0)