Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
8 changes: 8 additions & 0 deletions bin/configs/rust-server-overlapping-auth-schemes.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
generatorName: rust-server
outputDir: samples/server/petstore/rust-server/output/overlapping-auth-schemes
inputSpec: modules/openapi-generator/src/test/resources/3_0/rust-server/overlapping-auth-schemes.yaml
templateDir: modules/openapi-generator/src/main/resources/rust-server
generateAliasAsModel: true
additionalProperties:
hideGenerationTimestamp: "true"
packageName: overlapping-auth-schemes
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ impl<T, A, B, C, ReqBody> Service<Request<ReqBody>> for AddContext<T, A>
{{#isBasicBasic}}
{
use std::ops::Deref;
if let Some(auth) = swagger::auth::from_headers(headers) {
if let Some(auth @ AuthData::Basic(..)) = swagger::auth::from_headers(headers) {
let context = context.push(Some(auth));

return self.inner.call((request, context))
Expand All @@ -118,7 +118,7 @@ impl<T, A, B, C, ReqBody> Service<Request<ReqBody>> for AddContext<T, A>
{
use headers::authorization::Bearer;
use std::ops::Deref;
if let Some(bearer) = swagger::auth::from_headers(headers) {
if let Some(bearer @ AuthData::Bearer(..)) = swagger::auth::from_headers(headers) {
let context = context.push(Some(bearer));

return self.inner.call((request, context))
Expand All @@ -130,7 +130,7 @@ impl<T, A, B, C, ReqBody> Service<Request<ReqBody>> for AddContext<T, A>
{
use headers::authorization::Bearer;
use std::ops::Deref;
if let Some(bearer) = swagger::auth::from_headers(headers) {
if let Some(bearer @ AuthData::Bearer(..)) = swagger::auth::from_headers(headers) {
let context = context.push(Some(bearer));

return self.inner.call((request, context))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.openapitools.codegen.DefaultGenerator;
import org.openapitools.codegen.TestUtils;
import org.openapitools.codegen.config.CodegenConfigurator;
import org.testng.Assert;
import org.testng.annotations.Test;

import java.io.File;
Expand Down Expand Up @@ -214,4 +215,109 @@ public void testBinaryRequestBodyNotCoercedToUtf8() throws IOException {
// Clean up
target.toFile().deleteOnExit();
}

/**
* Test that each generated security scheme block in context.rs only matches the auth
* scheme it was generated for (see issue #24095).
*
* Since swagger-rs 7, swagger::auth::from_headers is no longer scheme-typed: it returns
* Option<AuthData> and matches either a Basic or a Bearer Authorization header. Because
* each generated block returns early on a match, an unrestricted block captures requests
* belonging to a different scheme and makes every later security scheme block
* unreachable - including in-header apiKey blocks, which is an authorization bypass.
*/
@Test
public void testAuthSchemeBlocksOnlyMatchTheirOwnScheme() throws IOException {
Path target = Files.createTempDirectory("test");
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("rust-server")
.setInputSpec("src/test/resources/2_0/rust-server/petstore-with-fake-endpoints-models-for-testing.yaml")
.setSkipOverwrite(false)
.setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
List<File> files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
files.forEach(File::deleteOnExit);

Path contextPath = Path.of(target.toString(), "/src/context.rs");
TestUtils.assertFileExists(contextPath);

// The oauth2 (petstore_auth) block must only accept a Bearer header.
TestUtils.assertFileContains(contextPath,
"if let Some(bearer @ AuthData::Bearer(..)) = swagger::auth::from_headers(headers) {");
// The basic (http_basic_test) block must only accept a Basic header.
TestUtils.assertFileContains(contextPath,
"if let Some(auth @ AuthData::Basic(..)) = swagger::auth::from_headers(headers) {");
// No block may accept any Authorization header regardless of scheme, which would
// short-circuit the api_key / api_key_query blocks that follow it.
TestUtils.assertFileNotContains(contextPath,
"if let Some(bearer) = swagger::auth::from_headers(headers) {");
TestUtils.assertFileNotContains(contextPath,
"if let Some(auth) = swagger::auth::from_headers(headers) {");

// The in-header apiKey block must still be generated and reachable.
TestUtils.assertFileContains(contextPath,
"if let Some(header) = api_key_from_header(headers, \"api_key\") {");

// Clean up
target.toFile().deleteOnExit();
}

/**
* Companion to {@link #testAuthSchemeBlocksOnlyMatchTheirOwnScheme()} covering the
* scheme combinations the petstore fixture cannot express.
*
* The petstore fixture pairs `isOAuth` with `isBasicBasic`, and declares HTTP Basic
* last so its block is generated after the apiKey blocks and cannot shadow them.
* This spec instead declares HTTP Basic, then an in-header apiKey scheme, then HTTP
* Bearer. That covers the `isBasicBasic` / `isBasicBearer` pairing - two HTTP schemes
* that both read the Authorization header, and so are the pair most able to swallow
* each other - and interleaves an apiKey block between them, which is the ordering
* that turns an unrestricted block into an authorization bypass: the Basic block
* claims credentials for a scheme it does not handle, returns early, and the apiKey
* block below it never runs.
*
* The same sample is generated into
* `samples/server/petstore/rust-server/output/overlapping-auth-schemes`, where
* `tests/auth_scheme_precedence.rs` asserts the same property at request level.
*/
@Test
public void testOverlappingAuthSchemeBlocksDoNotShadowEachOther() throws IOException {
Path target = Files.createTempDirectory("test");
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("rust-server")
.setInputSpec("src/test/resources/3_0/rust-server/overlapping-auth-schemes.yaml")
.setSkipOverwrite(false)
.setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
List<File> files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
files.forEach(File::deleteOnExit);

Path contextPath = Path.of(target.toString(), "/src/context.rs");
TestUtils.assertFileExists(contextPath);

String context = Files.readString(contextPath);

String basicBlock = "if let Some(auth @ AuthData::Basic(..)) = swagger::auth::from_headers(headers) {";
String bearerBlock = "if let Some(bearer @ AuthData::Bearer(..)) = swagger::auth::from_headers(headers) {";
String apiKeyBlock = "if let Some(header) = api_key_from_header(headers, \"x-api-key\") {";

// Each Authorization-based block must be restricted to its own scheme...
TestUtils.assertFileContains(contextPath, basicBlock);
TestUtils.assertFileContains(contextPath, bearerBlock);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
TestUtils.assertFileNotContains(contextPath,
"if let Some(auth) = swagger::auth::from_headers(headers) {");
TestUtils.assertFileNotContains(contextPath,
"if let Some(bearer) = swagger::auth::from_headers(headers) {");
// ...and the apiKey block sitting between them must still be generated.
TestUtils.assertFileContains(contextPath, apiKeyBlock);

// Guard the premise of this test: if the generator ever emits these blocks in a
// different order then this spec no longer exercises the shadowing case, and the
// assertions above would silently stop proving anything.
Assert.assertTrue(context.indexOf(basicBlock) < context.indexOf(apiKeyBlock),
"expected the Basic auth block to be generated before the apiKey block");
Assert.assertTrue(context.indexOf(apiKeyBlock) < context.indexOf(bearerBlock),
"expected the apiKey block to be generated between the two HTTP auth blocks");

// Clean up
target.toFile().deleteOnExit();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
openapi: 3.0.1
info:
title: overlapping auth schemes test
version: '1.0'
servers:
- url: 'http://localhost:8080/'
paths:
/ping:
get:
operationId: pingGet
responses:
'201':
description: OK
components:
# This spec exists to exercise the auth-scheme blocks generated into context.rs when
# several schemes compete for the same request. See issue #24095.
#
# Two properties matter, and no other rust-server fixture has both:
#
# * `basicAuth` and `bearerAuth` are HTTP schemes that both read the `Authorization`
# header, so an unrestricted block for either one also matches the other. This is
# the `isBasicBasic` / `isBasicBearer` pairing; the petstore fixture only covers
# `isBasicBasic` alongside `isOAuth`.
# * `apiKeyAuth` is declared *between* the two HTTP schemes. Blocks are emitted in
# declaration order and each returns early, so an unrestricted Basic block does not
# merely pick the wrong scheme for a bearer-credentialed request - it makes the
# apiKey block below it unreachable, which is an authorization bypass rather than a
# mislabelling. The interleaving is also what makes the bug observable at runtime:
# were the two HTTP blocks adjacent, a broken and a fixed generator would both
# resolve bearer credentials to `AuthData::Bearer` and no request-level test could
# tell them apart.
securitySchemes:
basicAuth:
scheme: basic
type: http
apiKeyAuth:
type: apiKey
name: x-api-key
in: header
bearerAuth:
scheme: bearer
bearerFormat: token
type: http
security:
- basicAuth: []
- apiKeyAuth: []
- bearerAuth: []
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ impl<T, A, B, C, ReqBody> Service<Request<ReqBody>> for AddContext<T, A>
{
use headers::authorization::Bearer;
use std::ops::Deref;
if let Some(bearer) = swagger::auth::from_headers(headers) {
if let Some(bearer @ AuthData::Bearer(..)) = swagger::auth::from_headers(headers) {
let context = context.push(Some(bearer));

return self.inner.call((request, context))
Expand All @@ -114,7 +114,7 @@ impl<T, A, B, C, ReqBody> Service<Request<ReqBody>> for AddContext<T, A>
{
use headers::authorization::Bearer;
use std::ops::Deref;
if let Some(bearer) = swagger::auth::from_headers(headers) {
if let Some(bearer @ AuthData::Bearer(..)) = swagger::auth::from_headers(headers) {
let context = context.push(Some(bearer));

return self.inner.call((request, context))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[build]
rustflags = [
"-W", "missing_docs", # detects missing documentation for public members

"-W", "trivial_casts", # detects trivial casts which could be removed

"-W", "trivial_numeric_casts", # detects trivial casts of numeric types which could be removed

# unsafe is used in `TokioIo` bridging code copied from `hyper`.
# "-W", "unsafe_code", # usage of `unsafe` code

"-W", "unused_qualifications", # detects unnecessarily qualified names

"-W", "unused_extern_crates", # extern crates that are never used

"-W", "unused_import_braces", # unnecessary braces around an imported item

"-D", "warnings", # all warnings should be denied
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
target
Cargo.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# OpenAPI Generator Ignore
# Generated by openapi-generator https://github.com/openapitools/openapi-generator

# Use this file to prevent files from being overwritten by the generator.
# The patterns follow closely to .gitignore or .dockerignore.

# As an example, the C# client generator defines ApiClient.cs.
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
#ApiClient.cs

# You can match any string of characters against a directory, file or extension with a single asterisk (*):
#foo/*/qux
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux

# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
#foo/**/qux
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux

# You can also negate patterns with an exclamation (!).
# For example, you can ignore all files in a docs folder with the file extension .md:
#docs/*.md
# Then explicitly reverse the ignore rule for a single file:
#!docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.cargo/config.toml
.gitignore
Cargo.toml
README.md
api/openapi.yaml
bin/cli.rs
docs/default_api.md
examples/ca.pem
examples/client/client_auth.rs
examples/client/main.rs
examples/server-chain.pem
examples/server-key.pem
examples/server/main.rs
examples/server/server.rs
examples/server/server_auth.rs
src/auth.rs
src/client/mod.rs
src/context.rs
src/header.rs
src/lib.rs
src/models.rs
src/server/mod.rs
src/server/server_auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
7.25.0-SNAPSHOT
Loading
Loading