/******************************************************************************* * Cloud Foundry * Copyright (c) [2009-2016] Pivotal Software, Inc. All Rights Reserved. * * This product is licensed to you under the Apache License, Version 2.0 (the "License"). * You may not use this product except in compliance with the License. * * This product includes a number of subcomponents with * separate copyright notices and license terms. Your use of these * subcomponents is subject to the terms and conditions of the * subcomponent's license, as noted in the LICENSE file. *******************************************************************************/ package org.cloudfoundry.identity.uaa.scim.jdbc; import org.cloudfoundry.identity.uaa.audit.event.EntityDeletedEvent; import org.cloudfoundry.identity.uaa.constants.OriginKeys; import org.cloudfoundry.identity.uaa.provider.IdentityProvider; import org.cloudfoundry.identity.uaa.provider.JdbcIdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.resources.SimpleAttributeNameMapper; import org.cloudfoundry.identity.uaa.resources.jdbc.JdbcPagingListFactory; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.ScimUser.Group; import org.cloudfoundry.identity.uaa.scim.ScimUser.PhoneNumber; import org.cloudfoundry.identity.uaa.scim.bootstrap.ScimUserBootstrapTests; import org.cloudfoundry.identity.uaa.scim.exception.InvalidScimResourceException; import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceAlreadyExistsException; import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceNotFoundException; import org.cloudfoundry.identity.uaa.scim.test.TestUtils; import org.cloudfoundry.identity.uaa.test.JdbcTestBase; import org.cloudfoundry.identity.uaa.user.UaaAuthority; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.cloudfoundry.identity.uaa.zone.JdbcIdentityZoneProvisioning; import org.cloudfoundry.identity.uaa.zone.MultitenancyFixture; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.Authentication; import org.springframework.security.crypto.bcrypt.BCrypt; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.LOGIN_SERVER; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; public class JdbcScimUserProvisioningTests extends JdbcTestBase { private JdbcScimUserProvisioning db; private JdbcIdentityProviderProvisioning providerDb; private JdbcIdentityZoneProvisioning zoneDb; private static final String JOE_ID = "550e8400-e29b-41d4-a716-446655440000"; private static final String MABEL_ID = UUID.randomUUID().toString(); private static final String SQL_INJECTION_FIELDS = "password,version,created,lastModified,username,email,givenName,familyName"; private static final String ADD_USER_SQL_FORMAT = "insert into users (id, username, password, email, givenName, familyName, phoneNumber, identity_zone_id) values ('%s','%s','%s','%s','%s', '%s', '%s', '%s')"; private static final String OLD_ADD_USER_SQL_FORMAT = "insert into users (id, username, password, email, givenName, familyName, phoneNumber) values ('%s','%s','%s','%s','%s', '%s', '%s')"; private static final String DELETE_USER_SQL_FORMAT = "delete from users where id='%s'"; private static final String VERIFY_USER_SQL_FORMAT = "select verified from users where id=?"; private static final String INSERT_MEMBERSHIP = "insert into group_membership (group_id, member_id, member_type,authorities,added, origin, identity_zone_id) values (?,?,?,?,?,?,?)"; private int existingUserCount = 0; private String defaultIdentityProviderId; private RandomValueStringGenerator generator = new RandomValueStringGenerator(); private JdbcPagingListFactory pagingListFactory; @Before public void initJdbcScimUserProvisioningTests() throws Exception { pagingListFactory = new JdbcPagingListFactory(jdbcTemplate, limitSqlAdapter); db = new JdbcScimUserProvisioning(jdbcTemplate, pagingListFactory); zoneDb = new JdbcIdentityZoneProvisioning(jdbcTemplate); providerDb = new JdbcIdentityProviderProvisioning(jdbcTemplate); ScimSearchQueryConverter filterConverter = new ScimSearchQueryConverter(); Map<String, String> replaceWith = new HashMap<String, String>(); replaceWith.put("emails\\.value", "email"); replaceWith.put("groups\\.display", "authorities"); replaceWith.put("phoneNumbers\\.value", "phoneNumber"); filterConverter.setAttributeNameMapper(new SimpleAttributeNameMapper(replaceWith)); db.setQueryConverter(filterConverter); BCryptPasswordEncoder pe = new BCryptPasswordEncoder(4); existingUserCount = jdbcTemplate.queryForObject("select count(id) from users", Integer.class); defaultIdentityProviderId = jdbcTemplate.queryForObject("select id from identity_provider where origin_key = ? and identity_zone_id = ?", String.class, OriginKeys.UAA, "uaa"); addUser(JOE_ID, "joe", pe.encode("joespassword"), "joe@joe.com", "Joe", "User", "+1-222-1234567", defaultIdentityProviderId, "uaa"); addUser(MABEL_ID, "mabel", pe.encode("mabelspassword"), "mabel@mabel.com", "Mabel", "User", "", defaultIdentityProviderId, "uaa"); } private String createUserForDelete() { String tmpUserId = UUID.randomUUID().toString(); addUser(tmpUserId, tmpUserId, "password", tmpUserId + "@delete.com", "ToDelete", "User", "+1-234-5678910", defaultIdentityProviderId, "uaa"); return tmpUserId; } private void addUser(String id, String username, String password, String email, String givenName, String familyName, String phoneNumber, String identityProviderId, String identityZoneId) { TestUtils.assertNoSuchUser(jdbcTemplate, "id", id); jdbcTemplate.execute(String.format(ADD_USER_SQL_FORMAT, id, username, password, email, givenName, familyName, phoneNumber, identityZoneId)); } private void removeUser(String id) { jdbcTemplate.execute(String.format(DELETE_USER_SQL_FORMAT, id)); } @After public void clear() throws Exception { jdbcTemplate.execute("delete from users where id = '" + JOE_ID + "'"); jdbcTemplate.execute("delete from users where id = '" + MABEL_ID + "'"); jdbcTemplate.execute("delete from users where upper(userName) = 'JO@FOO.COM'"); jdbcTemplate.execute("delete from users where upper(userName) = 'JONAH@FOO.COM'"); jdbcTemplate.execute("delete from users where upper(userName) = 'RO''GALLAGHER@EXAMPLE.COM'"); jdbcTemplate.execute("delete from users where upper(userName) = 'USER@EXAMPLE.COM'"); jdbcTemplate.execute("delete from identity_provider where identity_zone_id = 'my-zone-id'"); jdbcTemplate.execute("delete from identity_zone where id = 'my-zone-id'"); IdentityZoneHolder.clear(); } @Test public void canCreateUserWithExclamationMarkInUsername() { String userName = "jo!!@foo.com"; ScimUser user = new ScimUser(null, userName, "Jo", "User"); user.addEmail(userName); ScimUser created = db.createUser(user, "j7hyqpassX"); assertEquals(userName, created.getUserName()); } protected void addMembership(String userId, String origin) { Timestamp timestamp = new Timestamp(System.currentTimeMillis()); String zoneId = IdentityZoneHolder.get().getId(); jdbcTemplate.update(INSERT_MEMBERSHIP, userId, userId, "USER", "authorities", timestamp, origin, zoneId); } @Test public void test_can_delete_provider_users_in_default_zone() throws Exception { ScimUser user = new ScimUser(null, "jo@foo.com", "Jo", "User"); user.addEmail("jo@blah.com"); user.setOrigin(LOGIN_SERVER); ScimUser created = db.createUser(user, "j7hyqpassX"); assertEquals("jo@foo.com", created.getUserName()); assertNotNull(created.getId()); assertEquals(LOGIN_SERVER, created.getOrigin()); assertThat(jdbcTemplate.queryForObject( "select count(*) from users where origin=? and identity_zone_id=?", new Object[] {LOGIN_SERVER,IdentityZone.getUaa().getId()}, Integer.class ), is(1) ); addMembership(created.getId(), created.getOrigin()); assertThat(jdbcTemplate.queryForObject("select count(*) from group_membership where member_id=?", new Object[] {created.getId()}, Integer.class), is(1)); IdentityProvider loginServer = new IdentityProvider() .setOriginKey(LOGIN_SERVER) .setIdentityZoneId(IdentityZone.getUaa().getId()); db.onApplicationEvent(new EntityDeletedEvent<>(loginServer, null)); assertThat(jdbcTemplate.queryForObject("select count(*) from users where origin=? and identity_zone_id=?", new Object[] {LOGIN_SERVER, IdentityZone.getUaa().getId()}, Integer.class), is(0)); assertThat(jdbcTemplate.queryForObject("select count(*) from group_membership where member_id=?", new Object[] {created.getId()}, Integer.class), is(0)); } @Test public void test_can_delete_provider_users_in_other_zone() throws Exception { String id = generator.generate(); IdentityZone zone = MultitenancyFixture.identityZone(id, id); IdentityZoneHolder.set(zone); ScimUser user = new ScimUser(null, "jo@foo.com", "Jo", "User"); user.addEmail("jo@blah.com"); user.setOrigin(LOGIN_SERVER); ScimUser created = db.createUser(user, "j7hyqpassX"); assertEquals("jo@foo.com", created.getUserName()); assertNotNull(created.getId()); assertEquals(LOGIN_SERVER, created.getOrigin()); assertEquals(zone.getId(), created.getZoneId()); assertThat(jdbcTemplate.queryForObject("select count(*) from users where origin=? and identity_zone_id=?", new Object[] {LOGIN_SERVER, zone.getId()}, Integer.class), is(1)); addMembership(created.getId(), created.getOrigin()); assertThat(jdbcTemplate.queryForObject("select count(*) from group_membership where member_id=?", new Object[] {created.getId()}, Integer.class), is(1)); IdentityProvider loginServer = new IdentityProvider() .setOriginKey(LOGIN_SERVER) .setIdentityZoneId(zone.getId()); db.onApplicationEvent(new EntityDeletedEvent<>(loginServer, null)); assertThat(jdbcTemplate.queryForObject("select count(*) from users where origin=? and identity_zone_id=?", new Object[] {LOGIN_SERVER, zone.getId()}, Integer.class), is(0)); assertThat(jdbcTemplate.queryForObject("select count(*) from group_membership where member_id=?", new Object[] {created.getId()}, Integer.class), is(0)); } @Test public void test_can_delete_zone_users() throws Exception { String id = generator.generate(); IdentityZone zone = MultitenancyFixture.identityZone(id, id); IdentityZoneHolder.set(zone); ScimUser user = new ScimUser(null, "jo@foo.com", "Jo", "User"); user.addEmail("jo@blah.com"); user.setOrigin(UAA); ScimUser created = db.createUser(user, "j7hyqpassX"); assertEquals("jo@foo.com", created.getUserName()); assertNotNull(created.getId()); assertEquals(UAA, created.getOrigin()); assertEquals(zone.getId(), created.getZoneId()); assertThat(jdbcTemplate.queryForObject("select count(*) from users where origin=? and identity_zone_id=?", new Object[] {UAA, zone.getId()}, Integer.class), is(1)); addMembership(created.getId(), created.getOrigin()); assertThat(jdbcTemplate.queryForObject("select count(*) from group_membership where member_id=?", new Object[] {created.getId()}, Integer.class), is(1)); db.onApplicationEvent(new EntityDeletedEvent<>(zone, null)); assertThat(jdbcTemplate.queryForObject("select count(*) from users where origin=? and identity_zone_id=?", new Object[] {UAA, zone.getId()}, Integer.class), is(0)); assertThat(jdbcTemplate.queryForObject("select count(*) from group_membership where member_id=?", new Object[] {created.getId()}, Integer.class), is(0)); } @Test public void test_cannot_delete_uaa_zone_users() throws Exception { ScimUser user = new ScimUser(null, "jo@foo.com", "Jo", "User"); user.addEmail("jo@blah.com"); user.setOrigin(UAA); ScimUser created = db.createUser(user, "j7hyqpassX"); assertEquals("jo@foo.com", created.getUserName()); assertNotNull(created.getId()); assertEquals(UAA, created.getOrigin()); assertThat(jdbcTemplate.queryForObject("select count(*) from users where origin=? and identity_zone_id=?", new Object[] {UAA, IdentityZone.getUaa().getId()}, Integer.class), is(3)); IdentityProvider loginServer = new IdentityProvider() .setOriginKey(UAA) .setIdentityZoneId(IdentityZone.getUaa().getId()); db.onApplicationEvent(new EntityDeletedEvent<>(loginServer, null)); assertThat(jdbcTemplate.queryForObject("select count(*) from users where origin=? and identity_zone_id=?", new Object[] {UAA, IdentityZone.getUaa().getId()}, Integer.class), is(3)); } @Test public void test_cannot_delete_uaa_provider_users_in_other_zone() throws Exception { String id = generator.generate(); IdentityZone zone = MultitenancyFixture.identityZone(id, id); IdentityZoneHolder.set(zone); ScimUser user = new ScimUser(null, "jo@foo.com", "Jo", "User"); user.addEmail("jo@blah.com"); user.setOrigin(UAA); ScimUser created = db.createUser(user, "j7hyqpassX"); assertEquals("jo@foo.com", created.getUserName()); assertNotNull(created.getId()); assertEquals(UAA, created.getOrigin()); assertEquals(zone.getId(), created.getZoneId()); assertThat(jdbcTemplate.queryForObject("select count(*) from users where origin=? and identity_zone_id=?", new Object[] {UAA, zone.getId()}, Integer.class), is(1)); IdentityProvider loginServer = new IdentityProvider() .setOriginKey(UAA) .setIdentityZoneId(zone.getId()); db.onApplicationEvent(new EntityDeletedEvent<>(loginServer, null)); assertThat(jdbcTemplate.queryForObject("select count(*) from users where origin=? and identity_zone_id=?", new Object[] {UAA, zone.getId()}, Integer.class), is(1)); } @Test public void canCreateUserInDefaultIdentityZone() { ScimUser user = new ScimUser(null, "jo@foo.com", "Jo", "User"); user.addEmail("jo@blah.com"); ScimUser created = db.createUser(user, "j7hyqpassX"); assertEquals("jo@foo.com", created.getUserName()); assertNotNull(created.getId()); assertNotSame(user.getId(), created.getId()); Map<String, Object> map = jdbcTemplate.queryForMap("select * from users where id=?", created.getId()); assertEquals(user.getUserName(), map.get("userName")); assertEquals(user.getUserType(), map.get(UaaAuthority.UAA_USER.getUserType())); assertNull(created.getGroups()); assertEquals(OriginKeys.UAA, created.getOrigin()); assertEquals("uaa", map.get("identity_zone_id")); assertNull(user.getPasswordLastModified()); assertNotNull(created.getPasswordLastModified()); assertTrue(Math.abs(created.getMeta().getCreated().getTime() - created.getPasswordLastModified().getTime()) < 1001); //1 second at most given MySQL fractionless timestamp } @Test public void canModifyPassword() throws Exception { ScimUser user = new ScimUser(null, generator.generate()+ "@foo.com", "Jo", "User"); user.addEmail(user.getUserName()); ScimUser created = db.createUser(user, "j7hyqpassX"); assertNull(user.getPasswordLastModified()); assertNotNull(created.getPasswordLastModified()); assertTrue(Math.abs(created.getMeta().getCreated().getTime() - created.getPasswordLastModified().getTime()) < 1001); Thread.sleep(10); db.changePassword(created.getId(), "j7hyqpassX", "j7hyqpassXXX"); user = db.retrieve(created.getId()); assertNotNull(user.getPasswordLastModified()); assertTrue(Math.abs(user.getMeta().getLastModified().getTime() - user.getPasswordLastModified().getTime()) < 1001); } @Test public void testSetPasswordChangeRequired() { ScimUser user = new ScimUser(null, generator.generate()+ "@foo.com", "Jo", "User"); user.addEmail(user.getUserName()); ScimUser created = db.createUser(user, "j7hyqpassX"); assertFalse(db.checkPasswordChangeIndividuallyRequired(created.getId())); db.updatePasswordChangeRequired(created.getId(), true); assertTrue(db.checkPasswordChangeIndividuallyRequired(created.getId())); db.updatePasswordChangeRequired(created.getId(), false); assertFalse(db.checkPasswordChangeIndividuallyRequired(created.getId())); } @Test public void canCreateUserInOtherIdentityZone() { String otherZoneId = "my-zone-id"; createOtherIdentityZone(otherZoneId); String idpId = createOtherIdentityProvider(OriginKeys.UAA, otherZoneId); ScimUser user = new ScimUser(null, "jo@foo.com", "Jo", "User"); user.addEmail("jo@blah.com"); ScimUser created = db.createUser(user, "j7hyqpassX"); assertEquals("jo@foo.com", created.getUserName()); assertNotNull(created.getId()); assertNotSame(user.getId(), created.getId()); Map<String, Object> map = jdbcTemplate.queryForMap("select * from users where id=?", created.getId()); assertEquals(user.getUserName(), map.get("userName")); assertEquals(user.getUserType(), map.get(UaaAuthority.UAA_USER.getUserType())); assertNull(created.getGroups()); assertEquals(OriginKeys.UAA, created.getOrigin()); assertEquals("my-zone-id", map.get("identity_zone_id")); } @Test public void countUsersAcrossAllZones() { IdentityZoneHolder.clear(); int beginningCount = db.getTotalCount(); canCreateUserInDefaultIdentityZone(); IdentityZoneHolder.clear(); assertEquals(beginningCount+1, db.getTotalCount()); canCreateUserInOtherIdentityZone(); IdentityZoneHolder.clear(); assertEquals(beginningCount+2, db.getTotalCount()); } private void createOtherIdentityZone(String zoneId) { IdentityZone identityZone = MultitenancyFixture.identityZone(zoneId, "myzone"); zoneDb.create(identityZone); IdentityZoneHolder.set(identityZone); } private String createOtherIdentityProvider(String origin, String zoneId) { IdentityProvider identityProvider = MultitenancyFixture.identityProvider(origin, zoneId); return providerDb.create(identityProvider).getId(); } @Test public void validateOriginAndExternalIDDuringCreateAndUpdate() { String origin = "test"; ScimUserBootstrapTests.addIdentityProvider(jdbcTemplate, origin); String externalId = "testId"; ScimUser user = new ScimUser(null, "jo@foo.com", "Jo", "User"); user.setOrigin(origin); user.setExternalId(externalId); user.addEmail("jo@blah.com"); ScimUser created = db.createUser(user, "j7hyqpassX"); assertEquals("jo@foo.com", created.getUserName()); assertNotNull(created.getId()); assertNotSame(user.getId(), created.getId()); Map<String, Object> map = jdbcTemplate.queryForMap("select * from users where id=?", created.getId()); assertEquals(user.getUserName(), map.get("userName")); assertEquals(user.getUserType(), map.get(UaaAuthority.UAA_USER.getUserType())); assertNull(created.getGroups()); assertEquals(origin, created.getOrigin()); assertEquals(externalId, created.getExternalId()); String origin2 = "test2"; ScimUserBootstrapTests.addIdentityProvider(jdbcTemplate,origin2); String externalId2 = "testId2"; created.setOrigin(origin2); created.setExternalId(externalId2); ScimUser updated = db.update(created.getId(), created); assertEquals(origin2, updated.getOrigin()); assertEquals(externalId2, updated.getExternalId()); } @Test public void canCreateUserWithoutGivenNameAndFamilyName() { ScimUser user = new ScimUser(null, "jonah@foo.com", null, null); user.addEmail("jo@blah.com"); ScimUser created = db.createUser(user, "j7hyqpassX"); assertEquals("jonah@foo.com", created.getUserName()); assertNotNull(created.getId()); assertNotSame(user.getId(), created.getId()); Map<String, Object> map = jdbcTemplate.queryForMap("select * from users where id=?", created.getId()); assertEquals(user.getUserName(), map.get("userName")); assertEquals(user.getUserType(), map.get(UaaAuthority.UAA_USER.getUserType())); assertNull(created.getGroups()); } @Test public void canCreateUserWithSingleQuoteInEmailAndUsername() { ScimUser user = new ScimUser(null, "ro'gallagher@example.com", "Rob", "O'Gallagher"); user.addEmail("ro'gallagher@example.com"); db.createUser(user, "j7hyqpassX"); } @Test(expected = InvalidScimResourceException.class) public void cannotCreateUserWithNonAsciiUsername() { ScimUser user = new ScimUser(null, "joe$eph", "Jo", "User"); user.addEmail("jo@blah.com"); db.createUser(user, "j7hyqpassX"); } @Test(expected = IllegalArgumentException.class) public void cannotCreateScimUserWithEmptyEmail() { ScimUser user = new ScimUser(null, "joeyjoejoe", "joe", "young"); user.addEmail(""); } @Test(expected = InvalidScimResourceException.class) public void cannotPersistScimUserWithEmptyEmail() { ScimUser user = new ScimUser(null, "josephine", "Jo", "Jung"); List<ScimUser.Email> emails = new ArrayList<>(); ScimUser.Email email = new ScimUser.Email(); email.setValue(""); emails.add(email); user.setEmails(emails); db.createUser(user, "j7hyqpassX"); } @Test(expected = InvalidScimResourceException.class) public void cannotPersistScimUserWithEmptyandNonEmptyEmails() { ScimUser user = new ScimUser(null, "josephine", "Jo", "Jung"); List<ScimUser.Email> emails = new ArrayList<>(); ScimUser.Email email1 = new ScimUser.Email(); email1.setValue("sample@sample.com"); emails.add(email1); ScimUser.Email email2 = new ScimUser.Email(); email2.setValue(""); emails.add(email2); user.setEmails(emails); db.createUser(user, "j7hyqpassX"); } @Test public void canReadScimUserWithMissingEmail() { // Create a user with no email address, reflecting previous behavior JdbcScimUserProvisioning noValidateProvisioning = new JdbcScimUserProvisioning(jdbcTemplate, pagingListFactory) { @Override protected void validate(ScimUser user) throws InvalidScimResourceException { return; } @Override public ScimUser retrieve(String id) { ScimUser createdUserId = new ScimUser(); createdUserId.setId(id); return createdUserId; } }; ScimUser nohbdy = spy(new ScimUser(null, "nohbdy", "Missing", "Email")); ScimUser.Email emptyEmail = new ScimUser.Email(); emptyEmail.setValue(""); when(nohbdy.getEmails()).thenReturn(Collections.singletonList(emptyEmail)); when(nohbdy.getPrimaryEmail()).thenReturn(""); nohbdy.setUserType(UaaAuthority.UAA_ADMIN.getUserType()); nohbdy.setSalt("salt"); nohbdy.setPassword(generator.generate()); String createdUserId = noValidateProvisioning.create(nohbdy).getId(); db.retrieve(createdUserId); } @Test public void updateModifiesExpectedData() { ScimUser jo = new ScimUser(null, "josephine", "Jo", "NewUser"); jo.addEmail("jo@blah.com"); jo.setUserType(UaaAuthority.UAA_ADMIN.getUserType()); jo.setSalt("salt"); ScimUser joe = db.update(JOE_ID, jo); // Can change username assertEquals("josephine", joe.getUserName()); assertEquals("jo@blah.com", joe.getPrimaryEmail()); assertEquals("Jo", joe.getGivenName()); assertEquals("NewUser", joe.getFamilyName()); assertEquals(1, joe.getVersion()); assertEquals(JOE_ID, joe.getId()); assertNull(joe.getGroups()); assertEquals("salt", joe.getSalt()); } @Test public void updateWithEmptyPhoneListWorks() { ScimUser jo = new ScimUser(null, "josephine", "Jo", "NewUser"); PhoneNumber emptyNumber = new PhoneNumber(); jo.addEmail("jo@blah.com"); jo.setPhoneNumbers(new ArrayList<PhoneNumber>()); ScimUser joe = db.update(JOE_ID, jo); } @Test public void updateWithEmptyPhoneNumberWorks() { ScimUser jo = new ScimUser(null, "josephine", "Jo", "NewUser"); PhoneNumber emptyNumber = new PhoneNumber(); jo.addEmail("jo@blah.com"); jo.setPhoneNumbers(Arrays.asList(emptyNumber)); ScimUser joe = db.update(JOE_ID, jo); } @Test public void updateWithWhiteSpacePhoneNumberWorks() { ScimUser jo = new ScimUser(null, "josephine", "Jo", "NewUser"); PhoneNumber emptyNumber = new PhoneNumber(); emptyNumber.setValue(" "); jo.addEmail("jo@blah.com"); jo.setPhoneNumbers(Arrays.asList(emptyNumber)); ScimUser joe = db.update(JOE_ID, jo); } @Test public void updateCannotModifyGroups() { ScimUser jo = new ScimUser(null, "josephine", "Jo", "NewUser"); jo.addEmail("jo@blah.com"); jo.setGroups(Collections.singleton(new Group(null, "dash/user"))); ScimUser joe = db.update(JOE_ID, jo); assertEquals(JOE_ID, joe.getId()); assertNull(joe.getGroups()); } @Test(expected = OptimisticLockingFailureException.class) public void updateWithWrongVersionIsError() { ScimUser jo = new ScimUser(null, "josephine", "Jo", "NewUser"); jo.addEmail("jo@blah.com"); jo.setVersion(1); ScimUser joe = db.update(JOE_ID, jo); assertEquals("joe", joe.getUserName()); } @Test(expected = InvalidScimResourceException.class) public void updateWithBadUsernameIsError() { ScimUser jo = db.retrieve(JOE_ID); jo.setUserName("jo$ephione"); db.update(JOE_ID, jo); } @Test public void updateWithBadUsernameIsOk_For_Non_UAA() { ScimUser jo = new ScimUser(null, "jo$ephine", "Jo", "NewUser"); jo.setOrigin(OriginKeys.LDAP); jo.addEmail("jo@blah.com"); ScimUser joe = db.update(JOE_ID, jo); assertEquals("jo$ephine", joe.getUserName()); assertEquals(OriginKeys.LDAP, joe.getOrigin()); } /* * @Test(expected = InvalidScimResourceException.class) * public void updateWithCapitalLetterInUsernameIsError() throws Exception { * ScimUser jo = new ScimUser(null, "joSephine", "Jo", "NewUser"); * jo.addEmail("jo@blah.com"); * jo.setVersion(1); * ScimUser joe = db.update(JOE_ID, jo); * assertEquals("joe", joe.getUserId()); * } */ @Test public void canChangePasswordWithoutOldPassword() throws Exception { db.changePassword(JOE_ID, null, "koala123$marissa"); String storedPassword = jdbcTemplate.queryForObject("SELECT password from users where ID=?", String.class, JOE_ID); assertTrue(BCrypt.checkpw("koala123$marissa", storedPassword)); } @Test public void canChangePasswordWithCorrectOldPassword() throws Exception { db.changePassword(JOE_ID, "joespassword", "koala123$marissa"); String storedPassword = jdbcTemplate.queryForObject("SELECT password from users where ID=?", String.class, JOE_ID); assertTrue(BCrypt.checkpw("koala123$marissa", storedPassword)); } @Test(expected = BadCredentialsException.class) public void cannotChangePasswordNonexistentUser() { db.changePassword(JOE_ID, "notjoespassword", "newpassword"); } @Test(expected = ScimResourceNotFoundException.class) public void cannotChangePasswordIfOldPasswordDoesntMatch() { db.changePassword("9999", null, "newpassword"); } @Test public void canRetrieveExistingUser() { ScimUser joe = db.retrieve(JOE_ID); assertJoe(joe); } @Test(expected = ScimResourceNotFoundException.class) public void cannotRetrieveNonexistentUser() { ScimUser joe = db.retrieve("9999"); assertJoe(joe); } @Test public void canDeactivateExistingUser() { String tmpUserId = createUserForDelete(); ScimUser deletedUser = db.delete(tmpUserId, 0); assertEquals(1, jdbcTemplate.queryForList("select * from users where id=? and active=?", tmpUserId, false).size()); assertFalse(deletedUser.isActive()); assertEquals(1, db.query("username eq \"" + tmpUserId + "\" and active eq false").size()); removeUser(tmpUserId); } @Test(expected = ScimResourceAlreadyExistsException.class) public void cannotDeactivateExistingUserAndThenCreateHimAgain() { String tmpUserId = createUserForDelete(); ScimUser deletedUser = db.delete(tmpUserId, 0); deletedUser.setActive(true); try { db.createUser(deletedUser, "foobarspam1234"); } catch (ScimResourceAlreadyExistsException e) { removeUser(tmpUserId); throw e; } } @Test(expected = ScimResourceNotFoundException.class) public void cannotDeactivateNonexistentUser() { ScimUser joe = db.delete("9999", 0); assertJoe(joe); } @Test(expected = OptimisticLockingFailureException.class) public void deactivateWithWrongVersionIsError() { ScimUser joe = db.delete(JOE_ID, 1); assertJoe(joe); } @Test public void canDeleteExistingUserThroughEvent() { String tmpUserId = createUserForDelete(); ScimUser user = db.retrieve(tmpUserId); db.setDeactivateOnDelete(false); db.onApplicationEvent(new EntityDeletedEvent<Object>(user, mock(Authentication.class))); assertEquals(0, jdbcTemplate.queryForList("select * from users where id=?", tmpUserId).size()); assertEquals(0, db.query("username eq \"" + tmpUserId + "\"").size()); } @Test public void canDeleteExistingUser() { String tmpUserId = createUserForDelete(); db.setDeactivateOnDelete(false); db.delete(tmpUserId, 0); assertEquals(0, jdbcTemplate.queryForList("select * from users where id=?", tmpUserId).size()); assertEquals(0, db.query("username eq \"" + tmpUserId + "\"").size()); } @Test // (expected = ScimResourceAlreadyExistsException.class) public void canDeleteExistingUserAndThenCreateHimAgain() { String tmpUserId = createUserForDelete(); db.setDeactivateOnDelete(false); ScimUser deletedUser = db.delete(tmpUserId, 0); assertEquals(0, jdbcTemplate.queryForList("select * from users where id=?", tmpUserId).size()); deletedUser.setActive(true); ScimUser user = db.createUser(deletedUser, "foobarspam1234"); assertNotNull(user); assertNotNull(user.getId()); assertNotSame(tmpUserId, user.getId()); assertEquals(1, db.query("username eq \"" + tmpUserId + "\"").size()); removeUser(user.getId()); } @Test public void testCreatedUserNotVerified() { String tmpUserIdString = createUserForDelete(); boolean verified = jdbcTemplate.queryForObject(VERIFY_USER_SQL_FORMAT, Boolean.class, tmpUserIdString); assertFalse(verified); ScimUser user = db.retrieve(tmpUserIdString); assertFalse(user.isVerified()); removeUser(tmpUserIdString); } @Test public void testCreateUserWithDuplicateUsername() throws Exception { addUser("cba09242-aa43-4247-9aa0-b5c75c281f94", "user@example.com", "password", "user@example.com", "first", "user", "90438", defaultIdentityProviderId, "uaa"); ScimUser scimUser = new ScimUser("user-id-2", "user@example.com", "User", "Example"); ScimUser.Email email = new ScimUser.Email(); email.setValue("user@example.com"); scimUser.setEmails(Arrays.asList(email)); scimUser.setPassword("password"); try { db.create(scimUser); fail("Should have thrown an exception"); } catch (ScimResourceAlreadyExistsException e) { Map<String,Object> userDetails = new HashMap<>(); userDetails.put("active", true); userDetails.put("verified", false); userDetails.put("user_id", "cba09242-aa43-4247-9aa0-b5c75c281f94"); assertEquals(HttpStatus.CONFLICT, e.getStatus()); assertEquals("Username already in use: user@example.com", e.getMessage()); assertEquals(userDetails, e.getExtraInfo()); } } @Test public void testCreateUserCheckSalt() throws Exception { ScimUser scimUser = new ScimUser("user-id-3", "user3@example.com", "User", "Example"); ScimUser.Email email = new ScimUser.Email(); email.setValue("user@example.com"); scimUser.setEmails(Arrays.asList(email)); scimUser.setPassword("password"); scimUser.setSalt("salt"); scimUser = db.create(scimUser); assertNotNull(scimUser); assertEquals("salt", scimUser.getSalt()); scimUser.setSalt("newsalt"); scimUser = db.update(scimUser.getId(), scimUser); assertNotNull(scimUser); assertEquals("newsalt", scimUser.getSalt()); } @Test public void testUpdateUserPasswordDoesntChange() throws Exception { String username = "user-"+new RandomValueStringGenerator().generate()+"@test.org"; ScimUser scimUser = new ScimUser(null, username, "User", "Example"); ScimUser.Email email = new ScimUser.Email(); email.setValue(username); scimUser.setEmails(Arrays.asList(email)); scimUser.setSalt("salt"); scimUser = db.createUser(scimUser, "password"); assertNotNull(scimUser); assertEquals("salt", scimUser.getSalt()); scimUser.setSalt("newsalt"); String passwordHash = jdbcTemplate.queryForObject("select password from users where id=?",new Object[] {scimUser.getId()}, String.class); assertNotNull(passwordHash); db.changePassword(scimUser.getId(), null, "password"); assertEquals(passwordHash, jdbcTemplate.queryForObject("select password from users where id=?", new Object[]{scimUser.getId()}, String.class)); db.changePassword(scimUser.getId(), "password", "password"); assertEquals(passwordHash, jdbcTemplate.queryForObject("select password from users where id=?",new Object[] {scimUser.getId()}, String.class)); } @Test public void testCreateUserWithDuplicateUsernameInOtherIdp() throws Exception { addUser("cba09242-aa43-4247-9aa0-b5c75c281f94", "user@example.com", "password", "user@example.com", "first", "user", "90438", defaultIdentityProviderId, "uaa"); String origin = "test-origin"; createOtherIdentityProvider(origin, IdentityZone.getUaa().getId()); ScimUser scimUser = new ScimUser(null, "user@example.com", "User", "Example"); ScimUser.Email email = new ScimUser.Email(); email.setValue("user@example.com"); scimUser.setEmails(Arrays.asList(email)); scimUser.setPassword("password"); scimUser.setOrigin(origin); String userId2 = db.create(scimUser).getId(); assertNotNull(userId2); assertNotEquals("cba09242-aa43-4247-9aa0-b5c75c281f94", userId2); } @Test public void testUpdatedUserVerified() { String tmpUserIdString = createUserForDelete(); boolean verified = jdbcTemplate.queryForObject(VERIFY_USER_SQL_FORMAT, Boolean.class, tmpUserIdString); assertFalse(verified); db.verifyUser(tmpUserIdString, -1); verified = jdbcTemplate.queryForObject(VERIFY_USER_SQL_FORMAT, Boolean.class, tmpUserIdString); assertTrue(verified); removeUser(tmpUserIdString); } @Test public void createUserWithNoZoneDefaultsToUAAZone() { String id = UUID.randomUUID().toString(); jdbcTemplate.execute(String.format(OLD_ADD_USER_SQL_FORMAT, id, "test-username", "password", "test@email.com", "givenName", "familyName", "1234567890")); ScimUser user = db.retrieve(id); assertEquals("uaa", user.getZoneId()); assertNull(user.getSalt()); } @Test(expected=DuplicateKeyException.class) public void createUserWithNoZoneFailsIfUserAlreadyExistsInUaaZone() { addUser(UUID.randomUUID().toString(), "test-username", "password", "test@email.com", "givenName", "familyName", "1234567890", defaultIdentityProviderId, "uaa"); jdbcTemplate.execute(String.format(OLD_ADD_USER_SQL_FORMAT, UUID.randomUUID().toString(), "test-username", "password", "test@email.com", "givenName", "familyName", "1234567890")); } @Test public void testUpdatedVersionedUserVerified() { String tmpUserIdString = createUserForDelete(); ScimUser user = db.retrieve(tmpUserIdString); assertFalse(user.isVerified()); user = db.verifyUser(tmpUserIdString, user.getVersion()); assertTrue(user.isVerified()); removeUser(tmpUserIdString); } @Test public void testUserVerifiedThroughUpdate() { String tmpUserIdString = createUserForDelete(); ScimUser user = db.retrieve(tmpUserIdString); assertFalse(user.isVerified()); user.setVerified(true); user = db.update(tmpUserIdString, user); assertTrue(user.isVerified()); removeUser(tmpUserIdString); } @Test(expected = ScimResourceNotFoundException.class) public void testUserVerifiedInvalidUserId() { String tmpUserIdString = createUserForDelete(); try { ScimUser user = db.retrieve(tmpUserIdString); assertFalse(user.isVerified()); user = db.verifyUser("-1-1-1", -1); assertTrue(user.isVerified()); } finally { removeUser(tmpUserIdString); } } @Test(expected = ScimResourceNotFoundException.class) public void testUserUpdateInvalidUserId() { String tmpUserIdString = createUserForDelete(); try { ScimUser user = db.retrieve(tmpUserIdString); assertFalse(user.isVerified()); user.setVerified(true); user = db.update("-1-1-1", user); assertTrue(user.isVerified()); } finally { removeUser(tmpUserIdString); } } @Test(expected = OptimisticLockingFailureException.class) public void testUpdatedIncorrectVersionUserVerified() { String tmpUserIdString = createUserForDelete(); try { ScimUser user = db.retrieve(tmpUserIdString); assertFalse(user.isVerified()); user = db.verifyUser(tmpUserIdString, user.getVersion() + 50); assertTrue(user.isVerified()); } finally { removeUser(tmpUserIdString); } } @Test(expected = ScimResourceNotFoundException.class) public void cannotDeleteNonexistentUser() { db.setDeactivateOnDelete(false); ScimUser joe = db.delete("9999", 0); assertJoe(joe); } @Test(expected = OptimisticLockingFailureException.class) public void deleteWithWrongVersionIsError() { db.setDeactivateOnDelete(false); ScimUser joe = db.delete(JOE_ID, 1); assertJoe(joe); } @Test public void canRetrieveUsers() { assertTrue(2 <= db.retrieveAll().size()); } @Test public void canRetrieveUsersWithFilterExists() { assertTrue(2 <= db.query("username pr").size()); } @Test public void canRetrieveUsersWithFilterEquals() { assertEquals(1, db.query("username eq \"joe\"").size()); } @Test public void canRetrieveUsersWithFilterEqualsDoubleQuote() { assertEquals(1, db.query("username eq \"joe\"").size()); } @Test public void canRetrieveUsersWithFilterKeyCaseSensitivity() { // This actually depends on the RDBMS. assertEquals(1, db.query("USERNAME eq \"joe\"").size()); } @Test public void canRetrieveUsersWithFilterOperatorCaseSensitivity() { // This actually depends on the RDBMS. assertEquals(1, db.query("username EQ \"joe\"").size()); } @Test public void canRetrieveUsersWithFilterValueCaseSensitivity() { // This actually depends on the RDBMS. assertEquals(1, db.query("username eq \"Joe\"").size()); } @Test public void canRetrieveUsersWithFilterContains() { assertEquals(2, db.query("username co \"e\"").size()); } @Test public void canRetrieveUsersWithFilterStartsWith() { assertEquals(1, db.query("username sw \"joe\"").size()); } @Test public void canRetrieveUsersWithFilterGreater() { assertEquals(1 + existingUserCount, db.query("username gt \"joe\"").size()); } @Test public void canRetrieveUsersWithEmailFilter() { assertEquals(1, db.query("emails.value sw \"joe\"").size()); } @Test public void canRetrieveUsersWithGroupsFilter() { List<ScimUser> users = db.query("groups.display co \"uaa.user\""); assertEquals(2 + existingUserCount, users.size()); for (int i=0; i<users.size(); i++) { assertNotNull(users.get(i)); } } @Test public void canRetrieveUsersWithPhoneNumberFilter() { assertEquals(1, db.query("phoneNumbers.value sw \"+1-222\"").size()); } @Test public void canRetrieveUsersWithMetaVersionFilter() { assertEquals(1, db.query("userName eq \"joe\" and meta.version eq 0").size()); } @Test public void canRetrieveUsersWithMetaDateFilter() { assertEquals(2 + existingUserCount, db.query("meta.created gt \"1970-01-01T00:00:00.000Z\"").size()); } @Test public void canRetrieveUsersWithBooleanFilter() { assertEquals(2 + existingUserCount, db.query("username pr and active eq true").size()); } @Test public void canRetrieveUsersWithSortBy() { assertEquals(2 + existingUserCount, db.query("username pr", "username", true).size()); } @Test public void canRetrieveUsersWithSortByEmail() { assertEquals(2 + existingUserCount, db.query("username pr", "emails.value", true).size()); } @Test public void canRetrieveUsersWithFilterBooleanAnd() { assertEquals(2, db.query("username pr and emails.value co \".com\"").size()); } @Test public void canRetrieveUsersWithFilterBooleanOr() { assertEquals(2, db.query("username eq \"joe\" or emails.value co \".com\"").size()); } @Test public void canRetrieveUsersWithFilterBooleanOrMatchesSecond() { assertEquals(1, db.query("username eq \"foo\" or username eq \"joe\"").size()); } @Test(expected = IllegalArgumentException.class) public void cannotRetrieveUsersWithIllegalFilterField() { assertEquals(2, db.query("emails.type eq \"bar\"").size()); } @Test(expected = IllegalArgumentException.class) public void cannotRetrieveUsersWithIllegalPhoneNumberFilterField() { assertEquals(2, db.query("phoneNumbers.type eq \"bar\"").size()); } @Test(expected = IllegalArgumentException.class) public void cannotRetrieveUsersWithIllegalFilterQuotes() { assertEquals(2, db.query("username eq \"bar").size()); } @Test(expected = IllegalArgumentException.class) public void cannotRetrieveUsersWithNativeSqlInjectionAttack() { String password = jdbcTemplate.queryForObject("select password from users where username='joe'", String.class); assertNotNull(password); Collection<ScimUser> users = db.query("username=\"joe\"; select " + SQL_INJECTION_FIELDS + " from users where username='joe'"); assertEquals(password, users.iterator().next().getId()); } @Test(expected = IllegalArgumentException.class) public void cannotRetrieveUsersWithSqlInjectionAttackOnGt() { String password = jdbcTemplate.queryForObject("select password from users where username='joe'", String.class); assertNotNull(password); Collection<ScimUser> users = db.query("username gt \"h\"; select " + SQL_INJECTION_FIELDS + " from users where username='joe'"); assertEquals(password, users.iterator().next().getId()); } @Test(expected = IllegalArgumentException.class) public void cannotRetrieveUsersWithSqlInjectionAttack() { String password = jdbcTemplate.queryForObject("select password from users where username='joe'", String.class); assertNotNull(password); Collection<ScimUser> users = db.query("username eq \"joe\"; select " + SQL_INJECTION_FIELDS + " from users where username='joe'"); assertEquals(password, users.iterator().next().getId()); } @Test(expected = IllegalArgumentException.class) public void cannotRetrieveUsersWithAnotherSqlInjectionAttack() { String password = jdbcTemplate.queryForObject("select password from users where username='joe'", String.class); assertNotNull(password); Collection<ScimUser> users = db.query("username eq \"joe\"\"; select id from users where id='''; select " + SQL_INJECTION_FIELDS + " from users where username='joe'"); assertEquals(password, users.iterator().next().getId()); } @Test(expected = IllegalArgumentException.class) public void cannotRetrieveUsersWithYetAnotherSqlInjectionAttack() { String password = jdbcTemplate.queryForObject("select password from users where username='joe'", String.class); assertNotNull(password); Collection<ScimUser> users = db.query("username eq \"joe\"'; select " + SQL_INJECTION_FIELDS + " from users where username='joe''"); assertEquals(password, users.iterator().next().getId()); } @Test(expected = IllegalArgumentException.class) public void filterEqWithoutQuotesIsRejected() { db.query("username eq joe"); } @Test public void checkPasswordMatches_returnsTrue_PasswordMatches() { assertTrue(db.checkPasswordMatches(JOE_ID, "joespassword")); } @Test public void checkPasswordMatches_ReturnsFalse_newPasswordSameAsOld() { assertFalse(db.checkPasswordMatches(JOE_ID, "notjoepassword")); } @Test public void updateLastLogonTime() { ScimUser user = db.retrieve(JOE_ID); Long timeStampBeforeUpdate = user.getLastLogonTime(); assertNull(timeStampBeforeUpdate); db.updateLastLogonTime(JOE_ID); user = db.retrieve(JOE_ID); assertNotNull(user.getLastLogonTime()); } private void assertJoe(ScimUser joe) { assertNotNull(joe); assertEquals(JOE_ID, joe.getId()); assertEquals("Joe", joe.getGivenName()); assertEquals("User", joe.getFamilyName()); assertEquals("joe@joe.com", joe.getPrimaryEmail()); assertEquals("joe", joe.getUserName()); assertEquals("+1-222-1234567", joe.getPhoneNumbers().get(0).getValue()); assertNull(joe.getGroups()); } }