diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java index 2345f482c5f80..3fc9752fd8f91 100644 --- a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java @@ -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; @@ -1169,4 +1169,9 @@ private List 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")); + } } diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/OperationFilter.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/OperationFilter.java index f33c0e56460bd..640a919febf5d 100644 --- a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/OperationFilter.java +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/OperationFilter.java @@ -42,12 +42,13 @@ public class OperationFilter implements OASFilter { private final String defaultSecuritySchemeName; private final boolean doAutoTag; private final boolean doAutoOperation; + private final boolean alwaysIncludeScopesValidForScheme; public OperationFilter(Map classNameMap, Map> rolesAllowedMethodReferences, List authenticatedMethodReferences, String defaultSecuritySchemeName, - boolean doAutoTag, boolean doAutoOperation) { + boolean doAutoTag, boolean doAutoOperation, boolean alwaysIncludeScopesValidForScheme) { this.classNameMap = Objects.requireNonNull(classNameMap); this.rolesAllowedMethodReferences = Objects.requireNonNull(rolesAllowedMethodReferences); @@ -55,13 +56,14 @@ public OperationFilter(Map classNameMap, 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); diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedTestCase.java index 7a95fe1aaf5f3..ecbd54b7f603b 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedTestCase.java @@ -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; @@ -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")) @@ -173,4 +174,11 @@ void testOpenAPIAnnotations() { Matchers.equalTo("Not Allowed")); } + static Matcher> defaultSecurityScheme(String... roles) { + return allOf( + iterableWithSize(1), + hasItem(allOf( + aMapWithSize(1), + hasEntry(equalTo("JWTCompanyAuthentication"), containsInAnyOrder(roles))))); + } } diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedUnsupportedScopesTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedUnsupportedScopesTestCase.java new file mode 100644 index 0000000000000..fd7f6b9244352 --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedUnsupportedScopesTestCase.java @@ -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> 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")); + } + +} diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedWithInterfaceTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedWithInterfaceTestCase.java index 086456e1f0757..c2063d53f6c88 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedWithInterfaceTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoSecurityRolesAllowedWithInterfaceTestCase.java @@ -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; @@ -24,18 +24,18 @@ class AutoSecurityRolesAllowedWithInterfaceTestCase { .addClasses(ApplicationContext.class, FooAPI.class, FooResource.class)); - static Matcher> schemeArray(String schemeName) { + static Matcher> 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")