mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-30 11:29:57 -06:00
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:
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user