Skip to content

Commit

Permalink
Merge pull request #19680 from phillip-kruger/openapi-autosecurity
Browse files Browse the repository at this point in the history
OpenAPI Auto security based on extensions
  • Loading branch information
stuartwdouglas authored Sep 1, 2021
2 parents ee8028a + 236dbd0 commit fe6b68e
Show file tree
Hide file tree
Showing 14 changed files with 486 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import io.quarkus.oidc.runtime.TenantConfigBean;
import io.quarkus.runtime.TlsConfig;
import io.quarkus.vertx.core.deployment.CoreVertxBuildItem;
import io.quarkus.vertx.http.deployment.SecurityInformationBuildItem;
import io.smallrye.jwt.auth.cdi.ClaimValueProducer;
import io.smallrye.jwt.auth.cdi.CommonJwtProducer;
import io.smallrye.jwt.auth.cdi.JsonValueProducer;
Expand All @@ -51,6 +52,14 @@ FeatureBuildItem featureBuildItem() {
return new FeatureBuildItem(Feature.OIDC);
}

@BuildStep(onlyIf = IsEnabled.class)
public void provideSecurityInformation(BuildProducer<SecurityInformationBuildItem> securityInformationProducer) {
// TODO: By default quarkus.oidc.application-type = service
// Also look at other options (web-app, hybrid)
securityInformationProducer
.produce(SecurityInformationBuildItem.OPENIDCONNECT("quarkus.oidc.auth-server-url"));
}

@BuildStep(onlyIf = IsEnabled.class)
AdditionalBeanBuildItem jwtClaimIntegration(Capabilities capabilities) {
if (!capabilities.isPresent(Capability.JWT)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import io.quarkus.smallrye.jwt.runtime.auth.JwtPrincipalProducer;
import io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator;
import io.quarkus.smallrye.jwt.runtime.auth.RawOptionalClaimCreator;
import io.quarkus.vertx.http.deployment.SecurityInformationBuildItem;
import io.smallrye.jwt.algorithm.KeyEncryptionAlgorithm;
import io.smallrye.jwt.algorithm.SignatureAlgorithm;
import io.smallrye.jwt.auth.cdi.ClaimValueProducer;
Expand Down Expand Up @@ -69,6 +70,11 @@ EnableAllSecurityServicesBuildItem security() {
return new EnableAllSecurityServicesBuildItem();
}

@BuildStep(onlyIf = IsEnabled.class)
public void provideSecurityInformation(BuildProducer<SecurityInformationBuildItem> securityInformationProducer) {
securityInformationProducer.produce(SecurityInformationBuildItem.JWT());
}

/**
* Register the CDI beans that are needed by the MP-JWT extension
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ public final class SmallRyeOpenApiConfig {
@ConfigItem(defaultValue = "true")
public boolean autoAddTags;

/**
* This will automatically add security based on the security extension included (if any).
*/
@ConfigItem(defaultValue = "true")
public boolean autoAddSecurity;

/**
* Add a scheme value to the Basic HTTP Security Scheme
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,14 @@
import io.quarkus.smallrye.openapi.runtime.OpenApiDocumentService;
import io.quarkus.smallrye.openapi.runtime.OpenApiRecorder;
import io.quarkus.smallrye.openapi.runtime.OpenApiRuntimeConfig;
import io.quarkus.smallrye.openapi.runtime.filter.AutoBasicSecurityFilter;
import io.quarkus.smallrye.openapi.runtime.filter.AutoJWTSecurityFilter;
import io.quarkus.smallrye.openapi.runtime.filter.AutoUrl;
import io.quarkus.smallrye.openapi.runtime.filter.OpenIDConnectSecurityFilter;
import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem;
import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem;
import io.quarkus.vertx.http.deployment.RouteBuildItem;
import io.quarkus.vertx.http.deployment.SecurityInformationBuildItem;
import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem;
import io.quarkus.vertx.http.runtime.HttpConfiguration;
import io.smallrye.openapi.api.OpenApiConfig;
Expand Down Expand Up @@ -168,9 +173,11 @@ RouteBuildItem handler(LaunchModeBuildItem launch,
BuildProducer<NotFoundPageDisplayableEndpointBuildItem> displayableEndpoints,
OpenApiRecorder recorder,
NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem,
List<SecurityInformationBuildItem> securityInformationBuildItems,
OpenApiRuntimeConfig openApiRuntimeConfig,
ShutdownContextBuildItem shutdownContext,
SmallRyeOpenApiConfig openApiConfig,
OpenApiFilteredIndexViewBuildItem apiFilteredIndexViewBuildItem,
HttpConfiguration httpConfiguration) {
/*
* <em>Ugly Hack</em>
Expand All @@ -187,7 +194,17 @@ RouteBuildItem handler(LaunchModeBuildItem launch,
recorder.setupClDevMode(shutdownContext);
}

Handler<RoutingContext> handler = recorder.handler(openApiRuntimeConfig, httpConfiguration);
OASFilter 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);
}
}

Handler<RoutingContext> handler = recorder.handler(openApiRuntimeConfig, httpConfiguration, autoSecurityFilter);
return nonApplicationRootPathBuildItem.routeBuilder()
.route(openApiConfig.path)
.routeConfigKey("quarkus.smallrye-openapi.path")
Expand Down Expand Up @@ -277,6 +294,64 @@ void addSecurityFilter(BuildProducer<AddToOpenAPIDefinitionBuildItem> addToOpenA

}

private OASFilter getAutoSecurityFilter(List<SecurityInformationBuildItem> securityInformationBuildItems,
SmallRyeOpenApiConfig config) {

// Auto add a security from security extension(s)
if (!config.securityScheme.isPresent() && securityInformationBuildItems != null
&& !securityInformationBuildItems.isEmpty()) {
// This needs to be a filter in runtime as the config we use to auto configure is in runtime
for (SecurityInformationBuildItem securityInformationBuildItem : securityInformationBuildItems) {
SecurityInformationBuildItem.SecurityModel securityModel = securityInformationBuildItem.getSecurityModel();
switch (securityModel) {
case jwt:
return new AutoJWTSecurityFilter(
config.securitySchemeName,
config.securitySchemeDescription,
config.jwtSecuritySchemeValue,
config.jwtBearerFormat);
case basic:
return new AutoBasicSecurityFilter(
config.securitySchemeName,
config.securitySchemeDescription,
config.basicSecuritySchemeValue);
case oidc:
Optional<SecurityInformationBuildItem.OpenIDConnectInformation> maybeInfo = securityInformationBuildItem
.getOpenIDConnectInformation();

if (maybeInfo.isPresent()) {
SecurityInformationBuildItem.OpenIDConnectInformation info = maybeInfo.get();

AutoUrl authorizationUrl = new AutoUrl(
config.oidcOpenIdConnectUrl.orElse(null),
info.getUrlConfigKey(),
"/protocol/openid-connect/auth");

AutoUrl refreshUrl = new AutoUrl(
config.oidcOpenIdConnectUrl.orElse(null),
info.getUrlConfigKey(),
"/protocol/openid-connect/token");

AutoUrl tokenUrl = new AutoUrl(
config.oidcOpenIdConnectUrl.orElse(null),
info.getUrlConfigKey(),
"/protocol/openid-connect/token/introspect");

return new OpenIDConnectSecurityFilter(
config.securitySchemeName,
config.securitySchemeDescription,
authorizationUrl, refreshUrl, tokenUrl);

}
break;
default:
break;
}
}
}
return null;
}

private OASFilter getAutoRolesAllowedFilter(String securitySchemeName,
OpenApiFilteredIndexViewBuildItem apiFilteredIndexViewBuildItem,
SmallRyeOpenApiConfig config) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;

import org.eclipse.microprofile.config.Config;
Expand All @@ -30,16 +29,15 @@ public class OpenApiDocumentService implements OpenApiDocumentHolder {

private OpenApiDocumentHolder documentHolder;

@PostConstruct
void create() throws IOException {
void init(OASFilter autoSecurityFilter) {

Config config = ConfigProvider.getConfig();
this.alwaysRunFilter = config.getOptionalValue("quarkus.smallrye-openapi.always-run-filter", Boolean.class)
.orElse(Boolean.FALSE);
if (alwaysRunFilter) {
this.documentHolder = new DynamicDocument(config);
this.documentHolder = new DynamicDocument(config, autoSecurityFilter);
} else {
this.documentHolder = new StaticDocument(config);
this.documentHolder = new StaticDocument(config, autoSecurityFilter);
}
}

Expand All @@ -59,7 +57,7 @@ class StaticDocument implements OpenApiDocumentHolder {
private byte[] jsonDocument;
private byte[] yamlDocument;

StaticDocument(Config config) {
StaticDocument(Config config, OASFilter autoFilter) {
ClassLoader cl = OpenApiConstants.classLoader == null ? Thread.currentThread().getContextClassLoader()
: OpenApiConstants.classLoader;
try (InputStream is = cl.getResourceAsStream(OpenApiConstants.BASE_NAME + Format.JSON)) {
Expand All @@ -72,6 +70,9 @@ class StaticDocument implements OpenApiDocumentHolder {
document.reset();
document.config(openApiConfig);
document.modelFromStaticFile(OpenApiProcessor.modelFromStaticFile(staticFile));
if (autoFilter != null) {
document.filter(autoFilter);
}
document.filter(OpenApiProcessor.getFilter(openApiConfig, cl));
document.initialize();

Expand Down Expand Up @@ -104,17 +105,19 @@ class DynamicDocument implements OpenApiDocumentHolder {

private OpenAPI generatedOnBuild;
private OpenApiConfig openApiConfig;
private OASFilter filter;
private OASFilter userFilter;
private OASFilter autoFilter;

DynamicDocument(Config config) {
DynamicDocument(Config config, OASFilter autoFilter) {
ClassLoader cl = OpenApiConstants.classLoader == null ? Thread.currentThread().getContextClassLoader()
: OpenApiConstants.classLoader;
try (InputStream is = cl.getResourceAsStream(OpenApiConstants.BASE_NAME + Format.JSON)) {
if (is != null) {
try (OpenApiStaticFile staticFile = new OpenApiStaticFile(is, Format.JSON)) {
this.generatedOnBuild = OpenApiProcessor.modelFromStaticFile(staticFile);
this.openApiConfig = new OpenApiConfigImpl(config);
this.filter = OpenApiProcessor.getFilter(openApiConfig, cl);
this.userFilter = OpenApiProcessor.getFilter(openApiConfig, cl);
this.autoFilter = autoFilter;
}
}
} catch (IOException ex) {
Expand Down Expand Up @@ -153,7 +156,10 @@ private OpenApiDocument getOpenApiDocument() {
document.reset();
document.config(this.openApiConfig);
document.modelFromStaticFile(this.generatedOnBuild);
document.filter(this.filter);
if (this.autoFilter != null) {
document.filter(this.autoFilter);
}
document.filter(this.userFilter);
document.initialize();
return document;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import java.util.List;

import org.eclipse.microprofile.openapi.OASFilter;

import io.quarkus.arc.Arc;
import io.smallrye.openapi.runtime.io.Format;
import io.vertx.core.Handler;
Expand Down Expand Up @@ -32,9 +34,11 @@ public class OpenApiHandler implements Handler<RoutingContext> {
}

final boolean corsEnabled;
final OASFilter autoSecurityFilter;

public OpenApiHandler(boolean corsEnabled) {
public OpenApiHandler(boolean corsEnabled, OASFilter autoSecurityFilter) {
this.corsEnabled = corsEnabled;
this.autoSecurityFilter = autoSecurityFilter;
}

@Override
Expand Down Expand Up @@ -77,6 +81,7 @@ public void handle(RoutingContext event) {
private OpenApiDocumentService getOpenApiDocumentService() {
if (this.openApiDocumentService == null) {
this.openApiDocumentService = Arc.container().instance(OpenApiDocumentService.class).get();
this.openApiDocumentService.init(this.autoSecurityFilter);
}
return this.openApiDocumentService;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.net.URL;
import java.util.Enumeration;

import org.eclipse.microprofile.openapi.OASFilter;
import org.eclipse.microprofile.openapi.spi.OASFactoryResolver;

import io.quarkus.runtime.ShutdownContext;
Expand All @@ -16,9 +17,10 @@
@Recorder
public class OpenApiRecorder {

public Handler<RoutingContext> handler(OpenApiRuntimeConfig runtimeConfig, HttpConfiguration configuration) {
public Handler<RoutingContext> handler(OpenApiRuntimeConfig runtimeConfig, HttpConfiguration configuration,
OASFilter autoSecurityFilter) {
if (runtimeConfig.enable) {
return new OpenApiHandler(configuration.corsEnabled);
return new OpenApiHandler(configuration.corsEnabled, autoSecurityFilter);
} else {
return new OpenApiNotFoundHandler();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.quarkus.smallrye.openapi.runtime.filter;

import org.eclipse.microprofile.openapi.OASFactory;
import org.eclipse.microprofile.openapi.models.security.SecurityScheme;

/**
* Add Basic Authentication security automatically based on the added security extensions
*/
public class AutoBasicSecurityFilter extends AutoSecurityFilter {
private String basicSecuritySchemeValue;

public AutoBasicSecurityFilter() {
super();
}

public AutoBasicSecurityFilter(String securitySchemeName, String securitySchemeDescription,
String basicSecuritySchemeValue) {
super(securitySchemeName, securitySchemeDescription);
this.basicSecuritySchemeValue = basicSecuritySchemeValue;
}

public String getBasicSecuritySchemeValue() {
return basicSecuritySchemeValue;
}

public void setBasicSecuritySchemeValue(String basicSecuritySchemeValue) {
this.basicSecuritySchemeValue = basicSecuritySchemeValue;
}

@Override
protected SecurityScheme getSecurityScheme() {
SecurityScheme securityScheme = OASFactory.createSecurityScheme();
securityScheme.setType(SecurityScheme.Type.HTTP);
securityScheme.setScheme(basicSecuritySchemeValue);
return securityScheme;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.quarkus.smallrye.openapi.runtime.filter;

import org.eclipse.microprofile.openapi.OASFactory;
import org.eclipse.microprofile.openapi.models.security.SecurityScheme;

/**
* Add JWT Authentication security automatically based on the added security extensions
*/
public class AutoJWTSecurityFilter extends AutoSecurityFilter {
private String jwtSecuritySchemeValue;
private String jwtBearerFormat;

public AutoJWTSecurityFilter() {
super();
}

public AutoJWTSecurityFilter(String securitySchemeName, String securitySchemeDescription, String jwtSecuritySchemeValue,
String jwtBearerFormat) {
super(securitySchemeName, securitySchemeDescription);
this.jwtSecuritySchemeValue = jwtSecuritySchemeValue;
this.jwtBearerFormat = jwtBearerFormat;
}

public String getJwtSecuritySchemeValue() {
return jwtSecuritySchemeValue;
}

public void setJwtSecuritySchemeValue(String jwtSecuritySchemeValue) {
this.jwtSecuritySchemeValue = jwtSecuritySchemeValue;
}

public String getJwtBearerFormat() {
return jwtBearerFormat;
}

public void setJwtBearerFormat(String jwtBearerFormat) {
this.jwtBearerFormat = jwtBearerFormat;
}

@Override
protected SecurityScheme getSecurityScheme() {
SecurityScheme securityScheme = OASFactory.createSecurityScheme();
securityScheme.setType(SecurityScheme.Type.HTTP);
securityScheme.setScheme(jwtSecuritySchemeValue);
securityScheme.setBearerFormat(jwtBearerFormat);
return securityScheme;
}
}
Loading

0 comments on commit fe6b68e

Please sign in to comment.