Skip to content

Commit 6863eb5

Browse files
Fix #933 - Filter oauth security definitions from spec when generating CompositeAuthenticationProvider (#942) (#950)
* Fix #933 - Filter oauth security definitions from spec when generating * Make 'resolve' more complex by searching for assignable parameters * Incorporating @hbelmiro's review --------- Signed-off-by: Ricardo Zanini <[email protected]> Co-authored-by: Ricardo Zanini <[email protected]>
1 parent e44d23d commit 6863eb5

File tree

4 files changed

+148
-5
lines changed

4 files changed

+148
-5
lines changed

client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/template/OpenApiNamespaceResolver.java

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
package io.quarkiverse.openapi.generator.deployment.template;
22

33
import java.io.File;
4+
import java.lang.reflect.Method;
45
import java.nio.file.Path;
6+
import java.util.ArrayList;
7+
import java.util.Collections;
58
import java.util.HashMap;
9+
import java.util.LinkedHashMap;
10+
import java.util.List;
611
import java.util.concurrent.CompletableFuture;
712
import java.util.concurrent.CompletionStage;
813
import java.util.concurrent.ExecutionException;
14+
import java.util.stream.Collectors;
915

16+
import org.openapitools.codegen.CodegenSecurity;
1017
import org.openapitools.codegen.model.OperationMap;
1118

1219
import io.quarkiverse.openapi.generator.deployment.codegen.OpenApiGeneratorOutputPaths;
@@ -32,6 +39,7 @@ private OpenApiNamespaceResolver() {
3239
* @param codegenConfig Map with the model codegen properties
3340
* @return true if the given model class should generate the deprecated attributes
3441
*/
42+
@SuppressWarnings("unused")
3543
public boolean genDeprecatedModelAttr(final String pkg, final String classname,
3644
final HashMap<String, Object> codegenConfig) {
3745
final String key = String.format("%s.%s.%s", pkg, classname, GENERATE_DEPRECATED_PROP);
@@ -44,38 +52,92 @@ public boolean genDeprecatedModelAttr(final String pkg, final String classname,
4452
* @param codegenConfig Map with the model codegen properties
4553
* @return true if the given model class should generate the deprecated attributes
4654
*/
55+
@SuppressWarnings("unused")
4756
public boolean genDeprecatedApiAttr(final String pkg, final String classname,
4857
final HashMap<String, Object> codegenConfig) {
4958
final String key = String.format("%s.%s.%s", pkg, classname, GENERATE_DEPRECATED_PROP);
5059
return Boolean.parseBoolean(codegenConfig.getOrDefault(key, "true").toString());
5160
}
5261

62+
@SuppressWarnings("unused")
5363
public String parseUri(String uri) {
5464
return OpenApiGeneratorOutputPaths.getRelativePath(Path.of(uri)).toString().replace(File.separatorChar, '/');
5565
}
5666

67+
@SuppressWarnings("unused")
5768
public boolean hasAuthMethods(OperationMap operations) {
5869
return operations != null && operations.getOperation().stream().anyMatch(operation -> operation.hasAuthMethods);
5970
}
6071

72+
/**
73+
* Ignore the OAuth flows by filtering every oauth instance by name. The inner openapi-generator library duplicates the
74+
* OAuth instances per flow in the openapi spec.
75+
* So a specification file with more than one flow defined has two entries in the list. For now, we do not use this
76+
* information in runtime so it can be safely filtered and ignored.
77+
*
78+
* @param oauthOperations passed through the Qute template
79+
* @see "resources/templates/libraries/microprofile/auth/compositeAuthenticationProvider.qute"
80+
* @return The list filtered by unique auth name
81+
*/
82+
@SuppressWarnings("unused")
83+
public List<CodegenSecurity> getUniqueOAuthOperations(List<CodegenSecurity> oauthOperations) {
84+
if (oauthOperations != null) {
85+
return new ArrayList<>(oauthOperations.stream()
86+
.collect(Collectors.toMap(security -> security.name, security -> security,
87+
(existing, replacement) -> existing, LinkedHashMap::new))
88+
.values());
89+
}
90+
return Collections.emptyList();
91+
}
92+
6193
@Override
6294
public CompletionStage<Object> resolve(EvalContext context) {
6395
try {
64-
Class<?>[] classArgs = new Class[context.getParams().size()];
6596
Object[] args = new Object[context.getParams().size()];
97+
Class<?>[] classArgs = new Class[context.getParams().size()];
98+
6699
int i = 0;
67100
for (Expression expr : context.getParams()) {
68101
args[i] = context.evaluate(expr).toCompletableFuture().get();
69102
classArgs[i] = args[i].getClass();
70103
i++;
71104
}
72-
return CompletableFuture
73-
.completedFuture(this.getClass().getMethod(context.getName(), classArgs).invoke(this, args));
105+
106+
Method targetMethod = findCompatibleMethod(context.getName(), classArgs);
107+
if (targetMethod == null) {
108+
throw new NoSuchMethodException("No compatible method found for: " + context.getName());
109+
}
110+
111+
return CompletableFuture.completedFuture(targetMethod.invoke(this, args));
74112
} catch (ReflectiveOperationException | InterruptedException | ExecutionException ex) {
75113
return CompletableFuture.failedStage(ex);
76114
}
77115
}
78116

117+
private Method findCompatibleMethod(String methodName, Class<?>[] argTypes) {
118+
for (Method method : this.getClass().getMethods()) {
119+
if (method.getName().equals(methodName)) {
120+
Class<?>[] paramTypes = method.getParameterTypes();
121+
if (isAssignable(paramTypes, argTypes)) {
122+
return method;
123+
}
124+
}
125+
}
126+
return null;
127+
}
128+
129+
private boolean isAssignable(Class<?>[] paramTypes, Class<?>[] argTypes) {
130+
if (paramTypes.length != argTypes.length) {
131+
return false;
132+
}
133+
for (int i = 0; i < paramTypes.length; i++) {
134+
if (!paramTypes[i].isAssignableFrom(argTypes[i])) {
135+
return false;
136+
}
137+
}
138+
return true;
139+
}
140+
79141
@Override
80142
public String getNamespace() {
81143
return "openapi";

client/deployment/src/main/resources/templates/libraries/microprofile/auth/compositeAuthenticationProvider.qute

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package {apiPackage}.auth;
22

33
@jakarta.annotation.Priority(jakarta.ws.rs.Priorities.AUTHENTICATION)
4-
{#for auth in oauthMethods.orEmpty}
5-
@io.quarkiverse.openapi.generator.markers.OauthAuthenticationMarker(name="{auth.name}", openApiSpecId="{configKey}")
4+
{#for auth in openapi:getUniqueOAuthOperations(oauthMethods.orEmpty)}
5+
@io.quarkiverse.openapi.generator.markers.OauthAuthenticationMarker(name="{auth.name}", openApiSpecId="{quarkus-generator.openApiSpecId}")
66
{/for}
77
{#for auth in httpBasicMethods.orEmpty}
88
@io.quarkiverse.openapi.generator.markers.BasicAuthenticationMarker(name="{auth.name}", openApiSpecId="{quarkus-generator.openApiSpecId}")

client/deployment/src/test/java/io/quarkiverse/openapi/generator/deployment/wrapper/OpenApiClientGeneratorWrapperTest.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
import com.github.javaparser.ast.body.Parameter;
3434
import com.github.javaparser.ast.body.VariableDeclarator;
3535
import com.github.javaparser.ast.expr.AnnotationExpr;
36+
import com.github.javaparser.ast.expr.Expression;
37+
import com.github.javaparser.ast.expr.MemberValuePair;
3638
import com.github.javaparser.ast.expr.SimpleName;
3739
import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr;
3840
import com.github.javaparser.ast.nodeTypes.NodeWithName;
@@ -50,6 +52,38 @@ private static Optional<MethodDeclaration> getMethodDeclarationByIdentifier(List
5052
return methodDeclarations.stream().filter(md -> md.getName().getIdentifier().equals(methodName)).findAny();
5153
}
5254

55+
@Test
56+
void verifyOAuthDuplicateAnnotationOnCompositeAuthProvider() throws URISyntaxException, FileNotFoundException {
57+
OpenApiClientGeneratorWrapper generatorWrapper = createGeneratorWrapper("issue-933-security.yaml");
58+
final List<File> generatedFiles = generatorWrapper.generate("org.issue933");
59+
60+
assertNotNull(generatedFiles);
61+
assertFalse(generatedFiles.isEmpty());
62+
63+
final Optional<File> authProviderFile = generatedFiles.stream()
64+
.filter(f -> f.getName().endsWith("CompositeAuthenticationProvider.java")).findFirst();
65+
assertThat(authProviderFile).isPresent();
66+
67+
CompilationUnit compilationUnit = StaticJavaParser.parse(authProviderFile.orElseThrow());
68+
// Get the class declaration
69+
ClassOrInterfaceDeclaration classDeclaration = compilationUnit.findFirst(ClassOrInterfaceDeclaration.class)
70+
.orElseThrow(() -> new AssertionError("Class not found in the file"));
71+
72+
// Collect all OauthAuthenticationMarker annotations
73+
long oauthAnnotationsCount = classDeclaration.getAnnotations().stream()
74+
.filter(annotation -> annotation.getNameAsString()
75+
.equals("io.quarkiverse.openapi.generator.markers.OauthAuthenticationMarker"))
76+
.filter(Expression::isNormalAnnotationExpr)
77+
.filter(annotation -> annotation
78+
.findFirst(MemberValuePair.class,
79+
pair -> pair.getNameAsString().equals("name") && pair.getValue().toString().equals("\"oauth\""))
80+
.isPresent())
81+
.count();
82+
83+
// Assert that there's exactly one annotation with name=oauth
84+
assertThat(oauthAnnotationsCount).isEqualTo(1);
85+
}
86+
5387
@Test
5488
void verifyDiscriminatorGeneration() throws java.net.URISyntaxException, FileNotFoundException {
5589
OpenApiClientGeneratorWrapper generatorWrapper = createGeneratorWrapper("issue-852.json");
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
openapi: 3.0.3
2+
info:
3+
title: Generated API
4+
version: "1.0"
5+
paths:
6+
/:
7+
post:
8+
operationId: doOperation
9+
security:
10+
- client_id: [ ]
11+
- oauth: [ read, write ]
12+
- bearerAuth: [ ]
13+
requestBody:
14+
content:
15+
application/json:
16+
schema:
17+
$ref: '#/components/schemas/MultiplicationOperation'
18+
responses:
19+
"200":
20+
description: OK
21+
components:
22+
schemas:
23+
MultiplicationOperation:
24+
type: object
25+
securitySchemes:
26+
client_id:
27+
type: apiKey
28+
in: header
29+
name: X-Client-Id
30+
x-key-type: clientId
31+
bearerAuth:
32+
type: http
33+
scheme: bearer
34+
oauth:
35+
type: oauth2
36+
flows:
37+
authorizationCode:
38+
authorizationUrl: https://example.com/oauth/authorize
39+
tokenUrl: https://example.com/oauth/token
40+
scopes:
41+
read: Grants read access
42+
write: Grants write access
43+
admin: Grants read and write access to administrative information
44+
clientCredentials:
45+
tokenUrl: http://localhost:8382/oauth/token
46+
scopes:
47+
read: read

0 commit comments

Comments
 (0)