diff --git a/common/src/main/java/org/keycloak/common/util/ConcurrentMultivaluedHashMap.java b/common/src/main/java/org/keycloak/common/util/ConcurrentMultivaluedHashMap.java new file mode 100755 index 0000000000..56226e0241 --- /dev/null +++ b/common/src/main/java/org/keycloak/common/util/ConcurrentMultivaluedHashMap.java @@ -0,0 +1,102 @@ +/* + * Copyright 2016 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.common.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@SuppressWarnings("serial") +public class ConcurrentMultivaluedHashMap extends ConcurrentHashMap> +{ + public void putSingle(K key, V value) + { + List list = new CopyOnWriteArrayList<>(); + list.add(value); + put(key, list); + } + + public void addAll(K key, V... newValues) + { + for (V value : newValues) + { + add(key, value); + } + } + + public void addAll(K key, List valueList) + { + for (V value : valueList) + { + add(key, value); + } + } + + public void addFirst(K key, V value) + { + List list = get(key); + if (list == null) + { + add(key, value); + } + else + { + list.add(0, value); + } + } + public final void add(K key, V value) + { + getList(key).add(value); + } + + + public final void addMultiple(K key, Collection values) + { + getList(key).addAll(values); + } + + public V getFirst(K key) + { + List list = get(key); + return list == null ? null : list.get(0); + } + + public final List getList(K key) + { + List list = get(key); + if (list == null) + put(key, list = new CopyOnWriteArrayList()); + return list; + } + + public void addAll(ConcurrentMultivaluedHashMap other) + { + for (Entry> entry : other.entrySet()) + { + getList(entry.getKey()).addAll(entry.getValue()); + } + } + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheUserProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java similarity index 79% rename from model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheUserProviderFactory.java rename to model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java index e0409b1aa5..c5205381bf 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheUserProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java @@ -18,34 +18,28 @@ package org.keycloak.models.cache.infinispan; import org.infinispan.Cache; -import org.infinispan.notifications.Listener; -import org.infinispan.notifications.cachelistener.annotation.*; -import org.infinispan.notifications.cachelistener.event.*; import org.jboss.logging.Logger; import org.keycloak.Config; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.models.cache.CacheUserProvider; -import org.keycloak.models.cache.CacheUserProviderFactory; -import org.keycloak.models.cache.infinispan.entities.CachedUser; +import org.keycloak.models.cache.UserCache; +import org.keycloak.models.cache.UserCacheProviderFactory; import org.keycloak.models.cache.infinispan.entities.Revisioned; -import java.util.concurrent.ConcurrentHashMap; - /** * @author Stian Thorgersen */ -public class InfinispanCacheUserProviderFactory implements CacheUserProviderFactory { +public class InfinispanUserCacheProviderFactory implements UserCacheProviderFactory { - private static final Logger log = Logger.getLogger(InfinispanCacheUserProviderFactory.class); + private static final Logger log = Logger.getLogger(InfinispanUserCacheProviderFactory.class); protected volatile UserCacheManager userCache; @Override - public CacheUserProvider create(KeycloakSession session) { + public UserCache create(KeycloakSession session) { lazyInit(session); return new UserCacheSession(userCache, session); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java index d697ad236e..ea6698cf83 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java @@ -28,6 +28,7 @@ import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserModel; +import org.keycloak.models.cache.CachedUserModel; import org.keycloak.models.cache.infinispan.entities.CachedUser; import org.keycloak.models.cache.infinispan.entities.CachedUserConsent; import org.keycloak.models.utils.KeycloakModelUtils; @@ -39,12 +40,13 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; /** * @author Bill Burke * @version $Revision: 1 $ */ -public class UserAdapter implements UserModel { +public class UserAdapter implements CachedUserModel { protected UserModel updated; protected CachedUser cached; protected UserCacheSession userProviderCache; @@ -65,6 +67,22 @@ public class UserAdapter implements UserModel { if (updated == null) throw new IllegalStateException("Not found in database"); } } + + @Override + public void invalidate() { + getDelegateForUpdate(); + } + + @Override + public long getCacheTimestamp() { + return cached.getCacheTimestamp(); + } + + @Override + public ConcurrentHashMap getCachedWith() { + return cached.getCachedWith(); + } + @Override public String getId() { if (updated != null) return updated.getId(); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java index 3d966d1b62..b38d9aab1b 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java @@ -20,7 +20,6 @@ package org.keycloak.models.cache.infinispan; import org.jboss.logging.Logger; import org.keycloak.common.constants.ServiceAccountConstants; import org.keycloak.component.ComponentModel; -import org.keycloak.credential.CredentialInput; import org.keycloak.models.ClientModel; import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.FederatedIdentityModel; @@ -35,7 +34,9 @@ import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserProvider; -import org.keycloak.models.cache.CacheUserProvider; +import org.keycloak.models.cache.CachedUserModel; +import org.keycloak.models.cache.OnUserCache; +import org.keycloak.models.cache.UserCache; import org.keycloak.models.cache.infinispan.entities.CachedFederatedIdentityLinks; import org.keycloak.models.cache.infinispan.entities.CachedUser; import org.keycloak.models.cache.infinispan.entities.CachedUserConsent; @@ -48,7 +49,7 @@ import java.util.*; * @author Bill Burke * @version $Revision: 1 $ */ -public class UserCacheSession implements CacheUserProvider { +public class UserCacheSession implements UserCache { protected static final Logger logger = Logger.getLogger(UserCacheSession.class); protected UserCacheManager cache; protected KeycloakSession session; @@ -74,7 +75,6 @@ public class UserCacheSession implements CacheUserProvider { cache.clear(); } - @Override public UserProvider getDelegate() { if (!transactionActive) throw new IllegalStateException("Cannot access delegate without a transaction"); if (delegate != null) return delegate; @@ -90,6 +90,20 @@ public class UserCacheSession implements CacheUserProvider { if (realm.isIdentityFederationEnabled()) invalidations.add(getFederatedIdentityLinksCacheKey(user.getId())); } + @Override + public void evict(RealmModel realm, UserModel user) { + if (user instanceof CachedUserModel) { + ((CachedUserModel)user).invalidate(); + } else { + invalidations.add(user.getId()); + if (user.getEmail() != null) invalidations.add(getUserByEmailCacheKey(realm.getId(), user.getEmail())); + invalidations.add(getUserByUsernameCacheKey(realm.getId(), user.getUsername())); + if (realm.isIdentityFederationEnabled()) invalidations.add(getFederatedIdentityLinksCacheKey(user.getId())); + } + } + + + protected void runInvalidations() { for (String realmId : realmInvalidations) { cache.invalidateRealmUsers(realmId, invalidations); @@ -148,8 +162,13 @@ public class UserCacheSession implements CacheUserProvider { logger.trace("registered for invalidation return delegate"); return getDelegate().getUserById(id, realm); } + if (managedUsers.containsKey(id)) { + logger.trace("return managedusers"); + return managedUsers.get(id); + } CachedUser cached = cache.get(id, CachedUser.class); + boolean wasCached = cached != null; if (cached == null) { logger.trace("not cached"); Long loaded = cache.getCurrentRevision(id); @@ -158,19 +177,12 @@ public class UserCacheSession implements CacheUserProvider { logger.trace("delegate returning null"); return null; } - if (managedUsers.containsKey(id)) { - logger.trace("return managedusers"); - return managedUsers.get(id); - } - if (invalidations.contains(id)) return model; cached = new CachedUser(loaded, realm, model); cache.addRevisioned(cached, startupRevision); - } else if (managedUsers.containsKey(id)) { - logger.trace("return managedusers"); - return managedUsers.get(id); } logger.trace("returning new cache adapter"); UserAdapter adapter = new UserAdapter(cached, this, session, realm); + if (!wasCached) onCache(realm, adapter); managedUsers.put(id, adapter); return adapter; } @@ -224,13 +236,7 @@ public class UserCacheSession implements CacheUserProvider { return managedUsers.get(userId); } - CachedUser cached = cache.get(userId, CachedUser.class); - if (cached == null) { - cached = new CachedUser(loaded, realm, model); - cache.addRevisioned(cached, startupRevision); - } - logger.trace("return new cache adapter"); - UserAdapter adapter = new UserAdapter(cached, this, session, realm); + UserAdapter adapter = getUserAdapter(realm, userId, loaded, model); managedUsers.put(userId, adapter); return adapter; } else { @@ -245,6 +251,26 @@ public class UserCacheSession implements CacheUserProvider { } } + protected UserAdapter getUserAdapter(RealmModel realm, String userId, Long loaded, UserModel model) { + CachedUser cached = cache.get(userId, CachedUser.class); + boolean wasCached = cached != null; + if (cached == null) { + cached = new CachedUser(loaded, realm, model); + cache.addRevisioned(cached, startupRevision); + } + UserAdapter adapter = new UserAdapter(cached, this, session, realm); + if (!wasCached) { + onCache(realm, adapter); + } + return adapter; + + } + + private void onCache(RealmModel realm, UserAdapter adapter) { + ((OnUserCache)getDelegate()).onCache(realm, adapter); + ((OnUserCache)session.userCredentialManager()).onCache(realm, adapter); + } + @Override public UserModel getUserByEmail(String email, RealmModel realm) { if (email == null) return null; @@ -269,12 +295,7 @@ public class UserCacheSession implements CacheUserProvider { if (invalidations.contains(userId)) return model; if (managedUsers.containsKey(userId)) return managedUsers.get(userId); - CachedUser cached = cache.get(userId, CachedUser.class); - if (cached == null) { - cached = new CachedUser(loaded, realm, model); - cache.addRevisioned(cached, startupRevision); - } - UserAdapter adapter = new UserAdapter(cached, this, session, realm); + UserAdapter adapter = getUserAdapter(realm, userId, loaded, model); managedUsers.put(userId, adapter); return adapter; } else { @@ -317,12 +338,7 @@ public class UserCacheSession implements CacheUserProvider { if (invalidations.contains(userId)) return model; if (managedUsers.containsKey(userId)) return managedUsers.get(userId); - CachedUser cached = cache.get(userId, CachedUser.class); - if (cached == null) { - cached = new CachedUser(loaded, realm, model); - cache.addRevisioned(cached, startupRevision); - } - UserAdapter adapter = new UserAdapter(cached, this, session, realm); + UserAdapter adapter = getUserAdapter(realm, userId, loaded, model); managedUsers.put(userId, adapter); return adapter; } else { @@ -671,24 +687,4 @@ public class UserCacheSession implements CacheUserProvider { } - @Override - public boolean isValid(RealmModel realm, UserModel user, List inputs) { - return getDelegate().isValid(realm, user, inputs); - } - - @Override - public void updateCredential(RealmModel realm, UserModel user, CredentialInput input) { - getDelegate().updateCredential(realm, user, input); - } - - @Override - public boolean isConfiguredFor(RealmModel realm, UserModel user, String type) { - return getDelegate().isConfiguredFor(realm, user, type); - } - - @Override - public Set requiredActionsFor(RealmModel realm, UserModel user, String type) { - return getDelegate().requiredActionsFor(realm, user, type); - } - } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/AbstractExtendableRevisioned.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/AbstractExtendableRevisioned.java new file mode 100644 index 0000000000..9940732841 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/AbstractExtendableRevisioned.java @@ -0,0 +1,40 @@ +/* + * Copyright 2016 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.models.cache.infinispan.entities; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public abstract class AbstractExtendableRevisioned extends AbstractRevisioned { + protected ConcurrentHashMap cachedWith = new ConcurrentHashMap(); + + public AbstractExtendableRevisioned(Long revision, String id) { + super(revision, id); + } + + /** + * Cache things along with this cachable object + * + * @return + */ + public ConcurrentHashMap getCachedWith() { + return cachedWith; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/AbstractRevisioned.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/AbstractRevisioned.java index 89166202c5..ed49ddff1e 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/AbstractRevisioned.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/AbstractRevisioned.java @@ -9,6 +9,7 @@ import java.io.Serializable; public class AbstractRevisioned implements Revisioned, Serializable { private String id; private Long revision; + private final long cacheTimestamp = System.currentTimeMillis(); public AbstractRevisioned(Long revision, String id) { this.revision = revision; @@ -30,4 +31,12 @@ public class AbstractRevisioned implements Revisioned, Serializable { this.revision = revision; } + /** + * When was this cached + * + * @return + */ + public long getCacheTimestamp() { + return cacheTimestamp; + } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java index 89049cae99..7c24594885 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java @@ -36,7 +36,7 @@ import java.util.Set; * @author Bill Burke * @version $Revision: 1 $ */ -public class CachedUser extends AbstractRevisioned implements InRealm { +public class CachedUser extends AbstractExtendableRevisioned implements InRealm { private String realm; private String username; private Long createdTimestamp; diff --git a/model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.cache.CacheUserProviderFactory b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.cache.UserCacheProviderFactory similarity index 91% rename from model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.cache.CacheUserProviderFactory rename to model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.cache.UserCacheProviderFactory index f680e3a4c3..e558cf21e9 100755 --- a/model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.cache.CacheUserProviderFactory +++ b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.cache.UserCacheProviderFactory @@ -15,4 +15,4 @@ # limitations under the License. # -org.keycloak.models.cache.infinispan.InfinispanCacheUserProviderFactory \ No newline at end of file +org.keycloak.models.cache.infinispan.InfinispanUserCacheProviderFactory \ No newline at end of file diff --git a/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/ClusteredCacheBehaviorTest.java b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/ClusteredCacheBehaviorTest.java index de6e587d0a..4c5ccc47d0 100755 --- a/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/ClusteredCacheBehaviorTest.java +++ b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/ClusteredCacheBehaviorTest.java @@ -101,14 +101,19 @@ public class ClusteredCacheBehaviorTest { System.out.println("node1 create entry"); node1Cache.put("key", "node1"); + System.out.println("node1 create entry"); node1Cache.put("key", "node111"); + System.out.println("node2 create entry"); node2Cache.put("key", "node2"); + System.out.println("node1 remove entry"); node1Cache.remove("key"); + System.out.println("node2 remove entry"); node2Cache.remove("key"); + System.out.println("node2 put entry"); node2Cache.put("key", "node2"); System.out.println("node2 evict entry"); @@ -118,6 +123,28 @@ public class ClusteredCacheBehaviorTest { node2Cache.putForExternalRead("key", "common"); System.out.println("node2 remove entry"); node2Cache.remove("key"); + System.out.println("node1 remove entry"); + node1Cache.remove("key"); + + // test remove non-existing node 2, existing node 1 + System.out.println("Test non existent remove"); + System.out.println("node1 create entry"); + node1Cache.put("key", "value"); + System.out.println("node2 remove non-existent entry"); + System.out.println("exists?: " + node2Cache.containsKey("key")); + node2Cache.remove("key"); + + // test clear + System.out.println("Test clear cache"); + System.out.println("add key to node 1, key2 to node2"); + node1Cache.putForExternalRead("key", "value"); + node2Cache.putForExternalRead("key", "value"); + node2Cache.putForExternalRead("key2", "value"); + System.out.println("Clear from node1"); + node1Cache.clear(); + System.out.println("node 2 exists key2?: " + node2Cache.containsKey("key2")); + System.out.println("node 2 exists key?: " + node2Cache.containsKey("key")); + } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserCredentialStore.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserCredentialStore.java new file mode 100644 index 0000000000..d3539fe8d7 --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserCredentialStore.java @@ -0,0 +1,220 @@ +/* + * Copyright 2016 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.models.jpa; + +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.credential.CredentialModel; +import org.keycloak.credential.UserCredentialStore; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.jpa.entities.CredentialAttributeEntity; +import org.keycloak.models.jpa.entities.CredentialEntity; +import org.keycloak.models.jpa.entities.UserEntity; +import org.keycloak.models.utils.KeycloakModelUtils; + +import javax.persistence.EntityManager; +import javax.persistence.TypedQuery; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class JpaUserCredentialStore implements UserCredentialStore { + + private final KeycloakSession session; + protected final EntityManager em; + + public JpaUserCredentialStore(KeycloakSession session, EntityManager em) { + this.session = session; + this.em = em; + } + + @Override + public void updateCredential(RealmModel realm, UserModel user, CredentialModel cred) { + CredentialEntity entity = em.find(CredentialEntity.class, cred.getId()); + if (entity == null) return; + entity.setAlgorithm(cred.getAlgorithm()); + entity.setCounter(cred.getCounter()); + entity.setCreatedDate(cred.getCreatedDate()); + entity.setDevice(cred.getDevice()); + entity.setDigits(cred.getDigits()); + entity.setHashIterations(cred.getHashIterations()); + entity.setPeriod(cred.getPeriod()); + entity.setSalt(cred.getSalt()); + entity.setType(cred.getType()); + entity.setValue(cred.getValue()); + if (entity.getCredentialAttributes().isEmpty() && (cred.getConfig() == null || cred.getConfig().isEmpty())) { + + } else { + MultivaluedHashMap attrs = cred.getConfig(); + MultivaluedHashMap config = cred.getConfig(); + if (config == null) config = new MultivaluedHashMap<>(); + + Iterator it = entity.getCredentialAttributes().iterator(); + while (it.hasNext()) { + CredentialAttributeEntity attr = it.next(); + List values = config.getList(attr.getName()); + if (values == null || !values.contains(attr.getValue())) { + em.remove(attr); + it.remove(); + } else { + attrs.add(attr.getName(), attr.getValue()); + } + + } + for (String key : config.keySet()) { + List values = config.getList(key); + List attrValues = attrs.getList(key); + for (String val : values) { + if (attrValues == null || !attrValues.contains(val)) { + CredentialAttributeEntity attr = new CredentialAttributeEntity(); + attr.setId(KeycloakModelUtils.generateId()); + attr.setValue(val); + attr.setName(key); + attr.setCredential(entity); + em.persist(attr); + entity.getCredentialAttributes().add(attr); + } + } + } + + } + + } + + @Override + public CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel cred) { + CredentialEntity entity = new CredentialEntity(); + String id = cred.getId() == null ? KeycloakModelUtils.generateId() : cred.getId(); + entity.setId(id); + entity.setAlgorithm(cred.getAlgorithm()); + entity.setCounter(cred.getCounter()); + entity.setCreatedDate(cred.getCreatedDate()); + entity.setDevice(cred.getDevice()); + entity.setDigits(cred.getDigits()); + entity.setHashIterations(cred.getHashIterations()); + entity.setPeriod(cred.getPeriod()); + entity.setSalt(cred.getSalt()); + entity.setType(cred.getType()); + entity.setValue(cred.getValue()); + UserEntity userRef = em.getReference(UserEntity.class, user.getId()); + entity.setUser(userRef); + em.persist(entity); + MultivaluedHashMap config = cred.getConfig(); + if (config != null || !config.isEmpty()) { + + for (String key : config.keySet()) { + List values = config.getList(key); + for (String val : values) { + CredentialAttributeEntity attr = new CredentialAttributeEntity(); + attr.setId(KeycloakModelUtils.generateId()); + attr.setValue(val); + attr.setName(key); + attr.setCredential(entity); + em.persist(attr); + entity.getCredentialAttributes().add(attr); + } + } + + } + return toModel(entity); + } + + @Override + public boolean removeStoredCredential(RealmModel realm, UserModel user, String id) { + CredentialEntity entity = em.find(CredentialEntity.class, id); + if (entity == null) return false; + em.remove(entity); + return true; + } + + @Override + public CredentialModel getStoredCredentialById(RealmModel realm, UserModel user, String id) { + CredentialEntity entity = em.find(CredentialEntity.class, id); + if (entity == null) return null; + CredentialModel model = toModel(entity); + return model; + } + + protected CredentialModel toModel(CredentialEntity entity) { + CredentialModel model = new CredentialModel(); + model.setId(entity.getId()); + model.setType(entity.getType()); + model.setValue(entity.getValue()); + model.setAlgorithm(entity.getAlgorithm()); + model.setSalt(entity.getSalt()); + model.setPeriod(entity.getPeriod()); + model.setCounter(entity.getCounter()); + model.setCreatedDate(entity.getCreatedDate()); + model.setDevice(entity.getDevice()); + model.setDigits(entity.getDigits()); + MultivaluedHashMap config = new MultivaluedHashMap<>(); + model.setConfig(config); + for (CredentialAttributeEntity attr : entity.getCredentialAttributes()) { + config.add(attr.getName(), attr.getValue()); + } + return model; + } + + @Override + public List getStoredCredentials(RealmModel realm, UserModel user) { + UserEntity userEntity = em.getReference(UserEntity.class, user.getId()); + TypedQuery query = em.createNamedQuery("credentialByUser", CredentialEntity.class) + .setParameter("user", userEntity); + List results = query.getResultList(); + List rtn = new LinkedList<>(); + for (CredentialEntity entity : results) { + rtn.add(toModel(entity)); + } + return rtn; + } + + @Override + public List getStoredCredentialsByType(RealmModel realm, UserModel user, String type) { + UserEntity userEntity = em.getReference(UserEntity.class, user.getId()); + TypedQuery query = em.createNamedQuery("credentialByUserAndType", CredentialEntity.class) + .setParameter("type", type) + .setParameter("user", userEntity); + List results = query.getResultList(); + List rtn = new LinkedList<>(); + for (CredentialEntity entity : results) { + rtn.add(toModel(entity)); + } + return rtn; + } + + @Override + public CredentialModel getStoredCredentialByNameAndType(RealmModel realm, UserModel user, String name, String type) { + UserEntity userEntity = em.getReference(UserEntity.class, user.getId()); + TypedQuery query = em.createNamedQuery("credentialByNameAndType", CredentialEntity.class) + .setParameter("type", type) + .setParameter("device", name) + .setParameter("user", userEntity); + List results = query.getResultList(); + if (results.isEmpty()) return null; + return toModel(results.get(0)); + } + + @Override + public void close() { + + } +} diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserCredentialStoreFactory.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserCredentialStoreFactory.java new file mode 100755 index 0000000000..550299ba8b --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserCredentialStoreFactory.java @@ -0,0 +1,59 @@ +/* + * Copyright 2016 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.models.jpa; + +import org.keycloak.Config; +import org.keycloak.connections.jpa.JpaConnectionProvider; +import org.keycloak.credential.UserCredentialStore; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderFactory; + +import javax.persistence.EntityManager; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class JpaUserCredentialStoreFactory implements ProviderFactory { + + @Override + public void init(Config.Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public String getId() { + return "jpa"; + } + + @Override + public UserCredentialStore create(KeycloakSession session) { + EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager(); + return new JpaUserCredentialStore(session, em); + } + + @Override + public void close() { + } + +} diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java index 30752d3bee..d44c6fe171 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java @@ -17,8 +17,11 @@ package org.keycloak.models.jpa; +import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.component.ComponentModel; import org.keycloak.credential.CredentialInput; +import org.keycloak.credential.CredentialModel; +import org.keycloak.credential.UserCredentialStore; import org.keycloak.models.ClientModel; import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.FederatedIdentityModel; @@ -35,6 +38,8 @@ import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserProvider; +import org.keycloak.models.jpa.entities.CredentialAttributeEntity; +import org.keycloak.models.jpa.entities.CredentialEntity; import org.keycloak.models.jpa.entities.FederatedIdentityEntity; import org.keycloak.models.jpa.entities.UserAttributeEntity; import org.keycloak.models.jpa.entities.UserConsentEntity; @@ -51,6 +56,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -60,7 +66,7 @@ import java.util.Set; * @author Bill Burke * @version $Revision: 1 $ */ -public class JpaUserProvider implements UserProvider { +public class JpaUserProvider implements UserProvider, UserCredentialStore { private static final String EMAIL = "email"; private static final String USERNAME = "username"; @@ -724,24 +730,174 @@ public class JpaUserProvider implements UserProvider { public void preRemove(RealmModel realm, ComponentModel component) { } - @Override - public boolean isValid(RealmModel realm, UserModel user, List inputs) { - return false; - } @Override - public void updateCredential(RealmModel realm, UserModel user, CredentialInput input) { + public void updateCredential(RealmModel realm, UserModel user, CredentialModel cred) { + CredentialEntity entity = em.find(CredentialEntity.class, cred.getId()); + if (entity == null) return; + entity.setAlgorithm(cred.getAlgorithm()); + entity.setCounter(cred.getCounter()); + entity.setCreatedDate(cred.getCreatedDate()); + entity.setDevice(cred.getDevice()); + entity.setDigits(cred.getDigits()); + entity.setHashIterations(cred.getHashIterations()); + entity.setPeriod(cred.getPeriod()); + entity.setSalt(cred.getSalt()); + entity.setType(cred.getType()); + entity.setValue(cred.getValue()); + if (entity.getCredentialAttributes().isEmpty() && (cred.getConfig() == null || cred.getConfig().isEmpty())) { + + } else { + MultivaluedHashMap attrs = cred.getConfig(); + MultivaluedHashMap config = cred.getConfig(); + if (config == null) config = new MultivaluedHashMap<>(); + + Iterator it = entity.getCredentialAttributes().iterator(); + while (it.hasNext()) { + CredentialAttributeEntity attr = it.next(); + List values = config.getList(attr.getName()); + if (values == null || !values.contains(attr.getValue())) { + em.remove(attr); + it.remove(); + } else { + attrs.add(attr.getName(), attr.getValue()); + } + + } + for (String key : config.keySet()) { + List values = config.getList(key); + List attrValues = attrs.getList(key); + for (String val : values) { + if (attrValues == null || !attrValues.contains(val)) { + CredentialAttributeEntity attr = new CredentialAttributeEntity(); + attr.setId(KeycloakModelUtils.generateId()); + attr.setValue(val); + attr.setName(key); + attr.setCredential(entity); + em.persist(attr); + entity.getCredentialAttributes().add(attr); + } + } + } + + } } @Override - public boolean isConfiguredFor(RealmModel realm, UserModel user, String type) { - return false; + public CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel cred) { + CredentialEntity entity = new CredentialEntity(); + String id = cred.getId() == null ? KeycloakModelUtils.generateId() : cred.getId(); + entity.setId(id); + entity.setAlgorithm(cred.getAlgorithm()); + entity.setCounter(cred.getCounter()); + entity.setCreatedDate(cred.getCreatedDate()); + entity.setDevice(cred.getDevice()); + entity.setDigits(cred.getDigits()); + entity.setHashIterations(cred.getHashIterations()); + entity.setPeriod(cred.getPeriod()); + entity.setSalt(cred.getSalt()); + entity.setType(cred.getType()); + entity.setValue(cred.getValue()); + UserEntity userRef = em.getReference(UserEntity.class, user.getId()); + entity.setUser(userRef); + em.persist(entity); + MultivaluedHashMap config = cred.getConfig(); + if (config != null || !config.isEmpty()) { + + for (String key : config.keySet()) { + List values = config.getList(key); + for (String val : values) { + CredentialAttributeEntity attr = new CredentialAttributeEntity(); + attr.setId(KeycloakModelUtils.generateId()); + attr.setValue(val); + attr.setName(key); + attr.setCredential(entity); + em.persist(attr); + entity.getCredentialAttributes().add(attr); + } + } + + } + return toModel(entity); } @Override - public Set requiredActionsFor(RealmModel realm, UserModel user, String type) { - return Collections.EMPTY_SET; + public boolean removeStoredCredential(RealmModel realm, UserModel user, String id) { + CredentialEntity entity = em.find(CredentialEntity.class, id); + if (entity == null) return false; + em.remove(entity); + return true; } + @Override + public CredentialModel getStoredCredentialById(RealmModel realm, UserModel user, String id) { + CredentialEntity entity = em.find(CredentialEntity.class, id); + if (entity == null) return null; + CredentialModel model = toModel(entity); + return model; + } + + protected CredentialModel toModel(CredentialEntity entity) { + CredentialModel model = new CredentialModel(); + model.setId(entity.getId()); + model.setType(entity.getType()); + model.setValue(entity.getValue()); + model.setAlgorithm(entity.getAlgorithm()); + model.setSalt(entity.getSalt()); + model.setPeriod(entity.getPeriod()); + model.setCounter(entity.getCounter()); + model.setCreatedDate(entity.getCreatedDate()); + model.setDevice(entity.getDevice()); + model.setDigits(entity.getDigits()); + MultivaluedHashMap config = new MultivaluedHashMap<>(); + model.setConfig(config); + for (CredentialAttributeEntity attr : entity.getCredentialAttributes()) { + config.add(attr.getName(), attr.getValue()); + } + return model; + } + + @Override + public List getStoredCredentials(RealmModel realm, UserModel user) { + UserEntity userEntity = em.getReference(UserEntity.class, user.getId()); + TypedQuery query = em.createNamedQuery("credentialByUser", CredentialEntity.class) + .setParameter("user", userEntity); + List results = query.getResultList(); + List rtn = new LinkedList<>(); + for (CredentialEntity entity : results) { + rtn.add(toModel(entity)); + } + return rtn; + } + + @Override + public List getStoredCredentialsByType(RealmModel realm, UserModel user, String type) { + UserEntity userEntity = em.getReference(UserEntity.class, user.getId()); + TypedQuery query = em.createNamedQuery("credentialByUserAndType", CredentialEntity.class) + .setParameter("type", type) + .setParameter("user", userEntity); + List results = query.getResultList(); + List rtn = new LinkedList<>(); + for (CredentialEntity entity : results) { + rtn.add(toModel(entity)); + } + return rtn; + } + + @Override + public CredentialModel getStoredCredentialByNameAndType(RealmModel realm, UserModel user, String name, String type) { + UserEntity userEntity = em.getReference(UserEntity.class, user.getId()); + TypedQuery query = em.createNamedQuery("credentialByNameAndType", CredentialEntity.class) + .setParameter("type", type) + .setParameter("device", name) + .setParameter("user", userEntity); + List results = query.getResultList(); + if (results.isEmpty()) return null; + return toModel(results.get(0)); + } + + + + } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/CredentialEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/CredentialEntity.java index 2d048ee0d2..4c8f0b3e83 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/CredentialEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/CredentialEntity.java @@ -38,7 +38,9 @@ import java.util.Collection; * @version $Revision: 1 $ */ @NamedQueries({ + @NamedQuery(name="credentialByUser", query="select cred from CredentialEntity cred where cred.user = :user"), @NamedQuery(name="credentialByUserAndType", query="select cred from CredentialEntity cred where cred.user = :user and cred.type = :type"), + @NamedQuery(name="credentialByNameAndType", query="select cred from CredentialEntity cred where cred.user = :user and cred.type = :type and cred.device = :device"), @NamedQuery(name="deleteCredentialsByRealm", query="delete from CredentialEntity cred where cred.user IN (select u from UserEntity u where u.realmId=:realmId)"), @NamedQuery(name="deleteCredentialsByRealmAndLink", query="delete from CredentialEntity cred where cred.user IN (select u from UserEntity u where u.realmId=:realmId and u.federationLink=:link)") diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaFederatedUserCredentialStore.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaFederatedUserCredentialStore.java new file mode 100644 index 0000000000..13fa03218f --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaFederatedUserCredentialStore.java @@ -0,0 +1,218 @@ +/* + * Copyright 2016 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.storage.jpa; + +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.credential.CredentialModel; +import org.keycloak.credential.UserCredentialStore; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.storage.StorageId; +import org.keycloak.storage.jpa.entity.FederatedUserCredentialAttributeEntity; +import org.keycloak.storage.jpa.entity.FederatedUserCredentialEntity; + +import javax.persistence.EntityManager; +import javax.persistence.TypedQuery; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class JpaFederatedUserCredentialStore implements UserCredentialStore { + + private final KeycloakSession session; + protected final EntityManager em; + + public JpaFederatedUserCredentialStore(KeycloakSession session, EntityManager em) { + this.session = session; + this.em = em; + } + + @Override + public void updateCredential(RealmModel realm, UserModel user, CredentialModel cred) { + FederatedUserCredentialEntity entity = em.find(FederatedUserCredentialEntity.class, cred.getId()); + if (entity == null) return; + entity.setAlgorithm(cred.getAlgorithm()); + entity.setCounter(cred.getCounter()); + entity.setCreatedDate(cred.getCreatedDate()); + entity.setDevice(cred.getDevice()); + entity.setDigits(cred.getDigits()); + entity.setHashIterations(cred.getHashIterations()); + entity.setPeriod(cred.getPeriod()); + entity.setSalt(cred.getSalt()); + entity.setType(cred.getType()); + entity.setValue(cred.getValue()); + if (entity.getCredentialAttributes().isEmpty() && (cred.getConfig() == null || cred.getConfig().isEmpty())) { + + } else { + MultivaluedHashMap attrs = cred.getConfig(); + MultivaluedHashMap config = cred.getConfig(); + if (config == null) config = new MultivaluedHashMap<>(); + + Iterator it = entity.getCredentialAttributes().iterator(); + while (it.hasNext()) { + FederatedUserCredentialAttributeEntity attr = it.next(); + List values = config.getList(attr.getName()); + if (values == null || !values.contains(attr.getValue())) { + em.remove(attr); + it.remove(); + } else { + attrs.add(attr.getName(), attr.getValue()); + } + + } + for (String key : config.keySet()) { + List values = config.getList(key); + List attrValues = attrs.getList(key); + for (String val : values) { + if (attrValues == null || !attrValues.contains(val)) { + FederatedUserCredentialAttributeEntity attr = new FederatedUserCredentialAttributeEntity(); + attr.setId(KeycloakModelUtils.generateId()); + attr.setValue(val); + attr.setName(key); + attr.setCredential(entity); + em.persist(attr); + entity.getCredentialAttributes().add(attr); + } + } + } + + } + + } + + @Override + public CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel cred) { + FederatedUserCredentialEntity entity = new FederatedUserCredentialEntity(); + String id = cred.getId() == null ? KeycloakModelUtils.generateId() : cred.getId(); + entity.setId(id); + entity.setAlgorithm(cred.getAlgorithm()); + entity.setCounter(cred.getCounter()); + entity.setCreatedDate(cred.getCreatedDate()); + entity.setDevice(cred.getDevice()); + entity.setDigits(cred.getDigits()); + entity.setHashIterations(cred.getHashIterations()); + entity.setPeriod(cred.getPeriod()); + entity.setSalt(cred.getSalt()); + entity.setType(cred.getType()); + entity.setValue(cred.getValue()); + entity.setUserId(user.getId()); + entity.setRealmId(realm.getId()); + entity.setStorageProviderId(StorageId.resolveProviderId(user)); + em.persist(entity); + MultivaluedHashMap config = cred.getConfig(); + if (config != null || !config.isEmpty()) { + + for (String key : config.keySet()) { + List values = config.getList(key); + for (String val : values) { + FederatedUserCredentialAttributeEntity attr = new FederatedUserCredentialAttributeEntity(); + attr.setId(KeycloakModelUtils.generateId()); + attr.setValue(val); + attr.setName(key); + attr.setCredential(entity); + em.persist(attr); + entity.getCredentialAttributes().add(attr); + } + } + + } + return toModel(entity); + } + + @Override + public boolean removeStoredCredential(RealmModel realm, UserModel user, String id) { + FederatedUserCredentialEntity entity = em.find(FederatedUserCredentialEntity.class, id); + if (entity == null) return false; + em.remove(entity); + return true; + } + + @Override + public CredentialModel getStoredCredentialById(RealmModel realm, UserModel user, String id) { + FederatedUserCredentialEntity entity = em.find(FederatedUserCredentialEntity.class, id); + if (entity == null) return null; + CredentialModel model = toModel(entity); + return model; + } + + protected CredentialModel toModel(FederatedUserCredentialEntity entity) { + CredentialModel model = new CredentialModel(); + model.setId(entity.getId()); + model.setType(entity.getType()); + model.setValue(entity.getValue()); + model.setAlgorithm(entity.getAlgorithm()); + model.setSalt(entity.getSalt()); + model.setPeriod(entity.getPeriod()); + model.setCounter(entity.getCounter()); + model.setCreatedDate(entity.getCreatedDate()); + model.setDevice(entity.getDevice()); + model.setDigits(entity.getDigits()); + MultivaluedHashMap config = new MultivaluedHashMap<>(); + model.setConfig(config); + for (FederatedUserCredentialAttributeEntity attr : entity.getCredentialAttributes()) { + config.add(attr.getName(), attr.getValue()); + } + return model; + } + + @Override + public List getStoredCredentials(RealmModel realm, UserModel user) { + TypedQuery query = em.createNamedQuery("federatedUserCredentialByUser", FederatedUserCredentialEntity.class) + .setParameter("userId", user.getId()); + List results = query.getResultList(); + List rtn = new LinkedList<>(); + for (FederatedUserCredentialEntity entity : results) { + rtn.add(toModel(entity)); + } + return rtn; + } + + @Override + public List getStoredCredentialsByType(RealmModel realm, UserModel user, String type) { + TypedQuery query = em.createNamedQuery("federatedUserCredentialByUserAndType", FederatedUserCredentialEntity.class) + .setParameter("type", type) + .setParameter("userId", user.getId()); + List results = query.getResultList(); + List rtn = new LinkedList<>(); + for (FederatedUserCredentialEntity entity : results) { + rtn.add(toModel(entity)); + } + return rtn; + } + + @Override + public CredentialModel getStoredCredentialByNameAndType(RealmModel realm, UserModel user, String name, String type) { + TypedQuery query = em.createNamedQuery("federatedUserCredentialByNameAndType", FederatedUserCredentialEntity.class) + .setParameter("type", type) + .setParameter("device", name) + .setParameter("userId", user.getId()); + List results = query.getResultList(); + if (results.isEmpty()) return null; + return toModel(results.get(0)); + } + + @Override + public void close() { + + } +} diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java index d9d7de9903..1c72da0744 100644 --- a/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java +++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java @@ -19,6 +19,7 @@ package org.keycloak.storage.jpa; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.component.ComponentModel; import org.keycloak.credential.CredentialModel; +import org.keycloak.credential.UserCredentialStore; import org.keycloak.models.ClientModel; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.GroupModel; @@ -50,6 +51,7 @@ import org.keycloak.storage.jpa.entity.FederatedUserAttributeEntity; import org.keycloak.storage.jpa.entity.FederatedUserConsentEntity; import org.keycloak.storage.jpa.entity.FederatedUserConsentProtocolMapperEntity; import org.keycloak.storage.jpa.entity.FederatedUserConsentRoleEntity; +import org.keycloak.storage.jpa.entity.FederatedUserCredentialAttributeEntity; import org.keycloak.storage.jpa.entity.FederatedUserCredentialEntity; import org.keycloak.storage.jpa.entity.FederatedUserGroupMembershipEntity; import org.keycloak.storage.jpa.entity.FederatedUserRequiredActionEntity; @@ -60,6 +62,7 @@ import javax.persistence.TypedQuery; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; +import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -76,7 +79,8 @@ public class JpaUserFederatedStorageProvider implements UserCredentialsFederatedStorage, UserGroupMembershipFederatedStorage, UserRequiredActionsFederatedStorage, - UserRoleMappingsFederatedStorage { + UserRoleMappingsFederatedStorage, + UserCredentialStore { private final KeycloakSession session; protected EntityManager em; @@ -607,16 +611,6 @@ public class JpaUserFederatedStorageProvider implements em.flush(); } - @Override - public void updateCredential(RealmModel realm, UserModel user, CredentialModel cred) { - - } - - @Override - public CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel cred) { - return null; - } - @Override public boolean removeCredential(RealmModel realm, String id) { return false; @@ -647,6 +641,170 @@ public class JpaUserFederatedStorageProvider implements return null; } + @Override + public void updateCredential(RealmModel realm, UserModel user, CredentialModel cred) { + FederatedUserCredentialEntity entity = em.find(FederatedUserCredentialEntity.class, cred.getId()); + if (entity == null) return; + entity.setAlgorithm(cred.getAlgorithm()); + entity.setCounter(cred.getCounter()); + entity.setCreatedDate(cred.getCreatedDate()); + entity.setDevice(cred.getDevice()); + entity.setDigits(cred.getDigits()); + entity.setHashIterations(cred.getHashIterations()); + entity.setPeriod(cred.getPeriod()); + entity.setSalt(cred.getSalt()); + entity.setType(cred.getType()); + entity.setValue(cred.getValue()); + if (entity.getCredentialAttributes().isEmpty() && (cred.getConfig() == null || cred.getConfig().isEmpty())) { + + } else { + MultivaluedHashMap attrs = cred.getConfig(); + MultivaluedHashMap config = cred.getConfig(); + if (config == null) config = new MultivaluedHashMap<>(); + + Iterator it = entity.getCredentialAttributes().iterator(); + while (it.hasNext()) { + FederatedUserCredentialAttributeEntity attr = it.next(); + List values = config.getList(attr.getName()); + if (values == null || !values.contains(attr.getValue())) { + em.remove(attr); + it.remove(); + } else { + attrs.add(attr.getName(), attr.getValue()); + } + + } + for (String key : config.keySet()) { + List values = config.getList(key); + List attrValues = attrs.getList(key); + for (String val : values) { + if (attrValues == null || !attrValues.contains(val)) { + FederatedUserCredentialAttributeEntity attr = new FederatedUserCredentialAttributeEntity(); + attr.setId(KeycloakModelUtils.generateId()); + attr.setValue(val); + attr.setName(key); + attr.setCredential(entity); + em.persist(attr); + entity.getCredentialAttributes().add(attr); + } + } + } + + } + + } + + @Override + public CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel cred) { + FederatedUserCredentialEntity entity = new FederatedUserCredentialEntity(); + String id = cred.getId() == null ? KeycloakModelUtils.generateId() : cred.getId(); + entity.setId(id); + entity.setAlgorithm(cred.getAlgorithm()); + entity.setCounter(cred.getCounter()); + entity.setCreatedDate(cred.getCreatedDate()); + entity.setDevice(cred.getDevice()); + entity.setDigits(cred.getDigits()); + entity.setHashIterations(cred.getHashIterations()); + entity.setPeriod(cred.getPeriod()); + entity.setSalt(cred.getSalt()); + entity.setType(cred.getType()); + entity.setValue(cred.getValue()); + entity.setUserId(user.getId()); + entity.setRealmId(realm.getId()); + entity.setStorageProviderId(StorageId.resolveProviderId(user)); + em.persist(entity); + MultivaluedHashMap config = cred.getConfig(); + if (config != null || !config.isEmpty()) { + + for (String key : config.keySet()) { + List values = config.getList(key); + for (String val : values) { + FederatedUserCredentialAttributeEntity attr = new FederatedUserCredentialAttributeEntity(); + attr.setId(KeycloakModelUtils.generateId()); + attr.setValue(val); + attr.setName(key); + attr.setCredential(entity); + em.persist(attr); + entity.getCredentialAttributes().add(attr); + } + } + + } + return toModel(entity); + } + + @Override + public boolean removeStoredCredential(RealmModel realm, UserModel user, String id) { + FederatedUserCredentialEntity entity = em.find(FederatedUserCredentialEntity.class, id); + if (entity == null) return false; + em.remove(entity); + return true; + } + + @Override + public CredentialModel getStoredCredentialById(RealmModel realm, UserModel user, String id) { + FederatedUserCredentialEntity entity = em.find(FederatedUserCredentialEntity.class, id); + if (entity == null) return null; + CredentialModel model = toModel(entity); + return model; + } + + protected CredentialModel toModel(FederatedUserCredentialEntity entity) { + CredentialModel model = new CredentialModel(); + model.setId(entity.getId()); + model.setType(entity.getType()); + model.setValue(entity.getValue()); + model.setAlgorithm(entity.getAlgorithm()); + model.setSalt(entity.getSalt()); + model.setPeriod(entity.getPeriod()); + model.setCounter(entity.getCounter()); + model.setCreatedDate(entity.getCreatedDate()); + model.setDevice(entity.getDevice()); + model.setDigits(entity.getDigits()); + MultivaluedHashMap config = new MultivaluedHashMap<>(); + model.setConfig(config); + for (FederatedUserCredentialAttributeEntity attr : entity.getCredentialAttributes()) { + config.add(attr.getName(), attr.getValue()); + } + return model; + } + + @Override + public List getStoredCredentials(RealmModel realm, UserModel user) { + TypedQuery query = em.createNamedQuery("federatedUserCredentialByUser", FederatedUserCredentialEntity.class) + .setParameter("userId", user.getId()); + List results = query.getResultList(); + List rtn = new LinkedList<>(); + for (FederatedUserCredentialEntity entity : results) { + rtn.add(toModel(entity)); + } + return rtn; + } + + @Override + public List getStoredCredentialsByType(RealmModel realm, UserModel user, String type) { + TypedQuery query = em.createNamedQuery("federatedUserCredentialByUserAndType", FederatedUserCredentialEntity.class) + .setParameter("type", type) + .setParameter("userId", user.getId()); + List results = query.getResultList(); + List rtn = new LinkedList<>(); + for (FederatedUserCredentialEntity entity : results) { + rtn.add(toModel(entity)); + } + return rtn; + } + + @Override + public CredentialModel getStoredCredentialByNameAndType(RealmModel realm, UserModel user, String name, String type) { + TypedQuery query = em.createNamedQuery("federatedUserCredentialByNameAndType", FederatedUserCredentialEntity.class) + .setParameter("type", type) + .setParameter("device", name) + .setParameter("userId", user.getId()); + List results = query.getResultList(); + if (results.isEmpty()) return null; + return toModel(results.get(0)); + } + @Override public void preRemove(RealmModel realm) { int num = em.createNamedQuery("deleteFederatedUserConsentRolesByRealm") diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserCredentialEntity.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserCredentialEntity.java index da305b48b7..52a757cd8f 100755 --- a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserCredentialEntity.java +++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserCredentialEntity.java @@ -43,6 +43,7 @@ import java.util.Collection; @NamedQueries({ @NamedQuery(name="federatedUserCredentialByUser", query="select cred from FederatedUserCredentialEntity cred where cred.userId = :userId"), @NamedQuery(name="federatedUserCredentialByUserAndType", query="select cred from FederatedUserCredentialEntity cred where cred.userId = :userId and cred.type = :type"), + @NamedQuery(name="federatedUserCredentialByNameAndType", query="select cred from FederatedUserCredentialEntity cred where cred.userId = :userId and cred.type = :type and cred.device = :device"), @NamedQuery(name="deleteFederatedUserCredentialByUser", query="delete from FederatedUserCredentialEntity cred where cred.userId = :userId and cred.realmId = :realmId"), @NamedQuery(name="deleteFederatedUserCredentialByUserAndType", query="delete from FederatedUserCredentialEntity cred where cred.userId = :userId and cred.type = :type"), @NamedQuery(name="deleteFederatedUserCredentialByUserAndTypeAndDevice", query="delete from FederatedUserCredentialEntity cred where cred.userId = :userId and cred.type = :type and cred.device = :device"), diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java index 3888c7ab74..9ad2c1ea36 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java @@ -637,23 +637,4 @@ public class MongoUserProvider implements UserProvider { } - @Override - public boolean isValid(RealmModel realm, UserModel user, List inputs) { - return false; - } - - @Override - public void updateCredential(RealmModel realm, UserModel user, CredentialInput input) { - - } - - @Override - public boolean isConfiguredFor(RealmModel realm, UserModel user, String type) { - return false; - } - - @Override - public Set requiredActionsFor(RealmModel realm, UserModel user, String type) { - return Collections.EMPTY_SET; - } } diff --git a/server-spi/src/main/java/org/keycloak/credential/CredentialInputUpdater.java b/server-spi/src/main/java/org/keycloak/credential/CredentialInputUpdater.java index e24870c096..3456f8f5e1 100644 --- a/server-spi/src/main/java/org/keycloak/credential/CredentialInputUpdater.java +++ b/server-spi/src/main/java/org/keycloak/credential/CredentialInputUpdater.java @@ -27,6 +27,6 @@ import java.util.Set; */ public interface CredentialInputUpdater { boolean supportsCredentialType(String credentialType); - Set requiredActionsFor(RealmModel realm, UserModel user, String credentialType); - void updateCredential(RealmModel realm, UserModel user, CredentialInput input); + boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input); + void disableCredentialType(RealmModel realm, UserModel user, String credentialType); } diff --git a/server-spi/src/main/java/org/keycloak/credential/CredentialModel.java b/server-spi/src/main/java/org/keycloak/credential/CredentialModel.java index 082c64b770..24b7772398 100755 --- a/server-spi/src/main/java/org/keycloak/credential/CredentialModel.java +++ b/server-spi/src/main/java/org/keycloak/credential/CredentialModel.java @@ -29,6 +29,20 @@ import java.util.Map; * @author Marek Posolda */ public class CredentialModel implements Serializable { + public static final String PASSWORD = "password"; + public static final String PASSWORD_HISTORY = "password-history"; + public static final String PASSWORD_TOKEN = "password-token"; + + // Secret is same as password but it is not hashed + public static final String SECRET = "secret"; + public static final String TOTP = "totp"; + public static final String HOTP = "hotp"; + public static final String CLIENT_CERT = "cert"; + public static final String KERBEROS = "kerberos"; + public static final String OTP = "otp"; + + + private String id; private String type; private String value; diff --git a/server-spi/src/main/java/org/keycloak/credential/UserCredentialStore.java b/server-spi/src/main/java/org/keycloak/credential/UserCredentialStore.java index df497f1ad5..c99873911c 100644 --- a/server-spi/src/main/java/org/keycloak/credential/UserCredentialStore.java +++ b/server-spi/src/main/java/org/keycloak/credential/UserCredentialStore.java @@ -29,10 +29,9 @@ import java.util.List; public interface UserCredentialStore extends Provider { void updateCredential(RealmModel realm, UserModel user, CredentialModel cred); CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel cred); - boolean removeCredential(RealmModel realm, String id); - CredentialModel getCredentialById(String id); - List getCredentials(RealmModel realm); - List getCredentials(RealmModel realm, UserModel user); - List getCredentialsByType(RealmModel realm, UserModel user, String type); - CredentialModel getCredentialByNameAndType(RealmModel realm, UserModel user, String name, String type); + boolean removeStoredCredential(RealmModel realm, UserModel user, String id); + CredentialModel getStoredCredentialById(RealmModel realm, UserModel user, String id); + List getStoredCredentials(RealmModel realm, UserModel user); + List getStoredCredentialsByType(RealmModel realm, UserModel user, String type); + CredentialModel getStoredCredentialByNameAndType(RealmModel realm, UserModel user, String name, String type); } diff --git a/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java b/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java index 8b6dcd31d0..2a1ad0ff4c 100755 --- a/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java +++ b/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java @@ -17,6 +17,7 @@ package org.keycloak.models; +import org.keycloak.models.cache.UserCache; import org.keycloak.provider.Provider; import org.keycloak.scripting.ScriptingProvider; import org.keycloak.storage.federated.UserFederatedStorageProvider; @@ -74,6 +75,7 @@ public interface KeycloakSession { Object removeAttribute(String attribute); void setAttribute(String name, Object value); + void enlistForClose(Provider provider); KeycloakSessionFactory getKeycloakSessionFactory(); @@ -101,7 +103,14 @@ public interface KeycloakSession { void close(); /** - * A cached view of all users in system. + * The user cache + * + * @return may be null if cache is disabled + */ + UserCache getUserCache(); + + /** + * A possibly cached view of all users in system. * * @return */ @@ -115,8 +124,10 @@ public interface KeycloakSession { */ UserProvider userStorageManager(); + UserCredentialManager userCredentialManager(); + /** - * A cached view of all users in system that does NOT include users available from the deprecated UserFederationProvider SPI. + * A possibly cached view of all users in system that does NOT include users available from the deprecated UserFederationProvider SPI. */ UserProvider userStorage(); @@ -129,6 +140,7 @@ public interface KeycloakSession { /** * Hybrid storage for UserStorageProviders that can't store a specific piece of keycloak data in their external storage. + * No cache in front. * * @return */ diff --git a/server-spi/src/main/java/org/keycloak/models/UserCredentialManager.java b/server-spi/src/main/java/org/keycloak/models/UserCredentialManager.java new file mode 100644 index 0000000000..2da5716f92 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/models/UserCredentialManager.java @@ -0,0 +1,37 @@ +/* + * Copyright 2016 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.models; + +import org.keycloak.credential.CredentialInput; +import org.keycloak.credential.UserCredentialStore; + +import java.util.List; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public interface UserCredentialManager extends UserCredentialStore { + boolean isValid(RealmModel realm, UserModel user, List inputs); + + void updateCredential(RealmModel realm, UserModel user, CredentialInput input); + void disableCredential(RealmModel realm, UserModel user, String credentialType); + + boolean isConfiguredFor(RealmModel realm, UserModel user, String type); + +} diff --git a/server-spi/src/main/java/org/keycloak/models/UserCredentialModel.java b/server-spi/src/main/java/org/keycloak/models/UserCredentialModel.java index fa967074c0..4be355d19a 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserCredentialModel.java +++ b/server-spi/src/main/java/org/keycloak/models/UserCredentialModel.java @@ -18,6 +18,7 @@ package org.keycloak.models; import org.keycloak.credential.CredentialInput; +import org.keycloak.credential.CredentialModel; import java.util.UUID; @@ -26,16 +27,16 @@ import java.util.UUID; * @version $Revision: 1 $ */ public class UserCredentialModel implements CredentialInput { - public static final String PASSWORD = "password"; - public static final String PASSWORD_HISTORY = "password-history"; - public static final String PASSWORD_TOKEN = "password-token"; + public static final String PASSWORD = CredentialModel.PASSWORD; + public static final String PASSWORD_HISTORY = CredentialModel.PASSWORD_HISTORY; + public static final String PASSWORD_TOKEN = CredentialModel.PASSWORD_TOKEN; // Secret is same as password but it is not hashed - public static final String SECRET = "secret"; - public static final String TOTP = "totp"; - public static final String HOTP = "hotp"; - public static final String CLIENT_CERT = "cert"; - public static final String KERBEROS = "kerberos"; + public static final String SECRET = CredentialModel.SECRET; + public static final String TOTP = CredentialModel.TOTP; + public static final String HOTP = CredentialModel.HOTP; + public static final String CLIENT_CERT = CredentialModel.CLIENT_CERT; + public static final String KERBEROS = CredentialModel.KERBEROS; protected String type; protected String value; diff --git a/server-spi/src/main/java/org/keycloak/models/UserFederationManager.java b/server-spi/src/main/java/org/keycloak/models/UserFederationManager.java index 46c748edfd..e1037053fc 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserFederationManager.java +++ b/server-spi/src/main/java/org/keycloak/models/UserFederationManager.java @@ -445,27 +445,6 @@ public class UserFederationManager implements UserProvider { session.userStorage().grantToAllUsers(realm, role); } - @Override - public boolean isValid(RealmModel realm, UserModel user, List inputs) { - return session.userStorage().isValid(realm, user, inputs); - } - - @Override - public void updateCredential(RealmModel realm, UserModel user, CredentialInput input) { - session.userStorage().updateCredential(realm, user, input); - - } - - @Override - public boolean isConfiguredFor(RealmModel realm, UserModel user, String type) { - return session.userStorage().isConfiguredFor(realm, user, type); - } - - @Override - public Set requiredActionsFor(RealmModel realm, UserModel user, String type) { - return session.userStorage().requiredActionsFor(realm, user, type); - } - @Override public void preRemove(RealmModel realm) { for (UserFederationProviderModel federation : realm.getUserFederationProviders()) { diff --git a/server-spi/src/main/java/org/keycloak/models/UserProvider.java b/server-spi/src/main/java/org/keycloak/models/UserProvider.java index 5dbc04ea34..642131b518 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/UserProvider.java @@ -87,11 +87,4 @@ public interface UserProvider extends Provider, void preRemove(RealmModel realm, ComponentModel component); - boolean isValid(RealmModel realm, UserModel user, List inputs); - - void updateCredential(RealmModel realm, UserModel user, CredentialInput input); - - boolean isConfiguredFor(RealmModel realm, UserModel user, String type); - - Set requiredActionsFor(RealmModel realm, UserModel user, String type); } diff --git a/server-spi/src/main/java/org/keycloak/models/cache/CacheUserProviderSpi.java b/server-spi/src/main/java/org/keycloak/models/cache/CacheUserProviderSpi.java index c6723fba97..a0efa39e91 100755 --- a/server-spi/src/main/java/org/keycloak/models/cache/CacheUserProviderSpi.java +++ b/server-spi/src/main/java/org/keycloak/models/cache/CacheUserProviderSpi.java @@ -39,11 +39,11 @@ public class CacheUserProviderSpi implements Spi { @Override public Class getProviderClass() { - return CacheUserProvider.class; + return UserCache.class; } @Override public Class getProviderFactoryClass() { - return CacheUserProviderFactory.class; + return UserCacheProviderFactory.class; } } diff --git a/server-spi/src/main/java/org/keycloak/models/cache/CachedUserModel.java b/server-spi/src/main/java/org/keycloak/models/cache/CachedUserModel.java new file mode 100644 index 0000000000..5d9c7cd278 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/models/cache/CachedUserModel.java @@ -0,0 +1,43 @@ +/* + * Copyright 2016 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.models.cache; + +import org.keycloak.models.UserModel; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public interface CachedUserModel extends UserModel { + void invalidate(); + + /** + * When was the user loaded from database. + * + * @return + */ + long getCacheTimestamp(); + + /** + * Returns a map that contains custom things that are cached along with the user. You can write to this map. + * + * @return + */ + ConcurrentHashMap getCachedWith(); +} diff --git a/server-spi/src/main/java/org/keycloak/models/cache/CacheUserProvider.java b/server-spi/src/main/java/org/keycloak/models/cache/OnUserCache.java old mode 100755 new mode 100644 similarity index 85% rename from server-spi/src/main/java/org/keycloak/models/cache/CacheUserProvider.java rename to server-spi/src/main/java/org/keycloak/models/cache/OnUserCache.java index 63c97061d6..e676ce14e6 --- a/server-spi/src/main/java/org/keycloak/models/cache/CacheUserProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/cache/OnUserCache.java @@ -14,17 +14,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.keycloak.models.cache; import org.keycloak.models.RealmModel; -import org.keycloak.models.UserProvider; /** * @author Bill Burke * @version $Revision: 1 $ */ -public interface CacheUserProvider extends UserProvider { - void clear(); - UserProvider getDelegate(); +public interface OnUserCache { + void onCache(RealmModel realm, CachedUserModel user); } diff --git a/server-spi/src/main/java/org/keycloak/models/cache/UserCache.java b/server-spi/src/main/java/org/keycloak/models/cache/UserCache.java new file mode 100755 index 0000000000..f309079d94 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/models/cache/UserCache.java @@ -0,0 +1,36 @@ +/* + * Copyright 2016 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.models.cache; + +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserProvider; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public interface UserCache extends UserProvider { + /** + * Evict user from cache. + * + * @param user + */ + void evict(RealmModel realm, UserModel user); + void clear(); +} diff --git a/server-spi/src/main/java/org/keycloak/models/cache/CacheUserProviderFactory.java b/server-spi/src/main/java/org/keycloak/models/cache/UserCacheProviderFactory.java similarity index 90% rename from server-spi/src/main/java/org/keycloak/models/cache/CacheUserProviderFactory.java rename to server-spi/src/main/java/org/keycloak/models/cache/UserCacheProviderFactory.java index 37208e4711..fdf4fd881d 100755 --- a/server-spi/src/main/java/org/keycloak/models/cache/CacheUserProviderFactory.java +++ b/server-spi/src/main/java/org/keycloak/models/cache/UserCacheProviderFactory.java @@ -23,6 +23,6 @@ import org.keycloak.provider.ProviderFactory; * @author Bill Burke * @version $Revision: 1 $ */ -public interface CacheUserProviderFactory extends ProviderFactory { +public interface UserCacheProviderFactory extends ProviderFactory { } diff --git a/services/src/main/java/org/keycloak/credential/LocalOTPCredentialManager.java b/services/src/main/java/org/keycloak/credential/LocalOTPCredentialManager.java new file mode 100644 index 0000000000..6d9baf0345 --- /dev/null +++ b/services/src/main/java/org/keycloak/credential/LocalOTPCredentialManager.java @@ -0,0 +1,224 @@ +/* + * Copyright 2016 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.credential; + +import org.jboss.logging.Logger; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.OTPPolicy; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.cache.CachedUserModel; +import org.keycloak.models.cache.OnUserCache; +import org.keycloak.models.utils.HmacOTP; +import org.keycloak.models.utils.TimeBasedOTP; + +import java.util.Collections; +import java.util.List; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class LocalOTPCredentialManager implements CredentialInputValidator, CredentialInputUpdater, OnUserCache { + private static final Logger logger = Logger.getLogger(LocalOTPCredentialManager.class); + + protected KeycloakSession session; + + protected List getCachedCredentials(UserModel user, String type) { + if (!(user instanceof CachedUserModel)) return Collections.EMPTY_LIST; + CachedUserModel cached = (CachedUserModel)user; + List rtn = (List)cached.getCachedWith().get(LocalOTPCredentialManager.class.getName() + "." + type); + if (rtn == null) return Collections.EMPTY_LIST; + return rtn; + } + + protected UserCredentialStore getCredentialStore() { + return session.userCredentialManager(); + } + + @Override + public void onCache(RealmModel realm, CachedUserModel user) { + List creds = getCredentialStore().getStoredCredentialsByType(realm, user, CredentialModel.TOTP); + user.getCachedWith().put(LocalOTPCredentialManager.class.getName() + "." + CredentialModel.TOTP, creds); + + } + + public LocalOTPCredentialManager(KeycloakSession session) { + this.session = session; + } + + @Override + public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) { + if (!supportsCredentialType(input.getType())) return false; + + if (!(input instanceof UserCredentialModel)) { + logger.debug("Expected instance of UserCredentialModel for CredentialInput"); + return false; + } + UserCredentialModel inputModel = (UserCredentialModel)input; + CredentialModel model = null; + if (inputModel.getDevice() != null) { + model = getCredentialStore().getStoredCredentialByNameAndType(realm, user, inputModel.getDevice(), CredentialModel.TOTP); + if (model == null) { + model = getCredentialStore().getStoredCredentialByNameAndType(realm, user, inputModel.getDevice(), CredentialModel.HOTP); + } + } + if (model == null) { + // delete all existing + disableCredentialType(realm, user, CredentialModel.OTP); + model = new CredentialModel(); + } + + OTPPolicy policy = realm.getOTPPolicy(); + model.setDigits(policy.getDigits()); + model.setCounter(policy.getInitialCounter()); + model.setAlgorithm(policy.getAlgorithm()); + model.setType(policy.getType()); + model.setValue(inputModel.getValue()); + model.setDevice(inputModel.getDevice()); + model.setPeriod(policy.getPeriod()); + if (model.getId() == null) { + getCredentialStore().createCredential(realm, user, model); + } else { + getCredentialStore().updateCredential(realm, user, model); + } + session.getUserCache().evict(realm, user); + return true; + + + + } + + @Override + public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) { + boolean disableTOTP = false, disableHOTP = false; + if (CredentialModel.OTP.equals(credentialType)) { + disableTOTP = true; + disableHOTP = true; + } else if (CredentialModel.HOTP.equals(credentialType)) { + disableHOTP = true; + + } else if (CredentialModel.TOTP.equals(credentialType)) { + disableTOTP = true; + } + if (disableHOTP) { + List hotp = getCredentialStore().getStoredCredentialsByType(realm, user, CredentialModel.HOTP); + for (CredentialModel cred : hotp) { + getCredentialStore().removeStoredCredential(realm, user, cred.getId()); + } + + } + if (disableTOTP) { + List totp = getCredentialStore().getStoredCredentialsByType(realm, user, CredentialModel.TOTP); + if (!totp.isEmpty()) { + for (CredentialModel cred : totp) { + getCredentialStore().removeStoredCredential(realm, user, cred.getId()); + } + } + + } + if (disableTOTP || disableHOTP) { + session.getUserCache().evict(realm, user); + } + } + + @Override + public boolean supportsCredentialType(String credentialType) { + return CredentialModel.OTP.equals(credentialType) + || CredentialModel.HOTP.equals(credentialType) + || CredentialModel.TOTP.equals(credentialType); + } + + @Override + public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { + if (!supportsCredentialType(credentialType)) return false; + if (CredentialModel.OTP.equals(credentialType)) { + if (realm.getOTPPolicy().getType().equals(CredentialModel.HOTP)) { + return configuredForHOTP(realm, user); + } else { + return configuredForTOTP(realm, user); + } + } else if (CredentialModel.HOTP.equals(credentialType)) { + return configuredForHOTP(realm, user); + + } else if (CredentialModel.TOTP.equals(credentialType)) { + return configuredForTOTP(realm, user); + } else { + return false; + } + + } + + protected boolean configuredForHOTP(RealmModel realm, UserModel user) { + return !getCredentialStore().getStoredCredentialsByType(realm, user, CredentialModel.HOTP).isEmpty(); + } + + protected boolean configuredForTOTP(RealmModel realm, UserModel user) { + return !getCachedCredentials(user, CredentialModel.TOTP).isEmpty() + || !getCredentialStore().getStoredCredentialsByType(realm, user, CredentialModel.TOTP).isEmpty(); + } + + public static boolean validOTP(RealmModel realm, String token, String secret) { + OTPPolicy policy = realm.getOTPPolicy(); + if (policy.getType().equals(UserCredentialModel.TOTP)) { + TimeBasedOTP validator = new TimeBasedOTP(policy.getAlgorithm(), policy.getDigits(), policy.getPeriod(), policy.getLookAheadWindow()); + return validator.validateTOTP(token, secret.getBytes()); + } else { + HmacOTP validator = new HmacOTP(policy.getDigits(), policy.getAlgorithm(), policy.getLookAheadWindow()); + int c = validator.validateHOTP(token, secret, policy.getInitialCounter()); + return c > -1; + } + + } + + @Override + public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { + if (! (input instanceof UserCredentialModel)) { + logger.debug("Expected instance of UserCredentialModel for CredentialInput"); + return false; + + } + String token = ((UserCredentialModel)input).getValue(); + OTPPolicy policy = realm.getOTPPolicy(); + if (realm.getOTPPolicy().getType().equals(CredentialModel.HOTP)) { + HmacOTP validator = new HmacOTP(policy.getDigits(), policy.getAlgorithm(), policy.getLookAheadWindow()); + for (CredentialModel cred : getCredentialStore().getStoredCredentialsByType(realm, user, CredentialModel.HOTP)) { + int counter = validator.validateHOTP(token, cred.getValue(), cred.getCounter()); + if (counter < 0) continue; + cred.setCounter(counter); + getCredentialStore().updateCredential(realm, user, cred); + return true; + } + } else { + TimeBasedOTP validator = new TimeBasedOTP(policy.getAlgorithm(), policy.getDigits(), policy.getPeriod(), policy.getLookAheadWindow()); + List creds = getCachedCredentials(user, CredentialModel.TOTP); + if (creds.isEmpty()) { + creds = getCredentialStore().getStoredCredentialsByType(realm, user, CredentialModel.TOTP); + } else { + logger.debugv("Cache hit for TOTP for user {0}", user.getUsername()); + } + for (CredentialModel cred : creds) { + if (validator.validateTOTP(token, cred.getValue().getBytes())) { + return true; + } + } + + } + return false; + } +} diff --git a/services/src/main/java/org/keycloak/credential/UserCredentialStoreManager.java b/services/src/main/java/org/keycloak/credential/UserCredentialStoreManager.java new file mode 100644 index 0000000000..5f67768378 --- /dev/null +++ b/services/src/main/java/org/keycloak/credential/UserCredentialStoreManager.java @@ -0,0 +1,249 @@ +/* + * Copyright 2016 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.credential; + +import org.keycloak.common.util.reflections.Types; +import org.keycloak.component.ComponentModel; +import org.keycloak.component.PrioritizedComponentModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserCredentialManager; +import org.keycloak.models.UserModel; +import org.keycloak.models.cache.CachedUserModel; +import org.keycloak.models.cache.OnUserCache; +import org.keycloak.storage.StorageId; +import org.keycloak.storage.UserStorageManager; +import org.keycloak.storage.UserStorageProvider; + +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class UserCredentialStoreManager implements UserCredentialManager, OnUserCache { + protected KeycloakSession session; + + public UserCredentialStoreManager(KeycloakSession session) { + this.session = session; + } + + protected UserCredentialStore getStoreForUser(UserModel user) { + if (StorageId.isLocalStorage(user)) { + return (UserCredentialStore)session.userLocalStorage(); + } else { + return (UserCredentialStore)session.userFederatedStorage(); + } + } + + @Override + public void updateCredential(RealmModel realm, UserModel user, CredentialModel cred) { + getStoreForUser(user).updateCredential(realm, user, cred); + + } + + @Override + public CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel cred) { + return getStoreForUser(user).createCredential(realm, user, cred); + } + + @Override + public boolean removeStoredCredential(RealmModel realm, UserModel user, String id) { + return getStoreForUser(user).removeStoredCredential(realm, user, id); + } + + @Override + public CredentialModel getStoredCredentialById(RealmModel realm, UserModel user, String id) { + return getStoreForUser(user).getStoredCredentialById(realm, user, id); + } + + @Override + public List getStoredCredentials(RealmModel realm, UserModel user) { + return getStoreForUser(user).getStoredCredentials(realm, user); + } + + @Override + public List getStoredCredentialsByType(RealmModel realm, UserModel user, String type) { + return getStoreForUser(user).getStoredCredentialsByType(realm, user, type); + } + + @Override + public CredentialModel getStoredCredentialByNameAndType(RealmModel realm, UserModel user, String name, String type) { + return getStoreForUser(user).getStoredCredentialByNameAndType(realm, user, name, type); + } + + + @Override + public boolean isValid(RealmModel realm, UserModel user, List inputs) { + + List toValidate = new LinkedList<>(); + toValidate.addAll(inputs); + if (!StorageId.isLocalStorage(user)) { + String providerId = StorageId.resolveProviderId(user); + UserStorageProvider provider = UserStorageManager.getStorageProvider(session, realm, providerId); + if (provider instanceof CredentialInputValidator) { + Iterator it = toValidate.iterator(); + while (it.hasNext()) { + CredentialInput input = it.next(); + CredentialInputValidator validator = (CredentialInputValidator)provider; + if (validator.supportsCredentialType(input.getType()) && validator.isValid(realm, user, input)) { + it.remove(); + } + } + } + } + + if (toValidate.isEmpty()) return true; + + List components = getCredentialProviderComponents(realm); + for (ComponentModel component : components) { + CredentialProviderFactory factory = (CredentialProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(CredentialProvider.class, component.getProviderId()); + if (!Types.supports(CredentialInputValidator.class, factory, CredentialProviderFactory.class)) continue; + Iterator it = toValidate.iterator(); + while (it.hasNext()) { + CredentialInput input = it.next(); + CredentialInputValidator validator = (CredentialInputValidator)session.getAttribute(component.getId()); + if (validator == null) { + validator = (CredentialInputValidator)factory.create(session, component); + session.setAttribute(component.getId(), validator); + } + if (validator.supportsCredentialType(input.getType()) && validator.isValid(realm, user, input)) { + it.remove(); + } + } + } + + return toValidate.isEmpty(); + } + + protected List getCredentialProviderComponents(RealmModel realm) { + List components = realm.getComponents(realm.getId(), CredentialProvider.class.getName()); + if (components.isEmpty()) return components; + List copy = new LinkedList<>(); + copy.addAll(components); + Collections.sort(copy, PrioritizedComponentModel.comparator); + return copy; + } + + @Override + public void updateCredential(RealmModel realm, UserModel user, CredentialInput input) { + if (!StorageId.isLocalStorage(user)) { + String providerId = StorageId.resolveProviderId(user); + UserStorageProvider provider = UserStorageManager.getStorageProvider(session, realm, providerId); + if (provider instanceof CredentialInputUpdater) { + CredentialInputUpdater updater = (CredentialInputUpdater)provider; + if (updater.supportsCredentialType(input.getType())) { + if (updater.updateCredential(realm, user, input)) return; + } + } + } + + List components = getCredentialProviderComponents(realm); + for (ComponentModel component : components) { + CredentialProviderFactory factory = (CredentialProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(CredentialProvider.class, component.getProviderId()); + if (!Types.supports(CredentialInputUpdater.class, factory, CredentialProviderFactory.class)) continue; + CredentialInputUpdater updater = (CredentialInputUpdater)session.getAttribute(component.getId()); + if (updater == null) { + updater = (CredentialInputUpdater)factory.create(session, component); + session.setAttribute(component.getId(), updater); + } + if (!updater.supportsCredentialType(input.getType())) continue; + if (updater.updateCredential(realm, user, input)) return; + } + + } + @Override + public void disableCredential(RealmModel realm, UserModel user, String credentialType) { + if (!StorageId.isLocalStorage(user)) { + String providerId = StorageId.resolveProviderId(user); + UserStorageProvider provider = UserStorageManager.getStorageProvider(session, realm, providerId); + if (provider instanceof CredentialInputUpdater) { + CredentialInputUpdater updater = (CredentialInputUpdater)provider; + if (updater.supportsCredentialType(credentialType)) { + updater.disableCredentialType(realm, user, credentialType); + } + } + } + + List components = getCredentialProviderComponents(realm); + for (ComponentModel component : components) { + CredentialProviderFactory factory = (CredentialProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(CredentialProvider.class, component.getProviderId()); + if (!Types.supports(CredentialInputUpdater.class, factory, CredentialProviderFactory.class)) continue; + CredentialInputUpdater updater = (CredentialInputUpdater)session.getAttribute(component.getId()); + if (updater == null) { + updater = (CredentialInputUpdater)factory.create(session, component); + session.setAttribute(component.getId(), updater); + } + if (!updater.supportsCredentialType(credentialType)) continue; + updater.disableCredentialType(realm, user, credentialType); + } + + } + @Override + public boolean isConfiguredFor(RealmModel realm, UserModel user, String type) { + if (!StorageId.isLocalStorage(user)) { + String providerId = StorageId.resolveProviderId(user); + UserStorageProvider provider = UserStorageManager.getStorageProvider(session, realm, providerId); + if (provider instanceof CredentialInputValidator) { + CredentialInputValidator validator = (CredentialInputValidator)provider; + if (validator.supportsCredentialType(type) && validator.isConfiguredFor(realm, user, type)) { + return true; + } + } + } + + List components = getCredentialProviderComponents(realm); + for (ComponentModel component : components) { + CredentialProviderFactory factory = (CredentialProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(CredentialProvider.class, component.getProviderId()); + if (!Types.supports(CredentialInputUpdater.class, factory, CredentialProviderFactory.class)) continue; + CredentialInputValidator validator = (CredentialInputValidator)session.getAttribute(component.getId()); + if (validator == null) { + validator = (CredentialInputValidator)factory.create(session, component); + session.setAttribute(component.getId(), validator); + } + if (validator.supportsCredentialType(type) && validator.isConfiguredFor(realm, user, type)) { + return true; + } + } + return false; + + } + + @Override + public void onCache(RealmModel realm, CachedUserModel user) { + List components = getCredentialProviderComponents(realm); + for (ComponentModel component : components) { + CredentialProviderFactory factory = (CredentialProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(CredentialProvider.class, component.getProviderId()); + if (!Types.supports(OnUserCache.class, factory, CredentialProviderFactory.class)) continue; + OnUserCache validator = (OnUserCache)session.getAttribute(component.getId()); + if (validator == null) { + validator = (OnUserCache)factory.create(session, component); + session.setAttribute(component.getId(), validator); + } + validator.onCache(realm, user); + } + + } + + @Override + public void close() { + + } +} diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java index 1fae787117..539bafbb5a 100644 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java @@ -16,9 +16,11 @@ */ package org.keycloak.services; +import org.keycloak.credential.UserCredentialStore; +import org.keycloak.credential.UserCredentialStoreManager; import org.keycloak.models.*; import org.keycloak.models.cache.CacheRealmProvider; -import org.keycloak.models.cache.CacheUserProvider; +import org.keycloak.models.cache.UserCache; import org.keycloak.provider.Provider; import org.keycloak.provider.ProviderFactory; import org.keycloak.scripting.ScriptingProvider; @@ -40,6 +42,7 @@ public class DefaultKeycloakSession implements KeycloakSession { private RealmProvider model; private UserProvider userModel; private UserStorageManager userStorageManager; + private UserCredentialStoreManager userCredentialStorageManager; private ScriptingProvider scriptingProvider; private UserSessionProvider sessionProvider; private UserFederationManager federationManager; @@ -68,7 +71,7 @@ public class DefaultKeycloakSession implements KeycloakSession { } private UserProvider getUserProvider() { - CacheUserProvider cache = getProvider(CacheUserProvider.class); + UserCache cache = getProvider(UserCache.class); if (cache != null) { return cache; } else { @@ -76,6 +79,12 @@ public class DefaultKeycloakSession implements KeycloakSession { } } + @Override + public UserCache getUserCache() { + return getProvider(UserCache.class); + + } + @Override public void enlistForClose(Provider provider) { closable.add(provider); @@ -125,6 +134,12 @@ public class DefaultKeycloakSession implements KeycloakSession { return userStorageManager; } + @Override + public UserCredentialManager userCredentialManager() { + if (userCredentialStorageManager == null) userCredentialStorageManager = new UserCredentialStoreManager(this); + return userCredentialStorageManager; + } + @Override public UserProvider userStorage() { if (userModel == null) { diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index 4fd941b728..7d4922340d 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -34,8 +34,6 @@ import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; import org.keycloak.exportimport.ClientDescriptionConverter; import org.keycloak.exportimport.ClientDescriptionConverterFactory; -import org.keycloak.jose.jws.JWSBuilder; -import org.keycloak.jose.jws.JWSInput; import org.keycloak.models.ClientModel; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; @@ -44,7 +42,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.cache.CacheRealmProvider; -import org.keycloak.models.cache.CacheUserProvider; +import org.keycloak.models.cache.UserCache; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.RepresentationToModel; @@ -55,7 +53,6 @@ import org.keycloak.representations.idm.AdminEventRepresentation; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.GroupRepresentation; -import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.PartialImportRepresentation; import org.keycloak.representations.idm.RealmEventsConfigRepresentation; import org.keycloak.representations.idm.RealmRepresentation; @@ -67,7 +64,6 @@ import org.keycloak.services.ServicesLogger; import org.keycloak.services.managers.UsersSyncManager; import org.keycloak.services.ErrorResponse; import org.keycloak.services.resources.admin.RealmAuth.Resource; -import org.keycloak.timer.TimerProvider; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; @@ -85,8 +81,6 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.UriInfo; -import java.security.PrivateKey; -import java.security.PublicKey; import java.security.cert.X509Certificate; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -866,7 +860,7 @@ public class RealmAdminResource { public void clearUserCache() { auth.requireManage(); - CacheUserProvider cache = session.getProvider(CacheUserProvider.class); + UserCache cache = session.getProvider(UserCache.class); if (cache != null) { cache.clear(); } diff --git a/server-spi/src/main/java/org/keycloak/storage/UserStorageManager.java b/services/src/main/java/org/keycloak/storage/UserStorageManager.java similarity index 71% rename from server-spi/src/main/java/org/keycloak/storage/UserStorageManager.java rename to services/src/main/java/org/keycloak/storage/UserStorageManager.java index 6620f49aa4..e0b2ffd3c2 100755 --- a/server-spi/src/main/java/org/keycloak/storage/UserStorageManager.java +++ b/services/src/main/java/org/keycloak/storage/UserStorageManager.java @@ -20,12 +20,6 @@ package org.keycloak.storage; import org.jboss.logging.Logger; import org.keycloak.common.util.reflections.Types; import org.keycloak.component.ComponentModel; -import org.keycloak.component.PrioritizedComponentModel; -import org.keycloak.credential.CredentialInput; -import org.keycloak.credential.CredentialInputUpdater; -import org.keycloak.credential.CredentialInputValidator; -import org.keycloak.credential.CredentialProvider; -import org.keycloak.credential.CredentialProviderFactory; import org.keycloak.models.ClientModel; import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.FederatedIdentityModel; @@ -36,6 +30,8 @@ import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserConsentModel; +import org.keycloak.models.cache.CachedUserModel; +import org.keycloak.models.cache.OnUserCache; import org.keycloak.storage.user.UserCredentialAuthenticationProvider; import org.keycloak.models.UserCredentialModel; import org.keycloak.storage.user.UserCredentialValidatorProvider; @@ -62,7 +58,7 @@ import java.util.Set; * @author Bill Burke * @version $Revision: 1 $ */ -public class UserStorageManager implements UserProvider { +public class UserStorageManager implements UserProvider, OnUserCache { private static final Logger logger = Logger.getLogger(UserStorageManager.class); @@ -76,22 +72,22 @@ public class UserStorageManager implements UserProvider { return session.userLocalStorage(); } - protected List getStorageProviders(RealmModel realm) { + public static List getStorageProviders(RealmModel realm) { return realm.getUserStorageProviders(); } - protected T getFirstStorageProvider(RealmModel realm, Class type) { + public static T getFirstStorageProvider(KeycloakSession session, RealmModel realm, Class type) { for (UserStorageProviderModel model : getStorageProviders(realm)) { UserStorageProviderFactory factory = (UserStorageProviderFactory)session.getKeycloakSessionFactory().getProviderFactory(UserStorageProvider.class, model.getProviderId()); if (Types.supports(type, factory, UserStorageProviderFactory.class)) { - return type.cast(getStorageProviderInstance(model, factory)); + return type.cast(getStorageProviderInstance(session, model, factory)); } } return null; } - protected UserStorageProvider getStorageProviderInstance(UserStorageProviderModel model, UserStorageProviderFactory factory) { + public static UserStorageProvider getStorageProviderInstance(KeycloakSession session, UserStorageProviderModel model, UserStorageProviderFactory factory) { UserStorageProvider instance = (UserStorageProvider)session.getAttribute(model.getId()); if (instance != null) return instance; instance = factory.create(session, model); @@ -101,12 +97,12 @@ public class UserStorageManager implements UserProvider { } - protected List getStorageProviders(RealmModel realm, Class type) { + public static List getStorageProviders(KeycloakSession session, RealmModel realm, Class type) { List list = new LinkedList<>(); for (UserStorageProviderModel model : getStorageProviders(realm)) { UserStorageProviderFactory factory = (UserStorageProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(UserStorageProvider.class, model.getProviderId()); if (Types.supports(type, factory, UserStorageProviderFactory.class)) { - list.add(type.cast(getStorageProviderInstance(model, factory))); + list.add(type.cast(getStorageProviderInstance(session, model, factory))); } @@ -122,21 +118,21 @@ public class UserStorageManager implements UserProvider { @Override public UserModel addUser(RealmModel realm, String username) { - UserRegistrationProvider registry = getFirstStorageProvider(realm, UserRegistrationProvider.class); + UserRegistrationProvider registry = getFirstStorageProvider(session, realm, UserRegistrationProvider.class); if (registry != null) { return registry.addUser(realm, username); } return localStorage().addUser(realm, username.toLowerCase()); } - public UserStorageProvider getStorageProvider(RealmModel realm, String componentId) { + public static UserStorageProvider getStorageProvider(KeycloakSession session, RealmModel realm, String componentId) { ComponentModel model = realm.getComponent(componentId); if (model == null) return null; UserStorageProviderFactory factory = (UserStorageProviderFactory)session.getKeycloakSessionFactory().getProviderFactory(UserStorageProvider.class, model.getProviderId()); if (factory == null) { throw new ModelException("Could not find UserStorageProviderFactory for: " + model.getProviderId()); } - return getStorageProviderInstance(new UserStorageProviderModel(model), factory); + return getStorageProviderInstance(session, new UserStorageProviderModel(model), factory); } @Override @@ -146,7 +142,7 @@ public class UserStorageManager implements UserProvider { if (storageId.getProviderId() == null) { return localStorage().removeUser(realm, user); } - UserRegistrationProvider registry = (UserRegistrationProvider)getStorageProvider(realm, storageId.getProviderId()); + UserRegistrationProvider registry = (UserRegistrationProvider)getStorageProvider(session, realm, storageId.getProviderId()); if (registry == null) { throw new ModelException("Could not resolve StorageProvider: " + storageId.getProviderId()); } @@ -239,7 +235,7 @@ public class UserStorageManager implements UserProvider { if (storageId.getProviderId() == null) { return localStorage().getUserById(id, realm); } - UserLookupProvider provider = (UserLookupProvider)getStorageProvider(realm, storageId.getProviderId()); + UserLookupProvider provider = (UserLookupProvider)getStorageProvider(session, realm, storageId.getProviderId()); return provider.getUserById(id, realm); } @@ -252,7 +248,7 @@ public class UserStorageManager implements UserProvider { public UserModel getUserByUsername(String username, RealmModel realm) { UserModel user = localStorage().getUserByUsername(username, realm); if (user != null) return user; - for (UserLookupProvider provider : getStorageProviders(realm, UserLookupProvider.class)) { + for (UserLookupProvider provider : getStorageProviders(session, realm, UserLookupProvider.class)) { user = provider.getUserByUsername(username, realm); if (user != null) return user; } @@ -263,7 +259,7 @@ public class UserStorageManager implements UserProvider { public UserModel getUserByEmail(String email, RealmModel realm) { UserModel user = localStorage().getUserByEmail(email, realm); if (user != null) return user; - for (UserLookupProvider provider : getStorageProviders(realm, UserLookupProvider.class)) { + for (UserLookupProvider provider : getStorageProviders(session, realm, UserLookupProvider.class)) { user = provider.getUserByEmail(email, realm); if (user != null) return user; } @@ -305,7 +301,7 @@ public class UserStorageManager implements UserProvider { @Override public int getUsersCount(RealmModel realm) { int size = localStorage().getUsersCount(realm); - for (UserQueryProvider provider : getStorageProviders(realm, UserQueryProvider.class)) { + for (UserQueryProvider provider : getStorageProviders(session, realm, UserQueryProvider.class)) { size += provider.getUsersCount(realm); } return size; @@ -321,7 +317,7 @@ public class UserStorageManager implements UserProvider { - List storageProviders = getStorageProviders(realm, UserQueryProvider.class); + List storageProviders = getStorageProviders(session, realm, UserQueryProvider.class); // we can skip rest of method if there are no storage providers if (storageProviders.isEmpty()) { return pagedQuery.query(localStorage(), firstResult, maxResults); @@ -453,7 +449,7 @@ public class UserStorageManager implements UserProvider { @Override public void grantToAllUsers(RealmModel realm, RoleModel role) { // not federation-aware for now - List storageProviders = getStorageProviders(realm, UserRegistrationProvider.class); + List storageProviders = getStorageProviders(session, realm, UserRegistrationProvider.class); LinkedList providers = new LinkedList<>(); providers.add(localStorage()); providers.addAll(storageProviders); @@ -488,7 +484,7 @@ public class UserStorageManager implements UserProvider { public void preRemove(RealmModel realm) { localStorage().preRemove(realm); getFederatedStorage().preRemove(realm); - for (UserStorageProvider provider : getStorageProviders(realm, UserStorageProvider.class)) { + for (UserStorageProvider provider : getStorageProviders(session, realm, UserStorageProvider.class)) { provider.preRemove(realm); } } @@ -503,7 +499,7 @@ public class UserStorageManager implements UserProvider { public void preRemove(RealmModel realm, GroupModel group) { localStorage().preRemove(realm, group); getFederatedStorage().preRemove(realm, group); - for (UserStorageProvider provider : getStorageProviders(realm, UserStorageProvider.class)) { + for (UserStorageProvider provider : getStorageProviders(session, realm, UserStorageProvider.class)) { provider.preRemove(realm, group); } } @@ -512,7 +508,7 @@ public class UserStorageManager implements UserProvider { public void preRemove(RealmModel realm, RoleModel role) { localStorage().preRemove(realm, role); getFederatedStorage().preRemove(realm, role); - for (UserStorageProvider provider : getStorageProviders(realm, UserStorageProvider.class)) { + for (UserStorageProvider provider : getStorageProviders(session, realm, UserStorageProvider.class)) { provider.preRemove(realm, role); } } @@ -564,7 +560,7 @@ public class UserStorageManager implements UserProvider { if (toValidate.isEmpty()) return true; - UserStorageProvider provider = getStorageProvider(realm, StorageId.resolveProviderId(user)); + UserStorageProvider provider = getStorageProvider(session, realm, StorageId.resolveProviderId(user)); if (!(provider instanceof UserCredentialValidatorProvider)) { return false; } @@ -578,7 +574,7 @@ public class UserStorageManager implements UserProvider { @Override public CredentialValidationOutput validCredentials(KeycloakSession session, RealmModel realm, UserCredentialModel... input) { - List providers = getStorageProviders(realm, UserCredentialAuthenticationProvider.class); + List providers = getStorageProviders(session, realm, UserCredentialAuthenticationProvider.class); if (providers.isEmpty()) return CredentialValidationOutput.failed(); CredentialValidationOutput result = null; @@ -612,154 +608,23 @@ public class UserStorageManager implements UserProvider { } + @Override + public void onCache(RealmModel realm, CachedUserModel user) { + if (StorageId.isLocalStorage(user)) { + if (session.userLocalStorage() instanceof OnUserCache) { + ((OnUserCache)session.userLocalStorage()).onCache(realm, user); + } + } else { + Object provider = getStorageProvider(session, realm, StorageId.resolveProviderId(user)); + if (provider != null && provider instanceof OnUserCache) { + ((OnUserCache)provider).onCache(realm, user); + } + } + } + @Override public void close() { } - @Override - public boolean isValid(RealmModel realm, UserModel user, List inputs) { - - List toValidate = new LinkedList<>(); - toValidate.addAll(inputs); - if (!StorageId.isLocalStorage(user)) { - String providerId = StorageId.resolveProviderId(user); - UserStorageProvider provider = getStorageProvider(realm, providerId); - if (provider instanceof CredentialInputValidator) { - Iterator it = toValidate.iterator(); - while (it.hasNext()) { - CredentialInput input = it.next(); - CredentialInputValidator validator = (CredentialInputValidator)provider; - if (validator.supportsCredentialType(input.getType()) && validator.isValid(realm, user, input)) { - it.remove(); - } - } - } - } - - if (toValidate.isEmpty()) return true; - - List components = getCredentialProviderComponents(realm); - for (ComponentModel component : components) { - CredentialProviderFactory factory = (CredentialProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(CredentialProvider.class, component.getProviderId()); - if (!Types.supports(CredentialInputValidator.class, factory, CredentialProviderFactory.class)) continue; - Iterator it = toValidate.iterator(); - while (it.hasNext()) { - CredentialInput input = it.next(); - CredentialInputValidator validator = (CredentialInputValidator)session.getAttribute(component.getId()); - if (validator == null) { - validator = (CredentialInputValidator)factory.create(session, component); - session.setAttribute(component.getId(), validator); - } - if (validator.supportsCredentialType(input.getType()) && validator.isValid(realm, user, input)) { - it.remove(); - } - } - } - - return toValidate.isEmpty(); - } - - protected List getCredentialProviderComponents(RealmModel realm) { - List components = realm.getComponents(realm.getId(), CredentialProvider.class.getName()); - Collections.sort(components, PrioritizedComponentModel.comparator); - return components; - } - - @Override - public void updateCredential(RealmModel realm, UserModel user, CredentialInput input) { - if (!StorageId.isLocalStorage(user)) { - String providerId = StorageId.resolveProviderId(user); - UserStorageProvider provider = getStorageProvider(realm, providerId); - if (provider instanceof CredentialInputUpdater) { - CredentialInputUpdater updater = (CredentialInputUpdater)provider; - if (updater.supportsCredentialType(input.getType())) { - updater.updateCredential(realm, user, input); - return; - } - } - } - - List components = getCredentialProviderComponents(realm); - for (ComponentModel component : components) { - CredentialProviderFactory factory = (CredentialProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(CredentialProvider.class, component.getProviderId()); - if (!Types.supports(CredentialInputUpdater.class, factory, CredentialProviderFactory.class)) continue; - CredentialInputUpdater updater = (CredentialInputUpdater)session.getAttribute(component.getId()); - if (updater == null) { - updater = (CredentialInputUpdater)factory.create(session, component); - session.setAttribute(component.getId(), updater); - } - if (!updater.supportsCredentialType(input.getType())) continue; - updater.updateCredential(realm, user, input); - return; - } - - } - @Override - public boolean isConfiguredFor(RealmModel realm, UserModel user, String type) { - if (!StorageId.isLocalStorage(user)) { - String providerId = StorageId.resolveProviderId(user); - UserStorageProvider provider = getStorageProvider(realm, providerId); - if (provider instanceof CredentialInputValidator) { - CredentialInputValidator validator = (CredentialInputValidator)provider; - if (validator.supportsCredentialType(type) && validator.isConfiguredFor(realm, user, type)) { - return true; - } - } - } - - List components = getCredentialProviderComponents(realm); - for (ComponentModel component : components) { - CredentialProviderFactory factory = (CredentialProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(CredentialProvider.class, component.getProviderId()); - if (!Types.supports(CredentialInputUpdater.class, factory, CredentialProviderFactory.class)) continue; - CredentialInputValidator validator = (CredentialInputValidator)session.getAttribute(component.getId()); - if (validator == null) { - validator = (CredentialInputValidator)factory.create(session, component); - session.setAttribute(component.getId(), validator); - } - if (validator.supportsCredentialType(type) && validator.isConfiguredFor(realm, user, type)) { - return true; - } - } - return false; - - } - - @Override - public Set requiredActionsFor(RealmModel realm, UserModel user, String type) { - Set requiredActionsFor = Collections.EMPTY_SET; - if (!StorageId.isLocalStorage(user)) { - String providerId = StorageId.resolveProviderId(user); - UserStorageProvider provider = getStorageProvider(realm, providerId); - if (provider instanceof CredentialInputUpdater) { - CredentialInputUpdater updater = (CredentialInputUpdater)provider; - if (updater.supportsCredentialType(type)) { - requiredActionsFor = updater.requiredActionsFor(realm, user, type); - if (!requiredActionsFor.isEmpty()) { - return requiredActionsFor; - } - } - } - } - - List components = getCredentialProviderComponents(realm); - for (ComponentModel component : components) { - CredentialProviderFactory factory = (CredentialProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(CredentialProvider.class, component.getProviderId()); - if (!Types.supports(CredentialInputUpdater.class, factory, CredentialProviderFactory.class)) continue; - CredentialInputUpdater updater = (CredentialInputUpdater)session.getAttribute(component.getId()); - if (updater == null) { - updater = (CredentialInputUpdater)factory.create(session, component); - session.setAttribute(component.getId(), updater); - } - if (updater.supportsCredentialType(type)) { - requiredActionsFor = updater.requiredActionsFor(realm, user, type); - if (!requiredActionsFor.isEmpty()) { - return requiredActionsFor; - } - } - } - return requiredActionsFor; - - } - }