ARTEMIS-1740: Add support for regex based certificate authentication
This commit is contained in:
parent
64f07518a7
commit
1e81361a88
|
@ -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) == '/';
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<>();
|
||||||
|
|
|
@ -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]/
|
|
@ -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,
|
||||||
|
|
|
@ -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<>();
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue