Skip to content

Commit 0b60e56

Browse files
committed
[jaxrs-spec][quarkus] Add support for @permitAll annotation and update security handling for unauthenticated operations
1 parent d2d510b commit 0b60e56

14 files changed

Lines changed: 419 additions & 83 deletions

File tree

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

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,35 +40,38 @@
4040
* always be reconciled, so this class emits the least restrictive annotation that
4141
* is still correct for the OR group.
4242
*
43-
* <p>A single vendor extension {@code x-jakarta-roles-allowed} carries the value to
44-
* emit:
43+
* <p>Two mutually exclusive vendor extensions carry the emission decision:
4544
* <ul>
46-
* <li>{@code ["**"]} for the any-authenticated-user case, producing
47-
* {@code @RolesAllowed({"**"})}.
48-
* <li>A sorted, deduplicated list of scope names (e.g. {@code ["admin", "user"]})
49-
* when every OR alternative is scoped, producing
45+
* <li>{@code x-jakarta-roles-allowed} = {@code ["**"]} for the any-authenticated-user
46+
* case, producing {@code @RolesAllowed({"**"})}.
47+
* <li>{@code x-jakarta-roles-allowed} = sorted, deduplicated list of scope names
48+
* (e.g. {@code ["admin", "user"]}) when every OR alternative is scoped, producing
5049
* {@code @RolesAllowed({"admin","user"})}.
51-
* <li>Unset when the operation does not qualify (anonymous OR alternative,
52-
* mixed-scope AND group, etc.).
50+
* <li>{@code x-jakarta-permit-all} = {@code true} when the operation is unauthenticated
51+
* (explicit {@code security: []}, an anonymous {@code - {}} OR alternative, or an
52+
* entirely unsecured spec), producing {@code @PermitAll}.
53+
* <li>Neither set when the operation does not qualify (mixed-scope AND group,
54+
* undefined scheme, etc.) — nothing is emitted and a warning is logged.
5355
* </ul>
5456
*
55-
* <p>The wildcard and scoped emissions are mutually exclusive per operation: if any
56-
* OR alternative qualifies as "any authenticated user", the wildcard wins and the
57-
* scoped path is skipped.
57+
* <p>The three emissions are mutually exclusive per operation: if any OR alternative
58+
* qualifies as "any authenticated user", the wildcard wins; otherwise the scoped path
59+
* is tried; otherwise {@code @PermitAll} is tried.
5860
*/
5961
final class JakartaSecurityAnnotationProcessor {
6062

6163
static final String VENDOR_X_JAKARTA_ROLES_ALLOWED = "x-jakarta-roles-allowed";
64+
static final String VENDOR_X_JAKARTA_PERMIT_ALL = "x-jakarta-permit-all";
6265

6366
private static final List<String> ANY_AUTHENTICATED_ROLE = Collections.singletonList("**");
6467

6568
private final Logger LOGGER = LoggerFactory.getLogger(JakartaSecurityAnnotationProcessor.class);
6669

6770
/**
6871
* Inspects {@code rawOp}'s security requirements (falling back to the global
69-
* {@code openAPI.security} when the operation does not override) and sets
70-
* {@code x-jakarta-roles-allowed} on {@code op} when the operation qualifies
71-
* for {@code @RolesAllowed} emission.
72+
* {@code openAPI.security} when the operation does not override) and sets either
73+
* {@code x-jakarta-roles-allowed} (for {@code @RolesAllowed}) or
74+
* {@code x-jakarta-permit-all} (for {@code @PermitAll}) on {@code op}.
7275
*/
7376
void applyTo(CodegenOperation op, Operation rawOp, OpenAPI openAPI) {
7477
// Use the raw Operation here rather than op.authMethods: by the time postProcessOperationsWithModels
@@ -88,6 +91,10 @@ void applyTo(CodegenOperation op, Operation rawOp, OpenAPI openAPI) {
8891
List<String> scopes = collectRolesAllowedScopes(requirements, schemes);
8992
if (scopes != null && !scopes.isEmpty()) {
9093
op.vendorExtensions.put(VENDOR_X_JAKARTA_ROLES_ALLOWED, scopes);
94+
return;
95+
}
96+
if (qualifiesForPermitAll(rawOp, openAPI, requirements)) {
97+
op.vendorExtensions.put(VENDOR_X_JAKARTA_PERMIT_ALL, Boolean.TRUE);
9198
}
9299
}
93100

@@ -120,6 +127,55 @@ private boolean qualifiesForAnyRoles(List<SecurityRequirement> requirements,
120127
return anyQualifies;
121128
}
122129

130+
/**
131+
* Returns true when the operation should emit {@code @PermitAll} -- the
132+
* "no authentication required" cases that {@code qualifiesForAnyRoles} and
133+
* {@code collectRolesAllowedScopes} deliberately reject.
134+
*
135+
* <p>The decision uses raw op-level and global security fields (not the already
136+
* resolved {@code effectiveRequirements}) so it can distinguish explicit op-level
137+
* opt-out ({@code security: []}) from global inheritance.
138+
*
139+
* <ul>
140+
* <li>Op-level {@code security: []} -> always permit-all (overrides any global).
141+
* <li>No op-level security AND global {@code security: []} -> inherits empty.
142+
* <li>No op-level security AND no global security -> the spec declares the
143+
* entire API unauthenticated.
144+
* <li>Op-level OR list contains {@code - {}} -> least-restrictive wins.
145+
* </ul>
146+
*
147+
* <p>This method returns false for mixed-scope AND groups, undefined schemes, and
148+
* other ambiguous cases -- those bail with a warning at the {@code @RolesAllowed}
149+
* stage and must NOT silently fall through to {@code @PermitAll}.
150+
*/
151+
private boolean qualifiesForPermitAll(Operation rawOp, OpenAPI openAPI, List<SecurityRequirement> effectiveRequirements) {
152+
List<SecurityRequirement> opSecurity = rawOp.getSecurity();
153+
if (opSecurity != null && opSecurity.isEmpty()) {
154+
// Explicit op-level opt-out wins over any global setting.
155+
return true;
156+
}
157+
if (opSecurity == null) {
158+
List<SecurityRequirement> globalSecurity = openAPI.getSecurity();
159+
if (globalSecurity == null) {
160+
// Spec defines no security at all -- every operation is unauthenticated.
161+
return true;
162+
}
163+
if (globalSecurity.isEmpty()) {
164+
// Operation inherits the global empty list -- unauthenticated.
165+
return true;
166+
}
167+
}
168+
if (effectiveRequirements != null) {
169+
for (SecurityRequirement requirement : effectiveRequirements) {
170+
if (requirement.isEmpty()) {
171+
// Anonymous OR alternative -- least restrictive wins.
172+
return true;
173+
}
174+
}
175+
}
176+
return false;
177+
}
178+
123179
/**
124180
* A single {@link SecurityRequirement} is an AND group: all schemes must be
125181
* satisfied simultaneously. If any scheme in the group has explicit scopes

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,7 @@
5656
{{#vendorExtensions.x-jakarta-roles-allowed.0}}
5757
@jakarta.annotation.security.RolesAllowed({{openbrace}}{{#vendorExtensions.x-jakarta-roles-allowed}}"{{.}}"{{^-last}},{{/-last}}{{/vendorExtensions.x-jakarta-roles-allowed}}{{closebrace}})
5858
{{/vendorExtensions.x-jakarta-roles-allowed.0}}
59+
{{#vendorExtensions.x-jakarta-permit-all}}
60+
@jakarta.annotation.security.PermitAll
61+
{{/vendorExtensions.x-jakarta-permit-all}}
5962
{{#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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@
5050
{{#vendorExtensions.x-jakarta-roles-allowed.0}}
5151
@jakarta.annotation.security.RolesAllowed({{openbrace}}{{#vendorExtensions.x-jakarta-roles-allowed}}"{{.}}"{{^-last}},{{/-last}}{{/vendorExtensions.x-jakarta-roles-allowed}}{{closebrace}})
5252
{{/vendorExtensions.x-jakarta-roles-allowed.0}}
53+
{{#vendorExtensions.x-jakarta-permit-all}}
54+
@jakarta.annotation.security.PermitAll
55+
{{/vendorExtensions.x-jakarta-permit-all}}
5356
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}}) {
5457
return {{#supportAsync}}{{#useMutiny}}Uni.createFrom().item({{/useMutiny}}{{^useMutiny}}CompletableFuture.supplyAsync(() -> {{/useMutiny}}{{/supportAsync}}Response.ok().entity("magic!").build(){{#supportAsync}}){{/supportAsync}};
5558
}

0 commit comments

Comments
 (0)