Skip to content

Commit 23ca16e

Browse files
authored
[rust-server] Generate the auth scheme precedence tests (#24690)
* [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. * [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.
1 parent 4a0d560 commit 23ca16e

10 files changed

Lines changed: 482 additions & 103 deletions

File tree

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

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1383,9 +1383,84 @@ public Map<String, Object> postProcessSupportingFileData(Map<String, Object> bun
13831383
}
13841384
bundle.put("hasAuthScopes", hasAuthScopes);
13851385

1386+
addAuthSchemeTestsToBundle(authMethods, bundle);
1387+
13861388
return super.postProcessSupportingFileData(bundle);
13871389
}
13881390

1391+
/**
1392+
* Derive the facts the generated auth-scheme precedence tests need.
1393+
*
1394+
* Each generated block in `context.rs` returns early once it matches, so a block that fails to
1395+
* check which `AuthData` variant it received will claim credentials belonging to another scheme
1396+
* and make every later block unreachable (see issue #24095).
1397+
*
1398+
* Whether that is observable depends on block order, so the only thing the template cannot work
1399+
* out for itself is whether a block handling a given HTTP scheme precedes the apiKey block it
1400+
* could shadow. Everything else - which requests to send, which credentials to use - lives in
1401+
* the template.
1402+
*/
1403+
private void addAuthSchemeTestsToBundle(List<CodegenSecurity> authMethods, Map<String, Object> bundle) {
1404+
boolean hasBasic = false;
1405+
boolean hasBearer = false;
1406+
boolean basicPrecedesHeaderApiKey = false;
1407+
boolean bearerPrecedesHeaderApiKey = false;
1408+
boolean basicPrecedesQueryApiKey = false;
1409+
boolean bearerPrecedesQueryApiKey = false;
1410+
String apiKeyHeaderName = null;
1411+
String apiKeyQueryName = null;
1412+
1413+
if (authMethods != null) {
1414+
for (CodegenSecurity authMethod : authMethods) {
1415+
boolean isBasic = Boolean.TRUE.equals(authMethod.isBasicBasic);
1416+
boolean isBearer = Boolean.TRUE.equals(authMethod.isBasicBearer)
1417+
|| Boolean.TRUE.equals(authMethod.isOAuth);
1418+
boolean isApiKeyHeader = Boolean.TRUE.equals(authMethod.isApiKey)
1419+
&& Boolean.TRUE.equals(authMethod.isKeyInHeader);
1420+
boolean isApiKeyQuery = Boolean.TRUE.equals(authMethod.isApiKey)
1421+
&& Boolean.TRUE.equals(authMethod.isKeyInQuery);
1422+
1423+
hasBasic |= isBasic;
1424+
hasBearer |= isBearer;
1425+
// Only blocks generated before an apiKey block can shadow it, and the header and
1426+
// query blocks are shadowed independently: each matches a different part of the
1427+
// request, so a block that fails to match one may still precede and claim the other.
1428+
if (apiKeyHeaderName == null) {
1429+
basicPrecedesHeaderApiKey |= isBasic;
1430+
bearerPrecedesHeaderApiKey |= isBearer;
1431+
}
1432+
if (apiKeyQueryName == null) {
1433+
basicPrecedesQueryApiKey |= isBasic;
1434+
bearerPrecedesQueryApiKey |= isBearer;
1435+
}
1436+
if (isApiKeyHeader && apiKeyHeaderName == null) {
1437+
apiKeyHeaderName = authMethod.keyParamName.toLowerCase(Locale.ROOT);
1438+
}
1439+
if (isApiKeyQuery && apiKeyQueryName == null) {
1440+
apiKeyQueryName = authMethod.keyParamName;
1441+
}
1442+
}
1443+
}
1444+
1445+
bundle.put("authTestHasBasic", hasBasic);
1446+
bundle.put("authTestHasBearer", hasBearer);
1447+
bundle.put("authTestBasicPrecedesHeaderApiKey", basicPrecedesHeaderApiKey);
1448+
bundle.put("authTestBearerPrecedesHeaderApiKey", bearerPrecedesHeaderApiKey);
1449+
bundle.put("authTestBasicPrecedesQueryApiKey", basicPrecedesQueryApiKey);
1450+
bundle.put("authTestBearerPrecedesQueryApiKey", bearerPrecedesQueryApiKey);
1451+
bundle.put("authTestApiKeyHeader", apiKeyHeaderName);
1452+
bundle.put("authTestApiKeyQuery", apiKeyQueryName);
1453+
bundle.put("authTestHasApiKey", apiKeyHeaderName != null || apiKeyQueryName != null);
1454+
1455+
SupportingFile authTestFile =
1456+
new SupportingFile("tests-auth-scheme-precedence.mustache", "tests", "auth_scheme_precedence.rs");
1457+
if (hasBasic || hasBearer || apiKeyHeaderName != null || apiKeyQueryName != null) {
1458+
supportingFiles.add(authTestFile);
1459+
} else {
1460+
supportingFiles.remove(authTestFile);
1461+
}
1462+
}
1463+
13891464
/**
13901465
* Add a built path set map to the provided bundle
13911466
*
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
//! Runtime checks for auth-scheme precedence in the generated `AddContext` middleware.
2+
//!
3+
//! This file is generated. `swagger::auth::from_headers` returns an *untyped* `AuthData`,
4+
//! matching an `Authorization` header that carries either `Basic` or `Bearer` credentials.
5+
//! Every generated auth block returns early once it matches, so a block that does not check
6+
//! which variant it received will claim credentials belonging to a different scheme and
7+
//! prevent every later block - including API-key blocks - from ever running.
8+
//!
9+
//! The expectations below are derived from the security schemes this API declares, in the
10+
//! order their blocks are generated.
11+
12+
#![cfg(feature = "server")]
13+
14+
use std::sync::{Arc, Mutex};
15+
16+
use hyper::service::Service;
17+
use hyper::{Request, Response};
18+
use swagger::auth::AuthData;
19+
use swagger::{EmptyContext, Has};
20+
use {{{externCrateName}}}::context::AddContext;
21+
22+
/// Innermost service: records the `Option<AuthData>` that `AddContext` pushed onto the context.
23+
#[derive(Clone, Default)]
24+
struct CaptureAuthData(Arc<Mutex<Option<AuthData>>>);
25+
26+
impl<C, ReqBody> Service<(Request<ReqBody>, C)> for CaptureAuthData
27+
where
28+
C: Has<Option<AuthData>>,
29+
{
30+
type Response = Response<String>;
31+
type Error = std::convert::Infallible;
32+
type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
33+
34+
fn call(&self, (_request, context): (Request<ReqBody>, C)) -> Self::Future {
35+
let auth_data: &Option<AuthData> = context.get();
36+
*self.0.lock().expect("lock poisoned") = auth_data.clone();
37+
std::future::ready(Ok(Response::new(String::new())))
38+
}
39+
}
40+
41+
/// Drives a request through `AddContext` and returns the `AuthData` it resolved.
42+
fn resolve_auth_data(uri: &str, headers: &[(&str, &str)]) -> Option<AuthData> {
43+
let capture = CaptureAuthData::default();
44+
let service = AddContext::<_, EmptyContext>::new(capture.clone());
45+
46+
let mut builder = Request::get(uri);
47+
for (name, value) in headers {
48+
builder = builder.header(*name, *value);
49+
}
50+
let request = builder.body(()).expect("request should build");
51+
52+
futures::executor::block_on(service.call(request)).expect("service call should succeed");
53+
54+
let resolved = capture.0.lock().expect("lock poisoned").clone();
55+
resolved
56+
}
57+
58+
/// `dXNlcjpwYXNzd29yZA==` is `user:password`.
59+
const BASIC_HEADER: &str = "Basic dXNlcjpwYXNzd29yZA==";
60+
const BEARER_HEADER: &str = "Bearer some-token";
61+
{{#authTestHasApiKey}}
62+
const API_KEY: &str = "test-api-key";
63+
{{/authTestHasApiKey}}
64+
65+
#[test]
66+
fn no_credentials_resolve_to_no_auth_data() {
67+
assert_eq!(resolve_auth_data("/", &[]), None);
68+
}
69+
70+
#[test]
71+
{{#authTestHasBasic}}
72+
fn basic_credentials_resolve_to_the_declared_basic_scheme() {
73+
{{/authTestHasBasic}}
74+
{{^authTestHasBasic}}
75+
fn basic_credentials_resolve_to_none_when_no_basic_scheme_is_declared() {
76+
{{/authTestHasBasic}}
77+
assert_eq!(
78+
resolve_auth_data("/", &[("authorization", BASIC_HEADER)]),
79+
{{#authTestHasBasic}}Some(AuthData::Basic("user".to_owned(), "password".to_owned())){{/authTestHasBasic}}{{^authTestHasBasic}}None{{/authTestHasBasic}},
80+
);
81+
}
82+
83+
#[test]
84+
{{#authTestHasBearer}}
85+
fn bearer_credentials_resolve_to_the_declared_bearer_scheme() {
86+
{{/authTestHasBearer}}
87+
{{^authTestHasBearer}}
88+
fn bearer_credentials_resolve_to_none_when_no_bearer_scheme_is_declared() {
89+
{{/authTestHasBearer}}
90+
assert_eq!(
91+
resolve_auth_data("/", &[("authorization", BEARER_HEADER)]),
92+
{{#authTestHasBearer}}Some(AuthData::Bearer("some-token".to_owned())){{/authTestHasBearer}}{{^authTestHasBearer}}None{{/authTestHasBearer}},
93+
);
94+
}
95+
{{#authTestApiKeyHeader}}
96+
97+
#[test]
98+
fn header_api_key_resolves_when_it_is_the_only_credential() {
99+
assert_eq!(
100+
resolve_auth_data("/", &[("{{{.}}}", API_KEY)]),
101+
Some(AuthData::ApiKey(API_KEY.to_owned())),
102+
);
103+
}
104+
105+
/// An `Authorization` header must not shadow the apiKey block unless a block that actually
106+
/// handles that scheme is generated before it.
107+
#[test]
108+
fn header_api_key_is_reachable_alongside_bearer_credentials() {
109+
assert_eq!(
110+
resolve_auth_data("/", &[("authorization", BEARER_HEADER), ("{{{.}}}", API_KEY)]),
111+
{{#authTestBearerPrecedesHeaderApiKey}}Some(AuthData::Bearer("some-token".to_owned())){{/authTestBearerPrecedesHeaderApiKey}}{{^authTestBearerPrecedesHeaderApiKey}}Some(AuthData::ApiKey(API_KEY.to_owned())){{/authTestBearerPrecedesHeaderApiKey}},
112+
);
113+
}
114+
115+
#[test]
116+
fn header_api_key_is_reachable_alongside_basic_credentials() {
117+
assert_eq!(
118+
resolve_auth_data("/", &[("authorization", BASIC_HEADER), ("{{{.}}}", API_KEY)]),
119+
{{#authTestBasicPrecedesHeaderApiKey}}Some(AuthData::Basic("user".to_owned(), "password".to_owned())){{/authTestBasicPrecedesHeaderApiKey}}{{^authTestBasicPrecedesHeaderApiKey}}Some(AuthData::ApiKey(API_KEY.to_owned())){{/authTestBasicPrecedesHeaderApiKey}},
120+
);
121+
}
122+
{{/authTestApiKeyHeader}}
123+
{{#authTestApiKeyQuery}}
124+
125+
#[test]
126+
fn query_api_key_resolves_when_it_is_the_only_credential() {
127+
assert_eq!(
128+
resolve_auth_data("/?{{{.}}}=test-api-key", &[]),
129+
Some(AuthData::ApiKey(API_KEY.to_owned())),
130+
);
131+
}
132+
133+
/// The query apiKey block is shadowed independently of the header one: an `Authorization`
134+
/// header must not claim a request whose credentials are in the query string unless a block
135+
/// that actually handles that scheme is generated before the query block.
136+
#[test]
137+
fn query_api_key_is_reachable_alongside_bearer_credentials() {
138+
assert_eq!(
139+
resolve_auth_data("/?{{{.}}}=test-api-key", &[("authorization", BEARER_HEADER)]),
140+
{{#authTestBearerPrecedesQueryApiKey}}Some(AuthData::Bearer("some-token".to_owned())){{/authTestBearerPrecedesQueryApiKey}}{{^authTestBearerPrecedesQueryApiKey}}Some(AuthData::ApiKey(API_KEY.to_owned())){{/authTestBearerPrecedesQueryApiKey}},
141+
);
142+
}
143+
144+
#[test]
145+
fn query_api_key_is_reachable_alongside_basic_credentials() {
146+
assert_eq!(
147+
resolve_auth_data("/?{{{.}}}=test-api-key", &[("authorization", BASIC_HEADER)]),
148+
{{#authTestBasicPrecedesQueryApiKey}}Some(AuthData::Basic("user".to_owned(), "password".to_owned())){{/authTestBasicPrecedesQueryApiKey}}{{^authTestBasicPrecedesQueryApiKey}}Some(AuthData::ApiKey(API_KEY.to_owned())){{/authTestBasicPrecedesQueryApiKey}},
149+
);
150+
}
151+
{{/authTestApiKeyQuery}}

samples/server/petstore/rust-server/output/openapi-v3/.openapi-generator/FILES

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,4 @@ src/models.rs
6464
src/server/callbacks.rs
6565
src/server/mod.rs
6666
src/server/server_auth.rs
67+
tests/auth_scheme_precedence.rs
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//! Runtime checks for auth-scheme precedence in the generated `AddContext` middleware.
2+
//!
3+
//! This file is generated. `swagger::auth::from_headers` returns an *untyped* `AuthData`,
4+
//! matching an `Authorization` header that carries either `Basic` or `Bearer` credentials.
5+
//! Every generated auth block returns early once it matches, so a block that does not check
6+
//! which variant it received will claim credentials belonging to a different scheme and
7+
//! prevent every later block - including API-key blocks - from ever running.
8+
//!
9+
//! The expectations below are derived from the security schemes this API declares, in the
10+
//! order their blocks are generated.
11+
12+
#![cfg(feature = "server")]
13+
14+
use std::sync::{Arc, Mutex};
15+
16+
use hyper::service::Service;
17+
use hyper::{Request, Response};
18+
use swagger::auth::AuthData;
19+
use swagger::{EmptyContext, Has};
20+
use openapi_v3::context::AddContext;
21+
22+
/// Innermost service: records the `Option<AuthData>` that `AddContext` pushed onto the context.
23+
#[derive(Clone, Default)]
24+
struct CaptureAuthData(Arc<Mutex<Option<AuthData>>>);
25+
26+
impl<C, ReqBody> Service<(Request<ReqBody>, C)> for CaptureAuthData
27+
where
28+
C: Has<Option<AuthData>>,
29+
{
30+
type Response = Response<String>;
31+
type Error = std::convert::Infallible;
32+
type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
33+
34+
fn call(&self, (_request, context): (Request<ReqBody>, C)) -> Self::Future {
35+
let auth_data: &Option<AuthData> = context.get();
36+
*self.0.lock().expect("lock poisoned") = auth_data.clone();
37+
std::future::ready(Ok(Response::new(String::new())))
38+
}
39+
}
40+
41+
/// Drives a request through `AddContext` and returns the `AuthData` it resolved.
42+
fn resolve_auth_data(uri: &str, headers: &[(&str, &str)]) -> Option<AuthData> {
43+
let capture = CaptureAuthData::default();
44+
let service = AddContext::<_, EmptyContext>::new(capture.clone());
45+
46+
let mut builder = Request::get(uri);
47+
for (name, value) in headers {
48+
builder = builder.header(*name, *value);
49+
}
50+
let request = builder.body(()).expect("request should build");
51+
52+
futures::executor::block_on(service.call(request)).expect("service call should succeed");
53+
54+
let resolved = capture.0.lock().expect("lock poisoned").clone();
55+
resolved
56+
}
57+
58+
/// `dXNlcjpwYXNzd29yZA==` is `user:password`.
59+
const BASIC_HEADER: &str = "Basic dXNlcjpwYXNzd29yZA==";
60+
const BEARER_HEADER: &str = "Bearer some-token";
61+
62+
#[test]
63+
fn no_credentials_resolve_to_no_auth_data() {
64+
assert_eq!(resolve_auth_data("/", &[]), None);
65+
}
66+
67+
#[test]
68+
fn basic_credentials_resolve_to_none_when_no_basic_scheme_is_declared() {
69+
assert_eq!(
70+
resolve_auth_data("/", &[("authorization", BASIC_HEADER)]),
71+
None,
72+
);
73+
}
74+
75+
#[test]
76+
fn bearer_credentials_resolve_to_the_declared_bearer_scheme() {
77+
assert_eq!(
78+
resolve_auth_data("/", &[("authorization", BEARER_HEADER)]),
79+
Some(AuthData::Bearer("some-token".to_owned())),
80+
);
81+
}

samples/server/petstore/rust-server/output/overlapping-auth-schemes/.openapi-generator/FILES

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ src/lib.rs
2121
src/models.rs
2222
src/server/mod.rs
2323
src/server/server_auth.rs
24+
tests/auth_scheme_precedence.rs

0 commit comments

Comments
 (0)