Skip to content

Commit

Permalink
ARTEMIS-4280 - map roles from review group info, optional roles prope…
Browse files Browse the repository at this point in the history
…rties file
  • Loading branch information
gtully committed May 17, 2023
1 parent e959e3c commit d2abc56
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -68,10 +69,17 @@ public void initialize(Subject subject, CallbackHandler callbackHandler, Map<Str
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
Expand Down Expand Up @@ -109,9 +117,11 @@ public boolean commit() throws LoginException {
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 "
+ "}}";
Expand Down Expand Up @@ -138,6 +145,62 @@ public void testUnableToVerifyToken() throws LoginException {
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);
Expand Down
14 changes: 8 additions & 6 deletions docs/user-manual/en/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit d2abc56

Please sign in to comment.