This commit is contained in:
Clebert Suconic 2019-08-25 23:57:20 -04:00
commit 84ccb7e491
6 changed files with 345 additions and 3 deletions

View File

@ -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;
}
}

View File

@ -308,8 +308,9 @@ public class LDAPLoginModule implements LoginModule {
throw ex; throw ex;
} }
if (!isLoginPropertySet(USER_SEARCH_MATCHING)) if (!isLoginPropertySet(USER_SEARCH_MATCHING)) {
return dn; return username;
}
userSearchMatchingFormat = new MessageFormat(getLDAPPropertyValue(USER_SEARCH_MATCHING)); userSearchMatchingFormat = new MessageFormat(getLDAPPropertyValue(USER_SEARCH_MATCHING));
userSearchSubtreeBool = Boolean.valueOf(getLDAPPropertyValue(USER_SEARCH_SUBTREE)).booleanValue(); userSearchSubtreeBool = Boolean.valueOf(getLDAPPropertyValue(USER_SEARCH_SUBTREE)).booleanValue();

View File

@ -910,6 +910,30 @@ users=system,user
guests=guest 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 simplest way to make the login configuration available to JAAS is to add
the directory containing the file, `login.config`, to your CLASSPATH. the directory containing the file, `login.config`, to your CLASSPATH.

View File

@ -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();
}
}
}

View File

@ -117,3 +117,10 @@ objectclass: groupOfUniqueNames
objectclass: top objectclass: top
cn: admin cn: admin
uniquemember: uid=role1 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

View File

@ -176,6 +176,26 @@ Krb5Plus {
org.apache.activemq.jaas.properties.role="dual-authentication-roles.properties"; 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 { Krb5PlusLdap {
org.apache.activemq.artemis.spi.core.security.jaas.Krb5LoginModule required org.apache.activemq.artemis.spi.core.security.jaas.Krb5LoginModule required