Invalidate user cache entries when email or username are different from storage

Closes #40085

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
Signed-off-by: Alexander Schwartz <aschwart@redhat.com>
Signed-off-by: Alexander Schwartz <alexander.schwartz@gmx.net>
Co-authored-by: Alexander Schwartz <aschwart@redhat.com>
Co-authored-by: Alexander Schwartz <alexander.schwartz@gmx.net>
This commit is contained in:
Pedro Igor
2025-06-17 17:44:01 -03:00
committed by GitHub
parent 01dcb7a87a
commit 0188d276d8
12 changed files with 798 additions and 272 deletions

View File

@@ -667,15 +667,8 @@ public class LDAPStorageProvider implements UserStorageProvider,
private void doImportUser(final RealmModel realm, final UserModel user, final LDAPObject ldapUser) {
user.setEnabled(true);
realm.getComponentsStream(model.getId(), LDAPStorageMapper.class.getName())
.sorted(ldapMappersComparator.sortDesc())
.forEachOrdered(mapperModel -> {
if (logger.isTraceEnabled()) {
logger.tracef("Using mapper %s during import user from LDAP", mapperModel);
}
LDAPStorageMapper ldapMapper = mapperManager.getMapper(mapperModel);
ldapMapper.onImportUserFromLDAP(ldapUser, user, realm, true);
});
importUserAttributes(realm, user, ldapUser);
String userDN = ldapUser.getDn().toString();
if (model.isImportEnabled()) user.setFederationLink(model.getId());
@@ -784,6 +777,7 @@ public class LDAPStorageProvider implements UserStorageProvider,
LDAPUtils.checkUuid(ldapUser, ldapIdentityStore.getConfig());
// If email attribute mapper is set to "Always Read Value From LDAP" the user may be in Keycloak DB with an old email address
if (ldapUser.getUuid().equals(user.getFirstAttribute(LDAPConstants.LDAP_ID))) {
importUserAttributes(realm, user, ldapUser);
return proxy(realm, user, ldapUser, false);
}
throw new ModelDuplicateException("User with username '" + ldapUsername + "' already exists in Keycloak. It conflicts with LDAP user with email '" + email + "'");
@@ -1240,4 +1234,16 @@ public class LDAPStorageProvider implements UserStorageProvider,
}
return LDAPUtils.generalizedTimeToDate(value).getTime();
}
private void importUserAttributes(RealmModel realm, UserModel user, LDAPObject ldapUser) {
realm.getComponentsStream(model.getId(), LDAPStorageMapper.class.getName())
.sorted(ldapMappersComparator.sortDesc())
.forEachOrdered(mapperModel -> {
if (logger.isTraceEnabled()) {
logger.tracef("Using mapper %s during import user from LDAP", mapperModel);
}
LDAPStorageMapper ldapMapper = mapperManager.getMapper(mapperModel);
ldapMapper.onImportUserFromLDAP(ldapUser, user, realm, true);
});
}
}

View File

@@ -288,6 +288,22 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
super.setEmailVerified(verified);
}
@Override
public String getUsername() {
if (UserModel.USERNAME.equals(userModelAttrName)) {
return ldapUser.getAttributeAsString(ldapAttrName);
}
return super.getUsername();
}
@Override
public String getEmail() {
if (UserModel.EMAIL.equals(userModelAttrName)) {
return ldapUser.getAttributeAsString(ldapAttrName);
}
return super.getEmail();
}
protected boolean setLDAPAttribute(String modelAttrName, Object value) {
if (modelAttrName.equalsIgnoreCase(userModelAttrName)) {
if (UserAttributeLDAPStorageMapper.logger.isTraceEnabled()) {
@@ -506,11 +522,18 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
userModelProperty.setValue(user, null);
} else {
Class<Object> clazz = userModelProperty.getJavaClass();
Object currentValue = userModelProperty.getValue(user);
if (String.class.equals(clazz)) {
if (ldapAttrValue.equals(currentValue)) {
return;
}
userModelProperty.setValue(user, ldapAttrValue);
} else if (Boolean.class.equals(clazz) || boolean.class.equals(clazz)) {
Boolean boolVal = Boolean.valueOf(ldapAttrValue);
if (boolVal.equals(currentValue)) {
return;
}
userModelProperty.setValue(user, boolVal);
} else {
logger.warnf("Don't know how to set the property '%s' on user '%s' . Value of LDAP attribute is '%s' ", userModelProperty.getName(), user.getUsername(), ldapAttrValue.toString());

View File

@@ -200,7 +200,7 @@ public abstract class CacheManager {
private void put(String id, Revisioned object, long lifespan) {
if (lifespan < 0) {
cache.putForExternalRead(id, object);
} else {
} else if (lifespan > 0) {
cache.putForExternalRead(id, object, lifespan, TimeUnit.MILLISECONDS);
}
}

View File

@@ -1261,8 +1261,6 @@ public class RealmCacheSession implements CacheRealmProvider {
}
ClientStorageProviderModel model = new ClientStorageProviderModel(component);
// although we do set a timeout, Infinispan has no guarantees when the user will be evicted
// its also hard to test stuff
if (model.shouldInvalidate(cached)) {
registerClientInvalidation(cached.getId(), cached.getClientId(), realm.getId());
return getClientDelegate().getClientById(realm, cached.getId());

View File

@@ -17,6 +17,7 @@
package org.keycloak.models.cache.infinispan;
import static java.util.Optional.ofNullable;
import static org.keycloak.organization.utils.Organizations.isReadOnlyOrganizationMember;
import org.jboss.logging.Logger;
@@ -254,9 +255,12 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC
}
@Override
public UserModel getUserByUsername(RealmModel realm, String username) {
public UserModel getUserByUsername(RealmModel realm, String rawUsername) {
if (rawUsername == null) {
return null;
}
String username = rawUsername.toLowerCase();
logger.tracev("getUserByUsername: {0}", username);
username = username.toLowerCase();
if (realmInvalidations.contains(realm.getId())) {
logger.tracev("realmInvalidations");
return getDelegate().getUserByUsername(realm, username);
@@ -268,7 +272,6 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC
}
UserListQuery query = cache.get(cacheKey, UserListQuery.class);
String userId = null;
if (query == null) {
logger.tracev("query null");
Long loaded = cache.getCurrentRevision(cacheKey);
@@ -277,7 +280,7 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC
logger.tracev("model from delegate null");
return null;
}
userId = model.getId();
String userId = model.getId();
if (invalidations.contains(userId)) return model;
if (managedUsers.containsKey(userId)) {
logger.tracev("return managed user");
@@ -287,20 +290,59 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC
UserModel adapter = getUserAdapter(realm, userId, loaded, model);
if (adapter instanceof UserAdapter) { // this was cached, so we can cache query too
query = new UserListQuery(loaded, cacheKey, realm, model.getId());
cache.addRevisioned(query, startupRevision);
cache.addRevisioned(query, startupRevision, getLifespan(realm, adapter));
}
managedUsers.put(userId, adapter);
return adapter;
} else {
userId = query.getUsers().iterator().next();
if (invalidations.contains(userId)) {
logger.tracev("invalidated cache return delegate");
return getDelegate().getUserByUsername(realm, username);
}
logger.trace("return getUserById");
return getUserById(realm, userId);
}
String userId = query.getUsers().iterator().next();
if (invalidations.contains(userId)) {
logger.tracev("invalidated cache return delegate");
return getDelegate().getUserByUsername(realm, username);
}
logger.trace("return getUserById");
return ofNullable(getUserById(realm, userId))
// Validate for cases where the cached elements are not in sync.
// This might happen to changes in a federated store where caching is enabled and different items expire at different times,
// for example when they are evicted due to the limited size of the cache
.filter((u) -> username.equalsIgnoreCase(u.getUsername()))
.orElseGet(() -> {
registerInvalidation(cacheKey);
return getDelegate().getUserByUsername(realm, username);
});
}
private long getLifespan(RealmModel realm, UserModel user) {
if (!user.isFederated()) {
return -1; // cache infinite
}
String providerId = user.getFederationLink();
if (providerId == null) {
providerId = StorageId.providerId(user.getId());
}
ComponentModel component = realm.getComponent(providerId);
UserStorageProviderModel model = new UserStorageProviderModel(component);
if (model.isEnabled()) {
UserStorageProviderModel.CachePolicy policy = model.getCachePolicy();
if (policy == null) {
// no policy set, cache entries by default
return -1;
}
if (!UserStorageProviderModel.CachePolicy.NO_CACHE.equals(policy)) {
long lifespan = model.getLifespan();
return lifespan > 0 ? lifespan : -1;
}
}
return 0; // do not cache
}
protected UserModel getUserAdapter(RealmModel realm, String userId, Long loaded, UserModel delegate) {
@@ -327,8 +369,6 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC
}
CacheableStorageProviderModel model = new CacheableStorageProviderModel(component);
// although we do set a timeout, Infinispan has no guarantees when the user will be evicted
// its also hard to test stuff
if (model.shouldInvalidate(cached)) {
registerUserInvalidation(cached);
return supplier.get();
@@ -354,8 +394,9 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC
if (!model.isEnabled()) {
return new ReadOnlyUserModelDelegate(delegate, false);
}
UserStorageProviderModel.CachePolicy policy = model.getCachePolicy();
if (policy != null && policy == UserStorageProviderModel.CachePolicy.NO_CACHE) {
long lifespan = getLifespan(realm, delegate);
if (lifespan == 0) {
return delegate;
}
@@ -363,12 +404,7 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC
adapter = new UserAdapter(cached, this, session, realm);
onCache(realm, adapter, delegate);
long lifespan = model.getLifespan();
if (lifespan > 0) {
cache.addRevisioned(cached, startupRevision, lifespan);
} else {
cache.addRevisioned(cached, startupRevision);
}
cache.addRevisioned(cached, startupRevision, lifespan);
} else {
cached = new CachedUser(revision, realm, delegate, notBefore);
adapter = new UserAdapter(cached, this, session, realm);
@@ -384,9 +420,9 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC
}
@Override
public UserModel getUserByEmail(RealmModel realm, String email) {
if (email == null) return null;
email = email.toLowerCase();
public UserModel getUserByEmail(RealmModel realm, String rawEmail) {
if (rawEmail == null) return null;
String email = rawEmail.toLowerCase();
if (realmInvalidations.contains(realm.getId())) {
return getDelegate().getUserByEmail(realm, email);
}
@@ -396,30 +432,37 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC
}
UserListQuery query = cache.get(cacheKey, UserListQuery.class);
String userId = null;
if (query == null) {
Long loaded = cache.getCurrentRevision(cacheKey);
UserModel model = getDelegate().getUserByEmail(realm, email);
if (model == null) return null;
userId = model.getId();
String userId = model.getId();
if (invalidations.contains(userId)) return model;
if (managedUsers.containsKey(userId)) return managedUsers.get(userId);
UserModel adapter = getUserAdapter(realm, userId, loaded, model);
if (adapter instanceof UserAdapter) {
query = new UserListQuery(loaded, cacheKey, realm, model.getId());
cache.addRevisioned(query, startupRevision);
cache.addRevisioned(query, startupRevision, getLifespan(realm, adapter));
}
managedUsers.put(userId, adapter);
return adapter;
} else {
userId = query.getUsers().iterator().next();
if (invalidations.contains(userId)) {
return getDelegate().getUserByEmail(realm, email);
}
return getUserById(realm, userId);
}
String userId = query.getUsers().iterator().next();
if (invalidations.contains(userId)) {
return getDelegate().getUserByEmail(realm, email);
}
return ofNullable(getUserById(realm, userId))
// Validate for cases where the cached elements are not in sync.
// This might happen to changes in a federated store where caching is enabled and different items expire at different times,
// for example when they are evicted due to the limited size of the cache
.filter((u) -> email.equalsIgnoreCase(u.getEmail()))
.orElseGet(() -> {
registerInvalidation(cacheKey);
return getDelegate().getUserByEmail(realm, email);
});
}
@Override
@@ -453,7 +496,7 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC
UserModel adapter = getUserAdapter(realm, userId, loaded, model);
if (adapter instanceof UserAdapter) {
query = new UserListQuery(loaded, cacheKey, realm, model.getId());
cache.addRevisioned(query, startupRevision);
cache.addRevisioned(query, startupRevision, getLifespan(realm, adapter));
}
managedUsers.put(userId, adapter);
@@ -540,7 +583,7 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC
UserModel adapter = getUserAdapter(realm, userId, loaded, model);
if (adapter instanceof UserAdapter) { // this was cached, so we can cache query too
query = new UserListQuery(loaded, cacheKey, realm, model.getId());
cache.addRevisioned(query, startupRevision);
cache.addRevisioned(query, startupRevision, getLifespan(realm, adapter));
}
managedUsers.put(userId, adapter);
return adapter;
@@ -650,7 +693,7 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC
Set<FederatedIdentityModel> federatedIdentities = getDelegate().getFederatedIdentitiesStream(realm, user)
.collect(Collectors.toSet());
cachedLinks = new CachedFederatedIdentityLinks(loaded, cacheKey, realm, federatedIdentities);
cache.addRevisioned(cachedLinks, startupRevision);
cache.addRevisioned(cachedLinks, startupRevision); // this is Keycloak's internal store, cache indefinitely
return federatedIdentities.stream();
} else {
return cachedLinks.getFederatedIdentities().stream();
@@ -722,7 +765,7 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC
Long loaded = cache.getCurrentRevision(cacheKey);
cached = new CachedUserConsents(loaded, cacheKey, realm, consents, false);
cache.addRevisioned(cached, startupRevision);
cache.addRevisioned(cached, startupRevision); // this is from Keycloak's internal store, cache indefinitely
}
Map<String, CachedUserConsent> consents = cached.getConsents();
@@ -763,7 +806,7 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC
Long loaded = cache.getCurrentRevision(cacheKey);
List<UserConsentModel> consents = getDelegate().getConsentsStream(realm, userId).collect(Collectors.toList());
cached = new CachedUserConsents(loaded, cacheKey, realm, consents.stream().map(CachedUserConsent::new).collect(Collectors.toList()));
cache.addRevisioned(cached, startupRevision);
cache.addRevisioned(cached, startupRevision); // this is from Keycloak's internal store, cache indefinitely
return consents.stream();
} else {
return cached.getConsents().values().stream().map(cachedConsent -> toConsentModel(realm, cachedConsent))

View File

@@ -29,6 +29,8 @@ import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -113,7 +115,14 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
* @param user
* @return
*/
protected UserModel importValidation(RealmModel realm, UserModel user) {
protected UserModel validateUser(RealmModel realm, UserModel user) {
if (user == null) {
return null;
}
if (user.isFederated()) {
user = validateFederatedUser(realm, user);
}
if (isReadOnlyOrganizationMember(user)) {
if (user instanceof CachedUserModel cachedUserModel) {
@@ -122,14 +131,17 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
return new ReadOnlyUserModelDelegate(user, false);
}
if (user == null || !user.isFederated()) return user;
return user;
}
private UserModel validateFederatedUser(RealmModel realm, UserModel user) {
if (!user.isFederated()) {
return user;
}
UserStorageProviderModel model = getUserStorageProviderModel(realm, user);
UserStorageProviderModel model = getStorageProviderModel(realm, user.getFederationLink());
if (model == null) {
// remove linked user with unknown storage provider.
logger.debugf("Removed user with federation link of unknown storage provider '%s'", user.getUsername());
deleteInvalidUserCache(realm, user);
deleteInvalidUser(realm, user);
return null;
}
@@ -145,23 +157,59 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
return user;
}
ImportedUserValidation importedUserValidation = getStorageProviderInstance(model, ImportedUserValidation.class, true);
if (importedUserValidation == null) return user;
ImportedUserValidation validator = getStorageProviderInstance(model, ImportedUserValidation.class, true);
if (validator == null) {
return user;
}
UserModel validated = validator.validate(realm, user);
UserModel validated = importedUserValidation.validate(realm, user);
if (validated == null) {
deleteInvalidUserCache(realm, user);
if (model.isRemoveInvalidUsersEnabled()) {
deleteInvalidUser(realm, user);
return null;
}
return new ReadOnlyUserModelDelegate(user, false);
return deleteFederatedUser(realm, user);
}
return validated;
}
private ReadOnlyUserModelDelegate deleteFederatedUser(RealmModel realm, UserModel user) {
if (!user.isFederated()) {
return null;
}
UserStorageProviderModel model = getUserStorageProviderModel(realm, user);
if (model == null) {
return null;
}
deleteInvalidUserCache(realm, user);
if (model.isRemoveInvalidUsersEnabled()) {
deleteInvalidUser(realm, user);
return null;
}
return new ReadOnlyUserModelDelegate(user, false);
}
private UserStorageProviderModel getUserStorageProviderModel(RealmModel realm, UserModel user) {
if (user.isFederated()) {
UserStorageProviderModel model = getStorageProviderModel(realm, user.getFederationLink());
if (model == null) {
// remove linked user with unknown storage provider.
logger.debugf("Removed user with federation link of unknown storage provider '%s'", user.getUsername());
deleteInvalidUserCache(realm, user);
deleteInvalidUser(realm, user);
}
return model;
}
return null;
}
private static <T> Stream<T> getCredentialProviders(KeycloakSession session, Class<T> type) {
return session.getKeycloakSessionFactory().getProviderFactoriesStream(CredentialProvider.class)
.filter(f -> Types.supports(type, f, CredentialProviderFactory.class))
@@ -244,7 +292,7 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
}
protected Stream<UserModel> importValidation(RealmModel realm, Stream<UserModel> users) {
return users.map(user -> importValidation(realm, user)).filter(Objects::nonNull);
return users.map(user -> validateUser(realm, user)).filter(Objects::nonNull);
}
@FunctionalInterface
@@ -409,7 +457,7 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
StorageId storageId = new StorageId(id);
if (storageId.getProviderId() == null) {
UserModel user = localStorage().getUserById(realm, id);
return importValidation(realm, user);
return validateUser(realm, user);
}
UserLookupProvider provider = getStorageProviderInstance(realm, storageId.getProviderId(), UserLookupProvider.class);
@@ -420,28 +468,16 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
@Override
public UserModel getUserByUsername(RealmModel realm, String username) {
UserModel user = localStorage().getUserByUsername(realm, username);
if (user != null) {
return importValidation(realm, user);
}
return mapEnabledStorageProvidersWithTimeout(realm, UserLookupProvider.class,
provider -> provider.getUserByUsername(realm, username)).findFirst().orElse(null);
return getUserByAttribute(realm,
provider -> provider.getUserByUsername(realm, username),
u -> username.equalsIgnoreCase(u.getUsername()));
}
@Override
public UserModel getUserByEmail(RealmModel realm, String email) {
UserModel user = localStorage().getUserByEmail(realm, email);
if (user != null) {
user = importValidation(realm, user);
// Case when email was changed directly in the userStorage and doesn't correspond anymore to the email from local DB
if (user != null && email.equalsIgnoreCase(user.getEmail())) {
return user;
}
}
return mapEnabledStorageProvidersWithTimeout(realm, UserLookupProvider.class,
provider -> provider.getUserByEmail(realm, email)).findFirst().orElse(null);
return getUserByAttribute(realm,
provider -> provider.getUserByEmail(realm, email),
u -> email.equalsIgnoreCase(u.getEmail()));
}
/** {@link UserLookupProvider} methods implementations end here
@@ -807,7 +843,7 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
public UserModel getUserByFederatedIdentity(RealmModel realm, FederatedIdentityModel socialLink) {
UserModel user = localStorage().getUserByFederatedIdentity(realm, socialLink);
if (user != null) {
return importValidation(realm, user);
return validateUser(realm, user);
}
if (getFederatedStorage() == null) return null;
String id = getFederatedStorage().getUserByFederatedIdentity(socialLink, realm);
@@ -991,4 +1027,31 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
}
});
}
private UserModel getUserByAttribute(RealmModel realm, Function<UserLookupProvider, UserModel> loader, Predicate<UserModel> attributeValidator) {
// first try the local storage
UserModel user = loader.apply(localStorage());
if (user != null) {
// run global user validations
UserModel validated = validateUser(realm, user);
// make sure the attribute is valid
if (validated != null && attributeValidator.test(validated)) {
return validated;
}
// user or attribute not valid, invalidate cache
deleteInvalidUserCache(realm, user);
}
// try to resolve the user from the external storage
return tryResolveFederatedUser(realm, loader);
}
private UserModel tryResolveFederatedUser(RealmModel realm, Function<UserLookupProvider, UserModel> loader) {
return mapEnabledStorageProvidersWithTimeout(realm, UserLookupProvider.class, loader)
.findFirst()
.orElse(null);
}
}

View File

@@ -219,16 +219,13 @@ public class CacheableStorageProviderModel extends PrioritizedComponentModel {
public static long dailyTimeout(int hour, int minute) {
Calendar cal = Calendar.getInstance();
Calendar cal2 = Calendar.getInstance();
cal.setTimeInMillis(Time.currentTimeMillis());
cal2.setTimeInMillis(Time.currentTimeMillis());
cal2.set(Calendar.HOUR_OF_DAY, hour);
cal2.set(Calendar.MINUTE, minute);
if (cal2.getTimeInMillis() < cal.getTimeInMillis()) {
int add = (24 * 60 * 60 * 1000);
cal.add(Calendar.MILLISECOND, add);
} else {
cal = cal2;
cal.set(Calendar.HOUR_OF_DAY, hour);
cal.set(Calendar.MINUTE, minute);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
if (cal.getTimeInMillis() < Time.currentTimeMillis()) {
cal.add(Calendar.DAY_OF_YEAR, 1);
}
return cal.getTimeInMillis();
}
@@ -238,6 +235,8 @@ public class CacheableStorageProviderModel extends PrioritizedComponentModel {
cal.setTimeInMillis(Time.currentTimeMillis());
cal.set(Calendar.HOUR_OF_DAY, hour);
cal.set(Calendar.MINUTE, minute);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
if (cal.getTimeInMillis() > Time.currentTimeMillis()) {
// if daily evict for today hasn't happened yet set boundary
// to yesterday's time of eviction
@@ -248,18 +247,18 @@ public class CacheableStorageProviderModel extends PrioritizedComponentModel {
public static long weeklyTimeout(int day, int hour, int minute) {
Calendar cal = Calendar.getInstance();
Calendar cal2 = Calendar.getInstance();
cal.setTimeInMillis(Time.currentTimeMillis());
cal2.setTimeInMillis(Time.currentTimeMillis());
cal2.set(Calendar.HOUR_OF_DAY, hour);
cal2.set(Calendar.MINUTE, minute);
cal2.set(Calendar.DAY_OF_WEEK, day);
if (cal2.getTimeInMillis() < cal.getTimeInMillis()) {
cal.set(Calendar.HOUR_OF_DAY, hour);
cal.set(Calendar.MINUTE, minute);
cal.set(Calendar.DAY_OF_WEEK, day);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
if (cal.getTimeInMillis() < Time.currentTimeMillis()) {
int add = (7 * 24 * 60 * 60 * 1000);
cal2.add(Calendar.MILLISECOND, add);
cal.add(Calendar.MILLISECOND, add);
}
return cal2.getTimeInMillis();
return cal.getTimeInMillis();
}

View File

@@ -0,0 +1,166 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.federation.ldap;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.storage.UserStorageProviderModel;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.testsuite.util.LDAPRule;
import org.keycloak.testsuite.util.LDAPTestUtils;
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class LDAPExternalChangesTest extends AbstractLDAPTest {
@ClassRule
public static LDAPRule ldapRule = new LDAPRule();
@Override
protected LDAPRule getLDAPRule() {
return ldapRule;
}
@Override
protected void afterImportTestRealm() {
}
@Before
public void onBefore() {
testingClient.testing().setTestingInfinispanTimeService();
}
@After
public void onAfter() {
testingClient.testing().revertTestingInfinispanTimeService();
}
@Test
public void failAuthenticationIfEmailDifferentThanExternalStorage() {
testingClient.server().run((session) -> {
LDAPTestContext ctx = LDAPTestContext.init(session);
ctx.getLdapModel().setCachePolicy(UserStorageProviderModel.CachePolicy.MAX_LIFESPAN);
ctx.getLdapModel().setMaxLifespan(600000);
RealmModel realm = ctx.getRealm();
realm.updateComponent(ctx.getLdapModel());
LDAPObject john = LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), realm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234");
LDAPTestUtils.updateLDAPPassword(ctx.getLdapProvider(), john, "Password1");
realm.getClientByClientId("test-app").setDirectAccessGrantsEnabled(true);
});
String originalEmail = "john@email.org";
// import user from the ldap johnkeycloak and cache it reading it by id
List<UserRepresentation> users = testRealm().users().search("johnkeycloak", true);
assertEquals(1, users.size());
UserRepresentation user = users.get(0);
String userId = user.getId();
AccessTokenResponse tokenResponse = oauth.doPasswordGrantRequest(originalEmail, "Password1");
assertTrue(tokenResponse.isSuccess());
// modify the email of the user directly in ldap
String updatedEmail = "updatedjohnkeycloak@email.org";
testingClient.server().run(session -> {
LDAPTestContext ctx = LDAPTestContext.init(session);
LDAPObject johnLdapObject = ctx.getLdapProvider().loadLDAPUserByUsername(ctx.getRealm(), "johnkeycloak");
johnLdapObject.setSingleAttribute(LDAPConstants.EMAIL, updatedEmail);
ctx.getLdapProvider().getLdapIdentityStore().update(johnLdapObject);
});
tokenResponse = oauth.doPasswordGrantRequest(originalEmail, "Password1");
assertTrue(tokenResponse.isSuccess());
setTimeOffset(610);
tokenResponse = oauth.doPasswordGrantRequest(originalEmail, "Password1");
assertFalse(tokenResponse.isSuccess());
tokenResponse = oauth.doPasswordGrantRequest(updatedEmail, "Password1");
assertTrue(tokenResponse.isSuccess());
users = testRealm().users().search(originalEmail, true);
assertTrue(users.isEmpty());
users = testRealm().users().search("johnkeycloak", true);
assertEquals(1, users.size());
user = users.get(0);
assertEquals(userId, user.getId());
assertEquals(user.getEmail(), updatedEmail);
}
@Test
public void failAuthenticationIfUsernameDifferentThanExternalStorage() {
String originalUsername = "john";
testingClient.server().run((session) -> {
LDAPTestContext ctx = LDAPTestContext.init(session);
ctx.getLdapModel().setCachePolicy(UserStorageProviderModel.CachePolicy.MAX_LIFESPAN);
ctx.getLdapModel().setMaxLifespan(600000);
RealmModel realm = ctx.getRealm();
realm.updateComponent(ctx.getLdapModel());
LDAPObject john = LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), realm, originalUsername, "John", "Doe", "john@email.org", null, "1234");
LDAPTestUtils.updateLDAPPassword(ctx.getLdapProvider(), john, "Password1");
realm.getClientByClientId("test-app").setDirectAccessGrantsEnabled(true);
});
// import user from the ldap johnkeycloak and cache it reading it by id
List<UserRepresentation> users = testRealm().users().search(originalUsername, true);
assertEquals(1, users.size());
UserRepresentation user = users.get(0);
String userId = user.getId();
AccessTokenResponse tokenResponse = oauth.doPasswordGrantRequest(originalUsername, "Password1");
assertTrue(tokenResponse.isSuccess());
// modify the email of the user directly in ldap
String updatedUsername = "changed" + originalUsername;
testingClient.server().run(session -> {
LDAPTestContext ctx = LDAPTestContext.init(session);
LDAPObject johnLdapObject = ctx.getLdapProvider().loadLDAPUserByUsername(ctx.getRealm(), originalUsername);
johnLdapObject.setSingleAttribute(LDAPConstants.UID, updatedUsername);
ctx.getLdapProvider().getLdapIdentityStore().update(johnLdapObject);
});
tokenResponse = oauth.doPasswordGrantRequest(originalUsername, "Password1");
assertTrue(tokenResponse.isSuccess());
setTimeOffset(610);
tokenResponse = oauth.doPasswordGrantRequest(originalUsername, "Password1");
assertFalse(tokenResponse.isSuccess());
tokenResponse = oauth.doPasswordGrantRequest(updatedUsername, "Password1");
assertTrue(tokenResponse.isSuccess());
users = testRealm().users().search(originalUsername, true);
assertTrue(users.isEmpty());
users = testRealm().users().search(updatedUsername, true);
user = users.get(0);
assertEquals(userId, user.getId());
assertEquals(user.getUsername(), updatedUsername);
}
}

View File

@@ -1341,48 +1341,52 @@ public class LDAPProvidersIntegrationTest extends AbstractLDAPTest {
@Test
public void testLDAPUserRefreshCache() {
testingClient.server().run(session -> {
UserStorageUtil.userCache(session).clear();
});
try {
testingClient.testing().setTestingInfinispanTimeService();
testingClient.server().run(session -> {
UserStorageUtil.userCache(session).clear();
});
testingClient.server().run(session -> {
LDAPTestContext ctx = LDAPTestContext.init(session);
RealmModel appRealm = ctx.getRealm();
session.getContext().setRealm(appRealm);
testingClient.server().run(session -> {
LDAPTestContext ctx = LDAPTestContext.init(session);
RealmModel appRealm = ctx.getRealm();
session.getContext().setRealm(appRealm);
LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ctx.getLdapModel());
LDAPTestUtils.addLDAPUser(ldapProvider, appRealm, "johndirect", "John", "Direct", "johndirect@email.org", null, "1234");
LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ctx.getLdapModel());
LDAPTestUtils.addLDAPUser(ldapProvider, appRealm, "johndirect", "John", "Direct", "johndirect@email.org", null, "1234");
// Fetch user from LDAP and check that postalCode is filled
UserModel user = session.users().getUserByUsername(appRealm, "johndirect");
String postalCode = user.getFirstAttribute("postal_code");
Assert.assertEquals("1234", postalCode);
// Fetch user from LDAP and check that postalCode is filled
UserModel user = session.users().getUserByUsername(appRealm, "johndirect");
String postalCode = user.getFirstAttribute("postal_code");
Assert.assertEquals("1234", postalCode);
LDAPTestUtils.removeLDAPUserByUsername(ldapProvider, appRealm, ldapProvider.getLdapIdentityStore().getConfig(), "johndirect");
});
LDAPTestUtils.removeLDAPUserByUsername(ldapProvider, appRealm, ldapProvider.getLdapIdentityStore().getConfig(), "johndirect");
});
setTimeOffset(60 * 5); // 5 minutes in future, user should be cached still
setTimeOffset(60 * 5); // 5 minutes in future, user should be cached still
testingClient.server().run(session -> {
RealmModel appRealm = new RealmManager(session).getRealmByName("test");
session.getContext().setRealm(appRealm);
CachedUserModel user = (CachedUserModel) session.users().getUserByUsername(appRealm, "johndirect");
String postalCode = user.getFirstAttribute("postal_code");
String email = user.getEmail();
Assert.assertEquals("1234", postalCode);
Assert.assertEquals("johndirect@email.org", email);
});
testingClient.server().run(session -> {
RealmModel appRealm = new RealmManager(session).getRealmByName("test");
session.getContext().setRealm(appRealm);
CachedUserModel user = (CachedUserModel) session.users().getUserByUsername(appRealm, "johndirect");
String postalCode = user.getFirstAttribute("postal_code");
String email = user.getEmail();
Assert.assertEquals("1234", postalCode);
Assert.assertEquals("johndirect@email.org", email);
});
setTimeOffset(60 * 20); // 20 minutes into future, cache will be invalidated
setTimeOffset(60 * 20); // 20 minutes into future, cache will be invalidated
testingClient.server().run(session -> {
RealmModel appRealm = new RealmManager(session).getRealmByName("test");
session.getContext().setRealm(appRealm);
UserModel user = session.users().getUserByUsername(appRealm, "johndirect");
Assert.assertNull(user);
});
setTimeOffset(0);
testingClient.server().run(session -> {
RealmModel appRealm = new RealmManager(session).getRealmByName("test");
session.getContext().setRealm(appRealm);
UserModel user = session.users().getUserByUsername(appRealm, "johndirect");
Assert.assertNull(user);
});
} finally {
resetTimeOffset();
testingClient.testing().revertTestingInfinispanTimeService();
}
}
@Test
@@ -1447,6 +1451,7 @@ public class LDAPProvidersIntegrationTest extends AbstractLDAPTest {
@Test
public void testAlwaysReadValueFromLdapCached() throws Exception {
try {
testingClient.testing().setTestingInfinispanTimeService();
// import user from the ldap johnkeycloak and cache it reading it by id
List<UserRepresentation> users = testRealm().users().search("johnkeycloak", true);
Assert.assertEquals(1, users.size());
@@ -1489,6 +1494,8 @@ public class LDAPProvidersIntegrationTest extends AbstractLDAPTest {
johnLdapObject.setSingleAttribute(LDAPConstants.SN, "Doe");
ctx.getLdapProvider().getLdapIdentityStore().update(johnLdapObject);
});
resetTimeOffset();
testingClient.testing().revertTestingInfinispanTimeService();
}
}

View File

@@ -37,7 +37,9 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.storage.CacheableStorageProviderModel.CachePolicy;
import org.keycloak.storage.DatastoreProvider;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.StoreManagers;
import org.keycloak.storage.UserStoragePrivateUtil;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageUtil;
@@ -170,6 +172,7 @@ public class UserStorageTest extends AbstractAuthTest {
UserProfileResource userProfileRes = testRealmResource().users().userProfile();
UserProfileUtil.enableUnmanagedAttributes(userProfileRes);
testingClient.testing().setTestingInfinispanTimeService();
}
@After
@@ -191,6 +194,8 @@ public class UserStorageTest extends AbstractAuthTest {
Assert.assertNotNull(userMapStorageFactory);
userMapStorageFactory.clear();
});
resetTimeOffset();
testingClient.testing().revertTestingInfinispanTimeService();
}
protected ComponentRepresentation newPropProviderRW() {
@@ -595,125 +600,64 @@ public class UserStorageTest extends AbstractAuthTest {
// and they didn't use any time offset clock so they may have timestamps in the 'future'
// let's clear cache
testingClient.server().run(session -> {
UserStorageUtil.userCache(session).clear();
});
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
UserModel user = session.users().getUserByUsername(realm, "thor");
Assert.assertTrue(user instanceof CachedUserModel); // should be newly cached
});
clearUserCache();
setFirstname("thor", "Thor0");
// should be newly cached
validateFirstname("thor", "Thor0");
setFirstname("thor", "Thor1");
setTimeOfDay(23, 40, 0);
// lookup user again - make sure it's returned from cache
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
UserModel user = session.users().getUserByUsername(realm, "thor");
Assert.assertTrue(user instanceof CachedUserModel); // should be returned from cache
});
// should be returned from cache
validateFirstname("thor", "Thor0");
setTimeOfDay(23, 50, 0);
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
UserModel user = session.users().getUserByUsername(realm, "thor");
Assert.assertFalse(user instanceof CachedUserModel); // should have been invalidated
});
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
UserModel user = session.users().getUserByUsername(realm, "thor");
Assert.assertTrue(user instanceof CachedUserModel); // should have been newly cached
});
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
UserModel user = session.users().getUserByUsername(realm, "thor");
Assert.assertTrue(user instanceof CachedUserModel); // should be returned from cache
});
// should have been invalidated
validateFirstname("thor", "Thor1");
setFirstname("thor", "Thor2");
validateFirstname("thor", "Thor1"); // should be returned from cache
setTimeOfDay(23, 55, 0);
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
UserModel user = session.users().getUserByUsername(realm, "thor");
Assert.assertTrue(user instanceof CachedUserModel); // should be returned from cache
});
validateFirstname("thor", "Thor1"); // should be returned from cache
// at 00:30
// it's next day now. the daily eviction time is now in the future
setTimeOfDay(0, 30, 0, 24 * 60 * 60);
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
UserModel user = session.users().getUserByUsername(realm, "thor");
Assert.assertTrue(user instanceof CachedUserModel); // should be returned from cache - it's still good for almost the whole day
});
validateFirstname("thor", "Thor1"); // should be returned from cache - it's still good for almost the whole day
// at 23:30 next day
setTimeOfDay(23, 30, 0, 24 * 60 * 60);
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
UserModel user = session.users().getUserByUsername(realm, "thor");
Assert.assertTrue(user instanceof CachedUserModel); // should be returned from cache - it's still good until 23:45
});
validateFirstname("thor", "Thor1"); // should be returned from cache - it's still good until 23:45
// at 23:50
setTimeOfDay(23, 50, 0, 24 * 60 * 60);
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
UserModel user = session.users().getUserByUsername(realm, "thor");
Assert.assertFalse(user instanceof CachedUserModel); // should be invalidated
});
validateFirstname("thor", "Thor2"); // should be invalidated
setFirstname("thor", "Thor3");
setTimeOfDay(23, 55, 0, 24 * 60 * 60);
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
UserModel user = session.users().getUserByUsername(realm, "thor");
Assert.assertTrue(user instanceof CachedUserModel); // should be newly cached
});
validateFirstname("thor", "Thor2"); // should be newly cached
setTimeOfDay(23, 40, 0, 2 * 24 * 60 * 60);
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
UserModel user = session.users().getUserByUsername(realm, "thor");
Assert.assertTrue(user instanceof CachedUserModel); // should be returned from cache
});
validateFirstname("thor", "Thor2"); // should be returned from cache
setTimeOfDay(23, 50, 0, 2 * 24 * 60 * 60);
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
UserModel user = session.users().getUserByUsername(realm, "thor");
Assert.assertFalse(user instanceof CachedUserModel); // should be invalidated
});
validateFirstname("thor", "Thor3"); // should be invalidated
setFirstname("thor", "Thor4");
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
UserModel user = session.users().getUserByUsername(realm, "thor");
Assert.assertTrue(user instanceof CachedUserModel); // should be newly cached
});
validateFirstname("thor", "Thor3"); // should be newly cached
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
UserModel user = session.users().getUserByUsername(realm, "thor");
Assert.assertTrue(user instanceof CachedUserModel); // should be returned from cache
});
validateFirstname("thor", "Thor3"); // should be returned from cache
}
@Test
@@ -730,32 +674,21 @@ public class UserStorageTest extends AbstractAuthTest {
propProviderRW.getConfig().putSingle(EVICTION_MINUTE, Integer.toString(eviction.get(MINUTE)));
testRealmResource().components().component(propProviderRWId).update(propProviderRW);
clearUserCache();
setFirstname("thor", "Thor0");
// now
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
UserModel user = session.users().getUserByUsername(realm, "thor");
System.out.println("User class: " + user.getClass());
Assert.assertTrue(user instanceof CachedUserModel); // should still be cached
});
validateFirstname("thor", "Thor0"); // should still be cached
setFirstname("thor", "Thor1");
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(realm, "thor");
System.out.println("User class: " + user.getClass());
Assert.assertTrue(user instanceof CachedUserModel); // should still be cached
});
validateFirstname("thor", "Thor0"); // 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(realm, "thor");
System.out.println("User class: " + user.getClass());
Assert.assertFalse(user instanceof CachedUserModel); // should be evicted
});
validateFirstname("thor", "Thor1"); // should be evicted
}
@@ -769,31 +702,23 @@ public class UserStorageTest extends AbstractAuthTest {
propProviderRW.getConfig().putSingle(MAX_LIFESPAN, Long.toString(1 * 60 * 60 * 1000)); // 1 hour in milliseconds
testRealmResource().components().component(propProviderRWId).update(propProviderRW);
clearUserCache();
setFirstname("thor", "Thor0");
// now
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
UserModel user = session.users().getUserByUsername(realm, "thor");
System.out.println("User class: " + user.getClass());
Assert.assertTrue(user instanceof CachedUserModel); // should still be cached
});
validateFirstname("thor", "Thor0"); // Initial caching
setTimeOffset(1/2 * 60 * 60); // 1/2 hour in future
setFirstname("thor", "Thor1");
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
UserModel user = session.users().getUserByUsername(realm, "thor");
System.out.println("User class: " + user.getClass());
Assert.assertTrue(user instanceof CachedUserModel); // should still be cached
});
validateFirstname("thor", "Thor0"); // should still be cached
setTimeOffset(30 * 60); // 1/2 hour in future
validateFirstname("thor", "Thor0"); // 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(realm, "thor");
System.out.println("User class: " + user.getClass());
Assert.assertFalse(user instanceof CachedUserModel); // should be evicted
});
validateFirstname("thor", "Thor1"); // should be evicted
}
@@ -829,8 +754,8 @@ public class UserStorageTest extends AbstractAuthTest {
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
UserModel thor = session.users().getUserByUsername(realm, "thor");
System.out.println("Foo");
UserModel user = session.users().getUserByUsername(realm, "thor");
Assert.assertTrue(user instanceof CachedUserModel);
});
}
@@ -1182,4 +1107,27 @@ public class UserStorageTest extends AbstractAuthTest {
}
}
private void validateFirstname(String username, String firstname) {
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
UserModel user = session.users().getUserByUsername(realm, username);
MatcherAssert.assertThat(user.getFirstName(), Matchers.equalTo(firstname));
});
}
private void setFirstname(String username, String firstname) {
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName("test");
UserModel user = ((StoreManagers) session.getProvider(DatastoreProvider.class)).userStorageManager().getUserByUsername(realm, username);
user.setFirstName(firstname);
});
}
private void clearUserCache() {
testingClient.server().run(session -> {
UserStorageUtil.userCache(session).clear();
});
}
}

View File

@@ -0,0 +1,220 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.model.user;
import org.junit.Test;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RealmProvider;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.storage.CacheableStorageProviderModel;
import org.keycloak.storage.UserStoragePrivateUtil;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderFactory;
import org.keycloak.storage.UserStorageProviderModel;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.user.ImportSynchronization;
import org.keycloak.testsuite.model.KeycloakModelTest;
import org.keycloak.testsuite.model.RequireProvider;
import org.keycloak.testsuite.util.LDAPTestUtils;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assume.assumeThat;
import static org.keycloak.models.LDAPConstants.LDAP_ID;
@RequireProvider(UserProvider.class)
@RequireProvider(ClusterProvider.class)
@RequireProvider(RealmProvider.class)
@RequireProvider(value = UserStorageProvider.class, only = LDAPStorageProviderFactory.PROVIDER_NAME)
public class FederatedUserTest extends KeycloakModelTest {
private String realmId;
private String userFederationId;
@Override
protected boolean isUseSameKeycloakSessionFactoryForAllThreads() {
return true;
}
@Override
public void createEnvironment(KeycloakSession s) {
inComittedTransaction(session -> {
RealmModel realm = session.realms().createRealm("realm");
s.getContext().setRealm(realm);
realm.setDefaultRole(session.roles().addRealmRole(realm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + realm.getName()));
this.realmId = realm.getId();
});
getParameters(UserStorageProviderModel.class).forEach(fs -> inComittedTransaction(session -> {
if (userFederationId != null || !fs.isImportEnabled()) return;
RealmModel realm = session.realms().getRealm(realmId);
s.getContext().setRealm(realm);
fs.setParentId(realmId);
ComponentModel res = realm.addComponentModel(fs);
// Check if the provider implements ImportSynchronization interface
UserStorageProviderFactory userStorageProviderFactory = (UserStorageProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(UserStorageProvider.class, res.getProviderId());
if (!ImportSynchronization.class.isAssignableFrom(userStorageProviderFactory.getClass())) {
return;
}
userFederationId = res.getId();
log.infof("Added %s user federation provider: %s", fs.getName(), res.getId());
}));
assumeThat("Cannot run UserSyncTest because there is no user federation provider that supports sync", userFederationId, notNullValue());
}
@Override
public void cleanEnvironment(KeycloakSession s) {
final RealmModel realm = s.realms().getRealm(realmId);
s.getContext().setRealm(realm);
ComponentModel ldapModel = LDAPTestUtils.getLdapProviderModel(realm);
LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(s, ldapModel);
LDAPTestUtils.removeAllLDAPUsers(ldapFedProvider, realm);
s.realms().removeRealm(realmId);
}
private record TestContext (KeycloakSession session, RealmModel realm, String previousUserId, String previousLdapId) {};
private void assertAttributeDifferentThanExternalStorage(Consumer<TestContext> assertion) {
// create user1 in LDAP
String ldapId = withRealm(realmId, (session, realm) -> {
ComponentModel ldapModel = LDAPTestUtils.getLdapProviderModel(realm);
LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
LDAPObject ldapObject = LDAPTestUtils.addLDAPUser(ldapFedProvider, realm, "user1", "User1" + "FN", "User1" + "LN", "user1@email.org", "my-street 9", "12");
return ldapObject.getUuid();
});
// import user
String previous = withRealm(realmId, (session, realm) ->
session.users().getUserByUsername(realm, "user1").getId());
withRealm(realmId, (session, realm) -> {
ComponentModel ldapModel = LDAPTestUtils.getLdapProviderModel(realm);
LDAPStorageProvider provider = LDAPTestUtils.getLdapProvider(session, ldapModel);
LDAPObject ldapObject = provider.loadLDAPUserByUuid(realm, ldapId);
ldapObject.setSingleAttribute(LDAPConstants.UID, "changed");
provider.getLdapIdentityStore().update(ldapObject);
return null;
});
withRealm(realmId, (BiFunction<KeycloakSession, RealmModel, Void>) (session, realm) -> {
assertion.accept(new TestContext(session, realm, previous, ldapId));
return null;
});
}
@Test
public void testInvalidUsernameWhenDifferentThanExternalStorageNoCache() {
withRealm(realmId, (session, realm) -> {
UserStorageProviderModel providerModel = new UserStorageProviderModel(realm.getComponent(userFederationId));
providerModel.setCachePolicy(CacheableStorageProviderModel.CachePolicy.NO_CACHE);
realm.updateComponent(providerModel);
return null;
});
assertAttributeDifferentThanExternalStorage((context) -> {
KeycloakSession session = context.session();
RealmModel realm = context.realm();
UserModel cached = session.users().getUserByUsername(realm, "user1");
assertThat(cached, nullValue());
UserModel user = session.users().getUserByUsername(realm, "changed");
assertThat(user.getFirstAttribute(LDAP_ID), is(context.previousLdapId()));
assertThat(user.getId(), is(context.previousUserId()));
assertThat(user.getUsername(), is("changed"));
UserModel localUser = UserStoragePrivateUtil.userLocalStorage(session).getUserById(realm, context.previousUserId());
assertThat(localUser.getUsername(), is("changed"));
});
}
@Test
public void testInvalidUsernameWhenDifferentThanExternalStorageWithCache() {
withRealm(realmId, (session, realm) -> {
UserStorageProviderModel providerModel = new UserStorageProviderModel(realm.getComponent(userFederationId));
providerModel.setCachePolicy(CacheableStorageProviderModel.CachePolicy.DEFAULT);
realm.updateComponent(providerModel);
return null;
});
assertAttributeDifferentThanExternalStorage((context) -> {
KeycloakSession session = context.session();
RealmModel realm = context.realm();
// cache not yet invalidated, set a max lifespan if you want to eventually invalidate federated users
UserModel cached = session.users().getUserByUsername(realm, "user1");
assertThat(cached, notNullValue());
UserModel user = session.users().getUserByUsername(realm, "changed");
assertThat(user.getFirstAttribute(LDAP_ID), is(context.previousLdapId()));
assertThat(user.getId(), is(context.previousUserId()));
assertThat(user.getUsername(), is("changed"));
UserModel localUser = UserStoragePrivateUtil.userLocalStorage(session).getUserById(realm, context.previousUserId());
assertThat(localUser.getUsername(), is("changed"));
// cache now invalidated
cached = session.users().getUserByUsername(realm, "user1");
assertThat(cached, is(nullValue()));
});
}
@Test
public void testInvalidUsernameWhenDifferentThanExternalStorageWithCacheMaxLifespan() {
withRealm(realmId, (session, realm) -> {
UserStorageProviderModel providerModel = new UserStorageProviderModel(realm.getComponent(userFederationId));
providerModel.setCachePolicy(CacheableStorageProviderModel.CachePolicy.MAX_LIFESPAN);
providerModel.setMaxLifespan(60000);
realm.updateComponent(providerModel);
return null;
});
assertAttributeDifferentThanExternalStorage((context) -> {
KeycloakSession session = context.session();
RealmModel realm = context.realm();
UserModel cached = session.users().getUserByUsername(realm, "user1");
assertThat(cached, notNullValue());
setTimeOffset(120000);
cached = session.users().getUserByUsername(realm, "user1");
assertThat(cached, nullValue());
UserModel user = session.users().getUserByUsername(realm, "changed");
assertThat(user, notNullValue());
assertThat(user.getFirstAttribute(LDAP_ID), is(context.previousLdapId()));
assertThat(user.getId(), is(context.previousUserId()));
assertThat(user.getUsername(), is("changed"));
UserModel localUser = UserStoragePrivateUtil.userLocalStorage(session).getUserById(realm, context.previousUserId());
assertThat(localUser.getUsername(), is("changed"));
cached = session.users().getUserByUsername(realm, "user1");
assertThat(cached, nullValue());
});
}
}

View File

@@ -55,10 +55,12 @@ import java.util.stream.IntStream;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assume.assumeThat;
import static org.keycloak.models.LDAPConstants.LDAP_ID;
import static org.keycloak.storage.UserStorageProviderModel.REMOVE_INVALID_USERS_ENABLED;
@RequireProvider(UserProvider.class)
@@ -270,17 +272,17 @@ public class UserSyncTest extends KeycloakModelTest {
});
// import user
withRealm(realmId, (session, realm) -> {
String oldUserId = withRealm(realmId, (session, realm) -> {
UserModel user1 = session.users().getUserByUsername(realm, "user1");
user1.setSingleAttribute("LDAP_ID", "WRONG");
return user1;
return user1.getId();
});
// validate imported user
// validate imported user, user will be deleted and re-created
withRealm(realmId, (session, realm) -> {
assertThat(session.users().getUserByUsername(realm, "user1"), is(nullValue()));;
UserModel deletedUser = UserStoragePrivateUtil.userLocalStorage(session).getUserByUsername(realm, "user1");
assertThat(deletedUser, is(nullValue()));
UserModel user = session.users().getUserByUsername(realm, "user1");
assertThat(user, notNullValue());
assertThat(user.getId(), not(equalTo(oldUserId)));
return null;
});
}
@@ -344,5 +346,56 @@ public class UserSyncTest extends KeycloakModelTest {
return null;
});
}
@Test
public void testInvalidUsernameWhenDifferentThanExternalStorage() {
withRealm(realmId, (session, realm) -> {
UserStorageProviderModel providerModel = new UserStorageProviderModel(realm.getComponent(userFederationId));
providerModel.setCachePolicy(CacheableStorageProviderModel.CachePolicy.NO_CACHE);
providerModel.getConfig().putSingle(REMOVE_INVALID_USERS_ENABLED, Boolean.FALSE.toString()); // prevent local delete
realm.updateComponent(providerModel);
return null;
});
// create user1 in LDAP
String ldapId = withRealm(realmId, (session, realm) -> {
ComponentModel ldapModel = LDAPTestUtils.getLdapProviderModel(realm);
LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
int i = 1;
LDAPObject ldapObject = LDAPTestUtils.addLDAPUser(ldapFedProvider, realm, "user" + i, "User" + i + "FN", "User" + i + "LN", "user" + i + "@email.org", "my-street 9", "12" + i);
return ldapObject.getUuid();
});
// import user
String userId = withRealm(realmId, (session, realm) -> {
return session.users().getUserByUsername(realm, "user1").getId();
});
withRealm(realmId, (session, realm) -> {
ComponentModel ldapModel = LDAPTestUtils.getLdapProviderModel(realm);
LDAPStorageProvider provider = LDAPTestUtils.getLdapProvider(session, ldapModel);
LDAPObject ldapObject = provider.loadLDAPUserByUuid(realm, ldapId);
ldapObject.setSingleAttribute(LDAPConstants.UID, "changed");
provider.getLdapIdentityStore().update(ldapObject);
return null;
});
// user id changed, user cannot be resolved
withRealm(realmId, (session, realm) -> {
assertThat(session.users().getUserByUsername(realm, "user1"), nullValue());
return null;
});
// cache and local database reflecting the change in the database for the existing account
withRealm(realmId, (session, realm) -> {
UserModel user = session.users().getUserByUsername(realm, "changed");
assertThat(user.getFirstAttribute(LDAP_ID), is(ldapId));
assertThat(user.getId(), is(userId));
assertThat(user.getUsername(), is("changed"));
UserModel localUser = UserStoragePrivateUtil.userLocalStorage(session).getUserById(realm, userId);
assertThat(localUser.getUsername(), is("changed"));
return null;
});
}
}