package org.keycloak.testsuite.federation.storage; import java.io.File; import java.io.IOException; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import static java.util.Calendar.DAY_OF_WEEK; import static java.util.Calendar.HOUR_OF_DAY; import static java.util.Calendar.MINUTE; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.ws.rs.NotFoundException; import javax.ws.rs.core.Response; import org.apache.commons.io.FileUtils; import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.shrinkwrap.api.spec.WebArchive; import org.junit.After; import org.junit.Assert; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import static org.keycloak.models.UserModel.RequiredAction.UPDATE_PROFILE; import org.keycloak.models.cache.CachedUserModel; import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.representations.idm.GroupRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.storage.UserStorageProvider; import static org.keycloak.storage.UserStorageProviderModel.CACHE_POLICY; import org.keycloak.storage.UserStorageProviderModel.CachePolicy; import static org.keycloak.storage.UserStorageProviderModel.EVICTION_DAY; import static org.keycloak.storage.UserStorageProviderModel.EVICTION_HOUR; import static org.keycloak.storage.UserStorageProviderModel.EVICTION_MINUTE; import static org.keycloak.storage.UserStorageProviderModel.MAX_LIFESPAN; import org.keycloak.testsuite.AbstractAuthTest; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.federation.UserMapStorage; import org.keycloak.testsuite.federation.UserMapStorageFactory; import org.keycloak.testsuite.federation.UserPropertyFileStorageFactory; import org.keycloak.testsuite.runonserver.RunOnServerDeployment; import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlDoesntStartWith; import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith; /** * * @author tkyjovsk */ public class UserStorageTest extends AbstractAuthTest { private String memProviderId; private String propProviderROId; private String propProviderRWId; private static final File CONFIG_DIR = new File(System.getProperty("auth.server.config.dir", "")); @Before public void addProvidersBeforeTest() throws URISyntaxException, IOException { ComponentRepresentation memProvider = new ComponentRepresentation(); memProvider.setName("memory"); memProvider.setProviderId(UserMapStorageFactory.PROVIDER_ID); memProvider.setProviderType(UserStorageProvider.class.getName()); memProvider.setConfig(new MultivaluedHashMap<>()); memProvider.getConfig().putSingle("priority", Integer.toString(0)); memProviderId = addComponent(memProvider); // copy files used by the following RO/RW user providers File stResDir = new File(getClass().getResource("/storage-test").toURI()); if (stResDir.exists() && stResDir.isDirectory() && CONFIG_DIR.exists() && CONFIG_DIR.isDirectory()) { for (File f : stResDir.listFiles()) { log.infof("Copying %s to %s", f.getName(), CONFIG_DIR.getAbsolutePath()); FileUtils.copyFileToDirectory(f, CONFIG_DIR); } } else { throw new RuntimeException("Property `auth.server.config.dir` must be set to run UserStorageTests."); } ComponentRepresentation propProviderRO = new ComponentRepresentation(); propProviderRO.setName("read-only-user-props"); propProviderRO.setProviderId(UserPropertyFileStorageFactory.PROVIDER_ID); propProviderRO.setProviderType(UserStorageProvider.class.getName()); propProviderRO.setConfig(new MultivaluedHashMap<>()); propProviderRO.getConfig().putSingle("priority", Integer.toString(1)); propProviderRO.getConfig().putSingle("propertyFile", CONFIG_DIR.getAbsolutePath() + File.separator + "read-only-user-password.properties"); propProviderROId = addComponent(propProviderRO); propProviderRWId = addComponent(newPropProviderRW()); } @After public void removeTestUser() throws URISyntaxException, IOException { testingClient.server().run(session -> { RealmModel realm = session.realms().getRealmByName("test"); if (realm == null) { return; } UserModel user = session.users().getUserByUsername("thor", realm); if (user != null) { session.userLocalStorage().removeUser(realm, user); session.userCache().clear(); } }); } protected ComponentRepresentation newPropProviderRW() { ComponentRepresentation propProviderRW = new ComponentRepresentation(); propProviderRW.setName("user-props"); propProviderRW.setProviderId(UserPropertyFileStorageFactory.PROVIDER_ID); propProviderRW.setProviderType(UserStorageProvider.class.getName()); propProviderRW.setConfig(new MultivaluedHashMap<>()); propProviderRW.getConfig().putSingle("priority", Integer.toString(2)); propProviderRW.getConfig().putSingle("propertyFile", CONFIG_DIR.getAbsolutePath() + File.separator + "user-password.properties"); propProviderRW.getConfig().putSingle("federatedStorage", "true"); return propProviderRW; } protected String addComponent(ComponentRepresentation component) { Response resp = testRealmResource().components().add(component); resp.close(); String id = ApiUtil.getCreatedId(resp); getCleanup().addComponentId(id); return id; } private void loginSuccessAndLogout(String username, String password) { testRealmAccountPage.navigateTo(); testRealmLoginPage.form().login(username, password); assertCurrentUrlStartsWith(testRealmAccountPage); testRealmAccountPage.logOut(); } public void loginBadPassword(String username) { testRealmAccountPage.navigateTo(); testRealmLoginPage.form().login(username, "badpassword"); assertCurrentUrlDoesntStartWith(testRealmAccountPage); } // @Test public void listComponents() { log.info("COMPONENTS:"); testRealmResource().components().query().forEach((c) -> { log.infof("%s - %s - %s", c.getId(), c.getProviderType(), c.getName()); }); } @Test public void testLoginSuccess() { loginSuccessAndLogout("tbrady", "goat"); loginSuccessAndLogout("thor", "hammer"); loginBadPassword("tbrady"); } @Test public void testUpdate() { UserRepresentation thor = ApiUtil.findUserByUsername(testRealmResource(), "thor"); // update entity thor.setFirstName("Stian"); thor.setLastName("Thorgersen"); thor.setEmailVerified(true); long thorCreated = System.currentTimeMillis() - 100; thor.setCreatedTimestamp(thorCreated); thor.setEmail("thor@hammer.com"); thor.setAttributes(new HashMap<>()); thor.getAttributes().put("test-attribute", Arrays.asList("value")); thor.setRequiredActions(new ArrayList<>()); thor.getRequiredActions().add(UPDATE_PROFILE.name()); testRealmResource().users().get(thor.getId()).update(thor); // check entity thor = ApiUtil.findUserByUsername(testRealmResource(), "thor"); Assert.assertEquals("Stian", thor.getFirstName()); Assert.assertEquals("Thorgersen", thor.getLastName()); Assert.assertEquals("thor@hammer.com", thor.getEmail()); Assert.assertTrue(thor.getAttributes().containsKey("test-attribute")); Assert.assertEquals(1, thor.getAttributes().get("test-attribute").size()); Assert.assertEquals("value", thor.getAttributes().get("test-attribute").get(0)); Assert.assertTrue(thor.isEmailVerified()); // update group GroupRepresentation g = new GroupRepresentation(); g.setName("my-group"); String gid = ApiUtil.getCreatedId(testRealmResource().groups().add(g)); testRealmResource().users().get(thor.getId()).joinGroup(gid); // check group boolean foundGroup = false; for (GroupRepresentation ug : testRealmResource().users().get(thor.getId()).groups()) { if (ug.getId().equals(gid)) { foundGroup = true; } } Assert.assertTrue(foundGroup); // check required actions assertTrue(thor.getRequiredActions().contains(UPDATE_PROFILE.name())); // remove req. actions thor.getRequiredActions().remove(UPDATE_PROFILE.name()); testRealmResource().users().get(thor.getId()).update(thor); // change pass ApiUtil.resetUserPassword(testRealmResource().users().get(thor.getId()), "lightning", false); loginSuccessAndLogout("thor", "lightning"); // update role RoleRepresentation r = new RoleRepresentation("foo-role", "foo role", false); testRealmResource().roles().create(r); ApiUtil.assignRealmRoles(testRealmResource(), thor.getId(), "foo-role"); // check role boolean foundRole = false; for (RoleRepresentation rr : user(thor.getId()).roles().getAll().getRealmMappings()) { if ("foo-role".equals(rr.getName())) { foundRole = true; break; } } assertTrue(foundRole); // test removal of provider testRealmResource().components().component(propProviderRWId).remove(); propProviderRWId = addComponent(newPropProviderRW()); loginSuccessAndLogout("thor", "hammer"); thor = ApiUtil.findUserByUsername(testRealmResource(), "thor"); Assert.assertNull(thor.getFirstName()); Assert.assertNull(thor.getLastName()); Assert.assertNull(thor.getEmail()); Assert.assertNull(thor.getAttributes()); Assert.assertFalse(thor.isEmailVerified()); foundGroup = false; for (GroupRepresentation ug : testRealmResource().users().get(thor.getId()).groups()) { if (ug.getId().equals(gid)) { foundGroup = true; } } Assert.assertFalse(foundGroup); foundRole = false; for (RoleRepresentation rr : user(thor.getId()).roles().getAll().getRealmMappings()) { if ("foo-role".equals(rr.getName())) { foundRole = true; break; } } assertFalse(foundRole); } public UserResource user(String userId) { return testRealmResource().users().get(userId); } @Test public void testRegistration() { UserRepresentation memuser = new UserRepresentation(); memuser.setUsername("memuser"); String uid = ApiUtil.createUserAndResetPasswordWithAdminClient(testRealmResource(), memuser, "password"); loginSuccessAndLogout("memuser", "password"); loginSuccessAndLogout("memuser", "password"); loginSuccessAndLogout("memuser", "password"); memuser = user(uid).toRepresentation(); assertNotNull(memuser); assertNotNull(memuser.getOrigin()); ComponentRepresentation origin = testRealmResource().components().component(memuser.getOrigin()).toRepresentation(); Assert.assertEquals("memory", origin.getName()); testRealmResource().users().get(memuser.getId()).remove(); try { user(uid).toRepresentation(); // provider doesn't implement UserQueryProvider --> have to lookup by uid fail("`memuser` wasn't removed"); } catch (NotFoundException nfe) { // expected } } @Test public void testQuery() { Set<UserRepresentation> queried = new HashSet<>(); int first = 0; while (queried.size() < 8) { List<UserRepresentation> results = testRealmResource().users().search("", first, 3); log.debugf("first=%s, results: %s", first, results.size()); if (results.isEmpty()) { break; } first += results.size(); queried.addAll(results); } Set<String> usernames = new HashSet<>(); for (UserRepresentation user : queried) { usernames.add(user.getUsername()); log.info(user.getUsername()); } Assert.assertEquals(8, queried.size()); Assert.assertTrue(usernames.contains("thor")); Assert.assertTrue(usernames.contains("zeus")); Assert.assertTrue(usernames.contains("apollo")); Assert.assertTrue(usernames.contains("perseus")); Assert.assertTrue(usernames.contains("tbrady")); Assert.assertTrue(usernames.contains("rob")); Assert.assertTrue(usernames.contains("jules")); Assert.assertTrue(usernames.contains("danny")); // test searchForUser List<UserRepresentation> users = testRealmResource().users().search("tbrady", 0, Integer.MAX_VALUE); Assert.assertTrue(users.size() == 1); Assert.assertTrue(users.get(0).getUsername().equals("tbrady")); // test getGroupMembers() GroupRepresentation g = new GroupRepresentation(); g.setName("gods"); String gid = ApiUtil.getCreatedId(testRealmResource().groups().add(g)); UserRepresentation user = ApiUtil.findUserByUsername(testRealmResource(), "apollo"); testRealmResource().users().get(user.getId()).joinGroup(gid); user = ApiUtil.findUserByUsername(testRealmResource(), "zeus"); testRealmResource().users().get(user.getId()).joinGroup(gid); user = ApiUtil.findUserByUsername(testRealmResource(), "thor"); testRealmResource().users().get(user.getId()).joinGroup(gid); queried.clear(); usernames.clear(); first = 0; while (queried.size() < 8) { List<UserRepresentation> results = testRealmResource().groups().group(gid).members(first, 1); log.debugf("first=%s, results: %s", first, results.size()); if (results.isEmpty()) { break; } first += results.size(); queried.addAll(results); } for (UserRepresentation u : queried) { usernames.add(u.getUsername()); log.info(u.getUsername()); } Assert.assertEquals(3, queried.size()); Assert.assertTrue(usernames.contains("apollo")); Assert.assertTrue(usernames.contains("zeus")); Assert.assertTrue(usernames.contains("thor")); // search by single attribute // FIXME - no equivalent for model in REST } @Deployment public static WebArchive deploy() { return RunOnServerDeployment.create(UserResource.class) .addPackages(true, "org.keycloak.testsuite"); } @Test public void testDailyEviction() { ApiUtil.findUserByUsername(testRealmResource(), "thor"); // set eviction to 1 hour from now Calendar eviction = Calendar.getInstance(); eviction.add(Calendar.HOUR, 1); ComponentRepresentation propProviderRW = testRealmResource().components().component(propProviderRWId).toRepresentation(); propProviderRW.getConfig().putSingle(CACHE_POLICY, CachePolicy.EVICT_DAILY.name()); propProviderRW.getConfig().putSingle(EVICTION_HOUR, Integer.toString(eviction.get(HOUR_OF_DAY))); propProviderRW.getConfig().putSingle(EVICTION_MINUTE, Integer.toString(eviction.get(MINUTE))); testRealmResource().components().component(propProviderRWId).update(propProviderRW); // now testingClient.server().run(session -> { RealmModel realm = session.realms().getRealmByName("test"); UserModel user = session.users().getUserByUsername("thor", realm); System.out.println("User class: " + user.getClass()); Assert.assertTrue(user instanceof CachedUserModel); // should still be cached }); setTimeOffset(2 * 60 * 60); // 2 hours in future testingClient.server().run(session -> { RealmModel realm = session.realms().getRealmByName("test"); UserModel user = session.users().getUserByUsername("thor", realm); System.out.println("User class: " + user.getClass()); Assert.assertFalse(user instanceof CachedUserModel); // should be evicted }); } @Test public void testWeeklyEviction() { ApiUtil.findUserByUsername(testRealmResource(), "thor"); // set eviction to 4 days from now Calendar eviction = Calendar.getInstance(); eviction.add(Calendar.HOUR, 4 * 24); ComponentRepresentation propProviderRW = testRealmResource().components().component(propProviderRWId).toRepresentation(); propProviderRW.getConfig().putSingle(CACHE_POLICY, CachePolicy.EVICT_WEEKLY.name()); propProviderRW.getConfig().putSingle(EVICTION_DAY, Integer.toString(eviction.get(DAY_OF_WEEK))); propProviderRW.getConfig().putSingle(EVICTION_HOUR, Integer.toString(eviction.get(HOUR_OF_DAY))); propProviderRW.getConfig().putSingle(EVICTION_MINUTE, Integer.toString(eviction.get(MINUTE))); testRealmResource().components().component(propProviderRWId).update(propProviderRW); // now testingClient.server().run(session -> { RealmModel realm = session.realms().getRealmByName("test"); UserModel user = session.users().getUserByUsername("thor", realm); System.out.println("User class: " + user.getClass()); Assert.assertTrue(user instanceof CachedUserModel); // should still be cached }); setTimeOffset(2 * 24 * 60 * 60); // 2 days in future // now testingClient.server().run(session -> { RealmModel realm = session.realms().getRealmByName("test"); UserModel user = session.users().getUserByUsername("thor", realm); System.out.println("User class: " + user.getClass()); Assert.assertTrue(user instanceof CachedUserModel); // should still be cached }); setTimeOffset(5 * 24 * 60 * 60); // 5 days in future testingClient.server().run(session -> { RealmModel realm = session.realms().getRealmByName("test"); UserModel user = session.users().getUserByUsername("thor", realm); System.out.println("User class: " + user.getClass()); Assert.assertFalse(user instanceof CachedUserModel); // should be evicted }); } @Test public void testMaxLifespan() { ApiUtil.findUserByUsername(testRealmResource(), "thor"); // set eviction to 1 hour from now ComponentRepresentation propProviderRW = testRealmResource().components().component(propProviderRWId).toRepresentation(); propProviderRW.getConfig().putSingle(CACHE_POLICY, CachePolicy.MAX_LIFESPAN.name()); propProviderRW.getConfig().putSingle(MAX_LIFESPAN, Long.toString(1 * 60 * 60 * 1000)); // 1 hour in milliseconds testRealmResource().components().component(propProviderRWId).update(propProviderRW); // now testingClient.server().run(session -> { RealmModel realm = session.realms().getRealmByName("test"); UserModel user = session.users().getUserByUsername("thor", realm); System.out.println("User class: " + user.getClass()); Assert.assertTrue(user instanceof CachedUserModel); // should still be cached }); setTimeOffset(1/2 * 60 * 60); // 1/2 hour in future testingClient.server().run(session -> { RealmModel realm = session.realms().getRealmByName("test"); UserModel user = session.users().getUserByUsername("thor", realm); System.out.println("User class: " + user.getClass()); Assert.assertTrue(user instanceof CachedUserModel); // should still be cached }); setTimeOffset(2 * 60 * 60); // 2 hours in future testingClient.server().run(session -> { RealmModel realm = session.realms().getRealmByName("test"); UserModel user = session.users().getUserByUsername("thor", realm); System.out.println("User class: " + user.getClass()); Assert.assertFalse(user instanceof CachedUserModel); // should be evicted }); } @Test public void testNoCache() { ApiUtil.findUserByUsername(testRealmResource(), "thor"); // set NO_CACHE policy ComponentRepresentation propProviderRW = testRealmResource().components().component(propProviderRWId).toRepresentation(); propProviderRW.getConfig().putSingle(CACHE_POLICY, CachePolicy.NO_CACHE.name()); testRealmResource().components().component(propProviderRWId).update(propProviderRW); testingClient.server().run(session -> { RealmModel realm = session.realms().getRealmByName("test"); UserModel user = session.users().getUserByUsername("thor", realm); System.out.println("User class: " + user.getClass()); Assert.assertFalse(user instanceof CachedUserModel); // should be evicted }); } @Test public void testLifecycle() { testingClient.server().run(session -> { UserMapStorage.allocations.set(0); UserMapStorage.closings.set(0); RealmModel realm = session.realms().getRealmByName("test"); UserModel user = session.users().addUser(realm, "memuser"); Assert.assertNotNull(user); user = session.users().getUserByUsername("nonexistent", realm); Assert.assertNull(user); Assert.assertEquals(1, UserMapStorage.allocations.get()); Assert.assertEquals(0, UserMapStorage.closings.get()); }); testingClient.server().run(session -> { Assert.assertEquals(1, UserMapStorage.allocations.get()); Assert.assertEquals(1, UserMapStorage.closings.get()); }); } @Test public void testEntityRemovalHooks() { testingClient.server().run(session -> { UserMapStorage.realmRemovals.set(0); UserMapStorage.groupRemovals.set(0); UserMapStorage.roleRemovals.set(0); }); // remove group GroupRepresentation g1 = new GroupRepresentation(); g1.setName("group1"); GroupRepresentation g2 = new GroupRepresentation(); g2.setName("group2"); String gid1 = ApiUtil.getCreatedId(testRealmResource().groups().add(g1)); String gid2 = ApiUtil.getCreatedId(testRealmResource().groups().add(g2)); testRealmResource().groups().group(gid1).remove(); testRealmResource().groups().group(gid2).remove(); testingClient.server().run(session -> { Assert.assertEquals(2, UserMapStorage.groupRemovals.get()); UserMapStorage.realmRemovals.set(0); }); // remove role RoleRepresentation role1 = new RoleRepresentation(); role1.setName("role1"); RoleRepresentation role2 = new RoleRepresentation(); role2.setName("role2"); testRealmResource().roles().create(role1); testRealmResource().roles().create(role2); testRealmResource().roles().get("role1").remove(); testRealmResource().roles().get("role2").remove(); testingClient.server().run(session -> { Assert.assertEquals(2, UserMapStorage.roleRemovals.get()); UserMapStorage.realmRemovals.set(0); }); // remove realm RealmRepresentation testRealmRepresentation = testRealmResource().toRepresentation(); testRealmResource().remove(); testingClient.server().run(session -> { Assert.assertEquals(1, UserMapStorage.realmRemovals.get()); UserMapStorage.realmRemovals.set(0); }); // Re-create realm RealmRepresentation repOrig = testContext.getTestRealmReps().get(0); adminClient.realms().create(repOrig); } @Test @Ignore public void testEntityRemovalHooksCascade() { testingClient.server().run(session -> { UserMapStorage.realmRemovals.set(0); UserMapStorage.groupRemovals.set(0); UserMapStorage.roleRemovals.set(0); }); GroupRepresentation g1 = new GroupRepresentation(); g1.setName("group1"); GroupRepresentation g2 = new GroupRepresentation(); g2.setName("group2"); String gid1 = ApiUtil.getCreatedId(testRealmResource().groups().add(g1)); String gid2 = ApiUtil.getCreatedId(testRealmResource().groups().add(g2)); RoleRepresentation role1 = new RoleRepresentation(); role1.setName("role1"); RoleRepresentation role2 = new RoleRepresentation(); role2.setName("role2"); testRealmResource().roles().create(role1); testRealmResource().roles().create(role2); // remove realm with groups and roles in it testRealmResource().remove(); testingClient.server().run(session -> { Assert.assertEquals(1, UserMapStorage.realmRemovals.get()); Assert.assertEquals(2, UserMapStorage.groupRemovals.get()); // check if group removal hooks were called Assert.assertEquals(2, UserMapStorage.roleRemovals.get()); // check if role removal hooks were called }); } }