This closes #2768
This commit is contained in:
commit
84ccb7e491
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue