ARTEMIS-4280 - map roles from review group info, optional roles properties file

This commit is contained in:
Gary Tully 2023-05-15 16:29:30 +01:00
parent e959e3cfe8
commit d2abc56f56
3 changed files with 87 additions and 12 deletions

View File

@ -46,6 +46,7 @@ public class KubernetesLoginModule extends PropertiesLoader implements AuditLogi
private CallbackHandler handler;
private Subject subject;
private TokenReview tokenReview = new TokenReview();
private boolean ignoreTokenReviewRoles = false;
private Map<String, Set<String>> roles;
private final Set<Principal> principals = new HashSet<>();
private final KubernetesClient client;
@ -68,10 +69,17 @@ public class KubernetesLoginModule extends PropertiesLoader implements AuditLogi
if (debug) {
logger.debug("Initialized debug");
}
roles = load(K8S_ROLE_FILE_PROP_NAME, "k8s-roles.properties", options).invertedPropertiesValuesMap();
if (debug) {
logger.debug("loaded roles: {}", roles);
// role mapping file is optional
if (options.containsKey(K8S_ROLE_FILE_PROP_NAME)) {
roles = load(K8S_ROLE_FILE_PROP_NAME, null, options).invertedPropertiesValuesMap();
if (debug) {
logger.debug("loaded roles: {}", roles);
}
} else {
roles = Map.of();
}
ignoreTokenReviewRoles = booleanOption("ignoreTokenReviewRoles", options);
}
@Override
@ -109,9 +117,11 @@ public class KubernetesLoginModule extends PropertiesLoader implements AuditLogi
UserPrincipal userPrincipal = new ServiceAccountPrincipal(tokenReview.getUsername());
principals.add(userPrincipal);
authenticatedUsers.add(userPrincipal);
}
// populate roles for UserPrincipal from other login modules too
for (UserPrincipal userPrincipal : authenticatedUsers) {
if (!ignoreTokenReviewRoles) {
for (String role : tokenReview.getUser().getGroups()) {
principals.add(new RolePrincipal(role));
}
}
Set<String> matchedRoles = roles.get(userPrincipal.getName());
if (matchedRoles != null) {
for (String entry : matchedRoles) {

View File

@ -59,6 +59,13 @@ public class KubernetesLoginModuleTest {
+ " \"username\": \"" + USERNAME + "\""
+ "}}}";
public static final String AUTH_JSON_WITH_GROUPS = "{\"status\": {"
+ "\"authenticated\": true, "
+ "\"user\": {"
+ " \"username\": \"" + USERNAME + "\","
+ " \"groups\": [\"developers\", \"qa\"]"
+ "}}}";
public static final String UNAUTH_JSON = "{\"status\": {"
+ "\"authenticated\": false "
+ "}}";
@ -138,6 +145,62 @@ public class KubernetesLoginModuleTest {
verify(client, times(1)).getTokenReview(TOKEN);
}
@Test
public void testRolesFromReview() throws LoginException {
CallbackHandler handler = new TokenCallbackHandler(TOKEN);
Subject subject = new Subject();
loginModule.initialize(subject, handler, Collections.emptyMap(), Map.of());
TokenReview tr = TokenReview.fromJsonString(AUTH_JSON_WITH_GROUPS);
when(client.getTokenReview(TOKEN)).thenReturn(tr);
assertTrue(loginModule.login());
assertTrue(loginModule.commit());
assertThat(subject.getPrincipals(UserPrincipal.class), hasSize(1));
subject.getPrincipals(ServiceAccountPrincipal.class).forEach(p -> {
assertThat(p.getName(), is(USERNAME));
assertThat(p.getSaName(), is("kermit"));
assertThat(p.getNamespace(), is("some-ns"));
});
Set<RolePrincipal> roles = subject.getPrincipals(RolePrincipal.class);
assertThat(roles, hasSize(2));
assertThat(roles, containsInAnyOrder(new RolePrincipal("developers"), new RolePrincipal("qa")));
assertTrue(loginModule.logout());
assertFalse(loginModule.commit());
assertThat(subject.getPrincipals(), empty());
verify(client, times(1)).getTokenReview(TOKEN);
}
@Test
public void testIgnoreRolesFromReview() throws LoginException {
CallbackHandler handler = new TokenCallbackHandler(TOKEN);
Subject subject = new Subject();
loginModule.initialize(subject, handler, Collections.emptyMap(), Map.of("ignoreTokenReviewRoles", "true"));
TokenReview tr = TokenReview.fromJsonString(AUTH_JSON_WITH_GROUPS);
when(client.getTokenReview(TOKEN)).thenReturn(tr);
assertTrue(loginModule.login());
assertTrue(loginModule.commit());
assertThat(subject.getPrincipals(UserPrincipal.class), hasSize(1));
subject.getPrincipals(ServiceAccountPrincipal.class).forEach(p -> {
assertThat(p.getName(), is(USERNAME));
assertThat(p.getSaName(), is("kermit"));
assertThat(p.getNamespace(), is("some-ns"));
});
Set<RolePrincipal> roles = subject.getPrincipals(RolePrincipal.class);
assertThat(roles, hasSize(0));
assertTrue(loginModule.logout());
assertFalse(loginModule.commit());
assertThat(subject.getPrincipals(), empty());
verify(client, times(1)).getTokenReview(TOKEN);
}
private Map<String, ?> getDefaultOptions() {
String baseDirValue = new File(KubernetesLoginModuleTest.class.getClassLoader().getResource("k8s-roles.properties").getPath()).getParentFile().getAbsolutePath();
return Map.of(K8S_ROLE_FILE_PROP_NAME, "k8s-roles.properties", "baseDir",baseDirValue);

View File

@ -1088,26 +1088,28 @@ the directory containing the file, `login.config`, to your CLASSPATH.
The Kubernetes login module enables you to perform authentication and authorization
by validating the `Bearer` token against the Kubernetes API. The authentication is done
by submitting a `TokenReview` request that the Kubernetes cluster validates. The response will
tell whether the user is authenticated and the associated username. It is implemented by `org.apache.activemq.artemis.spi.core.security.jaas.KubernetesLoginModule`.
tell whether the user is authenticated and the associated username and roles. It is implemented by `org.apache.activemq.artemis.spi.core.security.jaas.KubernetesLoginModule`.
- `org.apache.activemq.jaas.kubernetes.role` - the path to the file which
contains user and role mapping
- `ignoreTokenReviewRoles` - when true, do not map roles from the TokenReview user groups. default false
- `reload` - boolean flag; whether or not to reload the properties files when a
- `org.apache.activemq.jaas.kubernetes.role` - the optional path to the file which
contains role mapping, useful when ignoreTokenReviewRoles=true
- `reload` - boolean flag; whether or not to reload the properties file when a
modification occurs; default is `false`
- `debug` - boolean flag; if `true`, enable debugging; this is used only for
testing or debugging; normally, it should be set to `false`, or omitted;
default is `false`
The login module must be allowed to query such Rest API. For that, it will use the available
The login module must be allowed to query the required Rest API. For that, it will use the available
token under `/var/run/secrets/kubernetes.io/serviceaccount/token`. Besides, in order to trust the
connection the client will use the `ca.crt` file existing in the same folder. These two files will
be mounted in the container. The service account running the KubernetesLoginModule must
be allowed to `create::TokenReview`. The `system:auth-delegator` role is typically use for
that purpose.
The `k8s-roles.properties` file consists of a list of properties of the form, `Role=UserList`, where `UserList` is a comma-separated list of users. For example, to define the roles admins, users, and guests, you could create a file like the following:
The optional roles properties file consists of a list of properties of the form, `Role=UserList`, where `UserList` is a comma-separated list of users. For example, to define the roles admins, users, and guests, you could create a file like the following:
```properties
admins=system:serviceaccounts:example-ns:admin-sa