diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaJAXRSSpecServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaJAXRSSpecServerCodegen.java index dd147b5bc7a5..c840c05f6299 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaJAXRSSpecServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaJAXRSSpecServerCodegen.java @@ -367,6 +367,13 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List postProcessAllModels(Map objs) } return result; } + + protected boolean shouldAddAuthenticatedAnnotation(CodegenOperation op) { + if (!op.hasAuthMethods) { + return false; + } + return op.authMethods.stream().anyMatch(m -> + (Boolean.TRUE.equals(m.isOAuth) && (m.scopes == null || m.scopes.isEmpty())) || + (Boolean.TRUE.equals(m.isOpenId) && (m.scopes == null || m.scopes.isEmpty())) || + Boolean.TRUE.equals(m.isBasicBasic) || + Boolean.TRUE.equals(m.isBasicBearer) || + Boolean.TRUE.equals(m.isApiKey) + ); + } } diff --git a/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/libraries/quarkus/apiInterface.mustache b/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/libraries/quarkus/apiInterface.mustache index b79a56fa34e0..c6ab258c0e4c 100644 --- a/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/libraries/quarkus/apiInterface.mustache +++ b/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/libraries/quarkus/apiInterface.mustache @@ -53,4 +53,7 @@ {{#vendorExtensions.x-java-success-response-code}} @ResponseStatus({{{vendorExtensions.x-java-success-response-code}}}) {{/vendorExtensions.x-java-success-response-code}} + {{#vendorExtensions.x-quarkus-authenticated}} + @io.quarkus.security.Authenticated + {{/vendorExtensions.x-quarkus-authenticated}} {{#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}}); diff --git a/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/libraries/quarkus/apiMethod.mustache b/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/libraries/quarkus/apiMethod.mustache index b97d3447cc32..05b88bd81a74 100644 --- a/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/libraries/quarkus/apiMethod.mustache +++ b/modules/openapi-generator/src/main/resources/JavaJaxRS/spec/libraries/quarkus/apiMethod.mustache @@ -47,6 +47,9 @@ {{^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}} }){{^-last}},{{/-last}}{{/responses}} }){{/hasProduces}}{{/useMicroProfileOpenAPIAnnotations}} + {{#vendorExtensions.x-quarkus-authenticated}} + @io.quarkus.security.Authenticated + {{/vendorExtensions.x-quarkus-authenticated}} 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}}) { return {{#supportAsync}}{{#useMutiny}}Uni.createFrom().item({{/useMutiny}}{{^useMutiny}}CompletableFuture.supplyAsync(() -> {{/useMutiny}}{{/supportAsync}}Response.ok().entity("magic!").build(){{#supportAsync}}){{/supportAsync}}; } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJAXRSSpecServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJAXRSSpecServerCodegenTest.java index 69da95430376..273cd449bcaf 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJAXRSSpecServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/jaxrs/JavaJAXRSSpecServerCodegenTest.java @@ -15,6 +15,7 @@ import org.openapitools.codegen.testutils.ConfigAssert; import org.testng.Assert; import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import java.io.File; @@ -1586,4 +1587,304 @@ public void useQuarkusSecurityAnnotationsNotProcessedForNonQuarkusLibrary() { // written back as a boolean — the key holds the raw Object we put in, not false Assert.assertNotEquals(false, codegen.additionalProperties().get(USE_QUARKUS_SECURITY_ANNOTATIONS)); } + + /** + * Parameterized test covering all @Authenticated annotation scenarios for Quarkus + OAuth2. + * Each row: (spec path, interfaceOnly, useQuarkusSecurityAnnotations, expected count). + */ + @DataProvider(name = "quarkusOAuth2AuthenticatedCases") + public Object[][] quarkusOAuth2AuthenticatedCases() { + return new Object[][] { + // single OAuth2 flow, no scopes — flag on → @Authenticated; flag off → absent + {"src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-no-scopes.yaml", true, true, 1}, + {"src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-no-scopes.yaml", true, false, 0}, + {"src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-no-scopes.yaml", false, true, 1}, + {"src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-no-scopes.yaml", false, false, 0}, + // single OAuth2 flow, non-empty scopes → no @Authenticated regardless of flag + {"src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-with-scopes.yaml", true, true, 0}, + {"src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-with-scopes.yaml", true, false, 0}, + {"src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-with-scopes.yaml", false, true, 0}, + {"src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-with-scopes.yaml", false, false, 0}, + // multiple OAuth2 flows, all no scopes — flag on → exactly once (no per-flow duplication) + {"src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-multi-flow-no-scopes.yaml", true, true, 1}, + {"src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-multi-flow-no-scopes.yaml", true, false, 0}, + {"src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-multi-flow-no-scopes.yaml", false, true, 1}, + {"src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-multi-flow-no-scopes.yaml", false, false, 0}, + // OR: one scheme no-scope + one scheme scoped — flag on → one op gets @Authenticated + {"src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-or-empty-and-scoped.yaml", true, true, 1}, + {"src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-or-empty-and-scoped.yaml", true, false, 0}, + {"src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-or-empty-and-scoped.yaml", false, true, 1}, + {"src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-or-empty-and-scoped.yaml", false, false, 0}, + }; + } + + @Test(dataProvider = "quarkusOAuth2AuthenticatedCases") + public void quarkusEmitsAuthenticatedAnnotationForOAuth2(String specPath, boolean interfaceOnly, boolean useFlag, int expectedCount) throws Exception { + final File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + final OpenAPI openAPI = new OpenAPIParser() + .readLocation(specPath, null, new ParseOptions()).getOpenAPI(); + + codegen.setOutputDir(output.getAbsolutePath()); + codegen.setLibrary(QUARKUS_LIBRARY); + codegen.additionalProperties().put(INTERFACE_ONLY, interfaceOnly); + codegen.additionalProperties().put(USE_JAKARTA_EE, true); + codegen.additionalProperties().put(USE_QUARKUS_SECURITY_ANNOTATIONS, useFlag); + + final ClientOptInput input = new ClientOptInput() + .openAPI(openAPI) + .config(codegen); + + final DefaultGenerator generator = new DefaultGenerator(); + final List files = generator.opts(input).generate(); + + validateJavaSourceFiles(files); + + TestUtils.ensureContainsFile(files, output, "src/gen/java/org/openapitools/api/ItemsApi.java"); + final String content = Files.readString(output.toPath().resolve("src/gen/java/org/openapitools/api/ItemsApi.java")); + Assert.assertEquals(TestUtils.countOccurrences(content, "@io\\.quarkus\\.security\\.Authenticated"), expectedCount); + } + + /** + * Parameterized test covering all @Authenticated annotation scenarios for Quarkus + httpBasic. + * Each row: (interfaceOnly, useQuarkusSecurityAnnotations, expected count). + */ + @DataProvider(name = "quarkusHttpBasicCases") + public Object[][] quarkusHttpBasicCases() { + return new Object[][] { + {true, true, 1}, + {true, false, 0}, + {false, true, 1}, + {false, false, 0}, + }; + } + + @Test(dataProvider = "quarkusHttpBasicCases") + public void quarkusEmitsAuthenticatedAnnotationForHttpBasic(boolean interfaceOnly, boolean useFlag, int expectedCount) throws Exception { + final File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + final OpenAPI openAPI = new OpenAPIParser() + .readLocation("src/test/resources/3_0/jaxrs-spec/quarkus-http-basic.yaml", null, new ParseOptions()).getOpenAPI(); + + codegen.setOutputDir(output.getAbsolutePath()); + codegen.setLibrary(QUARKUS_LIBRARY); + codegen.additionalProperties().put(INTERFACE_ONLY, interfaceOnly); + codegen.additionalProperties().put(USE_JAKARTA_EE, true); + codegen.additionalProperties().put(USE_QUARKUS_SECURITY_ANNOTATIONS, useFlag); + + final DefaultGenerator generator = new DefaultGenerator(); + final List files = generator.opts(new ClientOptInput().openAPI(openAPI).config(codegen)).generate(); + + validateJavaSourceFiles(files); + + TestUtils.ensureContainsFile(files, output, "src/gen/java/org/openapitools/api/ItemsApi.java"); + final String content = Files.readString(output.toPath().resolve("src/gen/java/org/openapitools/api/ItemsApi.java")); + Assert.assertEquals(TestUtils.countOccurrences(content, "@io\\.quarkus\\.security\\.Authenticated"), expectedCount); + } + + /** + * Parameterized test covering all @Authenticated annotation scenarios for Quarkus + http bearer. + * Each row: (interfaceOnly, useQuarkusSecurityAnnotations, expected count). + */ + @DataProvider(name = "quarkusHttpBearerCases") + public Object[][] quarkusHttpBearerCases() { + return new Object[][] { + {true, true, 1}, + {true, false, 0}, + {false, true, 1}, + {false, false, 0}, + }; + } + + @Test(dataProvider = "quarkusHttpBearerCases") + public void quarkusEmitsAuthenticatedAnnotationForHttpBearer(boolean interfaceOnly, boolean useFlag, int expectedCount) throws Exception { + final File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + final OpenAPI openAPI = new OpenAPIParser() + .readLocation("src/test/resources/3_0/jaxrs-spec/quarkus-http-bearer.yaml", null, new ParseOptions()).getOpenAPI(); + + codegen.setOutputDir(output.getAbsolutePath()); + codegen.setLibrary(QUARKUS_LIBRARY); + codegen.additionalProperties().put(INTERFACE_ONLY, interfaceOnly); + codegen.additionalProperties().put(USE_JAKARTA_EE, true); + codegen.additionalProperties().put(USE_QUARKUS_SECURITY_ANNOTATIONS, useFlag); + + final DefaultGenerator generator = new DefaultGenerator(); + final List files = generator.opts(new ClientOptInput().openAPI(openAPI).config(codegen)).generate(); + + validateJavaSourceFiles(files); + + TestUtils.ensureContainsFile(files, output, "src/gen/java/org/openapitools/api/ItemsApi.java"); + final String content = Files.readString(output.toPath().resolve("src/gen/java/org/openapitools/api/ItemsApi.java")); + Assert.assertEquals(TestUtils.countOccurrences(content, "@io\\.quarkus\\.security\\.Authenticated"), expectedCount); + } + + /** + * Parameterized test covering all @Authenticated annotation scenarios for Quarkus + api key. + * Each row: (interfaceOnly, useQuarkusSecurityAnnotations, expected count). + */ + @DataProvider(name = "quarkusApiKeyCases") + public Object[][] quarkusApiKeyCases() { + return new Object[][] { + {true, true, 1}, + {true, false, 0}, + {false, true, 1}, + {false, false, 0}, + }; + } + + @Test(dataProvider = "quarkusApiKeyCases") + public void quarkusEmitsAuthenticatedAnnotationForApiKey(boolean interfaceOnly, boolean useFlag, int expectedCount) throws Exception { + final File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + final OpenAPI openAPI = new OpenAPIParser() + .readLocation("src/test/resources/3_0/jaxrs-spec/quarkus-api-key.yaml", null, new ParseOptions()).getOpenAPI(); + + codegen.setOutputDir(output.getAbsolutePath()); + codegen.setLibrary(QUARKUS_LIBRARY); + codegen.additionalProperties().put(INTERFACE_ONLY, interfaceOnly); + codegen.additionalProperties().put(USE_JAKARTA_EE, true); + codegen.additionalProperties().put(USE_QUARKUS_SECURITY_ANNOTATIONS, useFlag); + + final DefaultGenerator generator = new DefaultGenerator(); + final List files = generator.opts(new ClientOptInput().openAPI(openAPI).config(codegen)).generate(); + + validateJavaSourceFiles(files); + + TestUtils.ensureContainsFile(files, output, "src/gen/java/org/openapitools/api/ItemsApi.java"); + final String content = Files.readString(output.toPath().resolve("src/gen/java/org/openapitools/api/ItemsApi.java")); + Assert.assertEquals(TestUtils.countOccurrences(content, "@io\\.quarkus\\.security\\.Authenticated"), expectedCount); + } + + /** + * Parameterized test for OpenID Connect: empty scopes → @Authenticated; explicit scopes → absent. + * Each row: (spec path, interfaceOnly, useQuarkusSecurityAnnotations, expected count). + */ + @DataProvider(name = "quarkusOpenIdConnectCases") + public Object[][] quarkusOpenIdConnectCases() { + return new Object[][] { + // no scopes — flag on → @Authenticated; flag off → absent + {"src/test/resources/3_0/jaxrs-spec/quarkus-openidconnect-no-scopes.yaml", true, true, 1}, + {"src/test/resources/3_0/jaxrs-spec/quarkus-openidconnect-no-scopes.yaml", true, false, 0}, + {"src/test/resources/3_0/jaxrs-spec/quarkus-openidconnect-no-scopes.yaml", false, true, 1}, + {"src/test/resources/3_0/jaxrs-spec/quarkus-openidconnect-no-scopes.yaml", false, false, 0}, + // explicit scopes → no @Authenticated regardless of flag + {"src/test/resources/3_0/jaxrs-spec/quarkus-openidconnect-with-scopes.yaml", true, true, 0}, + {"src/test/resources/3_0/jaxrs-spec/quarkus-openidconnect-with-scopes.yaml", true, false, 0}, + {"src/test/resources/3_0/jaxrs-spec/quarkus-openidconnect-with-scopes.yaml", false, true, 0}, + {"src/test/resources/3_0/jaxrs-spec/quarkus-openidconnect-with-scopes.yaml", false, false, 0}, + }; + } + + @Test(dataProvider = "quarkusOpenIdConnectCases") + public void quarkusEmitsAuthenticatedAnnotationForOpenIdConnect(String specPath, boolean interfaceOnly, boolean useFlag, int expectedCount) throws Exception { + final File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + final OpenAPI openAPI = new OpenAPIParser() + .readLocation(specPath, null, new ParseOptions()).getOpenAPI(); + + codegen.setOutputDir(output.getAbsolutePath()); + codegen.setLibrary(QUARKUS_LIBRARY); + codegen.additionalProperties().put(INTERFACE_ONLY, interfaceOnly); + codegen.additionalProperties().put(USE_JAKARTA_EE, true); + codegen.additionalProperties().put(USE_QUARKUS_SECURITY_ANNOTATIONS, useFlag); + + final DefaultGenerator generator = new DefaultGenerator(); + final List files = generator.opts(new ClientOptInput().openAPI(openAPI).config(codegen)).generate(); + + validateJavaSourceFiles(files); + + TestUtils.ensureContainsFile(files, output, "src/gen/java/org/openapitools/api/ItemsApi.java"); + final String content = Files.readString(output.toPath().resolve("src/gen/java/org/openapitools/api/ItemsApi.java")); + Assert.assertEquals(TestUtils.countOccurrences(content, "@io\\.quarkus\\.security\\.Authenticated"), expectedCount); + } + + /** + * Parameterized test for global security inheritance and per-operation security: [] override. + * Each row: (spec path, interfaceOnly, useQuarkusSecurityAnnotations, expected count). + */ + @DataProvider(name = "quarkusGlobalSecurityCases") + public Object[][] quarkusGlobalSecurityCases() { + return new Object[][] { + // global HTTP Basic + Bearer; GET inherits (→ @Authenticated), POST has security:[] (→ none) + {"src/test/resources/3_0/jaxrs-spec/quarkus-global-security-one-op-disabled.yaml", true, true, 1}, + {"src/test/resources/3_0/jaxrs-spec/quarkus-global-security-one-op-disabled.yaml", true, false, 0}, + {"src/test/resources/3_0/jaxrs-spec/quarkus-global-security-one-op-disabled.yaml", false, true, 1}, + {"src/test/resources/3_0/jaxrs-spec/quarkus-global-security-one-op-disabled.yaml", false, false, 0}, + // global OR: unscoped OAuth2 + scoped OAuth2; both ops inherit → both get @Authenticated + {"src/test/resources/3_0/jaxrs-spec/quarkus-global-oauth2-or-scoped-and-unscoped.yaml", true, true, 2}, + {"src/test/resources/3_0/jaxrs-spec/quarkus-global-oauth2-or-scoped-and-unscoped.yaml", true, false, 0}, + {"src/test/resources/3_0/jaxrs-spec/quarkus-global-oauth2-or-scoped-and-unscoped.yaml", false, true, 2}, + {"src/test/resources/3_0/jaxrs-spec/quarkus-global-oauth2-or-scoped-and-unscoped.yaml", false, false, 0}, + }; + } + + @Test(dataProvider = "quarkusGlobalSecurityCases") + public void quarkusHandlesGlobalSecurityScenarios(String specPath, boolean interfaceOnly, boolean useFlag, int expectedCount) throws Exception { + final File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + final OpenAPI openAPI = new OpenAPIParser() + .readLocation(specPath, null, new ParseOptions()).getOpenAPI(); + + codegen.setOutputDir(output.getAbsolutePath()); + codegen.setLibrary(QUARKUS_LIBRARY); + codegen.additionalProperties().put(INTERFACE_ONLY, interfaceOnly); + codegen.additionalProperties().put(USE_JAKARTA_EE, true); + codegen.additionalProperties().put(USE_QUARKUS_SECURITY_ANNOTATIONS, useFlag); + + final DefaultGenerator generator = new DefaultGenerator(); + final List files = generator.opts(new ClientOptInput().openAPI(openAPI).config(codegen)).generate(); + + validateJavaSourceFiles(files); + + TestUtils.ensureContainsFile(files, output, "src/gen/java/org/openapitools/api/ItemsApi.java"); + final String content = Files.readString(output.toPath().resolve("src/gen/java/org/openapitools/api/ItemsApi.java")); + Assert.assertEquals(TestUtils.countOccurrences(content, "@io\\.quarkus\\.security\\.Authenticated"), expectedCount); + } + + /** + * Parameterized test for cross-type OR lists where the qualifying scheme is not the first entry. + * Each row: (interfaceOnly, useQuarkusSecurityAnnotations, expected count). + * Spec: GET has scoped OAuth2 OR API Key — API Key qualifies even though OAuth2 alone would not. + * POST has scoped OAuth2 only — no qualifying scheme. + */ + @DataProvider(name = "quarkusMixedTypeOrCases") + public Object[][] quarkusMixedTypeOrCases() { + return new Object[][] { + {true, true, 1}, + {true, false, 0}, + {false, true, 1}, + {false, false, 0}, + }; + } + + @Test(dataProvider = "quarkusMixedTypeOrCases") + public void quarkusEmitsAuthenticatedForMixedTypeOrList(boolean interfaceOnly, boolean useFlag, int expectedCount) throws Exception { + final File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + final OpenAPI openAPI = new OpenAPIParser() + .readLocation("src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-scoped-or-api-key.yaml", null, new ParseOptions()).getOpenAPI(); + + codegen.setOutputDir(output.getAbsolutePath()); + codegen.setLibrary(QUARKUS_LIBRARY); + codegen.additionalProperties().put(INTERFACE_ONLY, interfaceOnly); + codegen.additionalProperties().put(USE_JAKARTA_EE, true); + codegen.additionalProperties().put(USE_QUARKUS_SECURITY_ANNOTATIONS, useFlag); + + final DefaultGenerator generator = new DefaultGenerator(); + final List files = generator.opts(new ClientOptInput().openAPI(openAPI).config(codegen)).generate(); + + validateJavaSourceFiles(files); + + TestUtils.ensureContainsFile(files, output, "src/gen/java/org/openapitools/api/ItemsApi.java"); + final String content = Files.readString(output.toPath().resolve("src/gen/java/org/openapitools/api/ItemsApi.java")); + Assert.assertEquals(TestUtils.countOccurrences(content, "@io\\.quarkus\\.security\\.Authenticated"), expectedCount); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-api-key.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-api-key.yaml new file mode 100644 index 000000000000..1d6bf8aff300 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-api-key.yaml @@ -0,0 +1,28 @@ +openapi: 3.0.1 +info: + title: Quarkus API Key auth test + version: '1.0' +servers: + - url: 'http://localhost:8080/' +paths: + /items: + get: + operationId: getItems + summary: Get items + security: + - api_key: [] + responses: + '200': + description: OK + post: + operationId: createItem + summary: Create item + responses: + '201': + description: Created +components: + securitySchemes: + api_key: + type: apiKey + in: header + name: X-API-Key diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-global-oauth2-or-scoped-and-unscoped.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-global-oauth2-or-scoped-and-unscoped.yaml new file mode 100644 index 000000000000..439036c191ae --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-global-oauth2-or-scoped-and-unscoped.yaml @@ -0,0 +1,39 @@ +openapi: 3.0.1 +info: + title: Quarkus global OAuth2 OR — unscoped and scoped entries + version: '1.0' +servers: + - url: 'http://localhost:8080/' +security: + - oauth2_no_scope: [] + - oauth2_with_scope: + - admin +paths: + /items: + get: + operationId: getItems + summary: Inherits global security — unscoped entry in OR list makes it least-restrictive + responses: + '200': + description: OK + post: + operationId: createItem + summary: Also inherits global security — same OR reasoning applies + responses: + '201': + description: Created +components: + securitySchemes: + oauth2_no_scope: + type: oauth2 + flows: + clientCredentials: + tokenUrl: https://example.com/oauth/token + scopes: {} + oauth2_with_scope: + type: oauth2 + flows: + clientCredentials: + tokenUrl: https://example.com/oauth/token + scopes: + admin: Admin access diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-global-security-one-op-disabled.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-global-security-one-op-disabled.yaml new file mode 100644 index 000000000000..3b9ef8072894 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-global-security-one-op-disabled.yaml @@ -0,0 +1,32 @@ +openapi: 3.0.1 +info: + title: Quarkus global security with one operation explicitly disabled + version: '1.0' +servers: + - url: 'http://localhost:8080/' +security: + - basic_auth: [] + - bearer_auth: [] +paths: + /items: + get: + operationId: getItems + summary: Inherits global security (HTTP Basic OR Bearer) — should get @Authenticated + responses: + '200': + description: OK + post: + operationId: createItem + summary: Explicitly disables security with security:[] — should NOT get @Authenticated + security: [] + responses: + '201': + description: Created +components: + securitySchemes: + basic_auth: + type: http + scheme: basic + bearer_auth: + type: http + scheme: bearer diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-http-basic.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-http-basic.yaml new file mode 100644 index 000000000000..d7fa295d765b --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-http-basic.yaml @@ -0,0 +1,27 @@ +openapi: 3.0.1 +info: + title: Quarkus HTTP Basic auth test + version: '1.0' +servers: + - url: 'http://localhost:8080/' +paths: + /items: + get: + operationId: getItems + summary: Get items + security: + - basic_auth: [] + responses: + '200': + description: OK + post: + operationId: createItem + summary: Create item + responses: + '201': + description: Created +components: + securitySchemes: + basic_auth: + type: http + scheme: basic diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-http-bearer.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-http-bearer.yaml new file mode 100644 index 000000000000..b006c4a3064e --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-http-bearer.yaml @@ -0,0 +1,27 @@ +openapi: 3.0.1 +info: + title: Quarkus HTTP Bearer auth test + version: '1.0' +servers: + - url: 'http://localhost:8080/' +paths: + /items: + get: + operationId: getItems + summary: Get items + security: + - bearer_auth: [] + responses: + '200': + description: OK + post: + operationId: createItem + summary: Create item + responses: + '201': + description: Created +components: + securitySchemes: + bearer_auth: + type: http + scheme: bearer diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-multi-flow-no-scopes.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-multi-flow-no-scopes.yaml new file mode 100644 index 000000000000..85002b3d6134 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-multi-flow-no-scopes.yaml @@ -0,0 +1,34 @@ +openapi: 3.0.1 +info: + title: Quarkus OAuth2 multi-flow no scopes test + version: '1.0' +servers: + - url: 'http://localhost:8080/' +paths: + /items: + get: + operationId: getItems + summary: Get items + security: + - oauth2_scheme: [] + responses: + '200': + description: OK + post: + operationId: createItem + summary: Create item + responses: + '201': + description: Created +components: + securitySchemes: + oauth2_scheme: + type: oauth2 + flows: + authorizationCode: + authorizationUrl: https://example.com/oauth/authorize + tokenUrl: https://example.com/oauth/token + scopes: {} + implicit: + authorizationUrl: https://example.com/api/oauth/dialog + scopes: {} diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-no-scopes.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-no-scopes.yaml new file mode 100644 index 000000000000..5b24d75a8e76 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-no-scopes.yaml @@ -0,0 +1,30 @@ +openapi: 3.0.1 +info: + title: Quarkus OAuth2 no scopes test + version: '1.0' +servers: + - url: 'http://localhost:8080/' +paths: + /items: + get: + operationId: getItems + summary: Get items + security: + - oauth2_scheme: [] + responses: + '200': + description: OK + post: + operationId: createItem + summary: Create item + responses: + '201': + description: Created +components: + securitySchemes: + oauth2_scheme: + type: oauth2 + flows: + clientCredentials: + tokenUrl: https://example.com/oauth/token + scopes: {} diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-or-empty-and-scoped.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-or-empty-and-scoped.yaml new file mode 100644 index 000000000000..64b4c7905082 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-or-empty-and-scoped.yaml @@ -0,0 +1,42 @@ +openapi: 3.0.1 +info: + title: Quarkus OAuth2 OR empty-and-scoped test + version: '1.0' +servers: + - url: 'http://localhost:8080/' +paths: + /items: + get: + operationId: getItems + summary: Get items — OR any-authenticated OR scoped + security: + - oauth2_no_scope: [] + - oauth2_with_scope: + - admin + responses: + '200': + description: OK + post: + operationId: createItem + summary: Create item — scoped only + security: + - oauth2_with_scope: + - admin + responses: + '201': + description: Created +components: + securitySchemes: + oauth2_no_scope: + type: oauth2 + flows: + clientCredentials: + tokenUrl: https://example.com/oauth/token + scopes: {} + oauth2_with_scope: + type: oauth2 + flows: + clientCredentials: + tokenUrl: https://example.com/oauth/token + scopes: + admin: Admin access diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-scoped-or-api-key.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-scoped-or-api-key.yaml new file mode 100644 index 000000000000..8b7ab02dc842 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-scoped-or-api-key.yaml @@ -0,0 +1,40 @@ +openapi: 3.0.1 +info: + title: Quarkus OAuth2 scoped OR API Key — API Key qualifies for @Authenticated + version: '1.0' +servers: + - url: 'http://localhost:8080/' +paths: + /items: + get: + operationId: getItems + summary: Scoped OAuth2 alone would not qualify, but API Key in the OR list does + security: + - oauth2_with_scope: + - admin + - api_key: [] + responses: + '200': + description: OK + post: + operationId: createItem + summary: Scoped OAuth2 only — no qualifying scheme + security: + - oauth2_with_scope: + - admin + responses: + '201': + description: Created +components: + securitySchemes: + oauth2_with_scope: + type: oauth2 + flows: + clientCredentials: + tokenUrl: https://example.com/oauth/token + scopes: + admin: Admin access + api_key: + type: apiKey + in: header + name: X-API-Key diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-with-scopes.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-with-scopes.yaml new file mode 100644 index 000000000000..0d9becab40cb --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-oauth2-with-scopes.yaml @@ -0,0 +1,36 @@ +openapi: 3.0.1 +info: + title: Quarkus OAuth2 with scopes test + version: '1.0' +servers: + - url: 'http://localhost:8080/' +paths: + /items: + get: + operationId: getItems + summary: Get items + security: + - oauth2_scheme: + - read:items + responses: + '200': + description: OK + post: + operationId: createItem + summary: Create item + security: + - oauth2_scheme: + - write:items + responses: + '201': + description: Created +components: + securitySchemes: + oauth2_scheme: + type: oauth2 + flows: + clientCredentials: + tokenUrl: https://example.com/oauth/token + scopes: + read:items: Read items + write:items: Write items diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-openidconnect-no-scopes.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-openidconnect-no-scopes.yaml new file mode 100644 index 000000000000..d12a96679b68 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-openidconnect-no-scopes.yaml @@ -0,0 +1,27 @@ +openapi: 3.0.1 +info: + title: Quarkus OpenID Connect no scopes test + version: '1.0' +servers: + - url: 'http://localhost:8080/' +paths: + /items: + get: + operationId: getItems + summary: Get items + security: + - oidc_scheme: [] + responses: + '200': + description: OK + post: + operationId: createItem + summary: Create item + responses: + '201': + description: Created +components: + securitySchemes: + oidc_scheme: + type: openIdConnect + openIdConnectUrl: https://example.com/.well-known/openid-configuration diff --git a/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-openidconnect-with-scopes.yaml b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-openidconnect-with-scopes.yaml new file mode 100644 index 000000000000..2e708cf412a1 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/quarkus-openidconnect-with-scopes.yaml @@ -0,0 +1,31 @@ +openapi: 3.0.1 +info: + title: Quarkus OpenID Connect with scopes test + version: '1.0' +servers: + - url: 'http://localhost:8080/' +paths: + /items: + get: + operationId: getItems + summary: Get items + security: + - oidc_scheme: + - read:items + responses: + '200': + description: OK + post: + operationId: createItem + summary: Create item + security: + - oidc_scheme: + - write:items + responses: + '201': + description: Created +components: + securitySchemes: + oidc_scheme: + type: openIdConnect + openIdConnectUrl: https://example.com/.well-known/openid-configuration diff --git a/quarkus-security-github-issue.md b/quarkus-security-github-issue.md new file mode 100644 index 000000000000..5aab4eb3bc6c --- /dev/null +++ b/quarkus-security-github-issue.md @@ -0,0 +1,209 @@ +# [jaxrs-spec][quarkus] Add runtime security annotation support (authentication & authorization) + +## Background + +The `jaxrs-spec` generator with the `quarkus` library already emits **MicroProfile OpenAPI** security scheme annotations (`@SecurityScheme`, `@SecurityRequirement`) on generated API interfaces. These annotations serve a **documentation-only** purpose: they populate the Swagger UI "Authorize" dialog so developers can test secured endpoints interactively. They have no effect at runtime — Quarkus does not enforce access control based on them. + +When a developer generates a Quarkus server stub from an OpenAPI document that defines `securitySchemes` and applies them to operations, the generated code today gives no runtime signal about which endpoints require authentication or what roles are needed. The developer must manually add Quarkus security annotations after generation — a step that is easy to miss and becomes a maintenance burden as the spec evolves. + +## Benefits of adding runtime security annotation support + +- **Security by default**: generated stubs are immediately deployable with correct access control; developers do not have to remember to add annotations manually +- **Spec as the source of truth**: when the OpenAPI document changes (e.g., a scope is added or removed), regenerating the client automatically updates the security annotations +- **Quarkus-native developer experience**: generated code uses idiomatic Quarkus/Jakarta EE security annotations that developers already know, rather than requiring them to understand how to translate OpenAPI security schemes into Quarkus constructs +- **Reduced security misconfigurations**: missing or incorrect hand-written security annotations are a common source of accidental endpoint exposure; generator-driven annotations eliminate this class of mistake + +## How Quarkus supports authentication and authorization + +Quarkus implements security through the standard Jakarta EE security annotations defined in `jakarta.annotation.security` and through its own extensions in `io.quarkus.security`. These annotations are enforced by Quarkus interceptors at request time. + +### Security annotations for JAX-RS endpoints + +| Annotation | Package | Description | +|---|---|---| +| `@Authenticated` | `io.quarkus.security` | Permits any authenticated user. Equivalent to `@RolesAllowed("**")`. | +| `@RolesAllowed({"role1", "role2"})` | `jakarta.annotation.security` | Permits users that have **at least one** of the listed roles (OR semantics within the annotation). | +| `@PermitAll` | `jakarta.annotation.security` | Permits all users, including unauthenticated ones. Used to mark explicitly public endpoints. | +| `@DenyAll` | `jakarta.annotation.security` | Denies all access regardless of identity. Used to mark endpoints that must never be called directly via HTTP. | +| `@PermissionsAllowed` | `io.quarkus.security` | Fine-grained permission check against `SecurityIdentity` permissions. Beyond the scope of generator support. | + +### Annotation placement + +Annotations can be placed on JAX-RS interface methods (recommended for generated code) or on implementation classes. Method-level annotations take precedence over class-level annotations. Quarkus supports annotations on the interface when the implementation class is not directly annotated. + +### Interaction with configuration-based policies + +`quarkus.http.auth.*` configuration-based policies are evaluated **before** annotation-based checks. This means `@PermitAll` does not bypass HTTP-level security configurations; it only relaxes restrictions imposed by other annotations. The generator focuses on annotation-based security, which is the standard approach for JAX-RS endpoint security in Quarkus. + +### Relevant Quarkus configuration properties + +| Property | Effect | +|---|---| +| `quarkus.security.jaxrs.deny-unannotated-endpoints=true` | Unannotated endpoints default to `@DenyAll`. Useful in combination with generated `@PermitAll` on explicitly public endpoints. | +| `quarkus.security.jaxrs.default-roles-allowed=**` | Sets a default role requirement for unannotated endpoints. Cannot be combined with `deny-unannotated-endpoints`. | + +## Mapping OpenAPI security schemes to Quarkus annotations + +OpenAPI 3.x supports the following security scheme types: `http` (Basic / Bearer), `apiKey`, `oauth2`, and `openIdConnect`. Operations declare their security requirements via the `security` array, where each entry in the top-level array represents an alternative (OR semantics), and multiple schemes within one entry represent a conjunction (AND semantics, rarely used in practice). + +### Mapping table + +| OpenAPI security requirement | Quarkus annotation | Rationale | +|---|---|---| +| Operation has no `security` requirement | `@PermitAll` | Explicitly marks the endpoint as public so auditors and tooling can identify intentionally open endpoints. | +| `http` scheme, `basic` | `@Authenticated` | Basic authentication validates identity; there are no roles or scopes to check. Any valid credential grants access. | +| `http` scheme, `bearer` | `@Authenticated` | A Bearer token (e.g., JWT) validates identity. Role/scope enforcement is handled by the OIDC/JWT extension separately; the annotation marks the intent. | +| `apiKey` | `@Authenticated` | An API key validates identity only. No role check is applicable. | +| `oauth2` with empty scopes (`[]`) | `@Authenticated` | An empty scope list means "any authenticated user" — no specific authorization is required beyond a valid token. | +| `openIdConnect` with empty scopes (`[]`) | `@Authenticated` | Same reasoning as OAuth2 with empty scopes. | +| `oauth2` with explicit scopes | `@RolesAllowed({"scope1", "scope2"})` | In Quarkus, OAuth2/OIDC token scopes are mapped to `SecurityIdentity` roles. `@RolesAllowed` receives the scopes as role names. | +| `openIdConnect` with explicit scopes | `@RolesAllowed({"scope1"})` | Same as OAuth2 with scopes. | +| OR list with at least one empty-scope scheme | `@Authenticated` | The least restrictive alternative dominates: if any scheme allows any authenticated user, the effective gate is authentication only. | +| OR list where all alternatives have scopes | `@RolesAllowed(union_of_all_scopes)` | The union of scopes across all OR-alternatives is passed to `@RolesAllowed`. Since `@RolesAllowed` itself uses OR semantics (user needs **any** listed role), this correctly allows a user to satisfy any one of the OR-alternatives. | + +### Limitation: OpenAPI OR semantics vs. Quarkus AND annotations + +OpenAPI's `security` array is **OR**: a request is authorized if *any one* of the listed security alternatives is satisfied. Quarkus standard annotations use **AND** semantics when stacked: `@Authenticated` + `@RolesAllowed("admin")` requires the user to be both authenticated *and* have the `admin` role. + +**This means the two models cannot always be reconciled perfectly.** The generator's strategy is to emit the **least restrictive annotation that is still correct** for the OR group: + +- If any alternative has empty scopes → `@Authenticated` (any authenticated user passes; specific scopes in other alternatives are not enforced at the annotation level) +- If all alternatives have explicit scopes → `@RolesAllowed(union_of_all_scopes)` (user needs any one of the scopes, matching any one alternative) + +Never emit both `@Authenticated` and `@RolesAllowed` on the same method: Quarkus would apply both interceptors, creating AND semantics stricter than the spec intends. + +> **Note for users:** For the case where an OR list mixes empty-scope and scoped schemes, the generator emits `@Authenticated` rather than `@RolesAllowed`. Scope-level enforcement for the scoped alternatives is left to the identity provider (e.g., the OAuth2 authorization server or OIDC provider validates token scopes before issuing the token). This is not a limitation of the generator but an inherent property of how OpenAPI security OR semantics interact with annotation-based RBAC. + +## Proposed implementation plan + +The work is broken into three focused pull requests, each independently reviewable and mergeable. + +### PR 1 — `@Authenticated` for identity-only security schemes + +> **Status: in progress** on branch `quarkus-authentication`. + +**Scope:** Emit `@io.quarkus.security.Authenticated` on JAX-RS interface methods and implementation stubs when an operation's security requirement is satisfied by any authenticated user, specifically when: + +- The operation uses `http: basic` authentication +- The operation uses `apiKey` authentication +- The operation uses `http: bearer` authentication +- The operation uses `oauth2` or `openIdConnect` with **empty scopes** (`[]`) +- The operation uses an OR list where at least one alternative has empty scopes + +**Implementation approach:** +- `JavaJAXRSSpecServerCodegen.postProcessOperationsWithModels` iterates operations and calls `shouldAddAuthenticatedAnnotation(op)` +- If the method returns `true`, sets `op.vendorExtensions["x-quarkus-authenticated"] = true` +- Templates `apiInterface.mustache` and `apiMethod.mustache` emit `@io.quarkus.security.Authenticated` when `{{#vendorExtensions.x-quarkus-authenticated}}` is true +- The vendor extension ensures the annotation is emitted exactly once, regardless of how many OAuth2 flows a scheme defines + +**Files:** +- `modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaJAXRSSpecServerCodegen.java` +- `modules/openapi-generator/src/main/resources/JavaJaxRS/spec/libraries/quarkus/apiInterface.mustache` +- `modules/openapi-generator/src/main/resources/JavaJaxRS/spec/libraries/quarkus/apiMethod.mustache` + +**Test coverage:** 4 YAML fixtures × 2 templates (interface + impl) = 8 test cases. + +--- + +### PR 2 — `@RolesAllowed` for operations where all OR-alternatives have explicit scopes + +> **Status: not yet started.** Depends on PR 1 being merged. + +**Scope:** When an operation's security requirement has **all** OR-alternatives specifying non-empty scopes, emit `@jakarta.annotation.security.RolesAllowed` with the union of all scopes across all alternatives. + +**Example:** + +```yaml +# OpenAPI spec +security: + - oauth2_read: [read:items] + - oauth2_admin: [admin] +``` + +Generated: + +```java +@jakarta.annotation.security.RolesAllowed({"read:items", "admin"}) +@GET +@Path("/items") +Response listItems(); +``` + +**Implementation approach:** +- Add `shouldAddRolesAllowedAnnotation(CodegenOperation op)` in `JavaJAXRSSpecServerCodegen`: + - Returns `true` when `op.hasAuthMethods && !x-quarkus-authenticated && all authMethods have non-empty scopes` +- Add `collectRolesAllowedScopes(CodegenOperation op)` to collect the deduplicated union of all scope names +- Set `op.vendorExtensions["x-quarkus-roles-allowed"]` to the comma-separated scope list (or a list structure for the template) +- Templates emit `@jakarta.annotation.security.RolesAllowed({...})` when `{{#vendorExtensions.x-quarkus-roles-allowed}}` is set +- The `x-quarkus-authenticated` gate from PR 1 ensures `@RolesAllowed` is never emitted on the same method as `@Authenticated` + +**Files:** +- `modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaJAXRSSpecServerCodegen.java` +- `modules/openapi-generator/src/main/resources/JavaJaxRS/spec/libraries/quarkus/apiInterface.mustache` +- `modules/openapi-generator/src/main/resources/JavaJaxRS/spec/libraries/quarkus/apiMethod.mustache` + +**Test cases to add:** +- Single scheme with one scope → `@RolesAllowed({"read:items"})` +- Single scheme with multiple scopes → `@RolesAllowed({"read:items", "write:items"})` +- OR list, two schemes each with scopes → `@RolesAllowed(union of both scope sets)` +- OR list, one scheme empty + one scoped → `@Authenticated` (not `@RolesAllowed`; verifies the gate works) + +--- + +### PR 3 — `@PermitAll` for operations with no security requirement + +> **Status: not yet started.** Can be developed independently of PR 2, but should be submitted after PR 1. + +**Scope:** When an operation has no `security` requirement (i.e., `op.hasAuthMethods == false` and no global security override), emit `@jakarta.annotation.security.PermitAll` to explicitly document that the endpoint is public. + +**Motivation:** Without `@PermitAll`, endpoints that are intentionally public are indistinguishable from endpoints whose security was simply forgotten. When the Quarkus application is configured with `quarkus.security.jaxrs.deny-unannotated-endpoints=true`, generated `@PermitAll` annotations correctly protect against accidental exposure while still generating working code for genuinely public endpoints. + +**Example:** + +```yaml +# No security applied to this operation +/health: + get: + operationId: healthCheck + responses: + '200': + description: OK +``` + +Generated: + +```java +@jakarta.annotation.security.PermitAll +@GET +@Path("/health") +Response healthCheck(); +``` + +**Implementation approach:** +- In `postProcessOperationsWithModels`, after the `@Authenticated` and `@RolesAllowed` checks: if `!op.hasAuthMethods`, set `op.vendorExtensions["x-quarkus-permit-all"] = true` +- Templates emit `@jakarta.annotation.security.PermitAll` when the vendor extension is set +- Consider a generator option `quarkusEmitPermitAll` (default `true`) to allow opt-out for users who manage open endpoints via `application.properties` configuration policies + +**Files:** +- `modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaJAXRSSpecServerCodegen.java` +- `modules/openapi-generator/src/main/resources/JavaJaxRS/spec/libraries/quarkus/apiInterface.mustache` +- `modules/openapi-generator/src/main/resources/JavaJaxRS/spec/libraries/quarkus/apiMethod.mustache` + +**Test cases to add:** +- Operation with no security → `@PermitAll` present +- Operation with security → `@PermitAll` absent +- Mix of secured and unsecured operations in one API → each gets the correct annotation + +--- + +## Open questions and future work + +- **`@PermissionsAllowed`**: Quarkus's `io.quarkus.security.PermissionsAllowed` supports fine-grained permission checks beyond role names. Mapping OpenAPI scopes to permissions (as opposed to roles) requires knowing how the application configures its identity provider. This is out of scope for the generator and left to the developer. +- **`quarkus.security.jaxrs.deny-unannotated-endpoints`**: The generator could emit a reminder comment or a documentation note suggesting users enable this property alongside the generated annotations for defense-in-depth. +- **Multi-tenant OIDC**: Quarkus supports multi-tenant OIDC (`quarkus-oidc`). The mapping from an OpenAPI `openIdConnect` scheme to a specific tenant is not expressible in annotations alone; this is a known limitation. +- **Global `security` overrides**: OpenAPI allows a global `security` block at the document level, with per-operation overrides. The generator should ensure it resolves effective security for each operation (global default minus per-operation override) before applying annotation logic. This needs verification in the existing implementation. +- **`@DenyAll`**: No mapping is proposed from OpenAPI constructs to `@DenyAll`. If needed, it could be expressed via a vendor extension (`x-quarkus-deny-all: true`) for operations that should be unreachable via HTTP. + +## Labels + +`enhancement`, `jaxrs-spec`, `quarkus`, `security`