/*
* Copyright 2010 - 2014 Glencoe Software, Inc. All rights reserved.
* Use is subject to license terms supplied in LICENSE.txt
*/
package ome.services.ldap;
import java.io.File;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.naming.NamingException;
import ome.api.local.LocalQuery;
import ome.conditions.ApiUsageException;
import ome.conditions.SecurityViolation;
import ome.conditions.ValidationException;
import ome.logic.LdapImpl;
import ome.model.meta.Experimenter;
import ome.security.auth.LdapConfig;
import ome.security.auth.LdapPasswordProvider;
import ome.security.auth.PasswordUtil;
import ome.security.auth.RoleProvider;
import ome.services.util.Executor;
import ome.system.EventContext;
import ome.system.OmeroContext;
import ome.system.Roles;
import ome.util.SqlAction;
import org.apache.commons.io.FileUtils;
import org.jmock.Mock;
import org.jmock.MockObjectTestCase;
import org.jmock.core.MockObjectSupportTestCase;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
import org.springframework.ldap.core.ContextMapper;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.util.ResourceUtils;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
/**
* Uses LDIF text files along with property files of good and bad user names to
* test that the LDAP API is properly functioning.
*/
@Test(groups = "ldap")
public class LdapTest extends MockObjectTestCase {
public class Fixture {
public ConfigurableApplicationContext ctx;
File file;
Mock role;
Mock sql;
Mock queryMock;
LocalQuery query;
LdapImpl ldap;
LdapConfig config;
LdapPasswordProvider provider;
public LdapTemplate template;
public OmeroContext applicationContext;
boolean ignoreCaseLookup;
public void createUserWithGroup(LdapTest t, final String dn,
String group) {
role.expects(atLeastOnce())
.method("createGroup")
.with(t.eq(group), MockObjectSupportTestCase.NULL,
t.eq(false)).will(returnValue(101L));
role.expects(once()).method("createExperimenter")
.will(returnValue(101L));
}
public Experimenter createUser(String user) {
return ldap.createUser(user);
}
public Experimenter createUser(String user, String string,
boolean checkPassword) {
return ldap.createUser(user, "password", checkPassword);
}
public Experimenter findExperimenter(String username) {
return ldap.findExperimenter(username);
}
public void setDN(Long experimenterID, String dn) {
ldap.setDN(experimenterID, dn);
}
public List<Experimenter> discover() {
return ldap.discover();
}
public EventContext login(String username, String group, String password) {
return null;
}
public Object execute(Executor.Work work) {
throw new RuntimeException("Only in subclasses!");
}
void close() {
if (ignoreCaseLookup) {
applicationContext.getBean("atomicIgnoreCase",
AtomicBoolean.class).set(false);
ignoreCaseLookup = false;
}
ctx.close();
}
}
/**
* Data provider which returns an array of XML files located under the root
* of the directory containing the class file of this test.
*/
@DataProvider(name = "ldif_files")
public Object[][] getLdifContexts() throws Exception {
String name = getClass().getName();
name = name.replaceAll("[.]", "//");
name = "classpath:" + name + ".class";
File file = ResourceUtils.getFile(name);
File dir = file.getParentFile();
Collection<?> coll = FileUtils.listFiles(dir, new String[] { "xml" },
true);
Object[][] files = new Object[coll.size()][];
int count = 0;
for (Object object : coll) {
files[count] = new Object[] { object };
count++;
}
return files;
}
/**
* Runs the LDAP test suite against each of the given LDIF files, by
* attempting to login against an embedded LDAP store with both the good
* names and the bad names.
*/
@Test(dataProvider = "ldif_files")
@SuppressWarnings("unchecked")
public void testLdiffFile(File file) throws Exception {
Fixture fixture = createFixture(file);
try {
Map<String, List<String>> good = fixture.ctx.getBean("good",
Map.class);
Map<String, List<String>> bad = fixture.ctx.getBean("bad",
Map.class);
assertPasses(fixture, good);
assertFails(fixture, bad);
assertCreateUserFromLdap(fixture, good);
if (!good.isEmpty()) {
assertDiscover(fixture, good);
}
} finally {
fixture.close();
}
}
protected Fixture createFixture(File ctxFile) throws Exception {
Fixture fixture = new Fixture();
fixture.ctx = new FileSystemXmlApplicationContext("file:"
+ ctxFile.getAbsolutePath());
fixture.config = (LdapConfig) fixture.ctx.getBean("config");
Map<String, LdapContextSource> sources = fixture.ctx
.getBeansOfType(LdapContextSource.class);
LdapContextSource source = sources.values().iterator().next();
String[] urls = source.getUrls();
assertEquals(1, urls.length);
fixture.template = new LdapTemplate(source);
fixture.role = mock(RoleProvider.class);
RoleProvider provider = (RoleProvider) fixture.role.proxy();
fixture.sql = mock(SqlAction.class);
SqlAction sql = (SqlAction) fixture.sql.proxy();
fixture.queryMock = mock(LocalQuery.class);
fixture.query = (LocalQuery) fixture.queryMock.proxy();
fixture.queryMock.expects(once()).method("findByString").will(
returnValue(null));
fixture.ldap = new LdapImpl(source, fixture.template, new Roles(),
fixture.config, provider, sql);
fixture.ldap.setQueryService(fixture.query);
fixture.provider = new LdapPasswordProvider(new PasswordUtil(sql),
fixture.ldap);
return fixture;
}
protected void assertPasses(Fixture fixture, Map<String, List<String>> users)
throws Exception {
LdapImpl ldap = fixture.ldap;
for (String user : users.keySet()) {
String dn = null;
assertTrue(1 <= users.get(user).size());
try {
dn = ldap.findDN(user);
} catch (ApiUsageException aue) {
// This will be one of the major errors: when we can't find a
// user that we expect to find. Adding a try/catch block for
// debugging purposes.
throw aue;
}
assertNotNull(dn);
assertEquals(fixture.ignoreCaseLookup ? user.toLowerCase() : user,
ldap.findExperimenter(user).getOmeName());
fixture.createUserWithGroup(this, dn, users.get(user).get(0));
assertNotNull(fixture.createUser(user, "password", true));
fixture.login(fixture.ignoreCaseLookup ? user.toLowerCase() : user,
users.get(user).get(0), "password");
}
}
protected void assertFails(Fixture fixture, Map<String, List<String>> users) {
LdapImpl ldap = fixture.ldap;
for (String user : users.keySet()) {
assertEquals(1, users.get(user).size());
try {
String dn = ldap.findDN(user);
assertNotNull(dn);
fixture.createUserWithGroup(this, dn, users.get(user).get(0));
assertNotNull(fixture.createUser(user, "password", true));
fixture.login(user, users.get(user).get(0), "password");
// Parsing afterwards to force an explosion to reproduce #2557
assertEquals(user, ldap.findExperimenter(user).getOmeName());
fail("user didn't fail");
} catch (ValidationException e) {
if (e.getMessage().equals(
"No group found for: cn=user,ou=attributeFilter")) {
// Good. This is the expected result for #8357
} else {
throw e;
// This means that we couldn't insert.
// See the thread on case-sensitivity in #2557
}
} catch (ApiUsageException e) {
// If not a ValidationException, but otherwise an
// ApiUsageException, then this will be the
// "Cannot find unique DN" which we are looking for.
} catch (SecurityViolation sv) {
// e.g. User 466 is not a member of group 54 and cannot login
// Also good.
}
}
}
protected void assertCreateUserFromLdap(Fixture fixture,
Map<String, List<String>> users) {
LdapImpl ldap = fixture.ldap;
for (String user : users.keySet()) {
String dn = null;
try {
dn = ldap.findDN(user);
} catch (ApiUsageException aue) {
throw aue;
}
assertNotNull(dn);
assertEquals(fixture.ignoreCaseLookup ? user.toLowerCase() : user,
ldap.findExperimenter(user).getOmeName());
fixture.createUserWithGroup(this, dn, users.get(user).get(0));
assertNotNull(fixture.createUser(user));
try {
fixture.createUser("nonExistingUserShouldNotBeCreated");
} catch (ApiUsageException aue) {
// Expected
continue;
}
fail("This user shouldn't have been created!");
}
}
protected void assertDiscover(Fixture fixture,
Map<String, List<String>> users) {
for (String user : users.keySet()) {
Experimenter experimenter = fixture.findExperimenter(user);
assertNotNull(experimenter);
fixture.setDN(experimenter.getId(), null);
List<Experimenter> discoveredExperimenters = fixture.discover();
if (!discoveredExperimenters.isEmpty()) {
boolean discovered = false;
for (Experimenter e : discoveredExperimenters) {
if (experimenter.getId().equals(e.getId())) {
discovered = true;
break;
}
}
assertTrue(discovered);
}
fixture.setDN(experimenter.getId(), "dn");
}
}
@SuppressWarnings("unchecked")
protected void addMemberOf(Fixture fixture, LdapTemplate template,
String user) throws NamingException {
List<String> dns = template.search("",
fixture.config.usernameFilter(user).encode(),
new ContextMapper() {
public Object mapFromContext(Object arg0) {
DirContextAdapter ctx = (DirContextAdapter) arg0;
return ctx.getNameInNamespace();
}
});
assertEquals(dns.toString(), 1, dns.size());
DistinguishedName name = new DistinguishedName(dns.get(0));
DistinguishedName root = new DistinguishedName(template
.getContextSource().getReadOnlyContext().getNameInNamespace());
// Build a relative name
for (int i = 0; i < root.size(); i++) {
name.removeFirst();
}
DirContextOperations context = template.lookupContext(name);
context.setAttributeValues("memberOf", new Object[] { "foo" });
template.modifyAttributes(context);
}
}