From 6ed9c5ae91dc7a08cdb3825fb17a5da24037fa36 Mon Sep 17 00:00:00 2001 From: jbertram Date: Thu, 1 Oct 2015 16:58:26 -0500 Subject: [PATCH] ARTEMIS-74 import JAAS auth from 5.x This change allows the use of JAAS login modules for basic authentication and authorization. --- .../activemq/artemis/cli/commands/Create.java | 53 +- .../artemis/factory/JaasSecurityHandler.java | 32 ++ .../artemis/broker/security/jaas-security | 17 + ...perties => artemis-roles-basic.properties} | 0 .../etc/artemis-roles-jaas.properties | 17 + .../artemis/cli/commands/etc/artemis.profile | 2 +- .../cli/commands/etc/artemis.profile.cmd | 2 +- .../etc/basic-broker-security-settings.txt | 5 + .../artemis/cli/commands/etc/bootstrap.xml | 5 +- .../etc/jaas-broker-security-settings.txt | 2 + .../artemis/cli/commands/etc/login.config | 22 + .../cli/test/StreamClassPathTest.java | 5 +- .../activemq/artemis/dto/JaasSecurityDTO.java | 30 ++ .../apache/activemq/artemis/dto/jaxb.index | 7 +- .../artemis/maven/ArtemisCreatePlugin.java | 5 +- artemis-server/pom.xml | 18 + .../security/ActiveMQJAASSecurityManager.java | 114 ++++ .../security/ActiveMQSecurityManager2.java | 2 +- .../core/security/JAASSecurityManager.java | 216 -------- .../security/jaas/CertificateCallback.java | 48 ++ .../security/jaas/CertificateLoginModule.java | 183 +++++++ .../core/security/jaas/GuestLoginModule.java | 132 +++++ .../jaas/JaasCertificateCallbackHandler.java | 65 +++ .../jaas/JaasCredentialCallbackHandler.java | 63 +++ .../core/security/jaas/LDAPLoginModule.java | 505 ++++++++++++++++++ .../core/security/jaas/LDAPLoginProperty.java | 41 ++ .../security/jaas/PrincipalProperties.java | 75 +++ .../security/jaas/PropertiesLoginModule.java | 215 ++++++++ .../spi/core/security/jaas/RolePrincipal.java | 68 +++ .../jaas/TextFileCertificateLoginModule.java | 147 +++++ .../spi/core/security/jaas/UserPrincipal.java | 68 +++ .../jaas/CertificateLoginModuleTest.java | 154 ++++++ .../security/jaas/GuestLoginModuleTest.java | 91 ++++ .../security/jaas/LDAPLoginModuleTest.java | 149 ++++++ .../jaas/LDAPModuleRoleExpansionTest.java | 136 +++++ ...ropertiesLoginModuleRaceConditionTest.java | 195 +++++++ .../jaas/PropertiesLoginModuleTest.java | 130 +++++ .../core/security/jaas/RolePrincipalTest.java | 61 +++ .../jaas/StubCertificateLoginModule.java | 47 ++ .../core/security/jaas/UserPrincipalTest.java | 61 +++ .../artemis/tests/util/ActiveMQTestBase.java | 24 +- .../src/test/resources/login.config | 118 ++++ .../src/test/resources/roles.properties | 20 + artemis-server/src/test/resources/test.ldif | 39 ++ .../src/test/resources/users.properties | 19 + docs/user-manual/en/security.md | 288 +++++++++- examples/features/standard/pom.xml | 2 + .../features/standard/security-jaas/pom.xml | 111 ++++ .../standard/security-jaas/readme.html | 324 +++++++++++ .../jms/example/JaasSecurityExample.java | 282 ++++++++++ .../activemq/server0/artemis-roles.properties | 20 + .../activemq/server0/artemis-users.properties | 20 + .../resources/activemq/server0/broker.xml | 81 +++ .../src/main/resources/jndi.properties | 22 + pom.xml | 2 + tests/integration-tests/pom.xml | 18 + .../security/LDAPSecurityTest.java | 347 ++++++++++++ .../integration/security/SecurityTest.java | 314 ++++++++++- .../src/test/resources/login.config | 118 ++++ .../src/test/resources/roles.properties | 20 + .../src/test/resources/test.ldif | 39 ++ .../src/test/resources/users.properties | 19 + 62 files changed, 5169 insertions(+), 266 deletions(-) create mode 100644 artemis-cli/src/main/java/org/apache/activemq/artemis/factory/JaasSecurityHandler.java create mode 100644 artemis-cli/src/main/resources/META-INF/services/org/apache/activemq/artemis/broker/security/jaas-security rename artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/{artemis-roles.properties => artemis-roles-basic.properties} (100%) create mode 100644 artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis-roles-jaas.properties create mode 100644 artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/basic-broker-security-settings.txt create mode 100644 artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/jaas-broker-security-settings.txt create mode 100644 artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/login.config create mode 100644 artemis-dto/src/main/java/org/apache/activemq/artemis/dto/JaasSecurityDTO.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/ActiveMQJAASSecurityManager.java delete mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/JAASSecurityManager.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/CertificateCallback.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/CertificateLoginModule.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/GuestLoginModule.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/JaasCertificateCallbackHandler.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/JaasCredentialCallbackHandler.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginProperty.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PrincipalProperties.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PropertiesLoginModule.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/RolePrincipal.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/TextFileCertificateLoginModule.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/UserPrincipal.java create mode 100644 artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/CertificateLoginModuleTest.java create mode 100644 artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/GuestLoginModuleTest.java create mode 100644 artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/LDAPLoginModuleTest.java create mode 100644 artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/LDAPModuleRoleExpansionTest.java create mode 100644 artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/PropertiesLoginModuleRaceConditionTest.java create mode 100644 artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/PropertiesLoginModuleTest.java create mode 100644 artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/RolePrincipalTest.java create mode 100644 artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/StubCertificateLoginModule.java create mode 100644 artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/UserPrincipalTest.java create mode 100644 artemis-server/src/test/resources/login.config create mode 100644 artemis-server/src/test/resources/roles.properties create mode 100644 artemis-server/src/test/resources/test.ldif create mode 100644 artemis-server/src/test/resources/users.properties create mode 100644 examples/features/standard/security-jaas/pom.xml create mode 100644 examples/features/standard/security-jaas/readme.html create mode 100644 examples/features/standard/security-jaas/src/main/java/org/apache/activemq/artemis/jms/example/JaasSecurityExample.java create mode 100644 examples/features/standard/security-jaas/src/main/resources/activemq/server0/artemis-roles.properties create mode 100644 examples/features/standard/security-jaas/src/main/resources/activemq/server0/artemis-users.properties create mode 100644 examples/features/standard/security-jaas/src/main/resources/activemq/server0/broker.xml create mode 100644 examples/features/standard/security-jaas/src/main/resources/jndi.properties create mode 100644 tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/LDAPSecurityTest.java create mode 100644 tests/integration-tests/src/test/resources/login.config create mode 100644 tests/integration-tests/src/test/resources/roles.properties create mode 100644 tests/integration-tests/src/test/resources/test.ldif create mode 100644 tests/integration-tests/src/test/resources/users.properties diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/Create.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/Create.java index 0a430a3b25..0c558f8010 100644 --- a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/Create.java +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/Create.java @@ -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(""); diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/factory/JaasSecurityHandler.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/factory/JaasSecurityHandler.java new file mode 100644 index 0000000000..2cd1785e11 --- /dev/null +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/factory/JaasSecurityHandler.java @@ -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; + } +} diff --git a/artemis-cli/src/main/resources/META-INF/services/org/apache/activemq/artemis/broker/security/jaas-security b/artemis-cli/src/main/resources/META-INF/services/org/apache/activemq/artemis/broker/security/jaas-security new file mode 100644 index 0000000000..013a63c20a --- /dev/null +++ b/artemis-cli/src/main/resources/META-INF/services/org/apache/activemq/artemis/broker/security/jaas-security @@ -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 diff --git a/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis-roles.properties b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis-roles-basic.properties similarity index 100% rename from artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis-roles.properties rename to artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis-roles-basic.properties diff --git a/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis-roles-jaas.properties b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis-roles-jaas.properties new file mode 100644 index 0000000000..c9443dd9db --- /dev/null +++ b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis-roles-jaas.properties @@ -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} \ No newline at end of file diff --git a/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis.profile b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis.profile index 2a51e2adba..76ca12f449 100644 --- a/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis.profile +++ b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis.profile @@ -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 diff --git a/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis.profile.cmd b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis.profile.cmd index c52d70f2f0..835c7b70fc 100644 --- a/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis.profile.cmd +++ b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/artemis.profile.cmd @@ -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 diff --git a/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/basic-broker-security-settings.txt b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/basic-broker-security-settings.txt new file mode 100644 index 0000000000..dd0a5f121b --- /dev/null +++ b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/basic-broker-security-settings.txt @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/bootstrap.xml b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/bootstrap.xml index be51734db5..fe3f864191 100644 --- a/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/bootstrap.xml +++ b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/bootstrap.xml @@ -18,10 +18,7 @@ - +${broker-security-settings} diff --git a/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/jaas-broker-security-settings.txt b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/jaas-broker-security-settings.txt new file mode 100644 index 0000000000..6521bf4b5f --- /dev/null +++ b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/jaas-broker-security-settings.txt @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/login.config b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/login.config new file mode 100644 index 0000000000..fe8ca36878 --- /dev/null +++ b/artemis-cli/src/main/resources/org/apache/activemq/artemis/cli/commands/etc/login.config @@ -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"; +}; \ No newline at end of file diff --git a/artemis-cli/src/test/java/org/apache/activemq/cli/test/StreamClassPathTest.java b/artemis-cli/src/test/java/org/apache/activemq/cli/test/StreamClassPathTest.java index 21579dc9b0..e1d045d05e 100644 --- a/artemis-cli/src/test/java/org/apache/activemq/cli/test/StreamClassPathTest.java +++ b/artemis-cli/src/test/java/org/apache/activemq/cli/test/StreamClassPathTest.java @@ -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 { diff --git a/artemis-dto/src/main/java/org/apache/activemq/artemis/dto/JaasSecurityDTO.java b/artemis-dto/src/main/java/org/apache/activemq/artemis/dto/JaasSecurityDTO.java new file mode 100644 index 0000000000..99163cf55c --- /dev/null +++ b/artemis-dto/src/main/java/org/apache/activemq/artemis/dto/JaasSecurityDTO.java @@ -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; +} diff --git a/artemis-dto/src/main/resources/org/apache/activemq/artemis/dto/jaxb.index b/artemis-dto/src/main/resources/org/apache/activemq/artemis/dto/jaxb.index index 5803f734c4..b0bacd7ba7 100644 --- a/artemis-dto/src/main/resources/org/apache/activemq/artemis/dto/jaxb.index +++ b/artemis-dto/src/main/resources/org/apache/activemq/artemis/dto/jaxb.index @@ -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 diff --git a/artemis-maven-plugin/src/main/java/org/apache/activemq/artemis/maven/ArtemisCreatePlugin.java b/artemis-maven-plugin/src/main/java/org/apache/activemq/artemis/maven/ArtemisCreatePlugin.java index 39b6d8eeda..ba6cb8e045 100644 --- a/artemis-maven-plugin/src/main/java/org/apache/activemq/artemis/maven/ArtemisCreatePlugin.java +++ b/artemis-maven-plugin/src/main/java/org/apache/activemq/artemis/maven/ArtemisCreatePlugin.java @@ -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 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"); diff --git a/artemis-server/pom.xml b/artemis-server/pom.xml index 652ef81686..36cef29408 100644 --- a/artemis-server/pom.xml +++ b/artemis-server/pom.xml @@ -82,6 +82,24 @@ junit test + + org.apache.directory.server + apacheds-server-integ + ${directory-version} + test + + + org.apache.directory.server + apacheds-core-integ + ${directory-version} + test + + + bouncycastle + bcprov-jdk15 + + + diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/ActiveMQJAASSecurityManager.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/ActiveMQJAASSecurityManager.java new file mode 100644 index 0000000000..6a13f22e6d --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/ActiveMQJAASSecurityManager.java @@ -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 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 rolesWithPermission = getPrincipalsInRole(checkType, roles); + + // Check the caller's roles + Set rolesForSubject = localSubject.getPrincipals(RolePrincipal.class); + if (rolesForSubject.size() > 0 && rolesWithPermission.size() > 0) { + Iterator rolesForSubjectIter = rolesForSubject.iterator(); + while (!authorized && rolesForSubjectIter.hasNext()) { + Iterator 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 getPrincipalsInRole(final CheckType checkType, final Set roles) { + Set 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; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/ActiveMQSecurityManager2.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/ActiveMQSecurityManager2.java index 2962153c52..1e3cb108a3 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/ActiveMQSecurityManager2.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/ActiveMQSecurityManager2.java @@ -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 roles, CheckType checkType, String address); -} +} \ No newline at end of file diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/JAASSecurityManager.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/JAASSecurityManager.java deleted file mode 100644 index 48699b64a1..0000000000 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/JAASSecurityManager.java +++ /dev/null @@ -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 Roles 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 roles, - final CheckType checkType) { - Subject localSubject = null; - try { - localSubject = getAuthenticatedSubject(user, password); - } - catch (LoginException e1) { - return false; - } - - boolean authenticated = true; - - if (localSubject != null) { - Set 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 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 subjectGroups = subject.getPrincipals(Group.class); - Iterator 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 getRolePrincipals(final CheckType checkType, final Set roles) { - Set principals = new HashSet(); - 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; - } - } - -} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/CertificateCallback.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/CertificateCallback.java new file mode 100644 index 0000000000..630dd32b4b --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/CertificateCallback.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activemq.artemis.spi.core.security.jaas; + +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; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/CertificateLoginModule.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/CertificateLoginModule.java new file mode 100644 index 0000000000..db8808b720 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/CertificateLoginModule.java @@ -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 groups; + private Set principals = new HashSet(); + 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 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; + } + } + +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/GuestLoginModule.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/GuestLoginModule.java new file mode 100644 index 0000000000..dbea86bad5 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/GuestLoginModule.java @@ -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 principals = new HashSet(); + 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; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/JaasCertificateCallbackHandler.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/JaasCertificateCallbackHandler.java new file mode 100644 index 0000000000..b53f946fbc --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/JaasCertificateCallbackHandler.java @@ -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); + } + } + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/JaasCredentialCallbackHandler.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/JaasCredentialCallbackHandler.java new file mode 100644 index 0000000000..34ae701467 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/JaasCredentialCallbackHandler.java @@ -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); + } + } + } + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java new file mode 100644 index 0000000000..6830828e58 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java @@ -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 groups = new HashSet(); + + @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 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 list = new ArrayList(); + 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 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 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 getRoles(DirContext context, + String dn, + String username, + List currentRoles) throws NamingException { + List 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(); + } + 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 haveSeenNames = new HashSet(); + Queue pendingNameExpansion = new LinkedList(); + NamingEnumeration 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 addAttributeValues(String attrId, + Attributes attrs, + List values) throws NamingException { + + if (attrId == null || attrs == null) { + return values; + } + if (values == null) { + values = new ArrayList(); + } + 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 env = new Hashtable(); + 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; + } + +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginProperty.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginProperty.java new file mode 100644 index 0000000000..139d6c62cd --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginProperty.java @@ -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; + } + +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PrincipalProperties.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PrincipalProperties.java new file mode 100644 index 0000000000..97e2355e5e --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PrincipalProperties.java @@ -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> 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; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PropertiesLoginModule.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PropertiesLoginModule.java new file mode 100644 index 0000000000..6b96ed0b2f --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PropertiesLoginModule.java @@ -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 principals = new HashSet(); + 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 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; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/RolePrincipal.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/RolePrincipal.java new file mode 100644 index 0000000000..3e5afd1eb5 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/RolePrincipal.java @@ -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; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/TextFileCertificateLoginModule.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/TextFileCertificateLoginModule.java new file mode 100644 index 0000000000..77f5a43fc5 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/TextFileCertificateLoginModule.java @@ -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 =. This class also + * uses a group definition file where each line is =,,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 keys = users.keys(); + for (Enumeration 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 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 userGroups = new HashSet(); + for (Enumeration 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; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/UserPrincipal.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/UserPrincipal.java new file mode 100644 index 0000000000..9e20f06949 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/UserPrincipal.java @@ -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; + } +} diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/CertificateLoginModuleTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/CertificateLoginModuleTest.java new file mode 100644 index 0000000000..9b3c33d17c --- /dev/null +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/CertificateLoginModuleTest.java @@ -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 GROUP_NAMES = new Vector(); + + 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 groupNames) throws LoginException { + loginModule = new StubCertificateLoginModule(userName, new HashSet(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(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()); + } + 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(GROUP_NAMES)); + } + catch (Exception e) { + fail("Unable to login: " + e.getMessage()); + } + + loginModule.logout(); + + assertEquals("logout should have cleared Subject principals.", 0, subject.getPrincipals().size()); + } +} diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/GuestLoginModuleTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/GuestLoginModuleTest.java new file mode 100644 index 0000000000..54e743a3d7 --- /dev/null +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/GuestLoginModuleTest.java @@ -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()); + } +} diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/LDAPLoginModuleTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/LDAPLoginModuleTest.java new file mode 100644 index 0000000000..f9bcbefae3 --- /dev/null +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/LDAPLoginModuleTest.java @@ -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"); + } +} diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/LDAPModuleRoleExpansionTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/LDAPModuleRoleExpansionTest.java new file mode 100644 index 0000000000..b63f89d2fd --- /dev/null +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/LDAPModuleRoleExpansionTest.java @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activemq.artemis.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(); + } +} diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/PropertiesLoginModuleRaceConditionTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/PropertiesLoginModuleRaceConditionTest.java new file mode 100644 index 0000000000..de72563203 --- /dev/null +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/PropertiesLoginModuleRaceConditionTest.java @@ -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 options; + private BlockingQueue errors; + private ExecutorService pool; + private CallbackHandler callback; + + private static class LoginTester implements Runnable { + + private final CountDownLatch finished; + private final BlockingQueue errors; + private final Map options; + private final CountDownLatch start; + private final CallbackHandler callback; + + LoginTester(CountDownLatch start, + CountDownLatch finished, + BlockingQueue errors, + Map 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(), 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(); + 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(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)); + } +} diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/PropertiesLoginModuleTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/PropertiesLoginModuleTest.java new file mode 100644 index 0000000000..5aec37f728 --- /dev/null +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/PropertiesLoginModuleTest.java @@ -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) { + } + + } +} diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/RolePrincipalTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/RolePrincipalTest.java new file mode 100644 index 0000000000..e23fe6aa0e --- /dev/null +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/RolePrincipalTest.java @@ -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)); + } +} diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/StubCertificateLoginModule.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/StubCertificateLoginModule.java new file mode 100644 index 0000000000..6c7bf24133 --- /dev/null +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/StubCertificateLoginModule.java @@ -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; + } +} diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/UserPrincipalTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/UserPrincipalTest.java new file mode 100644 index 0000000000..9a081414a4 --- /dev/null +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/UserPrincipalTest.java @@ -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)); + } +} diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/tests/util/ActiveMQTestBase.java b/artemis-server/src/test/java/org/apache/activemq/artemis/tests/util/ActiveMQTestBase.java index 715094c2a8..7cbe2f072e 100644 --- a/artemis-server/src/test/java/org/apache/activemq/artemis/tests/util/ActiveMQTestBase.java +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/tests/util/ActiveMQTestBase.java @@ -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); } /** diff --git a/artemis-server/src/test/resources/login.config b/artemis-server/src/test/resources/login.config new file mode 100644 index 0000000000..9b1e1c003b --- /dev/null +++ b/artemis-server/src/test/resources/login.config @@ -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 + ; +}; diff --git a/artemis-server/src/test/resources/roles.properties b/artemis-server/src/test/resources/roles.properties new file mode 100644 index 0000000000..de332d395d --- /dev/null +++ b/artemis-server/src/test/resources/roles.properties @@ -0,0 +1,20 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +programmers=first +accounting=second +employees=first,second diff --git a/artemis-server/src/test/resources/test.ldif b/artemis-server/src/test/resources/test.ldif new file mode 100644 index 0000000000..6d6bd588ce --- /dev/null +++ b/artemis-server/src/test/resources/test.ldif @@ -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 \ No newline at end of file diff --git a/artemis-server/src/test/resources/users.properties b/artemis-server/src/test/resources/users.properties new file mode 100644 index 0000000000..1087b0b3f1 --- /dev/null +++ b/artemis-server/src/test/resources/users.properties @@ -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 diff --git a/docs/user-manual/en/security.md b/docs/user-manual/en/security.md index eeb36c16d4..34e1a4926b 100644 --- a/docs/user-manual/en/security.md +++ b/docs/user-manual/en/security.md @@ -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: file:${activemq.home}/config/non-clustered/artemis-users.properties @@ -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: + + + +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 diff --git a/examples/features/standard/pom.xml b/examples/features/standard/pom.xml index fe56e8764f..eaf9b38765 100644 --- a/examples/features/standard/pom.xml +++ b/examples/features/standard/pom.xml @@ -82,6 +82,7 @@ under the License. request-reply scheduled-message security + security-jaas send-acknowledgements spring-integration ssl-enabled @@ -142,6 +143,7 @@ under the License. rest scheduled-message security + security-jaas send-acknowledgements spring-integration diff --git a/examples/features/standard/security-jaas/pom.xml b/examples/features/standard/security-jaas/pom.xml new file mode 100644 index 0000000000..ff975a319b --- /dev/null +++ b/examples/features/standard/security-jaas/pom.xml @@ -0,0 +1,111 @@ + + + + + 4.0.0 + + + org.apache.activemq.examples.broker + jms-examples + 1.1.1-SNAPSHOT + + + security-jaas + jar + ActiveMQ Artemis JMS JAAS Security Example + + + ${project.basedir}/../../../.. + + + + + org.apache.activemq + artemis-jms-client + ${project.version} + + + + + + + org.apache.activemq + artemis-maven-plugin + + + create + + create + + + ${noServer} + jaas + + + + start + + cli + + + ${noServer} + true + tcp://localhost:61616 + bill + activemq + + run + + + + + runClient + + runClient + + + org.apache.activemq.artemis.jms.example.JaasSecurityExample + + + + stop + + cli + + + ${noServer} + + stop + + + + + + + org.apache.activemq.examples.broker + security-jaas + ${project.version} + + + + + + + diff --git a/examples/features/standard/security-jaas/readme.html b/examples/features/standard/security-jaas/readme.html new file mode 100644 index 0000000000..9a678f6929 --- /dev/null +++ b/examples/features/standard/security-jaas/readme.html @@ -0,0 +1,324 @@ + + + + + ActiveMQ Artemis JMS Security Example + + + + + +

JMS JAAS Security Example

+ +
To run the example, simply type mvn verify from this directory, 
or mvn -PnoServer verify if you want to start and create the server manually.
+ + +

This example shows how to configure and use JAAS security using ActiveMQ Artemis.

+ +

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.

+ +

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.

+ +

For a full description of how to configure security with ActiveMQ Artemis, please consult the user + manual.

+ +

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.

+ +

First we need to configure users with roles. Since this example is using the PropertiesLogin JAAS + login module the users and roles are configured in artemis-users.properties and + artemis-roles.properties which are referenced from the login module's configuration in login.config. + This example has four users configured as below:

+ +
+     
+         bill=activemq
+         andrew=activemq1
+         frank=activemq2
+         sam=activemq3
+     
+     
+ +

And various roles for those users:

+ +
+     
+         user=bill,andrew,frank,sam
+         europe-user=andrew
+         us-user=frank
+         news-user=frank,sam
+     
+     
+ +

+ 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'. +

+

+ 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 broker.xml, like the following +

+

+      <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>
+     
+ +

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.

+ +

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:

+ +
  • Only role 'us-user' can create/delete and pulish messages to topics whose names match wild-card pattern 'news.us.#'.
  • +
  • Only role 'europe-user' can create/delete and publish messages to topics whose names match wild-card pattern 'news.europe.#'.
  • +
  • Only role 'news-user' can subscribe messages to topics whose names match wild-card pattern 'news.us.#' and 'news.europe.#'.
  • +
  • For any other topics that don't match any of the above wild-card patterns, permissions are granted to users of role 'user'.
  • + +

    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.#'.

    + +

    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 activemq-users.xml. + + +

    Example step-by-step

    +

    To run the example, simply type mvn verify -Pexample from this directory

    + +
      +
    1. 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 client-jndi.properties file in the directory ../common/config
    2. +
      +           
      +           InitialContext initialContext = getContext(0);
      +           
      +        
      + +
    3. We perform lookup on the topics
    4. +
      +           
      +           Topic genericTopic = (Topic) initialContext.lookup("/topic/genericTopic");
      +           Topic europeTopic = (Topic) initialContext.lookup("/topic/europeTopic");
      +           Topic usTopic = (Topic) initialContext.lookup("/topic/usTopic");
      +           
      +        
      + +
    5. We perform a lookup on the Connection Factory
    6. +
      +           
      +           ConnectionFactory cf = (ConnectionFactory) initialContext.lookup("/ConnectionFactory");
      +           
      +        
      + +
    7. We try to create a JMS Connection without user/password. It will fail.
    8. +
      +           
      +           try
      +           {
      +              cf.createConnection();
      +              result = false;
      +           }
      +           catch (JMSSecurityException e)
      +           {
      +              System.out.println("Default user cannot get a connection. Details: " + e.getMessage());
      +           }
      +           
      +        
      + +
    9. Bill tries to make a connection using wrong password
    10. +
      +           
      +           billConnection = null;
      +           try
      +           {
      +              billConnection = createConnection("bill", "activemq1", cf);
      +              result = false;
      +           }
      +           catch (JMSException e)
      +           {
      +              System.out.println("User bill failed to connect. Details: " + e.getMessage());
      +           }
      +           
      +        
      + +
    11. Bill makes a good connection.
    12. +
      +          
      +           billConnection = createConnection("bill", "activemq", cf);
      +           billConnection.start();
      +          
      +       
      + +
    13. Andrew makes a good connection
    14. +
      +           
      +           andrewConnection = createConnection("andrew", "activemq1", cf);
      +           andrewConnection.start();
      +           
      +         
      + +
    15. Frank makes a good connection
    16. +
      +           
      +           frankConnection = createConnection("frank", "activemq2", cf);
      +           frankConnection.start();
      +           
      +        
      + +
    17. Sam makes a good connection
    18. +
      +           
      +           samConnection = createConnection("sam", "activemq3", cf);
      +           samConnection.start();
      +           
      +        
      + +
    19. We check every user can publish/subscribe genericTopics
    20. +
      +           
      +           checkUserSendAndReceive(genericTopic, billConnection, "bill");
      +           checkUserSendAndReceive(genericTopic, andrewConnection, "andrew");
      +           checkUserSendAndReceive(genericTopic, frankConnection, "frank");
      +           checkUserSendAndReceive(genericTopic, samConnection, "sam");
      +           
      +        
      + +
    21. We check permissions on news.europe.europeTopic for bill: can't send and can't receive
    22. +
      +           
      +           checkUserNoSendNoReceive(europeTopic, billConnection, "bill", andrewConnection, frankConnection);
      +           
      +        
      + +
    23. We check permissions on news.europe.europeTopic for andrew: can send but can't receive
    24. +
      +           
      +           checkUserSendNoReceive(europeTopic, andrewConnection, "andrew", frankConnection);
      +           
      +        
      + +
    25. We check permissions on news.europe.europeTopic for frank: can't send but can receive
    26. +
      +           
      +           checkUserReceiveNoSend(europeTopic, frankConnection, "frank", andrewConnection);
      +           
      +        
      + +
    27. We check permissions on news.europe.europeTopic for sam: can't send but can receive
    28. +
      +           
      +           checkUserReceiveNoSend(europeTopic, samConnection, "sam", andrewConnection);
      +           
      +        
      + +
    29. We check permissions on news.us.usTopic for bill: can't send and can't receive
    30. +
      +           
      +           checkUserNoSendNoReceive(usTopic, billConnection, "bill");
      +           
      +        
      + +
    31. We check permissions on news.us.usTopic for andrew: can't send and can't receive
    32. +
      +           
      +           checkUserNoSendNoReceive(usTopic, andrewConnection, "andrew");
      +           
      +        
      + +
    33. We check permissions on news.us.usTopic for frank: can both send and receive
    34. +
      +           
      +           checkUserSendAndReceive(usTopic, frankConnection, "frank");
      +           
      +        
      + +
    35. We check permissions on news.us.usTopic for sam: can't send but can receive
    36. +
      +           
      +           checkUserReceiveNoSend(usTopic, samConnection, "sam", frankConnection);
      +           
      +        
      + +
    37. And finally, always remember to close your JMS connections and resources after use, in a finally block. Closing a JMS connection will automatically close all of its sessions, consumers, producer and browser objects
    38. + +
      +           
      +           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();
      +              }
      +           }
      +           
      +        
      +
    + + diff --git a/examples/features/standard/security-jaas/src/main/java/org/apache/activemq/artemis/jms/example/JaasSecurityExample.java b/examples/features/standard/security-jaas/src/main/java/org/apache/activemq/artemis/jms/example/JaasSecurityExample.java new file mode 100644 index 0000000000..eae3fdde17 --- /dev/null +++ b/examples/features/standard/security-jaas/src/main/java/org/apache/activemq/artemis/jms/example/JaasSecurityExample.java @@ -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); + } +} diff --git a/examples/features/standard/security-jaas/src/main/resources/activemq/server0/artemis-roles.properties b/examples/features/standard/security-jaas/src/main/resources/activemq/server0/artemis-roles.properties new file mode 100644 index 0000000000..243b341c71 --- /dev/null +++ b/examples/features/standard/security-jaas/src/main/resources/activemq/server0/artemis-roles.properties @@ -0,0 +1,20 @@ +## --------------------------------------------------------------------------- +## Licensed to the Apache Software Foundation (ASF) under one or more +## contributor license agreements. See the NOTICE file distributed with +## this work for additional information regarding copyright ownership. +## The ASF licenses this file to You under the Apache License, Version 2.0 +## (the "License"); you may not use this file except in compliance with +## the License. You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## --------------------------------------------------------------------------- +user=bill,andrew,frank,sam +europe-user=andrew +us-user=frank +news-user=frank,sam \ No newline at end of file diff --git a/examples/features/standard/security-jaas/src/main/resources/activemq/server0/artemis-users.properties b/examples/features/standard/security-jaas/src/main/resources/activemq/server0/artemis-users.properties new file mode 100644 index 0000000000..0a206c6053 --- /dev/null +++ b/examples/features/standard/security-jaas/src/main/resources/activemq/server0/artemis-users.properties @@ -0,0 +1,20 @@ +## --------------------------------------------------------------------------- +## Licensed to the Apache Software Foundation (ASF) under one or more +## contributor license agreements. See the NOTICE file distributed with +## this work for additional information regarding copyright ownership. +## The ASF licenses this file to You under the Apache License, Version 2.0 +## (the "License"); you may not use this file except in compliance with +## the License. You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## --------------------------------------------------------------------------- +bill=activemq +andrew=activemq1 +frank=activemq2 +sam=activemq3 \ No newline at end of file diff --git a/examples/features/standard/security-jaas/src/main/resources/activemq/server0/broker.xml b/examples/features/standard/security-jaas/src/main/resources/activemq/server0/broker.xml new file mode 100644 index 0000000000..e2dc187813 --- /dev/null +++ b/examples/features/standard/security-jaas/src/main/resources/activemq/server0/broker.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + ./data/messaging/bindings + + ./data/messaging/journal + + ./data/messaging/largemessages + + ./data/messaging/paging + + + + tcp://localhost:61616 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/features/standard/security-jaas/src/main/resources/jndi.properties b/examples/features/standard/security-jaas/src/main/resources/jndi.properties new file mode 100644 index 0000000000..0a3b640261 --- /dev/null +++ b/examples/features/standard/security-jaas/src/main/resources/jndi.properties @@ -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 diff --git a/pom.xml b/pom.xml index def5ea2b94..f1fd06862a 100644 --- a/pom.xml +++ b/pom.xml @@ -123,6 +123,8 @@ 1.0-alpha-2 javac-with-errorprone + + 1.5.7 diff --git a/tests/integration-tests/pom.xml b/tests/integration-tests/pom.xml index de8ca770f9..bb4ec15667 100644 --- a/tests/integration-tests/pom.xml +++ b/tests/integration-tests/pom.xml @@ -261,6 +261,24 @@ 1.0.1 test + + org.apache.directory.server + apacheds-server-integ + ${directory-version} + test + + + org.apache.directory.server + apacheds-core-integ + ${directory-version} + test + + + bouncycastle + bcprov-jdk15 + + + diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/LDAPSecurityTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/LDAPSecurityTest.java new file mode 100644 index 0000000000..f5aeec560a --- /dev/null +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/LDAPSecurityTest.java @@ -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 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 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); + } +} diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/SecurityTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/SecurityTest.java index 1eb0ed877a..269c3db6da 100644 --- a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/SecurityTest.java +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/SecurityTest.java @@ -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 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 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 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(); diff --git a/tests/integration-tests/src/test/resources/login.config b/tests/integration-tests/src/test/resources/login.config new file mode 100644 index 0000000000..9b1e1c003b --- /dev/null +++ b/tests/integration-tests/src/test/resources/login.config @@ -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 + ; +}; diff --git a/tests/integration-tests/src/test/resources/roles.properties b/tests/integration-tests/src/test/resources/roles.properties new file mode 100644 index 0000000000..de332d395d --- /dev/null +++ b/tests/integration-tests/src/test/resources/roles.properties @@ -0,0 +1,20 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +programmers=first +accounting=second +employees=first,second diff --git a/tests/integration-tests/src/test/resources/test.ldif b/tests/integration-tests/src/test/resources/test.ldif new file mode 100644 index 0000000000..6d6bd588ce --- /dev/null +++ b/tests/integration-tests/src/test/resources/test.ldif @@ -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 \ No newline at end of file diff --git a/tests/integration-tests/src/test/resources/users.properties b/tests/integration-tests/src/test/resources/users.properties new file mode 100644 index 0000000000..1087b0b3f1 --- /dev/null +++ b/tests/integration-tests/src/test/resources/users.properties @@ -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