From 926ad02cd56a44809e28925522025ed84d59d5aa Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Sun, 4 Jan 2026 19:25:37 +0100 Subject: [PATCH 1/2] refactor: update tests, API configurations, and router logic for API key support - Extract `X-Api-Key` into a constant for reusability in tests. - Improve router lifecycle handling with `DefaultRouter` adjustments. - Enhance YAML configuration for API key handling, including support for registry-based ApiKeyStores. - Minor refactoring and cleanup in `Apis.yaml` and related test files. --- .../predic8/membrane/core/cli/RouterCLI.java | 3 +- .../apikey/ApiKeysInterceptor.java | 10 ++++++- .../security/api-key/apikey-openapi/apis.yaml | 29 +++++++++++++++++++ .../test/APIKeyWithOpenAPIExampleTest.java | 16 +++++----- 4 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 distribution/examples/security/api-key/apikey-openapi/apis.yaml diff --git a/core/src/main/java/com/predic8/membrane/core/cli/RouterCLI.java b/core/src/main/java/com/predic8/membrane/core/cli/RouterCLI.java index cb6e2f054e..de4bc97fd5 100644 --- a/core/src/main/java/com/predic8/membrane/core/cli/RouterCLI.java +++ b/core/src/main/java/com/predic8/membrane/core/cli/RouterCLI.java @@ -75,8 +75,7 @@ private static void start(String[] args) { if (commandLine.getCommand().getName().equals("private-jwk-to-public")) { privateJwkToPublic(commandLine); } - var router = getRouter(commandLine); - if (router instanceof DefaultRouter dr) + if (getRouter(commandLine) instanceof DefaultRouter dr) dr.waitFor(); } diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/ApiKeysInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/ApiKeysInterceptor.java index d6cc29902e..93ebb89e8a 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/ApiKeysInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/ApiKeysInterceptor.java @@ -85,10 +85,18 @@ public String getLongDescription() { @Override public void init() { super.init(); - // At the moment the beanFactory is only there when the Membrane configuration was read from XML + + // Todo: Move logic into the registry + // The beanFactory is only there when the Membrane configuration was read from XML if (router.getBeanFactory() != null) { stores.addAll(router.getBeanFactory().getBeansOfType(ApiKeyStore.class).values()); } + // For YAML configuration + if (router.getRegistry() != null) { + var stores = router.getRegistry().getBeans(ApiKeyStore.class); + this.stores.addAll(stores); + } + stores.forEach(s -> s.init(router)); // Add the default extractor if none is configured diff --git a/distribution/examples/security/api-key/apikey-openapi/apis.yaml b/distribution/examples/security/api-key/apikey-openapi/apis.yaml new file mode 100644 index 0000000000..5401cec8df --- /dev/null +++ b/distribution/examples/security/api-key/apikey-openapi/apis.yaml @@ -0,0 +1,29 @@ +# yaml-language-server: $schema=https://www.membrane-api.io/v7.0.5.json + +components: + store: + apiKeyFileStore: + location: ./demo-keys.txt +--- + +api: + port: 2000 + specs: + - openapi: + location: fruitshop-api-v2-openapi-3-security.yml + validateSecurity: true + flow: + - log: + message: "Header: ${header['X-Api-Key']}" + - apiKey: + required: false + extractors: + - headerExtractor: + name: "X-Api-Key" +# - expressionExtractor: +# expression: "header['X-Api-Key']" + - openapiValidator: {} + - headerFilter: + rules: + - exclude: + pattern: "Origin" \ No newline at end of file diff --git a/distribution/src/test/java/com/predic8/membrane/examples/withinternet/test/APIKeyWithOpenAPIExampleTest.java b/distribution/src/test/java/com/predic8/membrane/examples/withinternet/test/APIKeyWithOpenAPIExampleTest.java index f51c2b045a..99844137d5 100644 --- a/distribution/src/test/java/com/predic8/membrane/examples/withinternet/test/APIKeyWithOpenAPIExampleTest.java +++ b/distribution/src/test/java/com/predic8/membrane/examples/withinternet/test/APIKeyWithOpenAPIExampleTest.java @@ -26,13 +26,15 @@ public class APIKeyWithOpenAPIExampleTest extends AbstractSampleMembraneStartStopTestcase { + public static final String API_KEY = "X-Api-Key"; + @Override protected String getExampleDirName() { return "security/api-key/apikey-openapi"; } @Test - public void noApiKey() { + void noApiKey() { when() .get("http://localhost:2000/shop/v2/products") .then().assertThat() @@ -41,9 +43,9 @@ public void noApiKey() { } @Test - public void noScopesGet() { + void noScopesGet() { given() - .header("X-Api-Key", "111") + .header(API_KEY, "111") .when() .get("http://localhost:2000/shop/v2/products") .then().assertThat() @@ -53,9 +55,9 @@ public void noScopesGet() { } @Test - public void noScopesPost() { + void noScopesPost() { given() - .header("X-Api-Key", "111") + .header(API_KEY, "111") .contentType(APPLICATION_JSON) .body(""" { @@ -78,9 +80,9 @@ public void noScopesPost() { } @Test - public void writeScopes() { + void writeScopes() { given() - .headers("X-Api-Key", "222") + .headers(API_KEY, "222") .contentType(APPLICATION_JSON) .body("{\"name\": \"Mango\", \"price\": 2.79}") .when() From 8fd57be7911f55834aac701bb6729ea446271d71 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Sun, 4 Jan 2026 20:34:52 +0100 Subject: [PATCH 2/2] refactor: rename API key constant and improve related tests, YAML, and router logic - Renamed `API_KEY` to `API_KEY_HEADER` for better clarity and consistency. - Updated tests and flow configurations to reflect the rename. - Removed unused imports and streamlined logic in `ApiKeysInterceptor`. - Enhanced YAML setup for OpenAPI validation and API key extraction ordering. --- .../interceptor/apikey/ApiKeysInterceptor.java | 7 ++----- .../apikey/extractors/ApiKeyHeaderExtractor.java | 1 - .../security/api-key/apikey-openapi/apis.yaml | 14 +++++--------- .../test/APIKeyWithOpenAPIExampleTest.java | 8 ++++---- 4 files changed, 11 insertions(+), 19 deletions(-) diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/ApiKeysInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/ApiKeysInterceptor.java index 93ebb89e8a..c598b02ce6 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/ApiKeysInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/ApiKeysInterceptor.java @@ -14,7 +14,6 @@ package com.predic8.membrane.core.interceptor.apikey; import com.predic8.membrane.annot.*; -import com.predic8.membrane.core.config.spring.*; import com.predic8.membrane.core.exchange.*; import com.predic8.membrane.core.interceptor.*; import com.predic8.membrane.core.interceptor.apikey.extractors.*; @@ -23,11 +22,10 @@ import org.slf4j.*; import java.util.*; -import java.util.stream.*; import static com.predic8.membrane.core.exceptions.ProblemDetails.*; import static com.predic8.membrane.core.interceptor.Outcome.*; -import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.*; import static java.util.stream.Stream.*; /** @@ -93,8 +91,7 @@ public void init() { } // For YAML configuration if (router.getRegistry() != null) { - var stores = router.getRegistry().getBeans(ApiKeyStore.class); - this.stores.addAll(stores); + this.stores.addAll(router.getRegistry().getBeans(ApiKeyStore.class)); } stores.forEach(s -> s.init(router)); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyHeaderExtractor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyHeaderExtractor.java index 1f2a82bd94..26b3c65315 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyHeaderExtractor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/apikey/extractors/ApiKeyHeaderExtractor.java @@ -22,7 +22,6 @@ import static com.predic8.membrane.core.security.ApiKeySecurityScheme.In.*; /** - * @deprecated Set an expression like ${header['api']} on apiKey * @description Extracts an API key from a specific HTTP request header. By default, the header name * is X-Api-Key. If the header is present, its first value is returned as the API key. *

diff --git a/distribution/examples/security/api-key/apikey-openapi/apis.yaml b/distribution/examples/security/api-key/apikey-openapi/apis.yaml index 5401cec8df..3227b1e5ee 100644 --- a/distribution/examples/security/api-key/apikey-openapi/apis.yaml +++ b/distribution/examples/security/api-key/apikey-openapi/apis.yaml @@ -13,17 +13,13 @@ api: location: fruitshop-api-v2-openapi-3-security.yml validateSecurity: true flow: - - log: - message: "Header: ${header['X-Api-Key']}" - apiKey: + # API keys are validated in the OpenAPI validator with validateSecurity: true. See the OpenAPI document for details. required: false extractors: - headerExtractor: name: "X-Api-Key" -# - expressionExtractor: -# expression: "header['X-Api-Key']" - - openapiValidator: {} - - headerFilter: - rules: - - exclude: - pattern: "Origin" \ No newline at end of file + # The API key must be extracted before the OpenAPI validator is called. + # Normally, the OpenAPI is validated before the flow is executed. By explicitly + # setting the openapiValidator to this position, the OpenAPI is validated after the apiKey plugin. + - openapiValidator: {} \ No newline at end of file diff --git a/distribution/src/test/java/com/predic8/membrane/examples/withinternet/test/APIKeyWithOpenAPIExampleTest.java b/distribution/src/test/java/com/predic8/membrane/examples/withinternet/test/APIKeyWithOpenAPIExampleTest.java index 99844137d5..50bc4ee199 100644 --- a/distribution/src/test/java/com/predic8/membrane/examples/withinternet/test/APIKeyWithOpenAPIExampleTest.java +++ b/distribution/src/test/java/com/predic8/membrane/examples/withinternet/test/APIKeyWithOpenAPIExampleTest.java @@ -26,7 +26,7 @@ public class APIKeyWithOpenAPIExampleTest extends AbstractSampleMembraneStartStopTestcase { - public static final String API_KEY = "X-Api-Key"; + public static final String API_KEY_HEADER = "X-Api-Key"; @Override protected String getExampleDirName() { @@ -45,7 +45,7 @@ void noApiKey() { @Test void noScopesGet() { given() - .header(API_KEY, "111") + .header(API_KEY_HEADER, "111") .when() .get("http://localhost:2000/shop/v2/products") .then().assertThat() @@ -57,7 +57,7 @@ void noScopesGet() { @Test void noScopesPost() { given() - .header(API_KEY, "111") + .header(API_KEY_HEADER, "111") .contentType(APPLICATION_JSON) .body(""" { @@ -82,7 +82,7 @@ void noScopesPost() { @Test void writeScopes() { given() - .headers(API_KEY, "222") + .headers(API_KEY_HEADER, "222") .contentType(APPLICATION_JSON) .body("{\"name\": \"Mango\", \"price\": 2.79}") .when()