Skip to content

Commit

Permalink
Include allowed roles in security scheme scopes in OpenApi 3.1+
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Nov 10, 2024
1 parent b5a1eac commit bd03e6c
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@ private OASFilter getOperationFilter(OpenApiFilteredIndexViewBuildItem indexView

if (!classNamesMethods.isEmpty() || !rolesAllowedMethods.isEmpty() || !authenticatedMethods.isEmpty()) {
return new OperationFilter(classNamesMethods, rolesAllowedMethods, authenticatedMethods, config.securitySchemeName,
config.autoAddTags, config.autoAddOperationSummary);
config.autoAddTags, config.autoAddOperationSummary, isOpenApi_3_1_0_OrGreater(config));
}

return null;
Expand Down Expand Up @@ -1169,4 +1169,9 @@ private List<String> getResourceFiles(Path resourcePath, Path target) {
}
return filenames;
}

private static boolean isOpenApi_3_1_0_OrGreater(SmallRyeOpenApiConfig config) {
final String openApiVersion = config.openApiVersion.orElse(null);
return openApiVersion == null || (!openApiVersion.startsWith("2") && !openApiVersion.startsWith("3.0"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,26 +42,28 @@ public class OperationFilter implements OASFilter {
private final String defaultSecuritySchemeName;
private final boolean doAutoTag;
private final boolean doAutoOperation;
private final boolean alwaysIncludeScopesValidForScheme;

public OperationFilter(Map<String, ClassAndMethod> classNameMap,
Map<String, List<String>> rolesAllowedMethodReferences,
List<String> authenticatedMethodReferences,
String defaultSecuritySchemeName,
boolean doAutoTag, boolean doAutoOperation) {
boolean doAutoTag, boolean doAutoOperation, boolean alwaysIncludeScopesValidForScheme) {

this.classNameMap = Objects.requireNonNull(classNameMap);
this.rolesAllowedMethodReferences = Objects.requireNonNull(rolesAllowedMethodReferences);
this.authenticatedMethodReferences = Objects.requireNonNull(authenticatedMethodReferences);
this.defaultSecuritySchemeName = Objects.requireNonNull(defaultSecuritySchemeName);
this.doAutoTag = doAutoTag;
this.doAutoOperation = doAutoOperation;
this.alwaysIncludeScopesValidForScheme = alwaysIncludeScopesValidForScheme;
}

@Override
public void filterOpenAPI(OpenAPI openAPI) {
var securityScheme = getSecurityScheme(openAPI);
String schemeName = securityScheme.map(Map.Entry::getKey).orElse(defaultSecuritySchemeName);
boolean scopesValidForScheme = securityScheme.map(Map.Entry::getValue)
boolean scopesValidForScheme = alwaysIncludeScopesValidForScheme || securityScheme.map(Map.Entry::getValue)
.map(SecurityScheme::getType)
.map(Set.of(SecurityScheme.Type.OAUTH2, SecurityScheme.Type.OPENIDCONNECT)::contains)
.orElse(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static org.hamcrest.Matchers.aMapWithSize;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.emptyIterable;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasEntry;
Expand Down Expand Up @@ -65,20 +66,20 @@ void testAutoSecurityRequirement() {
not(hasKey("my-extension2"))))
.and()
// OpenApiResourceSecuredAtMethodLevel
.body("paths.'/resource2/test-security/naked'.get.security", defaultSecurity)
.body("paths.'/resource2/test-security/naked'.get.security", defaultSecurityScheme("admin"))
.body("paths.'/resource2/test-security/annotated'.get.security", defaultSecurity)
.body("paths.'/resource2/test-security/methodLevel/1'.get.security", defaultSecurity)
.body("paths.'/resource2/test-security/methodLevel/2'.get.security", defaultSecurity)
.body("paths.'/resource2/test-security/methodLevel/1'.get.security", defaultSecurityScheme("user1"))
.body("paths.'/resource2/test-security/methodLevel/2'.get.security", defaultSecurityScheme("user2"))
.body("paths.'/resource2/test-security/methodLevel/public'.get.security", nullValue())
.body("paths.'/resource2/test-security/annotated/documented'.get.security", defaultSecurity)
.body("paths.'/resource2/test-security/methodLevel/3'.get.security", defaultSecurity)
.body("paths.'/resource2/test-security/methodLevel/3'.get.security", defaultSecurityScheme("admin"))
.body("paths.'/resource2/test-security/methodLevel/4'.get.security", defaultSecurity)
.and()
// OpenApiResourceSecuredAtClassLevel
.body("paths.'/resource2/test-security/classLevel/1'.get.security", defaultSecurity)
.body("paths.'/resource2/test-security/classLevel/2'.get.security", defaultSecurity)
.body("paths.'/resource2/test-security/classLevel/1'.get.security", defaultSecurityScheme("user1"))
.body("paths.'/resource2/test-security/classLevel/2'.get.security", defaultSecurityScheme("user2"))
.body("paths.'/resource2/test-security/classLevel/3'.get.security", schemeArray("MyOwnName"))
.body("paths.'/resource2/test-security/classLevel/4'.get.security", defaultSecurity)
.body("paths.'/resource2/test-security/classLevel/4'.get.security", defaultSecurityScheme("admin"))
.and()
// OpenApiResourceSecuredAtMethodLevel2
.body("paths.'/resource3/test-security/annotated'.get.security", schemeArray("AtClassLevel"))
Expand Down Expand Up @@ -173,4 +174,11 @@ void testOpenAPIAnnotations() {
Matchers.equalTo("Not Allowed"));
}

static Matcher<Iterable<Object>> defaultSecurityScheme(String... roles) {
return allOf(
iterableWithSize(1),
hasItem(allOf(
aMapWithSize(1),
hasEntry(equalTo("JWTCompanyAuthentication"), containsInAnyOrder(roles)))));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package io.quarkus.smallrye.openapi.test.jaxrs;

import static org.hamcrest.Matchers.aMapWithSize;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.emptyIterable;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasKey;
import static org.hamcrest.Matchers.iterableWithSize;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;

import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;

/**
* Run same tests as {@link AutoSecurityRolesAllowedTestCase}, but with OpenAPI version 3.0.2
* that only allowed security requirement scopes for Oauth2 and OpenID Connect schemes.
*/
public class AutoSecurityRolesAllowedUnsupportedScopesTestCase {

@RegisterExtension
static QuarkusUnitTest runner = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(ResourceBean.class, OpenApiResourceSecuredAtClassLevel.class,
OpenApiResourceSecuredAtClassLevel2.class, OpenApiResourceSecuredAtMethodLevel.class,
OpenApiResourceSecuredAtMethodLevel2.class)
.addAsResource(
new StringAsset("""
quarkus.smallrye-openapi.open-api-version=3.0.2
quarkus.smallrye-openapi.security-scheme=jwt
quarkus.smallrye-openapi.security-scheme-name=JWTCompanyAuthentication
quarkus.smallrye-openapi.security-scheme-description=JWT Authentication
quarkus.smallrye-openapi.security-scheme-extensions.x-my-extension1=extension-value
quarkus.smallrye-openapi.security-scheme-extensions.my-extension2=extension-value
"""),
"application.properties"));

static Matcher<Iterable<Object>> schemeArray(String schemeName) {
return allOf(
iterableWithSize(1),
hasItem(allOf(
aMapWithSize(1),
hasEntry(equalTo(schemeName), emptyIterable()))));
}

@Test
void testAutoSecurityRequirement() {
var defaultSecurity = schemeArray("JWTCompanyAuthentication");

RestAssured.given()
.header("Accept", "application/json")
.when()
.get("/q/openapi")
.then()
.log().body()
.and()
.body("openapi", Matchers.is("3.0.2"))
.body("components.securitySchemes.JWTCompanyAuthentication", allOf(
hasEntry("type", "http"),
hasEntry("scheme", "bearer"),
hasEntry("bearerFormat", "JWT"),
hasEntry("description", "JWT Authentication"),
hasEntry("x-my-extension1", "extension-value"),
not(hasKey("my-extension2"))))
.and()
// OpenApiResourceSecuredAtMethodLevel
.body("paths.'/resource2/test-security/naked'.get.security", defaultSecurity)
.body("paths.'/resource2/test-security/annotated'.get.security", defaultSecurity)
.body("paths.'/resource2/test-security/methodLevel/1'.get.security", defaultSecurity)
.body("paths.'/resource2/test-security/methodLevel/2'.get.security", defaultSecurity)
.body("paths.'/resource2/test-security/methodLevel/public'.get.security", nullValue())
.body("paths.'/resource2/test-security/annotated/documented'.get.security", defaultSecurity)
.body("paths.'/resource2/test-security/methodLevel/3'.get.security", defaultSecurity)
.body("paths.'/resource2/test-security/methodLevel/4'.get.security", defaultSecurity)
.and()
// OpenApiResourceSecuredAtClassLevel
.body("paths.'/resource2/test-security/classLevel/1'.get.security", defaultSecurity)
.body("paths.'/resource2/test-security/classLevel/2'.get.security", defaultSecurity)
.body("paths.'/resource2/test-security/classLevel/3'.get.security", schemeArray("MyOwnName"))
.body("paths.'/resource2/test-security/classLevel/4'.get.security", defaultSecurity)
.and()
// OpenApiResourceSecuredAtMethodLevel2
.body("paths.'/resource3/test-security/annotated'.get.security", schemeArray("AtClassLevel"))
.and()
// OpenApiResourceSecuredAtClassLevel2
.body("paths.'/resource3/test-security/classLevel-2/1'.get.security", defaultSecurity);
}

@Test
void testOpenAPIAnnotations() {
RestAssured.given().header("Accept", "application/json")
.when().get("/q/openapi")
.then()
.log().body()
.and()
.body("paths.'/resource2/test-security/classLevel/1'.get.responses.401.description",
Matchers.equalTo("Not Authorized"))
.and()
.body("paths.'/resource2/test-security/classLevel/1'.get.responses.403.description",
Matchers.equalTo("Not Allowed"))
.and()
.body("paths.'/resource2/test-security/classLevel/2'.get.responses.401.description",
Matchers.equalTo("Not Authorized"))
.and()
.body("paths.'/resource2/test-security/classLevel/2'.get.responses.403.description",
Matchers.equalTo("Not Allowed"))
.and()
.body("paths.'/resource2/test-security/classLevel/3'.get.responses.401.description",
Matchers.nullValue())
.and()
.body("paths.'/resource2/test-security/classLevel/3'.get.responses.403.description",
Matchers.nullValue())
.and()
.body("paths.'/resource2/test-security/classLevel/4'.get.responses.401.description",
Matchers.equalTo("Who are you?"))
.and()
.body("paths.'/resource2/test-security/classLevel/4'.get.responses.403.description",
Matchers.equalTo("You cannot do that."))
.and()
.body("paths.'/resource2/test-security/naked'.get.responses.401.description",
Matchers.equalTo("Not Authorized"))
.and()
.body("paths.'/resource2/test-security/naked'.get.responses.403.description",
Matchers.equalTo("Not Allowed"))
.and()
.body("paths.'/resource2/test-security/annotated'.get.responses.401.description",
Matchers.nullValue())
.and()
.body("paths.'/resource2/test-security/annotated'.get.responses.403.description",
Matchers.nullValue())
.and()
.body("paths.'/resource2/test-security/methodLevel/1'.get.responses.401.description",
Matchers.equalTo("Not Authorized"))
.and()
.body("paths.'/resource2/test-security/methodLevel/1'.get.responses.403.description",
Matchers.equalTo("Not Allowed"))
.and()
.body("paths.'/resource2/test-security/methodLevel/2'.get.responses.401.description",
Matchers.equalTo("Not Authorized"))
.and()
.body("paths.'/resource2/test-security/methodLevel/2'.get.responses.403.description",
Matchers.equalTo("Not Allowed"))
.and()
.body("paths.'/resource2/test-security/methodLevel/public'.get.responses.401.description",
Matchers.nullValue())
.and()
.body("paths.'/resource2/test-security/methodLevel/public'.get.responses.403.description",
Matchers.nullValue())
.and()
.body("paths.'/resource2/test-security/annotated/documented'.get.responses.401.description",
Matchers.equalTo("Who are you?"))
.and()
.body("paths.'/resource2/test-security/annotated/documented'.get.responses.403.description",
Matchers.equalTo("You cannot do that."))
.and()
.body("paths.'/resource2/test-security/methodLevel/3'.get.responses.401.description",
Matchers.equalTo("Who are you?"))
.and()
.body("paths.'/resource2/test-security/methodLevel/3'.get.responses.403.description",
Matchers.equalTo("You cannot do that."))
.and()
.body("paths.'/resource2/test-security/methodLevel/4'.get.responses.401.description",
Matchers.equalTo("Not Authorized"))
.and()
.body("paths.'/resource2/test-security/methodLevel/4'.get.responses.403.description",
Matchers.equalTo("Not Allowed"))
.and()
.body("paths.'/resource3/test-security/classLevel-2/1'.get.responses.401.description",
Matchers.equalTo("Not Authorized"))
.and()
.body("paths.'/resource3/test-security/classLevel-2/1'.get.responses.403.description",
Matchers.equalTo("Not Allowed"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import static org.hamcrest.Matchers.aMapWithSize;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.emptyIterable;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.equalToObject;
import static org.hamcrest.Matchers.hasEntry;
Expand All @@ -24,18 +24,18 @@ class AutoSecurityRolesAllowedWithInterfaceTestCase {
.addClasses(ApplicationContext.class,
FooAPI.class, FooResource.class));

static Matcher<Iterable<Object>> schemeArray(String schemeName) {
static Matcher<Iterable<Object>> schemeArray(String schemeName, String... roles) {
return allOf(
iterableWithSize(1),
hasItem(allOf(
aMapWithSize(1),
hasEntry(equalTo(schemeName), emptyIterable()))));
hasEntry(equalTo(schemeName), containsInAnyOrder(roles)))));
}

@Test
void testAutoSecurityRequirement() {

var oidcAuth = schemeArray("oidc_auth");
var oidcAuth = schemeArray("oidc_auth", "RoleXY");

RestAssured.given()
.header("Accept", "application/json")
Expand Down

0 comments on commit bd03e6c

Please sign in to comment.