Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1383,9 +1383,84 @@ public Map<String, Object> postProcessSupportingFileData(Map<String, Object> 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<CodegenSecurity> authMethods, Map<String, Object> 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
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AuthData>` that `AddContext` pushed onto the context.
#[derive(Clone, Default)]
struct CaptureAuthData(Arc<Mutex<Option<AuthData>>>);

impl<C, ReqBody> Service<(Request<ReqBody>, C)> for CaptureAuthData
where
C: Has<Option<AuthData>>,
{
type Response = Response<String>;
type Error = std::convert::Infallible;
type Future = std::future::Ready<Result<Self::Response, Self::Error>>;

fn call(&self, (_request, context): (Request<ReqBody>, C)) -> Self::Future {
let auth_data: &Option<AuthData> = 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<AuthData> {
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}}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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<AuthData>` that `AddContext` pushed onto the context.
#[derive(Clone, Default)]
struct CaptureAuthData(Arc<Mutex<Option<AuthData>>>);

impl<C, ReqBody> Service<(Request<ReqBody>, C)> for CaptureAuthData
where
C: Has<Option<AuthData>>,
{
type Response = Response<String>;
type Error = std::convert::Infallible;
type Future = std::future::Ready<Result<Self::Response, Self::Error>>;

fn call(&self, (_request, context): (Request<ReqBody>, C)) -> Self::Future {
let auth_data: &Option<AuthData> = 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<AuthData> {
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())),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ src/lib.rs
src/models.rs
src/server/mod.rs
src/server/server_auth.rs
tests/auth_scheme_precedence.rs
Loading
Loading