From 1a81758440b112cee9b6fff3bca8f684d8e174ad Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Tue, 16 Jan 2024 16:55:10 -0500 Subject: [PATCH] OpenAPI: Always run OpenIDConnectSecurityFilter at runtime Signed-off-by: Michael Edgar --- .../deployment/SmallRyeOpenApiProcessor.java | 310 +++++++++--------- .../jaxrs/OIDCSecurityAutoAddTestTest.java | 31 ++ .../test/jaxrs/OIDCSecurityTestBase.java | 24 ++ .../jaxrs/OIDCSecurityWithConfigTestCase.java | 19 +- .../smallrye/openapi/OpenApiFilter.java | 4 +- .../filter/AutoBasicSecurityFilter.java | 5 +- .../filter/AutoBearerTokenSecurityFilter.java | 5 +- .../runtime/filter/AutoSecurityFilter.java | 29 +- .../filter/OpenIDConnectSecurityFilter.java | 10 +- 9 files changed, 238 insertions(+), 199 deletions(-) create mode 100644 extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OIDCSecurityAutoAddTestTest.java create mode 100644 extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OIDCSecurityTestBase.java 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 b5308159a27d31..2cbc4ac4b1fefe 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 @@ -25,9 +25,11 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -45,6 +47,7 @@ import org.eclipse.microprofile.openapi.spi.OASFactoryResolver; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationTarget.Kind; import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.CompositeIndex; @@ -102,6 +105,7 @@ import io.quarkus.smallrye.openapi.runtime.RuntimeOnlyBuilder; import io.quarkus.smallrye.openapi.runtime.filter.AutoBasicSecurityFilter; import io.quarkus.smallrye.openapi.runtime.filter.AutoBearerTokenSecurityFilter; +import io.quarkus.smallrye.openapi.runtime.filter.AutoSecurityFilter; import io.quarkus.smallrye.openapi.runtime.filter.AutoUrl; import io.quarkus.smallrye.openapi.runtime.filter.OpenIDConnectSecurityFilter; import io.quarkus.vertx.http.deployment.FilterBuildItem; @@ -111,7 +115,6 @@ import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem; import io.quarkus.vertx.http.deployment.spi.RouteBuildItem; import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; -import io.quarkus.vertx.http.runtime.management.ManagementInterfaceConfiguration; import io.smallrye.openapi.api.OpenApiConfig; import io.smallrye.openapi.api.OpenApiConfigImpl; import io.smallrye.openapi.api.OpenApiDocument; @@ -151,6 +154,7 @@ public class SmallRyeOpenApiProcessor { private static final DotName OPENAPI_SCHEMA = DotName.createSimple(Schema.class.getName()); private static final DotName OPENAPI_RESPONSE = DotName.createSimple(APIResponse.class.getName()); private static final DotName OPENAPI_RESPONSES = DotName.createSimple(APIResponses.class.getName()); + private static final DotName OPENAPI_SECURITY_REQUIREMENT = DotName.createSimple(SecurityRequirement.class.getName()); private static final String OPENAPI_RESPONSE_CONTENT = "content"; private static final String OPENAPI_RESPONSE_SCHEMA = "schema"; @@ -163,6 +167,8 @@ public class SmallRyeOpenApiProcessor { private static final String SPRING = "Spring"; private static final String VERT_X = "Vert.x"; + private static final String MANAGEMENT_ENABLED = "quarkus.smallrye-openapi.management.enabled"; + static { System.setProperty(io.smallrye.openapi.api.constants.OpenApiConstants.DEFAULT_PRODUCES_STREAMING, "application/octet-stream"); @@ -229,20 +235,25 @@ void registerAutoSecurityFilter(BuildProducer syntheticB OpenApiFilteredIndexViewBuildItem apiFilteredIndexViewBuildItem, List securityInformationBuildItems, OpenApiRecorder recorder) { - OASFilter autoSecurityFilter = null; + AutoSecurityFilter autoSecurityFilter = null; + if (openApiConfig.autoAddSecurity) { - // Only add the security if there are secured endpoints - OASFilter autoRolesAllowedFilter = getAutoRolesAllowedFilter(openApiConfig.securitySchemeName, - apiFilteredIndexViewBuildItem, openApiConfig); - if (autoRolesAllowedFilter != null) { - autoSecurityFilter = getAutoSecurityFilter(securityInformationBuildItems, openApiConfig); - } + autoSecurityFilter = getAutoSecurityFilter(securityInformationBuildItems, openApiConfig) + .filter(securityFilter -> autoSecurityRuntimeEnabled(securityFilter, + () -> getAutoRolesAllowedFilter(apiFilteredIndexViewBuildItem, openApiConfig))) + .orElse(null); } syntheticBeans.produce(SyntheticBeanBuildItem.configure(OASFilter.class).setRuntimeInit() .supplier(recorder.autoSecurityFilterSupplier(autoSecurityFilter)).done()); } + static boolean autoSecurityRuntimeEnabled(AutoSecurityFilter autoSecurityFilter, + Supplier autoRolesAllowedFilterSource) { + // When the filter is not runtime required, add the security only if there are secured endpoints + return autoSecurityFilter.runtimeRequired() || autoRolesAllowedFilterSource.get() != null; + } + @BuildStep @Record(ExecutionTime.STATIC_INIT) void registerAnnotatedUserDefinedRuntimeFilters(BuildProducer syntheticBeans, @@ -274,8 +285,7 @@ void handler(LaunchModeBuildItem launch, ShutdownContextBuildItem shutdownContext, SmallRyeOpenApiConfig openApiConfig, List filterBuildItems, - ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, - ManagementInterfaceConfiguration managementInterfaceConfiguration) { + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig) { /* * Ugly Hack * In dev mode, we pass a classloader to load the up to date OpenAPI document. @@ -305,7 +315,7 @@ void handler(LaunchModeBuildItem launch, } } - routes.produce(RouteBuildItem.newManagementRoute(openApiConfig.path, "quarkus.smallrye-openapi.management.enabled") + routes.produce(RouteBuildItem.newManagementRoute(openApiConfig.path, MANAGEMENT_ENABLED) .withRouteCustomizer(corsFilter) .withRoutePathConfigKey("quarkus.smallrye-openapi.path") .withRequestHandler(handler) @@ -314,19 +324,19 @@ void handler(LaunchModeBuildItem launch, .build()); routes.produce( - RouteBuildItem.newManagementRoute(openApiConfig.path + ".json", "quarkus.smallrye-openapi.management.enabled") + RouteBuildItem.newManagementRoute(openApiConfig.path + ".json", MANAGEMENT_ENABLED) .withRouteCustomizer(corsFilter) .withRequestHandler(handler) .build()); routes.produce( - RouteBuildItem.newManagementRoute(openApiConfig.path + ".yaml", "quarkus.smallrye-openapi.management.enabled") + RouteBuildItem.newManagementRoute(openApiConfig.path + ".yaml", MANAGEMENT_ENABLED) .withRouteCustomizer(corsFilter) .withRequestHandler(handler) .build()); routes.produce( - RouteBuildItem.newManagementRoute(openApiConfig.path + ".yml", "quarkus.smallrye-openapi.management.enabled") + RouteBuildItem.newManagementRoute(openApiConfig.path + ".yml", MANAGEMENT_ENABLED) .withRouteCustomizer(corsFilter) .withRequestHandler(handler) .build()); @@ -344,7 +354,7 @@ void handler(LaunchModeBuildItem launch, } String managementUrl = getManagementRoot(launch, nonApplicationRootPathBuildItem, openApiConfig, - managementInterfaceBuildTimeConfig, managementInterfaceConfiguration); + managementInterfaceBuildTimeConfig); List origins = c.getOptionalValues("quarkus.http.cors.origins", String.class).orElse(new ArrayList<>()); if (!origins.contains(managementUrl)) { @@ -360,8 +370,7 @@ void handler(LaunchModeBuildItem launch, private String getManagementRoot(LaunchModeBuildItem launch, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, SmallRyeOpenApiConfig openApiConfig, - ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, - ManagementInterfaceConfiguration managementInterfaceConfiguration) { + ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig) { String managementRoot = nonApplicationRootPathBuildItem.resolveManagementPath("/", managementInterfaceBuildTimeConfig, launch, openApiConfig.managementEnabled); @@ -419,23 +428,22 @@ void addAutoFilters(BuildProducer addToOpenAPID LaunchModeBuildItem launchModeBuildItem, ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig) { + OASFilter autoRolesAllowedFilter = getAutoRolesAllowedFilter(apiFilteredIndexViewBuildItem, config); + // Add a security scheme from config if (config.securityScheme.isPresent()) { addToOpenAPIDefinitionProducer .produce(new AddToOpenAPIDefinitionBuildItem( new SecurityConfigFilter(config))); } else if (config.autoAddSecurity) { - OASFilter autoSecurityFilter = getAutoSecurityFilter(securityInformationBuildItems, config); - - if (autoSecurityFilter != null) { - addToOpenAPIDefinitionProducer - .produce(new AddToOpenAPIDefinitionBuildItem(autoSecurityFilter)); - } + getAutoSecurityFilter(securityInformationBuildItems, config) + // Only run the filter at build time if it will not be run at runtime + .filter(securityFilter -> !autoSecurityRuntimeEnabled(securityFilter, () -> autoRolesAllowedFilter)) + .map(AddToOpenAPIDefinitionBuildItem::new) + .ifPresent(addToOpenAPIDefinitionProducer::produce); } // Add Auto roles allowed - OASFilter autoRolesAllowedFilter = getAutoRolesAllowedFilter(config.securitySchemeName, apiFilteredIndexViewBuildItem, - config); if (autoRolesAllowedFilter != null) { addToOpenAPIDefinitionProducer.produce(new AddToOpenAPIDefinitionBuildItem(autoRolesAllowedFilter)); } @@ -459,12 +467,12 @@ void addAutoFilters(BuildProducer addToOpenAPID } } - private List getUserDefinedBuildtimeFilters(OpenApiConfig openApiConfig, IndexView index) { - return getUserDefinedFilters(openApiConfig, index, OpenApiFilter.RunStage.BUILD); + private List getUserDefinedBuildtimeFilters(IndexView index) { + return getUserDefinedFilters(index, OpenApiFilter.RunStage.BUILD); } private List getUserDefinedRuntimeFilters(OpenApiConfig openApiConfig, IndexView index) { - List userDefinedFilters = getUserDefinedFilters(openApiConfig, index, OpenApiFilter.RunStage.RUN); + List userDefinedFilters = getUserDefinedFilters(index, OpenApiFilter.RunStage.RUN); // Also add the MP way String filter = openApiConfig.filter(); if (filter != null) { @@ -473,7 +481,7 @@ private List getUserDefinedRuntimeFilters(OpenApiConfig openApiConfig, I return userDefinedFilters; } - private List getUserDefinedFilters(OpenApiConfig openApiConfig, IndexView index, OpenApiFilter.RunStage stage) { + private List getUserDefinedFilters(IndexView index, OpenApiFilter.RunStage stage) { EnumSet stages = EnumSet.of(OpenApiFilter.RunStage.BOTH, stage); Comparator comparator = Comparator .comparing(x -> ((AnnotationInstance) x).valueWithDefault(index, "priority").asInt()) @@ -486,7 +494,7 @@ private List getUserDefinedFilters(OpenApiConfig openApiConfig, IndexVie .map(ai -> ai.target().asClass()) .filter(c -> c.interfaceNames().contains(DotName.createSimple(OASFilter.class.getName()))) .map(c -> c.name().toString()) - .collect(Collectors.toList()); + .collect(Collectors.toCollection(ArrayList::new)); } private boolean isManagement(ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, @@ -496,77 +504,77 @@ private boolean isManagement(ManagementInterfaceBuildTimeConfig managementInterf && launchModeBuildItem.getLaunchMode().equals(LaunchMode.DEVELOPMENT); } - private OASFilter getAutoSecurityFilter(List securityInformationBuildItems, + private Optional getAutoSecurityFilter(List securityInformationBuildItems, SmallRyeOpenApiConfig config) { - // Auto add a security from security extension(s) - if (config.securityScheme.isEmpty() && securityInformationBuildItems != null - && !securityInformationBuildItems.isEmpty()) { - // This needs to be a filter in runtime as the config we use to autoconfigure is in runtime - for (SecurityInformationBuildItem securityInformationBuildItem : securityInformationBuildItems) { - SecurityInformationBuildItem.SecurityModel securityModel = securityInformationBuildItem.getSecurityModel(); - switch (securityModel) { - case jwt: - return new AutoBearerTokenSecurityFilter( - config.securitySchemeName, - config.securitySchemeDescription, - config.getValidSecuritySchemeExtentions(), - config.jwtSecuritySchemeValue, - config.jwtBearerFormat); - case oauth2: - return new AutoBearerTokenSecurityFilter( - config.securitySchemeName, - config.securitySchemeDescription, - config.getValidSecuritySchemeExtentions(), - config.oauth2SecuritySchemeValue, - config.oauth2BearerFormat); - case basic: - return new AutoBasicSecurityFilter( - config.securitySchemeName, - config.securitySchemeDescription, - config.getValidSecuritySchemeExtentions(), - config.basicSecuritySchemeValue); - case oidc: - return securityInformationBuildItem.getOpenIDConnectInformation() - .map(info -> { - AutoUrl openIdConnectUrl = new AutoUrl( - config.oidcOpenIdConnectUrl.orElse(null), - info.getUrlConfigKey(), - "/.well-known/openid-configuration"); - - return new OpenIDConnectSecurityFilter( - config.securitySchemeName, - config.securitySchemeDescription, - config.getValidSecuritySchemeExtentions(), - openIdConnectUrl); - }) - .orElse(null); - default: - break; - } - } + if (config.securityScheme.isPresent()) { + return Optional.empty(); } - return null; + + // Auto add a security from security extension(s) + return Optional.ofNullable(securityInformationBuildItems) + .map(Collection::stream) + .orElseGet(Stream::empty) + .map(securityInfo -> { + switch (securityInfo.getSecurityModel()) { + case jwt: + return new AutoBearerTokenSecurityFilter( + config.securitySchemeName, + config.securitySchemeDescription, + config.getValidSecuritySchemeExtentions(), + config.jwtSecuritySchemeValue, + config.jwtBearerFormat); + case oauth2: + return new AutoBearerTokenSecurityFilter( + config.securitySchemeName, + config.securitySchemeDescription, + config.getValidSecuritySchemeExtentions(), + config.oauth2SecuritySchemeValue, + config.oauth2BearerFormat); + case basic: + return new AutoBasicSecurityFilter( + config.securitySchemeName, + config.securitySchemeDescription, + config.getValidSecuritySchemeExtentions(), + config.basicSecuritySchemeValue); + case oidc: + // This needs to be a filter in runtime as the config we use to autoconfigure is in runtime + return securityInfo.getOpenIDConnectInformation() + .map(info -> { + AutoUrl openIdConnectUrl = new AutoUrl( + config.oidcOpenIdConnectUrl.orElse(null), + info.getUrlConfigKey(), + "/.well-known/openid-configuration"); + + return new OpenIDConnectSecurityFilter( + config.securitySchemeName, + config.securitySchemeDescription, + config.getValidSecuritySchemeExtentions(), + openIdConnectUrl); + }) + .orElse(null); + default: + return null; + } + }) + .filter(Objects::nonNull) + .findFirst(); } - private OASFilter getAutoRolesAllowedFilter(String securitySchemeName, + private OASFilter getAutoRolesAllowedFilter( OpenApiFilteredIndexViewBuildItem apiFilteredIndexViewBuildItem, SmallRyeOpenApiConfig config) { if (config.autoAddSecurityRequirement) { - if (securitySchemeName == null) { - securitySchemeName = config.securitySchemeName; - } - Map> rolesAllowedMethodReferences = getRolesAllowedMethodReferences( apiFilteredIndexViewBuildItem); List authenticatedMethodReferences = getAuthenticatedMethodReferences( apiFilteredIndexViewBuildItem); - if ((rolesAllowedMethodReferences != null && !rolesAllowedMethodReferences.isEmpty()) - || (authenticatedMethodReferences != null && !authenticatedMethodReferences.isEmpty())) { - - return new AutoRolesAllowedFilter(securitySchemeName, rolesAllowedMethodReferences, + if (!rolesAllowedMethodReferences.isEmpty() || !authenticatedMethodReferences.isEmpty()) { + return new AutoRolesAllowedFilter( + config.securitySchemeName, + rolesAllowedMethodReferences, authenticatedMethodReferences); } } @@ -577,9 +585,9 @@ private OASFilter getAutoTagFilter(OpenApiFilteredIndexViewBuildItem apiFiltered SmallRyeOpenApiConfig config) { if (config.autoAddTags) { - Map classNamesMethodReferences = getClassNamesMethodReferences(apiFilteredIndexViewBuildItem); - if (classNamesMethodReferences != null && !classNamesMethodReferences.isEmpty()) { + + if (!classNamesMethodReferences.isEmpty()) { return new AutoTagFilter(classNamesMethodReferences); } } @@ -607,64 +615,53 @@ private OASFilter getAutoServerFilter(SmallRyeOpenApiConfig config, boolean defa 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.declaringClass(), 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(classInfo, method); - methodReferences.putIfAbsent(ref, List.of(ai.value().asStringArray())); - } - } - } - } - return methodReferences; + private Map> getRolesAllowedMethodReferences(OpenApiFilteredIndexViewBuildItem indexViewBuildItem) { + return SecurityConstants.ROLES_ALLOWED + .stream() + .map(indexViewBuildItem.getIndex()::getAnnotations) + .flatMap(Collection::stream) + .flatMap(SmallRyeOpenApiProcessor::getMethods) + .collect(Collectors.toMap( + e -> JandexUtil.createUniqueMethodReference(e.getKey().declaringClass(), e.getKey()), + e -> List.of(e.getValue().value().asStringArray()), + (v1, v2) -> { + if (!Objects.equals(v1, v2)) { + log.warnf("Dropping duplicate annotation, but the values were different; v1: %s, v2: %s", v1, + v2); + } + return v1; + })); } - private List getAuthenticatedMethodReferences( - OpenApiFilteredIndexViewBuildItem apiFilteredIndexViewBuildItem) { - List authenticatedAnnotations = new ArrayList<>(); - authenticatedAnnotations.addAll( - apiFilteredIndexViewBuildItem.getIndex().getAnnotations(DotName.createSimple(Authenticated.class.getName()))); + private List getAuthenticatedMethodReferences(OpenApiFilteredIndexViewBuildItem indexViewBuildItem) { + return indexViewBuildItem.getIndex() + .getAnnotations(DotName.createSimple(Authenticated.class.getName())) + .stream() + .flatMap(SmallRyeOpenApiProcessor::getMethods) + .map(e -> JandexUtil.createUniqueMethodReference(e.getKey().declaringClass(), e.getKey())) + .distinct() + .toList(); + } - List methodReferences = new ArrayList<>(); - DotName securityRequirement = DotName.createSimple(SecurityRequirement.class.getName()); - for (AnnotationInstance ai : authenticatedAnnotations) { - if (ai.target().kind().equals(AnnotationTarget.Kind.METHOD)) { - MethodInfo method = ai.target().asMethod(); - if (isValidOpenAPIMethodForAutoAdd(method, securityRequirement)) { - String ref = JandexUtil.createUniqueMethodReference(method.declaringClass(), method); - methodReferences.add(ref); - } - } - 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(classInfo, method); - methodReferences.add(ref); - } - } + private static Stream> getMethods(AnnotationInstance annotation) { + if (annotation.target().kind() == Kind.METHOD) { + MethodInfo method = annotation.target().asMethod(); + + if (isValidOpenAPIMethodForAutoAdd(method)) { + return Stream.of(Map.entry(method, annotation)); } + } else if (annotation.target().kind() == Kind.CLASS) { + ClassInfo classInfo = annotation.target().asClass(); + + return classInfo.methods() + .stream() + // drop methods that specify the annotation directly + .filter(method -> !method.hasDeclaredAnnotation(annotation.name())) + .filter(method -> isValidOpenAPIMethodForAutoAdd(method)) + .map(method -> Map.entry(method, annotation)); } - return methodReferences; + + return Stream.empty(); } private Map getClassNamesMethodReferences(OpenApiFilteredIndexViewBuildItem apiFilteredIndexViewBuildItem) { @@ -712,9 +709,9 @@ void addMethodImplementationClassNames(MethodInfo method, Type[] params, Collect } } - private boolean isValidOpenAPIMethodForAutoAdd(MethodInfo method, DotName securityRequirement) { - return isOpenAPIEndpoint(method) && !method.hasAnnotation(securityRequirement) - && method.declaringClass().declaredAnnotation(securityRequirement) == null; + private static boolean isValidOpenAPIMethodForAutoAdd(MethodInfo method) { + return isOpenAPIEndpoint(method) && !method.hasAnnotation(OPENAPI_SECURITY_REQUIREMENT) + && method.declaringClass().declaredAnnotation(OPENAPI_SECURITY_REQUIREMENT) == null; } @BuildStep @@ -765,7 +762,7 @@ public void registerOpenApiSchemaClassesForReflection(BuildProducer httpAnnotations = getAllOpenAPIEndpoints(); for (DotName httpAnnotation : httpAnnotations) { if (method.hasAnnotation(httpAnnotation)) { @@ -775,7 +772,7 @@ private boolean isOpenAPIEndpoint(MethodInfo method) { return false; } - private Set getAllOpenAPIEndpoints() { + private static Set getAllOpenAPIEndpoints() { Set httpAnnotations = new HashSet<>(); httpAnnotations.addAll(JaxRsConstants.HTTP_METHODS); httpAnnotations.addAll(SpringConstants.HTTP_METHODS); @@ -831,7 +828,7 @@ public void build(BuildProducer feature, OutputTargetBuildItem out, SmallRyeOpenApiConfig smallRyeOpenApiConfig, OutputTargetBuildItem outputTargetBuildItem, - List ignoreStaticDocumentBuildItems) throws Exception { + List ignoreStaticDocumentBuildItems) throws IOException { FilteredIndexView index = openApiFilteredIndexViewBuildItem.getIndex(); Config config = ConfigProvider.getConfig(); @@ -845,7 +842,7 @@ public void build(BuildProducer feature, } OpenAPI staticModel = generateStaticModel(smallRyeOpenApiConfig, urlIgnorePatterns, - outputTargetBuildItem.getOutputDirectory(), config, openApiConfig); + outputTargetBuildItem.getOutputDirectory(), openApiConfig); OpenAPI annotationModel; @@ -933,15 +930,12 @@ private boolean shouldScanAnnotations(Capabilities capabilities, IndexView index } private boolean isUsingVertxRoute(IndexView index) { - if (!index.getAnnotations(VertxConstants.ROUTE).isEmpty() - || !index.getAnnotations(VertxConstants.ROUTE_BASE).isEmpty()) { - return true; - } - return false; + return !index.getAnnotations(VertxConstants.ROUTE).isEmpty() || + !index.getAnnotations(VertxConstants.ROUTE_BASE).isEmpty(); } private OpenAPI generateStaticModel(SmallRyeOpenApiConfig smallRyeOpenApiConfig, List ignorePatterns, Path target, - Config config, OpenApiConfig openApiConfig) + OpenApiConfig openApiConfig) throws IOException { if (smallRyeOpenApiConfig.ignoreStaticDocument) { @@ -1093,9 +1087,7 @@ private List getResourceFiles(Path resourcePath, Path target) throws IOE final Path targetResourceDir = target == null ? null : target.resolve("classes").resolve(resourcePath); if (targetResourceDir != null && Files.exists(targetResourceDir)) { try (Stream paths = Files.list(targetResourceDir)) { - return paths.map((t) -> { - return resourceName + "/" + t.getFileName().toString(); - }).collect(Collectors.toList()); + return paths.map(t -> resourceName + "/" + t.getFileName().toString()).toList(); } } else { ClassLoader cl = Thread.currentThread().getContextClassLoader(); @@ -1212,7 +1204,7 @@ private OpenApiDocument prepareOpenApiDocument(OpenAPI staticModel, document.filter(otherExtensionFilter); } // Add user defined Build time filters - List userDefinedFilters = getUserDefinedBuildtimeFilters(openApiConfig, index); + List userDefinedFilters = getUserDefinedBuildtimeFilters(index); for (String filter : userDefinedFilters) { document.filter(filter(filter, index)); } diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OIDCSecurityAutoAddTestTest.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OIDCSecurityAutoAddTestTest.java new file mode 100644 index 00000000000000..2519847243370a --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OIDCSecurityAutoAddTestTest.java @@ -0,0 +1,31 @@ +package io.quarkus.smallrye.openapi.test.jaxrs; + +import java.util.List; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.builder.Version; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.QuarkusUnitTest; + +class OIDCSecurityAutoAddTestTest extends OIDCSecurityTestBase { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(OpenApiResource.class, ResourceBean.class) + .addAsResource( + new StringAsset("" + + "quarkus.smallrye-openapi.security-scheme-name=OIDCCompanyAuthentication\n" + + "quarkus.smallrye-openapi.security-scheme-description=OIDC Authentication\n" + + "quarkus.smallrye-openapi.oidc-open-id-connect-url=BUILD-TIME-OVERRIDDEN\n" + + "quarkus.http.auth.permission.\"oidc\".policy=authenticated\n" + + "quarkus.http.auth.permission.\"oidc\".paths=/resource/*\n" + + "quarkus.oidc.auth-server-url=BUILD-TIME-OVERRIDDEN\n" + + "quarkus.devservices.enabled=false"), + "application.properties")) + .setForcedDependencies(List.of( + Dependency.of("io.quarkus", "quarkus-oidc", Version.getVersion()))) + .overrideRuntimeConfigKey("quarkus.oidc.auth-server-url", "http://localhost:8081/auth/realms/OpenAPIOIDC"); +} diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OIDCSecurityTestBase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OIDCSecurityTestBase.java new file mode 100644 index 00000000000000..300d438c5903b0 --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OIDCSecurityTestBase.java @@ -0,0 +1,24 @@ +package io.quarkus.smallrye.openapi.test.jaxrs; + +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.hasEntry; + +import org.junit.jupiter.api.Test; + +import io.restassured.RestAssured; + +abstract class OIDCSecurityTestBase { + + @Test + void testOIDCAuthentication() { + RestAssured.given().header("Accept", "application/json") + .when().get("/q/openapi") + .then().body("components.securitySchemes.OIDCCompanyAuthentication", + allOf( + hasEntry("type", "openIdConnect"), + hasEntry("description", "OIDC Authentication"), + hasEntry("openIdConnectUrl", + "http://localhost:8081/auth/realms/OpenAPIOIDC/.well-known/openid-configuration"))); + } + +} diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OIDCSecurityWithConfigTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OIDCSecurityWithConfigTestCase.java index e5de3451f54144..ac826c546f8a40 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OIDCSecurityWithConfigTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OIDCSecurityWithConfigTestCase.java @@ -1,14 +1,12 @@ package io.quarkus.smallrye.openapi.test.jaxrs; -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; -public class OIDCSecurityWithConfigTestCase { +class OIDCSecurityWithConfigTestCase extends OIDCSecurityTestBase { + @RegisterExtension static QuarkusUnitTest runner = new QuarkusUnitTest() .withApplicationRoot((jar) -> jar @@ -18,18 +16,5 @@ public class OIDCSecurityWithConfigTestCase { + "quarkus.smallrye-openapi.security-scheme-name=OIDCCompanyAuthentication\n" + "quarkus.smallrye-openapi.security-scheme-description=OIDC Authentication\n" + "quarkus.smallrye-openapi.oidc-open-id-connect-url=http://localhost:8081/auth/realms/OpenAPIOIDC/.well-known/openid-configuration"), - "application.properties")); - - @Test - public void testOIDCAuthentication() { - RestAssured.given().header("Accept", "application/json") - .when().get("/q/openapi") - .then().body("components.securitySchemes.OIDCCompanyAuthentication", Matchers.hasEntry("type", "openIdConnect")) - .and() - .body("components.securitySchemes.OIDCCompanyAuthentication", - Matchers.hasEntry("description", "OIDC Authentication")) - .and().body("components.securitySchemes.OIDCCompanyAuthentication", Matchers.hasEntry("openIdConnectUrl", - "http://localhost:8081/auth/realms/OpenAPIOIDC/.well-known/openid-configuration")); - } } diff --git a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/OpenApiFilter.java b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/OpenApiFilter.java index 7b288e732ad8d7..5daf415ab1ca82 100644 --- a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/OpenApiFilter.java +++ b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/OpenApiFilter.java @@ -26,9 +26,9 @@ */ int priority() default 1; - static enum RunStage { + enum RunStage { BUILD, RUN, BOTH } -} \ No newline at end of file +} diff --git a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/AutoBasicSecurityFilter.java b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/AutoBasicSecurityFilter.java index 2d24aa9aa17390..13b4ec85b8eaca 100644 --- a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/AutoBasicSecurityFilter.java +++ b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/AutoBasicSecurityFilter.java @@ -2,7 +2,6 @@ import java.util.Map; -import org.eclipse.microprofile.openapi.OASFactory; import org.eclipse.microprofile.openapi.models.security.SecurityScheme; /** @@ -31,10 +30,8 @@ public void setBasicSecuritySchemeValue(String basicSecuritySchemeValue) { } @Override - protected SecurityScheme getSecurityScheme() { - SecurityScheme securityScheme = OASFactory.createSecurityScheme(); + protected void updateSecurityScheme(SecurityScheme securityScheme) { securityScheme.setType(SecurityScheme.Type.HTTP); securityScheme.setScheme(basicSecuritySchemeValue); - return securityScheme; } } \ No newline at end of file diff --git a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/AutoBearerTokenSecurityFilter.java b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/AutoBearerTokenSecurityFilter.java index 845c559aeb55b8..f64a18684266b0 100644 --- a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/AutoBearerTokenSecurityFilter.java +++ b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/AutoBearerTokenSecurityFilter.java @@ -2,7 +2,6 @@ import java.util.Map; -import org.eclipse.microprofile.openapi.OASFactory; import org.eclipse.microprofile.openapi.models.security.SecurityScheme; /** @@ -42,11 +41,9 @@ public void setBearerFormat(String bearerFormat) { } @Override - protected SecurityScheme getSecurityScheme() { - SecurityScheme securityScheme = OASFactory.createSecurityScheme(); + protected void updateSecurityScheme(SecurityScheme securityScheme) { securityScheme.setType(SecurityScheme.Type.HTTP); securityScheme.setScheme(securitySchemeValue); securityScheme.setBearerFormat(bearerFormat); - return securityScheme; } } diff --git a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/AutoSecurityFilter.java b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/AutoSecurityFilter.java index bcb68289d3f428..a6b03919efd6cf 100644 --- a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/AutoSecurityFilter.java +++ b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/AutoSecurityFilter.java @@ -1,7 +1,8 @@ package io.quarkus.smallrye.openapi.runtime.filter; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; +import java.util.Optional; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; @@ -56,6 +57,10 @@ public void setSecuritySchemeExtensions(Map securitySchemeExtens this.securitySchemeExtensions = securitySchemeExtensions; } + public boolean runtimeRequired() { + return false; + } + @Override public void filterOpenAPI(OpenAPI openAPI) { // Make sure components are created @@ -63,23 +68,29 @@ public void filterOpenAPI(OpenAPI openAPI) { openAPI.setComponents(OASFactory.createComponents()); } - Map securitySchemes = new HashMap<>(); + Map securitySchemes = new LinkedHashMap<>(); // Add any existing security - if (openAPI.getComponents().getSecuritySchemes() != null - && !openAPI.getComponents().getSecuritySchemes().isEmpty()) { - securitySchemes.putAll(openAPI.getComponents().getSecuritySchemes()); + Optional.ofNullable(openAPI.getComponents().getSecuritySchemes()) + .ifPresent(securitySchemes::putAll); + + SecurityScheme securityScheme = securitySchemes.computeIfAbsent( + securitySchemeName, + name -> OASFactory.createSecurityScheme()); + + updateSecurityScheme(securityScheme); + + if (securitySchemeDescription != null) { + securityScheme.setDescription(securitySchemeDescription); } - SecurityScheme securityScheme = getSecurityScheme(); - securityScheme.setDescription(securitySchemeDescription); securitySchemeExtensions.forEach(securityScheme::addExtension); securitySchemes.put(securitySchemeName, securityScheme); openAPI.getComponents().setSecuritySchemes(securitySchemes); } - protected abstract SecurityScheme getSecurityScheme(); + protected abstract void updateSecurityScheme(SecurityScheme securityScheme); protected String getUrl(String configKey, String defaultValue, String shouldEndWith) { Config c = ConfigProvider.getConfig(); @@ -92,4 +103,4 @@ protected String getUrl(String configKey, String defaultValue, String shouldEndW return u; } -} \ No newline at end of file +} diff --git a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/OpenIDConnectSecurityFilter.java b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/OpenIDConnectSecurityFilter.java index f7aaa531610d3d..141e9d3f3671fe 100644 --- a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/OpenIDConnectSecurityFilter.java +++ b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/OpenIDConnectSecurityFilter.java @@ -2,7 +2,6 @@ import java.util.Map; -import org.eclipse.microprofile.openapi.OASFactory; import org.eclipse.microprofile.openapi.models.security.SecurityScheme; /** @@ -32,11 +31,14 @@ public void setOpenIdConnectUrl(AutoUrl openIdConnectUrl) { } @Override - protected SecurityScheme getSecurityScheme() { - SecurityScheme securityScheme = OASFactory.createSecurityScheme(); + public boolean runtimeRequired() { + return true; + } + + @Override + protected void updateSecurityScheme(SecurityScheme securityScheme) { securityScheme.setType(SecurityScheme.Type.OPENIDCONNECT); securityScheme.setOpenIdConnectUrl(openIdConnectUrl.getFinalUrlValue()); - return securityScheme; } }