https://issues.apache.org/jira/browse/AMQ-5876 - refactor properties loading such that it can be reused by cert and props login modules. Both loading on start and refreshing if reload=true and lastMod indicates change

This commit is contained in:
gtully 2015-07-08 12:06:30 +01:00
parent 6f457d2f5c
commit 59cd018979
11 changed files with 200494 additions and 196 deletions

View File

@ -43,7 +43,7 @@ import org.slf4j.LoggerFactory;
*
* @author sepandm@gmail.com (Sepand)
*/
public abstract class CertificateLoginModule implements LoginModule {
public abstract class CertificateLoginModule extends PropertiesLoader implements LoginModule {
private static final Logger LOG = LoggerFactory.getLogger(CertificateLoginModule.class);
@ -54,7 +54,6 @@ public abstract class CertificateLoginModule implements LoginModule {
private String username;
private Set<String> groups;
private Set<Principal> principals = new HashSet<Principal>();
private boolean debug;
/**
* Overriding to allow for proper initialization. Standard JAAS.
@ -63,12 +62,7 @@ public abstract class CertificateLoginModule implements LoginModule {
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) {
LOG.debug("Initialized debug");
}
init(options);
}
/**

View File

@ -1,71 +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.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.slf4j.Logger;
class PrincipalProperties {
private final Properties principals;
private final long reloadTime;
PrincipalProperties(final String type, final File source, final Logger log) {
Properties props = new Properties();
long reloadTime = 0;
try {
load(source, props);
reloadTime = System.currentTimeMillis();
} catch (IOException ioe) {
log.warn("Unable to load " + type + " properties file " + source);
}
this.reloadTime = reloadTime;
this.principals = props;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
Set<Map.Entry<String, String>> 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;
}
}

View File

@ -0,0 +1,135 @@
/**
* 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.jaas;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PropertiesLoader {
private static final Logger LOG = LoggerFactory.getLogger(PropertiesLoader.class);
static Map<FileNameKey, ReloadableProperties> staticCache = new HashMap<FileNameKey, ReloadableProperties>();
protected boolean debug;
public void init(Map options) {
debug = booleanOption("debug", options);
if (debug) {
LOG.debug("Initialized debug");
}
}
public ReloadableProperties load(String nameProperty, String fallbackName, Map options) {
ReloadableProperties result;
FileNameKey key = new FileNameKey(nameProperty, fallbackName, options);
key.setDebug(debug);
synchronized (staticCache) {
result = staticCache.get(key);
if (result == null) {
result = new ReloadableProperties(key);
staticCache.put(key, result);
}
}
return result.obtained();
}
private static boolean booleanOption(String name, Map options) {
return Boolean.parseBoolean((String) options.get(name));
}
public class FileNameKey {
final File file;
final String absPath;
final boolean reload;
private boolean decrypt;
private boolean debug;
public FileNameKey(String nameProperty, String fallbackName, Map options) {
this.file = new File(baseDir(options), stringOption(nameProperty, fallbackName, options));
absPath = file.getAbsolutePath();
reload = booleanOption("reload", options);
decrypt = booleanOption("decrypt", options);
}
@Override
public boolean equals(Object other) {
return other instanceof FileNameKey && this.absPath.equals(((FileNameKey) other).absPath);
}
public int hashCode() {
return this.absPath.hashCode();
}
public boolean isReload() {
return reload;
}
public File file() {
return file;
}
public boolean isDecrypt() {
return decrypt;
}
public void setDecrypt(boolean decrypt) {
this.decrypt = decrypt;
}
private String stringOption(String key, String nameDefault, Map options) {
Object result = options.get(key);
return result != null ? result.toString() : nameDefault;
}
private File baseDir(Map options) {
File baseDir = null;
if (options.get("baseDir") != null) {
baseDir = new File((String) options.get("baseDir"));
} else {
if (System.getProperty("java.security.auth.login.config") != null) {
baseDir = new File(System.getProperty("java.security.auth.login.config")).getParentFile();
}
}
if (debug) {
LOG.debug("Using basedir=" + baseDir.getAbsolutePath());
}
return baseDir;
}
public String toString() {
return "PropsFile=" + absPath;
}
public void setDebug(boolean debug) {
this.debug = debug;
}
public boolean isDebug() {
return debug;
}
}
/**
* For test-usage only.
*/
public static void resetUsersAndGroupsCache() {
staticCache.clear();
}
}

View File

@ -16,11 +16,11 @@
*/
package org.apache.activemq.jaas;
import java.io.File;
import java.io.IOException;
import java.security.Principal;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import javax.security.auth.Subject;
@ -36,80 +36,30 @@ import javax.security.auth.spi.LoginModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PropertiesLoginModule implements LoginModule {
public class PropertiesLoginModule extends PropertiesLoader 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.group";
private static final String USER_FILE_PROP_NAME = "org.apache.activemq.jaas.properties.user";
private static final String GROUP_FILE_PROP_NAME = "org.apache.activemq.jaas.properties.group";
private static final Logger LOG = LoggerFactory.getLogger(PropertiesLoginModule.class);
private Subject subject;
private CallbackHandler callbackHandler;
private boolean debug;
private boolean reload = false;
private static volatile PrincipalProperties users;
private static volatile PrincipalProperties groups;
private Properties users;
private Properties groups;
private String user;
private final Set<Principal> principals = new HashSet<Principal>();
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 = (String) 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) {
LOG.debug("Reloading users from " + uf.getAbsolutePath());
}
users = new PrincipalProperties("user", uf, LOG);
if( decrypt ) {
try {
EncryptionSupport.decrypt(users.getPrincipals());
} catch(NoClassDefFoundError e) {
// this Happens whe jasypt is not on the classpath..
decrypt = false;
LOG.info("jasypt is not on the classpath: password decryption disabled.");
}
}
}
String groupsFile = (String) options.get(GROUP_FILE) + "";
File gf = baseDir != null ? new File(baseDir, groupsFile) : new File(groupsFile);
if (reload || groups == null || gf.lastModified() > groups.getReloadTime()) {
if (debug) {
LOG.debug("Reloading groups from " + gf.getAbsolutePath());
}
groups = new PrincipalProperties("group", gf, LOG);
}
}
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) {
LOG.debug("Using basedir=" + baseDir.getAbsolutePath());
}
}
}
init(options);
users = load(USER_FILE_PROP_NAME, "user", options).getProps();
groups = load(GROUP_FILE_PROP_NAME, "group", options).getProps();
}
@Override
@ -155,9 +105,9 @@ public class PropertiesLoginModule implements LoginModule {
if (result) {
principals.add(new UserPrincipal(user));
for (Map.Entry<String, String> entry : groups.entries()) {
String name = entry.getKey();
String[] userList = entry.getValue().split(",");
for (Map.Entry<Object, Object> entry : groups.entrySet()) {
String name = (String) entry.getKey();
String[] userList = ((String)entry.getValue()).split(",");
for (int i = 0; i < userList.length; i++) {
if (user.equals(userList[i])) {
principals.add(new GroupPrincipal(name));
@ -204,11 +154,4 @@ public class PropertiesLoginModule implements LoginModule {
loginSucceeded = false;
}
/**
* For test-usage only.
*/
static void resetUsersAndGroupsCache() {
users = null;
groups = null;
}
}

View File

@ -0,0 +1,97 @@
/**
* 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.jaas;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ReloadableProperties {
private static final Logger LOG = LoggerFactory.getLogger(ReloadableProperties.class);
private Properties props = new Properties();
private Map<String, String> invertedProps;
private long reloadTime = -1;
private final PropertiesLoader.FileNameKey key;
public ReloadableProperties(PropertiesLoader.FileNameKey key) {
this.key = key;
}
public synchronized Properties getProps() {
return props;
}
public synchronized ReloadableProperties obtained() {
if (reloadTime < 0 || (key.isReload() && hasModificationAfter(reloadTime))) {
props = new Properties();
try {
load(key.file(), props);
invertedProps = null;
if (key.isDebug()) {
LOG.debug("Load of: " + key);
}
} catch (IOException e) {
LOG.error("Failed to load: " + key + ", reason:" + e.getLocalizedMessage());
if (key.isDebug()) {
LOG.debug("Load of: " + key + ", failure exception" + e);
}
}
reloadTime = System.currentTimeMillis();
}
return this;
}
public synchronized Map<String, String> invertedPropertiesMap() {
if (invertedProps == null) {
invertedProps = new HashMap<>(props.size());
for (Map.Entry<Object, Object> val : props.entrySet()) {
invertedProps.put((String) val.getValue(), (String) val.getKey());
}
}
return invertedProps;
}
private void load(final File source, Properties props) throws IOException {
FileInputStream in = new FileInputStream(source);
try {
props.load(in);
if (key.isDecrypt()) {
try {
EncryptionSupport.decrypt(this.props);
} catch (NoClassDefFoundError e) {
// this Happens whe jasypt is not on the classpath..
key.setDecrypt(false);
LOG.info("jasypt is not on the classpath: password decryption disabled.");
}
}
} finally {
in.close();
}
}
private boolean hasModificationAfter(long reloadTime) {
return key.file.lastModified() > reloadTime;
}
}

View File

@ -17,8 +17,6 @@
package org.apache.activemq.jaas;
import java.io.File;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
import java.util.HashSet;
@ -45,12 +43,11 @@ import javax.security.auth.login.LoginException;
*/
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 static final String USER_FILE_PROP_NAME = "org.apache.activemq.jaas.textfiledn.user";
private static final String GROUP_FILE_PROP_NAME = "org.apache.activemq.jaas.textfiledn.group";
private File baseDir;
private String usersFilePathname;
private String groupsFilePathname;
private Properties groups;
private Map<String, String> usersByDn;
/**
* Performs initialization of file paths. A standard JAAS override.
@ -58,14 +55,9 @@ public class TextFileCertificateLoginModule extends CertificateLoginModule {
@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) + "";
usersByDn = load(USER_FILE_PROP_NAME, "", options).invertedPropertiesMap();
groups = load(GROUP_FILE_PROP_NAME, "", options).getProps();
}
/**
@ -84,28 +76,7 @@ public class TextFileCertificateLoginModule extends CertificateLoginModule {
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);
} catch (IOException ioe) {
throw new LoginException("Unable to load user properties file " + usersFile);
}
String dn = getDistinguishedName(certs);
Enumeration<Object> keys = users.keys();
for (Enumeration<Object> vals = users.elements(); vals.hasMoreElements();) {
if (((String)vals.nextElement()).equals(dn)) {
return (String)keys.nextElement();
} else {
keys.nextElement();
}
}
return null;
return usersByDn.get(getDistinguishedName(certs));
}
/**
@ -118,16 +89,6 @@ public class TextFileCertificateLoginModule extends CertificateLoginModule {
*/
@Override
protected Set<String> 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<String> userGroups = new HashSet<String>();
for (Enumeration<Object> enumeration = groups.keys(); enumeration.hasMoreElements();) {
String groupName = (String)enumeration.nextElement();

View File

@ -121,7 +121,7 @@ public class PropertiesLoginModuleRaceConditionTest {
public void after() throws InterruptedException {
pool.shutdown();
assertTrue(pool.awaitTermination(500, TimeUnit.SECONDS));
PropertiesLoginModule.resetUsersAndGroupsCache();
PropertiesLoader.resetUsersAndGroupsCache();
}
@Test

View File

@ -0,0 +1,129 @@
/**
* 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.security;
import java.net.URL;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import javax.management.remote.JMXPrincipal;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginException;
import org.apache.activemq.jaas.CertificateLoginModule;
import org.apache.activemq.jaas.JaasCertificateCallbackHandler;
import org.apache.activemq.jaas.PropertiesLoader;
import org.apache.activemq.jaas.TextFileCertificateLoginModule;
import org.apache.activemq.transport.tcp.StubX509Certificate;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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_GROUPS_FILE = "cert-groups.properties";
private static final Logger LOG = LoggerFactory.getLogger(TextFileCertificateLoginModuleTest.class);
private static final int NUMBER_SUBJECTS = 10;
static {
String path = System.getProperty("java.security.auth.login.config");
if (path == null) {
URL resource = TextFileCertificateLoginModuleTest.class.getClassLoader().getResource("login.config");
if (resource != null) {
path = resource.getFile();
System.setProperty("java.security.auth.login.config", path);
}
}
}
private CertificateLoginModule loginModule;
@Before
public void setUp() throws Exception {
loginModule = new TextFileCertificateLoginModule();
}
@After
public void tearDown() throws Exception {
PropertiesLoader.resetUsersAndGroupsCache();
}
@Test
public void testLoginWithSMALLUsersFile() throws Exception {
loginTest(CERT_USERS_FILE_SMALL, CERT_GROUPS_FILE);
}
@Test
public void testLoginWithLARGEUsersFile() throws Exception {
loginTest(CERT_USERS_FILE_LARGE, CERT_GROUPS_FILE);
}
private void loginTest(String usersFiles, String groupsFile) throws LoginException {
HashMap options = new HashMap<String, String>();
options.put("org.apache.activemq.jaas.textfiledn.user", usersFiles);
options.put("org.apache.activemq.jaas.textfiledn.group", groupsFile);
options.put("reload", "true");
JaasCertificateCallbackHandler[] callbackHandlers = new JaasCertificateCallbackHandler[NUMBER_SUBJECTS];
Subject[] subjects = new Subject[NUMBER_SUBJECTS];
for (int i = 0; i < callbackHandlers.length; i++) {
callbackHandlers[i] = getJaasCertificateCallbackHandler("DN=TEST_USER_" + (i + 1));
}
long startTime = System.currentTimeMillis();
for (int outer=0; outer<500;outer++) {
for (int i = 0; i < NUMBER_SUBJECTS; i++) {
Subject subject = doAuthenticate(options, callbackHandlers[i]);
subjects[i] = subject;
}
}
long endTime = System.currentTimeMillis();
long timeTaken = endTime - startTime;
for (int i = 0; i < NUMBER_SUBJECTS; i++) {
LOG.info("subject is: " + subjects[i].getPrincipals().toString());
}
LOG.info(usersFiles + ": Time taken is " + timeTaken);
}
private JaasCertificateCallbackHandler getJaasCertificateCallbackHandler(String user) {
JMXPrincipal principal = new JMXPrincipal(user);
X509Certificate cert = new StubX509Certificate(principal);
return new JaasCertificateCallbackHandler(new X509Certificate[]{cert});
}
private Subject doAuthenticate(HashMap options, JaasCertificateCallbackHandler callbackHandler) throws LoginException {
Subject mySubject = new Subject();
loginModule.initialize(mySubject, callbackHandler, null, options);
loginModule.login();
loginModule.commit();
return mySubject;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,100 @@
1CN=TEST0000001, OU=TEST, O=TEST TEST TEST1 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
2CN=TEST0000001, OU=TEST, O=TEST TEST TEST2 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
3CN=TEST0000001, OU=TEST, O=TEST TEST TEST3 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
4CN=TEST0000001, OU=TEST, O=TEST TEST TEST4 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
5CN=TEST0000001, OU=TEST, O=TEST TEST TEST5 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
6CN=TEST0000001, OU=TEST, O=TEST TEST TEST6 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
7CN=TEST0000001, OU=TEST, O=TEST TEST TEST7 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
8CN=TEST0000001, OU=TEST, O=TEST TEST TEST8 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
9CN=TEST0000001, OU=TEST, O=TEST TEST TEST9 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
10CN=TEST0000001, OU=TEST, O=TEST TEST TEST10 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
11CN=TEST0000001, OU=TEST, O=TEST TEST TEST11 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
12CN=TEST0000001, OU=TEST, O=TEST TEST TEST12 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
13CN=TEST0000001, OU=TEST, O=TEST TEST TEST13 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
14CN=TEST0000001, OU=TEST, O=TEST TEST TEST14 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
15CN=TEST0000001, OU=TEST, O=TEST TEST TEST15 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
16CN=TEST0000001, OU=TEST, O=TEST TEST TEST16 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
17CN=TEST0000001, OU=TEST, O=TEST TEST TEST17 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
18CN=TEST0000001, OU=TEST, O=TEST TEST TEST18 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
19CN=TEST0000001, OU=TEST, O=TEST TEST TEST19 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
20CN=TEST0000001, OU=TEST, O=TEST TEST TEST20 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
21CN=TEST0000001, OU=TEST, O=TEST TEST TEST21 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
22CN=TEST0000001, OU=TEST, O=TEST TEST TEST22 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
23CN=TEST0000001, OU=TEST, O=TEST TEST TEST23 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
24CN=TEST0000001, OU=TEST, O=TEST TEST TEST24 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
25CN=TEST0000001, OU=TEST, O=TEST TEST TEST25 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
26CN=TEST0000001, OU=TEST, O=TEST TEST TEST26 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
27CN=TEST0000001, OU=TEST, O=TEST TEST TEST27 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
28CN=TEST0000001, OU=TEST, O=TEST TEST TEST28 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
29CN=TEST0000001, OU=TEST, O=TEST TEST TEST29 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
30CN=TEST0000001, OU=TEST, O=TEST TEST TEST30 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
31CN=TEST0000001, OU=TEST, O=TEST TEST TEST31 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
32CN=TEST0000001, OU=TEST, O=TEST TEST TEST32 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
33CN=TEST0000001, OU=TEST, O=TEST TEST TEST33 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
34CN=TEST0000001, OU=TEST, O=TEST TEST TEST34 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
35CN=TEST0000001, OU=TEST, O=TEST TEST TEST35 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
36CN=TEST0000001, OU=TEST, O=TEST TEST TEST36 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
37CN=TEST0000001, OU=TEST, O=TEST TEST TEST37 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
38CN=TEST0000001, OU=TEST, O=TEST TEST TEST38 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
39CN=TEST0000001, OU=TEST, O=TEST TEST TEST39 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
40CN=TEST0000001, OU=TEST, O=TEST TEST TEST40 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
41CN=TEST0000001, OU=TEST, O=TEST TEST TEST41 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
42CN=TEST0000001, OU=TEST, O=TEST TEST TEST42 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
43CN=TEST0000001, OU=TEST, O=TEST TEST TEST43 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
44CN=TEST0000001, OU=TEST, O=TEST TEST TEST44 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
45CN=TEST0000001, OU=TEST, O=TEST TEST TEST45 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
46CN=TEST0000001, OU=TEST, O=TEST TEST TEST46 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
47CN=TEST0000001, OU=TEST, O=TEST TEST TEST47 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
48CN=TEST0000001, OU=TEST, O=TEST TEST TEST48 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
49CN=TEST0000001, OU=TEST, O=TEST TEST TEST49 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
50CN=TEST0000001, OU=TEST, O=TEST TEST TEST50 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
51CN=TEST0000001, OU=TEST, O=TEST TEST TEST51 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
52CN=TEST0000001, OU=TEST, O=TEST TEST TEST52 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
53CN=TEST0000001, OU=TEST, O=TEST TEST TEST53 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
54CN=TEST0000001, OU=TEST, O=TEST TEST TEST54 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
55CN=TEST0000001, OU=TEST, O=TEST TEST TEST55 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
56CN=TEST0000001, OU=TEST, O=TEST TEST TEST56 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
57CN=TEST0000001, OU=TEST, O=TEST TEST TEST57 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
58CN=TEST0000001, OU=TEST, O=TEST TEST TEST58 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
59CN=TEST0000001, OU=TEST, O=TEST TEST TEST59 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
60CN=TEST0000001, OU=TEST, O=TEST TEST TEST60 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
61CN=TEST0000001, OU=TEST, O=TEST TEST TEST61 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
62CN=TEST0000001, OU=TEST, O=TEST TEST TEST62 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
63CN=TEST0000001, OU=TEST, O=TEST TEST TEST63 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
64CN=TEST0000001, OU=TEST, O=TEST TEST TEST64 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
65CN=TEST0000001, OU=TEST, O=TEST TEST TEST65 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
66CN=TEST0000001, OU=TEST, O=TEST TEST TEST66 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
67CN=TEST0000001, OU=TEST, O=TEST TEST TEST67 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
68CN=TEST0000001, OU=TEST, O=TEST TEST TEST68 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
69CN=TEST0000001, OU=TEST, O=TEST TEST TEST69 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
70CN=TEST0000001, OU=TEST, O=TEST TEST TEST70 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
71CN=TEST0000001, OU=TEST, O=TEST TEST TEST71 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
72CN=TEST0000001, OU=TEST, O=TEST TEST TEST72 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
73CN=TEST0000001, OU=TEST, O=TEST TEST TEST73 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
74CN=TEST0000001, OU=TEST, O=TEST TEST TEST74 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
75CN=TEST0000001, OU=TEST, O=TEST TEST TEST75 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
76CN=TEST0000001, OU=TEST, O=TEST TEST TEST76 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
77CN=TEST0000001, OU=TEST, O=TEST TEST TEST77 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
78CN=TEST0000001, OU=TEST, O=TEST TEST TEST78 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
79CN=TEST0000001, OU=TEST, O=TEST TEST TEST79 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
80CN=TEST0000001, OU=TEST, O=TEST TEST TEST80 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
81CN=TEST0000001, OU=TEST, O=TEST TEST TEST81 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
82CN=TEST0000001, OU=TEST, O=TEST TEST TEST82 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
83CN=TEST0000001, OU=TEST, O=TEST TEST TEST83 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
84CN=TEST0000001, OU=TEST, O=TEST TEST TEST84 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
85CN=TEST0000001, OU=TEST, O=TEST TEST TEST85 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
86CN=TEST0000001, OU=TEST, O=TEST TEST TEST86 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
87CN=TEST0000001, OU=TEST, O=TEST TEST TEST87 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
88CN=TEST0000001, OU=TEST, O=TEST TEST TEST88 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
89CN=TEST0000001, OU=TEST, O=TEST TEST TEST89 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
90CN=TEST0000001, OU=TEST, O=TEST TEST TEST90 TEST TEST TEST TEST TEST TEST, L=TEST, ST=TEST, C=GB
10001CN=DN=TEST_USER_1
10002CN=DN=TEST_USER_2
10003CN=DN=TEST_USER_3
10004CN=DN=TEST_USER_4
10005CN=DN=TEST_USER_5
10006CN=DN=TEST_USER_6
10007CN=DN=TEST_USER_7
10008CN=DN=TEST_USER_8
10009CN=DN=TEST_USER_9
10010CN=DN=TEST_USER_10