Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(auth): align header authentication user/group settings with LDAP and OIDC #794

Merged
merged 1 commit into from
Aug 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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