From 1e81361a88a2dd333d7f5c7f926dde515e5b7ac5 Mon Sep 17 00:00:00 2001 From: Lionel Cons Date: Wed, 11 Apr 2018 08:59:24 +0200 Subject: [PATCH] ARTEMIS-1740: Add support for regex based certificate authentication --- .../security/jaas/ReloadableProperties.java | 32 ++++++++++++++++++- .../jaas/TextFileCertificateLoginModule.java | 20 ++++++++++-- .../TextFileCertificateLoginModuleTest.java | 6 ++++ .../resources/cert-users-REGEXP.properties | 18 +++++++++++ docs/user-manual/en/security.md | 7 ++-- .../integration/security/SecurityTest.java | 18 ++++++++--- .../test/resources/cert-regexps.properties | 19 +++++++++++ .../src/test/resources/login.config | 7 ++++ 8 files changed, 118 insertions(+), 9 deletions(-) create mode 100644 artemis-server/src/test/resources/cert-users-REGEXP.properties create mode 100644 tests/integration-tests/src/test/resources/cert-regexps.properties diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/ReloadableProperties.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/ReloadableProperties.java index bed7d5f278..947c6d164b 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/ReloadableProperties.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/ReloadableProperties.java @@ -24,6 +24,8 @@ import java.util.HashSet; import java.util.Map; import java.util.Properties; import java.util.Set; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; import org.jboss.logging.Logger; @@ -35,6 +37,7 @@ public class ReloadableProperties { private Properties props = new Properties(); private Map invertedProps; private Map> invertedValueProps; + private Map regexpProps; private long reloadTime = -1; private final PropertiesLoader.FileNameKey key; @@ -53,6 +56,7 @@ public class ReloadableProperties { load(key.file(), props); invertedProps = null; invertedValueProps = null; + regexpProps = null; if (key.isDebug()) { logger.debug("Load of: " + key); } @@ -71,7 +75,10 @@ public class ReloadableProperties { if (invertedProps == null) { invertedProps = new HashMap<>(props.size()); for (Map.Entry val : props.entrySet()) { - invertedProps.put((String) val.getValue(), (String) val.getKey()); + String str = (String) val.getValue(); + if (!looksLikeRegexp(str)) { + invertedProps.put(str, (String) val.getKey()); + } } } return invertedProps; @@ -95,6 +102,24 @@ public class ReloadableProperties { return invertedValueProps; } + public synchronized Map regexpPropertiesMap() { + if (regexpProps == null) { + regexpProps = new HashMap<>(props.size()); + for (Map.Entry val : props.entrySet()) { + String str = (String) val.getValue(); + if (looksLikeRegexp(str)) { + try { + Pattern p = Pattern.compile(str.substring(1, str.length() - 1)); + regexpProps.put((String) val.getKey(), p); + } catch (PatternSyntaxException e) { + ActiveMQServerLogger.LOGGER.warn("Ignoring invalid regexp: " + str); + } + } + } + } + return regexpProps; + } + private void load(final File source, Properties props) throws IOException { try (FileInputStream in = new FileInputStream(source)) { props.load(in); @@ -115,4 +140,9 @@ public class ReloadableProperties { return key.file.lastModified() > reloadTime; } + private boolean looksLikeRegexp(String str) { + int len = str.length(); + return len > 2 && str.charAt(0) == '/' && str.charAt(len - 1) == '/'; + } + } 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 index ca103cd156..2b0f45cd45 100644 --- 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 @@ -23,6 +23,7 @@ import javax.security.cert.X509Certificate; import java.util.Collections; import java.util.Map; import java.util.Set; +import java.util.regex.Pattern; /** * A LoginModule allowing for SSL certificate based authentication based on @@ -41,6 +42,7 @@ public class TextFileCertificateLoginModule extends CertificateLoginModule { private static final String ROLE_FILE_PROP_NAME = "org.apache.activemq.jaas.textfiledn.role"; private Map> rolesByUser; + private Map regexpByUser; private Map usersByDn; /** @@ -53,6 +55,7 @@ public class TextFileCertificateLoginModule extends CertificateLoginModule { Map options) { super.initialize(subject, callbackHandler, sharedState, options); usersByDn = load(USER_FILE_PROP_NAME, "", options).invertedPropertiesMap(); + regexpByUser = load(USER_FILE_PROP_NAME, "", options).regexpPropertiesMap(); rolesByUser = load(ROLE_FILE_PROP_NAME, "", options).invertedPropertiesValuesMap(); } @@ -71,8 +74,8 @@ public class TextFileCertificateLoginModule extends CertificateLoginModule { if (certs == null) { throw new LoginException("Client certificates not found. Cannot authenticate."); } - - return usersByDn.get(getDistinguishedName(certs)); + String dn = getDistinguishedName(certs); + return usersByDn.containsKey(dn) ? usersByDn.get(dn) : getUserByRegexp(dn); } /** @@ -92,4 +95,17 @@ public class TextFileCertificateLoginModule extends CertificateLoginModule { return userRoles; } + + private synchronized String getUserByRegexp(String dn) { + String name = null; + for (Map.Entry val : regexpByUser.entrySet()) { + if (val.getValue().matcher(dn).matches()) { + name = val.getKey(); + break; + } + } + usersByDn.put(dn, name); + return name; + } + } diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/TextFileCertificateLoginModuleTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/TextFileCertificateLoginModuleTest.java index 2eebd06989..957dace03e 100644 --- a/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/TextFileCertificateLoginModuleTest.java +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/TextFileCertificateLoginModuleTest.java @@ -46,6 +46,7 @@ public class TextFileCertificateLoginModuleTest { private static final String CERT_USERS_FILE_SMALL = "cert-users-SMALL.properties"; private static final String CERT_USERS_FILE_LARGE = "cert-users-LARGE.properties"; + private static final String CERT_USERS_FILE_REGEXP = "cert-users-REGEXP.properties"; private static final String CERT_GROUPS_FILE = "cert-roles.properties"; private static final int NUMBER_SUBJECTS = 10; @@ -88,6 +89,11 @@ public class TextFileCertificateLoginModuleTest { loginTest(CERT_USERS_FILE_LARGE, CERT_GROUPS_FILE); } + @Test + public void testLoginWithREGEXPUsersFile() throws Exception { + loginTest(CERT_USERS_FILE_REGEXP, CERT_GROUPS_FILE); + } + private void loginTest(String usersFiles, String groupsFile) throws LoginException { HashMap options = new HashMap<>(); diff --git a/artemis-server/src/test/resources/cert-users-REGEXP.properties b/artemis-server/src/test/resources/cert-users-REGEXP.properties new file mode 100644 index 0000000000..22e67a69af --- /dev/null +++ b/artemis-server/src/test/resources/cert-users-REGEXP.properties @@ -0,0 +1,18 @@ +## --------------------------------------------------------------------------- +## 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. +## --------------------------------------------------------------------------- +CNODD=/DN=TEST_USER_\\d*[13579]/ +CNEVEN=/DN=TEST_USER_\\d*[02468]/ diff --git a/docs/user-manual/en/security.md b/docs/user-manual/en/security.md index ac0c76f998..2f70701493 100644 --- a/docs/user-manual/en/security.md +++ b/docs/user-manual/en/security.md @@ -615,12 +615,15 @@ login module. The options supported by this login module are as follows: - `reload` - boolean flag; whether or not to reload the properties files when a modification occurs; default is `false` In the context of the certificate login module, the `users.properties` file consists of a list of properties of the form, -`UserName=StringifiedSubjectDN`. For example, to define the users, system, user, and guest, you could create a file like -the following: +`UserName=StringifiedSubjectDN` or `UserName=/SubjectDNRegExp/`. For example, to define the users, `system`, `user` and +`guest` as well as a `hosts` user matching several DNs, you could create a file like the following: system=CN=system,O=Progress,C=US user=CN=humble user,O=Progress,C=US guest=CN=anon,O=Progress,C=DE + hosts=/CN=host\\d+\\.acme\\.com,O=Acme,C=UK/ + +Note that the backslash character has to be escaped because it has a special treatment in properties files. Each username is mapped to a subject DN, encoded as a string (where the string encoding is specified by RFC 2253). For example, the system username is mapped to the `CN=system,O=Progress,C=US` subject DN. When performing authentication, 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 2bced4783f..21b4d08298 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 @@ -150,16 +150,26 @@ public class SecurityTest extends ActiveMQTestBase { @Test public void testJAASSecurityManagerAuthenticationWithCerts() throws Exception { - testJAASSecurityManagerAuthenticationWithCerts(TransportConstants.NEED_CLIENT_AUTH_PROP_NAME); + testJAASSecurityManagerAuthenticationWithCerts("CertLogin", TransportConstants.NEED_CLIENT_AUTH_PROP_NAME); } @Test public void testJAASSecurityManagerAuthenticationWithCertsWantClientAuth() throws Exception { - testJAASSecurityManagerAuthenticationWithCerts(TransportConstants.WANT_CLIENT_AUTH_PROP_NAME); + testJAASSecurityManagerAuthenticationWithCerts("CertLogin", TransportConstants.WANT_CLIENT_AUTH_PROP_NAME); } - protected void testJAASSecurityManagerAuthenticationWithCerts(String clientAuthPropName) throws Exception { - ActiveMQJAASSecurityManager securityManager = new ActiveMQJAASSecurityManager("CertLogin"); + @Test + public void testJAASSecurityManagerAuthenticationWithRegexps() throws Exception { + testJAASSecurityManagerAuthenticationWithCerts("CertLoginWithRegexp", TransportConstants.NEED_CLIENT_AUTH_PROP_NAME); + } + + @Test + public void testJAASSecurityManagerAuthenticationWithRegexpsWantClientAuth() throws Exception { + testJAASSecurityManagerAuthenticationWithCerts("CertLoginWithRegexp", TransportConstants.WANT_CLIENT_AUTH_PROP_NAME); + } + + protected void testJAASSecurityManagerAuthenticationWithCerts(String secManager, String clientAuthPropName) throws Exception { + ActiveMQJAASSecurityManager securityManager = new ActiveMQJAASSecurityManager(secManager); ActiveMQServer server = addServer(ActiveMQServers.newActiveMQServer(createDefaultInVMConfig().setSecurityEnabled(true), ManagementFactory.getPlatformMBeanServer(), securityManager, false)); Map params = new HashMap<>(); diff --git a/tests/integration-tests/src/test/resources/cert-regexps.properties b/tests/integration-tests/src/test/resources/cert-regexps.properties new file mode 100644 index 0000000000..9677bd81ae --- /dev/null +++ b/tests/integration-tests/src/test/resources/cert-regexps.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=/CN=ActiveMQ Artemis Client, OU=Artemis, O=ActiveMQ(, [A-Z]+=AMQ)+/ +second=O=Internet Widgits Pty Ltd, C=AU, ST=Some-State, CN=lakalkalaoioislkxn diff --git a/tests/integration-tests/src/test/resources/login.config b/tests/integration-tests/src/test/resources/login.config index a5e40c159a..19d684e39b 100644 --- a/tests/integration-tests/src/test/resources/login.config +++ b/tests/integration-tests/src/test/resources/login.config @@ -124,6 +124,13 @@ CertLogin { org.apache.activemq.jaas.textfiledn.role="cert-roles.properties"; }; +CertLoginWithRegexp { + org.apache.activemq.artemis.spi.core.security.jaas.TextFileCertificateLoginModule required + debug=true + org.apache.activemq.jaas.textfiledn.user="cert-regexps.properties" + org.apache.activemq.jaas.textfiledn.role="cert-roles.properties"; +}; + DualAuthenticationCertLogin { org.apache.activemq.artemis.spi.core.security.jaas.TextFileCertificateLoginModule required debug=true