From b43f52d726b97a363a4137fe09964fae94920ead Mon Sep 17 00:00:00 2001 From: Piotr Przybylski Date: Mon, 16 Aug 2021 20:46:48 +0200 Subject: [PATCH] feat(auth): allow limiting header authentication to list of configured IP addresses (#787) (#793) --- README.md | 12 +++--- application.example.yml | 3 +- .../java/org/akhq/configs/HeaderAuth.java | 3 ++ .../modules/HeaderAuthenticationFetcher.java | 37 +++++++++++++++++++ .../controllers/HeaderAuthControllerTest.java | 22 +++++++++++ .../application-header-ip-disallow.yml | 7 ++++ 6 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 src/test/resources/application-header-ip-disallow.yml diff --git a/README.md b/README.md index a45b396ff..367353c04 100644 --- a/README.md +++ b/README.md @@ -617,7 +617,7 @@ The username field can be any string field, the roles field has to be a JSON arr ### Header configuration (reverse proxy) -To enable Header authentification in the application, you'll have to configure the header that will resolve users & groups: +To enable Header authentication in the application, you'll have to configure the header that will resolve users & groups: ```yaml akhq: @@ -627,6 +627,7 @@ akhq: user-header: x-akhq-user # mandatory (the header name that will contain username) groups-header: x-akhq-group # optional (the header name that will contain groups separated by groups-header-separator) groups-header-separator: , # optional (separator, defaults to ',') + ip-patterns: [0.0.0.0] # optional (Java regular expressions for matching trusted IP addresses, '0.0.0.0' matches all addresses) users: # optional, the users list to allow, if empty we only rely on `groups-header` - username: header-user # username matching the `user-header` value groups: # list of group for current users @@ -636,10 +637,11 @@ akhq: - admin ``` -* The `user-header` is mandatory in order to map the user with `users` list or to display the user on the ui if no `users` is provided. -* The `groups-header` is optional and can be used in order to inject a list of groups for all the users. This list will be merged with `groups` for the current users. -* The `groups-header-separator` is optional and can be used to customize group separator used when parsing `groups-header` header, defaults to `,`. -* The `users` is a list of allowed users. +* `user-header` is mandatory in order to map the user with `users` list or to display the user on the ui if no `users` is provided. +* `groups-header` is optional and can be used in order to inject a list of groups for all the users. This list will be merged with `groups` for the current users. +* `groups-header-separator` is optional and can be used to customize group separator used when parsing `groups-header` header, defaults to `,`. +* `ip-patterns` limits the IP addresses that header authentication will accept, given as a list of Java regular expressions, omit or set to `[0.0.0.0]` to allow all addresses +* `users` is a list of allowed users. ### External roles and attributes mapping diff --git a/application.example.yml b/application.example.yml index 676b17293..d4a5343ff 100644 --- a/application.example.yml +++ b/application.example.yml @@ -14,7 +14,7 @@ micronaut: groups: enabled: true base: "dc=example,dc=com" - # OIDC authentification configuration + # OIDC authentication configuration oauth2: enabled: true clients: @@ -265,6 +265,7 @@ akhq: user-header: x-akhq-user # mandatory (the header name that will contain username) groups-header: x-akhq-group # optional (the header name that will contain groups separated by groups-header-separator) groups-header-separator: , # optional (separator, defaults to ',') + ip-patterns: [127.0.0.*] # optional (Java regular expressions for matching trusted IP addresses, '0.0.0.0' matches all addresses) users: # optional, the users list to allow, if empty we only rely on `groups-header` - username: header-user # username matching the `user-header` value groups: # list of group for current users diff --git a/src/main/java/org/akhq/configs/HeaderAuth.java b/src/main/java/org/akhq/configs/HeaderAuth.java index c90479c2c..4d871fb35 100644 --- a/src/main/java/org/akhq/configs/HeaderAuth.java +++ b/src/main/java/org/akhq/configs/HeaderAuth.java @@ -1,9 +1,11 @@ package org.akhq.configs; import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.security.config.SecurityConfigurationProperties; import lombok.Data; import java.util.ArrayList; +import java.util.Collections; import java.util.List; @Data @@ -13,6 +15,7 @@ public class HeaderAuth { String groupsHeader; String groupsHeaderSeparator = ","; List users; + List ipPatterns = Collections.singletonList(SecurityConfigurationProperties.ANYWHERE); @Data public static class Users { diff --git a/src/main/java/org/akhq/modules/HeaderAuthenticationFetcher.java b/src/main/java/org/akhq/modules/HeaderAuthenticationFetcher.java index dd25c5ba0..2793a645c 100644 --- a/src/main/java/org/akhq/modules/HeaderAuthenticationFetcher.java +++ b/src/main/java/org/akhq/modules/HeaderAuthenticationFetcher.java @@ -7,6 +7,7 @@ import io.micronaut.security.authentication.AuthenticationUserDetailsAdapter; import io.micronaut.security.authentication.Authenticator; import io.micronaut.security.authentication.UserDetails; +import io.micronaut.security.config.SecurityConfigurationProperties; import io.micronaut.security.filters.AuthenticationFetcher; import io.micronaut.security.token.config.TokenConfiguration; import io.reactivex.Flowable; @@ -15,11 +16,14 @@ import org.akhq.utils.ClaimProvider; import org.reactivestreams.Publisher; +import java.net.InetSocketAddress; import java.util.Arrays; import java.util.List; import java.util.Optional; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.annotation.PostConstruct; import javax.inject.Inject; import javax.inject.Singleton; @@ -39,6 +43,16 @@ public class HeaderAuthenticationFetcher implements AuthenticationFetcher { @Inject TokenConfiguration configuration; + private List ipPatternList; + + @PostConstruct + public void init() { + this.ipPatternList = headerAuth.getIpPatterns() + .stream() + .map(Pattern::compile) + .collect(Collectors.toList()); + } + @Override public Publisher fetchAuthentication(HttpRequest request) { Optional userHeaders = headerAuth.getUserHeader() != null ? @@ -49,6 +63,29 @@ public Publisher fetchAuthentication(HttpRequest request) { return Publishers.empty(); } + if (!ipPatternList.isEmpty()) { + InetSocketAddress socketAddress = request.getRemoteAddress(); + //noinspection ConstantConditions https://github.com/micronaut-projects/micronaut-security/issues/186 + if (socketAddress == null) { + log.debug("Request remote address was not found. Skipping header authentication."); + return Publishers.empty(); + } + + if (socketAddress.getAddress() == null) { + log.debug("Could not resolve the InetAddress. Skipping header authentication."); + return Publishers.empty(); + } + + String hostAddress = socketAddress.getAddress().getHostAddress(); + if (ipPatternList.stream().noneMatch(pattern -> + pattern.pattern().equals(SecurityConfigurationProperties.ANYWHERE) || + pattern.matcher(hostAddress).matches())) { + log.warn("None of the IP patterns [{}] matched the host address [{}]. Skipping header authentication.", headerAuth.getIpPatterns(), hostAddress); + return Publishers.empty(); + } + log.debug("One or more of the IP patterns matched the host address [{}]. Continuing request processing.", hostAddress); + } + Optional groupHeaders = headerAuth.getGroupsHeader() != null ? request.getHeaders().get(headerAuth.getGroupsHeader(), String.class) : Optional.empty(); diff --git a/src/test/java/org/akhq/controllers/HeaderAuthControllerTest.java b/src/test/java/org/akhq/controllers/HeaderAuthControllerTest.java index 4e4314d6b..2ffadb170 100644 --- a/src/test/java/org/akhq/controllers/HeaderAuthControllerTest.java +++ b/src/test/java/org/akhq/controllers/HeaderAuthControllerTest.java @@ -10,6 +10,7 @@ import javax.inject.Inject; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; class HeaderAuthControllerTest extends AbstractTest { @Inject @@ -89,4 +90,25 @@ void invalidUser() { assertEquals(3, result.getRoles().size()); } } + + @MicronautTest(environments = "header-ip-disallow") + public static class UntrustedIp extends AbstractTest { + @Inject + @Client("/") + protected RxHttpClient client; + + @Test + void invalidIp() { + AkhqController.AuthUser result = client.toBlocking().retrieve( + HttpRequest + .GET("/api/me") + .header("x-akhq-user", "header-user") + .header("x-akhq-group", "limited,extra"), + AkhqController.AuthUser.class + ); + + assertNull(result.getUsername()); + assertNull(result.getRoles()); + } + } } diff --git a/src/test/resources/application-header-ip-disallow.yml b/src/test/resources/application-header-ip-disallow.yml new file mode 100644 index 000000000..98d3f0262 --- /dev/null +++ b/src/test/resources/application-header-ip-disallow.yml @@ -0,0 +1,7 @@ +akhq: + security: + header-auth: + user-header: x-akhq-user + groups-header: x-akhq-group + ip-patterns: ["none"] + default-group: invalid-group