From 3b6e6cb2b654e8bc484b3838d81c5049fc6cb411 Mon Sep 17 00:00:00 2001 From: Torben Burchgart Date: Mon, 25 Nov 2024 16:11:48 +0100 Subject: [PATCH 1/3] Add OpenAPI 3.1 references test class and update router setup - Create a new test class `OpenAPI31ReferencesTest` for validating OpenAPI 3.1 references. - Implement setup method to initialize the router and load OpenAPI specs. - Update test cases to handle requests for both `/pets` and `/users` endpoints. - Modify the existing OpenAPI specification to include server information in the YAML configuration. --- .../serviceproxy/OpenAPI31ReferencesTest.java | 58 +++++++++++++++++++ .../openapi/serviceproxy/OpenAPI31Test.java | 3 +- .../specs/oas31/request-reference.yaml | 2 + 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 core/src/test/java/com/predic8/membrane/core/openapi/serviceproxy/OpenAPI31ReferencesTest.java diff --git a/core/src/test/java/com/predic8/membrane/core/openapi/serviceproxy/OpenAPI31ReferencesTest.java b/core/src/test/java/com/predic8/membrane/core/openapi/serviceproxy/OpenAPI31ReferencesTest.java new file mode 100644 index 0000000000..9086a12d0e --- /dev/null +++ b/core/src/test/java/com/predic8/membrane/core/openapi/serviceproxy/OpenAPI31ReferencesTest.java @@ -0,0 +1,58 @@ +/* + * Copyright 2023 predic8 GmbH, www.predic8.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.predic8.membrane.core.openapi.serviceproxy; + +import com.predic8.membrane.core.Router; +import com.predic8.membrane.core.exchange.Exchange; +import com.predic8.membrane.core.http.Request; +import com.predic8.membrane.core.interceptor.Outcome; +import com.predic8.membrane.core.util.URIFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static com.predic8.membrane.core.openapi.util.TestUtils.createProxy; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class OpenAPI31ReferencesTest { + + OpenAPIInterceptor interceptor; + + OpenAPISpec referencesTest; + + Exchange exc = new Exchange(null); + + @BeforeEach + public void setUp() throws Exception { + Router router = new Router(); + router.setUriFactory(new URIFactory()); + + referencesTest = new OpenAPISpec(); + referencesTest.location = "src/test/resources/openapi/specs/oas31/request-reference.yaml"; + + exc.setRequest(new Request.Builder().method("GET").build()); + + interceptor = new OpenAPIInterceptor(createProxy(router, referencesTest), router); + interceptor.init(router); + } + + @Test + void simple() throws Exception { + exc.getRequest().setUri("/users"); + System.out.println(exc); + assertEquals(Outcome.RETURN, interceptor.handleRequest(exc)); + } +} diff --git a/core/src/test/java/com/predic8/membrane/core/openapi/serviceproxy/OpenAPI31Test.java b/core/src/test/java/com/predic8/membrane/core/openapi/serviceproxy/OpenAPI31Test.java index e69e46de13..1c0ff4c46c 100644 --- a/core/src/test/java/com/predic8/membrane/core/openapi/serviceproxy/OpenAPI31Test.java +++ b/core/src/test/java/com/predic8/membrane/core/openapi/serviceproxy/OpenAPI31Test.java @@ -51,6 +51,7 @@ public void setUp() throws Exception { @Test void simple() throws Exception { exc.getRequest().setUri("/pets"); - assertEquals(Outcome.RETURN, interceptor.handleRequest(exc)); + Outcome actual = interceptor.handleRequest(exc); + assertEquals(Outcome.RETURN, actual); } } diff --git a/core/src/test/resources/openapi/specs/oas31/request-reference.yaml b/core/src/test/resources/openapi/specs/oas31/request-reference.yaml index 7134063963..df5da48ccf 100644 --- a/core/src/test/resources/openapi/specs/oas31/request-reference.yaml +++ b/core/src/test/resources/openapi/specs/oas31/request-reference.yaml @@ -2,6 +2,8 @@ openapi: 3.1.0 info: title: Demo version: 1.0.0 +servers: + - url: https://localhost:3000 paths: /users: $ref: '/openapi/specs/oas31/request-schema.yaml#/paths/~1users' \ No newline at end of file From 3b4510bc9cb92e2de2130118104017596d50ea45 Mon Sep 17 00:00:00 2001 From: Torben Burchgart Date: Mon, 16 Dec 2024 16:00:21 +0100 Subject: [PATCH 2/3] Added more in-depth documentation and examples for ApiKeysInterceptor. --- README.md | 21 ++++++ .../apikey/ApiKeysInterceptor.java | 2 +- .../oauth2/membrane/client/service-proxy.sh | 0 .../examples/security/api-key/rbac/README.md | 64 +++++++++++++++++++ .../api-key/{simple => rbac}/demo-keys.txt | 0 .../security/api-key/rbac/proxies.xml | 45 +++++++++++++ .../security/api-key/rbac/requests.http | 15 +++++ .../security/api-key/rbac/service-proxy.bat | 18 ++++++ .../security/api-key/rbac/service-proxy.sh | 35 ++++++++++ .../security/api-key/simple/README.md | 59 ++--------------- .../security/api-key/simple/proxies.xml | 46 +++---------- .../security/api-key/simple/requests.http | 21 +----- 12 files changed, 215 insertions(+), 111 deletions(-) mode change 100644 => 100755 distribution/examples/oauth2/membrane/client/service-proxy.sh create mode 100644 distribution/examples/security/api-key/rbac/README.md rename distribution/examples/security/api-key/{simple => rbac}/demo-keys.txt (100%) create mode 100644 distribution/examples/security/api-key/rbac/proxies.xml create mode 100644 distribution/examples/security/api-key/rbac/requests.http create mode 100644 distribution/examples/security/api-key/rbac/service-proxy.bat create mode 100755 distribution/examples/security/api-key/rbac/service-proxy.sh diff --git a/README.md b/README.md index 64ecfff756..f49a9a974a 100644 --- a/README.md +++ b/README.md @@ -389,6 +389,27 @@ Also try the [Groovy](distribution/examples/groovy) and [Javascript example](dis Membrane offers lots of security features to protect backend servers. +## API Keys + +Secure any API using a simple API key configuration like this: + +```xml + + + + + + + + Hidden API + + +``` + +This will fetch the API key from the "X-Api-Key" header if present. +On incorrect key entry or missing key, access is denied and an error response is sent. +For more complex configurations using RBAC and file-based key stores see: [API Key Plugin Examples](./distribution/examples/security/api-key/rbac/README.md) + ## JSON Web Tokens The API below only allows requests with valid tokens from Microsoft's Azure AD. You can also use the JWT validator for other identity providers. 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 d56eda2ff2..6169121305 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 @@ -77,7 +77,7 @@ public Outcome handleRequest(Exchange exc) { .statusCode(401) .addSubType(TYPE_4XX) .title(TITLE_4XX) - .detail("Tried to access apiKey protected resource without key.") + .detail("Tried to access API key protected resource without key.") .build()); return RETURN; } diff --git a/distribution/examples/oauth2/membrane/client/service-proxy.sh b/distribution/examples/oauth2/membrane/client/service-proxy.sh old mode 100644 new mode 100755 diff --git a/distribution/examples/security/api-key/rbac/README.md b/distribution/examples/security/api-key/rbac/README.md new file mode 100644 index 0000000000..995eef59c9 --- /dev/null +++ b/distribution/examples/security/api-key/rbac/README.md @@ -0,0 +1,64 @@ +# API Key Authentication and Authorization + +Secure endpoints using API keys combined with role-based access control (RBAC). + +## Running the Sample +***Note:*** *The requests are also available in the requests.http file.* + +1. **Navigate** to the `examples/security/api-key/rbac` directory. +2. **Start** the API Gateway by executing `service-proxy.sh` (Linux/Mac) or `service-proxy.bat` (Windows). +3**Test Optional API Key with RBAC**: + - Access with a non-admin scope key to receive limited access: + ``` + curl http://localhost:3000 -H "X-Key: 123456789" -v + ``` + - Use an admin scope key for admin access: + ``` + curl http://localhost:3000 -H "X-Key: key_321_abc" -v + ``` + - Make a request without any key to see default behavior: + ``` + curl http://localhost:3000 -v + ``` + +## Understanding the Configuration + +### Key Stores +Key stores maintain API keys and their corresponding scopes through various methods. In this instance, we use the ApiKeyFileStore, which holds the keys within the local file system. File stores can either be globally registered for all apiKey plugins via a Spring Bean declaration or configured locally by embedding them directly within the apiKey plugin: + +```xml + + + +``` +**OR** +```xml + + ... + + +``` +### Optional API Key with RBAC +The configuration for port `3000` involves optional API key authentication with additional checks for user roles or scopes, as well as employing a custom header for API key extraction. +By using the conditional "if" plugin, we can check and validate provided scopes using built-in functions. + +```xml + + + + + + + + + + + + + + + + + + +``` diff --git a/distribution/examples/security/api-key/simple/demo-keys.txt b/distribution/examples/security/api-key/rbac/demo-keys.txt similarity index 100% rename from distribution/examples/security/api-key/simple/demo-keys.txt rename to distribution/examples/security/api-key/rbac/demo-keys.txt diff --git a/distribution/examples/security/api-key/rbac/proxies.xml b/distribution/examples/security/api-key/rbac/proxies.xml new file mode 100644 index 0000000000..4a69fa22c4 --- /dev/null +++ b/distribution/examples/security/api-key/rbac/proxies.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/distribution/examples/security/api-key/rbac/requests.http b/distribution/examples/security/api-key/rbac/requests.http new file mode 100644 index 0000000000..c3560f2136 --- /dev/null +++ b/distribution/examples/security/api-key/rbac/requests.http @@ -0,0 +1,15 @@ +# Branch depending on scopes +# See demo-keys.txt +GET / HTTP/1.1 +Host: localhost:3000 +X-Key: 123456789 +### + +GET / HTTP/1.1 +Host: localhost:3000 +X-Key: key_321_abc +### + +# Access will fail, no valid API key provided +GET / HTTP/1.1 +Host: localhost:3000 diff --git a/distribution/examples/security/api-key/rbac/service-proxy.bat b/distribution/examples/security/api-key/rbac/service-proxy.bat new file mode 100644 index 0000000000..a9ff5b820d --- /dev/null +++ b/distribution/examples/security/api-key/rbac/service-proxy.bat @@ -0,0 +1,18 @@ +@echo off +if not "%MEMBRANE_HOME%" == "" goto homeSet +set "MEMBRANE_HOME=%cd%\..\..\..\.." +echo "%MEMBRANE_HOME%" +if exist "%MEMBRANE_HOME%\service-proxy.bat" goto homeOk + +:homeSet +if exist "%MEMBRANE_HOME%\service-proxy.bat" goto homeOk +echo Please set the MEMBRANE_HOME environment variable to point to +echo the directory where you have extracted the Membrane software. +exit + +:homeOk +set "CLASSPATH=%MEMBRANE_HOME%" +set "CLASSPATH=%MEMBRANE_HOME%/conf" +set "CLASSPATH=%CLASSPATH%;%MEMBRANE_HOME%/starter.jar" +echo Membrane Router running... +java -classpath "%CLASSPATH%" com.predic8.membrane.core.Starter -c proxies.xml diff --git a/distribution/examples/security/api-key/rbac/service-proxy.sh b/distribution/examples/security/api-key/rbac/service-proxy.sh new file mode 100755 index 0000000000..a27e7bb8b2 --- /dev/null +++ b/distribution/examples/security/api-key/rbac/service-proxy.sh @@ -0,0 +1,35 @@ +#!/bin/bash +homeSet() { + echo "MEMBRANE_HOME variable is now set" + CLASSPATH="$MEMBRANE_HOME/conf" + CLASSPATH="$CLASSPATH:$MEMBRANE_HOME/starter.jar" + export CLASSPATH + echo Membrane Router running... + java -classpath "$CLASSPATH" com.predic8.membrane.core.Starter -c proxies.xml + +} + +terminate() { + echo "Starting of Membrane Router failed." + echo "Please execute this script from the appropriate subfolder of MEMBRANE_HOME/examples/" + +} + +homeNotSet() { + echo "MEMBRANE_HOME variable is not set" + + if [ -f "`pwd`/../../../../starter.jar" ] + then + export MEMBRANE_HOME="`pwd`/../../../.." + homeSet + else + terminate + fi +} + + +if [ "$MEMBRANE_HOME" ] + then homeSet + else homeNotSet +fi + diff --git a/distribution/examples/security/api-key/simple/README.md b/distribution/examples/security/api-key/simple/README.md index 8a1d506a8e..f7fe032bf3 100644 --- a/distribution/examples/security/api-key/simple/README.md +++ b/distribution/examples/security/api-key/simple/README.md @@ -1,6 +1,6 @@ # API Key Authentication and Authorization -Secure endpoints using API keys combined with role-based access control (RBAC). +Secure endpoints using API keys. ## Running the Sample ***Note:*** *The requests are also available in the requests.http file.* @@ -24,39 +24,11 @@ Secure endpoints using API keys combined with role-based access control (RBAC). ``` curl http://localhost:2000/?api-key=P8MBR -v ``` -4. **Test Optional API Key with RBAC**: - - Access with a non-admin scope key to receive limited access: - ``` - curl http://localhost:3000 -H "X-Key: 123456789" -v - ``` - - Use an admin scope key for admin access: - ``` - curl http://localhost:3000 -H "X-Key: key_321_abc" -v - ``` - - Make a request without any key to see default behavior: - ``` - curl http://localhost:3000 -v - ``` ## Understanding the Configuration -### File Stores -File stores maintain API keys and their corresponding scopes through various methods. In this instance, we use the ApiKeyFileStore, which holds the keys within the local file system. File stores can either be globally registered for all apiKey plugins via a Spring Bean declaration or configured locally by embedding them directly within the apiKey plugin: - -```xml - - - -``` -**OR** -```xml - - ... - - -``` - -**DECLARING API KEYS IN THE CONFIG** +### Key Stores +Key stores maintain API keys and their corresponding scopes through various methods. In this instance, we use the simple in-config "keys"-store, which holds the keys inside the `proxies.xml` file.: ```xml @@ -69,7 +41,6 @@ File stores maintain API keys and their corresponding scopes through various met ``` - ### Mandatory API Key Authentication This part of the configuration sets up an API on port `2000`, where providing an API key is mandatory. The setup allows API keys to be received either as HTTP headers or query parameters. On successful authentication we simply return with the message "Secret Area!". Here we would direct the request to the destination server. @@ -85,26 +56,4 @@ On successful authentication we simply return with the message "Secret Area!". H ``` ### Optional API Key with RBAC -The configuration for port `3000` involves optional API key authentication with additional checks for user roles or scopes, as well as employing a custom header for API key extraction. -By using the conditional "if" plugin, we can check and validate provided scopes using built-in functions. - -```xml - - - - - - - - - - - - - - - - - - -``` +See [API keys with RBAC](./../rbac/README.md) diff --git a/distribution/examples/security/api-key/simple/proxies.xml b/distribution/examples/security/api-key/simple/proxies.xml index d21a48012b..4991442673 100644 --- a/distribution/examples/security/api-key/simple/proxies.xml +++ b/distribution/examples/security/api-key/simple/proxies.xml @@ -4,56 +4,28 @@ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd http://membrane-soa.org/proxies/1/ http://membrane-soa.org/schemas/proxies-1.xsd"> - - - - - + + + + + + - - + Hidden API - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/distribution/examples/security/api-key/simple/requests.http b/distribution/examples/security/api-key/simple/requests.http index a52de0e20b..b882412751 100644 --- a/distribution/examples/security/api-key/simple/requests.http +++ b/distribution/examples/security/api-key/simple/requests.http @@ -12,25 +12,10 @@ X-Api-Key: 98765 # 3. Access granted. A valid API key was provided, we can access this endpoint. GET / HTTP/1.1 Host: localhost:2000 -X-Api-Key: P8MBR +X-Api-Key: demokey123 ### # 4. We can provide the key through query parameters as well. -GET /?api-key=P8MBR HTTP/1.1 +GET /?api-key=demokey123 HTTP/1.1 Host: localhost:2000 -### - -# 5. Branch depending on scopes -# See demo-keys.txt -GET / HTTP/1.1 -Host: localhost:3000 -X-Key: 123456789 -### - -GET / HTTP/1.1 -Host: localhost:3000 -X-Key: key_321_abc -### - -GET / HTTP/1.1 -Host: localhost:3000 +### \ No newline at end of file From bcf7cf6358bc0d5f3c239d1308b27dd48dd081ba Mon Sep 17 00:00:00 2001 From: Torben Burchgart Date: Wed, 18 Dec 2024 13:02:48 +0100 Subject: [PATCH 3/3] Update APIKeyTest with demo key and add APIKeyRBACTest for RBAC validation --- .../examples/ExampleTestsWithoutInternet.java | 1 + .../examples/tests/APIKeyRBACTest.java | 51 +++++++++++++++++++ .../membrane/examples/tests/APIKeyTest.java | 26 +--------- 3 files changed, 54 insertions(+), 24 deletions(-) create mode 100644 distribution/src/test/java/com/predic8/membrane/examples/tests/APIKeyRBACTest.java diff --git a/distribution/src/test/java/com/predic8/membrane/examples/ExampleTestsWithoutInternet.java b/distribution/src/test/java/com/predic8/membrane/examples/ExampleTestsWithoutInternet.java index 6a6c1d3ec9..941649298a 100644 --- a/distribution/src/test/java/com/predic8/membrane/examples/ExampleTestsWithoutInternet.java +++ b/distribution/src/test/java/com/predic8/membrane/examples/ExampleTestsWithoutInternet.java @@ -97,6 +97,7 @@ // Security JsonProtectionTest.class, APIKeyTest.class, + APIKeyRBACTest.class, APIKeyWithOpenAPITest.class, XMLTemplateTest.class, //DefaultConfigAdminConsoleTest.class*/ diff --git a/distribution/src/test/java/com/predic8/membrane/examples/tests/APIKeyRBACTest.java b/distribution/src/test/java/com/predic8/membrane/examples/tests/APIKeyRBACTest.java new file mode 100644 index 0000000000..99c2754193 --- /dev/null +++ b/distribution/src/test/java/com/predic8/membrane/examples/tests/APIKeyRBACTest.java @@ -0,0 +1,51 @@ +/* Copyright 2024 predic8 GmbH, www.predic8.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ +package com.predic8.membrane.examples.tests; + +import com.predic8.membrane.examples.util.AbstractSampleMembraneStartStopTestcase; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.equalTo; + +public class APIKeyRBACTest extends AbstractSampleMembraneStartStopTestcase { + + @Override + protected String getExampleDirName() { + return "security/api-key/rbac"; + } + + @Test + public void normalScope() { + given() + .header("X-Key", "123456789") + .when() + .get("http://localhost:3000") + .then().assertThat() + .statusCode(200) + .body(equalTo("Only for finance or accounting!")); + } + + @Test + public void conditionalScope() { + given() + .header("X-Key", "key_321_abc") + .when() + .get("http://localhost:3000") + .then().assertThat() + .statusCode(200) + .body(equalTo("Only for admins!")); + } +} diff --git a/distribution/src/test/java/com/predic8/membrane/examples/tests/APIKeyTest.java b/distribution/src/test/java/com/predic8/membrane/examples/tests/APIKeyTest.java index 4f5ecb6360..cdff4dc73f 100644 --- a/distribution/src/test/java/com/predic8/membrane/examples/tests/APIKeyTest.java +++ b/distribution/src/test/java/com/predic8/membrane/examples/tests/APIKeyTest.java @@ -48,7 +48,7 @@ public void notAuthorized() { @Test public void successKeyHeader() { given() - .header("X-Api-Key", "P8MBR") + .header("X-Api-Key", "demokey") .when() .get("http://localhost:2000") .then().assertThat() @@ -58,32 +58,10 @@ public void successKeyHeader() { @Test public void successQueryKey() { given() - .queryParam("api-key", "P8MBR") + .queryParam("api-key", "demokey") .when() .get("http://localhost:2000") .then().assertThat() .statusCode(200); } - - @Test - public void normalScope() { - given() - .header("X-Key", "123456789") - .when() - .get("http://localhost:3000") - .then().assertThat() - .statusCode(200) - .body(equalTo("Only for finance or accounting!")); - } - - @Test - public void conditionalScope() { - given() - .header("X-Key", "key_321_abc") - .when() - .get("http://localhost:3000") - .then().assertThat() - .statusCode(200) - .body(equalTo("Only for admins!")); - } }