From 3b3917ef74903420b4b950ba2236cbbc15964f3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cenk=20K=C4=B1l=C4=B1=C3=A7?= Date: Mon, 29 Apr 2024 11:09:03 +0200 Subject: [PATCH 1/8] feat(auth): add ability to send an authentication header to external service --- .../configuration/authentifications/external.md | 15 ++++++++++++++- .../akhq/security/claim/RestApiClaimProvider.java | 4 +++- .../security/claim/RestApiClaimProviderTest.java | 9 +++++++-- src/test/resources/application-rest-api.yml | 5 ++++- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/docs/docs/configuration/authentifications/external.md b/docs/docs/configuration/authentifications/external.md index 475ce8aa9..f9ba57a80 100644 --- a/docs/docs/configuration/authentifications/external.md +++ b/docs/docs/configuration/authentifications/external.md @@ -76,6 +76,19 @@ and expect the following JSON as response : } } ```` + +If you want to send a static authentication token to the external service where it might be public, you can extend the configuration for the rest interface as follows: +````yaml +akhq: + security: + rest: + enabled: true + url: https://external.service/get-roles-and-attributes + token: + header: "Authorization" + value: Bearer your-token +```` + ::: warning The response must contain the `Content-Type: application/json` header to prevent any issue when reading the response. ::: @@ -128,4 +141,4 @@ public class ClaimResponse { private List connectsFilterRegexp; private List consumerGroupsFilterRegexp; } -```` +```` \ No newline at end of file diff --git a/src/main/java/org/akhq/security/claim/RestApiClaimProvider.java b/src/main/java/org/akhq/security/claim/RestApiClaimProvider.java index 096b8b2c2..34292aea2 100644 --- a/src/main/java/org/akhq/security/claim/RestApiClaimProvider.java +++ b/src/main/java/org/akhq/security/claim/RestApiClaimProvider.java @@ -4,6 +4,7 @@ import io.micronaut.context.annotation.Requires; import io.micronaut.core.util.StringUtils; import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Header; import io.micronaut.http.annotation.Post; import io.micronaut.http.client.annotation.Client; import io.micronaut.scheduling.TaskExecutors; @@ -16,8 +17,9 @@ @Requires(property = "akhq.security.rest.enabled", value = StringUtils.TRUE) @Client("${akhq.security.rest.url}") @ExecuteOn(TaskExecutors.BLOCKING) +@Header(name = "${akhq.security.rest.token-header.name}", value = "${akhq.security.rest.token-header.value}") public interface RestApiClaimProvider extends ClaimProvider { @Post @Override ClaimResponse generateClaim(@Body ClaimRequest request); -} +} \ No newline at end of file diff --git a/src/test/java/org/akhq/security/claim/RestApiClaimProviderTest.java b/src/test/java/org/akhq/security/claim/RestApiClaimProviderTest.java index 653ead84a..da4785aa4 100644 --- a/src/test/java/org/akhq/security/claim/RestApiClaimProviderTest.java +++ b/src/test/java/org/akhq/security/claim/RestApiClaimProviderTest.java @@ -14,6 +14,7 @@ import io.micronaut.http.annotation.Body; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Filter; +import io.micronaut.http.annotation.Header; import io.micronaut.http.annotation.Post; import io.micronaut.http.client.annotation.Client; import io.micronaut.http.filter.HttpServerFilter; @@ -33,6 +34,7 @@ import java.util.Base64; import java.util.List; import java.util.Map; +import java.util.Optional; import jakarta.annotation.security.PermitAll; import static org.hamcrest.MatcherAssert.assertThat; @@ -90,7 +92,11 @@ public Publisher> doFilter(HttpRequest request, Server @Controller("/external-mock") static class RestApiExternalService { @Post - String generateClaim(@Body ClaimRequest request) { + String generateClaim(@Body ClaimRequest request, @Header("X-Custom-Authentication") Optional authHeader) { + if (authHeader.isEmpty() || !"Bearer custom-authentication".equals(authHeader)) { + throw new RuntimeException("Invalid custom authentication header."); + } + return "{\n" + " \"groups\" : {" + @@ -104,4 +110,3 @@ String generateClaim(@Body ClaimRequest request) { } } } - diff --git a/src/test/resources/application-rest-api.yml b/src/test/resources/application-rest-api.yml index 439d698c5..064c6d7d5 100644 --- a/src/test/resources/application-rest-api.yml +++ b/src/test/resources/application-rest-api.yml @@ -34,4 +34,7 @@ akhq: - admin rest: enabled: true - url: /external-mock \ No newline at end of file + token-header: + name: X-Custom-Authentication + value: Bearer custom-authentication + url: /external-mock From badf2f311ccc47639310351c65796531446aef96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cenk=20K=C4=B1l=C4=B1=C3=A7?= Date: Thu, 2 May 2024 14:40:17 +0200 Subject: [PATCH 2/8] fix(auth): use key,value pairs for external authentication --- .gitignore | 4 ++- .../authentifications/external.md | 6 ++-- .../security/claim/RestApiClaimProvider.java | 5 ++- .../claim/RestApiClaimProviderFilter.java | 35 +++++++++++++++++++ src/test/resources/application-rest-api.yml | 8 ++--- 5 files changed, 47 insertions(+), 11 deletions(-) create mode 100644 src/main/java/org/akhq/security/claim/RestApiClaimProviderFilter.java diff --git a/.gitignore b/.gitignore index 7ce562ca0..dc1490564 100644 --- a/.gitignore +++ b/.gitignore @@ -52,10 +52,12 @@ docker-compose.override.yml helm/akhq/README.md helm/akhq/LICENSE - ## Docs docs/node_modules docs/.vuepress/.temp docs/.vuepress/.cache docs/.vuepress/public/contributors.html docs/.vuepress/dist + +/.factorypath +/client/.settings \ No newline at end of file diff --git a/docs/docs/configuration/authentifications/external.md b/docs/docs/configuration/authentifications/external.md index f9ba57a80..be1ac5522 100644 --- a/docs/docs/configuration/authentifications/external.md +++ b/docs/docs/configuration/authentifications/external.md @@ -84,9 +84,9 @@ akhq: rest: enabled: true url: https://external.service/get-roles-and-attributes - token: - header: "Authorization" - value: Bearer your-token + headers: + - name: Authorization + value: Bearer your-token ```` ::: warning diff --git a/src/main/java/org/akhq/security/claim/RestApiClaimProvider.java b/src/main/java/org/akhq/security/claim/RestApiClaimProvider.java index 34292aea2..999740fd2 100644 --- a/src/main/java/org/akhq/security/claim/RestApiClaimProvider.java +++ b/src/main/java/org/akhq/security/claim/RestApiClaimProvider.java @@ -4,11 +4,11 @@ import io.micronaut.context.annotation.Requires; import io.micronaut.core.util.StringUtils; import io.micronaut.http.annotation.Body; -import io.micronaut.http.annotation.Header; import io.micronaut.http.annotation.Post; import io.micronaut.http.client.annotation.Client; import io.micronaut.scheduling.TaskExecutors; import io.micronaut.scheduling.annotation.ExecuteOn; + import org.akhq.models.security.ClaimProvider; import org.akhq.models.security.ClaimRequest; import org.akhq.models.security.ClaimResponse; @@ -17,9 +17,8 @@ @Requires(property = "akhq.security.rest.enabled", value = StringUtils.TRUE) @Client("${akhq.security.rest.url}") @ExecuteOn(TaskExecutors.BLOCKING) -@Header(name = "${akhq.security.rest.token-header.name}", value = "${akhq.security.rest.token-header.value}") public interface RestApiClaimProvider extends ClaimProvider { @Post @Override ClaimResponse generateClaim(@Body ClaimRequest request); -} \ No newline at end of file +} diff --git a/src/main/java/org/akhq/security/claim/RestApiClaimProviderFilter.java b/src/main/java/org/akhq/security/claim/RestApiClaimProviderFilter.java new file mode 100644 index 000000000..20508e605 --- /dev/null +++ b/src/main/java/org/akhq/security/claim/RestApiClaimProviderFilter.java @@ -0,0 +1,35 @@ +package org.akhq.security.claim; + +import java.util.List; +import java.util.Map; + +import org.reactivestreams.Publisher; + +import edu.umd.cs.findbugs.annotations.Nullable; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.util.StringUtils; +import io.micronaut.context.annotation.Value; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.annotation.ClientFilter; +import io.micronaut.http.filter.ClientFilterChain; +import io.micronaut.http.filter.HttpClientFilter; + +@Requires(property = "akhq.security.rest.enabled", value = StringUtils.TRUE) +@Requires(property = "akhq.security.rest.headers") +@ClientFilter("${akhq.security.rest.url}/**") +public class RestApiClaimProviderFilter implements HttpClientFilter { + private static final String HEADER_KEY = "name"; + private static final String HEADER_VALUE = "value"; + + @Nullable + @Value("${akhq.security.rest.headers}") + private List> headers; + + @Override + public Publisher> doFilter(MutableHttpRequest request, ClientFilterChain chain) { + this.headers.forEach(header -> request.header(header.get(HEADER_KEY), header.get(HEADER_VALUE))); + + return chain.proceed(request); + } +} \ No newline at end of file diff --git a/src/test/resources/application-rest-api.yml b/src/test/resources/application-rest-api.yml index 064c6d7d5..377dcfedd 100644 --- a/src/test/resources/application-rest-api.yml +++ b/src/test/resources/application-rest-api.yml @@ -34,7 +34,7 @@ akhq: - admin rest: enabled: true - token-header: - name: X-Custom-Authentication - value: Bearer custom-authentication - url: /external-mock + headers: + - name: X-Custom-Authentication + value: Bearer custom-authentication + url: /external-mock \ No newline at end of file From ff58014d9317cb7c9e7003a3d4c383ed106e12a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cenk=20K=C4=B1l=C4=B1=C3=A7?= Date: Thu, 2 May 2024 15:11:11 +0200 Subject: [PATCH 3/8] fix(auth): use client filter --- .../claim/RestApiClaimProviderFilter.java | 20 ++++++------------- src/test/resources/application-rest-api.yml | 2 +- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/akhq/security/claim/RestApiClaimProviderFilter.java b/src/main/java/org/akhq/security/claim/RestApiClaimProviderFilter.java index 20508e605..baf80b682 100644 --- a/src/main/java/org/akhq/security/claim/RestApiClaimProviderFilter.java +++ b/src/main/java/org/akhq/security/claim/RestApiClaimProviderFilter.java @@ -3,33 +3,25 @@ import java.util.List; import java.util.Map; -import org.reactivestreams.Publisher; - -import edu.umd.cs.findbugs.annotations.Nullable; import io.micronaut.context.annotation.Requires; import io.micronaut.core.util.StringUtils; import io.micronaut.context.annotation.Value; -import io.micronaut.http.HttpResponse; import io.micronaut.http.MutableHttpRequest; import io.micronaut.http.annotation.ClientFilter; -import io.micronaut.http.filter.ClientFilterChain; -import io.micronaut.http.filter.HttpClientFilter; +import io.micronaut.http.annotation.RequestFilter; @Requires(property = "akhq.security.rest.enabled", value = StringUtils.TRUE) @Requires(property = "akhq.security.rest.headers") -@ClientFilter("${akhq.security.rest.url}/**") -public class RestApiClaimProviderFilter implements HttpClientFilter { +@ClientFilter("${akhq.security.rest.url}") +public class RestApiClaimProviderFilter { private static final String HEADER_KEY = "name"; private static final String HEADER_VALUE = "value"; - @Nullable @Value("${akhq.security.rest.headers}") private List> headers; - @Override - public Publisher> doFilter(MutableHttpRequest request, ClientFilterChain chain) { + @RequestFilter + public void filter(MutableHttpRequest request) { this.headers.forEach(header -> request.header(header.get(HEADER_KEY), header.get(HEADER_VALUE))); - - return chain.proceed(request); } -} \ No newline at end of file +} diff --git a/src/test/resources/application-rest-api.yml b/src/test/resources/application-rest-api.yml index 377dcfedd..e0026ff13 100644 --- a/src/test/resources/application-rest-api.yml +++ b/src/test/resources/application-rest-api.yml @@ -34,7 +34,7 @@ akhq: - admin rest: enabled: true + url: /external-mock headers: - name: X-Custom-Authentication value: Bearer custom-authentication - url: /external-mock \ No newline at end of file From ffbffd0b70ff0f29d0427ca114e18d0d3f9a0ba5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cenk=20K=C4=B1l=C4=B1=C3=A7?= Date: Fri, 3 May 2024 10:11:47 +0200 Subject: [PATCH 4/8] fix(auth): update the rest api claim filter to be compatible --- .../claim/RestApiClaimProviderFilter.java | 14 +++++++++--- .../claim/RestApiClaimProviderTest.java | 22 +++++++++---------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/akhq/security/claim/RestApiClaimProviderFilter.java b/src/main/java/org/akhq/security/claim/RestApiClaimProviderFilter.java index baf80b682..cbdef21e4 100644 --- a/src/main/java/org/akhq/security/claim/RestApiClaimProviderFilter.java +++ b/src/main/java/org/akhq/security/claim/RestApiClaimProviderFilter.java @@ -3,25 +3,33 @@ import java.util.List; import java.util.Map; +import io.micronaut.context.annotation.Primary; +import io.micronaut.context.annotation.Property; import io.micronaut.context.annotation.Requires; import io.micronaut.core.util.StringUtils; -import io.micronaut.context.annotation.Value; import io.micronaut.http.MutableHttpRequest; import io.micronaut.http.annotation.ClientFilter; import io.micronaut.http.annotation.RequestFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +@Primary @Requires(property = "akhq.security.rest.enabled", value = StringUtils.TRUE) @Requires(property = "akhq.security.rest.headers") @ClientFilter("${akhq.security.rest.url}") public class RestApiClaimProviderFilter { + private static final Logger LOG = LoggerFactory.getLogger(RestApiClaimProviderFilter.class); + private static final String HEADER_KEY = "name"; private static final String HEADER_VALUE = "value"; - @Value("${akhq.security.rest.headers}") + @Property(name = "akhq.security.rest.headers") private List> headers; @RequestFilter public void filter(MutableHttpRequest request) { - this.headers.forEach(header -> request.header(header.get(HEADER_KEY), header.get(HEADER_VALUE))); + LOG.trace("Modifying outgoing authentication request."); + + headers.forEach(header -> request.header(header.get(HEADER_KEY), header.get(HEADER_VALUE))); } } diff --git a/src/test/java/org/akhq/security/claim/RestApiClaimProviderTest.java b/src/test/java/org/akhq/security/claim/RestApiClaimProviderTest.java index da4785aa4..75caade5b 100644 --- a/src/test/java/org/akhq/security/claim/RestApiClaimProviderTest.java +++ b/src/test/java/org/akhq/security/claim/RestApiClaimProviderTest.java @@ -92,21 +92,21 @@ public Publisher> doFilter(HttpRequest request, Server @Controller("/external-mock") static class RestApiExternalService { @Post - String generateClaim(@Body ClaimRequest request, @Header("X-Custom-Authentication") Optional authHeader) { - if (authHeader.isEmpty() || !"Bearer custom-authentication".equals(authHeader)) { + String generateClaim(@Body ClaimRequest request, + @Header("X-Custom-Authentication") Optional authHeader) { + if (authHeader.isEmpty() || !authHeader.get().equals("Bearer custom-authentication")) { throw new RuntimeException("Invalid custom authentication header."); } - return - "{\n" + + return "{\n" + " \"groups\" : {" + - "\"limited\": [{" + - "\"role\": \"topic-read\"," + - "\"patterns\": [\"user.*\"]," + - "\"clusters\": [\"pub.*\"]" + - "}]" + - "}"+ - "}"; + "\"limited\": [{" + + "\"role\": \"topic-read\"," + + "\"patterns\": [\"user.*\"]," + + "\"clusters\": [\"pub.*\"]" + + "}]" + + "}" + + "}"; } } } From 3c9e84d588dcd833d38d5453845322fd44cf5c20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cenk=20K=C4=B1l=C4=B1=C3=A7?= Date: Mon, 27 May 2024 09:33:11 +0200 Subject: [PATCH 5/8] revert: old indenting --- .../akhq/security/claim/RestApiClaimProviderTest.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/java/org/akhq/security/claim/RestApiClaimProviderTest.java b/src/test/java/org/akhq/security/claim/RestApiClaimProviderTest.java index 75caade5b..2738d808c 100644 --- a/src/test/java/org/akhq/security/claim/RestApiClaimProviderTest.java +++ b/src/test/java/org/akhq/security/claim/RestApiClaimProviderTest.java @@ -100,11 +100,11 @@ String generateClaim(@Body ClaimRequest request, return "{\n" + " \"groups\" : {" + - "\"limited\": [{" + - "\"role\": \"topic-read\"," + - "\"patterns\": [\"user.*\"]," + - "\"clusters\": [\"pub.*\"]" + - "}]" + + "\"limited\": [{" + + "\"role\": \"topic-read\"," + + "\"patterns\": [\"user.*\"]," + + "\"clusters\": [\"pub.*\"]" + + "}]" + "}" + "}"; } From 951f5283b7bef7ea6888db1e1418763d8e514c15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cenk=20K=C4=B1l=C4=B1=C3=A7?= Date: Mon, 27 May 2024 09:34:14 +0200 Subject: [PATCH 6/8] revert: spacing --- .../org/akhq/security/claim/RestApiClaimProviderTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/akhq/security/claim/RestApiClaimProviderTest.java b/src/test/java/org/akhq/security/claim/RestApiClaimProviderTest.java index 2738d808c..dfcfa0fcf 100644 --- a/src/test/java/org/akhq/security/claim/RestApiClaimProviderTest.java +++ b/src/test/java/org/akhq/security/claim/RestApiClaimProviderTest.java @@ -98,7 +98,8 @@ String generateClaim(@Body ClaimRequest request, throw new RuntimeException("Invalid custom authentication header."); } - return "{\n" + + return + "{\n" + " \"groups\" : {" + "\"limited\": [{" + "\"role\": \"topic-read\"," + @@ -106,7 +107,7 @@ String generateClaim(@Body ClaimRequest request, "\"clusters\": [\"pub.*\"]" + "}]" + "}" + - "}"; + "}"; } } } From 3b1ba3f621af7376fa4a7e16bed8a7f8082435b9 Mon Sep 17 00:00:00 2001 From: Mohamed Hammad Date: Mon, 3 Jun 2024 16:24:22 +0200 Subject: [PATCH 7/8] fix: changing the filter to implement HttpClient --- build.gradle | 9 ++++++++ .../claim/RestApiClaimProviderFilter.java | 22 +++++++++++-------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index 852cc8c6c..8ba048ad5 100644 --- a/build.gradle +++ b/build.gradle @@ -145,6 +145,15 @@ dependencies { implementation group: "com.google.protobuf", name: "protobuf-java", version: '3.25.0' implementation group: "com.google.protobuf", name: "protobuf-java-util", version: '3.25.0' + //cenk is testing + implementation("io.micronaut:micronaut-http-client") + implementation("io.micronaut:micronaut-inject") + implementation("io.micronaut:micronaut-runtime") + implementation("jakarta.inject:jakarta.inject-api") + implementation("org.reactivestreams:reactive-streams") + + + // Password hashing implementation group: "org.mindrot", name: "jbcrypt", version: "0.4" diff --git a/src/main/java/org/akhq/security/claim/RestApiClaimProviderFilter.java b/src/main/java/org/akhq/security/claim/RestApiClaimProviderFilter.java index cbdef21e4..45e51b476 100644 --- a/src/main/java/org/akhq/security/claim/RestApiClaimProviderFilter.java +++ b/src/main/java/org/akhq/security/claim/RestApiClaimProviderFilter.java @@ -1,23 +1,27 @@ package org.akhq.security.claim; -import java.util.List; -import java.util.Map; - import io.micronaut.context.annotation.Primary; import io.micronaut.context.annotation.Property; import io.micronaut.context.annotation.Requires; import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpResponse; import io.micronaut.http.MutableHttpRequest; import io.micronaut.http.annotation.ClientFilter; -import io.micronaut.http.annotation.RequestFilter; +import io.micronaut.http.filter.ClientFilterChain; +import io.micronaut.http.filter.HttpClientFilter; +import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Map; @Primary @Requires(property = "akhq.security.rest.enabled", value = StringUtils.TRUE) @Requires(property = "akhq.security.rest.headers") @ClientFilter("${akhq.security.rest.url}") -public class RestApiClaimProviderFilter { +public class RestApiClaimProviderFilter implements HttpClientFilter { private static final Logger LOG = LoggerFactory.getLogger(RestApiClaimProviderFilter.class); private static final String HEADER_KEY = "name"; @@ -26,10 +30,10 @@ public class RestApiClaimProviderFilter { @Property(name = "akhq.security.rest.headers") private List> headers; - @RequestFilter - public void filter(MutableHttpRequest request) { - LOG.trace("Modifying outgoing authentication request."); - + @Override + public Publisher> doFilter(MutableHttpRequest request, ClientFilterChain chain) { + LOG.trace("Modifying outgoing authentication request. from muttable"); headers.forEach(header -> request.header(header.get(HEADER_KEY), header.get(HEADER_VALUE))); + return Mono.from(chain.proceed(request)); } } From 1aae1c047466e5a27e0699fb9def84a16e72aed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cenk=20K=C4=B1l=C4=B1=C3=A7?= Date: Mon, 3 Jun 2024 16:36:07 +0200 Subject: [PATCH 8/8] chore(auth): update the log message --- .../org/akhq/security/claim/RestApiClaimProviderFilter.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/akhq/security/claim/RestApiClaimProviderFilter.java b/src/main/java/org/akhq/security/claim/RestApiClaimProviderFilter.java index 45e51b476..33267c668 100644 --- a/src/main/java/org/akhq/security/claim/RestApiClaimProviderFilter.java +++ b/src/main/java/org/akhq/security/claim/RestApiClaimProviderFilter.java @@ -32,8 +32,9 @@ public class RestApiClaimProviderFilter implements HttpClientFilter { @Override public Publisher> doFilter(MutableHttpRequest request, ClientFilterChain chain) { - LOG.trace("Modifying outgoing authentication request. from muttable"); + LOG.trace("Modifying outgoing authentication request."); headers.forEach(header -> request.header(header.get(HEADER_KEY), header.get(HEADER_VALUE))); + return Mono.from(chain.proceed(request)); } -} +} \ No newline at end of file