Skip to content

Commit 27e5a46

Browse files
committed
Fixed bugs and converged vendor extentions to annotate jakarata @RolesAllowed
1 parent c22c0f5 commit 27e5a46

23 files changed

Lines changed: 811 additions & 22 deletions

docs/generators/jaxrs-cxf-cdi.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,10 @@ These options may be applied as additional-properties (cli) or configOptions (pl
8282
|title|a title describing the application| |OpenAPI Server|
8383
|useBeanValidation|Use BeanValidation API annotations| |true|
8484
|useJakartaEe|whether to use Jakarta EE namespace instead of javax| |false|
85+
|useJakartaSecurityAnnotations|Whether to generate Jakarta security annotations (@RolesAllowed, @PermitAll). Requires useJakartaEe=true. Currently only supported when library is set to quarkus.| |false|
8586
|useMicroProfileOpenAPIAnnotations|Whether to generate Microprofile OpenAPI annotations. Only valid when library is set to quarkus.| |false|
8687
|useMutiny|Whether to use Smallrye Mutiny instead of CompletionStage for asynchronous computation. Only valid when library is set to quarkus.| |false|
8788
|useOneOfInterfaces|whether to use a java interface to describe a set of oneOf options, where each option is a class that implements the interface| |false|
88-
|useQuarkusSecurityAnnotations|Whether to generate Quarkus security annotations (@Authenticated, @RolesAllowed, @PermitAll). Only valid when library is set to quarkus.| |false|
8989
|useSwaggerAnnotations|Whether to generate Swagger annotations.| |true|
9090
|useSwaggerV3Annotations|Whether to generate Swagger v3 (OpenAPI v3) annotations.| |false|
9191
|useTags|use tags for creating interface and controller classnames| |false|

docs/generators/jaxrs-spec.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,10 @@ These options may be applied as additional-properties (cli) or configOptions (pl
8383
|title|a title describing the application| |OpenAPI Server|
8484
|useBeanValidation|Use BeanValidation API annotations| |true|
8585
|useJakartaEe|whether to use Jakarta EE namespace instead of javax| |false|
86+
|useJakartaSecurityAnnotations|Whether to generate Jakarta security annotations (@RolesAllowed, @PermitAll). Requires useJakartaEe=true. Currently only supported when library is set to quarkus.| |false|
8687
|useMicroProfileOpenAPIAnnotations|Whether to generate Microprofile OpenAPI annotations. Only valid when library is set to quarkus.| |false|
8788
|useMutiny|Whether to use Smallrye Mutiny instead of CompletionStage for asynchronous computation. Only valid when library is set to quarkus.| |false|
8889
|useOneOfInterfaces|whether to use a java interface to describe a set of oneOf options, where each option is a class that implements the interface| |false|
89-
|useQuarkusSecurityAnnotations|Whether to generate Quarkus security annotations (@Authenticated, @RolesAllowed, @PermitAll). Only valid when library is set to quarkus.| |false|
9090
|useSwaggerAnnotations|Whether to generate Swagger annotations.| |true|
9191
|useSwaggerV3Annotations|Whether to generate Swagger v3 (OpenAPI v3) annotations.| |false|
9292
|useTags|use tags for creating interface and controller classnames| |false|
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
* Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
3+
* Copyright 2018 SmartBear Software
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.openapitools.codegen.languages;
19+
20+
import io.swagger.v3.oas.models.OpenAPI;
21+
import io.swagger.v3.oas.models.Operation;
22+
import io.swagger.v3.oas.models.security.SecurityRequirement;
23+
import io.swagger.v3.oas.models.security.SecurityScheme;
24+
import java.util.Collections;
25+
import java.util.List;
26+
import java.util.Map;
27+
import org.openapitools.codegen.CodegenOperation;
28+
import org.slf4j.Logger;
29+
import org.slf4j.LoggerFactory;
30+
31+
/**
32+
* Translates an OpenAPI operation's security requirements into Jakarta security
33+
* vendor extensions on a {@link CodegenOperation} for downstream Mustache templates.
34+
*
35+
* <p>The OpenAPI {@code security} array uses OR semantics (any one alternative
36+
* satisfies the request); the Jakarta annotations are AND-stacked. The two cannot
37+
* always be reconciled, so this class emits the least restrictive annotation that
38+
* is still correct for the OR group.
39+
*
40+
* <p>A single vendor extension {@code x-jakarta-roles-allowed} carries the value to
41+
* emit. For the any-authenticated-user case it is set to the singleton list
42+
* {@code ["**"]}, producing {@code @RolesAllowed({"**"})}. Future PRs will reuse
43+
* the same extension to emit scoped roles (e.g. {@code ["admin"]}) without needing
44+
* a second flag or template branch.
45+
*/
46+
final class JakartaSecurityAnnotationProcessor {
47+
48+
static final String VENDOR_X_JAKARTA_ROLES_ALLOWED = "x-jakarta-roles-allowed";
49+
50+
private static final List<String> ANY_AUTHENTICATED_ROLE = Collections.singletonList("**");
51+
52+
private final Logger LOGGER = LoggerFactory.getLogger(JakartaSecurityAnnotationProcessor.class);
53+
54+
/**
55+
* Inspects {@code rawOp}'s security requirements (falling back to the global
56+
* {@code openAPI.security} when the operation does not override) and sets
57+
* {@code x-jakarta-roles-allowed} on {@code op} when the operation qualifies
58+
* for {@code @RolesAllowed} emission.
59+
*/
60+
void applyTo(CodegenOperation op, Operation rawOp, OpenAPI openAPI) {
61+
// Use the raw Operation here rather than op.authMethods: by the time postProcessOperationsWithModels
62+
// runs, DefaultGenerator.filterAuthMethods has flattened all SecurityRequirements into a plain list,
63+
// losing the AND-group structure needed to evaluate mixed-scope combinations correctly.
64+
List<SecurityRequirement> requirements = rawOp.getSecurity();
65+
if (requirements == null) {
66+
// Fall back to the global security block when the operation does not override it.
67+
requirements = openAPI.getSecurity();
68+
}
69+
Map<String, SecurityScheme> schemes = resolveSchemes(openAPI);
70+
71+
if (qualifiesForAnyRoles(requirements, schemes)) {
72+
op.vendorExtensions.put(VENDOR_X_JAKARTA_ROLES_ALLOWED, ANY_AUTHENTICATED_ROLE);
73+
}
74+
}
75+
76+
/**
77+
* Returns true when at least one OR alternative fully qualifies for
78+
* {@code @RolesAllowed({"**"})} and no alternative is anonymous ({@code - {}}).
79+
*
80+
* <p>An empty {@link SecurityRequirement} ({@code - {}}) inside the OR list means
81+
* the operation may also be called unauthenticated. When that is present, the
82+
* least-restrictive alternative is "no auth required", so emitting
83+
* {@code @RolesAllowed({"**"})} would force authentication and contradict the
84+
* spec -- we return false instead and let the future {@code @PermitAll} branch
85+
* handle that case.
86+
*/
87+
private boolean qualifiesForAnyRoles(List<SecurityRequirement> requirements,
88+
Map<String, SecurityScheme> schemes) {
89+
if (requirements == null || requirements.isEmpty()) {
90+
return false;
91+
}
92+
boolean anyQualifies = false;
93+
for (SecurityRequirement requirement : requirements) {
94+
if (requirement.isEmpty()) {
95+
// Anonymous OR alternative -- least restrictive wins; do not emit @RolesAllowed.
96+
return false;
97+
}
98+
if (andGroupQualifies(requirement, schemes)) {
99+
anyQualifies = true;
100+
}
101+
}
102+
return anyQualifies;
103+
}
104+
105+
/**
106+
* A single {@link SecurityRequirement} is an AND group: all schemes must be
107+
* satisfied simultaneously. If any scheme in the group has explicit scopes
108+
* (e.g. {@code oauth2: [admin:write]}), the combined requirement is more
109+
* restrictive than "any authenticated user" and does not qualify.
110+
*/
111+
private boolean andGroupQualifies(SecurityRequirement requirement, Map<String, SecurityScheme> schemes) {
112+
for (Map.Entry<String, List<String>> entry : requirement.entrySet()) {
113+
SecurityScheme scheme = schemes.get(entry.getKey());
114+
if (scheme == null) {
115+
LOGGER.warn("Security requirement references undefined scheme '{}' -- skipping Jakarta security annotation for this AND group.",
116+
entry.getKey());
117+
return false;
118+
}
119+
if (!schemeQualifies(scheme, entry.getValue())) {
120+
return false;
121+
}
122+
}
123+
return true;
124+
}
125+
126+
private boolean schemeQualifies(SecurityScheme scheme, List<String> scopes) {
127+
if (scheme.getType() == null) {
128+
LOGGER.warn("Security scheme is missing 'type' -- skipping Jakarta security annotation.");
129+
return false;
130+
}
131+
switch (scheme.getType()) {
132+
case OAUTH2:
133+
case OPENIDCONNECT:
134+
// Empty scope list means the operation requires authentication but no specific role,
135+
// so @RolesAllowed({"**"}) is correct. Non-empty scopes belong to a future @RolesAllowed({scope}) PR.
136+
return scopes == null || scopes.isEmpty();
137+
case HTTP:
138+
case APIKEY:
139+
case MUTUALTLS:
140+
// These schemes have no scope concept; any valid credential satisfies them.
141+
return true;
142+
default:
143+
LOGGER.warn("Unrecognised security scheme type '{}' -- skipping Jakarta security annotation.",
144+
scheme.getType());
145+
return false;
146+
}
147+
}
148+
149+
private static Map<String, SecurityScheme> resolveSchemes(OpenAPI openAPI) {
150+
if (openAPI.getComponents() != null && openAPI.getComponents().getSecuritySchemes() != null) {
151+
return openAPI.getComponents().getSecuritySchemes();
152+
}
153+
return Collections.emptyMap();
154+
}
155+
}

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaJAXRSSpecServerCodegen.java

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717

1818
package org.openapitools.codegen.languages;
1919

20+
import io.swagger.v3.oas.models.Operation;
2021
import io.swagger.v3.oas.models.media.Schema;
22+
import io.swagger.v3.oas.models.servers.Server;
2123
import java.util.Locale;
2224
import lombok.Getter;
2325
import lombok.Setter;
@@ -53,7 +55,7 @@ public class JavaJAXRSSpecServerCodegen extends AbstractJavaJAXRSServerCodegen {
5355
public static final String USE_MUTINY = "useMutiny";
5456
public static final String OPEN_API_SPEC_FILE_LOCATION = "openApiSpecFileLocation";
5557
public static final String GENERATE_JSON_CREATOR = "generateJsonCreator";
56-
public static final String USE_QUARKUS_SECURITY_ANNOTATIONS = "useQuarkusSecurityAnnotations";
58+
public static final String USE_JAKARTA_SECURITY_ANNOTATIONS = "useJakartaSecurityAnnotations";
5759

5860
public static final String QUARKUS_LIBRARY = "quarkus";
5961
public static final String THORNTAIL_LIBRARY = "thorntail";
@@ -69,7 +71,9 @@ public class JavaJAXRSSpecServerCodegen extends AbstractJavaJAXRSServerCodegen {
6971
private boolean useSwaggerV3Annotations = false;
7072
private boolean useMicroProfileOpenAPIAnnotations = false;
7173
private boolean useMutiny = false;
72-
private boolean useQuarkusSecurityAnnotations = false;
74+
private boolean useJakartaSecurityAnnotations = false;
75+
76+
private final JakartaSecurityAnnotationProcessor jakartaSecurityAnnotationProcessor = new JakartaSecurityAnnotationProcessor();
7377

7478
@Getter @Setter
7579
protected boolean generateJsonCreator = true;
@@ -149,7 +153,7 @@ public JavaJAXRSSpecServerCodegen() {
149153
cliOptions.add(CliOption.newString(OPEN_API_SPEC_FILE_LOCATION, "Location where the file containing the spec will be generated in the output folder. No file generated when set to null or empty string."));
150154
cliOptions.add(CliOption.newBoolean(SUPPORT_ASYNC, "Wrap responses in CompletionStage type, allowing asynchronous computation (requires JAX-RS 2.1).", supportAsync));
151155
cliOptions.add(CliOption.newBoolean(USE_MUTINY, "Whether to use Smallrye Mutiny instead of CompletionStage for asynchronous computation. Only valid when library is set to quarkus.", useMutiny));
152-
cliOptions.add(CliOption.newBoolean(USE_QUARKUS_SECURITY_ANNOTATIONS, "Whether to generate Quarkus security annotations (@Authenticated, @RolesAllowed, @PermitAll). Only valid when library is set to quarkus.", useQuarkusSecurityAnnotations));
156+
cliOptions.add(CliOption.newBoolean(USE_JAKARTA_SECURITY_ANNOTATIONS, "Whether to generate Jakarta security annotations (@RolesAllowed, @PermitAll). Requires useJakartaEe=true. Currently only supported when library is set to quarkus.", useJakartaSecurityAnnotations));
153157
cliOptions.add(CliOption.newBoolean(GENERATE_JSON_CREATOR, "Whether to generate @JsonCreator constructor for required properties.", generateJsonCreator));
154158
}
155159

@@ -192,10 +196,6 @@ public void processOpts() {
192196
convertPropertyToBooleanAndWriteBack(USE_MUTINY, value -> useMutiny = value);
193197
}
194198

195-
if (QUARKUS_LIBRARY.equals(library)) {
196-
convertPropertyToBooleanAndWriteBack(USE_QUARKUS_SECURITY_ANNOTATIONS, value -> useQuarkusSecurityAnnotations = value);
197-
}
198-
199199
convertPropertyToBooleanAndWriteBack(GENERATE_JSON_CREATOR, this::setGenerateJsonCreator);
200200

201201
if (additionalProperties.containsKey(OPEN_API_SPEC_FILE_LOCATION)) {
@@ -215,6 +215,18 @@ public void processOpts() {
215215

216216
super.processOpts();
217217

218+
// We need to call super.processOpts() before evaluating the `library`, otherwise `library` is null when set via `configOptions` instead of via `library.set("quarkus")` in Gradle
219+
if (QUARKUS_LIBRARY.equals(library)) {
220+
convertPropertyToBooleanAndWriteBack(USE_JAKARTA_SECURITY_ANNOTATIONS, value -> useJakartaSecurityAnnotations = value);
221+
}
222+
223+
if (useJakartaSecurityAnnotations && !useJakartaEe) {
224+
throw new IllegalArgumentException(
225+
"Flag '" + USE_JAKARTA_SECURITY_ANNOTATIONS + "' requires '" + USE_JAKARTA_EE
226+
+ "=true'. The generated annotation '@jakarta.annotation.security.RolesAllowed' "
227+
+ "is incompatible with the javax.* namespace.");
228+
}
229+
218230
// expose flags to templates
219231
additionalProperties.put(USE_SWAGGER_ANNOTATIONS, useSwaggerAnnotations);
220232
additionalProperties.put(USE_SWAGGER_V3_ANNOTATIONS, useSwaggerV3Annotations);
@@ -391,4 +403,13 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
391403
}
392404
return result;
393405
}
406+
407+
@Override
408+
public CodegenOperation fromOperation(String path, String httpMethod, Operation operation, List<Server> servers) {
409+
CodegenOperation op = super.fromOperation(path, httpMethod, operation, servers);
410+
if (QUARKUS_LIBRARY.equals(getLibrary()) && useJakartaSecurityAnnotations) {
411+
jakartaSecurityAnnotationProcessor.applyTo(op, operation, openAPI);
412+
}
413+
return op;
414+
}
394415
}

modules/openapi-generator/src/main/resources/JavaJaxRS/spec/libraries/quarkus/apiInterface.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,4 @@
5353
{{#vendorExtensions.x-java-success-response-code}}
5454
@ResponseStatus({{{vendorExtensions.x-java-success-response-code}}})
5555
{{/vendorExtensions.x-java-success-response-code}}
56-
{{#supportAsync}}{{>returnAsyncTypeInterface}}{{/supportAsync}}{{^supportAsync}}{{#returnJBossResponse}}{{>returnResponseTypeInterface}}{{/returnJBossResponse}}{{^returnJBossResponse}}{{#returnResponse}}Response{{/returnResponse}}{{^returnResponse}}{{>returnTypeInterface}}{{/returnResponse}}{{/returnJBossResponse}}{{/supportAsync}} {{nickname}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>cookieParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}});
56+
{{>jakartaSecurityAnnotations}}{{#supportAsync}}{{>returnAsyncTypeInterface}}{{/supportAsync}}{{^supportAsync}}{{#returnJBossResponse}}{{>returnResponseTypeInterface}}{{/returnJBossResponse}}{{^returnJBossResponse}}{{#returnResponse}}Response{{/returnResponse}}{{^returnResponse}}{{>returnTypeInterface}}{{/returnResponse}}{{/returnJBossResponse}}{{/supportAsync}} {{nickname}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>cookieParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}});

modules/openapi-generator/src/main/resources/JavaJaxRS/spec/libraries/quarkus/apiMethod.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,6 @@
4747
{{^vendorExtensions.x-java-is-response-void}}@org.eclipse.microprofile.openapi.annotations.media.Content(schema = @org.eclipse.microprofile.openapi.annotations.media.Schema(implementation = {{{baseType}}}.class{{#vendorExtensions.x-microprofile-open-api-return-schema-container}}, type = {{{.}}} {{/vendorExtensions.x-microprofile-open-api-return-schema-container}}{{#vendorExtensions.x-microprofile-open-api-return-unique-items}}, uniqueItems = true {{/vendorExtensions.x-microprofile-open-api-return-unique-items}})){{/vendorExtensions.x-java-is-response-void}}
4848
}){{^-last}},{{/-last}}{{/responses}}
4949
}){{/hasProduces}}{{/useMicroProfileOpenAPIAnnotations}}
50-
public {{#supportAsync}}{{#useMutiny}}Uni{{/useMutiny}}{{^useMutiny}}CompletionStage{{/useMutiny}}<{{/supportAsync}}{{#returnJBossResponse}}{{>returnResponseTypeInterface}}{{/returnJBossResponse}}{{^returnJBossResponse}}Response{{/returnJBossResponse}}{{#supportAsync}}>{{/supportAsync}} {{nickname}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>cookieParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}}) {
50+
{{>jakartaSecurityAnnotations}}public {{#supportAsync}}{{#useMutiny}}Uni{{/useMutiny}}{{^useMutiny}}CompletionStage{{/useMutiny}}<{{/supportAsync}}{{#returnJBossResponse}}{{>returnResponseTypeInterface}}{{/returnJBossResponse}}{{^returnJBossResponse}}Response{{/returnJBossResponse}}{{#supportAsync}}>{{/supportAsync}} {{nickname}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>cookieParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}}) {
5151
return {{#supportAsync}}{{#useMutiny}}Uni.createFrom().item({{/useMutiny}}{{^useMutiny}}CompletableFuture.supplyAsync(() -> {{/useMutiny}}{{/supportAsync}}Response.ok().entity("magic!").build(){{#supportAsync}}){{/supportAsync}};
5252
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{{#vendorExtensions.x-jakarta-roles-allowed.0}}@jakarta.annotation.security.RolesAllowed({{openbrace}}{{#vendorExtensions.x-jakarta-roles-allowed}}"{{.}}"{{^-last}},{{/-last}}{{/vendorExtensions.x-jakarta-roles-allowed}}{{closebrace}})
2+
{{/vendorExtensions.x-jakarta-roles-allowed.0}}

0 commit comments

Comments
 (0)