From 02bce44cdb6d5dd9b092f7b66b8bf2ae873a86cf Mon Sep 17 00:00:00 2001 From: Alice Twist Date: Wed, 12 Aug 2026 19:36:44 +0100 Subject: [PATCH 1/2] [rust-server] Generate the auth scheme precedence tests The auth scheme precedence tests added in #24607 were hand-written and duplicated across two samples. Because they are not emitted by the templates, they only cover the specs someone remembered to write them for, and they have to be maintained by hand as new samples are added. Emit them from a new `tests-auth-scheme-precedence.mustache` supporting file instead. `RustServerCodegen` publishes a small description of the spec's security schemes (which schemes are declared, the apiKey parameter names, and whether Basic/Bearer are dispatched before the apiKey block); the template selects the test cases and expected outcomes from those flags. This drops the hand-written files and covers four samples instead of two. Reintroducing the untyped `from_headers` bug that #24607 fixed now fails in all four, where the hand-written tests caught it in two. --- .../codegen/languages/RustServerCodegen.java | 65 ++++++++++ .../tests-auth-scheme-precedence.mustache | 122 ++++++++++++++++++ .../openapi-v3/.openapi-generator/FILES | 1 + .../tests/auth_scheme_precedence.rs | 81 ++++++++++++ .../.openapi-generator/FILES | 1 + .../tests/auth_scheme_precedence.rs | 93 +++++-------- .../.openapi-generator/FILES | 1 + .../tests/auth_scheme_precedence.rs | 81 ++++++------ .../ping-bearer-auth/.openapi-generator/FILES | 1 + .../tests/auth_scheme_precedence.rs | 81 ++++++++++++ 10 files changed, 424 insertions(+), 103 deletions(-) create mode 100644 modules/openapi-generator/src/main/resources/rust-server/tests-auth-scheme-precedence.mustache create mode 100644 samples/server/petstore/rust-server/output/openapi-v3/tests/auth_scheme_precedence.rs create mode 100644 samples/server/petstore/rust-server/output/ping-bearer-auth/tests/auth_scheme_precedence.rs diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustServerCodegen.java index 161400d9b83b..d7d1d23e1c05 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustServerCodegen.java @@ -1383,9 +1383,74 @@ public Map postProcessSupportingFileData(Map bun } bundle.put("hasAuthScopes", hasAuthScopes); + addAuthSchemeTestsToBundle(authMethods, bundle); + return super.postProcessSupportingFileData(bundle); } + /** + * Derive the facts the generated auth-scheme precedence tests need. + * + * Each generated block in `context.rs` returns early once it matches, so a block that fails to + * check which `AuthData` variant it received will claim credentials belonging to another scheme + * and make every later block unreachable (see issue #24095). + * + * Whether that is observable depends on block order, so the only thing the template cannot work + * out for itself is whether a block handling a given HTTP scheme precedes the first apiKey + * block. Everything else - which requests to send, which credentials to use - lives in the + * template. + */ + private void addAuthSchemeTestsToBundle(List authMethods, Map bundle) { + boolean hasBasic = false; + boolean hasBearer = false; + boolean basicPrecedesApiKey = false; + boolean bearerPrecedesApiKey = false; + String apiKeyHeaderName = null; + String apiKeyQueryName = null; + + if (authMethods != null) { + for (CodegenSecurity authMethod : authMethods) { + boolean isBasic = Boolean.TRUE.equals(authMethod.isBasicBasic); + boolean isBearer = Boolean.TRUE.equals(authMethod.isBasicBearer) + || Boolean.TRUE.equals(authMethod.isOAuth); + boolean isApiKeyHeader = Boolean.TRUE.equals(authMethod.isApiKey) + && Boolean.TRUE.equals(authMethod.isKeyInHeader); + boolean isApiKeyQuery = Boolean.TRUE.equals(authMethod.isApiKey) + && Boolean.TRUE.equals(authMethod.isKeyInQuery); + + hasBasic |= isBasic; + hasBearer |= isBearer; + // Only blocks generated before the first apiKey block can shadow it. + if (apiKeyHeaderName == null && apiKeyQueryName == null) { + basicPrecedesApiKey |= isBasic; + bearerPrecedesApiKey |= isBearer; + } + if (isApiKeyHeader && apiKeyHeaderName == null) { + apiKeyHeaderName = authMethod.keyParamName.toLowerCase(Locale.ROOT); + } + if (isApiKeyQuery && apiKeyQueryName == null) { + apiKeyQueryName = authMethod.keyParamName; + } + } + } + + bundle.put("authTestHasBasic", hasBasic); + bundle.put("authTestHasBearer", hasBearer); + bundle.put("authTestBasicPrecedesApiKey", basicPrecedesApiKey); + bundle.put("authTestBearerPrecedesApiKey", bearerPrecedesApiKey); + bundle.put("authTestApiKeyHeader", apiKeyHeaderName); + bundle.put("authTestApiKeyQuery", apiKeyQueryName); + bundle.put("authTestHasApiKey", apiKeyHeaderName != null || apiKeyQueryName != null); + + SupportingFile authTestFile = + new SupportingFile("tests-auth-scheme-precedence.mustache", "tests", "auth_scheme_precedence.rs"); + if (hasBasic || hasBearer || apiKeyHeaderName != null || apiKeyQueryName != null) { + supportingFiles.add(authTestFile); + } else { + supportingFiles.remove(authTestFile); + } + } + /** * Add a built path set map to the provided bundle * diff --git a/modules/openapi-generator/src/main/resources/rust-server/tests-auth-scheme-precedence.mustache b/modules/openapi-generator/src/main/resources/rust-server/tests-auth-scheme-precedence.mustache new file mode 100644 index 000000000000..a57e96ed6f00 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/rust-server/tests-auth-scheme-precedence.mustache @@ -0,0 +1,122 @@ +//! Runtime checks for auth-scheme precedence in the generated `AddContext` middleware. +//! +//! This file is generated. `swagger::auth::from_headers` returns an *untyped* `AuthData`, +//! matching an `Authorization` header that carries either `Basic` or `Bearer` credentials. +//! Every generated auth block returns early once it matches, so a block that does not check +//! which variant it received will claim credentials belonging to a different scheme and +//! prevent every later block - including API-key blocks - from ever running. +//! +//! The expectations below are derived from the security schemes this API declares, in the +//! order their blocks are generated. + +#![cfg(feature = "server")] + +use std::sync::{Arc, Mutex}; + +use hyper::service::Service; +use hyper::{Request, Response}; +use swagger::auth::AuthData; +use swagger::{EmptyContext, Has}; +use {{{externCrateName}}}::context::AddContext; + +/// Innermost service: records the `Option` that `AddContext` pushed onto the context. +#[derive(Clone, Default)] +struct CaptureAuthData(Arc>>); + +impl Service<(Request, C)> for CaptureAuthData +where + C: Has>, +{ + type Response = Response; + type Error = std::convert::Infallible; + type Future = std::future::Ready>; + + fn call(&self, (_request, context): (Request, C)) -> Self::Future { + let auth_data: &Option = context.get(); + *self.0.lock().expect("lock poisoned") = auth_data.clone(); + std::future::ready(Ok(Response::new(String::new()))) + } +} + +/// Drives a request through `AddContext` and returns the `AuthData` it resolved. +fn resolve_auth_data(uri: &str, headers: &[(&str, &str)]) -> Option { + let capture = CaptureAuthData::default(); + let service = AddContext::<_, EmptyContext>::new(capture.clone()); + + let mut builder = Request::get(uri); + for (name, value) in headers { + builder = builder.header(*name, *value); + } + let request = builder.body(()).expect("request should build"); + + futures::executor::block_on(service.call(request)).expect("service call should succeed"); + + let resolved = capture.0.lock().expect("lock poisoned").clone(); + resolved +} + +/// `dXNlcjpwYXNzd29yZA==` is `user:password`. +const BASIC_HEADER: &str = "Basic dXNlcjpwYXNzd29yZA=="; +const BEARER_HEADER: &str = "Bearer some-token"; +{{#authTestHasApiKey}} +const API_KEY: &str = "test-api-key"; +{{/authTestHasApiKey}} + +#[test] +fn no_credentials_resolve_to_no_auth_data() { + assert_eq!(resolve_auth_data("/", &[]), None); +} + +#[test] +fn basic_credentials_resolve_to_the_declared_scheme() { + assert_eq!( + resolve_auth_data("/", &[("authorization", BASIC_HEADER)]), + {{#authTestHasBasic}}Some(AuthData::Basic("user".to_owned(), "password".to_owned())){{/authTestHasBasic}}{{^authTestHasBasic}}None{{/authTestHasBasic}}, + ); +} + +#[test] +fn bearer_credentials_resolve_to_the_declared_scheme() { + assert_eq!( + resolve_auth_data("/", &[("authorization", BEARER_HEADER)]), + {{#authTestHasBearer}}Some(AuthData::Bearer("some-token".to_owned())){{/authTestHasBearer}}{{^authTestHasBearer}}None{{/authTestHasBearer}}, + ); +} +{{#authTestApiKeyHeader}} + +#[test] +fn header_api_key_resolves_when_it_is_the_only_credential() { + assert_eq!( + resolve_auth_data("/", &[("{{{.}}}", API_KEY)]), + Some(AuthData::ApiKey(API_KEY.to_owned())), + ); +} + +/// An `Authorization` header must not shadow the apiKey block unless a block that actually +/// handles that scheme is generated before it. +#[test] +fn header_api_key_is_reachable_alongside_bearer_credentials() { + assert_eq!( + resolve_auth_data("/", &[("authorization", BEARER_HEADER), ("{{{.}}}", API_KEY)]), + {{#authTestBearerPrecedesApiKey}}Some(AuthData::Bearer("some-token".to_owned())){{/authTestBearerPrecedesApiKey}}{{^authTestBearerPrecedesApiKey}}Some(AuthData::ApiKey(API_KEY.to_owned())){{/authTestBearerPrecedesApiKey}}, + ); +} + +#[test] +fn header_api_key_is_reachable_alongside_basic_credentials() { + assert_eq!( + resolve_auth_data("/", &[("authorization", BASIC_HEADER), ("{{{.}}}", API_KEY)]), + {{#authTestBasicPrecedesApiKey}}Some(AuthData::Basic("user".to_owned(), "password".to_owned())){{/authTestBasicPrecedesApiKey}}{{^authTestBasicPrecedesApiKey}}Some(AuthData::ApiKey(API_KEY.to_owned())){{/authTestBasicPrecedesApiKey}}, + ); +} +{{/authTestApiKeyHeader}} +{{#authTestApiKeyQuery}} + +#[test] +fn query_api_key_resolves_when_it_is_the_only_credential() { + assert_eq!( + resolve_auth_data("/?{{{.}}}=test-api-key", &[]), + Some(AuthData::ApiKey(API_KEY.to_owned())), + ); +} +{{/authTestApiKeyQuery}} diff --git a/samples/server/petstore/rust-server/output/openapi-v3/.openapi-generator/FILES b/samples/server/petstore/rust-server/output/openapi-v3/.openapi-generator/FILES index bbae578c96d2..02a74031e2c2 100644 --- a/samples/server/petstore/rust-server/output/openapi-v3/.openapi-generator/FILES +++ b/samples/server/petstore/rust-server/output/openapi-v3/.openapi-generator/FILES @@ -64,3 +64,4 @@ src/models.rs src/server/callbacks.rs src/server/mod.rs src/server/server_auth.rs +tests/auth_scheme_precedence.rs diff --git a/samples/server/petstore/rust-server/output/openapi-v3/tests/auth_scheme_precedence.rs b/samples/server/petstore/rust-server/output/openapi-v3/tests/auth_scheme_precedence.rs new file mode 100644 index 000000000000..f7c20d9222fa --- /dev/null +++ b/samples/server/petstore/rust-server/output/openapi-v3/tests/auth_scheme_precedence.rs @@ -0,0 +1,81 @@ +//! Runtime checks for auth-scheme precedence in the generated `AddContext` middleware. +//! +//! This file is generated. `swagger::auth::from_headers` returns an *untyped* `AuthData`, +//! matching an `Authorization` header that carries either `Basic` or `Bearer` credentials. +//! Every generated auth block returns early once it matches, so a block that does not check +//! which variant it received will claim credentials belonging to a different scheme and +//! prevent every later block - including API-key blocks - from ever running. +//! +//! The expectations below are derived from the security schemes this API declares, in the +//! order their blocks are generated. + +#![cfg(feature = "server")] + +use std::sync::{Arc, Mutex}; + +use hyper::service::Service; +use hyper::{Request, Response}; +use swagger::auth::AuthData; +use swagger::{EmptyContext, Has}; +use openapi_v3::context::AddContext; + +/// Innermost service: records the `Option` that `AddContext` pushed onto the context. +#[derive(Clone, Default)] +struct CaptureAuthData(Arc>>); + +impl Service<(Request, C)> for CaptureAuthData +where + C: Has>, +{ + type Response = Response; + type Error = std::convert::Infallible; + type Future = std::future::Ready>; + + fn call(&self, (_request, context): (Request, C)) -> Self::Future { + let auth_data: &Option = context.get(); + *self.0.lock().expect("lock poisoned") = auth_data.clone(); + std::future::ready(Ok(Response::new(String::new()))) + } +} + +/// Drives a request through `AddContext` and returns the `AuthData` it resolved. +fn resolve_auth_data(uri: &str, headers: &[(&str, &str)]) -> Option { + let capture = CaptureAuthData::default(); + let service = AddContext::<_, EmptyContext>::new(capture.clone()); + + let mut builder = Request::get(uri); + for (name, value) in headers { + builder = builder.header(*name, *value); + } + let request = builder.body(()).expect("request should build"); + + futures::executor::block_on(service.call(request)).expect("service call should succeed"); + + let resolved = capture.0.lock().expect("lock poisoned").clone(); + resolved +} + +/// `dXNlcjpwYXNzd29yZA==` is `user:password`. +const BASIC_HEADER: &str = "Basic dXNlcjpwYXNzd29yZA=="; +const BEARER_HEADER: &str = "Bearer some-token"; + +#[test] +fn no_credentials_resolve_to_no_auth_data() { + assert_eq!(resolve_auth_data("/", &[]), None); +} + +#[test] +fn basic_credentials_resolve_to_the_declared_scheme() { + assert_eq!( + resolve_auth_data("/", &[("authorization", BASIC_HEADER)]), + None, + ); +} + +#[test] +fn bearer_credentials_resolve_to_the_declared_scheme() { + assert_eq!( + resolve_auth_data("/", &[("authorization", BEARER_HEADER)]), + Some(AuthData::Bearer("some-token".to_owned())), + ); +} diff --git a/samples/server/petstore/rust-server/output/overlapping-auth-schemes/.openapi-generator/FILES b/samples/server/petstore/rust-server/output/overlapping-auth-schemes/.openapi-generator/FILES index 913ced3d98a4..5da8def13d90 100644 --- a/samples/server/petstore/rust-server/output/overlapping-auth-schemes/.openapi-generator/FILES +++ b/samples/server/petstore/rust-server/output/overlapping-auth-schemes/.openapi-generator/FILES @@ -21,3 +21,4 @@ src/lib.rs src/models.rs src/server/mod.rs src/server/server_auth.rs +tests/auth_scheme_precedence.rs diff --git a/samples/server/petstore/rust-server/output/overlapping-auth-schemes/tests/auth_scheme_precedence.rs b/samples/server/petstore/rust-server/output/overlapping-auth-schemes/tests/auth_scheme_precedence.rs index 5640cae3ed27..9b206ef8d655 100644 --- a/samples/server/petstore/rust-server/output/overlapping-auth-schemes/tests/auth_scheme_precedence.rs +++ b/samples/server/petstore/rust-server/output/overlapping-auth-schemes/tests/auth_scheme_precedence.rs @@ -1,26 +1,13 @@ -//! Runtime regression tests for auth-scheme precedence in the generated `AddContext` middleware. +//! Runtime checks for auth-scheme precedence in the generated `AddContext` middleware. //! -//! Companion to the same test in the `petstore-with-fake-endpoints-models-for-testing` -//! sample. That spec generates the OAuth2 block first and the HTTP Basic block last, so it -//! can only prove that an `isOAuth` block leaves Basic credentials alone. This spec covers -//! the opposite direction - an `isBasicBasic` block ahead of an `isBasicBearer` block - -//! which is the `from_headers` pairing issue #24095 was reported against. +//! This file is generated. `swagger::auth::from_headers` returns an *untyped* `AuthData`, +//! matching an `Authorization` header that carries either `Basic` or `Bearer` credentials. +//! Every generated auth block returns early once it matches, so a block that does not check +//! which variant it received will claim credentials belonging to a different scheme and +//! prevent every later block - including API-key blocks - from ever running. //! -//! This spec generates the blocks in the following order: -//! -//! 1. `basicAuth` - HTTP Basic, reads `Authorization` -//! 2. `apiKeyAuth` - API key, reads the `x-api-key` header -//! 3. `bearerAuth` - HTTP Bearer, reads `Authorization` -//! -//! `swagger::auth::from_headers` returns an *untyped* `AuthData` and matches an -//! `Authorization` header carrying either HTTP scheme, and every generated block returns -//! early once it matches. So an unrestricted block 1 claims bearer credentials, and in -//! doing so also makes blocks 2 and 3 unreachable. -//! -//! The apiKey scheme sits deliberately *between* the two HTTP schemes: that is what makes -//! the bug observable from outside. Were blocks 1 and 3 adjacent, both the broken and the -//! fixed generator would resolve bearer credentials to `AuthData::Bearer` - via the wrong -//! block in the broken case - and no request-level assertion could distinguish them. +//! The expectations below are derived from the security schemes this API declares, in the +//! order their blocks are generated. #![cfg(feature = "server")] @@ -28,9 +15,9 @@ use std::sync::{Arc, Mutex}; use hyper::service::Service; use hyper::{Request, Response}; -use overlapping_auth_schemes::context::AddContext; use swagger::auth::AuthData; use swagger::{EmptyContext, Has}; +use overlapping_auth_schemes::context::AddContext; /// Innermost service: records the `Option` that `AddContext` pushed onto the context. #[derive(Clone, Default)] @@ -52,11 +39,11 @@ where } /// Drives a request through `AddContext` and returns the `AuthData` it resolved. -fn resolve_auth_data(headers: &[(&str, &str)]) -> Option { +fn resolve_auth_data(uri: &str, headers: &[(&str, &str)]) -> Option { let capture = CaptureAuthData::default(); let service = AddContext::<_, EmptyContext>::new(capture.clone()); - let mut builder = Request::get("/"); + let mut builder = Request::get(uri); for (name, value) in headers { builder = builder.header(*name, *value); } @@ -71,65 +58,51 @@ fn resolve_auth_data(headers: &[(&str, &str)]) -> Option { /// `dXNlcjpwYXNzd29yZA==` is `user:password`. const BASIC_HEADER: &str = "Basic dXNlcjpwYXNzd29yZA=="; const BEARER_HEADER: &str = "Bearer some-token"; -const BEARER_TOKEN: &str = "some-token"; +const API_KEY: &str = "test-api-key"; #[test] -fn basic_block_does_not_swallow_bearer_credentials() { - // The regression, in the only form that is observable at runtime. With an unrestricted - // Basic block, block 1 claims the bearer credentials and returns early, so the - // `x-api-key` block below it never runs and the request resolves to `AuthData::Bearer`. - // With the fix, block 1 declines, and the API key below it is reached - which is what - // this test asserts. - assert_eq!( - resolve_auth_data(&[ - ("authorization", BEARER_HEADER), - ("x-api-key", "header-key") - ]), - Some(AuthData::ApiKey("header-key".to_owned())), - ); +fn no_credentials_resolve_to_no_auth_data() { + assert_eq!(resolve_auth_data("/", &[]), None); } #[test] -fn bearer_block_is_still_reached_when_it_is_the_only_match() { - // Restricting block 1 must not strand block 3: bearer credentials with no API key - // still have to fall all the way through to the Bearer block. +fn basic_credentials_resolve_to_the_declared_scheme() { assert_eq!( - resolve_auth_data(&[("authorization", BEARER_HEADER)]), - Some(AuthData::Bearer(BEARER_TOKEN.to_owned())), + resolve_auth_data("/", &[("authorization", BASIC_HEADER)]), + Some(AuthData::Basic("user".to_owned(), "password".to_owned())), ); } #[test] -fn basic_credentials_are_claimed_by_the_basic_block() { - // Block 1 legitimately matches here and must still take precedence over the API key. +fn bearer_credentials_resolve_to_the_declared_scheme() { assert_eq!( - resolve_auth_data(&[("authorization", BASIC_HEADER), ("x-api-key", "header-key")]), - Some(AuthData::Basic("user".to_owned(), "password".to_owned())), + resolve_auth_data("/", &[("authorization", BEARER_HEADER)]), + Some(AuthData::Bearer("some-token".to_owned())), ); } #[test] -fn header_api_key_is_reachable_when_an_unhandled_authorization_scheme_is_present() { - // Neither HTTP block handles `Digest`, so both must decline and leave the API key - // block reachable. +fn header_api_key_resolves_when_it_is_the_only_credential() { assert_eq!( - resolve_auth_data(&[ - ("authorization", "Digest username=\"user\""), - ("x-api-key", "header-key"), - ]), - Some(AuthData::ApiKey("header-key".to_owned())), + resolve_auth_data("/", &[("x-api-key", API_KEY)]), + Some(AuthData::ApiKey(API_KEY.to_owned())), ); } +/// An `Authorization` header must not shadow the apiKey block unless a block that actually +/// handles that scheme is generated before it. #[test] -fn header_api_key_resolves_when_no_authorization_header_is_present() { +fn header_api_key_is_reachable_alongside_bearer_credentials() { assert_eq!( - resolve_auth_data(&[("x-api-key", "header-key")]), - Some(AuthData::ApiKey("header-key".to_owned())), + resolve_auth_data("/", &[("authorization", BEARER_HEADER), ("x-api-key", API_KEY)]), + Some(AuthData::ApiKey(API_KEY.to_owned())), ); } #[test] -fn no_credentials_resolve_to_no_auth_data() { - assert_eq!(resolve_auth_data(&[]), None); +fn header_api_key_is_reachable_alongside_basic_credentials() { + assert_eq!( + resolve_auth_data("/", &[("authorization", BASIC_HEADER), ("x-api-key", API_KEY)]), + Some(AuthData::Basic("user".to_owned(), "password".to_owned())), + ); } diff --git a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/.openapi-generator/FILES b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/.openapi-generator/FILES index 5332ab08344d..c6e6d11d260f 100644 --- a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/.openapi-generator/FILES +++ b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/.openapi-generator/FILES @@ -78,3 +78,4 @@ src/lib.rs src/models.rs src/server/mod.rs src/server/server_auth.rs +tests/auth_scheme_precedence.rs diff --git a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/tests/auth_scheme_precedence.rs b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/tests/auth_scheme_precedence.rs index 0886639b6564..f4d853c580a0 100644 --- a/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/tests/auth_scheme_precedence.rs +++ b/samples/server/petstore/rust-server/output/petstore-with-fake-endpoints-models-for-testing/tests/auth_scheme_precedence.rs @@ -1,22 +1,13 @@ -//! Runtime regression tests for auth-scheme precedence in the generated `AddContext` middleware. +//! Runtime checks for auth-scheme precedence in the generated `AddContext` middleware. //! -//! `swagger::auth::from_headers` returns an *untyped* `AuthData`, matching an -//! `Authorization` header that carries either `Basic` or `Bearer` credentials. Every -//! generated auth block returns early once it matches, so a block that does not check +//! This file is generated. `swagger::auth::from_headers` returns an *untyped* `AuthData`, +//! matching an `Authorization` header that carries either `Basic` or `Bearer` credentials. +//! Every generated auth block returns early once it matches, so a block that does not check //! which variant it received will claim credentials belonging to a different scheme and //! prevent every later block - including API-key blocks - from ever running. //! -//! This spec generates the blocks in the following order, which is what makes the -//! behaviour observable from the outside: -//! -//! 1. `petstore_auth` - OAuth2, reads `Authorization: Bearer` -//! 2. `api_key` - API key, reads the `api_key` header -//! 3. `api_key_query` - API key, reads the `api_key_query` query parameter -//! 4. `http_basic_test` - HTTP Basic, reads `Authorization: Basic` -//! -//! Presenting Basic credentials alongside an API key therefore proves whether block 1 -//! stays in its lane: if it wrongly claims the Basic credentials it also swallows -//! blocks 2 and 3. +//! The expectations below are derived from the security schemes this API declares, in the +//! order their blocks are generated. #![cfg(feature = "server")] @@ -24,9 +15,9 @@ use std::sync::{Arc, Mutex}; use hyper::service::Service; use hyper::{Request, Response}; -use petstore_with_fake_endpoints_models_for_testing::context::AddContext; use swagger::auth::AuthData; use swagger::{EmptyContext, Has}; +use petstore_with_fake_endpoints_models_for_testing::context::AddContext; /// Innermost service: records the `Option` that `AddContext` pushed onto the context. #[derive(Clone, Default)] @@ -66,11 +57,16 @@ fn resolve_auth_data(uri: &str, headers: &[(&str, &str)]) -> Option { /// `dXNlcjpwYXNzd29yZA==` is `user:password`. const BASIC_HEADER: &str = "Basic dXNlcjpwYXNzd29yZA=="; +const BEARER_HEADER: &str = "Bearer some-token"; +const API_KEY: &str = "test-api-key"; + +#[test] +fn no_credentials_resolve_to_no_auth_data() { + assert_eq!(resolve_auth_data("/", &[]), None); +} #[test] -fn bearer_block_does_not_claim_basic_credentials() { - // The OAuth2 (Bearer) block is generated first. It must ignore Basic credentials and - // let them fall through to the HTTP Basic block generated last. +fn basic_credentials_resolve_to_the_declared_scheme() { assert_eq!( resolve_auth_data("/", &[("authorization", BASIC_HEADER)]), Some(AuthData::Basic("user".to_owned(), "password".to_owned())), @@ -78,44 +74,43 @@ fn bearer_block_does_not_claim_basic_credentials() { } #[test] -fn bearer_credentials_resolve_to_bearer_auth_data() { - // Note this cannot prove the *Basic* block stays in its lane: the OAuth block above it - // legitimately claims these credentials first, so a broken Basic block would be - // unobservable here. That direction is covered at request level by the - // `overlapping-auth-schemes` sample, whose spec interleaves an apiKey scheme between - // the Basic and Bearer blocks. +fn bearer_credentials_resolve_to_the_declared_scheme() { assert_eq!( - resolve_auth_data("/", &[("authorization", "Bearer some-token")]), + resolve_auth_data("/", &[("authorization", BEARER_HEADER)]), Some(AuthData::Bearer("some-token".to_owned())), ); } #[test] -fn header_api_key_is_reachable_when_basic_credentials_are_also_present() { - // Regression test: an unguarded Bearer block matches the Basic credentials, returns - // early, and the `api_key` header block below it never runs. +fn header_api_key_resolves_when_it_is_the_only_credential() { assert_eq!( - resolve_auth_data( - "/", - &[("authorization", BASIC_HEADER), ("api_key", "header-key")], - ), - Some(AuthData::ApiKey("header-key".to_owned())), + resolve_auth_data("/", &[("api_key", API_KEY)]), + Some(AuthData::ApiKey(API_KEY.to_owned())), ); } +/// An `Authorization` header must not shadow the apiKey block unless a block that actually +/// handles that scheme is generated before it. #[test] -fn query_api_key_is_reachable_when_basic_credentials_are_also_present() { - // Same regression, for the query-parameter API-key block. +fn header_api_key_is_reachable_alongside_bearer_credentials() { assert_eq!( - resolve_auth_data( - "/?api_key_query=query-key", - &[("authorization", BASIC_HEADER)], - ), - Some(AuthData::ApiKey("query-key".to_owned())), + resolve_auth_data("/", &[("authorization", BEARER_HEADER), ("api_key", API_KEY)]), + Some(AuthData::Bearer("some-token".to_owned())), ); } #[test] -fn no_credentials_resolve_to_no_auth_data() { - assert_eq!(resolve_auth_data("/", &[]), None); +fn header_api_key_is_reachable_alongside_basic_credentials() { + assert_eq!( + resolve_auth_data("/", &[("authorization", BASIC_HEADER), ("api_key", API_KEY)]), + Some(AuthData::ApiKey(API_KEY.to_owned())), + ); +} + +#[test] +fn query_api_key_resolves_when_it_is_the_only_credential() { + assert_eq!( + resolve_auth_data("/?api_key_query=test-api-key", &[]), + Some(AuthData::ApiKey(API_KEY.to_owned())), + ); } diff --git a/samples/server/petstore/rust-server/output/ping-bearer-auth/.openapi-generator/FILES b/samples/server/petstore/rust-server/output/ping-bearer-auth/.openapi-generator/FILES index 913ced3d98a4..5da8def13d90 100644 --- a/samples/server/petstore/rust-server/output/ping-bearer-auth/.openapi-generator/FILES +++ b/samples/server/petstore/rust-server/output/ping-bearer-auth/.openapi-generator/FILES @@ -21,3 +21,4 @@ src/lib.rs src/models.rs src/server/mod.rs src/server/server_auth.rs +tests/auth_scheme_precedence.rs diff --git a/samples/server/petstore/rust-server/output/ping-bearer-auth/tests/auth_scheme_precedence.rs b/samples/server/petstore/rust-server/output/ping-bearer-auth/tests/auth_scheme_precedence.rs new file mode 100644 index 000000000000..96da4b632541 --- /dev/null +++ b/samples/server/petstore/rust-server/output/ping-bearer-auth/tests/auth_scheme_precedence.rs @@ -0,0 +1,81 @@ +//! Runtime checks for auth-scheme precedence in the generated `AddContext` middleware. +//! +//! This file is generated. `swagger::auth::from_headers` returns an *untyped* `AuthData`, +//! matching an `Authorization` header that carries either `Basic` or `Bearer` credentials. +//! Every generated auth block returns early once it matches, so a block that does not check +//! which variant it received will claim credentials belonging to a different scheme and +//! prevent every later block - including API-key blocks - from ever running. +//! +//! The expectations below are derived from the security schemes this API declares, in the +//! order their blocks are generated. + +#![cfg(feature = "server")] + +use std::sync::{Arc, Mutex}; + +use hyper::service::Service; +use hyper::{Request, Response}; +use swagger::auth::AuthData; +use swagger::{EmptyContext, Has}; +use ping_bearer_auth::context::AddContext; + +/// Innermost service: records the `Option` that `AddContext` pushed onto the context. +#[derive(Clone, Default)] +struct CaptureAuthData(Arc>>); + +impl Service<(Request, C)> for CaptureAuthData +where + C: Has>, +{ + type Response = Response; + type Error = std::convert::Infallible; + type Future = std::future::Ready>; + + fn call(&self, (_request, context): (Request, C)) -> Self::Future { + let auth_data: &Option = context.get(); + *self.0.lock().expect("lock poisoned") = auth_data.clone(); + std::future::ready(Ok(Response::new(String::new()))) + } +} + +/// Drives a request through `AddContext` and returns the `AuthData` it resolved. +fn resolve_auth_data(uri: &str, headers: &[(&str, &str)]) -> Option { + let capture = CaptureAuthData::default(); + let service = AddContext::<_, EmptyContext>::new(capture.clone()); + + let mut builder = Request::get(uri); + for (name, value) in headers { + builder = builder.header(*name, *value); + } + let request = builder.body(()).expect("request should build"); + + futures::executor::block_on(service.call(request)).expect("service call should succeed"); + + let resolved = capture.0.lock().expect("lock poisoned").clone(); + resolved +} + +/// `dXNlcjpwYXNzd29yZA==` is `user:password`. +const BASIC_HEADER: &str = "Basic dXNlcjpwYXNzd29yZA=="; +const BEARER_HEADER: &str = "Bearer some-token"; + +#[test] +fn no_credentials_resolve_to_no_auth_data() { + assert_eq!(resolve_auth_data("/", &[]), None); +} + +#[test] +fn basic_credentials_resolve_to_the_declared_scheme() { + assert_eq!( + resolve_auth_data("/", &[("authorization", BASIC_HEADER)]), + None, + ); +} + +#[test] +fn bearer_credentials_resolve_to_the_declared_scheme() { + assert_eq!( + resolve_auth_data("/", &[("authorization", BEARER_HEADER)]), + Some(AuthData::Bearer("some-token".to_owned())), + ); +} From fb098e2a1b9103c3fecfab7473fd23424beaa3f5 Mon Sep 17 00:00:00 2001 From: Alice Twist Date: Wed, 12 Aug 2026 22:42:25 +0100 Subject: [PATCH 2/2] [rust-server] Track auth precedence per apiKey location Address review feedback on the generated auth scheme precedence tests. The header and query apiKey blocks are shadowed independently: each matches a different part of the request, so a preceding block that fails to claim one may still claim the other. Tracking a single "precedes the first apiKey block" flag got this wrong for specs that declare a query apiKey before an HTTP scheme, e.g. `[queryApiKey, bearer, headerApiKey]`, where the generated test expected `ApiKey` but the runtime resolves `Bearer`. Track precedence separately for each location. Also add the query-side counterparts of the header precedence tests, so a query apiKey declared after Basic or Bearer is checked for reachability too, and make the sole-credential test names reflect what they assert when the scheme is not declared. This takes the suite from 19 to 21 tests. --- .../codegen/languages/RustServerCodegen.java | 32 ++++++++++------ .../tests-auth-scheme-precedence.mustache | 37 +++++++++++++++++-- .../tests/auth_scheme_precedence.rs | 4 +- .../tests/auth_scheme_precedence.rs | 4 +- .../tests/auth_scheme_precedence.rs | 23 +++++++++++- .../tests/auth_scheme_precedence.rs | 4 +- 6 files changed, 81 insertions(+), 23 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustServerCodegen.java index d7d1d23e1c05..079e92f1ad92 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustServerCodegen.java @@ -1396,15 +1396,17 @@ public Map postProcessSupportingFileData(Map bun * and make every later block unreachable (see issue #24095). * * Whether that is observable depends on block order, so the only thing the template cannot work - * out for itself is whether a block handling a given HTTP scheme precedes the first apiKey - * block. Everything else - which requests to send, which credentials to use - lives in the - * template. + * out for itself is whether a block handling a given HTTP scheme precedes the apiKey block it + * could shadow. Everything else - which requests to send, which credentials to use - lives in + * the template. */ private void addAuthSchemeTestsToBundle(List authMethods, Map bundle) { boolean hasBasic = false; boolean hasBearer = false; - boolean basicPrecedesApiKey = false; - boolean bearerPrecedesApiKey = false; + boolean basicPrecedesHeaderApiKey = false; + boolean bearerPrecedesHeaderApiKey = false; + boolean basicPrecedesQueryApiKey = false; + boolean bearerPrecedesQueryApiKey = false; String apiKeyHeaderName = null; String apiKeyQueryName = null; @@ -1420,10 +1422,16 @@ private void addAuthSchemeTestsToBundle(List authMethods, Map authMethods, Map