Skip to content

[jaxrs-spec][quarkus] Emit @RolesAllowed({scope}) for OAuth2 and OpenID Connect operations with explicit scopes#23752

Open
Ignacio-Vidal wants to merge 1 commit into
OpenAPITools:masterfrom
Ignacio-Vidal:quarkus-authorisation
Open

[jaxrs-spec][quarkus] Emit @RolesAllowed({scope}) for OAuth2 and OpenID Connect operations with explicit scopes#23752
Ignacio-Vidal wants to merge 1 commit into
OpenAPITools:masterfrom
Ignacio-Vidal:quarkus-authorisation

Conversation

@Ignacio-Vidal
Copy link
Copy Markdown
Contributor

@Ignacio-Vidal Ignacio-Vidal commented May 10, 2026

This is part 3 of #23691 to improve authentication and authorisation support in the jaxrs-spec/quarkus server generator, building on #23680.

What this PR does

Extends the useJakartaSecurityAnnotations flag (introduced in #23680) to also emit @jakarta.annotation.security.RolesAllowed({"scope1","scope2"}) on JAX-RS interface methods and implementation stubs for operations whose OAuth2 or OpenID Connect security requirements carry explicit scopes.

PR #23680 covered the any-authenticated-user case (@RolesAllowed({"**"})). This PR covers the scoped-role case: when every OR alternative has at least one scope, the generator emits a single @RolesAllowed whose value is the deduplicated, alphabetically sorted union of all scopes across those alternatives.

The wildcard and scoped emissions are mutually exclusive per operation: if any OR alternative qualifies as "any authenticated user", the wildcard wins and no scoped annotation is emitted on that operation.

Semantic rules

When @RolesAllowed({scope}) is emitted

Condition Result
All OR alternatives are OAuth2/OIDC with non-empty scopes @RolesAllowed({union_of_all_scopes})
Any OR alternative is unscoped (HTTP, API key, mTLS, or empty-scope OAuth2/OIDC) @RolesAllowed({"**"}) wins — PR #23680 path
Any OR alternative is anonymous (- {}) No annotation — deferred to @PermitAll (next PR)
AND group has more than one scheme with competing scopes No annotation + WARN log

OR scope union

When all OR alternatives are scoped, their scope lists are unioned, deduplicated, and sorted alphabetically:

security:
  - oauth2: [admin]   # OR
  - oauth2: [user]    # OR
# emits: @RolesAllowed({"admin","user"})

AND group — single scoped scheme

A single SecurityRequirement map with multiple keys is an AND group. The generator emits scoped roles only when at most one scheme in the AND group carries scopes. Unscoped co-required schemes (HTTP, API key, mTLS) contribute nothing to the scope list:

security:
  - apiKey: []          # AND — no scopes
    oauth2: [admin]     # AND — scoped
# emits: @RolesAllowed({"admin"})

If two or more schemes in the same AND group carry competing scope sets, Quarkus cannot enforce AND-of-different-scopes with a single annotation, so no annotation is emitted and a WARN is logged.

Global vs per-operation security

Operations with no security field inherit openapi.security. Operations with an explicit security: [] override produce no annotation regardless of the global block. A global security: [] block (top-level empty list) also suppresses emission on inheriting operations.

Why scoped and wildcard are mutually exclusive

Emitting both @RolesAllowed({"**"}) and @RolesAllowed({"admin"}) on the same method would cause Quarkus to apply both interceptors with AND semantics — net effect is the stricter annotation alone, which contradicts the OR intent of the spec. The processor short-circuits in the wildcard branch and never evaluates the scoped path for the same operation.

Implementation notes

Single vendor extension. The processor sets a single x-jakarta-roles-allowed vendor extension whose value is a List<String>, and collapses what was previously two mutually-exclusive flags (x-jakarta-any-roles boolean and x-jakarta-roles-allowed list) into one

  • ["**"] for the wildcard case (any-authenticated-user).
  • The sorted, deduplicated union of scope names for the scoped case.
  • Unset when the operation does not qualify (anonymous OR alternative, mixed-scope AND group, etc.).

Processor changes in JakartaSecurityAnnotationProcessor:

No changes to JavaJAXRSSpecServerCodegen. The existing fromOperation override from #23680 already delegates to the processor.

Template change in jakartaSecurityAnnotations.mustache:

{{#vendorExtensions.x-jakarta-roles-allowed.0}}
@jakarta.annotation.security.RolesAllowed({{openbrace}}{{#vendorExtensions.x-jakarta-roles-allowed}}"{{.}}"{{^-last}},{{/-last}}{{/vendorExtensions.x-jakarta-roles-allowed}}{{closebrace}})
{{/vendorExtensions.x-jakarta-roles-allowed.0}}
  • Single section guarded by .0 (the list's first element being truthy) to emit the annotation exactly once.
  • The inner section iterates the list to produce comma-separated quoted scope names.
  • {{openbrace}} / {{closebrace}} (globally defined in DefaultCodegen) avoid Mustache parser ambiguity when { immediately follows a tag close.
  • The partial has no trailing newline to satisfy the Mustache standalone-partial rule — otherwise the call-site's leading whitespace was being prepended to a phantom output line, doubling the indentation of the line that follows the partial in apiInterface.mustache and apiMethod.mustache.

New CI sample bin/configs/jaxrs-spec-quarkus-security.yaml exercises all four annotation paths (security: [], empty-scope, single-scope, OR-union-scope) from a single spec fixture. The generated sample is committed under samples/server/petstore/jaxrs-spec/quarkus-security/.

Test coverage

A new parameterised test (quarkusEmitsExpectedScopedRolesAllowedCount) drives a 16-row @DataProvider covering:

Scenario Fixture
OAuth2 single scope — two operations quarkus-oauth2-single-scope.yaml
OAuth2 multiple scopes — alphabetical order quarkus-oauth2-multiple-scopes.yaml
OAuth2 OR different scopes — union quarkus-oauth2-or-different-scopes.yaml
OAuth2 existing fixture — now emits scoped (was 0) quarkus-oauth2-with-scopes.yaml
OpenID Connect scoped — existing fixture quarkus-openidconnect-with-scopes.yaml
Global scoped security — both ops inherit quarkus-global-with-scopes.yaml
Global scoped + per-op override + per-op disable quarkus-global-scopes-op-override.yaml
AND group: API key + scoped OAuth2 (single scoped scheme) quarkus-and-with-apikey-and-scoped-oauth2.yaml
AND group: two competing scoped schemes — no emission quarkus-and-multi-scoped-warns.yaml
Unscoped OR scoped — wildcard wins on the unscoped op, scoped on the scoped-only op quarkus-oauth2-or-empty-and-scoped.yaml
Anonymous OR scoped — no emission quarkus-or-empty-anonymous-with-scopes.yaml

The data provider exercises both interfaceOnly=true (apiInterface.mustache) and interfaceOnly=false (apiMethod.mustache) for the most representative fixtures, so regressions in either template path are caught.

Additional targeted tests verify scope content and ordering (quarkusEmitsAlphabeticallySortedDeduplicatedScopes, quarkusOrAlternativesProduceUnionedScopes, quarkusGlobalScopesAreInheritedByOperationsWithoutOverride, quarkusOperationOverrideShadowsGlobalScopedSecurity), MicroProfile coexistence (quarkusScopedJakartaCoexistsWithMicroProfileAnnotations), mutual exclusion (quarkusNeverEmitsBothWildcardAndScopedRolesAllowedOnSameOperation), global empty-security inheritance (quarkusGlobalEmptySecurityListEmitsNothing), and end-to-end validation of the CI sample's four endpoints (quarkusMixedSecuritySampleEmitsAllExpectedAnnotations). All 32 existing PR #23680 test rows continue to pass unchanged.

Closes #23691 (partial — @PermitAll for anonymous/unannotated operations remains for the next PR).

@wing328 @jmini @MelleD

PR checklist

  • Read the contribution guidelines.
  • Pull Request title clearly describes the work in the pull request and Pull Request description provides details about how to validate the work. Missing information here may result in delayed response from the community.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    (For Windows users, please run the script in WSL)
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
    IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed.
  • File the PR against the correct branch: master (upcoming 7.x.0 minor release - breaking changes with fallbacks), 8.0.x (breaking changes without fallbacks)
  • If your PR solves a reported issue, reference it using GitHub's linking syntax (e.g., having "fixes #123" present in the PR description)
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

Summary by cubic

Adds @jakarta.annotation.security.RolesAllowed to Quarkus JAX‑RS stubs for any‑authenticated operations and for OAuth2/OpenID Connect operations with explicit scopes, via a single x-jakarta-roles-allowed vendor extension. Includes a new Quarkus sample and CI wiring.

  • New Features

    • Emits @jakarta.annotation.security.RolesAllowed({"**"}) when any OR alternative is unscoped (HTTP, API key, mTLS, or OAuth2/OIDC with no scopes); anonymous (- {}) emits nothing. Wildcard and scoped roles are mutually exclusive per operation.
    • Emits scoped roles when all OR alternatives have scopes; scopes are unioned, deduped, and sorted. AND groups emit only if at most one scheme has scopes; otherwise skipped with a WARN.
    • Respects global vs per‑operation security, coexists with useMicroProfileOpenAPIAnnotations, and renders on interface and implementation templates via x-jakarta-roles-allowed.
    • Adds Quarkus sample samples/server/petstore/jaxrs-spec/quarkus-security, workflow entry, and targeted tests covering OAuth2/OIDC, OR/AND groups, global/override cases, and mutual exclusion.
  • Migration

    • Replace useQuarkusSecurityAnnotations with useJakartaSecurityAnnotations.
    • Requires useJakartaEe=true and library=quarkus (validated).
    • Example: --additional-properties=useJakartaEe=true,useJakartaSecurityAnnotations=true,library=quarkus.

Written for commit d2d510b. Summary will update on new commits.

@Ignacio-Vidal Ignacio-Vidal changed the title [jaxrs-spec][quarkus] emit roles [jaxrs-spec][quarkus] Emit @RolesAllowed({scope}) for OAuth2 and OpenID Connect operations with explicit scopes May 10, 2026
@Ignacio-Vidal Ignacio-Vidal force-pushed the quarkus-authorisation branch 3 times, most recently from 2def231 to 54089bc Compare May 11, 2026 22:19
@@ -0,0 +1,11 @@
generatorName: jaxrs-spec
outputDir: samples/server/petstore/jaxrs-spec/quarkus-security
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add this to the github workflow so that CI will test it moving forward

Copy link
Copy Markdown
Contributor Author

@Ignacio-Vidal Ignacio-Vidal May 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just merged #23680

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please rebase to resolve merge conflicts when you've time and then i'll merge it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wing328 - done, rebased and tested in a separate project as usual:

  • See the api spec with all cases covered in this MR
  • See the adapter implementing the generated interface
  • See the http.requests using intellij client
  • See the integration tests using the generated client with microprofile library
image

@Ignacio-Vidal Ignacio-Vidal force-pushed the quarkus-authorisation branch from 54089bc to 7fca67e Compare May 12, 2026 06:16
@Ignacio-Vidal Ignacio-Vidal marked this pull request as ready for review May 12, 2026 06:18
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 50 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="samples/server/petstore/jaxrs-spec/quarkus-security/src/main/docker/Dockerfile.native">

<violation number="1" location="samples/server/petstore/jaxrs-spec/quarkus-security/src/main/docker/Dockerfile.native:22">
P2: Container runs as root because no non-root `USER` is set before startup.</violation>
</file>

<file name="docs/generators/jaxrs-cxf-cdi.md">

<violation number="1" location="docs/generators/jaxrs-cxf-cdi.md:85">
P2: Docs overstate `useJakartaSecurityAnnotations` by claiming `@PermitAll` generation even though this PR only implements `@RolesAllowed`.</violation>
</file>

<file name="samples/server/petstore/jaxrs-spec/quarkus-security/src/main/docker/Dockerfile.jvm">

<violation number="1" location="samples/server/petstore/jaxrs-spec/quarkus-security/src/main/docker/Dockerfile.jvm:17">
P1: This sample container is based on Java 8, but the Quarkus 3 sample it runs requires a newer supported JRE, so the image is incompatible and can fail at runtime.</violation>
</file>

<file name="docs/generators/jaxrs-spec.md">

<violation number="1" location="docs/generators/jaxrs-spec.md:86">
P3: The option description overstates current behavior by advertising `@PermitAll` generation even though that path is intentionally deferred and not implemented yet.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread docs/generators/jaxrs-cxf-cdi.md
Comment thread docs/generators/jaxrs-spec.md
@Ignacio-Vidal Ignacio-Vidal force-pushed the quarkus-authorisation branch from 7fca67e to 357218f Compare May 12, 2026 18:17
@Ignacio-Vidal Ignacio-Vidal force-pushed the quarkus-authorisation branch from 357218f to d2d510b Compare May 12, 2026 18:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[jaxrs-spec][quarkus] - Emit Authentication & Authorisation annotations (@Authenticated, @RolesAllowed, @PermitAll) from Security Schemes

2 participants