ARTEMIS-1740: Add support for regex based certificate authentication

This commit is contained in:
Lionel Cons 2018-04-11 08:59:24 +02:00 committed by Clebert Suconic
parent 64f07518a7
commit 1e81361a88
8 changed files with 118 additions and 9 deletions

View File

@ -24,6 +24,8 @@ import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Properties; import java.util.Properties;
import java.util.Set; import java.util.Set;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; import org.apache.activemq.artemis.core.server.ActiveMQServerLogger;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
@ -35,6 +37,7 @@ public class ReloadableProperties {
private Properties props = new Properties(); private Properties props = new Properties();
private Map<String, String> invertedProps; private Map<String, String> invertedProps;
private Map<String, Set<String>> invertedValueProps; private Map<String, Set<String>> invertedValueProps;
private Map<String, Pattern> regexpProps;
private long reloadTime = -1; private long reloadTime = -1;
private final PropertiesLoader.FileNameKey key; private final PropertiesLoader.FileNameKey key;
@ -53,6 +56,7 @@ public class ReloadableProperties {
load(key.file(), props); load(key.file(), props);
invertedProps = null; invertedProps = null;
invertedValueProps = null; invertedValueProps = null;
regexpProps = null;
if (key.isDebug()) { if (key.isDebug()) {
logger.debug("Load of: " + key); logger.debug("Load of: " + key);
} }
@ -71,7 +75,10 @@ public class ReloadableProperties {
if (invertedProps == null) { if (invertedProps == null) {
invertedProps = new HashMap<>(props.size()); invertedProps = new HashMap<>(props.size());
for (Map.Entry<Object, Object> val : props.entrySet()) { for (Map.Entry<Object, Object> 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; return invertedProps;
@ -95,6 +102,24 @@ public class ReloadableProperties {
return invertedValueProps; return invertedValueProps;
} }
public synchronized Map<String, Pattern> regexpPropertiesMap() {
if (regexpProps == null) {
regexpProps = new HashMap<>(props.size());
for (Map.Entry<Object, Object> 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 { private void load(final File source, Properties props) throws IOException {
try (FileInputStream in = new FileInputStream(source)) { try (FileInputStream in = new FileInputStream(source)) {
props.load(in); props.load(in);
@ -115,4 +140,9 @@ public class ReloadableProperties {
return key.file.lastModified() > reloadTime; return key.file.lastModified() > reloadTime;
} }
private boolean looksLikeRegexp(String str) {
int len = str.length();
return len > 2 && str.charAt(0) == '/' && str.charAt(len - 1) == '/';
}
} }

View File

@ -23,6 +23,7 @@ import javax.security.cert.X509Certificate;
import java.util.Collections; import java.util.Collections;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.regex.Pattern;
/** /**
* A LoginModule allowing for SSL certificate based authentication based on * 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 static final String ROLE_FILE_PROP_NAME = "org.apache.activemq.jaas.textfiledn.role";
private Map<String, Set<String>> rolesByUser; private Map<String, Set<String>> rolesByUser;
private Map<String, Pattern> regexpByUser;
private Map<String, String> usersByDn; private Map<String, String> usersByDn;
/** /**
@ -53,6 +55,7 @@ public class TextFileCertificateLoginModule extends CertificateLoginModule {
Map<String, ?> options) { Map<String, ?> options) {
super.initialize(subject, callbackHandler, sharedState, options); super.initialize(subject, callbackHandler, sharedState, options);
usersByDn = load(USER_FILE_PROP_NAME, "", options).invertedPropertiesMap(); usersByDn = load(USER_FILE_PROP_NAME, "", options).invertedPropertiesMap();
regexpByUser = load(USER_FILE_PROP_NAME, "", options).regexpPropertiesMap();
rolesByUser = load(ROLE_FILE_PROP_NAME, "", options).invertedPropertiesValuesMap(); rolesByUser = load(ROLE_FILE_PROP_NAME, "", options).invertedPropertiesValuesMap();
} }
@ -71,8 +74,8 @@ public class TextFileCertificateLoginModule extends CertificateLoginModule {
if (certs == null) { if (certs == null) {
throw new LoginException("Client certificates not found. Cannot authenticate."); throw new LoginException("Client certificates not found. Cannot authenticate.");
} }
String dn = getDistinguishedName(certs);
return usersByDn.get(getDistinguishedName(certs)); return usersByDn.containsKey(dn) ? usersByDn.get(dn) : getUserByRegexp(dn);
} }
/** /**
@ -92,4 +95,17 @@ public class TextFileCertificateLoginModule extends CertificateLoginModule {
return userRoles; return userRoles;
} }
private synchronized String getUserByRegexp(String dn) {
String name = null;
for (Map.Entry<String, Pattern> val : regexpByUser.entrySet()) {
if (val.getValue().matcher(dn).matches()) {
name = val.getKey();
break;
}
}
usersByDn.put(dn, name);
return name;
}
} }

View File

@ -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_SMALL = "cert-users-SMALL.properties";
private static final String CERT_USERS_FILE_LARGE = "cert-users-LARGE.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 String CERT_GROUPS_FILE = "cert-roles.properties";
private static final int NUMBER_SUBJECTS = 10; private static final int NUMBER_SUBJECTS = 10;
@ -88,6 +89,11 @@ public class TextFileCertificateLoginModuleTest {
loginTest(CERT_USERS_FILE_LARGE, CERT_GROUPS_FILE); 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 { private void loginTest(String usersFiles, String groupsFile) throws LoginException {
HashMap<String, String> options = new HashMap<>(); HashMap<String, String> options = new HashMap<>();

View File

@ -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]/

View File

@ -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` - `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, 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 `UserName=StringifiedSubjectDN` or `UserName=/SubjectDNRegExp/`. For example, to define the users, `system`, `user` and
the following: `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 system=CN=system,O=Progress,C=US
user=CN=humble user,O=Progress,C=US user=CN=humble user,O=Progress,C=US
guest=CN=anon,O=Progress,C=DE 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 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, example, the system username is mapped to the `CN=system,O=Progress,C=US` subject DN. When performing authentication,

View File

@ -150,16 +150,26 @@ public class SecurityTest extends ActiveMQTestBase {
@Test @Test
public void testJAASSecurityManagerAuthenticationWithCerts() throws Exception { public void testJAASSecurityManagerAuthenticationWithCerts() throws Exception {
testJAASSecurityManagerAuthenticationWithCerts(TransportConstants.NEED_CLIENT_AUTH_PROP_NAME); testJAASSecurityManagerAuthenticationWithCerts("CertLogin", TransportConstants.NEED_CLIENT_AUTH_PROP_NAME);
} }
@Test @Test
public void testJAASSecurityManagerAuthenticationWithCertsWantClientAuth() throws Exception { 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 { @Test
ActiveMQJAASSecurityManager securityManager = new ActiveMQJAASSecurityManager("CertLogin"); 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)); ActiveMQServer server = addServer(ActiveMQServers.newActiveMQServer(createDefaultInVMConfig().setSecurityEnabled(true), ManagementFactory.getPlatformMBeanServer(), securityManager, false));
Map<String, Object> params = new HashMap<>(); Map<String, Object> params = new HashMap<>();

View File

@ -0,0 +1,19 @@
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
first=/CN=ActiveMQ Artemis Client, OU=Artemis, O=ActiveMQ(, [A-Z]+=AMQ)+/
second=O=Internet Widgits Pty Ltd, C=AU, ST=Some-State, CN=lakalkalaoioislkxn

View File

@ -124,6 +124,13 @@ CertLogin {
org.apache.activemq.jaas.textfiledn.role="cert-roles.properties"; 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 { DualAuthenticationCertLogin {
org.apache.activemq.artemis.spi.core.security.jaas.TextFileCertificateLoginModule required org.apache.activemq.artemis.spi.core.security.jaas.TextFileCertificateLoginModule required
debug=true debug=true