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..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 @@ -1383,9 +1383,84 @@ 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 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 basicPrecedesHeaderApiKey = false; + boolean bearerPrecedesHeaderApiKey = false; + boolean basicPrecedesQueryApiKey = false; + boolean bearerPrecedesQueryApiKey = 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 an apiKey block can shadow it, and the header and + // query blocks are shadowed independently: each matches a different part of the + // request, so a block that fails to match one may still precede and claim the other. + if (apiKeyHeaderName == null) { + basicPrecedesHeaderApiKey |= isBasic; + bearerPrecedesHeaderApiKey |= isBearer; + } + if (apiKeyQueryName == null) { + basicPrecedesQueryApiKey |= isBasic; + bearerPrecedesQueryApiKey |= 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("authTestBasicPrecedesHeaderApiKey", basicPrecedesHeaderApiKey); + bundle.put("authTestBearerPrecedesHeaderApiKey", bearerPrecedesHeaderApiKey); + bundle.put("authTestBasicPrecedesQueryApiKey", basicPrecedesQueryApiKey); + bundle.put("authTestBearerPrecedesQueryApiKey", bearerPrecedesQueryApiKey); + 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..c3f666173c94 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/rust-server/tests-auth-scheme-precedence.mustache @@ -0,0 +1,151 @@ +//! 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] +{{#authTestHasBasic}} +fn basic_credentials_resolve_to_the_declared_basic_scheme() { +{{/authTestHasBasic}} +{{^authTestHasBasic}} +fn basic_credentials_resolve_to_none_when_no_basic_scheme_is_declared() { +{{/authTestHasBasic}} + assert_eq!( + resolve_auth_data("/", &[("authorization", BASIC_HEADER)]), + {{#authTestHasBasic}}Some(AuthData::Basic("user".to_owned(), "password".to_owned())){{/authTestHasBasic}}{{^authTestHasBasic}}None{{/authTestHasBasic}}, + ); +} + +#[test] +{{#authTestHasBearer}} +fn bearer_credentials_resolve_to_the_declared_bearer_scheme() { +{{/authTestHasBearer}} +{{^authTestHasBearer}} +fn bearer_credentials_resolve_to_none_when_no_bearer_scheme_is_declared() { +{{/authTestHasBearer}} + 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)]), + {{#authTestBearerPrecedesHeaderApiKey}}Some(AuthData::Bearer("some-token".to_owned())){{/authTestBearerPrecedesHeaderApiKey}}{{^authTestBearerPrecedesHeaderApiKey}}Some(AuthData::ApiKey(API_KEY.to_owned())){{/authTestBearerPrecedesHeaderApiKey}}, + ); +} + +#[test] +fn header_api_key_is_reachable_alongside_basic_credentials() { + assert_eq!( + resolve_auth_data("/", &[("authorization", BASIC_HEADER), ("{{{.}}}", API_KEY)]), + {{#authTestBasicPrecedesHeaderApiKey}}Some(AuthData::Basic("user".to_owned(), "password".to_owned())){{/authTestBasicPrecedesHeaderApiKey}}{{^authTestBasicPrecedesHeaderApiKey}}Some(AuthData::ApiKey(API_KEY.to_owned())){{/authTestBasicPrecedesHeaderApiKey}}, + ); +} +{{/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())), + ); +} + +/// The query apiKey block is shadowed independently of the header one: an `Authorization` +/// header must not claim a request whose credentials are in the query string unless a block +/// that actually handles that scheme is generated before the query block. +#[test] +fn query_api_key_is_reachable_alongside_bearer_credentials() { + assert_eq!( + resolve_auth_data("/?{{{.}}}=test-api-key", &[("authorization", BEARER_HEADER)]), + {{#authTestBearerPrecedesQueryApiKey}}Some(AuthData::Bearer("some-token".to_owned())){{/authTestBearerPrecedesQueryApiKey}}{{^authTestBearerPrecedesQueryApiKey}}Some(AuthData::ApiKey(API_KEY.to_owned())){{/authTestBearerPrecedesQueryApiKey}}, + ); +} + +#[test] +fn query_api_key_is_reachable_alongside_basic_credentials() { + assert_eq!( + resolve_auth_data("/?{{{.}}}=test-api-key", &[("authorization", BASIC_HEADER)]), + {{#authTestBasicPrecedesQueryApiKey}}Some(AuthData::Basic("user".to_owned(), "password".to_owned())){{/authTestBasicPrecedesQueryApiKey}}{{^authTestBasicPrecedesQueryApiKey}}Some(AuthData::ApiKey(API_KEY.to_owned())){{/authTestBasicPrecedesQueryApiKey}}, + ); +} +{{/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..c0dc7e9f603a --- /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_none_when_no_basic_scheme_is_declared() { + assert_eq!( + resolve_auth_data("/", &[("authorization", BASIC_HEADER)]), + None, + ); +} + +#[test] +fn bearer_credentials_resolve_to_the_declared_bearer_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..2aa1835784ea 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_basic_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_bearer_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..f2c534173151 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 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 no_credentials_resolve_to_no_auth_data() { + assert_eq!(resolve_auth_data("/", &[]), None); +} + +#[test] +fn basic_credentials_resolve_to_the_declared_basic_scheme() { assert_eq!( resolve_auth_data("/", &[("authorization", BASIC_HEADER)]), Some(AuthData::Basic("user".to_owned(), "password".to_owned())), @@ -78,44 +74,62 @@ 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_bearer_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())), + ); +} + +/// The query apiKey block is shadowed independently of the header one: an `Authorization` +/// header must not claim a request whose credentials are in the query string unless a block +/// that actually handles that scheme is generated before the query block. +#[test] +fn query_api_key_is_reachable_alongside_bearer_credentials() { + assert_eq!( + resolve_auth_data("/?api_key_query=test-api-key", &[("authorization", BEARER_HEADER)]), + Some(AuthData::Bearer("some-token".to_owned())), + ); +} + +#[test] +fn query_api_key_is_reachable_alongside_basic_credentials() { + assert_eq!( + resolve_auth_data("/?api_key_query=test-api-key", &[("authorization", BASIC_HEADER)]), + 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..e6410b65e8fd --- /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_none_when_no_basic_scheme_is_declared() { + assert_eq!( + resolve_auth_data("/", &[("authorization", BASIC_HEADER)]), + None, + ); +} + +#[test] +fn bearer_credentials_resolve_to_the_declared_bearer_scheme() { + assert_eq!( + resolve_auth_data("/", &[("authorization", BEARER_HEADER)]), + Some(AuthData::Bearer("some-token".to_owned())), + ); +}