Skip to content

Commit

Permalink
Merge pull request #19464 from phillip-kruger/openapi-autotag
Browse files Browse the repository at this point in the history
OpenAPI: Added AutoTag and Response for Auto Security Requirement
  • Loading branch information
phillip-kruger authored Aug 20, 2021
2 parents 0cd5c9f + 6a2f5e2 commit 1a63a85
Show file tree
Hide file tree
Showing 8 changed files with 380 additions and 46 deletions.
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

0 comments on commit 1a63a85

Please sign in to comment.