Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OpenAPI: Added AutoTag and Response for Auto Security Requirement #19464

Merged
merged 1 commit into from
Aug 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@
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.SecurityConfigFilter;
import io.quarkus.smallrye.openapi.deployment.filter.AutoRolesAllowedFilter;
import io.quarkus.smallrye.openapi.deployment.filter.AutoTagFilter;
import io.quarkus.smallrye.openapi.deployment.filter.SecurityConfigFilter;
import io.quarkus.smallrye.openapi.deployment.spi.AddToOpenAPIDefinitionBuildItem;
import io.quarkus.smallrye.openapi.runtime.OpenApiConstants;
import io.quarkus.smallrye.openapi.runtime.OpenApiDocumentService;
Expand Down Expand Up @@ -252,8 +254,105 @@ void addSecurityFilter(BuildProducer<AddToOpenAPIDefinitionBuildItem> 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<String, List<String>> 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<String, String> classNamesMethodReferences = getClassNamesMethodReferences(apiFilteredIndexViewBuildItem);
if (classNamesMethodReferences != null && !classNamesMethodReferences.isEmpty()) {
return new AutoTagFilter(classNamesMethodReferences);
}
}
return null;
}

private Map<String, List<String>> getRolesAllowedMethodReferences(
OpenApiFilteredIndexViewBuildItem apiFilteredIndexViewBuildItem) {
List<AnnotationInstance> rolesAllowedAnnotations = new ArrayList<>();
for (DotName rolesAllowed : SecurityConstants.ROLES_ALLOWED) {
rolesAllowedAnnotations.addAll(apiFilteredIndexViewBuildItem.getIndex().getAnnotations(rolesAllowed));
}
Map<String, List<String>> 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<MethodInfo> 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<String, String> getClassNamesMethodReferences(OpenApiFilteredIndexViewBuildItem apiFilteredIndexViewBuildItem) {
List<AnnotationInstance> openapiAnnotations = new ArrayList<>();
Set<DotName> allOpenAPIEndpoints = getAllOpenAPIEndpoints();
for (DotName dotName : allOpenAPIEndpoints) {
openapiAnnotations.addAll(apiFilteredIndexViewBuildItem.getIndex().getAnnotations(dotName));
}

Map<String, String> 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) {
Expand Down Expand Up @@ -310,9 +409,7 @@ public void registerOpenApiSchemaClassesForReflection(BuildProducer<ReflectiveCl
}

private boolean isOpenAPIEndpoint(MethodInfo method) {
Set<DotName> httpAnnotations = new HashSet<>();
httpAnnotations.addAll(JaxRsConstants.HTTP_METHODS);
httpAnnotations.addAll(SpringConstants.HTTP_METHODS);
Set<DotName> httpAnnotations = getAllOpenAPIEndpoints();
for (DotName httpAnnotation : httpAnnotations) {
if (method.hasAnnotation(httpAnnotation)) {
return true;
Expand All @@ -321,6 +418,13 @@ private boolean isOpenAPIEndpoint(MethodInfo method) {
return false;
}

private Set<DotName> getAllOpenAPIEndpoints() {
Set<DotName> httpAnnotations = new HashSet<>();
httpAnnotations.addAll(JaxRsConstants.HTTP_METHODS);
httpAnnotations.addAll(SpringConstants.HTTP_METHODS);
return httpAnnotations;
}

private void registerReflectionForApiResponseSchemaSerialization(BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
BuildProducer<ReflectiveHierarchyBuildItem> reflectiveHierarchy,
Collection<AnnotationInstance> apiResponseAnnotationInstances) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package io.quarkus.smallrye.openapi.deployment.filter;

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<String, List<String>> methodReferences;
private String defaultSecuritySchemeName;

public AutoRolesAllowedFilter() {

}

public AutoRolesAllowedFilter(String defaultSecuritySchemeName, Map<String, List<String>> methodReferences) {
this.defaultSecuritySchemeName = defaultSecuritySchemeName;
this.methodReferences = methodReferences;
}

public Map<String, List<String>> getMethodReferences() {
return methodReferences;
}

public void setMethodReferences(Map<String, List<String>> 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<String, PathItem> pathItems = paths.getPathItems();
if (pathItems != null && !pathItems.isEmpty()) {
Set<Map.Entry<String, PathItem>> pathItemsEntries = pathItems.entrySet();
for (Map.Entry<String, PathItem> pathItem : pathItemsEntries) {
Map<PathItem.HttpMethod, Operation> 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<String> 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<String, SecurityScheme> securitySchemes = openAPI.getComponents().getSecuritySchemes();
return securitySchemes.keySet().iterator().next();
}
return defaultSecuritySchemeName;
}

private List<APIResponseImpl> getSecurityResponses() {
List<APIResponseImpl> 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;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package io.quarkus.smallrye.openapi.deployment.filter;

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<String, String> classNameMap;

public AutoTagFilter() {

}

public AutoTagFilter(Map<String, String> classNameMap) {
this.classNameMap = classNameMap;
}

public Map<String, String> getClassNameMap() {
return classNameMap;
}

public void setClassNameMap(Map<String, String> classNameMap) {
this.classNameMap = classNameMap;
}

@Override
public void filterOpenAPI(OpenAPI openAPI) {
if (!classNameMap.isEmpty()) {
Paths paths = openAPI.getPaths();
if (paths != null) {
Map<String, PathItem> pathItems = paths.getPathItems();
if (pathItems != null && !pathItems.isEmpty()) {
Set<Map.Entry<String, PathItem>> pathItemsEntries = pathItems.entrySet();
for (Map.Entry<String, PathItem> pathItem : pathItemsEntries) {
Map<PathItem.HttpMethod, Operation> 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])"),
" ");
}
}
Loading