From f16c03f50cb125cc6e0420b8bd1df0b839e597fe Mon Sep 17 00:00:00 2001 From: Phillip Kruger Date: Tue, 17 Aug 2021 21:34:26 +0200 Subject: [PATCH] OpenAPI: Added AutoTag and Response for Auto Security Requirement Signed-off-by:Phillip Kruger --- .../deployment/SmallRyeOpenApiConfig.java | 6 + .../deployment/SmallRyeOpenApiProcessor.java | 114 ++++++++++++++++- .../security/AutoRolesAllowedFilter.java | 120 ++++++++++++++++++ .../deployment/security/AutoTagFilter.java | 75 +++++++++++ .../security/SecurityConfigFilter.java | 42 +----- .../openapi/test/jaxrs/AutoTagTestCase.java | 33 +++++ .../test/jaxrs/OpenApiResourceWithNoTag.java | 31 +++++ 7 files changed, 377 insertions(+), 44 deletions(-) create mode 100644 extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/security/AutoRolesAllowedFilter.java create mode 100644 extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/security/AutoTagFilter.java create mode 100644 extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoTagTestCase.java create mode 100644 extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiResourceWithNoTag.java diff --git a/extensions/smallrye-openapi-common/deployment/src/main/java/io/quarkus/smallrye/openapi/common/deployment/SmallRyeOpenApiConfig.java b/extensions/smallrye-openapi-common/deployment/src/main/java/io/quarkus/smallrye/openapi/common/deployment/SmallRyeOpenApiConfig.java index 02174d5c3ef96f..ee2c9295eb5833 100644 --- a/extensions/smallrye-openapi-common/deployment/src/main/java/io/quarkus/smallrye/openapi/common/deployment/SmallRyeOpenApiConfig.java +++ b/extensions/smallrye-openapi-common/deployment/src/main/java/io/quarkus/smallrye/openapi/common/deployment/SmallRyeOpenApiConfig.java @@ -64,6 +64,12 @@ public final class SmallRyeOpenApiConfig { @ConfigItem(defaultValue = "true") public boolean autoAddSecurityRequirement; + /** + * This will automatically add tags to operations based on the Java class name. + */ + @ConfigItem(defaultValue = "true") + public boolean autoAddTags; + /** * Add a scheme value to the Basic HTTP Security Scheme */ 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 2a96133a559a41..ef1a5bf4f6a837 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 @@ -68,6 +68,8 @@ import io.quarkus.resteasy.server.common.spi.ResteasyJaxrsConfigBuildItem; import io.quarkus.runtime.LaunchMode; import io.quarkus.smallrye.openapi.common.deployment.SmallRyeOpenApiConfig; +import io.quarkus.smallrye.openapi.deployment.security.AutoRolesAllowedFilter; +import io.quarkus.smallrye.openapi.deployment.security.AutoTagFilter; import io.quarkus.smallrye.openapi.deployment.security.SecurityConfigFilter; import io.quarkus.smallrye.openapi.deployment.spi.AddToOpenAPIDefinitionBuildItem; import io.quarkus.smallrye.openapi.runtime.OpenApiConstants; @@ -252,8 +254,105 @@ void addSecurityFilter(BuildProducer addToOpenA } } - addToOpenAPIDefinitionProducer - .produce(new AddToOpenAPIDefinitionBuildItem(new SecurityConfigFilter(config, methodReferences))); + // Add a security scheme from config + if (config.securityScheme.isPresent()) { + addToOpenAPIDefinitionProducer + .produce(new AddToOpenAPIDefinitionBuildItem( + new SecurityConfigFilter(config))); + } + + // Add Auto roles allowed + OASFilter autoRolesAllowedFilter = getAutoRolesAllowedFilter(config.securitySchemeName, apiFilteredIndexViewBuildItem, + config); + if (autoRolesAllowedFilter != null) { + addToOpenAPIDefinitionProducer.produce(new AddToOpenAPIDefinitionBuildItem(autoRolesAllowedFilter)); + } + + // Add Auto Tag based on the class name + OASFilter autoTagFilter = getAutoTagFilter(apiFilteredIndexViewBuildItem, + config); + if (autoTagFilter != null) { + addToOpenAPIDefinitionProducer.produce(new AddToOpenAPIDefinitionBuildItem(autoTagFilter)); + } + + } + + private OASFilter getAutoRolesAllowedFilter(String securitySchemeName, + OpenApiFilteredIndexViewBuildItem apiFilteredIndexViewBuildItem, + SmallRyeOpenApiConfig config) { + if (config.autoAddSecurityRequirement) { + Map> rolesAllowedMethodReferences = getRolesAllowedMethodReferences( + apiFilteredIndexViewBuildItem); + if (rolesAllowedMethodReferences != null && !rolesAllowedMethodReferences.isEmpty()) { + if (securitySchemeName == null) { + securitySchemeName = config.securitySchemeName; + } + return new AutoRolesAllowedFilter(securitySchemeName, rolesAllowedMethodReferences); + } + } + return null; + } + + private OASFilter getAutoTagFilter(OpenApiFilteredIndexViewBuildItem apiFilteredIndexViewBuildItem, + SmallRyeOpenApiConfig config) { + + if (config.autoAddTags) { + + Map classNamesMethodReferences = getClassNamesMethodReferences(apiFilteredIndexViewBuildItem); + if (classNamesMethodReferences != null && !classNamesMethodReferences.isEmpty()) { + return new AutoTagFilter(classNamesMethodReferences); + } + } + return null; + } + + private Map> getRolesAllowedMethodReferences( + OpenApiFilteredIndexViewBuildItem apiFilteredIndexViewBuildItem) { + List rolesAllowedAnnotations = new ArrayList<>(); + for (DotName rolesAllowed : SecurityConstants.ROLES_ALLOWED) { + rolesAllowedAnnotations.addAll(apiFilteredIndexViewBuildItem.getIndex().getAnnotations(rolesAllowed)); + } + Map> methodReferences = new HashMap<>(); + DotName securityRequirement = DotName.createSimple(SecurityRequirement.class.getName()); + for (AnnotationInstance ai : rolesAllowedAnnotations) { + if (ai.target().kind().equals(AnnotationTarget.Kind.METHOD)) { + MethodInfo method = ai.target().asMethod(); + if (isValidOpenAPIMethodForAutoAdd(method, securityRequirement)) { + String ref = JandexUtil.createUniqueMethodReference(method); + methodReferences.put(ref, List.of(ai.value().asStringArray())); + } + } + if (ai.target().kind().equals(AnnotationTarget.Kind.CLASS)) { + ClassInfo classInfo = ai.target().asClass(); + List methods = classInfo.methods(); + for (MethodInfo method : methods) { + if (isValidOpenAPIMethodForAutoAdd(method, securityRequirement)) { + String ref = JandexUtil.createUniqueMethodReference(method); + methodReferences.put(ref, List.of(ai.value().asStringArray())); + } + } + } + } + return methodReferences; + } + + private Map getClassNamesMethodReferences(OpenApiFilteredIndexViewBuildItem apiFilteredIndexViewBuildItem) { + List openapiAnnotations = new ArrayList<>(); + Set allOpenAPIEndpoints = getAllOpenAPIEndpoints(); + for (DotName dotName : allOpenAPIEndpoints) { + openapiAnnotations.addAll(apiFilteredIndexViewBuildItem.getIndex().getAnnotations(dotName)); + } + + Map classNames = new HashMap<>(); + + for (AnnotationInstance ai : openapiAnnotations) { + if (ai.target().kind().equals(AnnotationTarget.Kind.METHOD)) { + MethodInfo method = ai.target().asMethod(); + String ref = JandexUtil.createUniqueMethodReference(method); + classNames.put(ref, method.declaringClass().simpleName()); + } + } + return classNames; } private boolean isValidOpenAPIMethodForAutoAdd(MethodInfo method, DotName securityRequirement) { @@ -310,9 +409,7 @@ public void registerOpenApiSchemaClassesForReflection(BuildProducer httpAnnotations = new HashSet<>(); - httpAnnotations.addAll(JaxRsConstants.HTTP_METHODS); - httpAnnotations.addAll(SpringConstants.HTTP_METHODS); + Set httpAnnotations = getAllOpenAPIEndpoints(); for (DotName httpAnnotation : httpAnnotations) { if (method.hasAnnotation(httpAnnotation)) { return true; @@ -321,6 +418,13 @@ private boolean isOpenAPIEndpoint(MethodInfo method) { return false; } + private Set getAllOpenAPIEndpoints() { + Set httpAnnotations = new HashSet<>(); + httpAnnotations.addAll(JaxRsConstants.HTTP_METHODS); + httpAnnotations.addAll(SpringConstants.HTTP_METHODS); + return httpAnnotations; + } + private void registerReflectionForApiResponseSchemaSerialization(BuildProducer reflectiveClass, BuildProducer reflectiveHierarchy, Collection apiResponseAnnotationInstances) { diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/security/AutoRolesAllowedFilter.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/security/AutoRolesAllowedFilter.java new file mode 100644 index 00000000000000..5aac9bf4039a43 --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/security/AutoRolesAllowedFilter.java @@ -0,0 +1,120 @@ +package io.quarkus.smallrye.openapi.deployment.security; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.microprofile.openapi.OASFilter; +import org.eclipse.microprofile.openapi.models.OpenAPI; +import org.eclipse.microprofile.openapi.models.Operation; +import org.eclipse.microprofile.openapi.models.PathItem; +import org.eclipse.microprofile.openapi.models.Paths; +import org.eclipse.microprofile.openapi.models.responses.APIResponses; +import org.eclipse.microprofile.openapi.models.security.SecurityRequirement; +import org.eclipse.microprofile.openapi.models.security.SecurityScheme; +import org.jboss.logging.Logger; + +import io.smallrye.openapi.api.models.OperationImpl; +import io.smallrye.openapi.api.models.responses.APIResponseImpl; +import io.smallrye.openapi.api.models.security.SecurityRequirementImpl; + +/** + * Automatically add security requirement to RolesAllowed methods + */ +public class AutoRolesAllowedFilter implements OASFilter { + private static final Logger log = Logger.getLogger(AutoRolesAllowedFilter.class); + + private Map> methodReferences; + private String defaultSecuritySchemeName; + + public AutoRolesAllowedFilter() { + + } + + public AutoRolesAllowedFilter(String defaultSecuritySchemeName, Map> methodReferences) { + this.defaultSecuritySchemeName = defaultSecuritySchemeName; + this.methodReferences = methodReferences; + } + + public Map> getMethodReferences() { + return methodReferences; + } + + public void setMethodReferences(Map> methodReferences) { + this.methodReferences = methodReferences; + } + + public String getDefaultSecuritySchemeName() { + return defaultSecuritySchemeName; + } + + public void setDefaultSecuritySchemeName(String defaultSecuritySchemeName) { + this.defaultSecuritySchemeName = defaultSecuritySchemeName; + } + + @Override + public void filterOpenAPI(OpenAPI openAPI) { + + if (!methodReferences.isEmpty()) { + String securitySchemeName = getSecuritySchemeName(openAPI); + Paths paths = openAPI.getPaths(); + if (paths != null) { + Map pathItems = paths.getPathItems(); + if (pathItems != null && !pathItems.isEmpty()) { + Set> pathItemsEntries = pathItems.entrySet(); + for (Map.Entry pathItem : pathItemsEntries) { + Map operations = pathItem.getValue().getOperations(); + if (operations != null && !operations.isEmpty()) { + + for (Operation operation : operations.values()) { + + OperationImpl operationImpl = (OperationImpl) operation; + + if (methodReferences.keySet().contains(operationImpl.getMethodRef())) { + SecurityRequirement securityRequirement = new SecurityRequirementImpl(); + List roles = methodReferences.get(operationImpl.getMethodRef()); + securityRequirement = securityRequirement.addScheme(securitySchemeName, roles); + operation = operation.addSecurityRequirement(securityRequirement); + APIResponses responses = operation.getResponses(); + for (APIResponseImpl response : getSecurityResponses()) { + responses.addAPIResponse(response.getResponseCode(), response); + } + operation = operation.responses(responses); + } + } + } + } + } + } + } + } + + private String getSecuritySchemeName(OpenAPI openAPI) { + + // Might be set in annotations + if (openAPI.getComponents() != null && openAPI.getComponents().getSecuritySchemes() != null + && !openAPI.getComponents().getSecuritySchemes().isEmpty()) { + Map securitySchemes = openAPI.getComponents().getSecuritySchemes(); + return securitySchemes.keySet().iterator().next(); + } + return defaultSecuritySchemeName; + } + + private List getSecurityResponses() { + List responses = new ArrayList<>(); + + APIResponseImpl notAuthorized = new APIResponseImpl(); + notAuthorized.setDescription("Not Authorized"); + notAuthorized.setResponseCode("401"); + responses.add(notAuthorized); + + APIResponseImpl forbidden = new APIResponseImpl(); + forbidden.setDescription("Not Allowed"); + forbidden.setResponseCode("403"); + responses.add(forbidden); + + return responses; + } + +} \ No newline at end of file diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/security/AutoTagFilter.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/security/AutoTagFilter.java new file mode 100644 index 00000000000000..391803a9dbe5e2 --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/security/AutoTagFilter.java @@ -0,0 +1,75 @@ +package io.quarkus.smallrye.openapi.deployment.security; + +import java.util.Map; +import java.util.Set; + +import org.eclipse.microprofile.openapi.OASFilter; +import org.eclipse.microprofile.openapi.models.OpenAPI; +import org.eclipse.microprofile.openapi.models.Operation; +import org.eclipse.microprofile.openapi.models.PathItem; +import org.eclipse.microprofile.openapi.models.Paths; +import org.jboss.logging.Logger; + +import io.smallrye.openapi.api.models.OperationImpl; + +/** + * Automatically tag operations based on the class name. + */ +public class AutoTagFilter implements OASFilter { + private static final Logger log = Logger.getLogger(AutoTagFilter.class); + + private Map classNameMap; + + public AutoTagFilter() { + + } + + public AutoTagFilter(Map classNameMap) { + this.classNameMap = classNameMap; + } + + public Map getClassNameMap() { + return classNameMap; + } + + public void setClassNameMap(Map classNameMap) { + this.classNameMap = classNameMap; + } + + @Override + public void filterOpenAPI(OpenAPI openAPI) { + if (!classNameMap.isEmpty()) { + Paths paths = openAPI.getPaths(); + if (paths != null) { + Map pathItems = paths.getPathItems(); + if (pathItems != null && !pathItems.isEmpty()) { + Set> pathItemsEntries = pathItems.entrySet(); + for (Map.Entry pathItem : pathItemsEntries) { + Map operations = pathItem.getValue().getOperations(); + if (operations != null && !operations.isEmpty()) { + for (Operation operation : operations.values()) { + if (operation.getTags() == null || operation.getTags().isEmpty()) { + // Auto add a tag + OperationImpl operationImpl = (OperationImpl) operation; + String methodRef = operationImpl.getMethodRef(); + if (classNameMap.containsKey(methodRef)) { + operation.addTag(splitCamelCase(classNameMap.get(methodRef))); + } + } + } + } + } + } + } + } + } + + private String splitCamelCase(String s) { + return s.replaceAll( + String.format("%s|%s|%s", + "(?<=[A-Z])(?=[A-Z][a-z])", + "(?<=[^A-Z])(?=[A-Z])", + "(?<=[A-Za-z])(?=[^A-Za-z])"), + " "); + } +} \ No newline at end of file diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/security/SecurityConfigFilter.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/security/SecurityConfigFilter.java index 7e17899f564408..10fa2ad5b4f287 100644 --- a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/security/SecurityConfigFilter.java +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/security/SecurityConfigFilter.java @@ -1,38 +1,28 @@ package io.quarkus.smallrye.openapi.deployment.security; import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.Set; import org.eclipse.microprofile.openapi.OASFactory; import org.eclipse.microprofile.openapi.OASFilter; import org.eclipse.microprofile.openapi.models.OpenAPI; -import org.eclipse.microprofile.openapi.models.Operation; -import org.eclipse.microprofile.openapi.models.PathItem; -import org.eclipse.microprofile.openapi.models.Paths; import org.eclipse.microprofile.openapi.models.security.OAuthFlow; import org.eclipse.microprofile.openapi.models.security.OAuthFlows; -import org.eclipse.microprofile.openapi.models.security.SecurityRequirement; import org.eclipse.microprofile.openapi.models.security.SecurityScheme; import org.jboss.logging.Logger; import io.quarkus.smallrye.openapi.common.deployment.SmallRyeOpenApiConfig; -import io.smallrye.openapi.api.models.OperationImpl; -import io.smallrye.openapi.api.models.security.SecurityRequirementImpl; /** * Add Basic Security via Config */ public class SecurityConfigFilter implements OASFilter { - private static final Logger log = Logger.getLogger("io.quarkus.smallrye.openapi"); + private static final Logger log = Logger.getLogger(SecurityConfigFilter.class); private final SmallRyeOpenApiConfig config; - private final Map> methodReferences; - public SecurityConfigFilter(SmallRyeOpenApiConfig config, Map> methodReferences) { + public SecurityConfigFilter(SmallRyeOpenApiConfig config) { this.config = config; - this.methodReferences = methodReferences; } @Override @@ -81,7 +71,7 @@ public void filterOpenAPI(OpenAPI openAPI) { oAuthFlow.authorizationUrl(config.oauth2ImplicitAuthorizationUrl.get()); } if (config.oauth2ImplicitRefreshUrl.isPresent()) { - oAuthFlow.authorizationUrl(config.oauth2ImplicitRefreshUrl.get()); + oAuthFlow.refreshUrl(config.oauth2ImplicitRefreshUrl.get()); } if (config.oauth2ImplicitTokenUrl.isPresent()) { oAuthFlow.tokenUrl(config.oauth2ImplicitTokenUrl.get()); @@ -102,32 +92,6 @@ public void filterOpenAPI(OpenAPI openAPI) { log.warn("Detected multiple Security Schemes, only one scheme is supported at the moment " + securitySchemes.keySet().toString()); } - - // Also add Security requirement for all methods annotated with Roles allowed - if (config.autoAddSecurityRequirement && !securitySchemes.isEmpty() && !methodReferences.isEmpty()) { - Paths paths = openAPI.getPaths(); - if (paths != null) { - Map pathItems = paths.getPathItems(); - if (pathItems != null && !pathItems.isEmpty()) { - Set> pathItemsEntries = pathItems.entrySet(); - for (Map.Entry pathItem : pathItemsEntries) { - Map operations = pathItem.getValue().getOperations(); - if (operations != null && !operations.isEmpty()) { - for (Operation operation : operations.values()) { - OperationImpl operationImpl = (OperationImpl) operation; - if (methodReferences.keySet().contains(operationImpl.getMethodRef())) { - SecurityRequirement securityRequirement = new SecurityRequirementImpl(); - List roles = methodReferences.get(operationImpl.getMethodRef()); - String name = securitySchemes.keySet().iterator().next(); - securityRequirement = securityRequirement.addScheme(name, roles); - operation = operation.addSecurityRequirement(securityRequirement); - } - } - } - } - } - } - } } } } diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoTagTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoTagTestCase.java new file mode 100644 index 00000000000000..ada6157131c0dc --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/AutoTagTestCase.java @@ -0,0 +1,33 @@ +package io.quarkus.smallrye.openapi.test.jaxrs; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class AutoTagTestCase { + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(OpenApiResourceWithNoTag.class)); + + @Test + public void testAutoSecurityRequirement() { + RestAssured.given().header("Accept", "application/json") + .when().get("/q/openapi") + .then() + .log().body() + .and() + .body("paths.'/resource/annotated'.get.tags", Matchers.hasItem("From Annotation")) + .and() + .body("paths.'/resource/auto'.get.tags", Matchers.hasItem("Open Api Resource With No Tag")) + .and() + .body("paths.'/resource/auto'.post.tags", Matchers.hasItem("Open Api Resource With No Tag")); + + } + +} diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiResourceWithNoTag.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiResourceWithNoTag.java new file mode 100644 index 00000000000000..c93cc0dc2f3c81 --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiResourceWithNoTag.java @@ -0,0 +1,31 @@ +package io.quarkus.smallrye.openapi.test.jaxrs; + +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +@Path("/resource") +public class OpenApiResourceWithNoTag { + + @GET + @Path("/auto") + public String auto() { + return "by auto tag"; + } + + @POST + @Path("/auto") + public String autopost() { + return "by auto tag"; + } + + @GET + @Path("/annotated") + @Tag(name = "From Annotation") + public String annotated() { + return "by annotation"; + } + +}