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 3e50014e0d [ARTEMIS-3168] Implement Kubernetes JaaS LoginModule
3e50014e0d is described below
commit 3e50014e0de9387fedfed942d51ae0264385f417
Author: ruromero <[email protected]>
AuthorDate: Mon Nov 21 15:23:22 2022 +0100
[ARTEMIS-3168] Implement Kubernetes JaaS LoginModule
Signed-off-by: ruromero <[email protected]>
---
artemis-server-osgi/pom.xml | 1 +
artemis-server/pom.xml | 9 +-
.../core/security/jaas/KubernetesLoginModule.java | 166 ++++++++++++++++++++
.../spi/core/security/jaas/PropertiesLoader.java | 2 +-
.../security/jaas/ServiceAccountPrincipal.java | 46 ++++++
.../jaas/kubernetes/client/KubernetesClient.java | 25 +++
.../kubernetes/client/KubernetesClientImpl.java | 173 +++++++++++++++++++++
.../jaas/kubernetes/model/TokenReview.java | 136 ++++++++++++++++
.../security/jaas/KubernetesLoginModuleTest.java | 144 +++++++++++++++++
.../jaas/kubernetes/TokenCallbackHandler.java | 48 ++++++
.../client/KubernetesClientImplTest.java | 140 +++++++++++++++++
.../model/ServiceAccountPrincipalTest.java | 50 ++++++
.../jaas/kubernetes/model/TokenReviewTest.java | 102 ++++++++++++
artemis-server/src/test/resources/client_token | 17 ++
.../src/test/resources/k8s-roles.properties | 20 +++
docs/user-manual/en/security.md | 31 ++++
pom.xml | 3 +-
17 files changed, 1110 insertions(+), 3 deletions(-)
diff --git a/artemis-server-osgi/pom.xml b/artemis-server-osgi/pom.xml
index d86b0c255b..27c867ed90 100644
--- a/artemis-server-osgi/pom.xml
+++ b/artemis-server-osgi/pom.xml
@@ -135,6 +135,7 @@
org.glassfish.json*;resolution:=optional,
org.postgresql*;resolution:=optional,
io.netty.buffer;io.netty.*;version="[4.1,5)",
+ java.net.http*;resolution:=optional,
*
</Import-Package>
<_exportcontents>org.apache.activemq.artemis.*;-noimport:=true</_exportcontents>
diff --git a/artemis-server/pom.xml b/artemis-server/pom.xml
index 22914df525..bb8c64b1b9 100644
--- a/artemis-server/pom.xml
+++ b/artemis-server/pom.xml
@@ -14,7 +14,8 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
@@ -263,6 +264,12 @@
<artifactId>jakarta.json-api</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.mock-server</groupId>
+ <artifactId>mockserver-netty</artifactId>
+ <version>${mockserver.version}</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<profiles>
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
new file mode 100644
index 0000000000..5a50952e86
--- /dev/null
+++
b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/KubernetesLoginModule.java
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.activemq.artemis.spi.core.security.jaas;
+
+import java.io.IOException;
+import java.security.Principal;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.FailedLoginException;
+import javax.security.auth.login.LoginException;
+
+import org.apache.activemq.artemis.logs.AuditLogger;
+import
org.apache.activemq.artemis.spi.core.security.jaas.kubernetes.client.KubernetesClient;
+import
org.apache.activemq.artemis.spi.core.security.jaas.kubernetes.client.KubernetesClientImpl;
+import
org.apache.activemq.artemis.spi.core.security.jaas.kubernetes.model.TokenReview;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class KubernetesLoginModule extends PropertiesLoader implements
AuditLoginModule {
+
+ private static final Logger logger =
LoggerFactory.getLogger(KubernetesLoginModule.class);
+
+ public static final String K8S_ROLE_FILE_PROP_NAME =
"org.apache.activemq.jaas.kubernetes.role";
+
+ private CallbackHandler handler;
+ private Subject subject;
+ private TokenReview tokenReview = new TokenReview();
+ private Map<String, Set<String>> roles;
+ private final Set<Principal> principals = new HashSet<>();
+ private final KubernetesClient client;
+
+ public KubernetesLoginModule(KubernetesClient client) {
+ this.client = client;
+ }
+
+ public KubernetesLoginModule() {
+ this(new KubernetesClientImpl());
+ }
+
+ @Override
+ public void initialize(Subject subject, CallbackHandler callbackHandler,
Map<String, ?> sharedState,
+ Map<String, ?> options) {
+ this.handler = callbackHandler;
+ this.subject = subject;
+
+ debug = booleanOption("debug", options);
+ 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);
+ }
+ }
+
+ @Override
+ public boolean login() throws LoginException {
+ Callback[] callbacks = new Callback[1];
+ callbacks[0] = new PasswordCallback("Password", false);
+
+ try {
+ handler.handle(callbacks);
+ } catch (IOException | UnsupportedCallbackException e) {
+ throw (LoginException) new LoginException().initCause(e);
+ }
+
+ char[] token = ((PasswordCallback) callbacks[0]).getPassword();
+
+ if (token.length == 0) {
+ throw new FailedLoginException("Bearer token is empty");
+ }
+
+ tokenReview = client.getTokenReview(new String(token));
+
+ if (debug) {
+ logger.debug("login {}", tokenReview);
+ }
+ return tokenReview.isAuthenticated();
+ }
+
+ @Override
+ public boolean commit() throws LoginException {
+ boolean result = false;
+ result = tokenReview.isAuthenticated();
+
+ Set<UserPrincipal> authenticatedUsers =
subject.getPrincipals(UserPrincipal.class);
+ if (result) {
+ 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) {
+ Set<String> matchedRoles = roles.get(userPrincipal.getName());
+ if (matchedRoles != null) {
+ for (String entry : matchedRoles) {
+ principals.add(new RolePrincipal(entry));
+ }
+ }
+ }
+
+ subject.getPrincipals().addAll(principals);
+
+ clear();
+
+ if (debug) {
+ logger.debug("commit, result: {}, principals: {}", result,
principals);
+ }
+ return result;
+ }
+
+ @Override
+ public boolean abort() throws LoginException {
+ registerFailureForAudit(tokenReview.getUsername());
+ clear();
+
+ if (debug) {
+ logger.debug("abort");
+ }
+ return true;
+ }
+
+ @Override
+ public void registerFailureForAudit(String name) {
+ Subject subject = new Subject();
+ subject.getPrincipals().add(new ServiceAccountPrincipal(name));
+ AuditLogger.setCurrentCaller(subject);
+ }
+
+ @Override
+ public boolean logout() throws LoginException {
+ subject.getPrincipals().removeAll(principals);
+ principals.clear();
+ clear();
+ if (debug) {
+ logger.debug("logout");
+ }
+ return true;
+ }
+
+ private void clear() {
+ tokenReview = new TokenReview();
+ }
+
+}
diff --git
a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PropertiesLoader.java
b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PropertiesLoader.java
index 80adf23ca4..6adf4dca41 100644
---
a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PropertiesLoader.java
+++
b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PropertiesLoader.java
@@ -54,7 +54,7 @@ public class PropertiesLoader {
return result.obtained();
}
- private static boolean booleanOption(String name, Map options) {
+ protected static boolean booleanOption(String name, Map options) {
return Boolean.parseBoolean((String) options.get(name));
}
diff --git
a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/ServiceAccountPrincipal.java
b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/ServiceAccountPrincipal.java
new file mode 100644
index 0000000000..757dc45caa
--- /dev/null
+++
b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/ServiceAccountPrincipal.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.activemq.artemis.spi.core.security.jaas;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ServiceAccountPrincipal extends UserPrincipal {
+
+ private static final Pattern SA_NAME_PATTERN =
Pattern.compile("system:serviceaccounts:([\\w-]+):([\\w-]+)");
+
+ private String saName;
+ private String namespace;
+
+ public ServiceAccountPrincipal(String name) {
+ super(name);
+ Matcher matcher = SA_NAME_PATTERN.matcher(name);
+ if (matcher.find()) {
+ namespace = matcher.group(1);
+ saName = matcher.group(2);
+ }
+ }
+
+ public String getSaName() {
+ return saName;
+ }
+
+ public String getNamespace() {
+ return namespace;
+ }
+
+}
diff --git
a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/kubernetes/client/KubernetesClient.java
b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/kubernetes/client/KubernetesClient.java
new file mode 100644
index 0000000000..fd0b8877df
--- /dev/null
+++
b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/kubernetes/client/KubernetesClient.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.activemq.artemis.spi.core.security.jaas.kubernetes.client;
+
+import
org.apache.activemq.artemis.spi.core.security.jaas.kubernetes.model.TokenReview;
+
+public interface KubernetesClient {
+
+ TokenReview getTokenReview(String token);
+
+}
diff --git
a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/kubernetes/client/KubernetesClientImpl.java
b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/kubernetes/client/KubernetesClientImpl.java
new file mode 100644
index 0000000000..6172bcd15a
--- /dev/null
+++
b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/kubernetes/client/KubernetesClientImpl.java
@@ -0,0 +1,173 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.activemq.artemis.spi.core.security.jaas.kubernetes.client;
+
+import static java.net.HttpURLConnection.HTTP_CREATED;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.nio.file.Path;
+import java.security.KeyStore;
+import java.security.SecureRandom;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Scanner;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManagerFactory;
+
+import
org.apache.activemq.artemis.spi.core.security.jaas.kubernetes.model.TokenReview;
+import org.apache.activemq.artemis.utils.JsonLoader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class KubernetesClientImpl implements KubernetesClient {
+
+ private static final Logger logger =
LoggerFactory.getLogger(KubernetesClientImpl.class);
+
+ private static final String KUBERNETES_HOST = "KUBERNETES_SERVICE_HOST";
+ private static final String KUBERNETES_PORT = "KUBERNETES_SERVICE_PORT";
+ private static final String KUBERNETES_TOKEN_PATH = "KUBERNETES_TOKEN_PATH";
+ private static final String KUBERNETES_CA_PATH = "KUBERNETES_CA_PATH";
+
+ private static final String KUBERNETES_TOKENREVIEW_URI_PATTERN =
"https://%s:%s/apis/authentication.k8s.io/v1/tokenreviews";
+
+ private static final String DEFAULT_KUBERNETES_TOKEN_PATH =
"/var/run/secrets/kubernetes.io/serviceaccount/token";
+ private static final String DEFAULT_KUBERNETES_CA_PATH =
"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt";
+
+ private URI apiUri;
+ private String tokenPath;
+ private String caPath;
+
+ public KubernetesClientImpl() {
+ this.tokenPath = getParam(KUBERNETES_TOKEN_PATH,
DEFAULT_KUBERNETES_TOKEN_PATH);
+ this.caPath = getParam(KUBERNETES_CA_PATH, DEFAULT_KUBERNETES_CA_PATH);
+ String host = getParam(KUBERNETES_HOST);
+ String port = getParam(KUBERNETES_PORT);
+ this.apiUri =
URI.create(String.format(KUBERNETES_TOKENREVIEW_URI_PATTERN, host, port));
+ }
+
+ private String getParam(String name, String defaultValue) {
+ String value = System.getenv(name);
+ if (value == null) {
+ value = System.getProperty(name, defaultValue);
+ }
+ if (value == null) {
+ return defaultValue;
+ }
+ return value;
+ }
+
+ private String getParam(String name) {
+ return getParam(name, null);
+ }
+
+ @Override
+ public TokenReview getTokenReview(String token) {
+ TokenReview tokenReview = new TokenReview();
+ String authToken = null;
+ try {
+ logger.debug("Loading client authentication token from {}",
tokenPath);
+ authToken = readFile(tokenPath);
+ logger.debug("Loaded client authentication token from {}", tokenPath);
+ } catch (IOException e) {
+ logger.error("Cannot retrieve Service Account Authentication Token
from " + tokenPath, e);
+ return tokenReview;
+ }
+ String jsonRequest = buildJsonRequest(token);
+
+ SSLContext ctx;
+ try {
+ ctx = buildSSLContext();
+ } catch (Exception e) {
+ logger.error("Unable to build a valid SSLContext", e);
+ return tokenReview;
+ }
+ HttpClient client = HttpClient.newBuilder().sslContext(ctx).build();
+
+ HttpRequest request = HttpRequest.newBuilder(apiUri)
+ .header("Authorization", "Bearer " + authToken)
+ .header("Accept", "application/json; charset=utf-8")
+ .POST(HttpRequest.BodyPublishers.ofString(jsonRequest)).build();
+ logger.debug("Submit TokenReview request to Kubernetes API");
+
+ try {
+ HttpResponse<String> response = client.send(request,
BodyHandlers.ofString());
+ if (response.statusCode() == HTTP_CREATED) {
+ logger.debug("Received valid TokenReview response");
+ return TokenReview.fromJsonString(response.body());
+ }
+ logger.error("Unable to retrieve a valid TokenReview. Received
StatusCode: {}. Body: {}",
+ response.statusCode(), response.body());
+ } catch (IOException | InterruptedException e) {
+ logger.error("Unable to request ReviewToken", e);
+ }
+ return tokenReview;
+ }
+
+ private String readFile(String path) throws IOException {
+ try (Scanner scanner = new Scanner(Path.of(path))) {
+ StringBuilder buffer = new StringBuilder();
+ while (scanner.hasNextLine()) {
+ String line = scanner.nextLine();
+ if (!line.isBlank() && !line.startsWith("#")) {
+ buffer.append(line);
+ }
+ }
+ return buffer.toString();
+ }
+ }
+
+ private String buildJsonRequest(String clientToken) {
+ return JsonLoader.createObjectBuilder()
+ .add("apiVersion", "authentication.k8s.io/v1")
+ .add("kind", "TokenReview")
+ .add("spec", JsonLoader.createObjectBuilder()
+ .add("token", clientToken)
+ .build())
+ .build().toString();
+ }
+
+ private SSLContext buildSSLContext() throws Exception {
+ SSLContext ctx = SSLContext.getInstance("SSL");
+ File certFile = new File(caPath);
+ if (!certFile.exists()) {
+ logger.debug("Kubernetes CA certificate not found at: {}. Truststore
not configured", caPath);
+ return ctx;
+ }
+ try (InputStream fis = new FileInputStream(certFile)) {
+ KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ CertificateFactory certFactory =
CertificateFactory.getInstance("X.509");
+ X509Certificate certificate = (X509Certificate)
certFactory.generateCertificate(fis);
+ trustStore.load(null, null);
+ trustStore.setCertificateEntry(certFile.getName(), certificate);
+ TrustManagerFactory tmFactory = TrustManagerFactory
+ .getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ tmFactory.init(trustStore);
+
+ ctx.init(null, tmFactory.getTrustManagers(), new SecureRandom());
+ }
+ return ctx;
+ }
+}
diff --git
a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/kubernetes/model/TokenReview.java
b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/kubernetes/model/TokenReview.java
new file mode 100644
index 0000000000..2b3d6bed7a
--- /dev/null
+++
b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/kubernetes/model/TokenReview.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.activemq.artemis.spi.core.security.jaas.kubernetes.model;
+
+import java.io.StringReader;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.activemq.artemis.json.JsonArray;
+import org.apache.activemq.artemis.json.JsonObject;
+import org.apache.activemq.artemis.json.JsonString;
+import org.apache.activemq.artemis.utils.JsonLoader;
+
+public class TokenReview {
+
+ private boolean authenticated;
+ private User user;
+ private List<String> audiences;
+
+ public boolean isAuthenticated() {
+ return authenticated;
+ }
+
+ public User getUser() {
+ return user;
+ }
+
+ public String getUsername() {
+ if (user == null) {
+ return null;
+ }
+ return user.getUsername();
+ }
+
+ public List<String> getAudiences() {
+ return audiences;
+ }
+
+ public static TokenReview fromJsonString(String obj) {
+ JsonObject json = JsonLoader.readObject(new StringReader(obj));
+ JsonObject status = json.getJsonObject("status");
+ return TokenReview.fromJson(status);
+ }
+
+ private static TokenReview fromJson(JsonObject obj) {
+ TokenReview t = new TokenReview();
+ if (obj == null) {
+ return t;
+ }
+ t.authenticated = obj.getBoolean("authenticated", false);
+ t.user = User.fromJson(obj.getJsonObject("user"));
+ t.audiences = listFromJson(obj.getJsonArray("audiences"));
+ return t;
+ }
+
+ private static List<String> listFromJson(JsonArray items) {
+ if (items == null) {
+ return Collections.emptyList();
+ }
+ return
Collections.unmodifiableList(items.getValuesAs(JsonString::getString));
+ }
+
+ public static class User {
+
+ private String username;
+ private String uid;
+ private List<String> groups;
+ private Extra extra;
+
+ public Extra getExtra() {
+ return extra;
+ }
+
+ public List<String> getGroups() {
+ return groups;
+ }
+
+ public String getUid() {
+ return uid;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public static User fromJson(JsonObject obj) {
+ if (obj == null) {
+ return null;
+ }
+ User u = new User();
+ u.username = obj.getString("username", null);
+ u.uid = obj.getString("uid", null);
+ u.groups = listFromJson(obj.getJsonArray("groups"));
+ u.extra = Extra.fromJson(obj.getJsonObject("extra"));
+ return u;
+ }
+
+ }
+
+ public static class Extra {
+ private List<String> podNames;
+ private List<String> podUids;
+
+ public List<String> getPodNames() {
+ return podNames;
+ }
+
+ public List<String> getPodUids() {
+ return podUids;
+ }
+
+ public static Extra fromJson(JsonObject obj) {
+ if (obj == null) {
+ return null;
+ }
+ Extra e = new Extra();
+ e.podNames =
listFromJson(obj.getJsonArray("authentication.kubernetes.io/pod-name"));
+ e.podUids =
listFromJson(obj.getJsonArray("authentication.kubernetes.io/pod-uid"));
+ return e;
+ }
+ }
+}
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
new file mode 100644
index 0000000000..be1aede8ea
--- /dev/null
+++
b/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/KubernetesLoginModuleTest.java
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.activemq.artemis.spi.core.security.jaas;
+
+import static
org.apache.activemq.artemis.spi.core.security.jaas.KubernetesLoginModule.K8S_ROLE_FILE_PROP_NAME;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.login.LoginException;
+
+import
org.apache.activemq.artemis.spi.core.security.jaas.kubernetes.TokenCallbackHandler;
+import
org.apache.activemq.artemis.spi.core.security.jaas.kubernetes.client.KubernetesClient;
+import
org.apache.activemq.artemis.spi.core.security.jaas.kubernetes.model.TokenReview;
+import org.junit.Test;
+
+public class KubernetesLoginModuleTest {
+
+ private final KubernetesClient client = mock(KubernetesClient.class);
+ private final KubernetesLoginModule loginModule = new
KubernetesLoginModule(client);
+ private static final String TOKEN = "the_token";
+
+ public static final String USERNAME =
"system:serviceaccounts:some-ns:kermit";
+ public static final String AUTH_JSON = "{\"status\": {"
+ + "\"authenticated\": true, "
+ + "\"user\": {"
+ + " \"username\": \"" + USERNAME + "\""
+ + "}}}";
+
+ public static final String UNAUTH_JSON = "{\"status\": {"
+ + "\"authenticated\": false "
+ + "}}";
+
+ @Test
+ public void testBasicLogin() throws LoginException {
+ CallbackHandler handler = new TokenCallbackHandler(TOKEN);
+ Subject subject = new Subject();
+ loginModule.initialize(subject, handler, Collections.emptyMap(),
getDefaultOptions());
+
+ TokenReview tr = TokenReview.fromJsonString(AUTH_JSON);
+ 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("muppet"), new
RolePrincipal("admin")));
+
+ assertTrue(loginModule.logout());
+ assertFalse(loginModule.commit());
+ assertThat(subject.getPrincipals(), empty());
+ verify(client, times(1)).getTokenReview(TOKEN);
+ }
+
+ @Test
+ public void testFailedLogin() throws LoginException {
+ CallbackHandler handler = new TokenCallbackHandler(TOKEN);
+ Subject subject = new Subject();
+ loginModule.initialize(subject, handler, Collections.emptyMap(),
getDefaultOptions());
+
+ TokenReview tr = TokenReview.fromJsonString(UNAUTH_JSON);
+ when(client.getTokenReview(TOKEN)).thenReturn(tr);
+
+ assertFalse(loginModule.login());
+ assertFalse(loginModule.commit());
+ assertThat(subject.getPrincipals(), empty());
+ verify(client, times(1)).getTokenReview(TOKEN);
+ }
+
+ @Test
+ public void testNullToken() throws LoginException {
+ CallbackHandler handler = new TokenCallbackHandler(null);
+ Subject subject = new Subject();
+ loginModule.initialize(subject, handler, Collections.emptyMap(),
getDefaultOptions());
+
+ try {
+ assertFalse(loginModule.login());
+ fail("Exception expected");
+ } catch (LoginException e) {
+ assertNotNull(e);
+ }
+
+ assertFalse(loginModule.commit());
+ assertThat(subject.getPrincipals(), empty());
+ verifyNoInteractions(client);
+ }
+
+ @Test
+ public void testUnableToVerifyToken() throws LoginException {
+ CallbackHandler handler = new TokenCallbackHandler(TOKEN);
+ Subject subject = new Subject();
+ loginModule.initialize(subject, handler, Collections.emptyMap(),
getDefaultOptions());
+
+ when(client.getTokenReview(TOKEN)).thenReturn(new TokenReview());
+
+ assertFalse(loginModule.login());
+ assertFalse(loginModule.commit());
+ assertThat(subject.getPrincipals(), empty());
+ verify(client, times(1)).getTokenReview(TOKEN);
+ }
+
+ private Map<String, ?> getDefaultOptions() {
+ return Map.of(K8S_ROLE_FILE_PROP_NAME,
+ "k8s-roles.properties");
+ }
+}
diff --git
a/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/kubernetes/TokenCallbackHandler.java
b/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/kubernetes/TokenCallbackHandler.java
new file mode 100644
index 0000000000..a13e89c7f0
--- /dev/null
+++
b/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/kubernetes/TokenCallbackHandler.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.activemq.artemis.spi.core.security.jaas.kubernetes;
+
+import java.io.IOException;
+
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+
+public class TokenCallbackHandler implements CallbackHandler {
+
+ private final char[] password;
+
+ public TokenCallbackHandler(String password) {
+ if (password != null) {
+ this.password = password.toCharArray();
+ } else {
+ this.password = new char[0];
+ }
+ }
+
+ @Override
+ public void handle(Callback[] callbacks) throws IOException,
UnsupportedCallbackException {
+ for (Callback c : callbacks) {
+ if (c instanceof PasswordCallback) {
+ ((PasswordCallback) c).setPassword(password);
+ } else {
+ throw new UnsupportedCallbackException(c);
+ }
+ }
+ }
+}
diff --git
a/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/kubernetes/client/KubernetesClientImplTest.java
b/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/kubernetes/client/KubernetesClientImplTest.java
new file mode 100644
index 0000000000..f746fa6a20
--- /dev/null
+++
b/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/kubernetes/client/KubernetesClientImplTest.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.activemq.artemis.spi.core.security.jaas.kubernetes.client;
+
+import static java.net.HttpURLConnection.HTTP_CREATED;
+import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
+import static
org.apache.activemq.artemis.spi.core.security.jaas.KubernetesLoginModuleTest.AUTH_JSON;
+import static
org.apache.activemq.artemis.spi.core.security.jaas.KubernetesLoginModuleTest.UNAUTH_JSON;
+import static
org.apache.activemq.artemis.spi.core.security.jaas.KubernetesLoginModuleTest.USERNAME;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockserver.model.HttpRequest.request;
+import static org.mockserver.model.HttpResponse.response;
+import static org.mockserver.model.JsonBody.json;
+
+import java.net.URL;
+
+import
org.apache.activemq.artemis.spi.core.security.jaas.kubernetes.model.TokenReview;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.mockserver.configuration.ConfigurationProperties;
+import org.mockserver.integration.ClientAndServer;
+import org.mockserver.matchers.MatchType;
+import org.mockserver.socket.PortFactory;
+import org.mockserver.verify.VerificationTimes;
+
+public class KubernetesClientImplTest {
+
+ private static final String API_PATH =
"/apis/authentication.k8s.io/v1/tokenreviews";
+ private static ClientAndServer mockServer;
+ private static final String host = "localhost";
+ private static String port;
+
+ private static final String BOB_REQUEST = "{\"apiVersion\":
\"authentication.k8s.io/v1\"," +
+ "\"kind\": \"TokenReview\", \"spec\": {\"token\": \"bob_token\"}}";
+
+ private static final String KERMIT_REQUEST = "{\"apiVersion\":
\"authentication.k8s.io/v1\"," +
+ "\"kind\": \"TokenReview\", \"spec\": {\"token\": \"kermit_token\"}}";
+
+ @BeforeClass
+ public static void startServer() {
+
ConfigurationProperties.dynamicallyCreateCertificateAuthorityCertificate(true);
+
ConfigurationProperties.directoryToSaveDynamicSSLCertificate("target/test-classes");
+ ConfigurationProperties.proactivelyInitialiseTLS(true);
+
+ mockServer =
ClientAndServer.startClientAndServer(PortFactory.findFreePort());
+ port = Integer.toString(mockServer.getPort());
+
+ assertNotNull(mockServer);
+ assertTrue(mockServer.isRunning());
+ System.setProperty("KUBERNETES_SERVICE_HOST", host);
+ System.setProperty("KUBERNETES_SERVICE_PORT", port);
+ System.setProperty("KUBERNETES_TOKEN_PATH",
+
KubernetesClientImplTest.class.getClassLoader().getResource("client_token").getPath());
+ URL caPath = KubernetesClientImplTest.class.getClassLoader()
+ .getResource("CertificateAuthorityCertificate.pem");
+ if (caPath != null) {
+ System.setProperty("KUBERNETES_CA_PATH", caPath.getPath());
+ }
+
+ mockServer.when(
+ request()
+ .withMethod("POST")
+ .withPath(API_PATH)
+ .withBody(json(BOB_REQUEST, MatchType.STRICT)))
+ .respond(
+ response()
+ .withStatusCode(HTTP_CREATED)
+ .withBody(UNAUTH_JSON));
+
+ mockServer.when(
+ request()
+ .withMethod("POST")
+ .withPath(API_PATH)
+ .withBody(json(KERMIT_REQUEST, MatchType.STRICT)))
+ .respond(
+ response()
+ .withStatusCode(HTTP_CREATED)
+ .withBody(AUTH_JSON));
+
+ mockServer.when(
+ request()
+ .withMethod("POST")
+ .withPath(API_PATH))
+ .respond(
+ response()
+ .withStatusCode(HTTP_INTERNAL_ERROR));
+
+ }
+
+ @AfterClass
+ public static void stopServer() {
+ mockServer.stop();
+ }
+
+ @Test
+ public void testGetTokenReview() {
+
+ KubernetesClient client = new KubernetesClientImpl();
+
+ TokenReview tr = client.getTokenReview("bob_token");
+ assertNotNull(tr);
+ assertFalse(tr.isAuthenticated());
+ assertNull(tr.getUser());
+ assertNull(tr.getUsername());
+
+ tr = client.getTokenReview("kermit_token");
+ assertNotNull(tr);
+ assertNotNull(tr.getUser());
+ assertThat(tr.getUsername(), is(USERNAME));
+ assertThat(tr.getUser().getUsername(), is(USERNAME));
+
+ tr = client.getTokenReview("other");
+ assertNotNull(tr);
+ assertFalse(tr.isAuthenticated());
+
+ mockServer.verify(request().withPath(API_PATH),
VerificationTimes.exactly(3));
+
+ }
+
+}
diff --git
a/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/kubernetes/model/ServiceAccountPrincipalTest.java
b/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/kubernetes/model/ServiceAccountPrincipalTest.java
new file mode 100644
index 0000000000..5f48c3282b
--- /dev/null
+++
b/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/kubernetes/model/ServiceAccountPrincipalTest.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.activemq.artemis.spi.core.security.jaas.kubernetes.model;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertNull;
+
+import
org.apache.activemq.artemis.spi.core.security.jaas.ServiceAccountPrincipal;
+import org.junit.Test;
+
+public class ServiceAccountPrincipalTest {
+
+ @Test
+ public void testFullName() {
+ String name = "system:serviceaccounts:some-ns:some-sa";
+
+ ServiceAccountPrincipal principal = new ServiceAccountPrincipal(name);
+
+ assertThat(principal.getNamespace(), is("some-ns"));
+ assertThat(principal.getSaName(), is("some-sa"));
+ assertThat(principal.getName(), is(name));
+ }
+
+ @Test
+ public void testSimpleName() {
+ String name = "foo";
+
+ ServiceAccountPrincipal principal = new ServiceAccountPrincipal(name);
+
+ assertThat(principal.getName(), is("foo"));
+ assertNull(principal.getSaName());
+ assertNull(principal.getNamespace());
+ }
+
+}
diff --git
a/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/kubernetes/model/TokenReviewTest.java
b/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/kubernetes/model/TokenReviewTest.java
new file mode 100644
index 0000000000..e3086bc38a
--- /dev/null
+++
b/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/kubernetes/model/TokenReviewTest.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.activemq.artemis.spi.core.security.jaas.kubernetes.model;
+
+import static
org.apache.activemq.artemis.spi.core.security.jaas.KubernetesLoginModuleTest.USERNAME;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.hamcrest.Matchers;
+import org.junit.Test;
+
+public class TokenReviewTest {
+
+ @Test
+ public void testEmpty() {
+ String json = "{}";
+ TokenReview tr = TokenReview.fromJsonString(json);
+
+ assertFalse(tr.isAuthenticated());
+ assertNull(tr.getUser());
+ assertNull(tr.getUsername());
+ }
+
+ @Test
+ public void testSimple() {
+ String json = "{\"status\": {\"authenticated\": true, \"user\":
{\"username\": \"" + USERNAME + "\"}}}";
+
+ TokenReview tr = TokenReview.fromJsonString(json);
+
+ assertNotNull(tr);
+ assertTrue(tr.isAuthenticated());
+ assertThat(tr.getUsername(), is(USERNAME));
+ assertNotNull(tr.getUser());
+ assertThat(tr.getUser().getUsername(), is(USERNAME));
+ assertThat(tr.getAudiences(), Matchers.empty());
+ assertNull(tr.getUser().getExtra());
+ }
+
+ @Test
+ public void testCompleteObject() {
+ String json = "{\"status\": {"
+ + "\"authenticated\": true, "
+ + "\"user\": {"
+ + " \"username\": \"" + USERNAME + "\","
+ + " \"uid\": \"kermit-uid\","
+ + " \"groups\": ["
+ + " \"group-1\","
+ + " \"group-2\""
+ + " ],"
+ + " \"extra\": {"
+ + " \"authentication.kubernetes.io/pod-name\": ["
+ + " \"pod-1\","
+ + " \"pod-2\""
+ + " ],"
+ + " \"authentication.kubernetes.io/pod-uid\": ["
+ + " \"pod-uid-1\","
+ + " \"pod-uid-2\""
+ + " ]"
+ + " }"
+ + "},"
+ + "\"audiences\": ["
+ + " \"audience-1\","
+ + " \"audience-2\""
+ + "]}}";
+
+ TokenReview tr = TokenReview.fromJsonString(json);
+
+ assertNotNull(tr);
+ assertTrue(tr.isAuthenticated());
+ assertThat(tr.getUsername(), is(USERNAME));
+ assertNotNull(tr.getUser());
+ assertThat(tr.getUser().getUsername(), is(USERNAME));
+ assertThat(tr.getAudiences(), containsInAnyOrder("audience-1",
"audience-2"));
+ assertThat(tr.getUser().getGroups(), containsInAnyOrder("group-1",
"group-2"));
+ assertThat(tr.getUser().getUid(), is("kermit-uid"));
+
+ assertNotNull(tr.getUser().getExtra());
+ assertThat(tr.getUser().getExtra().getPodNames(),
containsInAnyOrder("pod-1", "pod-2"));
+ assertThat(tr.getUser().getExtra().getPodUids(),
containsInAnyOrder("pod-uid-1", "pod-uid-2"));
+
+ }
+
+}
diff --git a/artemis-server/src/test/resources/client_token
b/artemis-server/src/test/resources/client_token
new file mode 100644
index 0000000000..a2909ae96c
--- /dev/null
+++ b/artemis-server/src/test/resources/client_token
@@ -0,0 +1,17 @@
+## ---------------------------------------------------------------------------
+## Licensed to the Apache Software Foundation (ASF) under one or more
+## contributor license agreements. See the NOTICE file distributed with
+## this work for additional information regarding copyright ownership.
+## The ASF licenses this file to You under the Apache License, Version 2.0
+## (the "License"); you may not use this file except in compliance with
+## the License. You may obtain a copy of the License at
+##
+## http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+## ---------------------------------------------------------------------------
+test_token
\ No newline at end of file
diff --git a/artemis-server/src/test/resources/k8s-roles.properties
b/artemis-server/src/test/resources/k8s-roles.properties
new file mode 100644
index 0000000000..fe7330316f
--- /dev/null
+++ b/artemis-server/src/test/resources/k8s-roles.properties
@@ -0,0 +1,20 @@
+## ---------------------------------------------------------------------------
+## Licensed to the Apache Software Foundation (ASF) under one or more
+## contributor license agreements. See the NOTICE file distributed with
+## this work for additional information regarding copyright ownership.
+## The ASF licenses this file to You under the Apache License, Version 2.0
+## (the "License"); you may not use this file except in compliance with
+## the License. You may obtain a copy of the License at
+##
+## http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+## ---------------------------------------------------------------------------
+
+admin=system:serviceaccounts:some-ns:kermit
+user=system:serviceaccounts:some-ns:gonzo,serviceaccounts:some-ns:joe
+muppet=system:serviceaccounts:some-ns:kermit,system:serviceaccounts:some-ns:gonzo
\ No newline at end of file
diff --git a/docs/user-manual/en/security.md b/docs/user-manual/en/security.md
index 7d49adbf0c..2cdb4692e5 100644
--- a/docs/user-manual/en/security.md
+++ b/docs/user-manual/en/security.md
@@ -1056,6 +1056,37 @@
org.apache.activemq.artemis.spi.core.security.jaas.Krb5LoginModule required
The simplest way to make the login configuration available to JAAS is to add
the directory containing the file, `login.config`, to your CLASSPATH.
+#### KubernetesLoginModule
+
+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`.
+
+- `org.apache.activemq.jaas.kubernetes.role` - the path to the file which
+ contains user and role mapping
+
+- `reload` - boolean flag; whether or not to reload the properties files 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
+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:
+
+```properties
+admins=system:serviceaccounts:example-ns:admin-sa
+users=system:serviceaccounts:other-ns:test-sa
+```
+
### SCRAM-SHA SASL Mechanism
SCRAM (Salted Challenge Response Authentication Mechanism) is an
authentication mechanism that can establish mutual
diff --git a/pom.xml b/pom.xml
index 51e76bfbf8..5f6269bcdf 100644
--- a/pom.xml
+++ b/pom.xml
@@ -169,6 +169,7 @@
<groovy.version>4.0.5</groovy.version>
<vertx.version>4.3.3</vertx.version>
<hadoop.minikdc.version>3.3.1</hadoop.minikdc.version>
+ <mockserver.version>5.13.2</mockserver.version>
<owasp.version>6.1.0</owasp.version>
<spring.version>5.3.20</spring.version>
@@ -232,7 +233,7 @@
<directory-version>2.0.0.AM25</directory-version>
<directory-jdbm2-version>2.0.0-M1</directory-jdbm2-version>
- <bcprov-jdk15on-version>1.69</bcprov-jdk15on-version>
+ <bcprov-jdk15on-version>1.70</bcprov-jdk15on-version>
<netty-transport-native-epoll-classifier>linux-x86_64</netty-transport-native-epoll-classifier>
<netty-transport-native-kqueue-classifier>osx-x86_64</netty-transport-native-kqueue-classifier>