This is an automated email from the ASF dual-hosted git repository.
clebertsuconic pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/activemq-artemis.git
The following commit(s) were added to refs/heads/master by this push:
new b20c259 ARTEMIS-2433 add ExternalCertificateLoginModule to surface a
SASL EXTERNAL identity (subjectDN) to JAAS.
new 84ccb7e This closes #2768
b20c259 is described below
commit b20c2593e99fc6de384af64554ae8266b3323998
Author: gtully <[email protected]>
AuthorDate: Fri Jul 26 10:34:55 2019 +0100
ARTEMIS-2433 add ExternalCertificateLoginModule to surface a SASL EXTERNAL
identity (subjectDN) to JAAS.
---
.../jaas/ExternalCertificateLoginModule.java | 109 +++++++++++++
.../spi/core/security/jaas/LDAPLoginModule.java | 5 +-
docs/user-manual/en/security.md | 24 +++
.../integration/amqp/JMSSaslExternalLDAPTest.java | 181 +++++++++++++++++++++
.../src/test/resources/AMQauth.ldif | 9 +-
.../src/test/resources/login.config | 20 +++
6 files changed, 345 insertions(+), 3 deletions(-)
diff --git
a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/ExternalCertificateLoginModule.java
b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/ExternalCertificateLoginModule.java
new file mode 100644
index 0000000..81d34c9
--- /dev/null
+++
b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/ExternalCertificateLoginModule.java
@@ -0,0 +1,109 @@
+/*
+ * 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 javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.LoginException;
+import javax.security.auth.spi.LoginModule;
+import javax.security.cert.X509Certificate;
+import java.io.IOException;
+import java.security.Principal;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.jboss.logging.Logger;
+
+/**
+ * A LoginModule that propagates TLS certificates subject DN as a
UserPrincipal.
+ */
+public class ExternalCertificateLoginModule implements LoginModule {
+
+ private static final Logger logger =
Logger.getLogger(ExternalCertificateLoginModule.class);
+
+ private CallbackHandler callbackHandler;
+ private Subject subject;
+ private String userName;
+
+ private final Set<Principal> principals = new HashSet<>();
+
+ @Override
+ public void initialize(Subject subject,
+ CallbackHandler callbackHandler,
+ Map<String, ?> sharedState,
+ Map<String, ?> options) {
+ this.subject = subject;
+ this.callbackHandler = callbackHandler;
+ }
+
+ @Override
+ public boolean login() throws LoginException {
+ Callback[] callbacks = new Callback[1];
+
+ callbacks[0] = new CertificateCallback();
+ try {
+ callbackHandler.handle(callbacks);
+ } catch (IOException ioe) {
+ throw new LoginException(ioe.getMessage());
+ } catch (UnsupportedCallbackException uce) {
+ throw new LoginException("Unable to obtain client certificates: " +
uce.getMessage());
+ }
+
+ X509Certificate[] certificates = ((CertificateCallback)
callbacks[0]).getCertificates();
+ if (certificates != null && certificates.length > 0 && certificates[0]
!= null) {
+ userName = certificates[0].getSubjectDN().getName();
+ }
+
+ logger.debug("Certificates: " + Arrays.toString(certificates) + ",
userName: " + userName);
+ return userName != null;
+ }
+
+ @Override
+ public boolean commit() throws LoginException {
+ if (userName != null) {
+ principals.add(new UserPrincipal(userName));
+ subject.getPrincipals().addAll(principals);
+ }
+
+ clear();
+ logger.debug("commit");
+ return true;
+ }
+
+ @Override
+ public boolean abort() throws LoginException {
+ clear();
+ logger.debug("abort");
+ return true;
+ }
+
+ @Override
+ public boolean logout() {
+ subject.getPrincipals().removeAll(principals);
+ principals.clear();
+ logger.debug("logout");
+ return true;
+ }
+
+ private void clear() {
+ userName = null;
+ }
+}
diff --git
a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java
b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java
index a098ccd..b56fde8 100644
---
a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java
+++
b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java
@@ -308,8 +308,9 @@ public class LDAPLoginModule implements LoginModule {
throw ex;
}
- if (!isLoginPropertySet(USER_SEARCH_MATCHING))
- return dn;
+ if (!isLoginPropertySet(USER_SEARCH_MATCHING)) {
+ return username;
+ }
userSearchMatchingFormat = new
MessageFormat(getLDAPPropertyValue(USER_SEARCH_MATCHING));
userSearchSubtreeBool =
Boolean.valueOf(getLDAPPropertyValue(USER_SEARCH_SUBTREE)).booleanValue();
diff --git a/docs/user-manual/en/security.md b/docs/user-manual/en/security.md
index 2263f20..6a45271 100644
--- a/docs/user-manual/en/security.md
+++ b/docs/user-manual/en/security.md
@@ -910,6 +910,30 @@ users=system,user
guests=guest
```
+
+#### Krb5LoginModule
+
+The Kerberos login module is used to propagate a validated SASL GSSAPI
kerberos token
+identity into a validated JAAS UserPrincipal. This allows subsequent login
modules to
+do role mapping for the kerberos identity.
+
+```
+org.apache.activemq.artemis.spi.core.security.jaas.Krb5LoginModule required
+ ;
+```
+
+#### ExternalCertificateLoginModule
+
+The external certificate login module is used to propagate a validated TLS
client
+certificate's subjectDN into a JAAS UserPrincipal. This allows subsequent
login modules to
+do role mapping for the TLS client certificate.
+
+```
+org.apache.activemq.artemis.spi.core.security.jaas.ExternalCertificateLoginModule
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.
diff --git
a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/JMSSaslExternalLDAPTest.java
b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/JMSSaslExternalLDAPTest.java
new file mode 100644
index 0000000..6320b4f
--- /dev/null
+++
b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/JMSSaslExternalLDAPTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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.tests.integration.amqp;
+
+import javax.jms.Connection;
+import javax.jms.MessageConsumer;
+import javax.jms.MessageProducer;
+import javax.jms.Session;
+import javax.jms.TextMessage;
+import java.io.File;
+import java.lang.management.ManagementFactory;
+import java.net.URI;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.activemq.artemis.api.core.TransportConfiguration;
+import org.apache.activemq.artemis.core.config.Configuration;
+import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl;
+import org.apache.activemq.artemis.core.remoting.impl.invm.InVMAcceptorFactory;
+import
org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory;
+import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants;
+import org.apache.activemq.artemis.core.security.Role;
+import org.apache.activemq.artemis.core.server.ActiveMQServer;
+import org.apache.activemq.artemis.core.server.ActiveMQServers;
+import
org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager;
+import org.apache.activemq.artemis.tests.util.ActiveMQTestBase;
+import org.apache.activemq.artemis.utils.RandomUtil;
+import org.apache.directory.server.annotations.CreateLdapServer;
+import org.apache.directory.server.annotations.CreateTransport;
+import org.apache.directory.server.core.annotations.ApplyLdifFiles;
+import org.apache.directory.server.core.integ.AbstractLdapTestUnit;
+import org.apache.directory.server.core.integ.FrameworkRunner;
+import org.apache.qpid.jms.JmsConnectionFactory;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertEquals;
+
+@RunWith(FrameworkRunner.class)
+@CreateLdapServer(transports = {@CreateTransport(protocol = "LDAP", port =
1024)})
+@ApplyLdifFiles("AMQauth.ldif")
+public class JMSSaslExternalLDAPTest extends AbstractLdapTestUnit {
+
+ static {
+ String path = System.getProperty("java.security.auth.login.config");
+ if (path == null) {
+ URL resource =
JMSSaslExternalTest.class.getClassLoader().getResource("login.config");
+ if (resource != null) {
+ path = resource.getFile();
+ System.setProperty("java.security.auth.login.config", path);
+ }
+ }
+ }
+
+ private ActiveMQServer server;
+ private final boolean debug = false;
+
+ public static final String TARGET_TMP = "./target/tmp";
+
+ public JMSSaslExternalLDAPTest() {
+ super();
+ File parent = new File(TARGET_TMP);
+ parent.mkdirs();
+ temporaryFolder = new TemporaryFolder(parent);
+ }
+
+ @Rule
+ public TemporaryFolder temporaryFolder;
+ private String testDir;
+
+ @Before
+ public void setUp() throws Exception {
+
+ testDir = temporaryFolder.getRoot().getAbsolutePath();
+
+ if (debug) {
+ for (java.util.logging.Logger logger : new java.util.logging.Logger[]
{java.util.logging.Logger.getLogger("javax.security.sasl"),
java.util.logging.Logger.getLogger("org.apache.qpid.proton")}) {
+ logger.setLevel(java.util.logging.Level.FINEST);
+ logger.addHandler(new java.util.logging.ConsoleHandler());
+ for (java.util.logging.Handler handler : logger.getHandlers()) {
+ handler.setLevel(java.util.logging.Level.FINEST);
+ }
+ }
+ }
+ }
+
+
+ @Before
+ public void startServer() throws Exception {
+
+ ActiveMQJAASSecurityManager securityManager = new
ActiveMQJAASSecurityManager("SaslExternalPlusLdap");
+ Configuration configuration = new
ConfigurationImpl().setSecurityEnabled(true).addAcceptorConfiguration(new
TransportConfiguration(InVMAcceptorFactory.class.getCanonicalName())).setJournalDirectory(ActiveMQTestBase.getJournalDir(testDir,
0, false)).setBindingsDirectory(ActiveMQTestBase.getBindingsDir(testDir, 0,
false)).setPagingDirectory(ActiveMQTestBase.getPageDir(testDir, 0,
false)).setLargeMessagesDirectory(ActiveMQTestBase.getLargeMessagesDir(testDir,
0, false));
+ server = ActiveMQServers.newActiveMQServer(configuration,
ManagementFactory.getPlatformMBeanServer(), securityManager, false);
+
+
+ Map<String, Object> params = new HashMap<>();
+ params.put(TransportConstants.SSL_ENABLED_PROP_NAME, true);
+ params.put(TransportConstants.KEYSTORE_PATH_PROP_NAME, "keystore1.jks");
+ params.put(TransportConstants.KEYSTORE_PASSWORD_PROP_NAME, "changeit");
+ params.put(TransportConstants.TRUSTSTORE_PATH_PROP_NAME,
"truststore.jks");
+ params.put(TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME, "changeit");
+ params.put(TransportConstants.NEED_CLIENT_AUTH_PROP_NAME, true);
+
+ Map<String, Object> extraParams = new HashMap<>();
+ extraParams.put("saslMechanisms", "EXTERNAL");
+
+ server.getConfiguration().addAcceptorConfiguration(new
TransportConfiguration(NettyAcceptorFactory.class.getCanonicalName(), params,
"netty", extraParams));
+
+ // role mapping via CertLogin - TextFileCertificateLoginModule
+ final String roleName = "widgets";
+ Role role = new Role(roleName, true, true, true, true, true, true, true,
true, true, true);
+ Set<Role> roles = new HashSet<>();
+ roles.add(role);
+ server.getSecurityRepository().addMatch("TEST", roles);
+
+ server.start();
+ }
+
+ @After
+ public void stopServer() throws Exception {
+ server.stop();
+ }
+
+ @Test(timeout = 600000)
+ public void testRoundTrip() throws Exception {
+
+ final String keystore =
this.getClass().getClassLoader().getResource("client_not_revoked.jks").getFile();
+ final String truststore =
this.getClass().getClassLoader().getResource("truststore.jks").getFile();
+
+ String connOptions = "?amqp.saslMechanisms=EXTERNAL" + "&" +
+ "transport.trustStoreLocation=" + truststore + "&" +
+ "transport.trustStorePassword=changeit" + "&" +
+ "transport.keyStoreLocation=" + keystore + "&" +
+ "transport.keyStorePassword=changeit" + "&" +
+ "transport.verifyHost=false";
+
+ JmsConnectionFactory factory = new JmsConnectionFactory(new
URI("amqps://localhost:" + 61616 + connOptions));
+ Connection connection = factory.createConnection("client", null);
+ connection.start();
+
+ try {
+ Session session = connection.createSession(false,
Session.AUTO_ACKNOWLEDGE);
+ javax.jms.Queue queue = session.createQueue("TEST");
+ MessageConsumer consumer = session.createConsumer(queue);
+ MessageProducer producer = session.createProducer(queue);
+
+ final String text = RandomUtil.randomString();
+ producer.send(session.createTextMessage(text));
+
+ TextMessage m = (TextMessage) consumer.receive(1000);
+ assertNotNull(m);
+ assertEquals(text, m.getText());
+
+ } finally {
+ connection.close();
+ }
+ }
+
+}
diff --git a/tests/integration-tests/src/test/resources/AMQauth.ldif
b/tests/integration-tests/src/test/resources/AMQauth.ldif
index 70e11f3..9036dfe 100755
--- a/tests/integration-tests/src/test/resources/AMQauth.ldif
+++ b/tests/integration-tests/src/test/resources/AMQauth.ldif
@@ -116,4 +116,11 @@ dn:
cn=admin,uid=activemq.management,ou=queues,ou=destinations,o=ActiveMQ,ou=sys
objectclass: groupOfUniqueNames
objectclass: top
cn: admin
-uniquemember: uid=role1
\ No newline at end of file
+uniquemember: uid=role1
+
+## group with member identified just by DN from SASL external tls certificate
subject DN
+dn: cn=widgets,ou=system
+cn: widgets
+member: uid=O=Internet Widgits Pty Ltd,C=AU,ST=Some-State,CN=lakalkalaoioislkxn
+objectClass: groupOfNames
+objectClass: top
\ No newline at end of file
diff --git a/tests/integration-tests/src/test/resources/login.config
b/tests/integration-tests/src/test/resources/login.config
index fb70205..a70836b 100644
--- a/tests/integration-tests/src/test/resources/login.config
+++ b/tests/integration-tests/src/test/resources/login.config
@@ -176,6 +176,26 @@ Krb5Plus {
org.apache.activemq.jaas.properties.role="dual-authentication-roles.properties";
};
+SaslExternalPlusLdap {
+
+
org.apache.activemq.artemis.spi.core.security.jaas.ExternalCertificateLoginModule
required
+ debug=true;
+
+ org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule optional
+ debug=true
+ initialContextFactory=com.sun.jndi.ldap.LdapCtxFactory
+ connectionURL="ldap://localhost:1024"
+ connectionUsername="uid=admin,ou=system"
+ connectionPassword=secret
+ connectionProtocol=s
+ authentication=simple
+ authenticateUser=false
+ roleBase="ou=system"
+ roleName=cn
+ roleSearchMatching="(member=uid={1})"
+ ;
+};
+
Krb5PlusLdap {
org.apache.activemq.artemis.spi.core.security.jaas.Krb5LoginModule required