Skip to content

Commit

Permalink
feat(auth): align header authentication user/group settings with LDAP…
Browse files Browse the repository at this point in the history
… and OIDC
  • Loading branch information
piotrp committed Aug 16, 2021
1 parent b43f52d commit ffab0d4
Show file tree
Hide file tree
Showing 8 changed files with 74 additions and 81 deletions.
24 changes: 17 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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**
Expand All @@ -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"]
Expand Down
13 changes: 9 additions & 4 deletions application.example.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
micronaut:
security:
enabled: true
# Ldap authentificaton configuration
# Ldap authenticaton configuration
ldap:
default:
enabled: true
Expand Down Expand Up @@ -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
- admin
12 changes: 5 additions & 7 deletions src/main/java/org/akhq/configs/HeaderAuth.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,10 @@ public class HeaderAuth {
String userHeader;
String groupsHeader;
String groupsHeaderSeparator = ",";
List<Users> users;
List<String> ipPatterns = Collections.singletonList(SecurityConfigurationProperties.ANYWHERE);

@Data
public static class Users {
String username;
List<String> groups = new ArrayList<>();
}
String defaultGroup;
List<GroupMapping> groups = new ArrayList<>();
List<UserMapping> users = new ArrayList<>();

List<String> ipPatterns = Collections.singletonList(SecurityConfigurationProperties.ANYWHERE);
}
35 changes: 5 additions & 30 deletions src/main/java/org/akhq/modules/HeaderAuthenticationFetcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -92,18 +91,17 @@ public Publisher<Authentication> fetchAuthentication(HttpRequest<?> request) {

return Flowable
.fromCallable(() -> {
List<String> strings = groupsMapper(userHeaders.get(), groupHeaders);

if (strings.size() == 0) {
return Optional.<ClaimProvider.AKHQClaimResponse>empty();
}
List<String> 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));
Expand All @@ -130,27 +128,4 @@ public Publisher<Authentication> fetchAuthentication(HttpRequest<?> request) {
}
});
}

private List<String> groupsMapper(String user, Optional<String> 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<String> groupsSplit(Optional<String> groupHeaders) {
return groupHeaders
.stream()
.flatMap(s -> Arrays.stream(s.split(headerAuth.getGroupsHeaderSeparator())));
}
}
12 changes: 11 additions & 1 deletion src/main/java/org/akhq/utils/LocalSecurityClaimProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public class LocalSecurityClaimProvider implements ClaimProvider {
@Inject
SecurityProperties securityProperties;
@Inject
HeaderAuth headerAuthProperties;
@Inject
Ldap ldapProperties;
@Inject
Oidc oidcProperties;
Expand All @@ -31,10 +33,18 @@ public AKHQClaimResponse generateClaim(AKHQClaimRequest request) {
List<String> 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
Expand Down
46 changes: 20 additions & 26 deletions src/test/java/org/akhq/controllers/HeaderAuthControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
6 changes: 0 additions & 6 deletions src/test/resources/application-overridegroups.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,3 @@ akhq:
extra:
roles:
- topic/read
- topic/write

header-auth:
user-header: x-akhq-user
groups-header: x-akhq-group
users: []
7 changes: 7 additions & 0 deletions src/test/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,13 @@ akhq:
- username: header-admin
groups:
- admin
groups:
- name: external-operator
groups:
- operator
- name: external-limited
groups:
- limited

ldap:
groups:
Expand Down

0 comments on commit ffab0d4

Please sign in to comment.