Merge pull request #478 from patriot1burke/master

user cache
This commit is contained in:
Bill Burke 2014-06-20 17:08:16 -04:00
commit 8bcb01fadb
12 changed files with 522 additions and 26 deletions

View file

@ -16,4 +16,6 @@ public interface CacheKeycloakSession extends KeycloakSession {
void registerRoleInvalidation(String id);
void registerOAuthClientInvalidation(String id);
void registerUserInvalidation(String id);
}

View file

@ -174,8 +174,9 @@ public abstract class ClientAdapter implements ClientModel {
public boolean hasScope(RoleModel role) {
if (updatedClient != null) return updatedClient.hasScope(role);
if (cachedClient.getScope().contains(role.getId())) return true;
Set<RoleModel> roles = getScopeMappings();
if (roles.contains(role)) return true;
for (RoleModel mapping : roles) {
if (mapping.hasRole(role)) return true;

View file

@ -17,6 +17,7 @@ import org.keycloak.models.cache.entities.CachedOAuthClient;
import org.keycloak.models.cache.entities.CachedRealm;
import org.keycloak.models.cache.entities.CachedRealmRole;
import org.keycloak.models.cache.entities.CachedRole;
import org.keycloak.models.cache.entities.CachedUser;
import org.keycloak.provider.ProviderSession;
import java.util.HashMap;
@ -41,10 +42,12 @@ public class DefaultCacheKeycloakSession implements CacheKeycloakSession {
protected Set<String> appInvalidations = new HashSet<String>();
protected Set<String> roleInvalidations = new HashSet<String>();
protected Set<String> clientInvalidations = new HashSet<String>();
protected Set<String> userInvalidations = new HashSet<String>();
protected Map<String, RealmModel> managedRealms = new HashMap<String, RealmModel>();
protected Map<String, ApplicationModel> managedApplications = new HashMap<String, ApplicationModel>();
protected Map<String, OAuthClientModel> managedClients = new HashMap<String, OAuthClientModel>();
protected Map<String, RoleModel> managedRoles = new HashMap<String, RoleModel>();
protected Map<String, UserModel> managedUsers = new HashMap<String, UserModel>();
protected boolean clearAll;
@ -88,6 +91,11 @@ public class DefaultCacheKeycloakSession implements CacheKeycloakSession {
clientInvalidations.add(id);
}
@Override
public void registerUserInvalidation(String id) {
userInvalidations.add(id);
}
protected void runInvalidations() {
for (String id : realmInvalidations) {
cache.invalidateCachedRealmById(id);
@ -101,6 +109,9 @@ public class DefaultCacheKeycloakSession implements CacheKeycloakSession {
for (String id : clientInvalidations) {
cache.invalidateCachedOAuthClientById(id);
}
for (String id : userInvalidations) {
cache.invalidateCachedUserById(id);
}
}
@ -210,17 +221,59 @@ public class DefaultCacheKeycloakSession implements CacheKeycloakSession {
@Override
public UserModel getUserById(String id, RealmModel realm) {
return getDelegate().getUserById(id, realm);
CachedUser cached = cache.getCachedUser(id);
if (cached == null) {
UserModel model = getDelegate().getUserById(id, realm);
if (model == null) return null;
if (userInvalidations.contains(id)) return model;
cached = new CachedUser(realm, model);
cache.addCachedUser(cached);
} else if (userInvalidations.contains(id)) {
return getDelegate().getUserById(id, realm);
} else if (managedUsers.containsKey(id)) {
return managedUsers.get(id);
}
UserAdapter adapter = new UserAdapter(cached, cache, this, realm);
managedUsers.put(id, adapter);
return adapter;
}
@Override
public UserModel getUserByUsername(String username, RealmModel realm) {
return getDelegate().getUserByUsername(username, realm);
CachedUser cached = cache.getCachedUserByUsername(username, realm);
if (cached == null) {
UserModel model = getDelegate().getUserByUsername(username, realm);
if (model == null) return null;
if (userInvalidations.contains(model.getId())) return model;
cached = new CachedUser(realm, model);
cache.addCachedUser(cached);
} else if (userInvalidations.contains(cached.getId())) {
return getDelegate().getUserById(cached.getId(), realm);
} else if (managedUsers.containsKey(cached.getId())) {
return managedUsers.get(cached.getId());
}
UserAdapter adapter = new UserAdapter(cached, cache, this, realm);
managedUsers.put(cached.getId(), adapter);
return adapter;
}
@Override
public UserModel getUserByEmail(String email, RealmModel realm) {
return getDelegate().getUserByEmail(email, realm);
CachedUser cached = cache.getCachedUserByEmail(email, realm);
if (cached == null) {
UserModel model = getDelegate().getUserByEmail(email, realm);
if (model == null) return null;
if (userInvalidations.contains(model.getId())) return model;
cached = new CachedUser(realm, model);
cache.addCachedUser(cached);
} else if (userInvalidations.contains(cached.getId())) {
return getDelegate().getUserByEmail(email, realm);
} else if (managedUsers.containsKey(cached.getId())) {
return managedUsers.get(cached.getId());
}
UserAdapter adapter = new UserAdapter(cached, cache, this, realm);
managedUsers.put(cached.getId(), adapter);
return adapter;
}
@Override

View file

@ -1,9 +1,11 @@
package org.keycloak.models.cache;
import org.keycloak.models.RealmModel;
import org.keycloak.models.cache.entities.CachedApplication;
import org.keycloak.models.cache.entities.CachedOAuthClient;
import org.keycloak.models.cache.entities.CachedRealm;
import org.keycloak.models.cache.entities.CachedRole;
import org.keycloak.models.cache.entities.CachedUser;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -48,4 +50,17 @@ public interface KeycloakCache {
void invalidateRoleById(String id);
CachedUser getCachedUser(String id);
void invalidateCachedUser(CachedUser user);
void addCachedUser(CachedUser user);
CachedUser getCachedUserByUsername(String name, RealmModel realm);
CachedUser getCachedUserByEmail(String name, RealmModel realm);
void invalidedCachedUserById(String id);
void invalidateCachedUserById(String id);
}

View file

@ -278,4 +278,9 @@ public class NoCacheKeycloakSession implements CacheKeycloakSession {
public void removeUserSessions(RealmModel realm) {
getDelegate().removeUserSessions(realm);
}
@Override
public void registerUserInvalidation(String id) {
//To change body of implemented methods use File | Settings | File Templates.
}
}

View file

@ -1,18 +1,15 @@
package org.keycloak.models.cache;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakTransaction;
import org.keycloak.models.RealmModel;
import org.keycloak.models.cache.entities.CachedApplication;
import org.keycloak.models.cache.entities.CachedOAuthClient;
import org.keycloak.models.cache.entities.CachedRealm;
import org.keycloak.models.cache.entities.CachedRole;
import org.keycloak.provider.ProviderSession;
import org.keycloak.provider.ProviderSessionFactory;
import org.keycloak.models.cache.entities.CachedUser;
import java.util.List;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
@ -27,6 +24,121 @@ public class SimpleCache implements KeycloakCache {
protected ConcurrentHashMap<String, CachedOAuthClient> clientCache = new ConcurrentHashMap<String, CachedOAuthClient>();
protected ConcurrentHashMap<String, CachedRole> roleCache = new ConcurrentHashMap<String, CachedRole>();
protected int maxUserCacheSize = 10000;
protected boolean userCacheEnabled = true;
protected Map<String, CachedUser> usersById = Collections.synchronizedMap(new LRUCache());
protected Map<String, CachedUser> usersByUsername = new ConcurrentHashMap<String, CachedUser>();
protected Map<String, CachedUser> usersByEmail = new ConcurrentHashMap<String, CachedUser>();
protected class LRUCache extends LinkedHashMap<String, CachedUser> {
public LRUCache() {
super(1000, 1.1F, true);
}
@Override
public CachedUser put(String key, CachedUser value) {
usersByUsername.put(value.getUsernameKey(), value);
if (value.getEmail() != null) {
usersByEmail.put(value.getEmailKey(), value);
}
return super.put(key, value);
}
@Override
public CachedUser remove(Object key) {
CachedUser user = super.remove(key);
if (user == null) return null;
removeUser(user);
return user;
}
@Override
public void clear() {
super.clear();
usersByUsername.clear();
usersByEmail.clear();
}
@Override
protected boolean removeEldestEntry(Map.Entry<String, CachedUser> eldest) {
boolean evict = size() > maxUserCacheSize;
if (evict) {
removeUser(eldest.getValue());
}
return evict;
}
private void removeUser(CachedUser value) {
usersByUsername.remove(value.getUsernameKey());
if (value.getEmail() != null) usersByEmail.remove(value.getEmailKey());
}
}
public int getMaxUserCacheSize() {
return maxUserCacheSize;
}
public void setMaxUserCacheSize(int maxUserCacheSize) {
this.maxUserCacheSize = maxUserCacheSize;
}
public boolean isUserCacheEnabled() {
return userCacheEnabled;
}
public void setUserCacheEnabled(boolean userCacheEnabled) {
this.userCacheEnabled = userCacheEnabled;
}
@Override
public CachedUser getCachedUser(String id) {
if (!userCacheEnabled) return null;
return usersById.get(id);
}
@Override
public void invalidateCachedUser(CachedUser user) {
if (!userCacheEnabled) return;
usersById.remove(user.getId());
}
@Override
public void invalidateCachedUserById(String id) {
if (!userCacheEnabled) return;
usersById.remove(id);
}
@Override
public void addCachedUser(CachedUser user) {
if (!userCacheEnabled) return;
usersById.put(user.getId(), user);
}
@Override
public CachedUser getCachedUserByUsername(String name, RealmModel realm) {
if (!userCacheEnabled) return null;
CachedUser user = usersByUsername.get(realm.getId() + "." +name);
if (user == null) return null;
usersById.get(user.getId()); // refresh cache entry age
return user;
}
@Override
public CachedUser getCachedUserByEmail(String name, RealmModel realm) {
if (!userCacheEnabled) return null;
CachedUser user = usersByEmail.get(realm.getId() + "." +name);
if (user == null) return null;
usersById.get(user.getId()); // refresh cache entry age
return user;
}
@Override
public void invalidedCachedUserById(String id) {
if (!userCacheEnabled) return;
usersById.remove(id);
}
@Override
public void clear() {
realmCache.clear();
@ -34,6 +146,7 @@ public class SimpleCache implements KeycloakCache {
applicationCache.clear();
clientCache.clear();
roleCache.clear();
usersById.clear();
}
@Override

View file

@ -0,0 +1,283 @@
package org.keycloak.models.cache;
import org.keycloak.models.ApplicationModel;
import org.keycloak.models.AuthenticationLinkModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.cache.entities.CachedUser;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class UserAdapter implements UserModel {
protected UserModel updated;
protected CachedUser cached;
protected KeycloakCache cache;
protected CacheKeycloakSession cacheSession;
protected RealmModel realm;
public UserAdapter(CachedUser cached, KeycloakCache cache, CacheKeycloakSession session, RealmModel realm) {
this.cached = cached;
this.cache = cache;
this.cacheSession = session;
this.realm = realm;
}
protected void getDelegateForUpdate() {
if (updated == null) {
cacheSession.registerUserInvalidation(getId());
updated = cacheSession.getDelegate().getUserById(getId(), realm);
if (updated == null) throw new IllegalStateException("Not found in database");
}
}
@Override
public String getId() {
if (updated != null) return updated.getId();
return cached.getId();
}
@Override
public String getLoginName() {
if (updated != null) return updated.getLoginName();
return cached.getLoginName();
}
@Override
public void setLoginName(String loginName) {
getDelegateForUpdate();
updated.setLoginName(loginName);
}
@Override
public boolean isEnabled() {
if (updated != null) return updated.isEnabled();
return cached.isEnabled();
}
@Override
public boolean isTotp() {
if (updated != null) return updated.isTotp();
return cached.isTotp();
}
@Override
public void setEnabled(boolean enabled) {
getDelegateForUpdate();
updated.setEnabled(enabled);
}
@Override
public void setAttribute(String name, String value) {
getDelegateForUpdate();
updated.setAttribute(name, value);
}
@Override
public void removeAttribute(String name) {
getDelegateForUpdate();
updated.removeAttribute(name);
}
@Override
public String getAttribute(String name) {
if (updated != null) return updated.getAttribute(name);
return cached.getAttributes().get(name);
}
@Override
public Map<String, String> getAttributes() {
if (updated != null) return updated.getAttributes();
return cached.getAttributes();
}
@Override
public Set<RequiredAction> getRequiredActions() {
if (updated != null) return updated.getRequiredActions();
return cached.getRequiredActions();
}
@Override
public void addRequiredAction(RequiredAction action) {
getDelegateForUpdate();
updated.addRequiredAction(action);
}
@Override
public void removeRequiredAction(RequiredAction action) {
getDelegateForUpdate();
updated.removeRequiredAction(action);
}
@Override
public String getFirstName() {
if (updated != null) return updated.getFirstName();
return cached.getFirstName();
}
@Override
public void setFirstName(String firstName) {
getDelegateForUpdate();
updated.setFirstName(firstName);
}
@Override
public String getLastName() {
if (updated != null) return updated.getLastName();
return cached.getLastName();
}
@Override
public void setLastName(String lastName) {
getDelegateForUpdate();
updated.setLastName(lastName);
}
@Override
public String getEmail() {
if (updated != null) return updated.getEmail();
return cached.getEmail();
}
@Override
public void setEmail(String email) {
getDelegateForUpdate();
updated.setEmail(email);
}
@Override
public boolean isEmailVerified() {
if (updated != null) return updated.isEmailVerified();
return cached.isEmailVerified();
}
@Override
public void setEmailVerified(boolean verified) {
getDelegateForUpdate();
updated.setEmailVerified(verified);
}
@Override
public void setTotp(boolean totp) {
getDelegateForUpdate();
updated.setTotp(totp);
}
@Override
public int getNotBefore() {
if (updated != null) return updated.getNotBefore();
return cached.getNotBefore();
}
@Override
public void setNotBefore(int notBefore) {
getDelegateForUpdate();
updated.setNotBefore(notBefore);
}
@Override
public void updateCredential(UserCredentialModel cred) {
getDelegateForUpdate();
updated.updateCredential(cred);
}
@Override
public List<UserCredentialValueModel> getCredentialsDirectly() {
if (updated != null) return updated.getCredentialsDirectly();
return cached.getCredentials();
}
@Override
public void updateCredentialDirectly(UserCredentialValueModel cred) {
getDelegateForUpdate();
updated.updateCredentialDirectly(cred);
}
@Override
public AuthenticationLinkModel getAuthenticationLink() {
if (updated != null) return updated.getAuthenticationLink();
return cached.getAuthenticationLink();
}
@Override
public void setAuthenticationLink(AuthenticationLinkModel authenticationLink) {
getDelegateForUpdate();
updated.setAuthenticationLink(authenticationLink);
}
@Override
public Set<RoleModel> getRealmRoleMappings() {
if (updated != null) return updated.getRealmRoleMappings();
Set<RoleModel> roleMappings = getRoleMappings();
Set<RoleModel> realmMappings = new HashSet<RoleModel>();
for (RoleModel role : roleMappings) {
RoleContainerModel container = role.getContainer();
if (container instanceof RealmModel) {
if (((RealmModel) container).getId().equals(realm.getId())) {
realmMappings.add(role);
}
}
}
return realmMappings;
}
@Override
public Set<RoleModel> getApplicationRoleMappings(ApplicationModel app) {
if (updated != null) return updated.getApplicationRoleMappings(app);
Set<RoleModel> roleMappings = getRoleMappings();
Set<RoleModel> appMappings = new HashSet<RoleModel>();
for (RoleModel role : roleMappings) {
RoleContainerModel container = role.getContainer();
if (container instanceof ApplicationModel) {
if (((ApplicationModel) container).getId().equals(app.getId())) {
appMappings.add(role);
}
}
}
return appMappings;
}
@Override
public boolean hasRole(RoleModel role) {
if (updated != null) return updated.hasRole(role);
if (cached.getRoleMappings().contains(role.getId())) return true;
Set<RoleModel> mappings = getRoleMappings();
for (RoleModel mapping: mappings) {
if (mapping.hasRole(role)) return true;
}
return false;
}
@Override
public void grantRole(RoleModel role) {
getDelegateForUpdate();
updated.grantRole(role);
}
@Override
public Set<RoleModel> getRoleMappings() {
if (updated != null) return updated.getRoleMappings();
Set<RoleModel> roles = new HashSet<RoleModel>();
for (String id : cached.getRoleMappings()) {
roles.add(cacheSession.getRoleById(id, realm));
}
return roles;
}
@Override
public void deleteRoleMapping(RoleModel role) {
getDelegateForUpdate();
updated.deleteRoleMapping(role);
}
}

View file

@ -1,5 +1,7 @@
package org.keycloak.models.cache.entities;
import org.keycloak.models.AuthenticationLinkModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel;
@ -18,32 +20,40 @@ import java.util.Set;
public class CachedUser {
private String id;
private String loginName;
private String usernameKey;
private String firstName;
private String lastName;
private String email;
private String emailKey;
private boolean emailVerified;
private int notBefore;
private List<UserCredentialValueModel> credentials = new LinkedList<UserCredentialValueModel>();
private boolean enabled;
private boolean totp;
private AuthenticationLinkModel authenticationLink;
private Map<String, String> attributes = new HashMap<String, String>();
private Set<UserModel.RequiredAction> requiredActions = new HashSet<UserModel.RequiredAction>();
private Set<String> roleMappings = new HashSet<String>();
public CachedUser(UserModel user) {
public CachedUser(RealmModel realm, UserModel user) {
this.id = user.getId();
this.loginName = user.getLoginName();
this.usernameKey = realm.getId() + "." + this.loginName;
this.firstName = user.getFirstName();
this.lastName = user.getLastName();
this.attributes.putAll(user.getAttributes());
this.email = user.getEmail();
if (this.email != null) {
this.emailKey = realm.getId() + "." + this.email;
}
this.emailVerified = user.isEmailVerified();
this.notBefore = user.getNotBefore();
this.credentials.addAll(user.getCredentialsDirectly());
this.enabled = user.isEnabled();
this.totp = user.isTotp();
this.requiredActions.addAll(user.getRequiredActions());
this.authenticationLink = user.getAuthenticationLink();
for (RoleModel role : user.getRoleMappings()) {
roleMappings.add(role.getId());
}
@ -57,6 +67,14 @@ public class CachedUser {
return loginName;
}
public String getUsernameKey() {
return usernameKey;
}
public String getEmailKey() {
return emailKey;
}
public String getFirstName() {
return firstName;
}
@ -100,4 +118,8 @@ public class CachedUser {
public Set<String> getRoleMappings() {
return roleMappings;
}
public AuthenticationLinkModel getAuthenticationLink() {
return authenticationLink;
}
}

View file

@ -254,7 +254,8 @@ public class MongoKeycloakSession implements KeycloakSession {
}
@Override
public Set<SocialLinkModel> getSocialLinks(UserModel user, RealmModel realm) {
public Set<SocialLinkModel> getSocialLinks(UserModel userModel, RealmModel realm) {
UserModel user = getUserById(userModel.getId(), realm);
MongoUserEntity userEntity = ((UserAdapter) user).getUser();
List<SocialLinkEntity> linkEntities = userEntity.getSocialLinks();
@ -271,7 +272,8 @@ public class MongoKeycloakSession implements KeycloakSession {
return result;
}
private SocialLinkEntity findSocialLink(UserModel user, String socialProvider) {
private SocialLinkEntity findSocialLink(UserModel userModel, String socialProvider, RealmModel realm) {
UserModel user = getUserById(userModel.getId(), realm);
MongoUserEntity userEntity = ((UserAdapter) user).getUser();
List<SocialLinkEntity> linkEntities = userEntity.getSocialLinks();
if (linkEntities == null) {
@ -289,7 +291,7 @@ public class MongoKeycloakSession implements KeycloakSession {
@Override
public SocialLinkModel getSocialLink(UserModel user, String socialProvider, RealmModel realm) {
SocialLinkEntity socialLinkEntity = findSocialLink(user, socialProvider);
SocialLinkEntity socialLinkEntity = findSocialLink(user, socialProvider, realm);
return socialLinkEntity != null ? new SocialLinkModel(socialLinkEntity.getSocialProvider(), socialLinkEntity.getSocialUserId(), socialLinkEntity.getSocialUsername()) : null;
}

View file

@ -857,18 +857,17 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
}
@Override
public boolean removeSocialLink(UserModel user, String socialProvider) {
SocialLinkEntity socialLinkEntity = findSocialLink(user, socialProvider);
public boolean removeSocialLink(UserModel userModel, String socialProvider) {
UserModel user = getUserById(userModel.getId());
MongoUserEntity userEntity = ((UserAdapter) user).getUser();
SocialLinkEntity socialLinkEntity = findSocialLink(userEntity, socialProvider);
if (socialLinkEntity == null) {
return false;
}
MongoUserEntity userEntity = ((UserAdapter) user).getUser();
return getMongoStore().pullItemFromList(userEntity, "socialLinks", socialLinkEntity, invocationContext);
}
private SocialLinkEntity findSocialLink(UserModel user, String socialProvider) {
MongoUserEntity userEntity = ((UserAdapter) user).getUser();
private SocialLinkEntity findSocialLink(MongoUserEntity userEntity, String socialProvider) {
List<SocialLinkEntity> linkEntities = userEntity.getSocialLinks();
if (linkEntities == null) {
return null;

View file

@ -24,7 +24,7 @@
},
"modelCache": {
"provider": "${keycloak.model.cache.provider:none}"
"provider": "${keycloak.model.cache.provider:simple}"
},
"timer": {

View file

@ -34,6 +34,7 @@ import org.keycloak.models.ApplicationModel;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.representations.idm.CredentialRepresentation;
@ -144,7 +145,7 @@ public class AccountTest {
@After
public void after() {
keycloakRule.configure(new KeycloakSetup() {
keycloakRule.update(new KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) {
UserModel user = appRealm.getUser("test-user@localhost");
@ -239,7 +240,7 @@ public class AccountTest {
@Test
public void changePasswordWithPasswordPolicy() {
keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
appRealm.setPasswordPolicy(new PasswordPolicy("length"));
@ -263,11 +264,11 @@ public class AccountTest {
events.expectAccount(EventType.UPDATE_PASSWORD).assertEvent();
} finally {
keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
appRealm.setPasswordPolicy(new PasswordPolicy(null));
}
}
});
}
}