AMQ-7230 - Add support for regex based certificate authentication

This commit is contained in:
Lionel Cons 2019-06-17 13:39:13 +02:00
parent dc56fa3f6e
commit c4927638da
4 changed files with 80 additions and 8 deletions

View File

@ -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.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -33,6 +35,7 @@ public class ReloadableProperties {
private Properties props = new Properties();
private Map<String, String> invertedProps;
private Map<String, Set<String>> invertedValueProps;
private Map<String, Pattern> regexpProps;
private long reloadTime = -1;
private final PropertiesLoader.FileNameKey key;
@ -51,6 +54,7 @@ public class ReloadableProperties {
load(key.file(), props);
invertedProps = null;
invertedValueProps = null;
regexpProps = null;
if (key.isDebug()) {
LOG.debug("Load of: " + key);
}
@ -69,7 +73,10 @@ public class ReloadableProperties {
if (invertedProps == null) {
invertedProps = new HashMap<>(props.size());
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;
@ -93,6 +100,24 @@ public class ReloadableProperties {
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) {
LOG.warn("Ignoring invalid regexp: " + str);
}
}
}
}
return regexpProps;
}
private void load(final File source, Properties props) throws IOException {
FileInputStream in = new FileInputStream(source);
try {
@ -116,4 +141,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) == '/';
}
}

View File

@ -24,6 +24,7 @@ import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Pattern;
import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
@ -32,13 +33,14 @@ import javax.security.auth.login.LoginException;
/**
* A LoginModule allowing for SSL certificate based authentication based on
* Distinguished Names (DN) stored in text files. The DNs are parsed using a
* Properties class where each line is <user_name>=<user_DN>. This class also
* uses a group definition file where each line is <group_name>=<user_name_1>,<user_name_2>,etc.
* Properties class where each line is either <UserName>=<StringifiedSubjectDN>
* or <UserName>=/<SubjectDNRegExp>/. This class also uses a group definition
* file where each line is <GroupName>=<UserName1>,<UserName2>,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).
* org.apache.activemq.jaas.textfiledn.group 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).
*
* @author sepandm@gmail.com (Sepand)
*/
@ -48,6 +50,7 @@ public class TextFileCertificateLoginModule extends CertificateLoginModule {
private static final String GROUP_FILE_PROP_NAME = "org.apache.activemq.jaas.textfiledn.group";
private Map<String, Set<String>> groupsByUser;
private Map<String, Pattern> regexpByUser;
private Map<String, String> usersByDn;
/**
@ -58,6 +61,7 @@ public class TextFileCertificateLoginModule extends CertificateLoginModule {
super.initialize(subject, callbackHandler, sharedState, options);
usersByDn = load(USER_FILE_PROP_NAME, "", options).invertedPropertiesMap();
regexpByUser = load(USER_FILE_PROP_NAME, "", options).regexpPropertiesMap();
groupsByUser = load(GROUP_FILE_PROP_NAME, "", options).invertedPropertiesValuesMap();
}
@ -76,8 +80,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);
}
/**
@ -96,4 +100,17 @@ public class TextFileCertificateLoginModule extends CertificateLoginModule {
}
return userGroups;
}
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

@ -38,6 +38,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-groups.properties";
private static final Logger LOG = LoggerFactory.getLogger(TextFileCertificateLoginModuleTest.class);
@ -76,6 +77,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<String, String>();

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.
## ---------------------------------------------------------------------------
CNODD=/DN=TEST_USER_\\d*[13579]/
CNEVEN=/DN=TEST_USER_\\d*[02468]/