/*******************************************************************************
* 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.user;
import org.cloudfoundry.identity.uaa.constants.OriginKeys;
import org.cloudfoundry.identity.uaa.test.JdbcTestBase;
import org.cloudfoundry.identity.uaa.test.TestUtils;
import org.cloudfoundry.identity.uaa.util.TimeService;
import org.cloudfoundry.identity.uaa.zone.IdentityZone;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Matchers;
import org.mockito.Mockito;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
import org.springframework.util.LinkedMultiValueMap;
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import static org.cloudfoundry.identity.uaa.user.JdbcUaaUserDatabase.DEFAULT_CASE_INSENSITIVE_USER_BY_EMAIL_AND_ORIGIN_QUERY;
import static org.cloudfoundry.identity.uaa.user.JdbcUaaUserDatabase.DEFAULT_CASE_INSENSITIVE_USER_BY_USERNAME_QUERY;
import static org.cloudfoundry.identity.uaa.user.JdbcUaaUserDatabase.DEFAULT_CASE_SENSITIVE_USER_BY_EMAIL_AND_ORIGIN_QUERY;
import static org.cloudfoundry.identity.uaa.user.JdbcUaaUserDatabase.DEFAULT_CASE_SENSITIVE_USER_BY_USERNAME_QUERY;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class JdbcUaaUserDatabaseTests extends JdbcTestBase {
private JdbcUaaUserDatabase db;
private static final String JOE_ID = "550e8400-e29b-41d4-a716-446655440000";
private static final String addUserSql = "insert into users (id, username, password, email, givenName, familyName, phoneNumber, origin, identity_zone_id, created, lastmodified, passwd_lastmodified, passwd_change_required) values (?,?,?,?,?,?,?,?,?,?,?,?,?)";
private static final String getAuthoritiesSql = "select authorities from users where id=?";
private static final String addAuthoritySql = "update users set authorities=? where id=?";
private static final String addSaltSql = "update users set salt=? where id=?";
private static final String MABEL_ID = UUID.randomUUID().toString();
private static final String ALICE_ID = UUID.randomUUID().toString();
private IdentityZone otherIdentityZone;
private JdbcTemplate template;
public static final String ADD_GROUP_SQL = "insert into groups (id, displayName, identity_zone_id) values (?,?,?)";
public static final String ADD_MEMBER_SQL = "insert into group_membership (group_id, member_id, member_type, authorities) values (?,?,?,?)";
private TimeService timeService;
private void addUser(String id, String name, String password, boolean requiresPasswordChange) {
TestUtils.assertNoSuchUser(template, "id", id);
Timestamp t = new Timestamp(System.currentTimeMillis());
template.update(addUserSql, id, name, password, name.toLowerCase() + "@test.org", name, name, "", OriginKeys.UAA, IdentityZoneHolder.get().getId(),t,t,t,requiresPasswordChange);
}
private void addAuthority(String authority, String userId) {
String id = new RandomValueStringGenerator().generate();
jdbcTemplate.update(ADD_GROUP_SQL, id, authority, IdentityZoneHolder.get().getId());
jdbcTemplate.update(ADD_MEMBER_SQL, id, userId, "USER", "MEMBER");
}
@Before
public void initializeDb() throws Exception {
timeService = mock(TimeService.class);
IdentityZoneHolder.clear();
otherIdentityZone = new IdentityZone();
otherIdentityZone.setId("some-other-zone-id");
template = new JdbcTemplate(dataSource);
db = new JdbcUaaUserDatabase(template, timeService);
db.setDefaultAuthorities(Collections.singleton("uaa.user"));
TestUtils.assertNoSuchUser(template, "id", JOE_ID);
TestUtils.assertNoSuchUser(template, "id", MABEL_ID);
TestUtils.assertNoSuchUser(template, "id", ALICE_ID);
TestUtils.assertNoSuchUser(template, "userName", "jo@foo.com");
addUser(JOE_ID, "Joe", "joespassword", true);
addUser(MABEL_ID, "mabel", "mabelspassword", false);
IdentityZoneHolder.set(otherIdentityZone);
addUser(ALICE_ID, "alice", "alicespassword", false);
IdentityZoneHolder.clear();
}
@After
public void clearDb() throws Exception {
IdentityZoneHolder.clear();
TestUtils.deleteFrom(dataSource, "users");
}
@Test(expected = NullPointerException.class)
public void testStoreUserInfoWithoutId() {
db.storeUserInfo(null, new UserInfo());
}
@Test
public void testStoreNullUserInfo() {
String id = "id";
db.storeUserInfo(id, null);
UserInfo info2 = db.getUserInfo(id);
assertNull(info2.getRoles());
assertNull(info2.getUserAttributes());
}
@Test
public void testStoreUserInfo() {
UserInfo info = new UserInfo();
String id = "id";
LinkedMultiValueMap<String, String> userAttributes = new LinkedMultiValueMap<>();
userAttributes.add("single", "1");
userAttributes.add("multi", "2");
userAttributes.add("multi", "3");
info.setUserAttributes(userAttributes);
List<String> roles = new LinkedList(Arrays.asList("role1", "role2", "role3"));
info.setRoles(roles);
db.storeUserInfo(id, info);
UserInfo info2 = db.getUserInfo(id);
assertEquals(info, info2);
assertEquals(userAttributes, info2.getUserAttributes());
assertEquals(roles, info2.getRoles());
roles.add("role4");
userAttributes.add("multi", "4");
db.storeUserInfo(id, info);
UserInfo info3 = db.getUserInfo(id);
assertEquals(info, info3);
assertEquals(userAttributes, info3.getUserAttributes());
assertEquals(roles, info3.getRoles());
}
@Test
public void addedUserHasNoLegacyVerificationBehavior() {
assertFalse(db.retrieveUserById(JOE_ID).isLegacyVerificationBehavior());
assertFalse(db.retrieveUserById(MABEL_ID).isLegacyVerificationBehavior());
IdentityZoneHolder.set(otherIdentityZone);
assertFalse(db.retrieveUserById(ALICE_ID).isLegacyVerificationBehavior());
}
@Test
public void getValidUserSucceeds() {
UaaUser joe = db.retrieveUserByName("joe", OriginKeys.UAA);
validateJoe(joe);
assertNull(joe.getSalt());
assertNotNull(joe.getPasswordLastModified());
assertEquals(joe.getCreated(), joe.getPasswordLastModified());
}
@Test
public void getSaltValueWorks() {
UaaUser joe = db.retrieveUserByName("joe", OriginKeys.UAA);
assertNotNull(joe);
assertNull(joe.getSalt());
template.update(addSaltSql, "salt", JOE_ID);
joe = db.retrieveUserByName("joe", OriginKeys.UAA);
assertNotNull(joe);
assertEquals("salt", joe.getSalt());
}
public boolean isMySQL() {
for (String s : environment.getActiveProfiles()) {
if (s.contains("mysql")) {
return true;
}
}
return false;
}
@Test
public void is_the_right_query_used() throws Exception {
JdbcTemplate template = mock(JdbcTemplate.class);
db.setJdbcTemplate(template);
String username = new RandomValueStringGenerator().generate()+"@test.org";
db.retrieveUserByName(username, OriginKeys.UAA);
verify(template).queryForObject(eq(DEFAULT_CASE_SENSITIVE_USER_BY_USERNAME_QUERY), eq(db.getMapper()), eq(username.toLowerCase()), eq(true), eq(OriginKeys.UAA), eq(OriginKeys.UAA));
db.retrieveUserByEmail(username, OriginKeys.UAA);
verify(template).query(eq(DEFAULT_CASE_SENSITIVE_USER_BY_EMAIL_AND_ORIGIN_QUERY), eq(db.getMapper()), eq(username.toLowerCase()), eq(true), eq(OriginKeys.UAA), eq(OriginKeys.UAA));
db.setCaseInsensitive(true);
db.retrieveUserByName(username, OriginKeys.UAA);
verify(template).queryForObject(eq(DEFAULT_CASE_INSENSITIVE_USER_BY_USERNAME_QUERY), eq(db.getMapper()), eq(username.toLowerCase()), eq(true), eq(OriginKeys.UAA), eq(OriginKeys.UAA));
db.retrieveUserByEmail(username, OriginKeys.UAA);
verify(template).query(eq(DEFAULT_CASE_INSENSITIVE_USER_BY_EMAIL_AND_ORIGIN_QUERY), eq(db.getMapper()), eq(username.toLowerCase()), eq(true), eq(OriginKeys.UAA), eq(OriginKeys.UAA));
}
@Test
public void getValidUserCaseInsensitive() {
for (boolean caseInsensitive : Arrays.asList(true,false)) {
try {
db.setCaseInsensitive(caseInsensitive);
UaaUser joe = db.retrieveUserByName("JOE", OriginKeys.UAA);
validateJoe(joe);
joe = db.retrieveUserByName("joe", OriginKeys.UAA);
validateJoe(joe);
joe = db.retrieveUserByName("Joe", OriginKeys.UAA);
validateJoe(joe);
joe = db.retrieveUserByEmail("joe@test.org", OriginKeys.UAA);
validateJoe(joe);
joe = db.retrieveUserByEmail("JOE@TEST.ORG", OriginKeys.UAA);
validateJoe(joe);
joe = db.retrieveUserByEmail("Joe@Test.Org", OriginKeys.UAA);
validateJoe(joe);
} catch (UsernameNotFoundException x) {
if (!caseInsensitive) {
throw x;
}
if (isMySQL()) {
throw x;
}
}
}
}
protected void validateJoe(UaaUser joe) {
assertNotNull(joe);
assertEquals(JOE_ID, joe.getId());
assertEquals("Joe", joe.getUsername());
assertEquals("joe@test.org", joe.getEmail());
assertEquals("joespassword", joe.getPassword());
assertEquals(true, joe.isPasswordChangeRequired());
assertTrue("authorities does not contain uaa.user",
joe.getAuthorities().contains(new SimpleGrantedAuthority("uaa.user")));
}
@Test(expected = UsernameNotFoundException.class)
public void getNonExistentUserRaisedNotFoundException() {
db.retrieveUserByName("jo", OriginKeys.UAA);
}
@Test
public void getUserWithExtraAuthorities() {
addAuthority("dash.admin", JOE_ID);
UaaUser joe = db.retrieveUserByName("joe", OriginKeys.UAA);
assertTrue("authorities does not contain uaa.user",
joe.getAuthorities().contains(new SimpleGrantedAuthority("uaa.user")));
assertTrue("authorities does not contain dash.admin",
joe.getAuthorities().contains(new SimpleGrantedAuthority("dash.admin")));
}
@Test
public void getUserWithMultipleExtraAuthorities() {
addAuthority("additional", JOE_ID);
addAuthority("anotherOne", JOE_ID);
JdbcTemplate spy = Mockito.spy(jdbcTemplate);
db.setJdbcTemplate(spy);
UaaUser joe = db.retrieveUserByName("joe", OriginKeys.UAA);
verify(spy, times(2)).queryForList(anyString(), Matchers.<String>anyVararg());
assertTrue("authorities does not contain uaa.user",
joe.getAuthorities().contains(new SimpleGrantedAuthority("uaa.user")));
assertTrue("authorities does not contain additional",
joe.getAuthorities().contains(new SimpleGrantedAuthority("additional")));
assertTrue("authorities does not contain anotherOne",
joe.getAuthorities().contains(new SimpleGrantedAuthority("anotherOne")));
}
@Test
public void getUserWithNestedAuthoritiesWorks() {
UaaUser joe = db.retrieveUserByName("joe", OriginKeys.UAA);
assertThat(joe.getAuthorities(),
containsInAnyOrder(
new SimpleGrantedAuthority("uaa.user")
)
);
String directId = new RandomValueStringGenerator().generate();
String indirectId = new RandomValueStringGenerator().generate();
jdbcTemplate.update(ADD_GROUP_SQL, directId, "direct", IdentityZoneHolder.get().getId());
jdbcTemplate.update(ADD_GROUP_SQL, indirectId, "indirect", IdentityZoneHolder.get().getId());
jdbcTemplate.update(ADD_MEMBER_SQL, indirectId, directId, "GROUP", "MEMBER");
jdbcTemplate.update(ADD_MEMBER_SQL, directId, joe.getId(), "USER", "MEMBER");
evaluateNestedJoe();
//add a circular group
jdbcTemplate.update(ADD_MEMBER_SQL, directId, indirectId, "GROUP", "MEMBER");
evaluateNestedJoe();
}
protected void evaluateNestedJoe() {
UaaUser joe;
joe = db.retrieveUserByName("joe", OriginKeys.UAA);
assertThat(joe.getAuthorities(),
containsInAnyOrder(
new SimpleGrantedAuthority("direct"),
new SimpleGrantedAuthority("uaa.user"),
new SimpleGrantedAuthority("indirect")
)
);
}
@Test
public void testUpdatePreviousAndLastLogonTime() {
when(timeService.getCurrentTimeMillis()).thenReturn(1000L);
db.updateLastLogonTime(JOE_ID);
UaaUser joe = db.retrieveUserById(JOE_ID);
assertEquals((long) joe.getLastLogonTime(), 1000L);
assertNull(joe.getPreviousLogonTime());
when(timeService.getCurrentTimeMillis()).thenReturn(2000L);
db.updateLastLogonTime(JOE_ID);
joe = db.retrieveUserById(JOE_ID);
assertEquals((long) joe.getPreviousLogonTime(), 1000L);
assertEquals((long) joe.getLastLogonTime(), 2000L);
}
@Test(expected = UsernameNotFoundException.class)
public void getValidUserInDefaultZoneFromOtherZoneFails() {
IdentityZoneHolder.set(otherIdentityZone);
getValidUserSucceeds();
fail("Should have thrown an exception.");
}
@Test
public void getValidUserInOtherZoneFromOtherZone() {
IdentityZoneHolder.set(otherIdentityZone);
getValidUserInOtherZoneFromDefaultZoneFails();
}
@Test(expected = UsernameNotFoundException.class)
public void getValidUserInOtherZoneFromDefaultZoneFails() {
db.retrieveUserByName("alice", OriginKeys.UAA);
}
@Test
public void retrieveUserByEmail_also_isCaseInsensitive() {
UaaUser joe = db.retrieveUserByEmail("JOE@test.org", OriginKeys.UAA);
validateJoe(joe);
assertNull(joe.getSalt());
assertNotNull(joe.getPasswordLastModified());
assertEquals(joe.getCreated(), joe.getPasswordLastModified());
}
@Test
public void null_if_noUserWithEmail() {
assertNull(db.retrieveUserByEmail("email@doesnot.exist", OriginKeys.UAA));
}
@Test
public void null_if_userWithEmail_in_differentZone(){
assertNull(db.retrieveUserByEmail("alice@test.org", OriginKeys.UAA));
}
}