Skip to content

Commit

Permalink
Scan Spring Security @Secured annotation
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Edgar <michael@xlate.io>
  • Loading branch information
MikeEdgar committed Nov 11, 2024
1 parent 458fb2f commit f019328
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand All @@ -29,7 +32,9 @@
public class JavaSecurityProcessor {

public void addRolesAllowedToScopes(String[] roles) {
resourceRolesAllowed = roles;
if (roles != null) {
resourceRolesAllowed.addAll(Arrays.asList(roles));
}
addScopes(roles);
}

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

public void processSecurityRoles(MethodInfo method, Operation operation) {
processSecurityRolesForMethodOperation(method, operation);
processSecurityRolesForMethodOperation(method, operation,
() -> context.annotations().getAnnotationValue(method, SecurityConstants.ROLES_ALLOWED));
}

public void processSecurityRoles(MethodInfo method, Operation operation, Supplier<String[]> roleSupplier) {
processSecurityRolesForMethodOperation(method, operation, roleSupplier);
}

private final AnnotationScannerContext context;
private String currentSecurityScheme;
private List<OAuthFlow> currentFlows;
private String[] resourceRolesAllowed;
private final Set<String> resourceRolesAllowed = new LinkedHashSet<>();

public JavaSecurityProcessor(AnnotationScannerContext context) {
this.context = context;
Expand All @@ -53,7 +63,7 @@ public JavaSecurityProcessor(AnnotationScannerContext context) {
public void initialize(OpenAPI openApi) {
currentSecurityScheme = null;
currentFlows = null;
resourceRolesAllowed = null;
resourceRolesAllowed.clear();
checkSecurityScheme(openApi);
}

Expand Down Expand Up @@ -96,21 +106,22 @@ private void addScopes(String[] roles) {
* @param method the current JAX-RS method
* @param operation the OpenAPI Operation
*/
private void processSecurityRolesForMethodOperation(MethodInfo method, Operation operation) {
private void processSecurityRolesForMethodOperation(MethodInfo method, Operation operation,
Supplier<String[]> roleSupplier) {
if (this.currentSecurityScheme != null) {
String[] rolesAllowed = context.annotations().getAnnotationValue(method, SecurityConstants.ROLES_ALLOWED);
String[] rolesAllowed = roleSupplier.get();

if (rolesAllowed != null) {
addScopes(rolesAllowed);
addRolesAllowed(operation, rolesAllowed);
} else if (this.resourceRolesAllowed != null) {
} else if (!this.resourceRolesAllowed.isEmpty()) {
boolean denyAll = context.annotations().getAnnotation(method, SecurityConstants.DENY_ALL) != null;
boolean permitAll = context.annotations().getAnnotation(method, SecurityConstants.PERMIT_ALL) != null;

if (denyAll) {
addRolesAllowed(operation, new String[0]);
} else if (!permitAll) {
addRolesAllowed(operation, this.resourceRolesAllowed);
addRolesAllowed(operation, this.resourceRolesAllowed.toArray(String[]::new));
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions extension-spring/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

<properties>
<version.spring>6.1.13</version.spring>
<version.spring-security>6.3.4</version.spring-security>
</properties>

<dependencyManagement>
Expand Down Expand Up @@ -63,6 +64,12 @@
<artifactId>spring-webmvc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>${version.spring-security}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Stream;

import org.eclipse.microprofile.openapi.OASFactory;
import org.eclipse.microprofile.openapi.models.OpenAPI;
Expand All @@ -24,13 +26,16 @@
import org.jboss.jandex.MethodParameterInfo;
import org.jboss.jandex.Type;

import io.smallrye.openapi.api.constants.SecurityConstants;
import io.smallrye.openapi.api.util.ListUtil;
import io.smallrye.openapi.api.util.MergeUtil;
import io.smallrye.openapi.runtime.scanner.AnnotationScannerExtension;
import io.smallrye.openapi.runtime.scanner.ResourceParameters;
import io.smallrye.openapi.runtime.scanner.dataobject.TypeResolver;
import io.smallrye.openapi.runtime.scanner.processor.JavaSecurityProcessor;
import io.smallrye.openapi.runtime.scanner.spi.AbstractAnnotationScanner;
import io.smallrye.openapi.runtime.scanner.spi.AnnotationScannerContext;
import io.smallrye.openapi.runtime.util.Annotations;
import io.smallrye.openapi.runtime.util.ModelUtil;

/**
Expand Down Expand Up @@ -211,6 +216,15 @@ private OpenAPI processControllerClass(ClassInfo controllerClass) {
return openApi;
}

@Override
public void processJavaSecurity(AnnotationScannerContext context, ClassInfo resourceClass, OpenAPI openApi) {
super.processJavaSecurity(context, resourceClass, openApi);
JavaSecurityProcessor securityProcessor = context.getJavaSecurityProcessor();
securityProcessor
.addRolesAllowedToScopes(
context.annotations().getAnnotationValue(resourceClass, SpringConstants.SECURED));
}

/**
* Process the Spring controller Operation methods
*
Expand Down Expand Up @@ -334,7 +348,7 @@ private void processControllerMethod(final ClassInfo resourceClass,
processExtensions(context, method, operation);

// Process Security Roles
context.getJavaSecurityProcessor().processSecurityRoles(method, operation);
context.getJavaSecurityProcessor().processSecurityRoles(method, operation, () -> getDeclaredRoles(method));

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

private String[] getDeclaredRoles(MethodInfo method) {
Annotations annotations = context.annotations();
String[] rolesAllowed = annotations.getAnnotationValue(method, SecurityConstants.ROLES_ALLOWED);
String[] securedRoles = annotations.getAnnotationValue(method, SpringConstants.SECURED);

if (rolesAllowed == null && securedRoles == null) {
return null; // NOSONAR
}

return Stream.of(rolesAllowed, securedRoles)
.filter(Objects::nonNull)
.flatMap(Arrays::stream)
.toArray(String[]::new);
}

private ResourceParameters getResourceParameters(final ClassInfo resourceClass,
final MethodInfo method) {
Function<AnnotationInstance, Parameter> reader = t -> context.io().parameterIO().read(t);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
*/
public class SpringConstants {

static final DotName SECURED = DotName.createSimple("org.springframework.security.access.annotation.Secured");

static final DotName REST_CONTROLLER = DotName.createSimple("org.springframework.web.bind.annotation.RestController");
static final DotName REQUEST_MAPPING = DotName.createSimple("org.springframework.web.bind.annotation.RequestMapping");
static final DotName GET_MAPPING = DotName.createSimple("org.springframework.web.bind.annotation.GetMapping");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package test.io.smallrye.openapi.runtime.scanner.resources;

import org.eclipse.microprofile.openapi.annotations.enums.SecuritySchemeType;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.security.SecurityScheme;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -17,6 +20,8 @@
*/
@RestController
@RequestMapping(value = "/greeting", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
@Secured({ "roles:removal" })
@SecurityScheme(securitySchemeName = "oauth", type = SecuritySchemeType.OAUTH2)
public class GreetingDeleteController {

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package test.io.smallrye.openapi.runtime.scanner.resources;

import jakarta.annotation.security.RolesAllowed;

import org.eclipse.microprofile.openapi.annotations.enums.SecuritySchemeType;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.security.SecurityScheme;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
Expand All @@ -17,6 +21,8 @@
*/
@RestController
@RequestMapping(value = "/greeting", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
@RolesAllowed({ "roles:removal" })
@SecurityScheme(securitySchemeName = "oauth", type = SecuritySchemeType.OAUTH2)
public class GreetingDeleteControllerAlt {

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
{
"openapi" : "3.1.0",
"components" : {
"securitySchemes" : {
"oauth" : {
"type" : "oauth2"
}
}
},
"paths" : {
"/greeting/greet/{id}" : {
"delete" : {
Expand All @@ -15,7 +22,10 @@
"204" : {
"description" : "No Content"
}
}
},
"security" : [ {
"oauth" : [ "roles:removal" ]
} ]
}
},
"/greeting/greetWithResponse/{id}" : {
Expand All @@ -32,7 +42,10 @@
"204" : {
"description" : "No Content"
}
}
},
"security" : [ {
"oauth" : [ "roles:removal" ]
} ]
}
},
"/greeting/greetWithResponseTyped/{id}" : {
Expand All @@ -49,8 +62,11 @@
"204" : {
"description" : "No Content"
}
}
},
"security" : [ {
"oauth" : [ "roles:removal" ]
} ]
}
}
}
}
}

0 comments on commit f019328

Please sign in to comment.