/* * $Id$ * * Copyright 2006-2014 University of Dundee. All rights reserved. * Use is subject to license terms supplied in LICENSE.txt */ package ome.server.utests.sec; import java.io.File; import java.nio.charset.Charset; import java.util.Arrays; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import ome.api.ILdap; import ome.conditions.ApiUsageException; import ome.logic.LdapImpl; import ome.model.meta.Experimenter; import ome.security.auth.ConfigurablePasswordProvider; import ome.security.auth.FilePasswordProvider; import ome.security.auth.JdbcPasswordProvider; import ome.security.auth.LdapConfig; import ome.security.auth.LdapPasswordProvider; import ome.security.auth.PasswordChangeException; import ome.security.auth.PasswordProvider; import ome.security.auth.PasswordProviders; import ome.security.auth.PasswordUtil; import ome.security.auth.PasswordUtility; import ome.system.OmeroContext; import ome.system.Roles; import ome.util.SqlAction; import org.jmock.Mock; import org.jmock.MockObjectTestCase; import org.jmock.core.Constraint; import org.springframework.util.ResourceUtils; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; @Test public class PasswordTest extends MockObjectTestCase { private final static Long GUEST_ID = new Roles().getGuestId(); private final static Long NON_GUEST_ID = new Roles().getGuestId() + 1; static File file = null; static { try { file = ResourceUtils.getFile("classpath:ome/server/utests/sec/" + "PasswordTest_FilePasswordProvider.properties"); } catch (Exception e) { e.printStackTrace(); } } /** * Concrete implementation of {@link ConfigurablePasswordProvider}'s default * actions. */ static class PP extends ConfigurablePasswordProvider { PP() { super(null); } } Charset latin1 = Charset.forName("ISO-8859-1"), utf8 = Charset.forName("UTF-8"); PasswordProvider provider; PasswordUtil utf8Util, latin1Util; Mock mockSql, mockLdap; SqlAction sql; LdapImpl ldap; AtomicBoolean validPassword = new AtomicBoolean(); AtomicReference<String> currentDn = new AtomicReference<String>(); AtomicReference<Experimenter> createdUser = new AtomicReference<Experimenter>(); @BeforeMethod protected void initJdbc() { initJdbc(utf8); } protected void initJdbc(Charset ch) { initJdbc(ch, true); } protected void initJdbc(Charset ch, boolean requirePassword) { initJdbc(ch, requirePassword, true); } protected void initJdbc(Charset ch, boolean requirePassword, boolean salt) { mockSql = mock(SqlAction.class); sql = (SqlAction) mockSql.proxy(); utf8Util = new PasswordUtil(sql, requirePassword, utf8); latin1Util = new PasswordUtil(sql, requirePassword, latin1); if (utf8 == ch) { initProvider(utf8Util, salt); } else { initProvider(latin1Util, salt); } } protected void initProvider(PasswordUtil util) { initProvider(util, true); } protected void initProvider(PasswordUtil util, boolean salt) { provider = new JdbcPasswordProvider(util, false, salt); setApplicationContext(); } protected void setApplicationContext() { ((ConfigurablePasswordProvider) provider).setApplicationContext( new OmeroContext(new String[]{})); } protected void initLdap(boolean setting) { initJdbc(); mockLdap = mock(ILdap.class); // ldap = (ILdap) mockLdap.proxy(); ldap = new LdapImpl(null, null, null, new LdapConfig(setting, "", "", "", "", "", false, ""), null, sql) { @Override public String findDN(String username) { return currentDn.get(); } @Override public boolean validatePassword(String dn, String password) { return validPassword.get(); } @Override public Experimenter createUser(String username, String password, boolean checkPassword) { return createdUser.get(); } @Override public String lookupLdapAuthExperimenter(Long id) { return currentDn.get(); } }; mockLdap.expects(atLeastOnce()).method("getSetting").will( returnValue(setting)); } // CONFIGURABLE /** * By default, the base class should return False ("Password rejected") */ public void testConfigurableDefaultsReturnsFalse() { provider = new PP(); assertFalse(provider.checkPassword("", "", false)); assertFalse(provider.hasPassword("")); } /** * By default, the base class should return False ("Password rejected") and * throw a {@link PasswordChangeException} ("Can't change") */ @Test(expectedExceptions = PasswordChangeException.class) public void testConfigurableDefaultsThrows() throws Exception { provider = new PP(); provider.changePassword("", ""); } // FILE public void testFileDefaults() throws Exception { provider = new FilePasswordProvider(new PasswordUtil(sql), file); setApplicationContext(); assertTrue(provider.hasPassword("test")); assertTrue(provider.checkPassword("test", "test", false)); assertFalse(provider.checkPassword("unknown", "anything", false)); } public void testFilesDontIgnoreUnknownReturnsNull() throws Exception { provider = new FilePasswordProvider(null, file, true); setApplicationContext(); assertFalse(provider.hasPassword("unknown")); assertNull(provider.checkPassword("unknown", "anything", false)); } @Test(expectedExceptions = PasswordChangeException.class) public void testFilesThrowsOnChange() throws Exception { provider = new FilePasswordProvider(null, file, true); setApplicationContext(); provider.changePassword("test", "something new"); } // JDBC public void testJdbcDefaults() throws Exception { initJdbc(); userIdReturns1(); provider.hasPassword("test"); userIdReturnsNull(); provider.hasPassword("unknown"); String encoded = ((PasswordUtility) provider).encodePassword("test"); getPasswordHash(encoded); userIdReturns1(); assertTrue(provider.checkPassword("test", "test", false)); getPasswordHash(encoded); userIdReturns1(); assertFalse(provider.checkPassword("test", "GARBAGE", false)); } public void tesJdbcIgnoreUnknownReturnsFalse() throws Exception { initJdbc(); userIdReturnsNull(); assertFalse(provider.checkPassword("unknown", "anything", false)); } public void tesJdbcDontIgnoreUnknownReturnsNull() throws Exception { initJdbc(); userIdReturnsNull(); provider = new JdbcPasswordProvider(new PasswordUtil(sql), true); setApplicationContext(); assertNull(provider.checkPassword("unknown", "anything", false)); } public void testJdbcChangesPassword() throws Exception { initJdbc(); userIdReturns1(); setHashCalledWith(eq(1l), ANYTHING); provider.changePassword("a", "b"); } @Test(expectedExceptions = PasswordChangeException.class) public void testJdbcThrowsOnBadUsername() throws Exception { initJdbc(); userIdReturnsNull(); provider.changePassword("a", "b"); } // LDAP public void tesLdapDefaults() throws Exception { initLdap(true); provider = new LdapPasswordProvider(new PasswordUtil(sql), ldap); setApplicationContext(); String encoded = ((PasswordUtility) provider).encodePassword("test"); getPasswordHash(encoded); getDn("dn"); userIdReturns1(); validateLdapPassword(true); assertTrue(provider.checkPassword("test", "test", false)); getPasswordHash(encoded); userIdReturns1(); validateLdapPassword(false); getDn("dn"); assertFalse(provider.checkPassword("test", "GARBAGE", false)); userIdReturnsNull(); assertFalse(provider.hasPassword("unknown")); userIdReturns1(); getPasswordHash(null); validateLdapPassword(false); getDn(null); assertFalse(provider.hasPassword("no-dn")); getPasswordHash("dn"); getDn("dn"); userIdReturns1(); assertTrue(provider.hasPassword("dn")); } public void tesLdapIgnoreUnknownCreatesFailsReturnsFalse() throws Exception { initLdap(true); userIdReturnsNull(); ldapCreatesUser(false); provider = new LdapPasswordProvider(new PasswordUtil(sql), ldap, true); setApplicationContext(); assertNull(provider.checkPassword("unknown", "anything", false)); } public void tesLdapIgnoreUnknownCreatesSucceedsReturnsTrue() throws Exception { initLdap(true); userIdReturnsNull(); ldapCreatesUser(true); provider = new LdapPasswordProvider(new PasswordUtil(sql), ldap, true); setApplicationContext(); assertTrue(provider.checkPassword("unknown", "anything", false)); } public void tesLdapIgnoreUnknownCreatesThrows() throws Exception { initLdap(true); userIdReturnsNull(); ldapCreatesUserAndThrows(); provider = new LdapPasswordProvider(new PasswordUtil(sql), ldap, true); setApplicationContext(); assertNull(provider.checkPassword("unknown", "anything", false)); } public void tesLdapDontIgnoreUnknownCreatesFailsReturnsFalse() throws Exception { initLdap(true); userIdReturnsNull(); ldapCreatesUser(false); provider = new LdapPasswordProvider(new PasswordUtil(sql), ldap, false); setApplicationContext(); assertFalse(provider.checkPassword("unknown", "anything", false)); } public void tesLdapDontIgnoreUnknownCreatesSucceedsReturnsTrue() throws Exception { initLdap(true); userIdReturnsNull(); ldapCreatesUser(true); provider = new LdapPasswordProvider(new PasswordUtil(sql), ldap, false); setApplicationContext(); assertTrue(provider.checkPassword("unknown", "anything", false)); } public void tesLdapDontIgnoreUnknownCreatesThrows() throws Exception { initLdap(true); userIdReturnsNull(); ldapCreatesUserAndThrows(); provider = new LdapPasswordProvider(new PasswordUtil(sql), ldap, false); setApplicationContext(); assertFalse(provider.checkPassword("unknown", "anything", false)); } @Test(expectedExceptions = PasswordChangeException.class) public void testLdapChangesPasswordThrows() throws Exception { initLdap(true); provider = new LdapPasswordProvider(new PasswordUtil(sql), ldap); setApplicationContext(); provider.changePassword("a", "b"); } /** * Straight-forward stub to allow easy composite testing. */ static class Stub implements PasswordProvider { Boolean check = null; boolean exception = true; boolean hasPasswordCalled = false; boolean changePasswordCalled = false; boolean checkPasswordCalled = false; public Stub() { } public Stub(Boolean check, boolean exc) { this.check = check; this.exception = exc; } public void changePassword(String user, String password) throws PasswordChangeException { changePasswordCalled = true; if (exception) { throw new PasswordChangeException(""); } } public boolean hasPassword(String user) { hasPasswordCalled = true; return check == null ? false : check.booleanValue(); } public Boolean checkPassword(String user, String password, boolean readOnly) { checkPasswordCalled = true; return check; } void assertChangePasswordCalled() { if (!changePasswordCalled) { fail(); } } void assertChangePasswordNotCalled() { if (changePasswordCalled) { fail(); } } void assertCheckPasswordCalled() { if (!checkPasswordCalled) { fail(); } } void assertCheckPasswordNotCalled() { if (checkPasswordCalled) { fail(); } } void assertHasPasswordCalled() { if (!hasPasswordCalled) { fail(); } } void assertHasPasswordNotCalled() { if (hasPasswordCalled) { fail(); } } } // COMPOSITE LDAP FIRST THEN JDBC (standard) public void testChainedUnknownPropogatesToSecondStub() throws Exception { Stub s1 = new Stub(); Stub s2 = new Stub(); provider = new PasswordProviders(s1, s2); assertNull(provider.checkPassword("known", "password", false)); s1.assertCheckPasswordCalled(); s2.assertCheckPasswordCalled(); } public void testChainedUnknownPropogatesToSecondStubWhichFails() throws Exception { Stub s1 = new Stub(); Stub s2 = new Stub(false, false); provider = new PasswordProviders(s1, s2); assertFalse(provider.checkPassword("known", "password", false)); s1.assertCheckPasswordCalled(); s2.assertCheckPasswordCalled(); } public void testChainedKnownPropogatesToSecondStubWhichSucceeds() throws Exception { Stub s1 = new Stub(); Stub s2 = new Stub(true, false); provider = new PasswordProviders(s1, s2); assertTrue(provider.checkPassword("known", "password", false)); s1.assertCheckPasswordCalled(); s2.assertCheckPasswordCalled(); } public void testChainedKnownDoesntPropagate() throws Exception { Stub s1 = new Stub(true, false); Stub s2 = new Stub(true, false); provider = new PasswordProviders(s1, s2); assertTrue(provider.checkPassword("known", "password", false)); s1.assertCheckPasswordCalled(); s2.assertCheckPasswordNotCalled(); } public void testChainedUnknownDoesntPropagate() throws Exception { Stub s1 = new Stub(false, false); Stub s2 = new Stub(false, false); provider = new PasswordProviders(s1, s2); assertFalse(provider.checkPassword("unknown", "password", false)); s1.assertCheckPasswordCalled(); s2.assertCheckPasswordNotCalled(); } public void testChainedFirstChangePassword() throws Exception { Stub s1 = new Stub(true, false); Stub s2 = new Stub(true, false); provider = new PasswordProviders(s1, s2); provider.changePassword("",""); s1.assertHasPasswordCalled(); s1.assertChangePasswordCalled(); s2.assertHasPasswordNotCalled(); s2.assertChangePasswordNotCalled(); } public void testChainedSecondChangePassword() throws Exception { Stub s1 = new Stub(false, false); Stub s2 = new Stub(true, false); provider = new PasswordProviders(s1, s2); provider.changePassword("",""); s1.assertHasPasswordCalled(); s1.assertChangePasswordNotCalled(); s2.assertHasPasswordCalled(); s2.assertChangePasswordCalled(); } public void testChainedNoneChangePassword() throws Exception { Stub s1 = new Stub(false, false); Stub s2 = new Stub(false, false); provider = new PasswordProviders(s1, s2); assertChangeThrows(); s1.assertHasPasswordCalled(); s1.assertChangePasswordNotCalled(); s2.assertHasPasswordCalled(); s2.assertChangePasswordNotCalled(); } public void testChainedFirstWontChangePassword() throws Exception { Stub s1 = new Stub(true, true); Stub s2 = new Stub(false, false); provider = new PasswordProviders(s1, s2); assertChangeThrows(); s1.assertHasPasswordCalled(); s2.assertHasPasswordNotCalled(); } public void testChainedSecondWontChangePassword() throws Exception { Stub s1 = new Stub(false, true); Stub s2 = new Stub(true, true); provider = new PasswordProviders(s1, s2); assertChangeThrows(); s1.assertHasPasswordCalled(); s2.assertHasPasswordCalled(); } // ~ password encoding // ========================================================================= final static String good = "ążćę"; final static String bad = "????"; final static String badHash = "6U8L+rjJh6dDe6ThaXwcwA=="; final static String badHashSalt1 = "AfSXrwNujnMYx1CagyujVA=="; final static String goodHash = "iIoEyIOGsGsDhWZMYNBTKQ=="; final static String goodHashSalt1 = "RBH/4oA/c43qLXeotWM/XA=="; public void testLatin1Encoding() { final PasswordUtil latin1Util = new PasswordUtil(sql, latin1); byte[] badBytes = bad.getBytes(latin1); byte[] goodBytes = good.getBytes(latin1); assertTrue(Arrays.equals(badBytes, goodBytes)); assertEquals(bad, new String(good.getBytes(latin1))); assertEquals(badHash, latin1Util.passwordDigest(bad)); assertEquals(badHash, latin1Util.passwordDigest(good)); } public void testUtf8Encoding() { final PasswordUtil utf8Util = new PasswordUtil(sql, utf8); assertEquals(badHash, utf8Util.passwordDigest(bad)); assertEquals(goodHash, utf8Util.passwordDigest(good)); assertFalse(goodHash.equals(badHash)); } public void testJdbcLatin1PasswordOldUtil() throws Exception { initJdbc(latin1); // Setting the password with latin1 uses the bad hash userIdReturns1(); setHashCalledWith(eq(1L), eq(badHashSalt1)); provider.changePassword("test", good); // Checking the password whether good or bad passes // 1) Good: Yes userIdReturns1(); getPasswordHash(badHash); assertTrue(provider.checkPassword("test", good, true)); // 1) Bad: Yes?! This was the bug. userIdReturns1(); getPasswordHash(badHash); assertTrue(provider.checkPassword("test", bad, true)); } public void testJdbcLatin1PasswordNewUtil() throws Exception { initJdbc(utf8); // For this to work, the old util must be set. ((JdbcPasswordProvider) provider).setLegacyUtil(latin1Util); // Here we don't worry about testing the setting of the password // since a latin1 util was used for the setting. // Checking the password whether good or bad passes // 1) Good: Yes // Requires rather more calls due to automatic upgrade/change of legacy password. mockSql.expects(atLeastOnce()).method("getUserId").will(returnValue(1L)); mockSql.expects(once()).method("getUsername").will(returnValue("guest")); mockSql.expects(once()).method("setUserPassword").will(returnValue(true)); mockSql.expects(once()).method("clearPermissionsBit").will(returnValue(true)); getPasswordHash(badHash); assertTrue(provider.checkPassword("test", good, true)); // 1) Bad: Yes, but ERROR printed to the logs. userIdReturns1(); getPasswordHash(badHash); assertTrue(provider.checkPassword("test", bad, true)); } public void testJdbcUtf8PasswordNewUtil() throws Exception { initJdbc(utf8); // Setting the password with utf8 uses the good hash userIdReturns1(); setHashCalledWith(eq(1L), eq(goodHashSalt1)); provider.changePassword("test", good); // Only checking the password with good passes. // 1) Good: yes userIdReturns1(); getPasswordHash(goodHashSalt1); assertTrue(provider.checkPassword("test", good, true)); // 2) Bad: NO! userIdReturns1(); getPasswordHash(goodHashSalt1); assertFalse(provider.checkPassword("test", bad, true)); } // ~ empty passwords // ========================================================================= public void testIsPasswordRequiredWithoutStrictSetting() { PasswordUtil util = new PasswordUtil(sql, false); assertFalse(util.isPasswordRequired(null)); assertFalse(util.isPasswordRequired(456l)); assertFalse(util.isPasswordRequired(GUEST_ID)); } public void testIsPasswordRequiredWithStrictSetting() { PasswordUtil util = new PasswordUtil(sql, true); assertTrue(util.isPasswordRequired(null)); assertTrue(util.isPasswordRequired(456l)); assertFalse(util.isPasswordRequired(GUEST_ID)); } public void testNonstrictProviderAcceptsEmptyGuest() throws Exception { initJdbc(utf8, false); userIdReturns(GUEST_ID); setHashCalledWith(eq(GUEST_ID), eq("")); provider.changePassword("test", ""); userIdReturns(GUEST_ID); getPasswordHash(""); assertTrue(provider.checkPassword("test", "", true)); } public void testNonstrictProviderAcceptsEmptyUser() throws Exception { initJdbc(utf8, false); userIdReturns(NON_GUEST_ID); setHashCalledWith(eq(NON_GUEST_ID), eq("")); getPasswordHash(""); provider.changePassword("test", ""); userIdReturns(NON_GUEST_ID); assertTrue(provider.checkPassword("test", "", true)); } public void testStrictProviderAcceptsEmptyGuestNoLock() throws Exception { initJdbc(utf8, true); userIdReturns(GUEST_ID); setHashCalledWith(eq(GUEST_ID), eq("")); provider.changePassword("test", ""); userIdReturns(GUEST_ID); getPasswordHash(""); assertTrue(provider.checkPassword("test", "", true)); } public void testStrictProviderAcceptsEmptyGuestNoSaltStillNoLock() throws Exception { // Now without salting turned on, see if we can still have // the "require_password" setting adhered to. initJdbc(utf8, true, false); userIdReturns(GUEST_ID); setHashCalledWith(eq(GUEST_ID), eq("")); provider.changePassword("test", ""); userIdReturns(GUEST_ID); getPasswordHash(""); assertTrue(provider.checkPassword("test", "", true)); } public void testStrictProviderAcceptsEmptyUserLocks() throws Exception { initJdbc(utf8, true); userIdReturns(NON_GUEST_ID); setHashCalledWith(eq(NON_GUEST_ID), eq(null)); provider.changePassword("test", ""); userIdReturns(NON_GUEST_ID); getPasswordHash(null); assertFalse(provider.checkPassword("test", "", true)); } // ~ Helpers // ========================================================================= private void setHashCalledWith(Constraint... constraints) { mockSql.expects(once()).method("clearPermissionsBit") .will(returnValue(true)); mockSql.expects(once()).method("setUserPassword") .with(constraints).will(returnValue(true)); } private void getPasswordHash(String value) { mockSql.expects(once()).method("getPasswordHash").will(returnValue(value)); } private void getDn(String value) { currentDn.set(value); } private void userIdReturnsNull() { mockSql.expects(once()).method("getUserId").will(returnValue(null)); } private void userIdReturns1() { userIdReturns(1L); } private void userIdReturns(Long id) { mockSql.expects(once()).method("getUserId").will(returnValue(id)); } private void ldapCreatesUser(boolean andReturns) { Experimenter e = andReturns ? new Experimenter() : null; createdUser.set(e); mockLdap.expects(once()).method("createUser").will( returnValue(andReturns)); } private void ldapCreatesUserAndThrows() { createdUser.set(null); mockLdap.expects(once()).method("createUser").will( throwException(new ApiUsageException(""))); } private void validateLdapPassword(boolean v) { validPassword.set(v); } private void assertChangeThrows() { try { provider.changePassword("", ""); fail("must throw"); } catch (PasswordChangeException pce) { // good. } } }