This closes #188

This commit is contained in:
Clebert Suconic 2015-10-09 15:57:53 -04:00
commit 1b49559c64
62 changed files with 5169 additions and 266 deletions

View File

@ -70,8 +70,16 @@ public class Create extends InputAbstract {
public static final String ETC_LOGGING_PROPERTIES = "etc/logging.properties";
public static final String ETC_BOOTSTRAP_XML = "etc/bootstrap.xml";
public static final String ETC_BROKER_XML = "etc/broker.xml";
// The JAAS PropertiesLogin module uses role=user(s) syntax, but the basic security uses user=role(s) syntax so we need 2 different files here
public static final String ETC_ARTEMIS_ROLES_PROPERTIES = "etc/artemis-roles.properties";
public static final String ETC_ARTEMIS_ROLES_BASIC_PROPERTIES = "etc/artemis-roles-basic.properties";
public static final String ETC_ARTEMIS_ROLES_JAAS_PROPERTIES = "etc/artemis-roles-jaas.properties";
public static final String ETC_ARTEMIS_USERS_PROPERTIES = "etc/artemis-users.properties";
public static final String ETC_JAAS_BROKER_SECURITY_SETTINGS_TXT = "etc/jaas-broker-security-settings.txt";
public static final String ETC_BASIC_BROKER_SECURITY_SETTINGS_TXT = "etc/basic-broker-security-settings.txt";
public static final String ETC_LOGIN_CONFIG = "etc/login.config";
public static final String ETC_REPLICATED_SETTINGS_TXT = "etc/replicated-settings.txt";
public static final String ETC_SHARED_STORE_SETTINGS_TXT = "etc/shared-store-settings.txt";
public static final String ETC_CLUSTER_SECURITY_SETTINGS_TXT = "etc/cluster-security-settings.txt";
@ -161,10 +169,24 @@ public class Create extends InputAbstract {
@Option(name = "--aio", description = "Force aio journal on the configuration regardless of the library being available or not.")
boolean forceLibaio;
@Option(name = "--broker-security", description = "Use basic, file-based security or JAAS login module for broker security (Default: basic)")
String brokerSecurity;
boolean IS_WINDOWS;
boolean IS_CYGWIN;
public String getBrokerSecurity() {
if (brokerSecurity == null) {
brokerSecurity = "basic";
}
return brokerSecurity;
}
public void setBrokerSecurity(String security) {
this.brokerSecurity = security;
}
public int getMaxHops() {
return maxHops;
}
@ -535,6 +557,29 @@ public class Create extends InputAbstract {
filters.put("${java-opts}", javaOptions);
if (isAllowAnonymous()) {
filters.put("${bootstrap.guest}", "default-user=\"" + getUser() + "\"");
}
else {
filters.put("${bootstrap.guest}", "");
}
if (brokerSecurity != null && brokerSecurity.equalsIgnoreCase("jaas")) {
filters.put("${broker-security-settings}", applyFilters(readTextFile(ETC_JAAS_BROKER_SECURITY_SETTINGS_TXT), filters));
filters.put("${login-config}", "-Djava.security.auth.login.config=" + path(directory, false) + "/etc/login.config");
write(ETC_LOGIN_CONFIG, filters, false);
write(ETC_ARTEMIS_ROLES_JAAS_PROPERTIES, filters, false);
File file = new File(directory, ETC_ARTEMIS_ROLES_JAAS_PROPERTIES);
file.renameTo(new File(directory, ETC_ARTEMIS_ROLES_PROPERTIES));
}
else {
filters.put("${broker-security-settings}", applyFilters(readTextFile(ETC_BASIC_BROKER_SECURITY_SETTINGS_TXT), filters));
filters.put("${login-config}", "");
write(ETC_ARTEMIS_ROLES_BASIC_PROPERTIES, filters, false);
File file = new File(directory, ETC_ARTEMIS_ROLES_BASIC_PROPERTIES);
file.renameTo(new File(directory, ETC_ARTEMIS_ROLES_PROPERTIES));
}
if (IS_WINDOWS) {
write(BIN_ARTEMIS_CMD, null, false);
write(BIN_ARTEMIS_SERVICE_EXE);
@ -553,13 +598,6 @@ public class Create extends InputAbstract {
write(ETC_LOGGING_PROPERTIES, null, false);
if (isAllowAnonymous()) {
filters.put("${bootstrap.guest}", "default-user=\"" + getUser() + "\"");
}
else {
filters.put("${bootstrap.guest}", "");
}
if (noWeb) {
filters.put("${bootstrap-web-settings}", "");
}
@ -571,7 +609,6 @@ public class Create extends InputAbstract {
write(ETC_BOOTSTRAP_XML, filters, false);
write(ETC_BROKER_XML, filters, false);
write(ETC_ARTEMIS_ROLES_PROPERTIES, filters, false);
write(ETC_ARTEMIS_USERS_PROPERTIES, filters, false);
context.out.println("");

View File

@ -0,0 +1,32 @@
/*
* 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.factory;
import org.apache.activemq.artemis.dto.JaasSecurityDTO;
import org.apache.activemq.artemis.dto.SecurityDTO;
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager;
import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager;
public class JaasSecurityHandler implements SecurityHandler {
@Override
public ActiveMQSecurityManager createSecurityManager(SecurityDTO security) throws Exception {
JaasSecurityDTO jaasSecurity = (JaasSecurityDTO) security;
ActiveMQJAASSecurityManager securityManager = new ActiveMQJAASSecurityManager();
securityManager.setConfigurationName(jaasSecurity.loginModule);
return securityManager;
}
}

View File

@ -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.
## ---------------------------------------------------------------------------
class=org.apache.activemq.artemis.factory.JaasSecurityHandler

View File

@ -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.
## ---------------------------------------------------------------------------
${role}=${user}

View File

@ -23,7 +23,7 @@ ARTEMIS_HOME='${artemis.home}'
# Java Opts
JAVA_ARGS="-XX:+UseParallelGC -XX:+AggressiveOpts -XX:+UseFastAccessorMethods -Xms512M -Xmx1024M -Xbootclasspath/a:$ARTEMIS_HOME/lib/${logmanager} -Djava.util.logging.manager=org.jboss.logmanager.LogManager ${java-opts}"
JAVA_ARGS="-XX:+UseParallelGC -XX:+AggressiveOpts -XX:+UseFastAccessorMethods -Xms512M -Xmx1024M -Xbootclasspath/a:$ARTEMIS_HOME/lib/${logmanager} -Djava.util.logging.manager=org.jboss.logmanager.LogManager ${login-config} ${java-opts}"
# Debug args: Uncomment to enable debug

View File

@ -21,7 +21,7 @@ rem Cluster Properties: Used to pass arguments to ActiveMQ Artemis which can be
rem set ARTEMIS_CLUSTER_PROPS=-Dactivemq.remoting.default.port=61617 -Dactivemq.remoting.amqp.port=5673 -Dactivemq.remoting.stomp.port=61614 -Dactivemq.remoting.hornetq.port=5446
rem Java Opts
set JAVA_ARGS=-XX:+UseParallelGC -XX:+AggressiveOpts -XX:+UseFastAccessorMethods -Xms512M -Xmx1024M ${java-opts}
set JAVA_ARGS=-XX:+UseParallelGC -XX:+AggressiveOpts -XX:+UseFastAccessorMethods -Xms512M -Xmx1024M ${login-config} ${java-opts}
rem Debug args: Uncomment to enable debug
rem set DEBUG_ARGS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005

View File

@ -0,0 +1,5 @@
<basic-security
users="file:${artemis.instance}/etc/artemis-users.properties"
roles="file:${artemis.instance}/etc/artemis-roles.properties"
${bootstrap.guest}/>

View File

@ -18,10 +18,7 @@
<broker xmlns="http://activemq.org/schema">
<basic-security
users="file:${artemis.instance}/etc/artemis-users.properties"
roles="file:${artemis.instance}/etc/artemis-roles.properties"
${bootstrap.guest}/>
${broker-security-settings}
<server configuration="file:${artemis.instance}/etc/broker.xml"/>

View File

@ -0,0 +1,2 @@
<jaas-security login-module="PropertiesLogin"/>

View File

@ -0,0 +1,22 @@
/*
* 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.
*/
PropertiesLogin {
org.apache.activemq.artemis.spi.core.security.jaas.PropertiesLoginModule required
debug=true
org.apache.activemq.jaas.properties.user="artemis-users.properties"
org.apache.activemq.jaas.properties.role="artemis-roles.properties";
};

View File

@ -40,7 +40,8 @@ public class StreamClassPathTest {
openStream(Create.ETC_LOGGING_PROPERTIES);
openStream(Create.ETC_BOOTSTRAP_XML);
openStream(Create.ETC_BROKER_XML);
openStream(Create.ETC_ARTEMIS_ROLES_PROPERTIES);
openStream(Create.ETC_ARTEMIS_ROLES_BASIC_PROPERTIES);
openStream(Create.ETC_ARTEMIS_ROLES_JAAS_PROPERTIES);
openStream(Create.ETC_ARTEMIS_USERS_PROPERTIES);
openStream(Create.ETC_REPLICATED_SETTINGS_TXT);
openStream(Create.ETC_REPLICATED_SETTINGS_TXT);
@ -50,6 +51,8 @@ public class StreamClassPathTest {
openStream(Create.ETC_CONNECTOR_SETTINGS_TXT);
openStream(Create.ETC_BOOTSTRAP_WEB_SETTINGS_TXT);
openStream(Create.ETC_JOURNAL_BUFFER_SETTINGS);
openStream(Create.ETC_JAAS_BROKER_SECURITY_SETTINGS_TXT);
openStream(Create.ETC_BASIC_BROKER_SECURITY_SETTINGS_TXT);
}
private void openStream(String source) throws Exception {

View File

@ -0,0 +1,30 @@
/*
* 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.dto;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement(name = "jaas-security")
@XmlAccessorType(XmlAccessType.FIELD)
public class JaasSecurityDTO extends SecurityDTO {
@XmlAttribute(name = "login-module", required = true)
public String loginModule;
}

View File

@ -2,19 +2,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 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
## See the License for the specific language governing permissions and
## limitations under the License.
## ---------------------------------------------------------------------------
BrokerDTO
SecurityDTO
BasicSecurityDTO
JaasSecurityDTO

View File

@ -113,6 +113,9 @@ public class ArtemisCreatePlugin extends ArtemisAbstractPlugin {
@Parameter(defaultValue = "ON_DEMAND")
private String messageLoadBalancing;
@Parameter(defaultValue = "basic")
private String brokerSecurity;
/**
* For extra stuff not covered by the properties
*/
@ -200,7 +203,7 @@ public class ArtemisCreatePlugin extends ArtemisAbstractPlugin {
ArrayList<String> listCommands = new ArrayList<>();
add(listCommands, "create", "--allow-anonymous", "--silent", "--force", "--no-web", "--user", user, "--password", password, "--role", role, "--port-offset", "" + portOffset, "--data", dataFolder);
add(listCommands, "create", "--allow-anonymous", "--silent", "--force", "--no-web", "--user", user, "--password", password, "--role", role, "--port-offset", "" + portOffset, "--data", dataFolder, "--broker-security", brokerSecurity);
if (allowAnonymous) {
add(listCommands, "--allow-anonymous");

View File

@ -82,6 +82,24 @@
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-server-integ</artifactId>
<version>${directory-version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-core-integ</artifactId>
<version>${directory-version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>bouncycastle</groupId>
<artifactId>bcprov-jdk15</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<profiles>

View File

@ -0,0 +1,114 @@
/*
* 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;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import java.security.Principal;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import org.apache.activemq.artemis.core.security.CheckType;
import org.apache.activemq.artemis.core.security.Role;
import org.apache.activemq.artemis.core.server.ActiveMQServerLogger;
import org.apache.activemq.artemis.spi.core.security.jaas.JaasCredentialCallbackHandler;
import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal;
/**
* This implementation delegates to the JAAS security interfaces.
*
* The {@link Subject} returned by the login context is expecting to have a set of {@link RolePrincipal} for each
* role of the user.
*/
public class ActiveMQJAASSecurityManager implements ActiveMQSecurityManager {
private final boolean trace = ActiveMQServerLogger.LOGGER.isTraceEnabled();
private String configurationName;
public boolean validateUser(final String user, final String password) {
try {
getAuthenticatedSubject(user, password);
return true;
}
catch (LoginException e) {
ActiveMQServerLogger.LOGGER.debug("Couldn't validate user: " + user, e);
return false;
}
}
public boolean validateUserAndRole(final String user,
final String password,
final Set<Role> roles,
final CheckType checkType) {
Subject localSubject;
try {
localSubject = getAuthenticatedSubject(user, password);
}
catch (LoginException e) {
ActiveMQServerLogger.LOGGER.debug("Couldn't validate user: " + user, e);
return false;
}
boolean authorized = false;
if (localSubject != null) {
Set<RolePrincipal> rolesWithPermission = getPrincipalsInRole(checkType, roles);
// Check the caller's roles
Set<RolePrincipal> rolesForSubject = localSubject.getPrincipals(RolePrincipal.class);
if (rolesForSubject.size() > 0 && rolesWithPermission.size() > 0) {
Iterator<RolePrincipal> rolesForSubjectIter = rolesForSubject.iterator();
while (!authorized && rolesForSubjectIter.hasNext()) {
Iterator<RolePrincipal> rolesWithPermissionIter = rolesWithPermission.iterator();
while (!authorized && rolesWithPermissionIter.hasNext()) {
Principal role = rolesWithPermissionIter.next();
authorized = rolesForSubjectIter.next().equals(role);
}
}
}
if (trace) {
ActiveMQServerLogger.LOGGER.trace("user " + user + (authorized ? " is " : " is NOT ") + "authorized");
}
}
return authorized;
}
private Subject getAuthenticatedSubject(final String user, final String password) throws LoginException {
LoginContext lc = new LoginContext(configurationName, new JaasCredentialCallbackHandler(user, password));
lc.login();
return lc.getSubject();
}
private Set<RolePrincipal> getPrincipalsInRole(final CheckType checkType, final Set<Role> roles) {
Set<RolePrincipal> principals = new HashSet<>();
for (Role role : roles) {
if (checkType.hasRole(role)) {
principals.add(new RolePrincipal(role.getName()));
}
}
return principals;
}
public void setConfigurationName(final String configurationName) {
this.configurationName = configurationName;
}
}

View File

@ -46,4 +46,4 @@ public interface ActiveMQSecurityManager2 extends ActiveMQSecurityManager {
* @return true if the user is valid and they have the correct roles for the given destination address
*/
boolean validateUserAndRole(String user, String password, Set<Role> roles, CheckType checkType, String address);
}
}

View File

@ -1,216 +0,0 @@
/*
* 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;
import java.security.Principal;
import java.security.acl.Group;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import org.apache.activemq.artemis.core.security.CheckType;
import org.apache.activemq.artemis.core.security.Role;
import org.apache.activemq.artemis.core.server.ActiveMQServerLogger;
/**
* This implementation delegates to the JAAS security interfaces.
*
* The {@link Subject} returned by the login context is expecting to have a {@link Group} with the <code>Roles</code> name
* containing a set of {@link Principal} for each role of the user.
*/
public class JAASSecurityManager implements ActiveMQSecurityManager {
// Static --------------------------------------------------------
// Attributes ----------------------------------------------------
private final boolean trace = ActiveMQServerLogger.LOGGER.isTraceEnabled();
private String configurationName;
private CallbackHandler callbackHandler;
private Configuration config;
// ActiveMQSecurityManager implementation -----------------------------
public boolean validateUser(final String user, final String password) {
try {
getAuthenticatedSubject(user, password);
return true;
}
catch (LoginException e1) {
return false;
}
}
public boolean validateUserAndRole(final String user,
final String password,
final Set<Role> roles,
final CheckType checkType) {
Subject localSubject = null;
try {
localSubject = getAuthenticatedSubject(user, password);
}
catch (LoginException e1) {
return false;
}
boolean authenticated = true;
if (localSubject != null) {
Set<Principal> rolePrincipals = getRolePrincipals(checkType, roles);
// authenticated = realmMapping.doesUserHaveRole(principal, rolePrincipals);
boolean hasRole = false;
// check that the caller is authenticated to the current thread
// Check the caller's roles
Group subjectRoles = getSubjectRoles(localSubject);
if (subjectRoles != null) {
Iterator<Principal> iter = rolePrincipals.iterator();
while (!hasRole && iter.hasNext()) {
Principal role = iter.next();
hasRole = subjectRoles.isMember(role);
}
}
authenticated = hasRole;
if (trace) {
ActiveMQServerLogger.LOGGER.trace("user " + user + (authenticated ? " is " : " is NOT ") + "authorized");
}
}
return authenticated;
}
private Subject getAuthenticatedSubject(final String user, final String password) throws LoginException {
SimplePrincipal principal = user == null ? null : new SimplePrincipal(user);
char[] passwordChars = null;
if (password != null) {
passwordChars = password.toCharArray();
}
Subject subject = new Subject();
if (user != null) {
subject.getPrincipals().add(principal);
}
subject.getPrivateCredentials().add(passwordChars);
LoginContext lc = new LoginContext(configurationName, subject, callbackHandler, config);
lc.login();
return lc.getSubject();
}
private Group getSubjectRoles(final Subject subject) {
Set<Group> subjectGroups = subject.getPrincipals(Group.class);
Iterator<Group> iter = subjectGroups.iterator();
Group roles = null;
while (iter.hasNext()) {
Group grp = iter.next();
String name = grp.getName();
if (name.equals("Roles")) {
roles = grp;
}
}
return roles;
}
private Set<Principal> getRolePrincipals(final CheckType checkType, final Set<Role> roles) {
Set<Principal> principals = new HashSet<Principal>();
for (Role role : roles) {
if (checkType.hasRole(role)) {
principals.add(new SimplePrincipal(role.getName()));
}
}
return principals;
}
// Public --------------------------------------------------------
public void setConfigurationName(final String configurationName) {
this.configurationName = configurationName;
}
public void setCallbackHandler(final CallbackHandler handler) {
callbackHandler = handler;
}
public void setConfiguration(final Configuration config) {
this.config = config;
}
// Private -------------------------------------------------------
// Inner classes -------------------------------------------------
public static class SimplePrincipal implements Principal, java.io.Serializable {
private static final long serialVersionUID = 1L;
private final String name;
public SimplePrincipal(final String name) {
this.name = name;
}
/**
* Compare this SimplePrincipal's name against another Principal
*
* @return true if name equals another.getName();
*/
@Override
public boolean equals(final Object another) {
if (!(another instanceof Principal)) {
return false;
}
String anotherName = ((Principal) another).getName();
boolean equals = false;
if (name == null) {
equals = anotherName == null;
}
else {
equals = name.equals(anotherName);
}
return equals;
}
@Override
public int hashCode() {
return name == null ? 0 : name.hashCode();
}
@Override
public String toString() {
return name;
}
public String getName() {
return name;
}
}
}

View File

@ -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;
import javax.security.auth.callback.Callback;
import java.security.cert.X509Certificate;
/**
* A Callback for SSL certificates.
*
* Will return a certificate chain to its client.
*/
public class CertificateCallback implements Callback {
X509Certificate[] certificates;
/**
* Setter for certificate chain.
*
* @param certs The certificates to be returned.
*/
public void setCertificates(X509Certificate[] certs) {
certificates = certs;
}
/**
* Getter for certificate chain.
*
* @return The certificates being carried.
*/
public X509Certificate[] getCertificates() {
return certificates;
}
}

View File

@ -0,0 +1,183 @@
/*
* 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.FailedLoginException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import java.io.IOException;
import java.security.Principal;
import java.security.cert.X509Certificate;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.activemq.artemis.core.server.ActiveMQServerLogger;
/**
* A LoginModule that allows for authentication based on SSL certificates.
* Allows for subclasses to define methods used to verify user certificates and
* find user groups. Uses CertificateCallbacks to retrieve certificates.
*/
public abstract class CertificateLoginModule implements LoginModule {
private CallbackHandler callbackHandler;
private Subject subject;
private X509Certificate[] certificates;
private String username;
private Set<String> groups;
private Set<Principal> principals = new HashSet<Principal>();
private boolean debug;
/**
* Overriding to allow for proper initialization. Standard JAAS.
*/
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
this.subject = subject;
this.callbackHandler = callbackHandler;
debug = "true".equalsIgnoreCase((String) options.get("debug"));
if (debug) {
ActiveMQServerLogger.LOGGER.debug("Initialized debug");
}
}
/**
* Overriding to allow for certificate-based login. Standard JAAS.
*/
@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(uce.getMessage() + " Unable to obtain client certificates.");
}
certificates = ((CertificateCallback) callbacks[0]).getCertificates();
username = getUserNameForCertificates(certificates);
if (username == null) {
throw new FailedLoginException("No user for client certificate: " + getDistinguishedName(certificates));
}
groups = getUserGroups(username);
if (debug) {
ActiveMQServerLogger.LOGGER.debug("Certificate for user: " + username);
}
return true;
}
/**
* Overriding to complete login process. Standard JAAS.
*/
@Override
public boolean commit() throws LoginException {
principals.add(new UserPrincipal(username));
for (String group : groups) {
principals.add(new RolePrincipal(group));
}
subject.getPrincipals().addAll(principals);
clear();
if (debug) {
ActiveMQServerLogger.LOGGER.debug("commit");
}
return true;
}
/**
* Standard JAAS override.
*/
@Override
public boolean abort() throws LoginException {
clear();
if (debug) {
ActiveMQServerLogger.LOGGER.debug("abort");
}
return true;
}
/**
* Standard JAAS override.
*/
@Override
public boolean logout() {
subject.getPrincipals().removeAll(principals);
principals.clear();
if (debug) {
ActiveMQServerLogger.LOGGER.debug("logout");
}
return true;
}
/**
* Helper method.
*/
private void clear() {
groups.clear();
certificates = null;
}
/**
* Should return a unique name corresponding to the certificates given. The
* name returned will be used to look up access levels as well as group
* associations.
*
* @param certs The distinguished name.
* @return The unique name if the certificate is recognized, null otherwise.
*/
protected abstract String getUserNameForCertificates(final X509Certificate[] certs) throws LoginException;
/**
* Should return a set of the groups this user belongs to. The groups
* returned will be added to the user's credentials.
*
* @param username The username of the client. This is the same name that
* getUserNameForDn returned for the user's DN.
* @return A Set of the names of the groups this user belongs to.
*/
protected abstract Set<String> getUserGroups(final String username) throws LoginException;
protected String getDistinguishedName(final X509Certificate[] certs) {
if (certs != null && certs.length > 0 && certs[0] != null) {
return certs[0].getSubjectDN().getName();
}
else {
return null;
}
}
}

View File

@ -0,0 +1,132 @@
/*
* 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.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import java.io.IOException;
import java.security.Principal;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.activemq.artemis.core.server.ActiveMQServerLogger;
/**
* Always login the user with a default 'guest' identity.
*
* Useful for unauthenticated communication channels being used in the
* same broker as authenticated ones.
*
*/
public class GuestLoginModule implements LoginModule {
private static final String GUEST_USER = "org.apache.activemq.jaas.guest.user";
private static final String GUEST_ROLE = "org.apache.activemq.jaas.guest.role";
private String userName = "guest";
private String roleName = "guests";
private Subject subject;
private boolean debug;
private boolean credentialsInvalidate;
private Set<Principal> principals = new HashSet<Principal>();
private CallbackHandler callbackHandler;
private boolean loginSucceeded;
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
this.subject = subject;
this.callbackHandler = callbackHandler;
debug = "true".equalsIgnoreCase((String) options.get("debug"));
credentialsInvalidate = "true".equalsIgnoreCase((String) options.get("credentialsInvalidate"));
if (options.get(GUEST_USER) != null) {
userName = (String) options.get(GUEST_USER);
}
if (options.get(GUEST_ROLE) != null) {
roleName = (String) options.get(GUEST_ROLE);
}
principals.add(new UserPrincipal(userName));
principals.add(new RolePrincipal(roleName));
if (debug) {
ActiveMQServerLogger.LOGGER.debug("Initialized debug=" + debug + " guestUser=" + userName + " guestGroup=" + roleName);
}
}
@Override
public boolean login() throws LoginException {
loginSucceeded = true;
if (credentialsInvalidate) {
PasswordCallback passwordCallback = new PasswordCallback("Password: ", false);
try {
callbackHandler.handle(new Callback[]{passwordCallback});
if (passwordCallback.getPassword() != null) {
if (debug) {
ActiveMQServerLogger.LOGGER.debug("Guest login failing (credentialsInvalidate=true) on presence of a password");
}
loginSucceeded = false;
passwordCallback.clearPassword();
}
}
catch (IOException ioe) {
}
catch (UnsupportedCallbackException uce) {
}
}
if (debug) {
ActiveMQServerLogger.LOGGER.debug("Guest login " + loginSucceeded);
}
return loginSucceeded;
}
@Override
public boolean commit() throws LoginException {
if (loginSucceeded) {
subject.getPrincipals().addAll(principals);
}
if (debug) {
ActiveMQServerLogger.LOGGER.debug("commit");
}
return loginSucceeded;
}
@Override
public boolean abort() throws LoginException {
if (debug) {
ActiveMQServerLogger.LOGGER.debug("abort");
}
return true;
}
@Override
public boolean logout() throws LoginException {
subject.getPrincipals().removeAll(principals);
if (debug) {
ActiveMQServerLogger.LOGGER.debug("logout");
}
return true;
}
}

View File

@ -0,0 +1,65 @@
/*
* 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.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import java.io.IOException;
import java.security.cert.X509Certificate;
/**
* A Standard JAAS callback handler for SSL certificate requests. Will only
* handle callbacks of type CertificateCallback.
*/
public class JaasCertificateCallbackHandler implements CallbackHandler {
final X509Certificate[] certificates;
/**
* Basic constructor.
*
* @param certs The certificate returned when calling back.
*/
public JaasCertificateCallbackHandler(X509Certificate[] certs) {
certificates = certs;
}
/**
* Overriding handle method to handle certificates.
*
* @param callbacks The callbacks requested.
* @throws IOException
* @throws UnsupportedCallbackException Thrown if an unkown Callback type is
* encountered.
*/
@Override
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (int i = 0; i < callbacks.length; i++) {
Callback callback = callbacks[i];
if (callback instanceof CertificateCallback) {
CertificateCallback certCallback = (CertificateCallback) callback;
certCallback.setCertificates(certificates);
}
else {
throw new UnsupportedCallbackException(callback);
}
}
}
}

View File

@ -0,0 +1,63 @@
/*
* 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.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import java.io.IOException;
/**
* A JAAS username password CallbackHandler.
*/
public class JaasCredentialCallbackHandler implements CallbackHandler {
private final String username;
private final String password;
public JaasCredentialCallbackHandler(String username, String password) {
this.username = username;
this.password = password;
}
@Override
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (int i = 0; i < callbacks.length; i++) {
Callback callback = callbacks[i];
if (callback instanceof PasswordCallback) {
PasswordCallback passwordCallback = (PasswordCallback) callback;
if (password == null) {
passwordCallback.setPassword(null);
}
else {
passwordCallback.setPassword(password.toCharArray());
}
}
else if (callback instanceof NameCallback) {
NameCallback nameCallback = (NameCallback) callback;
if (username == null) {
nameCallback.setName(null);
}
else {
nameCallback.setName(username);
}
}
}
}
}

View File

@ -0,0 +1,505 @@
/*
* 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.naming.AuthenticationException;
import javax.naming.CommunicationException;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.NameParser;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
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 javax.security.auth.spi.LoginModule;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.Principal;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import org.apache.activemq.artemis.core.server.ActiveMQServerLogger;
public class LDAPLoginModule implements LoginModule {
private static final String INITIAL_CONTEXT_FACTORY = "initialContextFactory";
private static final String CONNECTION_URL = "connectionURL";
private static final String CONNECTION_USERNAME = "connectionUsername";
private static final String CONNECTION_PASSWORD = "connectionPassword";
private static final String CONNECTION_PROTOCOL = "connectionProtocol";
private static final String AUTHENTICATION = "authentication";
private static final String USER_BASE = "userBase";
private static final String USER_SEARCH_MATCHING = "userSearchMatching";
private static final String USER_SEARCH_SUBTREE = "userSearchSubtree";
private static final String ROLE_BASE = "roleBase";
private static final String ROLE_NAME = "roleName";
private static final String ROLE_SEARCH_MATCHING = "roleSearchMatching";
private static final String ROLE_SEARCH_SUBTREE = "roleSearchSubtree";
private static final String USER_ROLE_NAME = "userRoleName";
private static final String EXPAND_ROLES = "expandRoles";
private static final String EXPAND_ROLES_MATCHING = "expandRolesMatching";
protected DirContext context;
private Subject subject;
private CallbackHandler handler;
private LDAPLoginProperty[] config;
private String username;
private Set<RolePrincipal> groups = new HashSet<RolePrincipal>();
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
this.subject = subject;
this.handler = callbackHandler;
config = new LDAPLoginProperty[]{new LDAPLoginProperty(INITIAL_CONTEXT_FACTORY, (String) options.get(INITIAL_CONTEXT_FACTORY)), new LDAPLoginProperty(CONNECTION_URL, (String) options.get(CONNECTION_URL)), new LDAPLoginProperty(CONNECTION_USERNAME, (String) options.get(CONNECTION_USERNAME)), new LDAPLoginProperty(CONNECTION_PASSWORD, (String) options.get(CONNECTION_PASSWORD)), new LDAPLoginProperty(CONNECTION_PROTOCOL, (String) options.get(CONNECTION_PROTOCOL)), new LDAPLoginProperty(AUTHENTICATION, (String) options.get(AUTHENTICATION)), new LDAPLoginProperty(USER_BASE, (String) options.get(USER_BASE)), new LDAPLoginProperty(USER_SEARCH_MATCHING, (String) options.get(USER_SEARCH_MATCHING)), new LDAPLoginProperty(USER_SEARCH_SUBTREE, (String) options.get(USER_SEARCH_SUBTREE)), new LDAPLoginProperty(ROLE_BASE, (String) options.get(ROLE_BASE)), new LDAPLoginProperty(ROLE_NAME, (String) options.get(ROLE_NAME)), new LDAPLoginProperty(ROLE_SEARCH_MATCHING, (String) options.get(ROLE_SEARCH_MATCHING)), new LDAPLoginProperty(ROLE_SEARCH_SUBTREE, (String) options.get(ROLE_SEARCH_SUBTREE)), new LDAPLoginProperty(USER_ROLE_NAME, (String) options.get(USER_ROLE_NAME)), new LDAPLoginProperty(EXPAND_ROLES, (String) options.get(EXPAND_ROLES)), new LDAPLoginProperty(EXPAND_ROLES_MATCHING, (String) options.get(EXPAND_ROLES_MATCHING))};
}
@Override
public boolean login() throws LoginException {
Callback[] callbacks = new Callback[2];
callbacks[0] = new NameCallback("User name");
callbacks[1] = new PasswordCallback("Password", false);
try {
handler.handle(callbacks);
}
catch (IOException ioe) {
throw (LoginException) new LoginException().initCause(ioe);
}
catch (UnsupportedCallbackException uce) {
throw (LoginException) new LoginException().initCause(uce);
}
String password;
username = ((NameCallback) callbacks[0]).getName();
if (username == null)
return false;
if (((PasswordCallback) callbacks[1]).getPassword() != null)
password = new String(((PasswordCallback) callbacks[1]).getPassword());
else
password = "";
// authenticate will throw LoginException
// in case of failed authentication
authenticate(username, password);
return true;
}
@Override
public boolean logout() throws LoginException {
username = null;
return true;
}
@Override
public boolean commit() throws LoginException {
Set<Principal> principals = subject.getPrincipals();
principals.add(new UserPrincipal(username));
for (RolePrincipal gp : groups) {
principals.add(gp);
}
return true;
}
@Override
public boolean abort() throws LoginException {
username = null;
return true;
}
protected void close(DirContext context) {
try {
context.close();
}
catch (Exception e) {
ActiveMQServerLogger.LOGGER.error(e.toString());
}
}
protected boolean authenticate(String username, String password) throws LoginException {
MessageFormat userSearchMatchingFormat;
boolean userSearchSubtreeBool;
DirContext context = null;
if (ActiveMQServerLogger.LOGGER.isDebugEnabled()) {
ActiveMQServerLogger.LOGGER.debug("Create the LDAP initial context.");
}
try {
context = open();
}
catch (NamingException ne) {
FailedLoginException ex = new FailedLoginException("Error opening LDAP connection");
ex.initCause(ne);
throw ex;
}
if (!isLoginPropertySet(USER_SEARCH_MATCHING))
return false;
userSearchMatchingFormat = new MessageFormat(getLDAPPropertyValue(USER_SEARCH_MATCHING));
userSearchSubtreeBool = Boolean.valueOf(getLDAPPropertyValue(USER_SEARCH_SUBTREE)).booleanValue();
try {
String filter = userSearchMatchingFormat.format(new String[]{doRFC2254Encoding(username)});
SearchControls constraints = new SearchControls();
if (userSearchSubtreeBool) {
constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
}
else {
constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
}
// setup attributes
List<String> list = new ArrayList<String>();
if (isLoginPropertySet(USER_ROLE_NAME)) {
list.add(getLDAPPropertyValue(USER_ROLE_NAME));
}
String[] attribs = new String[list.size()];
list.toArray(attribs);
constraints.setReturningAttributes(attribs);
if (ActiveMQServerLogger.LOGGER.isDebugEnabled()) {
ActiveMQServerLogger.LOGGER.debug("Get the user DN.");
ActiveMQServerLogger.LOGGER.debug("Looking for the user in LDAP with ");
ActiveMQServerLogger.LOGGER.debug(" base DN: " + getLDAPPropertyValue(USER_BASE));
ActiveMQServerLogger.LOGGER.debug(" filter: " + filter);
}
NamingEnumeration<SearchResult> results = context.search(getLDAPPropertyValue(USER_BASE), filter, constraints);
if (results == null || !results.hasMore()) {
ActiveMQServerLogger.LOGGER.warn("User " + username + " not found in LDAP.");
throw new FailedLoginException("User " + username + " not found in LDAP.");
}
SearchResult result = results.next();
if (results.hasMore()) {
// ignore for now
}
String dn;
if (result.isRelative()) {
ActiveMQServerLogger.LOGGER.debug("LDAP returned a relative name: " + result.getName());
NameParser parser = context.getNameParser("");
Name contextName = parser.parse(context.getNameInNamespace());
Name baseName = parser.parse(getLDAPPropertyValue(USER_BASE));
Name entryName = parser.parse(result.getName());
Name name = contextName.addAll(baseName);
name = name.addAll(entryName);
dn = name.toString();
}
else {
ActiveMQServerLogger.LOGGER.debug("LDAP returned an absolute name: " + result.getName());
try {
URI uri = new URI(result.getName());
String path = uri.getPath();
if (path.startsWith("/")) {
dn = path.substring(1);
}
else {
dn = path;
}
}
catch (URISyntaxException e) {
if (context != null) {
close(context);
}
FailedLoginException ex = new FailedLoginException("Error parsing absolute name as URI.");
ex.initCause(e);
throw ex;
}
}
if (ActiveMQServerLogger.LOGGER.isDebugEnabled()) {
ActiveMQServerLogger.LOGGER.debug("Using DN [" + dn + "] for binding.");
}
Attributes attrs = result.getAttributes();
if (attrs == null) {
throw new FailedLoginException("User found, but LDAP entry malformed: " + username);
}
List<String> roles = null;
if (isLoginPropertySet(USER_ROLE_NAME)) {
roles = addAttributeValues(getLDAPPropertyValue(USER_ROLE_NAME), attrs, roles);
}
// check the credentials by binding to server
if (bindUser(context, dn, password)) {
// if authenticated add more roles
roles = getRoles(context, dn, username, roles);
if (ActiveMQServerLogger.LOGGER.isDebugEnabled()) {
ActiveMQServerLogger.LOGGER.debug("Roles " + roles + " for user " + username);
}
for (int i = 0; i < roles.size(); i++) {
groups.add(new RolePrincipal(roles.get(i)));
}
}
else {
throw new FailedLoginException("Password does not match for user: " + username);
}
}
catch (CommunicationException e) {
FailedLoginException ex = new FailedLoginException("Error contacting LDAP");
ex.initCause(e);
throw ex;
}
catch (NamingException e) {
if (context != null) {
close(context);
}
FailedLoginException ex = new FailedLoginException("Error contacting LDAP");
ex.initCause(e);
throw ex;
}
return true;
}
protected List<String> getRoles(DirContext context,
String dn,
String username,
List<String> currentRoles) throws NamingException {
List<String> list = currentRoles;
MessageFormat roleSearchMatchingFormat;
boolean roleSearchSubtreeBool;
boolean expandRolesBool;
roleSearchMatchingFormat = new MessageFormat(getLDAPPropertyValue(ROLE_SEARCH_MATCHING));
roleSearchSubtreeBool = Boolean.valueOf(getLDAPPropertyValue(ROLE_SEARCH_SUBTREE)).booleanValue();
expandRolesBool = Boolean.valueOf(getLDAPPropertyValue(EXPAND_ROLES)).booleanValue();
if (list == null) {
list = new ArrayList<String>();
}
if (!isLoginPropertySet(ROLE_NAME)) {
return list;
}
String filter = roleSearchMatchingFormat.format(new String[]{doRFC2254Encoding(dn), doRFC2254Encoding(username)});
SearchControls constraints = new SearchControls();
if (roleSearchSubtreeBool) {
constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
}
else {
constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
}
if (ActiveMQServerLogger.LOGGER.isDebugEnabled()) {
ActiveMQServerLogger.LOGGER.debug("Get user roles.");
ActiveMQServerLogger.LOGGER.debug("Looking for the user roles in LDAP with ");
ActiveMQServerLogger.LOGGER.debug(" base DN: " + getLDAPPropertyValue(ROLE_BASE));
ActiveMQServerLogger.LOGGER.debug(" filter: " + filter);
}
HashSet<String> haveSeenNames = new HashSet<String>();
Queue<String> pendingNameExpansion = new LinkedList<String>();
NamingEnumeration<SearchResult> results = context.search(getLDAPPropertyValue(ROLE_BASE), filter, constraints);
while (results.hasMore()) {
SearchResult result = results.next();
Attributes attrs = result.getAttributes();
if (expandRolesBool) {
haveSeenNames.add(result.getNameInNamespace());
pendingNameExpansion.add(result.getNameInNamespace());
}
if (attrs == null) {
continue;
}
list = addAttributeValues(getLDAPPropertyValue(ROLE_NAME), attrs, list);
}
if (expandRolesBool) {
MessageFormat expandRolesMatchingFormat = new MessageFormat(getLDAPPropertyValue(EXPAND_ROLES_MATCHING));
while (!pendingNameExpansion.isEmpty()) {
String name = pendingNameExpansion.remove();
filter = expandRolesMatchingFormat.format(new String[]{name});
results = context.search(getLDAPPropertyValue(ROLE_BASE), filter, constraints);
while (results.hasMore()) {
SearchResult result = results.next();
name = result.getNameInNamespace();
if (!haveSeenNames.contains(name)) {
Attributes attrs = result.getAttributes();
list = addAttributeValues(getLDAPPropertyValue(ROLE_NAME), attrs, list);
haveSeenNames.add(name);
pendingNameExpansion.add(name);
}
}
}
}
return list;
}
protected String doRFC2254Encoding(String inputString) {
StringBuffer buf = new StringBuffer(inputString.length());
for (int i = 0; i < inputString.length(); i++) {
char c = inputString.charAt(i);
switch (c) {
case '\\':
buf.append("\\5c");
break;
case '*':
buf.append("\\2a");
break;
case '(':
buf.append("\\28");
break;
case ')':
buf.append("\\29");
break;
case '\0':
buf.append("\\00");
break;
default:
buf.append(c);
break;
}
}
return buf.toString();
}
protected boolean bindUser(DirContext context, String dn, String password) throws NamingException {
boolean isValid = false;
if (ActiveMQServerLogger.LOGGER.isDebugEnabled()) {
ActiveMQServerLogger.LOGGER.debug("Binding the user.");
}
context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn);
context.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
try {
context.getAttributes("", null);
isValid = true;
if (ActiveMQServerLogger.LOGGER.isDebugEnabled()) {
ActiveMQServerLogger.LOGGER.debug("User " + dn + " successfully bound.");
}
}
catch (AuthenticationException e) {
isValid = false;
if (ActiveMQServerLogger.LOGGER.isDebugEnabled()) {
ActiveMQServerLogger.LOGGER.debug("Authentication failed for dn=" + dn);
}
}
if (isLoginPropertySet(CONNECTION_USERNAME)) {
context.addToEnvironment(Context.SECURITY_PRINCIPAL, getLDAPPropertyValue(CONNECTION_USERNAME));
}
else {
context.removeFromEnvironment(Context.SECURITY_PRINCIPAL);
}
if (isLoginPropertySet(CONNECTION_PASSWORD)) {
context.addToEnvironment(Context.SECURITY_CREDENTIALS, getLDAPPropertyValue(CONNECTION_PASSWORD));
}
else {
context.removeFromEnvironment(Context.SECURITY_CREDENTIALS);
}
return isValid;
}
private List<String> addAttributeValues(String attrId,
Attributes attrs,
List<String> values) throws NamingException {
if (attrId == null || attrs == null) {
return values;
}
if (values == null) {
values = new ArrayList<String>();
}
Attribute attr = attrs.get(attrId);
if (attr == null) {
return values;
}
NamingEnumeration<?> e = attr.getAll();
while (e.hasMore()) {
String value = (String) e.next();
values.add(value);
}
return values;
}
protected DirContext open() throws NamingException {
try {
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY, getLDAPPropertyValue(INITIAL_CONTEXT_FACTORY));
if (isLoginPropertySet(CONNECTION_USERNAME)) {
env.put(Context.SECURITY_PRINCIPAL, getLDAPPropertyValue(CONNECTION_USERNAME));
}
else {
throw new NamingException("Empty username is not allowed");
}
if (isLoginPropertySet(CONNECTION_PASSWORD)) {
env.put(Context.SECURITY_CREDENTIALS, getLDAPPropertyValue(CONNECTION_PASSWORD));
}
else {
throw new NamingException("Empty password is not allowed");
}
env.put(Context.SECURITY_PROTOCOL, getLDAPPropertyValue(CONNECTION_PROTOCOL));
env.put(Context.PROVIDER_URL, getLDAPPropertyValue(CONNECTION_URL));
env.put(Context.SECURITY_AUTHENTICATION, getLDAPPropertyValue(AUTHENTICATION));
context = new InitialDirContext(env);
}
catch (NamingException e) {
ActiveMQServerLogger.LOGGER.error(e.toString());
throw e;
}
return context;
}
private String getLDAPPropertyValue(String propertyName) {
for (int i = 0; i < config.length; i++)
if (config[i].getPropertyName() == propertyName)
return config[i].getPropertyValue();
return null;
}
private boolean isLoginPropertySet(String propertyName) {
for (int i = 0; i < config.length; i++) {
if (config[i].getPropertyName() == propertyName && (config[i].getPropertyValue() != null && !"".equals(config[i].getPropertyValue())))
return true;
}
return false;
}
}

View File

@ -0,0 +1,41 @@
/*
* 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;
public class LDAPLoginProperty {
private String name;
private String value;
public LDAPLoginProperty(String name) {
this.name = name;
}
public LDAPLoginProperty(String name, String value) {
this.name = name;
this.value = value;
}
public String getPropertyName() {
return this.name;
}
public String getPropertyValue() {
return this.value;
}
}

View File

@ -0,0 +1,75 @@
/*
* 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.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.apache.activemq.artemis.core.server.ActiveMQServerLogger;
class PrincipalProperties {
private final Properties principals;
private final long reloadTime;
PrincipalProperties(final String type, final File source, final ActiveMQServerLogger log) {
Properties props = new Properties();
long reloadTime = 0;
try {
load(source, props);
reloadTime = System.currentTimeMillis();
}
catch (IOException ioe) {
ioe.printStackTrace();
log.warn("Unable to load " + type + " properties file " + source);
}
this.reloadTime = reloadTime;
this.principals = props;
}
@SuppressWarnings({"unchecked", "rawtypes"})
Set<Map.Entry<String, String>> entries() {
return (Set) principals.entrySet();
}
String getProperty(String name) {
return principals.getProperty(name);
}
long getReloadTime() {
return reloadTime;
}
private void load(final File source, Properties props) throws FileNotFoundException, IOException {
FileInputStream in = new FileInputStream(source);
try {
props.load(in);
}
finally {
in.close();
}
}
Properties getPrincipals() {
return principals;
}
}

View File

@ -0,0 +1,215 @@
/*
* 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.NameCallback;
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 javax.security.auth.spi.LoginModule;
import java.io.File;
import java.io.IOException;
import java.security.Principal;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.activemq.artemis.core.server.ActiveMQServerLogger;
public class PropertiesLoginModule implements LoginModule {
private static final String USER_FILE = "org.apache.activemq.jaas.properties.user";
private static final String GROUP_FILE = "org.apache.activemq.jaas.properties.role";
private Subject subject;
private CallbackHandler callbackHandler;
private boolean debug;
private boolean reload = false;
private static volatile PrincipalProperties users;
private static volatile PrincipalProperties roles;
private String user;
private final Set<Principal> principals = new HashSet<Principal>();
private File baseDir;
private boolean loginSucceeded;
// private boolean decrypt = true;
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
this.subject = subject;
this.callbackHandler = callbackHandler;
loginSucceeded = false;
debug = "true".equalsIgnoreCase((String) options.get("debug"));
if (options.get("reload") != null) {
reload = "true".equalsIgnoreCase((String) options.get("reload"));
}
if (options.get("baseDir") != null) {
baseDir = new File((String) options.get("baseDir"));
}
setBaseDir();
String usersFile = options.get(USER_FILE) + "";
File uf = baseDir != null ? new File(baseDir, usersFile) : new File(usersFile);
if (reload || users == null || uf.lastModified() > users.getReloadTime()) {
if (debug) {
ActiveMQServerLogger.LOGGER.debug("Reloading users from " + uf.getAbsolutePath());
}
users = new PrincipalProperties("user", uf, ActiveMQServerLogger.LOGGER);
// if( decrypt ) {
// try {
// EncryptionSupport.decrypt(users.getPrincipals());
// } catch(NoClassDefFoundError e) {
// // this Happens whe jasypt is not on the classpath..
// decrypt = false;
// ActiveMQServerLogger.LOGGER.info("jasypt is not on the classpath: password decryption disabled.");
// }
// }
}
String groupsFile = options.get(GROUP_FILE) + "";
File gf = baseDir != null ? new File(baseDir, groupsFile) : new File(groupsFile);
if (reload || roles == null || gf.lastModified() > roles.getReloadTime()) {
if (debug) {
ActiveMQServerLogger.LOGGER.debug("Reloading roles from " + gf.getAbsolutePath());
}
roles = new PrincipalProperties("role", gf, ActiveMQServerLogger.LOGGER);
}
}
private void setBaseDir() {
if (baseDir == null) {
if (System.getProperty("java.security.auth.login.config") != null) {
baseDir = new File(System.getProperty("java.security.auth.login.config")).getParentFile();
if (debug) {
ActiveMQServerLogger.LOGGER.debug("Using basedir=" + baseDir.getAbsolutePath());
}
}
}
}
@Override
public boolean login() throws LoginException {
Callback[] callbacks = new Callback[2];
callbacks[0] = new NameCallback("Username: ");
callbacks[1] = new PasswordCallback("Password: ", false);
try {
callbackHandler.handle(callbacks);
}
catch (IOException ioe) {
throw new LoginException(ioe.getMessage());
}
catch (UnsupportedCallbackException uce) {
throw new LoginException(uce.getMessage() + " not available to obtain information from user");
}
user = ((NameCallback) callbacks[0]).getName();
char[] tmpPassword = ((PasswordCallback) callbacks[1]).getPassword();
if (tmpPassword == null) {
tmpPassword = new char[0];
}
if (user == null) {
throw new FailedLoginException("user name is null");
}
String password = users.getProperty(user);
if (password == null) {
throw new FailedLoginException("User does exist");
}
if (!password.equals(new String(tmpPassword))) {
throw new FailedLoginException("Password does not match");
}
loginSucceeded = true;
if (debug) {
ActiveMQServerLogger.LOGGER.debug("login " + user);
}
return loginSucceeded;
}
@Override
public boolean commit() throws LoginException {
boolean result = loginSucceeded;
if (result) {
principals.add(new UserPrincipal(user));
for (Map.Entry<String, String> entry : roles.entries()) {
String name = entry.getKey();
if (debug) {
ActiveMQServerLogger.LOGGER.debug("Inspecting role '" + name + "' with user(s): " + entry.getValue());
}
String[] userList = entry.getValue().split(",");
for (int i = 0; i < userList.length; i++) {
if (user.equals(userList[i])) {
principals.add(new RolePrincipal(name));
break;
}
}
}
subject.getPrincipals().addAll(principals);
}
// will whack loginSucceeded
clear();
if (debug) {
ActiveMQServerLogger.LOGGER.debug("commit, result: " + result);
}
return result;
}
@Override
public boolean abort() throws LoginException {
clear();
if (debug) {
ActiveMQServerLogger.LOGGER.debug("abort");
}
return true;
}
@Override
public boolean logout() throws LoginException {
subject.getPrincipals().removeAll(principals);
principals.clear();
clear();
if (debug) {
ActiveMQServerLogger.LOGGER.debug("logout");
}
return true;
}
private void clear() {
user = null;
loginSucceeded = false;
}
/**
* For test-usage only.
*/
public static void resetUsersAndGroupsCache() {
users = null;
roles = null;
}
}

View File

@ -0,0 +1,68 @@
/*
* 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.security.Principal;
public class RolePrincipal implements Principal {
private final String name;
private transient int hash;
public RolePrincipal(String name) {
if (name == null) {
throw new IllegalArgumentException("name cannot be null");
}
this.name = name;
}
@Override
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final RolePrincipal that = (RolePrincipal) o;
if (!name.equals(that.name)) {
return false;
}
return true;
}
@Override
public int hashCode() {
if (hash == 0) {
hash = name.hashCode();
}
return hash;
}
@Override
public String toString() {
return name;
}
}

View File

@ -0,0 +1,147 @@
/*
* 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.CallbackHandler;
import javax.security.auth.login.LoginException;
import java.io.File;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
/**
* A LoginModule allowing for SSL certificate based authentication based on
* Distinguished Names (DN) stored in text files. The DNs are parsed using a
* Properties class where each line is <user_name>=<user_DN>. This class also
* uses a group definition file where each line is <group_name>=<user_name_1>,<user_name_2>,etc.
* The user and group files' locations must be specified in the
* org.apache.activemq.jaas.textfiledn.user and
* org.apache.activemq.jaas.textfiledn.user properties respectively. NOTE: This
* class will re-read user and group files for every authentication (i.e it does
* live updates of allowed groups and users).
*/
public class TextFileCertificateLoginModule extends CertificateLoginModule {
private static final String USER_FILE = "org.apache.activemq.jaas.textfiledn.user";
private static final String GROUP_FILE = "org.apache.activemq.jaas.textfiledn.group";
private File baseDir;
private String usersFilePathname;
private String groupsFilePathname;
/**
* Performs initialization of file paths. A standard JAAS override.
*/
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
super.initialize(subject, callbackHandler, sharedState, options);
if (System.getProperty("java.security.auth.login.config") != null) {
baseDir = new File(System.getProperty("java.security.auth.login.config")).getParentFile();
}
else {
baseDir = new File(".");
}
usersFilePathname = (String) options.get(USER_FILE) + "";
groupsFilePathname = (String) options.get(GROUP_FILE) + "";
}
/**
* Overriding to allow DN authorization based on DNs specified in text
* files.
*
* @param certs The certificate the incoming connection provided.
* @return The user's authenticated name or null if unable to authenticate
* the user.
* @throws LoginException Thrown if unable to find user file or connection
* certificate.
*/
@Override
protected String getUserNameForCertificates(final X509Certificate[] certs) throws LoginException {
if (certs == null) {
throw new LoginException("Client certificates not found. Cannot authenticate.");
}
File usersFile = new File(baseDir, usersFilePathname);
Properties users = new Properties();
try {
java.io.FileInputStream in = new java.io.FileInputStream(usersFile);
users.load(in);
in.close();
}
catch (IOException ioe) {
throw new LoginException("Unable to load user properties file " + usersFile);
}
String dn = getDistinguishedName(certs);
Enumeration<Object> keys = users.keys();
for (Enumeration<Object> vals = users.elements(); vals.hasMoreElements(); ) {
if (((String) vals.nextElement()).equals(dn)) {
return (String) keys.nextElement();
}
else {
keys.nextElement();
}
}
return null;
}
/**
* Overriding to allow for group discovery based on text files.
*
* @param username The name of the user being examined. This is the same
* name returned by getUserNameForCertificates.
* @return A Set of name Strings for groups this user belongs to.
* @throws LoginException Thrown if unable to find group definition file.
*/
@Override
protected Set<String> getUserGroups(String username) throws LoginException {
File groupsFile = new File(baseDir, groupsFilePathname);
Properties groups = new Properties();
try {
java.io.FileInputStream in = new java.io.FileInputStream(groupsFile);
groups.load(in);
in.close();
}
catch (IOException ioe) {
throw new LoginException("Unable to load group properties file " + groupsFile);
}
Set<String> userGroups = new HashSet<String>();
for (Enumeration<Object> enumeration = groups.keys(); enumeration.hasMoreElements(); ) {
String groupName = (String) enumeration.nextElement();
String[] userList = (groups.getProperty(groupName) + "").split(",");
for (int i = 0; i < userList.length; i++) {
if (username.equals(userList[i])) {
userGroups.add(groupName);
break;
}
}
}
return userGroups;
}
}

View File

@ -0,0 +1,68 @@
/*
* 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.security.Principal;
public class UserPrincipal implements Principal {
private final String name;
private transient int hash;
public UserPrincipal(String name) {
if (name == null) {
throw new IllegalArgumentException("name cannot be null");
}
this.name = name;
}
@Override
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final UserPrincipal that = (UserPrincipal) o;
if (!name.equals(that.name)) {
return false;
}
return true;
}
@Override
public int hashCode() {
if (hash == 0) {
hash = name.hashCode();
}
return hash;
}
@Override
public String toString() {
return name;
}
}

View File

@ -0,0 +1,154 @@
/*
* 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.core.security.jaas;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginException;
import java.io.IOException;
import java.security.Principal;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Vector;
import org.apache.activemq.artemis.spi.core.security.jaas.JaasCertificateCallbackHandler;
import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal;
import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
public class CertificateLoginModuleTest extends Assert {
private static final String USER_NAME = "testUser";
private static final List<String> GROUP_NAMES = new Vector<String>();
private StubCertificateLoginModule loginModule;
private Subject subject;
public CertificateLoginModuleTest() {
GROUP_NAMES.add("testGroup1");
GROUP_NAMES.add("testGroup2");
GROUP_NAMES.add("testGroup3");
GROUP_NAMES.add("testGroup4");
}
@Before
public void setUp() throws Exception {
subject = new Subject();
}
private void loginWithCredentials(String userName, Set<String> groupNames) throws LoginException {
loginModule = new StubCertificateLoginModule(userName, new HashSet<String>(groupNames));
JaasCertificateCallbackHandler callbackHandler = new JaasCertificateCallbackHandler(null);
loginModule.initialize(subject, callbackHandler, null, new HashMap());
loginModule.login();
loginModule.commit();
}
private void checkPrincipalsMatch(Subject subject) {
boolean nameFound = false;
boolean[] groupsFound = new boolean[GROUP_NAMES.size()];
for (int i = 0; i < groupsFound.length; ++i) {
groupsFound[i] = false;
}
for (Iterator iter = subject.getPrincipals().iterator(); iter.hasNext(); ) {
Principal currentPrincipal = (Principal) iter.next();
if (currentPrincipal instanceof UserPrincipal) {
if (currentPrincipal.getName().equals(USER_NAME)) {
if (!nameFound) {
nameFound = true;
}
else {
fail("UserPrincipal found twice.");
}
}
else {
fail("Unknown UserPrincipal found.");
}
}
else if (currentPrincipal instanceof RolePrincipal) {
int principalIdx = GROUP_NAMES.indexOf(((RolePrincipal) currentPrincipal).getName());
if (principalIdx < 0) {
fail("Unknown GroupPrincipal found.");
}
if (!groupsFound[principalIdx]) {
groupsFound[principalIdx] = true;
}
else {
fail("GroupPrincipal found twice.");
}
}
else {
fail("Unknown Principal type found.");
}
}
}
@Test
public void testLoginSuccess() throws IOException {
try {
loginWithCredentials(USER_NAME, new HashSet<String>(GROUP_NAMES));
}
catch (Exception e) {
fail("Unable to login: " + e.getMessage());
}
checkPrincipalsMatch(subject);
}
@Test
public void testLoginFailure() throws IOException {
boolean loginFailed = false;
try {
loginWithCredentials(null, new HashSet<String>());
}
catch (LoginException e) {
loginFailed = true;
}
if (!loginFailed) {
fail("Logged in with unknown certificate.");
}
}
@Test
public void testLogOut() throws IOException {
try {
loginWithCredentials(USER_NAME, new HashSet<String>(GROUP_NAMES));
}
catch (Exception e) {
fail("Unable to login: " + e.getMessage());
}
loginModule.logout();
assertEquals("logout should have cleared Subject principals.", 0, subject.getPrincipals().size());
}
}

View File

@ -0,0 +1,91 @@
/*
* 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.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.LoginContext;
import javax.security.auth.login.LoginException;
import java.io.IOException;
import java.net.URL;
import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal;
import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal;
import org.junit.Assert;
import org.junit.Test;
public class GuestLoginModuleTest extends Assert {
static {
String path = System.getProperty("java.security.auth.login.config");
if (path == null) {
URL resource = GuestLoginModuleTest.class.getClassLoader().getResource("login.config");
if (resource != null) {
path = resource.getFile();
System.setProperty("java.security.auth.login.config", path);
}
}
}
@Test
public void testLogin() throws LoginException {
LoginContext context = new LoginContext("GuestLogin", new CallbackHandler() {
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
assertEquals("Should have no Callbacks", 0, callbacks.length);
}
});
context.login();
Subject subject = context.getSubject();
assertEquals("Should have two principals", 2, subject.getPrincipals().size());
assertEquals("Should have one user principal", 1, subject.getPrincipals(UserPrincipal.class).size());
assertTrue("User principal is 'foo'", subject.getPrincipals(UserPrincipal.class).contains(new UserPrincipal("foo")));
assertEquals("Should have one group principal", 1, subject.getPrincipals(RolePrincipal.class).size());
assertTrue("Role principal is 'bar'", subject.getPrincipals(RolePrincipal.class).contains(new RolePrincipal("bar")));
context.logout();
assertEquals("Should have zero principals", 0, subject.getPrincipals().size());
}
@Test
public void testLoginWithDefaults() throws LoginException {
LoginContext context = new LoginContext("GuestLoginWithDefaults", new CallbackHandler() {
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
assertEquals("Should have no Callbacks", 0, callbacks.length);
}
});
context.login();
Subject subject = context.getSubject();
assertEquals("Should have two principals", 2, subject.getPrincipals().size());
assertEquals("Should have one user principal", 1, subject.getPrincipals(UserPrincipal.class).size());
assertTrue("User principal is 'guest'", subject.getPrincipals(UserPrincipal.class).contains(new UserPrincipal("guest")));
assertEquals("Should have one group principal", 1, subject.getPrincipals(RolePrincipal.class).size());
assertTrue("Role principal is 'guests'", subject.getPrincipals(RolePrincipal.class).contains(new RolePrincipal("guests")));
context.logout();
assertEquals("Should have zero principals", 0, subject.getPrincipals().size());
}
}

View File

@ -0,0 +1,149 @@
/*
* 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.core.security.jaas;
import javax.naming.Context;
import javax.naming.NameClassPair;
import javax.naming.NamingEnumeration;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import java.io.IOException;
import java.util.HashSet;
import java.util.Hashtable;
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.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@RunWith(FrameworkRunner.class)
@CreateLdapServer(transports = {@CreateTransport(protocol = "LDAP", port = 1024)})
@ApplyLdifFiles("test.ldif")
public class LDAPLoginModuleTest extends AbstractLdapTestUnit {
private static final String PRINCIPAL = "uid=admin,ou=system";
private static final String CREDENTIALS = "secret";
private final String loginConfigSysPropName = "java.security.auth.login.config";
private String oldLoginConfig;
@Before
public void setLoginConfigSysProperty() {
oldLoginConfig = System.getProperty(loginConfigSysPropName, null);
System.setProperty(loginConfigSysPropName, "src/test/resources/login.config");
}
@After
public void resetLoginConfigSysProperty() {
if (oldLoginConfig != null) {
System.setProperty(loginConfigSysPropName, oldLoginConfig);
}
}
@SuppressWarnings("unchecked")
@Test
public void testRunning() throws Exception {
Hashtable env = new Hashtable();
env.put(Context.PROVIDER_URL, "ldap://localhost:1024");
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, PRINCIPAL);
env.put(Context.SECURITY_CREDENTIALS, CREDENTIALS);
DirContext ctx = new InitialDirContext(env);
HashSet set = new HashSet();
NamingEnumeration list = ctx.list("ou=system");
while (list.hasMore()) {
NameClassPair ncp = (NameClassPair) list.next();
set.add(ncp.getName());
}
assertTrue(set.contains("uid=admin"));
assertTrue(set.contains("ou=users"));
assertTrue(set.contains("ou=groups"));
assertTrue(set.contains("ou=configuration"));
assertTrue(set.contains("prefNodeName=sysPrefRoot"));
}
@Test
public void testLogin() throws LoginException {
LoginContext context = new LoginContext("LDAPLogin", new CallbackHandler() {
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (int i = 0; i < callbacks.length; i++) {
if (callbacks[i] instanceof NameCallback) {
((NameCallback) callbacks[i]).setName("first");
}
else if (callbacks[i] instanceof PasswordCallback) {
((PasswordCallback) callbacks[i]).setPassword("secret".toCharArray());
}
else {
throw new UnsupportedCallbackException(callbacks[i]);
}
}
}
});
context.login();
context.logout();
}
@Test
public void testUnauthenticated() throws LoginException {
LoginContext context = new LoginContext("UnAuthenticatedLDAPLogin", new CallbackHandler() {
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (int i = 0; i < callbacks.length; i++) {
if (callbacks[i] instanceof NameCallback) {
((NameCallback) callbacks[i]).setName("first");
}
else if (callbacks[i] instanceof PasswordCallback) {
((PasswordCallback) callbacks[i]).setPassword("secret".toCharArray());
}
else {
throw new UnsupportedCallbackException(callbacks[i]);
}
}
}
});
try {
context.login();
}
catch (LoginException le) {
assertEquals(le.getCause().getMessage(), "Empty password is not allowed");
return;
}
fail("Should have failed authenticating");
}
}

View File

@ -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.core.security.jaas;
import javax.naming.Context;
import javax.naming.NameClassPair;
import javax.naming.NamingEnumeration;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import java.io.IOException;
import java.security.Principal;
import java.util.HashSet;
import java.util.Hashtable;
import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal;
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.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertTrue;
@RunWith(FrameworkRunner.class)
@CreateLdapServer(transports = {@CreateTransport(protocol = "LDAP", port = 1024)})
@ApplyLdifFiles("test.ldif")
public class LDAPModuleRoleExpansionTest extends AbstractLdapTestUnit {
private static final String PRINCIPAL = "uid=admin,ou=system";
private static final String CREDENTIALS = "secret";
private final String loginConfigSysPropName = "java.security.auth.login.config";
private String oldLoginConfig;
@Before
public void setLoginConfigSysProperty() {
oldLoginConfig = System.getProperty(loginConfigSysPropName, null);
System.setProperty(loginConfigSysPropName, "src/test/resources/login.config");
}
@After
public void resetLoginConfigSysProperty() {
if (oldLoginConfig != null) {
System.setProperty(loginConfigSysPropName, oldLoginConfig);
}
}
@SuppressWarnings("unchecked")
@Test
public void testRunning() throws Exception {
Hashtable env = new Hashtable();
env.put(Context.PROVIDER_URL, "ldap://localhost:1024");
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, PRINCIPAL);
env.put(Context.SECURITY_CREDENTIALS, CREDENTIALS);
DirContext ctx = new InitialDirContext(env);
HashSet set = new HashSet();
NamingEnumeration list = ctx.list("ou=system");
while (list.hasMore()) {
NameClassPair ncp = (NameClassPair) list.next();
set.add(ncp.getName());
}
assertTrue(set.contains("uid=admin"));
assertTrue(set.contains("ou=users"));
assertTrue(set.contains("ou=groups"));
assertTrue(set.contains("ou=configuration"));
assertTrue(set.contains("prefNodeName=sysPrefRoot"));
}
@Test
public void testRoleExpansion() throws LoginException {
LoginContext context = new LoginContext("ExpandedLDAPLogin", new CallbackHandler() {
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (int i = 0; i < callbacks.length; i++) {
if (callbacks[i] instanceof NameCallback) {
((NameCallback) callbacks[i]).setName("first");
}
else if (callbacks[i] instanceof PasswordCallback) {
((PasswordCallback) callbacks[i]).setPassword("secret".toCharArray());
}
else {
throw new UnsupportedCallbackException(callbacks[i]);
}
}
}
});
context.login();
Subject subject = context.getSubject();
boolean isAdmin = false;
boolean isUser = false;
for (Principal principal : subject.getPrincipals()) {
if (principal instanceof RolePrincipal) {
RolePrincipal groupPrincipal = (RolePrincipal) principal;
if (groupPrincipal.getName().equalsIgnoreCase("admins"))
isAdmin = true;
if (groupPrincipal.getName().equalsIgnoreCase("users"))
isUser = true;
}
}
// Should be in users by virtue of being in admins
assertTrue(isAdmin && isUser);
context.logout();
}
}

View File

@ -0,0 +1,195 @@
/*
* 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.core.security.jaas;
import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import org.apache.activemq.artemis.spi.core.security.jaas.JaasCredentialCallbackHandler;
import org.apache.activemq.artemis.spi.core.security.jaas.PropertiesLoginModule;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ErrorCollector;
import org.junit.rules.TemporaryFolder;
import org.junit.rules.TestName;
import static org.junit.Assert.assertTrue;
public class PropertiesLoginModuleRaceConditionTest {
private static final String GROUPS_FILE = "roles.properties";
private static final String USERS_FILE = "users.properties";
private static final String USERNAME = "first";
private static final String PASSWORD = "secret";
@Rule
public final ErrorCollector e = new ErrorCollector();
@Rule
public final TemporaryFolder temp = new TemporaryFolder();
@Rule
public final TestName name = new TestName();
private Map<String, String> options;
private BlockingQueue<Exception> errors;
private ExecutorService pool;
private CallbackHandler callback;
private static class LoginTester implements Runnable {
private final CountDownLatch finished;
private final BlockingQueue<Exception> errors;
private final Map<String, String> options;
private final CountDownLatch start;
private final CallbackHandler callback;
LoginTester(CountDownLatch start,
CountDownLatch finished,
BlockingQueue<Exception> errors,
Map<String, String> options,
CallbackHandler callbackHandler) {
this.finished = finished;
this.errors = errors;
this.options = options;
this.start = start;
this.callback = callbackHandler;
}
@Override
public void run() {
try {
start.await();
Subject subject = new Subject();
PropertiesLoginModule module = new PropertiesLoginModule();
module.initialize(subject, callback, new HashMap<Object, Object>(), options);
module.login();
module.commit();
}
catch (Exception e) {
errors.offer(e);
}
finally {
finished.countDown();
}
}
}
@Before
public void before() throws FileNotFoundException, IOException {
createUsers();
createGroups();
options = new HashMap<String, String>();
options.put("reload", "true"); // Used to simplify reproduction of the
// race condition
options.put("org.apache.activemq.jaas.properties.user", USERS_FILE);
options.put("org.apache.activemq.jaas.properties.role", GROUPS_FILE);
options.put("baseDir", temp.getRoot().getAbsolutePath());
errors = new ArrayBlockingQueue<Exception>(processorCount());
pool = Executors.newFixedThreadPool(processorCount());
callback = new JaasCredentialCallbackHandler(USERNAME, PASSWORD);
}
@After
public void after() throws InterruptedException {
pool.shutdown();
assertTrue(pool.awaitTermination(500, TimeUnit.SECONDS));
PropertiesLoginModule.resetUsersAndGroupsCache();
}
@Test
public void raceConditionInUsersAndGroupsLoading() throws InterruptedException, FileNotFoundException, IOException {
// Brute force approach to increase the likelihood of the race condition occurring
for (int i = 0; i < 25000; i++) {
final CountDownLatch start = new CountDownLatch(1);
final CountDownLatch finished = new CountDownLatch(processorCount());
prepareLoginThreads(start, finished);
// Releases every login thread simultaneously to increase our chances of
// encountering the race condition
start.countDown();
finished.await();
if (isRaceConditionDetected()) {
e.addError(new AssertionError("At least one race condition in PropertiesLoginModule " + "has been encountered. Please examine the " + "following stack traces for more details:"));
for (Exception exception : errors) {
e.addError(exception);
}
return;
}
}
}
private boolean isRaceConditionDetected() {
return errors.size() > 0;
}
private void prepareLoginThreads(final CountDownLatch start, final CountDownLatch finished) {
for (int processor = 1; processor <= processorCount() * 2; processor++) {
pool.submit(new LoginTester(start, finished, errors, options, callback));
}
}
private int processorCount() {
return Runtime.getRuntime().availableProcessors();
}
private void store(Properties from, File to) throws FileNotFoundException, IOException {
FileOutputStream output = new FileOutputStream(to);
try {
from.store(output, "Generated by " + name.getMethodName());
}
finally {
output.close();
}
}
private void createGroups() throws FileNotFoundException, IOException {
Properties groups = new Properties();
for (int i = 0; i < 100; i++) {
groups.put("group" + i, "first,second,third");
}
store(groups, temp.newFile(GROUPS_FILE));
}
private void createUsers() throws FileNotFoundException, IOException {
Properties users = new Properties();
users.put(USERNAME, PASSWORD);
users.put("second", PASSWORD);
users.put("third", PASSWORD);
store(users, temp.newFile(USERS_FILE));
}
}

View File

@ -0,0 +1,130 @@
/*
* 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.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.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import java.io.IOException;
import java.net.URL;
import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal;
import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal;
import org.junit.Assert;
import org.junit.Test;
public class PropertiesLoginModuleTest extends Assert {
static {
String path = System.getProperty("java.security.auth.login.config");
if (path == null) {
URL resource = PropertiesLoginModuleTest.class.getClassLoader().getResource("login.config");
if (resource != null) {
path = resource.getFile();
System.setProperty("java.security.auth.login.config", path);
}
}
}
@Test
public void testLogin() throws LoginException {
LoginContext context = new LoginContext("PropertiesLogin", new CallbackHandler() {
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (int i = 0; i < callbacks.length; i++) {
if (callbacks[i] instanceof NameCallback) {
((NameCallback) callbacks[i]).setName("first");
}
else if (callbacks[i] instanceof PasswordCallback) {
((PasswordCallback) callbacks[i]).setPassword("secret".toCharArray());
}
else {
throw new UnsupportedCallbackException(callbacks[i]);
}
}
}
});
context.login();
Subject subject = context.getSubject();
assertEquals("Should have three principals", 3, subject.getPrincipals().size());
assertEquals("Should have one user principal", 1, subject.getPrincipals(UserPrincipal.class).size());
assertEquals("Should have two group principals", 2, subject.getPrincipals(RolePrincipal.class).size());
context.logout();
assertEquals("Should have zero principals", 0, subject.getPrincipals().size());
}
@Test
public void testBadUseridLogin() throws Exception {
LoginContext context = new LoginContext("PropertiesLogin", new CallbackHandler() {
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (int i = 0; i < callbacks.length; i++) {
if (callbacks[i] instanceof NameCallback) {
((NameCallback) callbacks[i]).setName("BAD");
}
else if (callbacks[i] instanceof PasswordCallback) {
((PasswordCallback) callbacks[i]).setPassword("secret".toCharArray());
}
else {
throw new UnsupportedCallbackException(callbacks[i]);
}
}
}
});
try {
context.login();
fail("Should have thrown a FailedLoginException");
}
catch (FailedLoginException doNothing) {
}
}
@Test
public void testBadPWLogin() throws Exception {
LoginContext context = new LoginContext("PropertiesLogin", new CallbackHandler() {
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (int i = 0; i < callbacks.length; i++) {
if (callbacks[i] instanceof NameCallback) {
((NameCallback) callbacks[i]).setName("first");
}
else if (callbacks[i] instanceof PasswordCallback) {
((PasswordCallback) callbacks[i]).setPassword("BAD".toCharArray());
}
else {
throw new UnsupportedCallbackException(callbacks[i]);
}
}
}
});
try {
context.login();
fail("Should have thrown a FailedLoginException");
}
catch (FailedLoginException doNothing) {
}
}
}

View File

@ -0,0 +1,61 @@
/*
* 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.core.security.jaas;
import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal;
import org.junit.Assert;
import org.junit.Test;
public class RolePrincipalTest extends Assert {
@Test
public void testArguments() {
RolePrincipal principal = new RolePrincipal("FOO");
assertEquals("FOO", principal.getName());
try {
new RolePrincipal(null);
fail("Should have thrown IllegalArgumentException");
}
catch (IllegalArgumentException ingore) {
}
}
@Test
public void testHash() {
RolePrincipal p1 = new RolePrincipal("FOO");
RolePrincipal p2 = new RolePrincipal("FOO");
assertEquals(p1.hashCode(), p1.hashCode());
assertEquals(p1.hashCode(), p2.hashCode());
}
@Test
public void testEquals() {
RolePrincipal p1 = new RolePrincipal("FOO");
RolePrincipal p2 = new RolePrincipal("FOO");
RolePrincipal p3 = new RolePrincipal("BAR");
assertTrue(p1.equals(p1));
assertTrue(p1.equals(p2));
assertFalse(p1.equals(null));
assertFalse(p1.equals("FOO"));
assertFalse(p1.equals(p3));
}
}

View File

@ -0,0 +1,47 @@
/*
* 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.core.security.jaas;
import javax.security.auth.login.LoginException;
import java.security.cert.X509Certificate;
import java.util.Set;
import org.apache.activemq.artemis.spi.core.security.jaas.CertificateLoginModule;
public class StubCertificateLoginModule extends CertificateLoginModule {
final String userName;
final Set groupNames;
String lastUserName;
X509Certificate[] lastCertChain;
public StubCertificateLoginModule(String userName, Set groupNames) {
this.userName = userName;
this.groupNames = groupNames;
}
protected String getUserNameForCertificates(X509Certificate[] certs) throws LoginException {
lastCertChain = certs;
return userName;
}
protected Set getUserGroups(String username) throws LoginException {
lastUserName = username;
return this.groupNames;
}
}

View File

@ -0,0 +1,61 @@
/*
* 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.core.security.jaas;
import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal;
import org.junit.Assert;
import org.junit.Test;
public class UserPrincipalTest extends Assert {
@Test
public void testArguments() {
UserPrincipal principal = new UserPrincipal("FOO");
assertEquals("FOO", principal.getName());
try {
new UserPrincipal(null);
fail("Should have thrown IllegalArgumentException");
}
catch (IllegalArgumentException ingore) {
}
}
@Test
public void testHash() {
UserPrincipal p1 = new UserPrincipal("FOO");
UserPrincipal p2 = new UserPrincipal("FOO");
assertEquals(p1.hashCode(), p1.hashCode());
assertEquals(p1.hashCode(), p2.hashCode());
}
@Test
public void testEquals() {
UserPrincipal p1 = new UserPrincipal("FOO");
UserPrincipal p2 = new UserPrincipal("FOO");
UserPrincipal p3 = new UserPrincipal("BAR");
assertTrue(p1.equals(p1));
assertTrue(p1.equals(p2));
assertFalse(p1.equals(null));
assertFalse(p1.equals("FOO"));
assertFalse(p1.equals(p3));
}
}

View File

@ -793,20 +793,24 @@ public abstract class ActiveMQTestBase extends Assert {
}
protected final void clearDataRecreateServerDirs() {
clearDataRecreateServerDirs(getTestDir());
clearDataRecreateServerDirs(0, false);
}
protected void clearDataRecreateServerDirs(final String testDir1) {
protected final void clearDataRecreateServerDirs(int index, boolean backup) {
clearDataRecreateServerDirs(getTestDir(), index, backup);
}
protected void clearDataRecreateServerDirs(final String testDir1, int index, boolean backup) {
// Need to delete the root
File file = new File(testDir1);
deleteDirectory(file);
file.mkdirs();
recreateDirectory(getJournalDir(testDir1));
recreateDirectory(getBindingsDir(testDir1));
recreateDirectory(getPageDir(testDir1));
recreateDirectory(getLargeMessagesDir(testDir1));
recreateDirectory(getJournalDir(testDir1, index, backup));
recreateDirectory(getBindingsDir(testDir1, index, backup));
recreateDirectory(getPageDir(testDir1, index, backup));
recreateDirectory(getLargeMessagesDir(testDir1, index, backup));
recreateDirectory(getClientLargeMessagesDir(testDir1));
recreateDirectory(getTemporaryDir(testDir1));
}
@ -815,7 +819,7 @@ public abstract class ActiveMQTestBase extends Assert {
* @return the journalDir
*/
public String getJournalDir() {
return getJournalDir(getTestDir());
return getJournalDir(0, false);
}
protected static String getJournalDir(final String testDir1) {
@ -834,7 +838,7 @@ public abstract class ActiveMQTestBase extends Assert {
* @return the bindingsDir
*/
protected String getBindingsDir() {
return getBindingsDir(getTestDir());
return getBindingsDir(0, false);
}
/**
@ -859,7 +863,7 @@ public abstract class ActiveMQTestBase extends Assert {
* @return the pageDir
*/
protected String getPageDir() {
return getPageDir(getTestDir());
return getPageDir(0, false);
}
protected File getPageDirFile() {
@ -886,7 +890,7 @@ public abstract class ActiveMQTestBase extends Assert {
* @return the largeMessagesDir
*/
protected String getLargeMessagesDir() {
return getLargeMessagesDir(getTestDir());
return getLargeMessagesDir(0, false);
}
/**

View File

@ -0,0 +1,118 @@
/*
* 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.
*/
PropertiesLogin {
org.apache.activemq.artemis.spi.core.security.jaas.PropertiesLoginModule required
debug=true
org.apache.activemq.jaas.properties.user="users.properties"
org.apache.activemq.jaas.properties.role="roles.properties";
};
LDAPLogin {
org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule required
debug=true
initialContextFactory=com.sun.jndi.ldap.LdapCtxFactory
connectionURL="ldap://localhost:1024"
connectionUsername="uid=admin,ou=system"
connectionPassword=secret
connectionProtocol=s
authentication=simple
userBase="ou=system"
userSearchMatching="(uid={0})"
userSearchSubtree=false
roleBase="ou=system"
roleName=cn
roleSearchMatching="(member=uid={1},ou=system)"
roleSearchSubtree=false
;
};
UnAuthenticatedLDAPLogin {
org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule required
debug=true
initialContextFactory=com.sun.jndi.ldap.LdapCtxFactory
connectionURL="ldap://localhost:1024"
connectionUsername="uid=admin,ou=system"
connectionPassword=""
connectionProtocol=s
authentication=simple
userBase="ou=system"
userSearchMatching="(uid={0})"
userSearchSubtree=false
roleBase="ou=system"
roleName=dummyRoleName
roleSearchMatching="(uid={1})"
roleSearchSubtree=false
;
};
ExpandedLDAPLogin {
org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule required
debug=true
initialContextFactory=com.sun.jndi.ldap.LdapCtxFactory
connectionURL="ldap://localhost:1024"
connectionUsername="uid=admin,ou=system"
connectionPassword=secret
connectionProtocol=s
authentication=simple
userBase="ou=system"
userSearchMatching="(uid={0})"
userSearchSubtree=false
roleBase="ou=system"
roleName=cn
roleSearchMatching="(uid={1})"
roleSearchSubtree=false
expandRoles=true
expandRolesMatching="(member={0})"
;
};
GuestLogin {
org.apache.activemq.artemis.spi.core.security.jaas.GuestLoginModule required
debug=true
org.apache.activemq.jaas.guest.user="foo"
org.apache.activemq.jaas.guest.role="bar";
};
GuestLoginWithDefaults {
org.apache.activemq.artemis.spi.core.security.jaas.GuestLoginModule required
debug=true;
};
OpenLdapConfiguration {
org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule required
debug=true
initialContextFactory=com.sun.jndi.ldap.LdapCtxFactory
connectionURL="ldap://localhost:389"
connectionUsername="cn=mqbroker,ou=Services,ou=system,dc=fusesource,dc=com"
connectionPassword="sunflower"
connectionProtocol="s"
topicSearchMatchingFormat="cn={0},ou=Topic,ou=Destination,ou=ActiveMQ,ou=system,dc=fusesource,dc=com"
topicSearchSubtreeBool=true
authentication=simple
userBase="ou=User,ou=ActiveMQ,ou=system,dc=fusesource,dc=com"
userSearchMatching="(uid={0})"
userSearchSubtree=false
roleSearchMatching="(uid={1})"
queueSearchMatchingFormat="cn={0},ou=Queue,ou=Destination,ou=ActiveMQ,ou=system,dc=fusesource,dc=com"
queueSearchSubtreeBool=true
roleBase="ou=Group,ou=ActiveMQ,ou=system,dc=fusesource,dc=com"
roleName=cn
roleSearchMatching="(member:=uid={1})"
roleSearchSubtree=true
;
};

View File

@ -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.
#
programmers=first
accounting=second
employees=first,second

View File

@ -0,0 +1,39 @@
## ---------------------------------------------------------------------------
## 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.
## ---------------------------------------------------------------------------
dn: uid=first,ou=system
uid: first
userPassword: secret
objectClass: account
objectClass: simpleSecurityObject
objectClass: top
###################
## Define groups ##
###################
dn: cn=admins,ou=system
cn: admins
member: uid=first,ou=system
objectClass: groupOfNames
objectClass: top
dn: cn=users,ou=system
cn: users
member: cn=admins,ou=system
objectClass: groupOfNames
objectClass: top

View File

@ -0,0 +1,19 @@
#
# 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.
#
first=secret
second=password

View File

@ -129,17 +129,21 @@ Sockets Layer (SSL) transport.
For more information on configuring the SSL transport, please see [Configuring the Transport](configuring-transports.md).
## Basic user credentials
## User credentials
Apache ActiveMQ Artemis ships with a security manager implementation that reads user
credentials, i.e. user names, passwords and role information from properties
files on the classpath called `artemis-users.properties` and `artemis-roles.properties`. This is the default security manager.
Apache ActiveMQ Artemis ships with two security manager implementations:
- The legacy, deprecated `ActiveMQSecurityManager` that reads user credentials, i.e. user names, passwords and role
information from properties files on the classpath called `artemis-users.properties` and `artemis-roles.properties`.
This is the default security manager.
If you wish to use this security manager, then users, passwords and
roles can easily be added into these files.
- The flexible, pluggable `ActiveMQJAASSecurityManager` which supports any standard JAAS login module. Artemis ships
with several login modules which will be discussed further down.
To configure this manager then it needs to be added to the `bootstrap.xml` configuration.
Lets take a look at what this might look like:
### Non-JAAS Security Manager
If you wish to use the legacy, deprecated `ActiveMQSecurityManager`, then it needs to be added to the `bootstrap.xml`
configuration. Lets take a look at what this might look like:
<basic-security>
<users>file:${activemq.home}/config/non-clustered/artemis-users.properties</users>
@ -149,28 +153,276 @@ Lets take a look at what this might look like:
The first 2 elements `users` and `roles` define what properties files should be used to load in the users and passwords.
The next thing to note is the element `defaultuser`. This defines what
user will be assumed when the client does not specify a
username/password when creating a session. In this case they will be the
user `guest`. Multiple roles can be specified for a default user in the
`artemis-roles.properties`.
The next thing to note is the element `defaultuser`. This defines what user will be assumed when the client does not
specify a username/password when creating a session. In this case they will be the user `guest`. Multiple roles can be
specified for a default user in the `artemis-roles.properties`.
Lets now take alook at the `artemis-users.properties` file, this is basically
just a set of key value pairs that define the users and their password, like so:
Lets now take a look at the `artemis-users.properties` file, this is basically just a set of key value pairs that define
the users and their password, like so:
bill=activemq
andrew=activemq1
frank=activemq2
sam=activemq3
The `artemis-roles.properties` defines what groups these users belong too
where the key is the user and the value is a comma separated list of the groups
the user belongs to, like so:
The `artemis-roles.properties` defines what groups these users belong too where the key is the user and the value is a
comma separated list of the groups the user belongs to, like so:
bill=user
andrew=europe-user,user
frank=us-user,news-user,user
sam=news-user,user
### JAAS Security Manager
When using JAAS much of the configuration depends on which login module is used. However, there are a few commonalities
for every case. Just like in the non-JAAS use-case, the first place to look is in `bootstrap.xml`. Here is an example
using the `PropertiesLogin` JAAS login module which reads user, password, and role information from properties files
much like the non-JAAS security manager implementation:
<jaas-security login-module="PropertiesLogin"/>
No matter what login module you're using, you'll need to specify it here in `bootstrap.xml`. The `login-module` attribute
here refers to the relevant login module entry in `login.config`. For example:
PropertiesLogin {
org.apache.activemq.artemis.spi.core.security.jaas.PropertiesLoginModule required
debug=true
org.apache.activemq.jaas.properties.user="artemis-users.properties"
org.apache.activemq.jaas.properties.role="artemis-roles.properties";
};
The `login.config` file is a standard JAAS configuration file. You can read more about this file on
[Oracle's website](https://docs.oracle.com/javase/8/docs/technotes/guides/security/jgss/tutorials/LoginConfigFile.html).
In short, the file defines:
- an alias for a configuration (e.g. `PropertiesLogin`)
- the implementation class (e.g. `org.apache.activemq.artemis.spi.core.security.jaas.PropertiesLoginModule`)
- a flag which indicates whether the success of the LoginModule is `required`, `requisite`, `sufficient`, or `optional`
- a list of configuration options specific to the login module implementation
By default, the location and name of `login.config` is specified on the Artemis command-line which is set by
`etc/artemis.profile` on linux and `etc\artemis.profile.cmd` on Windows.
### JAAS Login Modules
#### GuestLoginModule
Allows users without credentials (and, depending on how it is configured, possibly also users with invalid credentials)
to access the broker. Normally, the guest login module is chained with another login module, such as a properties login
module. It is implemented by `org.apache.activemq.artemis.spi.core.security.jaas.GuestLoginModule`.
- `org.apache.activemq.jaas.guest.user` - the user name to assign; default is "guest"
- `org.apache.activemq.jaas.guest.role` - the role name to assign; default is "guests"
- `credentialsInvalidate` - boolean flag; if `true`, reject login requests that include a password (i.e. guest login
succeeds only when the user does not provide a password); 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`
There are two basic use cases for the guest login module, as follows:
- Guests with no credentials or invalid credentials.
- Guests with no credentials only.
The following snippet shows how to configure a JAAS login entry for the use case where users with no credentials or
invalid credentials are logged in as guests. In this example, the guest login module is used in combination with the
properties login module.
activemq-domain {
org.apache.activemq.artemis.spi.core.security.jaas.PropertiesLoginModule sufficient
debug=true
org.apache.activemq.jaas.properties.user="artemis-users.properties"
org.apache.activemq.jaas.properties.role="artemis-roles.properties";
org.apache.activemq.artemis.spi.core.security.jaas.GuestLoginModule sufficient
debug=true
org.apache.activemq.jaas.guest.user="anyone"
org.apache.activemq.jaas.guest.role="restricted";
};
Depending on the user login data, authentication proceeds as follows:
- User logs in with a valid password — the properties login module successfully authenticates the user and returns
immediately. The guest login module is not invoked.
- User logs in with an invalid password — the properties login module fails to authenticate the user, and authentication
proceeds to the guest login module. The guest login module successfully authenticates the user and returns the guest principal.
- User logs in with a blank password — the properties login module fails to authenticate the user, and authentication
proceeds to the guest login module. The guest login module successfully authenticates the user and returns the guest principal.
The following snipped shows how to configure a JAAS login entry for the use case where only those users with no
credentials are logged in as guests. To support this use case, you must set the credentialsInvalidate option to true in
the configuration of the guest login module. You should also note that, compared with the preceding example, the order
of the login modules is reversed and the flag attached to the properties login module is changed to requisite.
activemq-guest-when-no-creds-only-domain {
org.apache.activemq.artemis.spi.core.security.jaas.GuestLoginModule sufficient
debug=true
credentialsInvalidate=true
org.apache.activemq.jaas.guest.user="guest"
org.apache.activemq.jaas.guest.role="guests";
org.apache.activemq.artemis.spi.core.security.jaas.PropertiesLoginModule requisite
debug=true
org.apache.activemq.jaas.properties.user="artemis-users.properties"
org.apache.activemq.jaas.properties.role="artemis-roles.properties";
};
Depending on the user login data, authentication proceeds as follows:
- User logs in with a valid password — the guest login module fails to authenticate the user (because the user has
presented a password while the credentialsInvalidate option is enabled) and authentication proceeds to the properties
login module. The properties login module sucessfully authenticates the user and returns.
- User logs in with an invalid password — the guest login module fails to authenticate the user and authentication proceeds
to the properties login module. The properties login module also fails to authenticate the user. The nett result is
authentication failure.
- User logs in with a blank password — the guest login module successfully authenticates the user and returns immediately.
The properties login module is not invoked.
#### PropertiesLoginModule
The JAAS properties login module provides a simple store of authentication data, where the relevant user data is stored
in a pair of flat files. This is convenient for demonstrations and testing, but for an enterprise system, the integration
with LDAP is preferable. It is implemented by `org.apache.activemq.artemis.spi.core.security.jaas.PropertiesLoginModule`.
- `org.apache.activemq.jaas.properties.user` - the path to the file which contains user and password properties
- `org.apache.activemq.jaas.properties.role` - the path to the file which contains user and role properties
- `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`
In the context of the properties login module, the `artemis-users.properties` file consists of a list of properties of the
form, `UserName=Password`. For example, to define the users `system`, `user`, and `guest`, you could create a file like
the following:
system=manager
user=password
guest=password
The `artemis-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:
admins=system
users=system,user
guests=guest
#### LDAPLoginModule
The LDAP login module enables you to perform authentication and authorization by checking the incoming credentials against
user data stored in a central X.500 directory server. For systems that already have an X.500 directory server in place,
this means that you can rapidly integrate ActiveMQ Artemis with the existing security database and user accounts can be
managed using the X.500 system. It is implemented by `org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule`.
- `initialContextFactory` - must always be set to `com.sun.jndi.ldap.LdapCtxFactory`
- `connectionURL` - specify the location of the directory server using an ldap URL, ldap://Host:Port. You can
optionally qualify this URL, by adding a forward slash, `/`, followed by the DN of a particular node in the directory
tree. For example, ldap://ldapserver:10389/ou=system.
- `authentication` - specifies the authentication method used when binding to the LDAP server. Can take either of
the values, `simple` (username and password) or `none` (anonymous).
- `connectionUsername` - the DN of the user that opens the connection to the directory server. For example,
`uid=admin,ou=system`. Directory servers generally require clients to present username/password credentials in order
to open a connection.
- `connectionPassword` - the password that matches the DN from `connectionUsername`. In the directory server,
in the DIT, the password is normally stored as a `userPassword` attribute in the corresponding directory entry.
- `connectionProtocol` - currently, the only supported value is a blank string. In future, this option will allow
you to select the Secure Socket Layer (SSL) for the connection to the directory server. This option must be set
explicitly to an empty string, because it has no default value.
- `userBase` - selects a particular subtree of the DIT to search for user entries. The subtree is specified by a
DN, which specifes the base node of the subtree. For example, by setting this option to `ou=User,ou=ActiveMQ,ou=system`,
the search for user entries is restricted to the subtree beneath the `ou=User,ou=ActiveMQ,ou=system` node.
- `userSearchMatching` - specifies an LDAP search filter, which is applied to the subtree selected by `userBase`.
Before passing to the LDAP search operation, the string value you provide here is subjected to string substitution,
as implemented by the `java.text.MessageFormat` class. Essentially, this means that the special string, `{0}`, is
substituted by the username, as extracted from the incoming client credentials.
After substitution, the string is interpreted as an LDAP search filter, where the LDAP search filter syntax is
defined by the IETF standard, RFC 2254. A short introduction to the search filter syntax is available from Oracle's
JNDI tutorial, [Search Filters](http://download.oracle.com/javase/jndi/tutorial/basics/directory/filter.html).
For example, if this option is set to `(uid={0})` and the received username is `jdoe`, the search filter becomes
`(uid=jdoe)` after string substitution. If the resulting search filter is applied to the subtree selected by the
user base, `ou=User,ou=ActiveMQ,ou=system`, it would match the entry, `uid=jdoe,ou=User,ou=ActiveMQ,ou=system`
(and possibly more deeply nested entries, depending on the specified search depth—see the `userSearchSubtree` option).
- `userSearchSubtree` - specify the search depth for user entries, relative to the node specified by `userBase`.
This option is a boolean. `false` indicates it will try to match one of the child entries of the `userBase` node
(maps to `javax.naming.directory.SearchControls.ONELEVEL_SCOPE`). `true` indicates it will try to match any entry
belonging to the subtree of the `userBase` node (maps to `javax.naming.directory.SearchControls.SUBTREE_SCOPE`).
- `userRoleName` - specifies the name of the multi-valued attribute of the user entry that contains a list of
role names for the user (where the role names are interpreted as group names by the broker's authorization plug-in).
If you omit this option, no role names are extracted from the user entry.
- `roleBase` - if you want to store role data directly in the directory server, you can use a combination of role
options (`roleBase`, `roleSearchMatching`, `roleSearchSubtree`, and `roleName`) as an alternative to (or in addition
to) specifying the `userRoleName` option. This option selects a particular subtree of the DIT to search for role/group
entries. The subtree is specified by a DN, which specifes the base node of the subtree. For example, by setting this
option to `ou=Group,ou=ActiveMQ,ou=system`, the search for role/group entries is restricted to the subtree beneath
the `ou=Group,ou=ActiveMQ,ou=system` node.
- `roleName` - specifies the attribute type of the role entry that contains the name of the role/group (e.g. C, O,
OU, etc.). If you omit this option, the role search feature is effectively disabled.
- `roleSearchMatching` - specifies an LDAP search filter, which is applied to the subtree selected by `roleBase`.
This works in a similar manner to the `userSearchMatching` option, except that it supports two substitution strings,
as follows:
- `{0}` - substitutes the full DN of the matched user entry (that is, the result of the user search). For
example, for the user, `jdoe`, the substituted string could be `uid=jdoe,ou=User,ou=ActiveMQ,ou=system`.
- `{1}` - substitutes the received username. For example, `jdoe`.
For example, if this option is set to `(member=uid={1})` and the received username is `jdoe`, the search filter
becomes `(member=uid=jdoe)` after string substitution (assuming ApacheDS search filter syntax). If the resulting
search filter is applied to the subtree selected by the role base, `ou=Group,ou=ActiveMQ,ou=system`, it matches all
role entries that have a `member` attribute equal to `uid=jdoe` (the value of a `member` attribute is a DN).
This option must always be set, even if role searching is disabled, because it has no default value.
If you use OpenLDAP, the syntax of the search filter is `(member:=uid=jdoe)`.
- `roleSearchSubtree` - specify the search depth for role entries, relative to the node specified by `roleBase`.
This option can take boolean values, as follows:
- `false` (default) - try to match one of the child entries of the roleBase node (maps to
`javax.naming.directory.SearchControls.ONELEVEL_SCOPE`).
- `true` — try to match any entry belonging to the subtree of the roleBase node (maps to
`javax.naming.directory.SearchControls.SUBTREE_SCOPE`).
- `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`
Add user entries under the node specified by the `userBase` option. When creating a new user entry in the directory,
choose an object class that supports the `userPassword` attribute (for example, the `person` or `inetOrgPerson` object
classes are typically suitable). After creating the user entry, add the `userPassword` attribute, to hold the user's
password.
If you want to store role data in dedicated role entries (where each node represents a particular role), create a role
entry as follows. Create a new child of the `roleBase` node, where the `objectClass` of the child is `groupOfNames`. Set
the `cn` (or whatever attribute type is specified by `roleName`) of the new child node equal to the name of the
role/group. Define a `member` attribute for each member of the role/group, setting the `member` value to the DN of the
corresponding user (where the DN is specified either fully, `uid=jdoe,ou=User,ou=ActiveMQ,ou=system`, or partially,
`uid=jdoe`).
If you want to add roles to user entries, you would need to customize the directory schema, by adding a suitable
attribute type to the user entry's object class. The chosen attribute type must be capable of handling multiple values.
## Changing the username/password for clustering

View File

@ -82,6 +82,7 @@ under the License.
<module>request-reply</module>
<module>scheduled-message</module>
<module>security</module>
<module>security-jaas</module>
<module>send-acknowledgements</module>
<module>spring-integration</module>
<module>ssl-enabled</module>
@ -142,6 +143,7 @@ under the License.
<module>rest</module>
<module>scheduled-message</module>
<module>security</module>
<module>security-jaas</module>
<module>send-acknowledgements</module>
<module>spring-integration</module>

View File

@ -0,0 +1,111 @@
<?xml version='1.0'?>
<!--
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.
-->
<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>
<groupId>org.apache.activemq.examples.broker</groupId>
<artifactId>jms-examples</artifactId>
<version>1.1.1-SNAPSHOT</version>
</parent>
<artifactId>security-jaas</artifactId>
<packaging>jar</packaging>
<name>ActiveMQ Artemis JMS JAAS Security Example</name>
<properties>
<activemq.basedir>${project.basedir}/../../../..</activemq.basedir>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>artemis-jms-client</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.activemq</groupId>
<artifactId>artemis-maven-plugin</artifactId>
<executions>
<execution>
<id>create</id>
<goals>
<goal>create</goal>
</goals>
<configuration>
<ignore>${noServer}</ignore>
<brokerSecurity>jaas</brokerSecurity>
</configuration>
</execution>
<execution>
<id>start</id>
<goals>
<goal>cli</goal>
</goals>
<configuration>
<ignore>${noServer}</ignore>
<spawn>true</spawn>
<testURI>tcp://localhost:61616</testURI>
<testUser>bill</testUser>
<testPassword>activemq</testPassword>
<args>
<param>run</param>
</args>
</configuration>
</execution>
<execution>
<id>runClient</id>
<goals>
<goal>runClient</goal>
</goals>
<configuration>
<clientClass>org.apache.activemq.artemis.jms.example.JaasSecurityExample</clientClass>
</configuration>
</execution>
<execution>
<id>stop</id>
<goals>
<goal>cli</goal>
</goals>
<configuration>
<ignore>${noServer}</ignore>
<args>
<param>stop</param>
</args>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.apache.activemq.examples.broker</groupId>
<artifactId>security-jaas</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,324 @@
<!--
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.
-->
<html>
<head>
<title>ActiveMQ Artemis JMS Security Example</title>
<link rel="stylesheet" type="text/css" href="../../../common/common.css" />
<link rel="stylesheet" type="text/css" href="../../../common/prettify.css" />
<script type="text/javascript" src="../../../common/prettify.js"></script>
</head>
<body onload="prettyPrint()">
<h1>JMS JAAS Security Example</h1>
<pre>To run the example, simply type <b>mvn verify</b> from this directory, <br>or <b>mvn -PnoServer verify</b> if you want to start and create the server manually.</pre>
<p>This example shows how to configure and use JAAS security using ActiveMQ Artemis.</p>
<p>With security properly configured, ActiveMQ Artemis can restrict client access to its resources, including
connection creation, message sending/receiving, etc. This is done by configuring users and roles as well as permissions in
the configuration files.</p>
<p>ActiveMQ Artemis supports wild-card security configuration. This feature makes security configuration very
flexible and enables fine-grained control over permissions in an efficient way.</p>
<p>For a full description of how to configure security with ActiveMQ Artemis, please consult the user
manual.</p>
<p>This example demonstrates how to configure users/roles using a JAAS login module, how to configure topics with
proper permissions using wild-card expressions, and how they take effects in a simple program. </p>
<p>First we need to configure users with roles. Since this example is using the <code>PropertiesLogin</code> JAAS
login module the users and roles are configured in <code>artemis-users.properties</code> and
<code>artemis-roles.properties</code> which are referenced from the login module's configuration in <code>login.config</code>.
This example has four users configured as below:</p>
<pre class="prettyprint">
<code>
bill=activemq
andrew=activemq1
frank=activemq2
sam=activemq3
</code>
</pre>
<p>And various roles for those users:</p>
<pre class="prettyprint">
<code>
user=bill,andrew,frank,sam
europe-user=andrew
us-user=frank
news-user=frank,sam
</code>
</pre>
<p>
Each user has three properties available: user name, password, and roles it belongs to. It should be noted that
a user can belong to more than one role. In the above configuration, all users belong to role 'user'. User 'andrew' also
belongs to role 'europe-user', user 'frank' also belongs to 'us-user' and 'news-user' and user 'sam' also belongs to 'news-user'.
</p>
<p>
User name and password consists of a valid account that can be used to establish connections to a ActiveMQ Artemis server, while
roles are used in controlling the access privileges against ActiveMQ Artemis topics and queues. You can achieve this control by
configuring proper permissions in <code>broker.xml</code>, like the following
</p>
<pre class="prettyprint"><code>
&lt;security-settings&gt;
&lt;!-- any user can have full control of generic topics --&gt;
&lt;security-setting match=&quot;jms.topic.#&quot;&gt;
&lt;permission type=&quot;createDurableQueue&quot; roles=&quot;user&quot;/&gt;
&lt;permission type=&quot;deleteDurableQueue&quot; roles=&quot;user&quot;/&gt;
&lt;permission type=&quot;createNonDurableQueue&quot; roles=&quot;user&quot;/&gt;
&lt;permission type=&quot;deleteNonDurableQueue&quot; roles=&quot;user&quot;/&gt;
&lt;permission type=&quot;send&quot; roles=&quot;user&quot;/&gt;
&lt;permission type=&quot;consume&quot; roles=&quot;user&quot;/&gt;
&lt;/security-setting&gt;
&lt;security-setting match=&quot;jms.topic.news.europe.#&quot;&gt;
&lt;permission type=&quot;createDurableQueue&quot; roles=&quot;user&quot;/&gt;
&lt;permission type=&quot;deleteDurableQueue&quot; roles=&quot;user&quot;/&gt;
&lt;permission type=&quot;createNonDurableQueue&quot; roles=&quot;user&quot;/&gt;
&lt;permission type=&quot;deleteNonDurableQueue&quot; roles=&quot;user&quot;/&gt;
&lt;permission type=&quot;send&quot; roles=&quot;europe-user&quot;/&gt;
&lt;permission type=&quot;consume&quot; roles=&quot;news-user&quot;/&gt;
&lt;/security-setting&gt;
&lt;security-setting match=&quot;jms.topic.news.us.#&quot;&gt;
&lt;permission type=&quot;createDurableQueue&quot; roles=&quot;user&quot;/&gt;
&lt;permission type=&quot;deleteDurableQueue&quot; roles=&quot;user&quot;/&gt;
&lt;permission type=&quot;createNonDurableQueue&quot; roles=&quot;user&quot;/&gt;
&lt;permission type=&quot;deleteNonDurableQueue&quot; roles=&quot;user&quot;/&gt;
&lt;permission type=&quot;send&quot; roles=&quot;us-user&quot;/&gt;
&lt;permission type=&quot;consume&quot; roles=&quot;news-user&quot;/&gt;
&lt;/security-setting&gt;
&lt;/security-settings&gt;
</code></pre>
<p>Permissions can be defined on any group of queues, by using a wildcard. You can easily specify
wildcards to apply certain permissions to a set of matching queues and topics. In the above configuration
we have created four sets of permissions, each set matches against a special group of targets, indicated by wild-card match attributes.</p>
<p>You can provide a very broad permission control as a default and then add more strict control
over specific addresses. By the above we define the following access rules:</p>
<li>Only role 'us-user' can create/delete and pulish messages to topics whose names match wild-card pattern 'news.us.#'.</li>
<li>Only role 'europe-user' can create/delete and publish messages to topics whose names match wild-card pattern 'news.europe.#'.</li>
<li>Only role 'news-user' can subscribe messages to topics whose names match wild-card pattern 'news.us.#' and 'news.europe.#'.</li>
<li>For any other topics that don't match any of the above wild-card patterns, permissions are granted to users of role 'user'.</li>
<p>To illustrate the effect of permissions, three topics are deployed. Topic 'genericTopic' matches 'jms.topic.#' wild-card, topic 'news.europe.europeTopic' matches
jms.topic.news.europe.#' wild-cards, and topic 'news.us.usTopic' matches 'jms.topic.news.us.#'.</p>
<p>With ActiveMQ Artemis, the security manager is also configurable. You can use JAASSecurityManager or JBossASSecurityManager based on you need. Please
check out the activemq-beans.xml for how to do. In this example we just use the basic ActiveMQSecurityManagerImpl which reads users/roles/passwords from the xml
file <code>activemq-users.xml</code>.
<h2>Example step-by-step</h2>
<p><i>To run the example, simply type <code>mvn verify -Pexample</code> from this directory</i></p>
<ol>
<li>First we need to get an initial context so we can look-up the JMS connection factory and destination objects from JNDI. This initial context will get it's properties from the <code>client-jndi.properties</code> file in the directory <code>../common/config</code></li>
<pre class="prettyprint">
<code>
InitialContext initialContext = getContext(0);
</code>
</pre>
<li>We perform lookup on the topics</li>
<pre class="prettyprint">
<code>
Topic genericTopic = (Topic) initialContext.lookup("/topic/genericTopic");
Topic europeTopic = (Topic) initialContext.lookup("/topic/europeTopic");
Topic usTopic = (Topic) initialContext.lookup("/topic/usTopic");
</code>
</pre>
<li>We perform a lookup on the Connection Factory</li>
<pre class="prettyprint">
<code>
ConnectionFactory cf = (ConnectionFactory) initialContext.lookup("/ConnectionFactory");
</code>
</pre>
<li>We try to create a JMS Connection without user/password. It will fail.</li>
<pre class="prettyprint">
<code>
try
{
cf.createConnection();
result = false;
}
catch (JMSSecurityException e)
{
System.out.println("Default user cannot get a connection. Details: " + e.getMessage());
}
</code>
</pre>
<li>Bill tries to make a connection using wrong password</li>
<pre class="prettyprint">
<code>
billConnection = null;
try
{
billConnection = createConnection("bill", "activemq1", cf);
result = false;
}
catch (JMSException e)
{
System.out.println("User bill failed to connect. Details: " + e.getMessage());
}
</code>
</pre>
<li>Bill makes a good connection.</li>
<pre class="prettyprint">
<code>
billConnection = createConnection("bill", "activemq", cf);
billConnection.start();
</code>
</pre>
<li>Andrew makes a good connection</li>
<pre class="prettyprint">
<code>
andrewConnection = createConnection("andrew", "activemq1", cf);
andrewConnection.start();
</code>
</pre>
<li>Frank makes a good connection</li>
<pre class="prettyprint">
<code>
frankConnection = createConnection("frank", "activemq2", cf);
frankConnection.start();
</code>
</pre>
<li>Sam makes a good connection</li>
<pre class="prettyprint">
<code>
samConnection = createConnection("sam", "activemq3", cf);
samConnection.start();
</code>
</pre>
<li>We check every user can publish/subscribe genericTopics</li>
<pre class="prettyprint">
<code>
checkUserSendAndReceive(genericTopic, billConnection, "bill");
checkUserSendAndReceive(genericTopic, andrewConnection, "andrew");
checkUserSendAndReceive(genericTopic, frankConnection, "frank");
checkUserSendAndReceive(genericTopic, samConnection, "sam");
</code>
</pre>
<li>We check permissions on news.europe.europeTopic for bill: can't send and can't receive</li>
<pre class="prettyprint">
<code>
checkUserNoSendNoReceive(europeTopic, billConnection, "bill", andrewConnection, frankConnection);
</code>
</pre>
<li>We check permissions on news.europe.europeTopic for andrew: can send but can't receive</li>
<pre class="prettyprint">
<code>
checkUserSendNoReceive(europeTopic, andrewConnection, "andrew", frankConnection);
</code>
</pre>
<li>We check permissions on news.europe.europeTopic for frank: can't send but can receive</li>
<pre class="prettyprint">
<code>
checkUserReceiveNoSend(europeTopic, frankConnection, "frank", andrewConnection);
</code>
</pre>
<li>We check permissions on news.europe.europeTopic for sam: can't send but can receive</li>
<pre class="prettyprint">
<code>
checkUserReceiveNoSend(europeTopic, samConnection, "sam", andrewConnection);
</code>
</pre>
<li>We check permissions on news.us.usTopic for bill: can't send and can't receive</li>
<pre class="prettyprint">
<code>
checkUserNoSendNoReceive(usTopic, billConnection, "bill");
</code>
</pre>
<li>We check permissions on news.us.usTopic for andrew: can't send and can't receive</li>
<pre class="prettyprint">
<code>
checkUserNoSendNoReceive(usTopic, andrewConnection, "andrew");
</code>
</pre>
<li>We check permissions on news.us.usTopic for frank: can both send and receive</li>
<pre class="prettyprint">
<code>
checkUserSendAndReceive(usTopic, frankConnection, "frank");
</code>
</pre>
<li>We check permissions on news.us.usTopic for sam: can't send but can receive</li>
<pre class="prettyprint">
<code>
checkUserReceiveNoSend(usTopic, samConnection, "sam", frankConnection);
</code>
</pre>
<li>And finally, <b>always</b> remember to close your JMS connections and resources after use, in a <code>finally</code> block. Closing a JMS connection will automatically close all of its sessions, consumers, producer and browser objects</li>
<pre class="prettyprint">
<code>
finally
{
if (billConnection != null)
{
billConnection.close();
}
if (andrewConnection != null)
{
andrewConnection.close();
}
if (frankConnection != null)
{
frankConnection.close();
}
if (samConnection != null)
{
samConnection.close();
}
// Also the initialContext
if (initialContext != null)
{
initialContext.close();
}
}
</code>
</pre>
</ol>
</body>
</html>

View File

@ -0,0 +1,282 @@
/*
* 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.jms.example;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.JMSSecurityException;
import javax.jms.MessageConsumer;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.jms.Topic;
import javax.naming.InitialContext;
public class JaasSecurityExample {
public static void main(final String[] args) throws Exception {
boolean result = true;
Connection failConnection = null;
Connection billConnection = null;
Connection andrewConnection = null;
Connection frankConnection = null;
Connection samConnection = null;
InitialContext initialContext = null;
try {
// /Step 1. Create an initial context to perform the JNDI lookup.
initialContext = new InitialContext();
// Step 2. perform lookup on the topics
Topic genericTopic = (Topic) initialContext.lookup("topic/genericTopic");
Topic europeTopic = (Topic) initialContext.lookup("topic/europeTopic");
Topic usTopic = (Topic) initialContext.lookup("topic/usTopic");
// Step 3. perform a lookup on the Connection Factory
ConnectionFactory cf = (ConnectionFactory) initialContext.lookup("ConnectionFactory");
// Step 4. Try to create a JMS Connection without user/password. It will fail.
try {
failConnection = cf.createConnection();
result = false;
}
catch (JMSSecurityException e) {
System.out.println("Default user cannot get a connection. Details: " + e.getMessage());
}
// Step 5. bill tries to make a connection using wrong password
billConnection = null;
try {
billConnection = createConnection("bill", "activemq1", cf);
result = false;
}
catch (JMSException e) {
System.out.println("User bill failed to connect. Details: " + e.getMessage());
}
// Step 6. bill makes a good connection.
billConnection = createConnection("bill", "activemq", cf);
billConnection.start();
// Step 7. andrew makes a good connection.
andrewConnection = createConnection("andrew", "activemq1", cf);
andrewConnection.start();
// Step 8. frank makes a good connection.
frankConnection = createConnection("frank", "activemq2", cf);
frankConnection.start();
// Step 9. sam makes a good connection.
samConnection = createConnection("sam", "activemq3", cf);
samConnection.start();
// Step 10. Check every user can publish/subscribe genericTopics.
System.out.println("------------------------Checking permissions on " + genericTopic + "----------------");
checkUserSendAndReceive(genericTopic, billConnection, "bill");
checkUserSendAndReceive(genericTopic, andrewConnection, "andrew");
checkUserSendAndReceive(genericTopic, frankConnection, "frank");
checkUserSendAndReceive(genericTopic, samConnection, "sam");
System.out.println("-------------------------------------------------------------------------------------");
System.out.println("------------------------Checking permissions on " + europeTopic + "----------------");
// Step 11. Check permissions on news.europe.europeTopic for bill: can't send and can't receive
checkUserNoSendNoReceive(europeTopic, billConnection, "bill");
// Step 12. Check permissions on news.europe.europeTopic for andrew: can send but can't receive
checkUserSendNoReceive(europeTopic, andrewConnection, "andrew", frankConnection);
// Step 13. Check permissions on news.europe.europeTopic for frank: can't send but can receive
checkUserReceiveNoSend(europeTopic, frankConnection, "frank", andrewConnection);
// Step 14. Check permissions on news.europe.europeTopic for sam: can't send but can receive
checkUserReceiveNoSend(europeTopic, samConnection, "sam", andrewConnection);
System.out.println("-------------------------------------------------------------------------------------");
System.out.println("------------------------Checking permissions on " + usTopic + "----------------");
// Step 15. Check permissions on news.us.usTopic for bill: can't send and can't receive
checkUserNoSendNoReceive(usTopic, billConnection, "bill");
// Step 16. Check permissions on news.us.usTopic for andrew: can't send and can't receive
checkUserNoSendNoReceive(usTopic, andrewConnection, "andrew");
// Step 17. Check permissions on news.us.usTopic for frank: can both send and receive
checkUserSendAndReceive(usTopic, frankConnection, "frank");
// Step 18. Check permissions on news.us.usTopic for sam: can't send but can receive
checkUserReceiveNoSend(usTopic, samConnection, "sam", frankConnection);
System.out.println("-------------------------------------------------------------------------------------");
}
finally {
// Step 19. Be sure to close our JMS resources!
if (failConnection != null) {
failConnection.close();
}
if (billConnection != null) {
billConnection.close();
}
if (andrewConnection != null) {
andrewConnection.close();
}
if (frankConnection != null) {
frankConnection.close();
}
if (samConnection != null) {
samConnection.close();
}
// Also the initialContext
if (initialContext != null) {
initialContext.close();
}
}
}
// Check the user can receive message but cannot send message.
private static void checkUserReceiveNoSend(final Topic topic,
final Connection connection,
final String user,
final Connection sendingConn) throws JMSException {
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageProducer producer = session.createProducer(topic);
MessageConsumer consumer = session.createConsumer(topic);
TextMessage msg = session.createTextMessage("hello-world-1");
try {
producer.send(msg);
throw new IllegalStateException("Security setting is broken! User " + user +
" can send message [" +
msg.getText() +
"] to topic " +
topic);
}
catch (JMSException e) {
System.out.println("User " + user + " cannot send message [" + msg.getText() + "] to topic: " + topic);
}
// Now send a good message
Session session1 = sendingConn.createSession(false, Session.AUTO_ACKNOWLEDGE);
producer = session1.createProducer(topic);
producer.send(msg);
TextMessage receivedMsg = (TextMessage) consumer.receive(2000);
if (receivedMsg != null) {
System.out.println("User " + user + " can receive message [" + receivedMsg.getText() + "] from topic " + topic);
}
else {
throw new IllegalStateException("Security setting is broken! User " + user + " cannot receive message from topic " + topic);
}
session1.close();
session.close();
}
// Check the user can send message but cannot receive message
private static void checkUserSendNoReceive(final Topic topic,
final Connection connection,
final String user,
final Connection receivingConn) throws JMSException {
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageProducer producer = session.createProducer(topic);
try {
session.createConsumer(topic);
}
catch (JMSException e) {
System.out.println("User " + user + " cannot receive any message from topic " + topic);
}
Session session1 = receivingConn.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageConsumer goodConsumer = session1.createConsumer(topic);
TextMessage msg = session.createTextMessage("hello-world-2");
producer.send(msg);
TextMessage receivedMsg = (TextMessage) goodConsumer.receive(2000);
if (receivedMsg != null) {
System.out.println("User " + user + " can send message [" + receivedMsg.getText() + "] to topic " + topic);
}
else {
throw new IllegalStateException("Security setting is broken! User " + user +
" cannot send message [" +
msg.getText() +
"] to topic " +
topic);
}
session.close();
session1.close();
}
// Check the user has neither send nor receive permission on topic
private static void checkUserNoSendNoReceive(final Topic topic,
final Connection connection,
final String user) throws JMSException {
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageProducer producer = session.createProducer(topic);
try {
session.createConsumer(topic);
}
catch (JMSException e) {
System.out.println("User " + user + " cannot create consumer on topic " + topic);
}
TextMessage msg = session.createTextMessage("hello-world-3");
try {
producer.send(msg);
throw new IllegalStateException("Security setting is broken! User " + user +
" can send message [" +
msg.getText() +
"] to topic " +
topic);
}
catch (JMSException e) {
System.out.println("User " + user + " cannot send message [" + msg.getText() + "] to topic: " + topic);
}
session.close();
}
// Check the user connection has both send and receive permissions on the topic
private static void checkUserSendAndReceive(final Topic topic,
final Connection connection,
final String user) throws JMSException {
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
TextMessage msg = session.createTextMessage("hello-world-4");
MessageProducer producer = session.createProducer(topic);
MessageConsumer consumer = session.createConsumer(topic);
producer.send(msg);
TextMessage receivedMsg = (TextMessage) consumer.receive(5000);
if (receivedMsg != null) {
System.out.println("User " + user + " can send message: [" + msg.getText() + "] to topic: " + topic);
System.out.println("User " + user + " can receive message: [" + msg.getText() + "] from topic: " + topic);
}
else {
throw new IllegalStateException("Error! User " + user + " cannot receive the message! ");
}
session.close();
}
private static Connection createConnection(final String username,
final String password,
final ConnectionFactory cf) throws JMSException {
return cf.createConnection(username, password);
}
}

View File

@ -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.
## ---------------------------------------------------------------------------
user=bill,andrew,frank,sam
europe-user=andrew
us-user=frank
news-user=frank,sam

View File

@ -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.
## ---------------------------------------------------------------------------
bill=activemq
andrew=activemq1
frank=activemq2
sam=activemq3

View File

@ -0,0 +1,81 @@
<?xml version='1.0'?>
<!--
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.
-->
<configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="urn:activemq"
xsi:schemaLocation="urn:activemq /schema/artemis-server.xsd">
<jms xmlns="urn:activemq:jms">
<topic name="genericTopic"/>
<topic name="news.europe.europeTopic"/>
<topic name="news.us.usTopic"/>
</jms>
<core xmlns="urn:activemq:core">
<bindings-directory>./data/messaging/bindings</bindings-directory>
<journal-directory>./data/messaging/journal</journal-directory>
<large-messages-directory>./data/messaging/largemessages</large-messages-directory>
<paging-directory>./data/messaging/paging</paging-directory>
<!-- Acceptors -->
<acceptors>
<acceptor name="netty-acceptor">tcp://localhost:61616</acceptor>
</acceptors>
<!-- Other config -->
<security-settings>
<!-- any user can have full control of generic topics -->
<security-setting match="jms.topic.#">
<permission type="createDurableQueue" roles="user"/>
<permission type="deleteDurableQueue" roles="user"/>
<permission type="createNonDurableQueue" roles="user"/>
<permission type="deleteNonDurableQueue" roles="user"/>
<permission type="send" roles="user"/>
<permission type="consume" roles="user"/>
</security-setting>
<security-setting match="jms.topic.news.europe.#">
<permission type="createDurableQueue" roles="user"/>
<permission type="deleteDurableQueue" roles="user"/>
<permission type="createNonDurableQueue" roles="user"/>
<permission type="deleteNonDurableQueue" roles="user"/>
<permission type="send" roles="europe-user"/>
<permission type="consume" roles="news-user"/>
</security-setting>
<security-setting match="jms.topic.news.us.#">
<permission type="createDurableQueue" roles="user"/>
<permission type="deleteDurableQueue" roles="user"/>
<permission type="createNonDurableQueue" roles="user"/>
<permission type="deleteNonDurableQueue" roles="user"/>
<permission type="send" roles="us-user"/>
<permission type="consume" roles="news-user"/>
</security-setting>
</security-settings>
</core>
</configuration>

View File

@ -0,0 +1,22 @@
# 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.
java.naming.factory.initial=org.apache.activemq.artemis.jndi.ActiveMQInitialContextFactory
connectionFactory.ConnectionFactory=tcp://localhost:61616
topic.topic/genericTopic=genericTopic
topic.topic/europeTopic=news.europe.europeTopic
topic.topic/usTopic=news.us.usTopic

View File

@ -123,6 +123,8 @@
<geronimo.jms.2.spec.version>1.0-alpha-2</geronimo.jms.2.spec.version>
<javac-compiler-id>javac-with-errorprone</javac-compiler-id>
<directory-version>1.5.7</directory-version>
</properties>
<scm>

View File

@ -261,6 +261,24 @@
<version>1.0.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-server-integ</artifactId>
<version>${directory-version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-core-integ</artifactId>
<version>${directory-version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>bouncycastle</groupId>
<artifactId>bcprov-jdk15</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>

View File

@ -0,0 +1,347 @@
/*
* 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.security;
import javax.naming.Context;
import javax.naming.NameClassPair;
import javax.naming.NamingEnumeration;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import java.io.File;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Set;
import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.api.core.TransportConfiguration;
import org.apache.activemq.artemis.api.core.client.ActiveMQClient;
import org.apache.activemq.artemis.api.core.client.ClientConsumer;
import org.apache.activemq.artemis.api.core.client.ClientProducer;
import org.apache.activemq.artemis.api.core.client.ClientSession;
import org.apache.activemq.artemis.api.core.client.ClientSessionFactory;
import org.apache.activemq.artemis.api.core.client.ServerLocator;
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.invm.InVMConnectorFactory;
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.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.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
@RunWith(FrameworkRunner.class)
@CreateLdapServer(transports = {@CreateTransport(protocol = "LDAP", port = 1024)})
@ApplyLdifFiles("test.ldif")
public class LDAPSecurityTest extends AbstractLdapTestUnit {
static {
String path = System.getProperty("java.security.auth.login.config");
if (path == null) {
URL resource = LDAPSecurityTest.class.getClassLoader().getResource("login.config");
if (resource != null) {
path = resource.getFile();
System.setProperty("java.security.auth.login.config", path);
}
}
}
private ServerLocator locator;
public static final String TARGET_TMP = "./target/tmp";
private static final String PRINCIPAL = "uid=admin,ou=system";
private static final String CREDENTIALS = "secret";
public LDAPSecurityTest() {
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 {
locator = ActiveMQClient.createServerLocatorWithHA(new TransportConfiguration(InVMConnectorFactory.class.getCanonicalName()));
testDir = temporaryFolder.getRoot().getAbsolutePath();
}
@SuppressWarnings("unchecked")
@Test
public void testRunning() throws Exception {
Hashtable env = new Hashtable();
env.put(Context.PROVIDER_URL, "ldap://localhost:1024");
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, PRINCIPAL);
env.put(Context.SECURITY_CREDENTIALS, CREDENTIALS);
DirContext ctx = new InitialDirContext(env);
HashSet set = new HashSet();
NamingEnumeration list = ctx.list("ou=system");
while (list.hasMore()) {
NameClassPair ncp = (NameClassPair) list.next();
set.add(ncp.getName());
}
Assert.assertTrue(set.contains("uid=admin"));
Assert.assertTrue(set.contains("ou=users"));
Assert.assertTrue(set.contains("ou=groups"));
Assert.assertTrue(set.contains("ou=configuration"));
Assert.assertTrue(set.contains("prefNodeName=sysPrefRoot"));
}
@Test
public void testJAASSecurityManagerAuthentication() throws Exception {
ActiveMQServer server = getActiveMQServer();
server.start();
ClientSessionFactory cf = locator.createSessionFactory();
try {
ClientSession session = cf.createSession("first", "secret", false, true, true, false, 0);
session.close();
}
catch (ActiveMQException e) {
e.printStackTrace();
Assert.fail("should not throw exception");
}
cf.close();
locator.close();
server.stop();
}
@Test
public void testJAASSecurityManagerAuthenticationBadPassword() throws Exception {
ActiveMQServer server = getActiveMQServer();
server.start();
ClientSessionFactory cf = locator.createSessionFactory();
try {
cf.createSession("first", "badpassword", false, true, true, false, 0);
Assert.fail("should throw exception here");
}
catch (Exception e) {
// ignore
}
cf.close();
locator.close();
server.stop();
}
@Test
public void testJAASSecurityManagerAuthorizationNegative() throws Exception {
final SimpleString ADDRESS = new SimpleString("address");
final SimpleString DURABLE_QUEUE = new SimpleString("durableQueue");
final SimpleString NON_DURABLE_QUEUE = new SimpleString("nonDurableQueue");
ActiveMQServer server = getActiveMQServer();
Set<Role> roles = new HashSet<>();
roles.add(new Role("programmers", false, false, false, false, false, false, false));
server.getConfiguration().getSecurityRoles().put("#", roles);
server.start();
server.createQueue(ADDRESS, DURABLE_QUEUE, null, true, false);
server.createQueue(ADDRESS, NON_DURABLE_QUEUE, null, false, false);
ClientSessionFactory cf = locator.createSessionFactory();
ClientSession session = cf.createSession("first", "secret", false, true, true, false, 0);
// CREATE_DURABLE_QUEUE
try {
session.createQueue(ADDRESS, DURABLE_QUEUE, true);
Assert.fail("should throw exception here");
}
catch (ActiveMQException e) {
// ignore
}
// DELETE_DURABLE_QUEUE
try {
session.deleteQueue(DURABLE_QUEUE);
Assert.fail("should throw exception here");
}
catch (ActiveMQException e) {
// ignore
}
// CREATE_NON_DURABLE_QUEUE
try {
session.createQueue(ADDRESS, NON_DURABLE_QUEUE, false);
Assert.fail("should throw exception here");
}
catch (ActiveMQException e) {
// ignore
}
// DELETE_NON_DURABLE_QUEUE
try {
session.deleteQueue(NON_DURABLE_QUEUE);
Assert.fail("should throw exception here");
}
catch (ActiveMQException e) {
// ignore
}
// PRODUCE
try {
ClientProducer producer = session.createProducer(ADDRESS);
producer.send(session.createMessage(true));
Assert.fail("should throw exception here");
}
catch (ActiveMQException e) {
// ignore
}
// CONSUME
try {
ClientConsumer consumer = session.createConsumer(DURABLE_QUEUE);
Assert.fail("should throw exception here");
}
catch (ActiveMQException e) {
// ignore
}
// MANAGE
try {
ClientProducer producer = session.createProducer(server.getConfiguration().getManagementAddress());
producer.send(session.createMessage(true));
Assert.fail("should throw exception here");
}
catch (ActiveMQException e) {
// ignore
}
session.close();
cf.close();
locator.close();
server.stop();
}
@Test
public void testJAASSecurityManagerAuthorizationPositive() throws Exception {
final SimpleString ADDRESS = new SimpleString("address");
final SimpleString DURABLE_QUEUE = new SimpleString("durableQueue");
final SimpleString NON_DURABLE_QUEUE = new SimpleString("nonDurableQueue");
ActiveMQServer server = getActiveMQServer();
Set<Role> roles = new HashSet<>();
roles.add(new Role("admins", true, true, true, true, true, true, true));
server.getConfiguration().getSecurityRoles().put("#", roles);
server.start();
ClientSessionFactory cf = locator.createSessionFactory();
ClientSession session = cf.createSession("first", "secret", false, true, true, false, 0);
// CREATE_DURABLE_QUEUE
try {
session.createQueue(ADDRESS, DURABLE_QUEUE, true);
}
catch (ActiveMQException e) {
e.printStackTrace();
Assert.fail("should not throw exception here");
}
// DELETE_DURABLE_QUEUE
try {
session.deleteQueue(DURABLE_QUEUE);
}
catch (ActiveMQException e) {
e.printStackTrace();
Assert.fail("should not throw exception here");
}
// CREATE_NON_DURABLE_QUEUE
try {
session.createQueue(ADDRESS, NON_DURABLE_QUEUE, false);
}
catch (ActiveMQException e) {
Assert.fail("should not throw exception here");
}
// DELETE_NON_DURABLE_QUEUE
try {
session.deleteQueue(NON_DURABLE_QUEUE);
}
catch (ActiveMQException e) {
Assert.fail("should not throw exception here");
}
session.createQueue(ADDRESS, DURABLE_QUEUE, true);
// PRODUCE
try {
ClientProducer producer = session.createProducer(ADDRESS);
producer.send(session.createMessage(true));
}
catch (ActiveMQException e) {
Assert.fail("should not throw exception here");
}
// CONSUME
try {
session.createConsumer(DURABLE_QUEUE);
}
catch (ActiveMQException e) {
Assert.fail("should not throw exception here");
}
// MANAGE
try {
ClientProducer producer = session.createProducer(server.getConfiguration().getManagementAddress());
producer.send(session.createMessage(true));
}
catch (ActiveMQException e) {
Assert.fail("should not throw exception here");
}
session.close();
cf.close();
locator.close();
server.stop();
}
private ActiveMQServer getActiveMQServer() {
ActiveMQJAASSecurityManager securityManager = new ActiveMQJAASSecurityManager();
securityManager.setConfigurationName("LDAPLogin");
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));
return ActiveMQServers.newActiveMQServer(configuration, ManagementFactory.getPlatformMBeanServer(), securityManager, false);
}
}

View File

@ -18,6 +18,8 @@ package org.apache.activemq.artemis.tests.integration.security;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;
@ -30,24 +32,37 @@ import org.apache.activemq.artemis.api.core.client.ClientProducer;
import org.apache.activemq.artemis.api.core.client.ClientSession;
import org.apache.activemq.artemis.api.core.client.ClientSessionFactory;
import org.apache.activemq.artemis.api.core.client.ServerLocator;
import org.apache.activemq.artemis.tests.util.CreateMessage;
import org.apache.activemq.artemis.tests.util.ActiveMQTestBase;
import org.apache.activemq.artemis.core.config.Configuration;
import org.apache.activemq.artemis.core.security.CheckType;
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.core.server.Queue;
import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl;
import org.apache.activemq.artemis.core.settings.HierarchicalRepository;
import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager;
import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager2;
import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManagerImpl;
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager;
import org.apache.activemq.artemis.tests.util.ActiveMQTestBase;
import org.apache.activemq.artemis.tests.util.CreateMessage;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
public class SecurityTest extends ActiveMQTestBase {
static {
String path = System.getProperty("java.security.auth.login.config");
if (path == null) {
URL resource = SecurityTest.class.getClassLoader().getResource("login.config");
if (resource != null) {
path = resource.getFile();
System.setProperty("java.security.auth.login.config", path);
}
}
}
/*
* create session tests
*/
@ -67,6 +82,301 @@ public class SecurityTest extends ActiveMQTestBase {
locator = createInVMNonHALocator();
}
@Test
public void testJAASSecurityManagerAuthentication() throws Exception {
ActiveMQJAASSecurityManager securityManager = new ActiveMQJAASSecurityManager();
securityManager.setConfigurationName("PropertiesLogin");
ActiveMQServer server = addServer(ActiveMQServers.newActiveMQServer(createDefaultInVMConfig().setSecurityEnabled(true), ManagementFactory.getPlatformMBeanServer(), securityManager, false));
server.start();
ClientSessionFactory cf = createSessionFactory(locator);
try {
ClientSession session = cf.createSession("first", "secret", false, true, true, false, 0);
session.close();
}
catch (ActiveMQException e) {
e.printStackTrace();
Assert.fail("should not throw exception");
}
}
@Test
public void testJAASSecurityManagerAuthenticationBadPassword() throws Exception {
ActiveMQJAASSecurityManager securityManager = new ActiveMQJAASSecurityManager();
securityManager.setConfigurationName("PropertiesLogin");
ActiveMQServer server = addServer(ActiveMQServers.newActiveMQServer(createDefaultInVMConfig().setSecurityEnabled(true), ManagementFactory.getPlatformMBeanServer(), securityManager, false));
server.start();
ClientSessionFactory cf = createSessionFactory(locator);
try {
cf.createSession("first", "badpassword", false, true, true, false, 0);
Assert.fail("should throw exception here");
}
catch (Exception e) {
// ignore
}
}
@Test
public void testJAASSecurityManagerAuthenticationGuest() throws Exception {
ActiveMQJAASSecurityManager securityManager = new ActiveMQJAASSecurityManager();
securityManager.setConfigurationName("GuestLogin");
ActiveMQServer server = addServer(ActiveMQServers.newActiveMQServer(createDefaultInVMConfig().setSecurityEnabled(true), ManagementFactory.getPlatformMBeanServer(), securityManager, false));
server.start();
ClientSessionFactory cf = createSessionFactory(locator);
try {
ClientSession session = cf.createSession("first", "secret", false, true, true, false, 0);
session.close();
}
catch (ActiveMQException e) {
e.printStackTrace();
Assert.fail("should not throw exception");
}
}
@Test
public void testJAASSecurityManagerAuthorizationNegative() throws Exception {
final SimpleString ADDRESS = new SimpleString("address");
final SimpleString DURABLE_QUEUE = new SimpleString("durableQueue");
final SimpleString NON_DURABLE_QUEUE = new SimpleString("nonDurableQueue");
ActiveMQJAASSecurityManager securityManager = new ActiveMQJAASSecurityManager();
securityManager.setConfigurationName("PropertiesLogin");
ActiveMQServer server = addServer(ActiveMQServers.newActiveMQServer(createDefaultInVMConfig().setSecurityEnabled(true), ManagementFactory.getPlatformMBeanServer(), securityManager, false));
Set<Role> roles = new HashSet<>();
roles.add(new Role("programmers", false, false, false, false, false, false, false));
server.getConfiguration().getSecurityRoles().put("#", roles);
server.start();
server.createQueue(ADDRESS, DURABLE_QUEUE, null, true, false);
server.createQueue(ADDRESS, NON_DURABLE_QUEUE, null, false, false);
ClientSessionFactory cf = createSessionFactory(locator);
ClientSession session = addClientSession(cf.createSession("first", "secret", false, true, true, false, 0));
// CREATE_DURABLE_QUEUE
try {
session.createQueue(ADDRESS, DURABLE_QUEUE, true);
Assert.fail("should throw exception here");
}
catch (ActiveMQException e) {
// ignore
}
// DELETE_DURABLE_QUEUE
try {
session.deleteQueue(DURABLE_QUEUE);
Assert.fail("should throw exception here");
}
catch (ActiveMQException e) {
// ignore
}
// CREATE_NON_DURABLE_QUEUE
try {
session.createQueue(ADDRESS, NON_DURABLE_QUEUE, false);
Assert.fail("should throw exception here");
}
catch (ActiveMQException e) {
// ignore
}
// DELETE_NON_DURABLE_QUEUE
try {
session.deleteQueue(NON_DURABLE_QUEUE);
Assert.fail("should throw exception here");
}
catch (ActiveMQException e) {
// ignore
}
// PRODUCE
try {
ClientProducer producer = session.createProducer(ADDRESS);
producer.send(session.createMessage(true));
Assert.fail("should throw exception here");
}
catch (ActiveMQException e) {
// ignore
}
// CONSUME
try {
ClientConsumer consumer = session.createConsumer(DURABLE_QUEUE);
Assert.fail("should throw exception here");
}
catch (ActiveMQException e) {
// ignore
}
// MANAGE
try {
ClientProducer producer = session.createProducer(server.getConfiguration().getManagementAddress());
producer.send(session.createMessage(true));
Assert.fail("should throw exception here");
}
catch (ActiveMQException e) {
// ignore
}
}
@Test
public void testJAASSecurityManagerAuthorizationPositive() throws Exception {
final SimpleString ADDRESS = new SimpleString("address");
final SimpleString DURABLE_QUEUE = new SimpleString("durableQueue");
final SimpleString NON_DURABLE_QUEUE = new SimpleString("nonDurableQueue");
ActiveMQJAASSecurityManager securityManager = new ActiveMQJAASSecurityManager();
securityManager.setConfigurationName("PropertiesLogin");
ActiveMQServer server = addServer(ActiveMQServers.newActiveMQServer(createDefaultInVMConfig().setSecurityEnabled(true), ManagementFactory.getPlatformMBeanServer(), securityManager, false));
Set<Role> roles = new HashSet<>();
roles.add(new Role("programmers", true, true, true, true, true, true, true));
server.getConfiguration().getSecurityRoles().put("#", roles);
server.start();
ClientSessionFactory cf = createSessionFactory(locator);
ClientSession session = addClientSession(cf.createSession("first", "secret", false, true, true, false, 0));
// CREATE_DURABLE_QUEUE
try {
session.createQueue(ADDRESS, DURABLE_QUEUE, true);
}
catch (ActiveMQException e) {
Assert.fail("should not throw exception here");
}
// DELETE_DURABLE_QUEUE
try {
session.deleteQueue(DURABLE_QUEUE);
}
catch (ActiveMQException e) {
Assert.fail("should not throw exception here");
}
// CREATE_NON_DURABLE_QUEUE
try {
session.createQueue(ADDRESS, NON_DURABLE_QUEUE, false);
}
catch (ActiveMQException e) {
Assert.fail("should not throw exception here");
}
// DELETE_NON_DURABLE_QUEUE
try {
session.deleteQueue(NON_DURABLE_QUEUE);
}
catch (ActiveMQException e) {
Assert.fail("should not throw exception here");
}
session.createQueue(ADDRESS, DURABLE_QUEUE, true);
// PRODUCE
try {
ClientProducer producer = session.createProducer(ADDRESS);
producer.send(session.createMessage(true));
}
catch (ActiveMQException e) {
Assert.fail("should not throw exception here");
}
// CONSUME
try {
session.createConsumer(DURABLE_QUEUE);
}
catch (ActiveMQException e) {
Assert.fail("should not throw exception here");
}
// MANAGE
try {
ClientProducer producer = session.createProducer(server.getConfiguration().getManagementAddress());
producer.send(session.createMessage(true));
}
catch (ActiveMQException e) {
Assert.fail("should not throw exception here");
}
}
@Test
public void testJAASSecurityManagerAuthorizationPositiveGuest() throws Exception {
final SimpleString ADDRESS = new SimpleString("address");
final SimpleString DURABLE_QUEUE = new SimpleString("durableQueue");
final SimpleString NON_DURABLE_QUEUE = new SimpleString("nonDurableQueue");
ActiveMQJAASSecurityManager securityManager = new ActiveMQJAASSecurityManager();
securityManager.setConfigurationName("GuestLogin");
ActiveMQServer server = addServer(ActiveMQServers.newActiveMQServer(createDefaultInVMConfig().setSecurityEnabled(true), ManagementFactory.getPlatformMBeanServer(), securityManager, false));
Set<Role> roles = new HashSet<>();
roles.add(new Role("bar", true, true, true, true, true, true, true));
server.getConfiguration().getSecurityRoles().put("#", roles);
server.start();
ClientSessionFactory cf = createSessionFactory(locator);
ClientSession session = addClientSession(cf.createSession("junk", "junk", false, true, true, false, 0));
// CREATE_DURABLE_QUEUE
try {
session.createQueue(ADDRESS, DURABLE_QUEUE, true);
}
catch (ActiveMQException e) {
e.printStackTrace();
Assert.fail("should not throw exception here");
}
// DELETE_DURABLE_QUEUE
try {
session.deleteQueue(DURABLE_QUEUE);
}
catch (ActiveMQException e) {
Assert.fail("should not throw exception here");
}
// CREATE_NON_DURABLE_QUEUE
try {
session.createQueue(ADDRESS, NON_DURABLE_QUEUE, false);
}
catch (ActiveMQException e) {
Assert.fail("should not throw exception here");
}
// DELETE_NON_DURABLE_QUEUE
try {
session.deleteQueue(NON_DURABLE_QUEUE);
}
catch (ActiveMQException e) {
Assert.fail("should not throw exception here");
}
session.createQueue(ADDRESS, DURABLE_QUEUE, true);
// PRODUCE
try {
ClientProducer producer = session.createProducer(ADDRESS);
producer.send(session.createMessage(true));
}
catch (ActiveMQException e) {
Assert.fail("should not throw exception here");
}
// CONSUME
try {
session.createConsumer(DURABLE_QUEUE);
}
catch (ActiveMQException e) {
Assert.fail("should not throw exception here");
}
// MANAGE
try {
ClientProducer producer = session.createProducer(server.getConfiguration().getManagementAddress());
producer.send(session.createMessage(true));
}
catch (ActiveMQException e) {
Assert.fail("should not throw exception here");
}
}
@Test
public void testCreateSessionWithNullUserPass() throws Exception {
ActiveMQServer server = createServer();

View File

@ -0,0 +1,118 @@
/*
* 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.
*/
PropertiesLogin {
org.apache.activemq.artemis.spi.core.security.jaas.PropertiesLoginModule required
debug=true
org.apache.activemq.jaas.properties.user="users.properties"
org.apache.activemq.jaas.properties.role="roles.properties";
};
LDAPLogin {
org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule required
debug=true
initialContextFactory=com.sun.jndi.ldap.LdapCtxFactory
connectionURL="ldap://localhost:1024"
connectionUsername="uid=admin,ou=system"
connectionPassword=secret
connectionProtocol=s
authentication=simple
userBase="ou=system"
userSearchMatching="(uid={0})"
userSearchSubtree=false
roleBase="ou=system"
roleName=cn
roleSearchMatching="(member=uid={1},ou=system)"
roleSearchSubtree=false
;
};
UnAuthenticatedLDAPLogin {
org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule required
debug=true
initialContextFactory=com.sun.jndi.ldap.LdapCtxFactory
connectionURL="ldap://localhost:1024"
connectionUsername="uid=admin,ou=system"
connectionPassword=""
connectionProtocol=s
authentication=simple
userBase="ou=system"
userSearchMatching="(uid={0})"
userSearchSubtree=false
roleBase="ou=system"
roleName=dummyRoleName
roleSearchMatching="(uid={1})"
roleSearchSubtree=false
;
};
ExpandedLDAPLogin {
org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule required
debug=true
initialContextFactory=com.sun.jndi.ldap.LdapCtxFactory
connectionURL="ldap://localhost:1024"
connectionUsername="uid=admin,ou=system"
connectionPassword=secret
connectionProtocol=s
authentication=simple
userBase="ou=system"
userSearchMatching="(uid={0})"
userSearchSubtree=false
roleBase="ou=system"
roleName=cn
roleSearchMatching="(uid={1})"
roleSearchSubtree=false
expandRoles=true
expandRolesMatching="(member={0})"
;
};
GuestLogin {
org.apache.activemq.artemis.spi.core.security.jaas.GuestLoginModule required
debug=true
org.apache.activemq.jaas.guest.user="foo"
org.apache.activemq.jaas.guest.role="bar";
};
GuestLoginWithDefaults {
org.apache.activemq.artemis.spi.core.security.jaas.GuestLoginModule required
debug=true;
};
OpenLdapConfiguration {
org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule required
debug=true
initialContextFactory=com.sun.jndi.ldap.LdapCtxFactory
connectionURL="ldap://localhost:389"
connectionUsername="cn=mqbroker,ou=Services,ou=system,dc=fusesource,dc=com"
connectionPassword="sunflower"
connectionProtocol="s"
topicSearchMatchingFormat="cn={0},ou=Topic,ou=Destination,ou=ActiveMQ,ou=system,dc=fusesource,dc=com"
topicSearchSubtreeBool=true
authentication=simple
userBase="ou=User,ou=ActiveMQ,ou=system,dc=fusesource,dc=com"
userSearchMatching="(uid={0})"
userSearchSubtree=false
roleSearchMatching="(uid={1})"
queueSearchMatchingFormat="cn={0},ou=Queue,ou=Destination,ou=ActiveMQ,ou=system,dc=fusesource,dc=com"
queueSearchSubtreeBool=true
roleBase="ou=Group,ou=ActiveMQ,ou=system,dc=fusesource,dc=com"
roleName=cn
roleSearchMatching="(member:=uid={1})"
roleSearchSubtree=true
;
};

View File

@ -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.
#
programmers=first
accounting=second
employees=first,second

View File

@ -0,0 +1,39 @@
## ---------------------------------------------------------------------------
## 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.
## ---------------------------------------------------------------------------
dn: uid=first,ou=system
uid: first
userPassword: secret
objectClass: account
objectClass: simpleSecurityObject
objectClass: top
###################
## Define groups ##
###################
dn: cn=admins,ou=system
cn: admins
member: uid=first,ou=system
objectClass: groupOfNames
objectClass: top
dn: cn=users,ou=system
cn: users
member: cn=admins,ou=system
objectClass: groupOfNames
objectClass: top

View File

@ -0,0 +1,19 @@
#
# 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.
#
first=secret
second=password