From ffab0d49a355513b29f6d66385ba2c0b24ba9bc3 Mon Sep 17 00:00:00 2001 From: Piotr Przybylski Date: Mon, 16 Aug 2021 21:03:33 +0200 Subject: [PATCH] feat(auth): align header authentication user/group settings with LDAP and OIDC --- README.md | 24 +++++++--- application.example.yml | 13 ++++-- .../java/org/akhq/configs/HeaderAuth.java | 12 ++--- .../modules/HeaderAuthenticationFetcher.java | 35 ++------------ .../utils/LocalSecurityClaimProvider.java | 12 ++++- .../controllers/HeaderAuthControllerTest.java | 46 ++++++++----------- .../resources/application-overridegroups.yml | 6 --- src/test/resources/application.yml | 7 +++ 8 files changed, 74 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 367353c04..c961e1e19 100644 --- a/README.md +++ b/README.md @@ -628,10 +628,17 @@ akhq: 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` + default-group: topic-reader + groups: # optional + # the name of the user group read from header + - name: header-admin-group + groups: + # the corresponding akhq groups (eg. topic-reader/writer or akhq default groups like admin/reader/no-role) + - admin + users: # optional - username: header-user # username matching the `user-header` value - groups: # list of group for current users - - topic-reader + groups: # list of groups / additional groups + - topic-writer - username: header-admin groups: - admin @@ -641,13 +648,15 @@ akhq: * `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. +* `default-group` default AKHQ group, used when no groups were read from `groups-header` +* `groups` maps external group names read from headers to AKHQ groups. +* `users` assigns additional AKHQ groups to users. ### External roles and attributes mapping -If you managed which topics (or any other resource) in an external system, you have access to 2 more implementations mechanisms to map your authenticated user (from either Local, LDAP or OIDC Authent) into AKHQ roles and attributes: +If you managed which topics (or any other resource) in an external system, you have access to 2 more implementations mechanisms to map your authenticated user (from either Local, Header, LDAP or OIDC) into AKHQ roles and attributes: -If you use this mechanism, keep in mind it will take the local user's groups for local Auth, and the external groups for LDAP/OIDC (ie. this will NOT do the mapping between LDAP/OIDC and local groups) +If you use this mechanism, keep in mind it will take the local user's groups for local Auth, and the external groups for Header/LDAP/OIDC (ie. this will NOT do the mapping between Header/LDAP/OIDC and local groups) **Default configuration-based** This is the current implementation and the default one (doesn't break compatibility) @@ -665,6 +674,7 @@ akhq: roles: [] ldap: # LDAP users/groups to AKHQ groups mapping oidc: # OIDC users/groups to AKHQ groups mapping + header-auth: # header authentication users/groups to AKHQ groups mapping ```` **REST API** @@ -682,7 +692,7 @@ In this mode, AKHQ will send to the ``akhq.security.rest.url`` endpoint a POST r ````json { - "providerType": "LDAP or OIDC or BASIC_AUTH", + "providerType": "LDAP or OIDC or BASIC_AUTH or HEADER", "providerName": "OIDC provider name (OIDC only)", "username": "user", "groups": ["LDAP-GROUP-1", "LDAP-GROUP-2", "LDAP-GROUP-3"] diff --git a/application.example.yml b/application.example.yml index d4a5343ff..78fb3d1fa 100644 --- a/application.example.yml +++ b/application.example.yml @@ -1,7 +1,7 @@ micronaut: security: enabled: true - # Ldap authentificaton configuration + # Ldap authenticaton configuration ldap: default: enabled: true @@ -266,10 +266,15 @@ akhq: 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` + default-group: topic-reader + groups: + - name: header-admin-group + groups: + - admin + users: # optional - username: header-user # username matching the `user-header` value - groups: # list of group for current users + groups: # list of groups / additional groups - topic-reader - username: header-admin groups: - - admin \ No newline at end of file + - admin diff --git a/src/main/java/org/akhq/configs/HeaderAuth.java b/src/main/java/org/akhq/configs/HeaderAuth.java index 4d871fb35..625b65c00 100644 --- a/src/main/java/org/akhq/configs/HeaderAuth.java +++ b/src/main/java/org/akhq/configs/HeaderAuth.java @@ -14,12 +14,10 @@ public class HeaderAuth { String userHeader; String groupsHeader; String groupsHeaderSeparator = ","; - List users; - List ipPatterns = Collections.singletonList(SecurityConfigurationProperties.ANYWHERE); - @Data - public static class Users { - String username; - List groups = new ArrayList<>(); - } + String defaultGroup; + List groups = new ArrayList<>(); + List users = new ArrayList<>(); + + List ipPatterns = Collections.singletonList(SecurityConfigurationProperties.ANYWHERE); } diff --git a/src/main/java/org/akhq/modules/HeaderAuthenticationFetcher.java b/src/main/java/org/akhq/modules/HeaderAuthenticationFetcher.java index 2793a645c..4dee5ca0a 100644 --- a/src/main/java/org/akhq/modules/HeaderAuthenticationFetcher.java +++ b/src/main/java/org/akhq/modules/HeaderAuthenticationFetcher.java @@ -22,7 +22,6 @@ 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; @@ -92,18 +91,17 @@ public Publisher fetchAuthentication(HttpRequest request) { return Flowable .fromCallable(() -> { - List strings = groupsMapper(userHeaders.get(), groupHeaders); - - if (strings.size() == 0) { - return Optional.empty(); - } + List groups = groupHeaders + .stream() + .flatMap(s -> Arrays.stream(s.split(headerAuth.getGroupsHeaderSeparator()))) + .collect(Collectors.toList()); ClaimProvider.AKHQClaimRequest claim = ClaimProvider.AKHQClaimRequest.builder() .providerType(ClaimProvider.ProviderType.HEADER) .providerName(null) .username(userHeaders.get()) - .groups(strings) + .groups(groups) .build(); return Optional.of(claimProvider.generateClaim(claim)); @@ -130,27 +128,4 @@ public Publisher fetchAuthentication(HttpRequest request) { } }); } - - private List groupsMapper(String user, Optional groupHeaders) { - if (headerAuth.getUsers() == null || headerAuth.getUsers().size() == 0) { - return groupsSplit(groupHeaders) - .collect(Collectors.toList()); - } - - return headerAuth - .getUsers() - .stream() - .filter(users -> users.getUsername().equals(user)) - .flatMap(users -> Stream.concat( - groupsSplit(groupHeaders), - users.getGroups() != null ? users.getGroups().stream() : Stream.empty() - )) - .collect(Collectors.toList()); - } - - private Stream groupsSplit(Optional groupHeaders) { - return groupHeaders - .stream() - .flatMap(s -> Arrays.stream(s.split(headerAuth.getGroupsHeaderSeparator()))); - } } diff --git a/src/main/java/org/akhq/utils/LocalSecurityClaimProvider.java b/src/main/java/org/akhq/utils/LocalSecurityClaimProvider.java index 5980680f5..3d3e9f752 100644 --- a/src/main/java/org/akhq/utils/LocalSecurityClaimProvider.java +++ b/src/main/java/org/akhq/utils/LocalSecurityClaimProvider.java @@ -19,6 +19,8 @@ public class LocalSecurityClaimProvider implements ClaimProvider { @Inject SecurityProperties securityProperties; @Inject + HeaderAuth headerAuthProperties; + @Inject Ldap ldapProperties; @Inject Oidc oidcProperties; @@ -31,10 +33,18 @@ public AKHQClaimResponse generateClaim(AKHQClaimRequest request) { List akhqGroups = new ArrayList<>(); switch (request.getProviderType()) { case BASIC_AUTH: - case HEADER: // we already have target AKHQ groups akhqGroups.addAll(request.getGroups()); break; + case HEADER: + // we need to convert from externally provided groups to AKHQ groups to find the roles and attributes + // using akhq.security.header-auth.groups and akhq.security.header-auth.users + // as well as akhq.security.header-auth.default-group + userMappings = headerAuthProperties.getUsers(); + groupMappings = headerAuthProperties.getGroups(); + defaultGroup = headerAuthProperties.getDefaultGroup(); + akhqGroups.addAll(mapToAkhqGroups(request.getUsername(), request.getGroups(), groupMappings, userMappings, defaultGroup)); + break; case LDAP: // we need to convert from LDAP groups to AKHQ groups to find the roles and attributes // using akhq.security.ldap.groups and akhq.security.ldap.users diff --git a/src/test/java/org/akhq/controllers/HeaderAuthControllerTest.java b/src/test/java/org/akhq/controllers/HeaderAuthControllerTest.java index 2ffadb170..e819bd185 100644 --- a/src/test/java/org/akhq/controllers/HeaderAuthControllerTest.java +++ b/src/test/java/org/akhq/controllers/HeaderAuthControllerTest.java @@ -44,51 +44,45 @@ void admin() { } @Test - void userGroup() { + void externalUserAndGroup() { AkhqController.AuthUser result = client.toBlocking().retrieve( HttpRequest .GET("/api/me") - .header("x-akhq-user", "header-user") - .header("x-akhq-group", "limited,operator"), + .header("x-akhq-user", "header-user-operator") + .header("x-akhq-group", "external-operator,external-limited"), AkhqController.AuthUser.class ); - assertEquals("header-user", result.getUsername()); + assertEquals("header-user-operator", result.getUsername()); assertEquals(11, result.getRoles().size()); } @Test - void invalidUser() { + void userWithAdditionalExternalGroup() { AkhqController.AuthUser result = client.toBlocking().retrieve( HttpRequest .GET("/api/me") - .header("x-akhq-user", "header-invalid"), + .header("x-akhq-user", "header-user") + .header("x-akhq-group", "external-limited"), AkhqController.AuthUser.class ); - assertEquals(null, result.getUsername()); - assertEquals(7, result.getRoles().size()); + assertEquals("header-user", result.getUsername()); + // operator from 'users' and externally provided 'limited' + assertEquals(11, result.getRoles().size()); } - @MicronautTest(environments = "overridegroups") - public static class NoUser extends AbstractTest { - @Inject - @Client("/") - protected RxHttpClient client; - - @Test - void invalidUser() { - 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 - ); + @Test + void userWithoutAnyGroup() { + AkhqController.AuthUser result = client.toBlocking().retrieve( + HttpRequest + .GET("/api/me") + .header("x-akhq-user", "header-invalid"), + AkhqController.AuthUser.class + ); - assertEquals("header-user", result.getUsername()); - assertEquals(3, result.getRoles().size()); - } + assertEquals("header-invalid", result.getUsername()); + assertNull(result.getRoles()); } @MicronautTest(environments = "header-ip-disallow") diff --git a/src/test/resources/application-overridegroups.yml b/src/test/resources/application-overridegroups.yml index 75e1772c9..73b8e0cc2 100644 --- a/src/test/resources/application-overridegroups.yml +++ b/src/test/resources/application-overridegroups.yml @@ -10,9 +10,3 @@ akhq: extra: roles: - topic/read - - topic/write - - header-auth: - user-header: x-akhq-user - groups-header: x-akhq-group - users: [] \ No newline at end of file diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index adf23057c..bbf63b07d 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -155,6 +155,13 @@ akhq: - username: header-admin groups: - admin + groups: + - name: external-operator + groups: + - operator + - name: external-limited + groups: + - limited ldap: groups: