Skip to content

Commit 357218f

Browse files
committed
squash
1 parent c1a7cd5 commit 357218f

29 files changed

Lines changed: 1118 additions & 5 deletions

.github/workflows/samples-jaxrs.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ jobs:
3737
- samples/server/petstore/jaxrs-spec-swagger-annotations
3838
- samples/server/petstore/jaxrs-spec-swagger-v3-annotations-jakarta
3939
- samples/server/petstore/jaxrs-spec-swagger-v3-annotations
40+
- samples/server/petstore/jaxrs-spec/quarkus-security
4041
steps:
4142
- uses: actions/checkout@v5
4243
- uses: actions/setup-java@v5
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
generatorName: jaxrs-spec
2+
outputDir: samples/server/petstore/jaxrs-spec/quarkus-security
3+
library: quarkus
4+
inputSpec: modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-mixed-security.yaml
5+
templateDir: modules/openapi-generator/src/main/resources/JavaJaxRS/spec
6+
additionalProperties:
7+
artifactId: jaxrs-spec-quarkus-security
8+
hideGenerationTimestamp: "true"
9+
useJakartaEe: "true"
10+
useJakartaSecurityAnnotations: "true"
11+
interfaceOnly: "true"

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

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@
2121
import io.swagger.v3.oas.models.Operation;
2222
import io.swagger.v3.oas.models.security.SecurityRequirement;
2323
import io.swagger.v3.oas.models.security.SecurityScheme;
24+
import java.util.ArrayList;
2425
import java.util.Collections;
2526
import java.util.List;
2627
import java.util.Map;
28+
import java.util.Set;
29+
import java.util.TreeSet;
2730
import org.openapitools.codegen.CodegenOperation;
2831
import org.slf4j.Logger;
2932
import org.slf4j.LoggerFactory;
@@ -38,10 +41,20 @@
3841
* is still correct for the OR group.
3942
*
4043
* <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.
44+
* emit:
45+
* <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
50+
* {@code @RolesAllowed({"admin","user"})}.
51+
* <li>Unset when the operation does not qualify (anonymous OR alternative,
52+
* mixed-scope AND group, etc.).
53+
* </ul>
54+
*
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.
4558
*/
4659
final class JakartaSecurityAnnotationProcessor {
4760

@@ -70,6 +83,11 @@ void applyTo(CodegenOperation op, Operation rawOp, OpenAPI openAPI) {
7083

7184
if (qualifiesForAnyRoles(requirements, schemes)) {
7285
op.vendorExtensions.put(VENDOR_X_JAKARTA_ROLES_ALLOWED, ANY_AUTHENTICATED_ROLE);
86+
return; // mutually exclusive -- short-circuit before the scoped path runs
87+
}
88+
List<String> scopes = collectRolesAllowedScopes(requirements, schemes);
89+
if (scopes != null && !scopes.isEmpty()) {
90+
op.vendorExtensions.put(VENDOR_X_JAKARTA_ROLES_ALLOWED, scopes);
7391
}
7492
}
7593

@@ -132,7 +150,7 @@ private boolean schemeQualifies(SecurityScheme scheme, List<String> scopes) {
132150
case OAUTH2:
133151
case OPENIDCONNECT:
134152
// 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.
153+
// so @RolesAllowed({"**"}) is correct. Non-empty scopes are handled by collectRolesAllowedScopes.
136154
return scopes == null || scopes.isEmpty();
137155
case HTTP:
138156
case APIKEY:
@@ -146,6 +164,92 @@ private boolean schemeQualifies(SecurityScheme scheme, List<String> scopes) {
146164
}
147165
}
148166

167+
/**
168+
* Returns the deduplicated, alphabetically sorted union of scope names across every OR
169+
* alternative, or {@code null} if the requirement set does not qualify (anonymous OR
170+
* alternative, mixed-scope AND group, undefined scheme, or no requirements at all).
171+
*
172+
* <p>A {@code null} return means the scoped {@code @RolesAllowed} annotation must not
173+
* be emitted for this operation.
174+
*/
175+
private List<String> collectRolesAllowedScopes(List<SecurityRequirement> requirements,
176+
Map<String, SecurityScheme> schemes) {
177+
if (requirements == null || requirements.isEmpty()) {
178+
return null;
179+
}
180+
Set<String> union = new TreeSet<>(); // sorted, deduplicated
181+
for (SecurityRequirement requirement : requirements) {
182+
if (requirement.isEmpty()) {
183+
// Anonymous OR alternative -- defer to @PermitAll (future PR).
184+
return null;
185+
}
186+
List<String> groupScopes = collectAndGroupScopes(requirement, schemes);
187+
if (groupScopes == null) {
188+
// Unscopable AND group -- bail the entire operation.
189+
return null;
190+
}
191+
union.addAll(groupScopes);
192+
}
193+
return new ArrayList<>(union);
194+
}
195+
196+
/**
197+
* Returns the scope list contributed by a single AND group, or {@code null} if the AND
198+
* group cannot be expressed as a single Jakarta {@code @RolesAllowed} annotation.
199+
*
200+
* <p>At most ONE scheme in the AND group may have non-empty scopes (the "scoped scheme").
201+
* If two or more schemes carry competing scope sets, Quarkus annotations cannot express
202+
* the AND-of-different-scope-sets relationship -- we log a warning and return {@code null}.
203+
*
204+
* <p>An empty list (not {@code null}) is returned when the AND group is valid but no
205+
* scheme contributes scopes; the caller treats that as "no scopes from this alternative".
206+
*/
207+
private List<String> collectAndGroupScopes(SecurityRequirement requirement,
208+
Map<String, SecurityScheme> schemes) {
209+
List<String> scopedSchemeScopes = null;
210+
int scopedSchemeCount = 0;
211+
for (Map.Entry<String, List<String>> entry : requirement.entrySet()) {
212+
SecurityScheme scheme = schemes.get(entry.getKey());
213+
if (scheme == null) {
214+
LOGGER.warn("Security requirement references undefined scheme '{}' -- skipping Jakarta scoped @RolesAllowed for this operation.",
215+
entry.getKey());
216+
return null;
217+
}
218+
if (scheme.getType() == null) {
219+
LOGGER.warn("Security scheme '{}' is missing 'type' -- skipping Jakarta scoped @RolesAllowed.",
220+
entry.getKey());
221+
return null;
222+
}
223+
switch (scheme.getType()) {
224+
case OAUTH2:
225+
case OPENIDCONNECT:
226+
List<String> scopes = entry.getValue();
227+
if (scopes != null && !scopes.isEmpty()) {
228+
scopedSchemeCount++;
229+
if (scopedSchemeCount > 1) {
230+
LOGGER.warn(
231+
"AND-group contains multiple scoped schemes (e.g. '{}'); Jakarta @RolesAllowed cannot express AND of different scope sets -- skipping scoped @RolesAllowed for this operation.",
232+
entry.getKey());
233+
return null;
234+
}
235+
scopedSchemeScopes = scopes;
236+
}
237+
// Unscoped OAuth2/OIDC contributes nothing to the scope list.
238+
break;
239+
case HTTP:
240+
case APIKEY:
241+
case MUTUALTLS:
242+
// No scope concept; participates in the AND group but contributes no scopes.
243+
break;
244+
default:
245+
LOGGER.warn("Unrecognised security scheme type '{}' -- skipping Jakarta scoped @RolesAllowed.",
246+
scheme.getType());
247+
return null;
248+
}
249+
}
250+
return scopedSchemeScopes != null ? scopedSchemeScopes : Collections.emptyList();
251+
}
252+
149253
private static Map<String, SecurityScheme> resolveSchemes(OpenAPI openAPI) {
150254
if (openAPI.getComponents() != null && openAPI.getComponents().getSecuritySchemes() != null) {
151255
return openAPI.getComponents().getSecuritySchemes();

0 commit comments

Comments
 (0)