This is an automated email from the ASF dual-hosted git repository. gtully pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/activemq-artemis.git
The following commit(s) were added to refs/heads/main by this push: new d2abc56f56 ARTEMIS-4280 - map roles from review group info, optional roles properties file d2abc56f56 is described below commit d2abc56f56e9573eb5481ea23f7ed3fcff888c82 Author: Gary Tully <gary.tu...@gmail.com> AuthorDate: Mon May 15 16:29:30 2023 +0100 ARTEMIS-4280 - map roles from review group info, optional roles properties file --- .../core/security/jaas/KubernetesLoginModule.java | 22 +++++--- .../security/jaas/KubernetesLoginModuleTest.java | 63 ++++++++++++++++++++++ docs/user-manual/en/security.md | 14 ++--- 3 files changed, 87 insertions(+), 12 deletions(-) diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/KubernetesLoginModule.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/KubernetesLoginModule.java index 5a50952e86..cc0551ad2e 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/KubernetesLoginModule.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/KubernetesLoginModule.java @@ -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) { diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/KubernetesLoginModuleTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/KubernetesLoginModuleTest.java index fed189dca1..ad4490f35a 100644 --- a/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/KubernetesLoginModuleTest.java +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/KubernetesLoginModuleTest.java @@ -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); diff --git a/docs/user-manual/en/security.md b/docs/user-manual/en/security.md index df66ea4a25..1e8da2b76e 100644 --- a/docs/user-manual/en/security.md +++ b/docs/user-manual/en/security.md @@ -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