diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java index 77c2c45bd7..83c4f8abd9 100755 --- a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java @@ -67,6 +67,7 @@ public class ClientRepresentation { private Boolean useTemplateMappers; private ResourceServerRepresentation authorizationSettings; private Map access; + protected String origin; public String getId() { @@ -384,4 +385,19 @@ public class ClientRepresentation { public void setAccess(Map access) { this.access = access; } + + + /** + * Returns id of ClientStorageProvider that loaded this user + * + * @return NULL if user stored locally + */ + public String getOrigin() { + return origin; + } + + public void setOrigin(String origin) { + this.origin = origin; + } + } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java index a5823ceeed..4c28ad69e5 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java @@ -23,6 +23,7 @@ import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleModel; +import org.keycloak.models.cache.CachedObject; import org.keycloak.models.cache.infinispan.entities.CachedClient; import java.security.MessageDigest; @@ -36,17 +37,15 @@ import java.util.Set; * @author Bill Burke * @version $Revision: 1 $ */ -public class ClientAdapter implements ClientModel { +public class ClientAdapter implements ClientModel, CachedObject { protected RealmCacheSession cacheSession; protected RealmModel cachedRealm; - protected RealmCache cache; protected ClientModel updated; protected CachedClient cached; - public ClientAdapter(RealmModel cachedRealm, CachedClient cached, RealmCacheSession cacheSession, RealmCache cache) { + public ClientAdapter(RealmModel cachedRealm, CachedClient cached, RealmCacheSession cacheSession) { this.cachedRealm = cachedRealm; - this.cache = cache; this.cacheSession = cacheSession; this.cached = cached; } @@ -54,7 +53,7 @@ public class ClientAdapter implements ClientModel { private void getDelegateForUpdate() { if (updated == null) { cacheSession.registerClientInvalidation(cached.getId(), cached.getClientId(), cachedRealm.getId()); - updated = cacheSession.getDelegate().getClientById(cached.getId(), cachedRealm); + updated = cacheSession.getRealmDelegate().getClientById(cached.getId(), cachedRealm); if (updated == null) throw new IllegalStateException("Not found in database"); } } @@ -66,11 +65,16 @@ public class ClientAdapter implements ClientModel { protected boolean isUpdated() { if (updated != null) return true; if (!invalidated) return false; - updated = cacheSession.getDelegate().getClientById(cached.getId(), cachedRealm); + updated = cacheSession.getRealmDelegate().getClientById(cached.getId(), cachedRealm); if (updated == null) throw new IllegalStateException("Not found in database"); return true; } + @Override + public long getCacheTimestamp() { + return cached.getCacheTimestamp(); + } + @Override public void updateClient() { if (updated != null) updated.updateClient(); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java index a521ef2545..4d2ce42654 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java @@ -50,7 +50,7 @@ public class ClientTemplateAdapter implements ClientTemplateModel { private void getDelegateForUpdate() { if (updated == null) { cacheSession.registerClientTemplateInvalidation(cached.getId()); - updated = cacheSession.getDelegate().getClientTemplateById(cached.getId(), cachedRealm); + updated = cacheSession.getRealmDelegate().getClientTemplateById(cached.getId(), cachedRealm); if (updated == null) throw new IllegalStateException("Not found in database"); } } @@ -63,7 +63,7 @@ public class ClientTemplateAdapter implements ClientTemplateModel { protected boolean isUpdated() { if (updated != null) return true; if (!invalidated) return false; - updated = cacheSession.getDelegate().getClientTemplateById(cached.getId(), cachedRealm); + updated = cacheSession.getRealmDelegate().getClientTemplateById(cached.getId(), cachedRealm); if (updated == null) throw new IllegalStateException("Not found in database"); return true; } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java index 21bcc66ed6..0e69c68e10 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java @@ -51,7 +51,7 @@ public class GroupAdapter implements GroupModel { protected void getDelegateForUpdate() { if (updated == null) { cacheSession.registerGroupInvalidation(cached.getId()); - updated = cacheSession.getDelegate().getGroupById(cached.getId(), realm); + updated = cacheSession.getRealmDelegate().getGroupById(cached.getId(), realm); if (updated == null) throw new IllegalStateException("Not found in database"); } } @@ -64,7 +64,7 @@ public class GroupAdapter implements GroupModel { protected boolean isUpdated() { if (updated != null) return true; if (!invalidated) return false; - updated = cacheSession.getDelegate().getGroupById(cached.getId(), realm); + updated = cacheSession.getRealmDelegate().getGroupById(cached.getId(), realm); if (updated == null) throw new IllegalStateException("Not found in database"); return true; } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java index bfa00e0b43..dd623775f7 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java @@ -24,6 +24,7 @@ import org.keycloak.models.*; import org.keycloak.models.cache.CachedRealmModel; import org.keycloak.models.cache.infinispan.entities.CachedRealm; import org.keycloak.storage.UserStorageProvider; +import org.keycloak.storage.client.ClientStorageProvider; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -36,7 +37,6 @@ public class RealmAdapter implements CachedRealmModel { protected CachedRealm cached; protected RealmCacheSession cacheSession; protected volatile RealmModel updated; - protected RealmCache cache; protected KeycloakSession session; public RealmAdapter(KeycloakSession session, CachedRealm cached, RealmCacheSession cacheSession) { @@ -49,7 +49,7 @@ public class RealmAdapter implements CachedRealmModel { public RealmModel getDelegateForUpdate() { if (updated == null) { cacheSession.registerRealmInvalidation(cached.getId(), cached.getName()); - updated = cacheSession.getDelegate().getRealm(cached.getId()); + updated = cacheSession.getRealmDelegate().getRealm(cached.getId()); if (updated == null) throw new IllegalStateException("Not found in database"); } return updated; @@ -81,7 +81,7 @@ public class RealmAdapter implements CachedRealmModel { protected boolean isUpdated() { if (updated != null) return true; if (!invalidated) return false; - updated = cacheSession.getDelegate().getRealm(cached.getId()); + updated = cacheSession.getRealmDelegate().getRealm(cached.getId()); if (updated == null) throw new IllegalStateException("Not found in database"); return true; } @@ -1323,35 +1323,43 @@ public class RealmAdapter implements CachedRealmModel { @Override public ComponentModel addComponentModel(ComponentModel model) { getDelegateForUpdate(); - evictUsers(model); + executeEvictions(model); return updated.addComponentModel(model); } @Override public ComponentModel importComponentModel(ComponentModel model) { getDelegateForUpdate(); - evictUsers(model); + executeEvictions(model); return updated.importComponentModel(model); } - public void evictUsers(ComponentModel model) { - String parentId = model.getParentId(); - evictUsers(parentId); - } - - public void evictUsers(String parentId) { - if (parentId != null && !parentId.equals(getId())) { - ComponentModel parent = getComponent(parentId); + public void executeEvictions(ComponentModel model) { + if (model == null) return; + // If not realm component, check to see if it is a user storage provider child component (i.e. LDAP mapper) + if (model.getParentId() != null && !model.getParentId().equals(getId())) { + ComponentModel parent = getComponent(model.getParentId()); if (parent != null && UserStorageProvider.class.getName().equals(parent.getProviderType())) { session.userCache().evict(this); } + return; + } + + // invalidate entire user cache if we're dealing with user storage SPI + if (UserStorageProvider.class.getName().equals(model.getProviderType())) { + session.userCache().evict(this); + } + // invalidate entire realm if we're dealing with client storage SPI + // entire realm because of client roles, client lists, and clients + if (ClientStorageProvider.class.getName().equals(model.getProviderType())) { + cacheSession.evictRealmOnRemoval(this); } } @Override public void updateComponent(ComponentModel component) { getDelegateForUpdate(); - evictUsers(component); + executeEvictions(component); updated.updateComponent(component); } @@ -1359,7 +1367,7 @@ public class RealmAdapter implements CachedRealmModel { @Override public void removeComponent(ComponentModel component) { getDelegateForUpdate(); - evictUsers(component); + executeEvictions(component); updated.removeComponent(component); } @@ -1367,7 +1375,6 @@ public class RealmAdapter implements CachedRealmModel { @Override public void removeComponents(String parentId) { getDelegateForUpdate(); - evictUsers(parentId); updated.removeComponents(parentId); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCache.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCache.java deleted file mode 100755 index d683bb015b..0000000000 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCache.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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; - -import org.keycloak.models.cache.infinispan.entities.CachedClient; -import org.keycloak.models.cache.infinispan.entities.CachedClientTemplate; -import org.keycloak.models.cache.infinispan.entities.CachedGroup; -import org.keycloak.models.cache.infinispan.entities.CachedRealm; -import org.keycloak.models.cache.infinispan.entities.CachedRole; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public interface RealmCache { - void clear(); - - CachedRealm getRealm(String id); - - void invalidateRealm(CachedRealm realm); - - void addRealm(CachedRealm realm); - - CachedRealm getRealmByName(String name); - - void invalidateRealmById(String id); - - CachedClient getClient(String id); - - void invalidateClient(CachedClient app); - - void evictClientById(String id); - - void addClient(CachedClient app); - - void invalidateClientById(String id); - - CachedRole getRole(String id); - - void invalidateRole(CachedRole role); - - void evictRoleById(String id); - - void addRole(CachedRole role); - - void invalidateRoleById(String id); - - CachedGroup getGroup(String id); - - void invalidateGroup(CachedGroup role); - - void addGroup(CachedGroup role); - - void invalidateGroupById(String id); - - CachedClientTemplate getClientTemplate(String id); - - void invalidateClientTemplate(CachedClientTemplate app); - - void evictClientTemplateById(String id); - - void addClientTemplate(CachedClientTemplate app); - - void invalidateClientTemplateById(String id); - -} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java index 83bafb116d..018213f6a8 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java @@ -19,6 +19,7 @@ package org.keycloak.models.cache.infinispan; import org.jboss.logging.Logger; import org.keycloak.cluster.ClusterProvider; +import org.keycloak.component.ComponentModel; import org.keycloak.migration.MigrationModel; import org.keycloak.models.*; import org.keycloak.models.cache.CacheRealmProvider; @@ -26,6 +27,9 @@ import org.keycloak.models.cache.CachedRealmModel; import org.keycloak.models.cache.infinispan.entities.*; import org.keycloak.models.cache.infinispan.events.*; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.storage.CacheableStorageProviderModel; +import org.keycloak.storage.StorageId; +import org.keycloak.storage.client.ClientStorageProviderModel; import java.util.*; @@ -94,12 +98,13 @@ public class RealmCacheSession implements CacheRealmProvider { public static final String ROLES_QUERY_SUFFIX = ".roles"; protected RealmCacheManager cache; protected KeycloakSession session; - protected RealmProvider delegate; + protected RealmProvider realmDelegate; + protected ClientProvider clientDelegate; protected boolean transactionActive; protected boolean setRollbackOnly; protected Map managedRealms = new HashMap<>(); - protected Map managedApplications = new HashMap<>(); + protected Map managedApplications = new HashMap<>(); protected Map managedClientTemplates = new HashMap<>(); protected Map managedRoles = new HashMap<>(); protected Map managedGroups = new HashMap<>(); @@ -134,16 +139,25 @@ public class RealmCacheSession implements CacheRealmProvider { @Override public MigrationModel getMigrationModel() { - return getDelegate().getMigrationModel(); + return getRealmDelegate().getMigrationModel(); } @Override - public RealmProvider getDelegate() { + public RealmProvider getRealmDelegate() { if (!transactionActive) throw new IllegalStateException("Cannot access delegate without a transaction"); - if (delegate != null) return delegate; - delegate = session.getProvider(RealmProvider.class); - return delegate; + if (realmDelegate != null) return realmDelegate; + realmDelegate = session.realmLocalStorage(); + return realmDelegate; } + public ClientProvider getClientDelegate() { + if (!transactionActive) throw new IllegalStateException("Cannot access delegate without a transaction"); + if (clientDelegate != null) return clientDelegate; + clientDelegate = session.clientStorageManager(); + return clientDelegate; + } + + + @Override public void registerRealmInvalidation(String id, String name) { @@ -163,8 +177,8 @@ public class RealmCacheSession implements CacheRealmProvider { private void invalidateClient(String id) { invalidations.add(id); - ClientAdapter adapter = managedApplications.get(id); - if (adapter != null) adapter.invalidate(); + ClientModel adapter = managedApplications.get(id); + if (adapter != null && adapter instanceof ClientAdapter) ((ClientAdapter)adapter).invalidate(); } @Override @@ -194,9 +208,9 @@ public class RealmCacheSession implements CacheRealmProvider { invalidations.addAll(newInvalidations); // need to make sure that scope and group mapping clients and groups are invalidated for (String id : newInvalidations) { - ClientAdapter adapter = managedApplications.get(id); - if (adapter != null) { - adapter.invalidate(); + ClientModel adapter = managedApplications.get(id); + if (adapter != null && adapter instanceof ClientAdapter){ + ((ClientAdapter)adapter).invalidate(); continue; } GroupAdapter group = managedGroups.get(id); @@ -319,7 +333,6 @@ public class RealmCacheSession implements CacheRealmProvider { @Override public void commit() { try { - if (delegate == null) return; if (clearAll) { cache.clear(); } @@ -360,14 +373,14 @@ public class RealmCacheSession implements CacheRealmProvider { @Override public RealmModel createRealm(String name) { - RealmModel realm = getDelegate().createRealm(name); + RealmModel realm = getRealmDelegate().createRealm(name); registerRealmInvalidation(realm.getId(), realm.getName()); return realm; } @Override public RealmModel createRealm(String id, String name) { - RealmModel realm = getDelegate().createRealm(id, name); + RealmModel realm = getRealmDelegate().createRealm(id, name); registerRealmInvalidation(realm.getId(), realm.getName()); return realm; } @@ -381,14 +394,14 @@ public class RealmCacheSession implements CacheRealmProvider { boolean wasCached = false; if (cached == null) { Long loaded = cache.getCurrentRevision(id); - RealmModel model = getDelegate().getRealm(id); + RealmModel model = getRealmDelegate().getRealm(id); if (model == null) return null; if (invalidations.contains(id)) return model; cached = new CachedRealm(loaded, model); cache.addRevisioned(cached, startupRevision); wasCached =true; } else if (invalidations.contains(id)) { - return getDelegate().getRealm(id); + return getRealmDelegate().getRealm(id); } else if (managedRealms.containsKey(id)) { return managedRealms.get(id); } @@ -420,18 +433,18 @@ public class RealmCacheSession implements CacheRealmProvider { } if (query == null) { Long loaded = cache.getCurrentRevision(cacheKey); - RealmModel model = getDelegate().getRealmByName(name); + RealmModel model = getRealmDelegate().getRealmByName(name); if (model == null) return null; if (invalidations.contains(model.getId())) return model; query = new RealmListQuery(loaded, cacheKey, model.getId()); cache.addRevisioned(query, startupRevision); return model; } else if (invalidations.contains(cacheKey)) { - return getDelegate().getRealmByName(name); + return getRealmDelegate().getRealmByName(name); } else { String realmId = query.getRealms().iterator().next(); if (invalidations.contains(realmId)) { - return getDelegate().getRealmByName(name); + return getRealmDelegate().getRealmByName(name); } return getRealm(realmId); } @@ -444,7 +457,7 @@ public class RealmCacheSession implements CacheRealmProvider { @Override public List getRealms() { // Retrieve realms from backend - List backendRealms = getDelegate().getRealms(); + List backendRealms = getRealmDelegate().getRealms(); // Return cache delegates to ensure cache invalidated during write operations List cachedRealms = new LinkedList(); @@ -460,22 +473,26 @@ public class RealmCacheSession implements CacheRealmProvider { RealmModel realm = getRealm(id); if (realm == null) return false; - cache.invalidateObject(id); - invalidationEvents.add(RealmRemovedEvent.create(id, realm.getName())); - cache.realmRemoval(id, realm.getName(), invalidations); - return getDelegate().removeRealm(id); + evictRealmOnRemoval(realm); + return getRealmDelegate().removeRealm(id); + } + + public void evictRealmOnRemoval(RealmModel realm) { + cache.invalidateObject(realm.getId()); + invalidationEvents.add(RealmRemovedEvent.create(realm.getId(), realm.getName())); + cache.realmRemoval(realm.getId(), realm.getName(), invalidations); } @Override public ClientModel addClient(RealmModel realm, String clientId) { - ClientModel client = getDelegate().addClient(realm, clientId); + ClientModel client = getRealmDelegate().addClient(realm, clientId); return addedClient(realm, client); } @Override public ClientModel addClient(RealmModel realm, String id, String clientId) { - ClientModel client = getDelegate().addClient(realm, id, clientId); + ClientModel client = getRealmDelegate().addClient(realm, id, clientId); return addedClient(realm, client); } @@ -515,7 +532,7 @@ public class RealmCacheSession implements CacheRealmProvider { String cacheKey = getRealmClientsQueryCacheKey(realm.getId()); boolean queryDB = invalidations.contains(cacheKey) || listInvalidations.contains(realm.getId()); if (queryDB) { - return getDelegate().getClients(realm); + return getClientDelegate().getClients(realm); } ClientListQuery query = cache.get(cacheKey, ClientListQuery.class); @@ -525,7 +542,7 @@ public class RealmCacheSession implements CacheRealmProvider { if (query == null) { Long loaded = cache.getCurrentRevision(cacheKey); - List model = getDelegate().getClients(realm); + List model = getClientDelegate().getClients(realm); if (model == null) return null; Set ids = new HashSet<>(); for (ClientModel client : model) ids.add(client.getId()); @@ -540,7 +557,7 @@ public class RealmCacheSession implements CacheRealmProvider { if (client == null) { // TODO: Handle with cluster invalidations too invalidations.add(cacheKey); - return getDelegate().getClients(realm); + return getRealmDelegate().getClients(realm); } list.add(client); } @@ -563,13 +580,14 @@ public class RealmCacheSession implements CacheRealmProvider { for (RoleModel role : client.getRoles()) { roleRemovalInvalidations(role.getId(), role.getName(), client.getId()); } - return getDelegate().removeClient(id, realm); + return getRealmDelegate().removeClient(id, realm); } @Override public void close() { - if (delegate != null) delegate.close(); + if (realmDelegate != null) realmDelegate.close(); + if (clientDelegate != null) clientDelegate.close(); } @Override @@ -579,7 +597,7 @@ public class RealmCacheSession implements CacheRealmProvider { @Override public RoleModel addRealmRole(RealmModel realm, String id, String name) { - RoleModel role = getDelegate().addRealmRole(realm, id, name); + RoleModel role = getRealmDelegate().addRealmRole(realm, id, name); addedRole(role.getId(), realm.getId()); return role; } @@ -589,7 +607,7 @@ public class RealmCacheSession implements CacheRealmProvider { String cacheKey = getRolesCacheKey(realm.getId()); boolean queryDB = invalidations.contains(cacheKey) || listInvalidations.contains(realm.getId()); if (queryDB) { - return getDelegate().getRealmRoles(realm); + return getRealmDelegate().getRealmRoles(realm); } RoleListQuery query = cache.get(cacheKey, RoleListQuery.class); @@ -599,7 +617,7 @@ public class RealmCacheSession implements CacheRealmProvider { if (query == null) { Long loaded = cache.getCurrentRevision(cacheKey); - Set model = getDelegate().getRealmRoles(realm); + Set model = getRealmDelegate().getRealmRoles(realm); if (model == null) return null; Set ids = new HashSet<>(); for (RoleModel role : model) ids.add(role.getId()); @@ -613,7 +631,7 @@ public class RealmCacheSession implements CacheRealmProvider { RoleModel role = session.realms().getRoleById(id, realm); if (role == null) { invalidations.add(cacheKey); - return getDelegate().getRealmRoles(realm); + return getRealmDelegate().getRealmRoles(realm); } list.add(role); } @@ -625,7 +643,7 @@ public class RealmCacheSession implements CacheRealmProvider { String cacheKey = getRolesCacheKey(client.getId()); boolean queryDB = invalidations.contains(cacheKey) || listInvalidations.contains(client.getId()); if (queryDB) { - return getDelegate().getClientRoles(realm, client); + return getRealmDelegate().getClientRoles(realm, client); } RoleListQuery query = cache.get(cacheKey, RoleListQuery.class); @@ -635,7 +653,7 @@ public class RealmCacheSession implements CacheRealmProvider { if (query == null) { Long loaded = cache.getCurrentRevision(cacheKey); - Set model = getDelegate().getClientRoles(realm, client); + Set model = getRealmDelegate().getClientRoles(realm, client); if (model == null) return null; Set ids = new HashSet<>(); for (RoleModel role : model) ids.add(role.getId()); @@ -649,7 +667,7 @@ public class RealmCacheSession implements CacheRealmProvider { RoleModel role = session.realms().getRoleById(id, realm); if (role == null) { invalidations.add(cacheKey); - return getDelegate().getClientRoles(realm, client); + return getRealmDelegate().getClientRoles(realm, client); } list.add(role); } @@ -663,7 +681,7 @@ public class RealmCacheSession implements CacheRealmProvider { @Override public RoleModel addClientRole(RealmModel realm, ClientModel client, String id, String name) { - RoleModel role = getDelegate().addClientRole(realm, client, id, name); + RoleModel role = getRealmDelegate().addClientRole(realm, client, id, name); addedRole(role.getId(), client.getId()); return role; } @@ -673,7 +691,7 @@ public class RealmCacheSession implements CacheRealmProvider { String cacheKey = getRoleByNameCacheKey(realm.getId(), name); boolean queryDB = invalidations.contains(cacheKey) || listInvalidations.contains(realm.getId()); if (queryDB) { - return getDelegate().getRealmRole(realm, name); + return getRealmDelegate().getRealmRole(realm, name); } RoleListQuery query = cache.get(cacheKey, RoleListQuery.class); @@ -683,7 +701,7 @@ public class RealmCacheSession implements CacheRealmProvider { if (query == null) { Long loaded = cache.getCurrentRevision(cacheKey); - RoleModel model = getDelegate().getRealmRole(realm, name); + RoleModel model = getRealmDelegate().getRealmRole(realm, name); if (model == null) return null; query = new RoleListQuery(loaded, cacheKey, realm, model.getId()); logger.tracev("adding realm role cache miss: client {0} key {1}", realm.getName(), cacheKey); @@ -693,7 +711,7 @@ public class RealmCacheSession implements CacheRealmProvider { RoleModel role = getRoleById(query.getRoles().iterator().next(), realm); if (role == null) { invalidations.add(cacheKey); - return getDelegate().getRealmRole(realm, name); + return getRealmDelegate().getRealmRole(realm, name); } return role; } @@ -703,7 +721,7 @@ public class RealmCacheSession implements CacheRealmProvider { String cacheKey = getRoleByNameCacheKey(client.getId(), name); boolean queryDB = invalidations.contains(cacheKey) || listInvalidations.contains(client.getId()); if (queryDB) { - return getDelegate().getClientRole(realm, client, name); + return getRealmDelegate().getClientRole(realm, client, name); } RoleListQuery query = cache.get(cacheKey, RoleListQuery.class); @@ -713,7 +731,7 @@ public class RealmCacheSession implements CacheRealmProvider { if (query == null) { Long loaded = cache.getCurrentRevision(cacheKey); - RoleModel model = getDelegate().getClientRole(realm, client, name); + RoleModel model = getRealmDelegate().getClientRole(realm, client, name); if (model == null) return null; query = new RoleListQuery(loaded, cacheKey, realm, model.getId(), client.getClientId()); logger.tracev("adding client role cache miss: client {0} key {1}", client.getClientId(), cacheKey); @@ -723,7 +741,7 @@ public class RealmCacheSession implements CacheRealmProvider { RoleModel role = getRoleById(query.getRoles().iterator().next(), realm); if (role == null) { invalidations.add(cacheKey); - return getDelegate().getClientRole(realm, client, name); + return getRealmDelegate().getClientRole(realm, client, name); } return role; } @@ -736,7 +754,7 @@ public class RealmCacheSession implements CacheRealmProvider { invalidationEvents.add(RoleRemovedEvent.create(role.getId(), role.getName(), role.getContainer().getId())); roleRemovalInvalidations(role.getId(), role.getName(), role.getContainer().getId()); - return getDelegate().removeRole(realm, role); + return getRealmDelegate().removeRole(realm, role); } @Override @@ -748,7 +766,7 @@ public class RealmCacheSession implements CacheRealmProvider { if (cached == null) { Long loaded = cache.getCurrentRevision(id); - RoleModel model = getDelegate().getRoleById(id, realm); + RoleModel model = getRealmDelegate().getRoleById(id, realm); if (model == null) return null; if (invalidations.contains(id)) return model; if (model.isClientRole()) { @@ -759,7 +777,7 @@ public class RealmCacheSession implements CacheRealmProvider { cache.addRevisioned(cached, startupRevision); } else if (invalidations.contains(id)) { - return getDelegate().getRoleById(id, realm); + return getRealmDelegate().getRoleById(id, realm); } else if (managedRoles.containsKey(id)) { return managedRoles.get(id); } @@ -777,14 +795,14 @@ public class RealmCacheSession implements CacheRealmProvider { if (cached == null) { Long loaded = cache.getCurrentRevision(id); - GroupModel model = getDelegate().getGroupById(id, realm); + GroupModel model = getRealmDelegate().getGroupById(id, realm); if (model == null) return null; if (invalidations.contains(id)) return model; cached = new CachedGroup(loaded, realm, model); cache.addRevisioned(cached, startupRevision); } else if (invalidations.contains(id)) { - return getDelegate().getGroupById(id, realm); + return getRealmDelegate().getGroupById(id, realm); } else if (managedGroups.containsKey(id)) { return managedGroups.get(id); } @@ -800,7 +818,7 @@ public class RealmCacheSession implements CacheRealmProvider { listInvalidations.add(realm.getId()); invalidationEvents.add(GroupMovedEvent.create(group, toParent, realm.getId())); - getDelegate().moveGroup(realm, group, toParent); + getRealmDelegate().moveGroup(realm, group, toParent); } @Override @@ -808,7 +826,7 @@ public class RealmCacheSession implements CacheRealmProvider { String cacheKey = getGroupsQueryCacheKey(realm.getId()); boolean queryDB = invalidations.contains(cacheKey) || listInvalidations.contains(realm.getId()); if (queryDB) { - return getDelegate().getGroups(realm); + return getRealmDelegate().getGroups(realm); } GroupListQuery query = cache.get(cacheKey, GroupListQuery.class); @@ -818,7 +836,7 @@ public class RealmCacheSession implements CacheRealmProvider { if (query == null) { Long loaded = cache.getCurrentRevision(cacheKey); - List model = getDelegate().getGroups(realm); + List model = getRealmDelegate().getGroups(realm); if (model == null) return null; Set ids = new HashSet<>(); for (GroupModel client : model) ids.add(client.getId()); @@ -832,7 +850,7 @@ public class RealmCacheSession implements CacheRealmProvider { GroupModel group = session.realms().getGroupById(id, realm); if (group == null) { invalidations.add(cacheKey); - return getDelegate().getGroups(realm); + return getRealmDelegate().getGroups(realm); } list.add(group); } @@ -844,12 +862,12 @@ public class RealmCacheSession implements CacheRealmProvider { @Override public Long getGroupsCount(RealmModel realm, Boolean onlyTopGroups) { - return getDelegate().getGroupsCount(realm, onlyTopGroups); + return getRealmDelegate().getGroupsCount(realm, onlyTopGroups); } @Override public Long getGroupsCountByNameContaining(RealmModel realm, String search) { - return getDelegate().getGroupsCountByNameContaining(realm, search); + return getRealmDelegate().getGroupsCountByNameContaining(realm, search); } @Override @@ -857,7 +875,7 @@ public class RealmCacheSession implements CacheRealmProvider { String cacheKey = getTopGroupsQueryCacheKey(realm.getId()); boolean queryDB = invalidations.contains(cacheKey) || listInvalidations.contains(realm.getId()); if (queryDB) { - return getDelegate().getTopLevelGroups(realm); + return getRealmDelegate().getTopLevelGroups(realm); } GroupListQuery query = cache.get(cacheKey, GroupListQuery.class); @@ -867,7 +885,7 @@ public class RealmCacheSession implements CacheRealmProvider { if (query == null) { Long loaded = cache.getCurrentRevision(cacheKey); - List model = getDelegate().getTopLevelGroups(realm); + List model = getRealmDelegate().getTopLevelGroups(realm); if (model == null) return null; Set ids = new HashSet<>(); for (GroupModel client : model) ids.add(client.getId()); @@ -881,7 +899,7 @@ public class RealmCacheSession implements CacheRealmProvider { GroupModel group = session.realms().getGroupById(id, realm); if (group == null) { invalidations.add(cacheKey); - return getDelegate().getTopLevelGroups(realm); + return getRealmDelegate().getTopLevelGroups(realm); } list.add(group); } @@ -896,7 +914,7 @@ public class RealmCacheSession implements CacheRealmProvider { String cacheKey = getTopGroupsQueryCacheKey(realm.getId() + first + max); boolean queryDB = invalidations.contains(cacheKey) || listInvalidations.contains(realm.getId() + first + max); if (queryDB) { - return getDelegate().getTopLevelGroups(realm, first, max); + return getRealmDelegate().getTopLevelGroups(realm, first, max); } GroupListQuery query = cache.get(cacheKey, GroupListQuery.class); @@ -906,7 +924,7 @@ public class RealmCacheSession implements CacheRealmProvider { if (Objects.isNull(query)) { Long loaded = cache.getCurrentRevision(cacheKey); - List model = getDelegate().getTopLevelGroups(realm, first, max); + List model = getRealmDelegate().getTopLevelGroups(realm, first, max); if (model == null) return null; Set ids = new HashSet<>(); for (GroupModel client : model) ids.add(client.getId()); @@ -920,7 +938,7 @@ public class RealmCacheSession implements CacheRealmProvider { GroupModel group = session.realms().getGroupById(id, realm); if (Objects.isNull(group)) { invalidations.add(cacheKey); - return getDelegate().getTopLevelGroups(realm); + return getRealmDelegate().getTopLevelGroups(realm); } list.add(group); } @@ -932,7 +950,7 @@ public class RealmCacheSession implements CacheRealmProvider { @Override public List searchForGroupByName(RealmModel realm, String search, Integer first, Integer max) { - return getDelegate().searchForGroupByName(realm, search, first, max); + return getRealmDelegate().searchForGroupByName(realm, search, first, max); } @Override @@ -946,12 +964,12 @@ public class RealmCacheSession implements CacheRealmProvider { invalidationEvents.add(GroupRemovedEvent.create(group, realm.getId())); - return getDelegate().removeGroup(realm, group); + return getRealmDelegate().removeGroup(realm, group); } @Override public GroupModel createGroup(RealmModel realm, String name) { - GroupModel group = getDelegate().createGroup(realm, name); + GroupModel group = getRealmDelegate().createGroup(realm, name); return groupAdded(realm, group); } @@ -965,7 +983,7 @@ public class RealmCacheSession implements CacheRealmProvider { @Override public GroupModel createGroup(RealmModel realm, String id, String name) { - GroupModel group = getDelegate().createGroup(realm, id, name); + GroupModel group = getRealmDelegate().createGroup(realm, id, name); return groupAdded(realm, group); } @@ -978,7 +996,7 @@ public class RealmCacheSession implements CacheRealmProvider { addGroupEventIfAbsent(GroupMovedEvent.create(subGroup, null, realm.getId())); - getDelegate().addTopLevelGroup(realm, subGroup); + getRealmDelegate().addTopLevelGroup(realm, subGroup); } @@ -1007,22 +1025,80 @@ public class RealmCacheSession implements CacheRealmProvider { if (cached == null) { Long loaded = cache.getCurrentRevision(id); - ClientModel model = getDelegate().getClientById(id, realm); + ClientModel model = getClientDelegate().getClientById(id, realm); if (model == null) return null; - if (invalidations.contains(id)) return model; - cached = new CachedClient(loaded, realm, model); - logger.tracev("adding client by id cache miss: {0}", cached.getClientId()); - cache.addRevisioned(cached, startupRevision); + ClientModel adapter = cacheClient(realm, model, loaded); + managedApplications.put(id, adapter); + return adapter; } else if (invalidations.contains(id)) { - return getDelegate().getClientById(id, realm); + return getRealmDelegate().getClientById(id, realm); } else if (managedApplications.containsKey(id)) { return managedApplications.get(id); } - ClientAdapter adapter = new ClientAdapter(realm, cached, this, null); + ClientModel adapter = validateCache(realm, cached); managedApplications.put(id, adapter); return adapter; } + protected ClientModel cacheClient(RealmModel realm, ClientModel delegate, Long revision) { + if (invalidations.contains(delegate.getId())) return delegate; + StorageId storageId = new StorageId(delegate.getId()); + CachedClient cached = null; + ClientAdapter adapter = null; + + if (!storageId.isLocal()) { + ComponentModel component = realm.getComponent(storageId.getProviderId()); + ClientStorageProviderModel model = new ClientStorageProviderModel(component); + if (!model.isEnabled()) { + return delegate; + } + ClientStorageProviderModel.CachePolicy policy = model.getCachePolicy(); + if (policy != null && policy == ClientStorageProviderModel.CachePolicy.NO_CACHE) { + return delegate; + } + + cached = new CachedClient(revision, realm, delegate); + adapter = new ClientAdapter(realm, cached, this); + + long lifespan = model.getLifespan(); + if (lifespan > 0) { + cache.addRevisioned(cached, startupRevision, lifespan); + } else { + cache.addRevisioned(cached, startupRevision); + } + } else { + cached = new CachedClient(revision, realm, delegate); + adapter = new ClientAdapter(realm, cached, this); + cache.addRevisioned(cached, startupRevision); + } + + return adapter; + } + + + protected ClientModel validateCache(RealmModel realm, CachedClient cached) { + if (!realm.getId().equals(cached.getRealm())) { + return null; + } + + StorageId storageId = new StorageId(cached.getId()); + if (!storageId.isLocal()) { + ComponentModel component = realm.getComponent(storageId.getProviderId()); + ClientStorageProviderModel model = new ClientStorageProviderModel(component); + + // although we do set a timeout, Infinispan has no guarantees when the user will be evicted + // its also hard to test stuff + if (model.shouldInvalidate(cached)) { + registerClientInvalidation(cached.getId(), cached.getClientId(), realm.getId()); + return getClientDelegate().getClientById(cached.getId(), realm); + } + } + ClientAdapter adapter = new ClientAdapter(realm, cached, this); + + return adapter; + } + + @Override public ClientModel getClientByClientId(String clientId, RealmModel realm) { String cacheKey = getClientByClientIdCacheKey(clientId, realm.getId()); @@ -1035,7 +1111,7 @@ public class RealmCacheSession implements CacheRealmProvider { if (query == null) { Long loaded = cache.getCurrentRevision(cacheKey); - ClientModel model = getDelegate().getClientByClientId(clientId, realm); + ClientModel model = getClientDelegate().getClientByClientId(clientId, realm); if (model == null) return null; if (invalidations.contains(model.getId())) return model; id = model.getId(); @@ -1043,11 +1119,11 @@ public class RealmCacheSession implements CacheRealmProvider { logger.tracev("adding client by name cache miss: {0}", clientId); cache.addRevisioned(query, startupRevision); } else if (invalidations.contains(cacheKey)) { - return getDelegate().getClientByClientId(clientId, realm); + return getClientDelegate().getClientByClientId(clientId, realm); } else { id = query.getClients().iterator().next(); if (invalidations.contains(id)) { - return getDelegate().getClientByClientId(clientId, realm); + return getClientDelegate().getClientByClientId(clientId, realm); } } return getClientById(id, realm); @@ -1066,13 +1142,13 @@ public class RealmCacheSession implements CacheRealmProvider { if (cached == null) { Long loaded = cache.getCurrentRevision(id); - ClientTemplateModel model = getDelegate().getClientTemplateById(id, realm); + ClientTemplateModel model = getRealmDelegate().getClientTemplateById(id, realm); if (model == null) return null; if (invalidations.contains(id)) return model; cached = new CachedClientTemplate(loaded, realm, model); cache.addRevisioned(cached, startupRevision); } else if (invalidations.contains(id)) { - return getDelegate().getClientTemplateById(id, realm); + return getRealmDelegate().getClientTemplateById(id, realm); } else if (managedClientTemplates.containsKey(id)) { return managedClientTemplates.get(id); } @@ -1084,31 +1160,31 @@ public class RealmCacheSession implements CacheRealmProvider { // Don't cache ClientInitialAccessModel for now @Override public ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count) { - return getDelegate().createClientInitialAccessModel(realm, expiration, count); + return getRealmDelegate().createClientInitialAccessModel(realm, expiration, count); } @Override public ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id) { - return getDelegate().getClientInitialAccessModel(realm, id); + return getRealmDelegate().getClientInitialAccessModel(realm, id); } @Override public void removeClientInitialAccessModel(RealmModel realm, String id) { - getDelegate().removeClientInitialAccessModel(realm, id); + getRealmDelegate().removeClientInitialAccessModel(realm, id); } @Override public List listClientInitialAccess(RealmModel realm) { - return getDelegate().listClientInitialAccess(realm); + return getRealmDelegate().listClientInitialAccess(realm); } @Override public void removeExpiredClientInitialAccess() { - getDelegate().removeExpiredClientInitialAccess(); + getRealmDelegate().removeExpiredClientInitialAccess(); } @Override public void decreaseRemainingCount(RealmModel realm, ClientInitialAccessModel clientInitialAccess) { - getDelegate().decreaseRemainingCount(realm, clientInitialAccess); + getRealmDelegate().decreaseRemainingCount(realm, clientInitialAccess); } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java index 00e41f3081..24ed6d91cf 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java @@ -49,7 +49,7 @@ public class RoleAdapter implements RoleModel { protected void getDelegateForUpdate() { if (updated == null) { cacheSession.registerRoleInvalidation(cached.getId(), cached.getName(), getContainerId()); - updated = cacheSession.getDelegate().getRoleById(cached.getId(), realm); + updated = cacheSession.getRealmDelegate().getRoleById(cached.getId(), realm); if (updated == null) throw new IllegalStateException("Not found in database"); } } @@ -62,7 +62,7 @@ public class RoleAdapter implements RoleModel { protected boolean isUpdated() { if (updated != null) return true; if (!invalidated) return false; - updated = cacheSession.getDelegate().getRoleById(cached.getId(), realm); + updated = cacheSession.getRealmDelegate().getRoleById(cached.getId(), realm); if (updated == null) throw new IllegalStateException("Not found in database"); return true; } 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 4b5309a613..be3d0ad679 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 @@ -19,6 +19,7 @@ package org.keycloak.models.cache.infinispan; import org.jboss.logging.Logger; import org.keycloak.cluster.ClusterProvider; +import org.keycloak.models.cache.CachedObject; import org.keycloak.models.cache.infinispan.events.InvalidationEvent; import org.keycloak.common.constants.ServiceAccountConstants; import org.keycloak.common.util.Time; @@ -49,9 +50,11 @@ import org.keycloak.models.cache.infinispan.events.UserFederationLinkUpdatedEven import org.keycloak.models.cache.infinispan.events.UserFullInvalidationEvent; import org.keycloak.models.cache.infinispan.events.UserUpdatedEvent; import org.keycloak.models.utils.ReadOnlyUserModelDelegate; +import org.keycloak.storage.CacheableStorageProviderModel; import org.keycloak.storage.StorageId; import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProviderModel; +import org.keycloak.storage.client.ClientStorageProvider; import java.util.Calendar; import java.util.HashMap; @@ -144,7 +147,6 @@ public class UserCacheSession implements UserCache { @Override public void commit() { - if (delegate == null) return; runInvalidations(); transactionActive = false; } @@ -296,46 +298,11 @@ public class UserCacheSession implements UserCache { if (!storageId.isLocal()) { ComponentModel component = realm.getComponent(storageId.getProviderId()); - UserStorageProviderModel model = new UserStorageProviderModel(component); + CacheableStorageProviderModel model = new CacheableStorageProviderModel(component); // although we do set a timeout, Infinispan has no guarantees when the user will be evicted // its also hard to test stuff - boolean invalidate = false; - if (!model.isEnabled()) { - invalidate = true; - } else { - UserStorageProviderModel.CachePolicy policy = model.getCachePolicy(); - if (policy != null) { - //String currentTime = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(Time.currentTimeMillis())); - if (policy == UserStorageProviderModel.CachePolicy.NO_CACHE) { - invalidate = true; - } else if (cached.getCacheTimestamp() < model.getCacheInvalidBefore()) { - invalidate = true; - } else if (policy == UserStorageProviderModel.CachePolicy.MAX_LIFESPAN) { - if (cached.getCacheTimestamp() + model.getMaxLifespan() < Time.currentTimeMillis()) { - invalidate = true; - } - } else if (policy == UserStorageProviderModel.CachePolicy.EVICT_DAILY) { - long dailyTimeout = dailyTimeout(model.getEvictionHour(), model.getEvictionMinute()); - dailyTimeout = dailyTimeout - (24 * 60 * 60 * 1000); - //String timeout = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(dailyTimeout)); - //String stamp = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(cached.getCacheTimestamp())); - if (cached.getCacheTimestamp() <= dailyTimeout) { - invalidate = true; - } - } else if (policy == UserStorageProviderModel.CachePolicy.EVICT_WEEKLY) { - int oneWeek = 7 * 24 * 60 * 60 * 1000; - long weeklyTimeout = weeklyTimeout(model.getEvictionDay(), model.getEvictionHour(), model.getEvictionMinute()); - long lastTimeout = weeklyTimeout - oneWeek; - //String timeout = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(weeklyTimeout)); - //String stamp = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(cached.getCacheTimestamp())); - if (cached.getCacheTimestamp() <= lastTimeout) { - invalidate = true; - } - } - } - } - if (invalidate) { + if (model.shouldInvalidate(cached)) { registerUserInvalidation(realm, cached); return getDelegate().getUserById(cached.getId(), realm); } @@ -371,26 +338,11 @@ public class UserCacheSession implements UserCache { adapter = new UserAdapter(cached, this, session, realm); onCache(realm, adapter, delegate); - if (policy == null || policy == UserStorageProviderModel.CachePolicy.DEFAULT) { - cache.addRevisioned(cached, startupRevision); + long lifespan = model.getLifespan(); + if (lifespan > 0) { + cache.addRevisioned(cached, startupRevision, lifespan); } else { - long lifespan = -1; - if (policy == UserStorageProviderModel.CachePolicy.EVICT_DAILY) { - if (model.getEvictionHour() > -1 && model.getEvictionMinute() > -1) { - lifespan = dailyTimeout(model.getEvictionHour(), model.getEvictionMinute()) - Time.currentTimeMillis(); - } - } else if (policy == UserStorageProviderModel.CachePolicy.EVICT_WEEKLY) { - if (model.getEvictionDay() > 0 && model.getEvictionHour() > -1 && model.getEvictionMinute() > -1) { - lifespan = weeklyTimeout(model.getEvictionDay(), model.getEvictionHour(), model.getEvictionMinute()) - Time.currentTimeMillis(); - } - } else if (policy == UserStorageProviderModel.CachePolicy.MAX_LIFESPAN) { - lifespan = model.getMaxLifespan(); - } - if (lifespan > 0) { - cache.addRevisioned(cached, startupRevision, lifespan); - } else { - cache.addRevisioned(cached, startupRevision); - } + cache.addRevisioned(cached, startupRevision); } } else { cached = new CachedUser(revision, realm, delegate, notBefore); @@ -402,39 +354,6 @@ public class UserCacheSession implements UserCache { return adapter; } - - public static long dailyTimeout(int hour, int minute) { - Calendar cal = Calendar.getInstance(); - Calendar cal2 = Calendar.getInstance(); - cal.setTimeInMillis(Time.currentTimeMillis()); - cal2.setTimeInMillis(Time.currentTimeMillis()); - cal2.set(Calendar.HOUR_OF_DAY, hour); - cal2.set(Calendar.MINUTE, minute); - if (cal2.getTimeInMillis() < cal.getTimeInMillis()) { - int add = (24 * 60 * 60 * 1000); - cal.add(Calendar.MILLISECOND, add); - } else { - cal.add(Calendar.MILLISECOND, (int)(cal2.getTimeInMillis() - cal.getTimeInMillis())); - } - return cal.getTimeInMillis(); - } - - public static long weeklyTimeout(int day, int hour, int minute) { - Calendar cal = Calendar.getInstance(); - Calendar cal2 = Calendar.getInstance(); - cal.setTimeInMillis(Time.currentTimeMillis()); - cal2.setTimeInMillis(Time.currentTimeMillis()); - cal2.set(Calendar.HOUR_OF_DAY, hour); - cal2.set(Calendar.MINUTE, minute); - cal2.set(Calendar.DAY_OF_WEEK, day); - if (cal2.getTimeInMillis() < cal.getTimeInMillis()) { - int add = (7 * 24 * 60 * 60 * 1000); - cal2.add(Calendar.MILLISECOND, add); - } - - return cal2.getTimeInMillis(); - } - private void onCache(RealmModel realm, UserAdapter adapter, UserModel delegate) { ((OnUserCache)getDelegate()).onCache(realm, adapter, delegate); ((OnUserCache)session.userCredentialManager()).onCache(realm, adapter, delegate); @@ -935,7 +854,7 @@ public class UserCacheSession implements UserCache { @Override public void preRemove(RealmModel realm, ComponentModel component) { - if (!component.getProviderType().equals(UserStorageProvider.class.getName())) return; + if (!component.getProviderType().equals(UserStorageProvider.class.getName()) && !component.getProviderType().equals(ClientStorageProvider.class.getName())) return; addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm getDelegate().preRemove(realm, component); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java index ab73c949d7..cb5d0606d6 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java @@ -41,6 +41,7 @@ import org.keycloak.authorization.store.ScopeStore; import org.keycloak.authorization.store.StoreFactory; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakTransaction; +import org.keycloak.models.ModelException; import org.keycloak.models.cache.authorization.CachedStoreFactoryProvider; import org.keycloak.models.cache.infinispan.authorization.entities.CachedPolicy; import org.keycloak.models.cache.infinispan.authorization.entities.CachedResource; @@ -64,6 +65,7 @@ import org.keycloak.models.cache.infinispan.authorization.events.ScopeRemovedEve import org.keycloak.models.cache.infinispan.authorization.events.ScopeUpdatedEvent; import org.keycloak.models.cache.infinispan.events.InvalidationEvent; import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation; +import org.keycloak.storage.StorageId; /** * @author Bill Burke @@ -348,6 +350,9 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider { protected class ResourceServerCache implements ResourceServerStore { @Override public ResourceServer create(String clientId) { + if (!StorageId.isLocalStorage(clientId)) { + throw new ModelException("Creating resource server from federated ClientModel not supported"); + } ResourceServer server = getResourceServerStoreDelegate().create(clientId); registerResourceServerInvalidation(server.getId()); return server; 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 22fef962c4..8c88df774f 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 @@ -1,6 +1,7 @@ package org.keycloak.models.cache.infinispan.entities; import org.keycloak.common.util.Time; +import org.keycloak.models.cache.CachedObject; import java.io.Serializable; @@ -8,7 +9,7 @@ import java.io.Serializable; * @author Bill Burke * @version $Revision: 1 $ */ -public class AbstractRevisioned implements Revisioned, Serializable { +public class AbstractRevisioned implements Revisioned, Serializable, CachedObject { private String id; private Long revision; private final long cacheTimestamp = Time.currentTimeMillis(); @@ -38,6 +39,7 @@ public class AbstractRevisioned implements Revisioned, Serializable { * * @return */ + @Override public long getCacheTimestamp() { return cacheTimestamp; } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java index 4cfea1d7c5..c0b1aa05d6 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java @@ -20,6 +20,8 @@ package org.keycloak.models.sessions.infinispan; import org.infinispan.Cache; import org.infinispan.client.hotrod.RemoteCache; import org.infinispan.context.Flag; +import org.infinispan.stream.CacheCollectors; +import org.infinispan.stream.SerializableSupplier; import org.jboss.logging.Logger; import org.keycloak.cluster.ClusterProvider; import org.keycloak.common.util.Time; @@ -59,16 +61,21 @@ import org.keycloak.models.sessions.infinispan.util.InfinispanKeyGenerator; import org.keycloak.models.sessions.infinispan.util.InfinispanUtil; import org.keycloak.models.utils.SessionTimeoutHelper; +import java.io.Serializable; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collector; +import java.util.stream.Collectors; import java.util.stream.Stream; /** @@ -297,16 +304,21 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { } protected List getUserSessions(final RealmModel realm, ClientModel client, int firstResult, int maxResults, final boolean offline) { + final String clientUuid = client.getId(); + UserSessionPredicate predicate = UserSessionPredicate.create(realm.getId()).client(clientUuid); + + return getUserSessionModels(realm, firstResult, maxResults, offline, predicate); + } + + protected List getUserSessionModels(RealmModel realm, int firstResult, int maxResults, boolean offline, UserSessionPredicate predicate) { Cache> cache = getCache(offline); cache = CacheDecorators.skipCacheLoaders(cache); Cache> clientSessionCache = getClientSessionCache(offline); Cache> clientSessionCacheDecorated = CacheDecorators.skipCacheLoaders(clientSessionCache); - final String clientUuid = client.getId(); - Stream stream = cache.entrySet().stream() - .filter(UserSessionPredicate.create(realm.getId()).client(clientUuid)) + .filter(predicate) .map(Mappers.userSessionEntity()) .sorted(Comparators.userSessionLastSessionRefresh()); @@ -330,7 +342,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { return sessions; } - @Override public UserSessionModel getUserSessionWithPredicate(RealmModel realm, String id, boolean offline, Predicate predicate) { UserSessionModel userSession = getUserSession(realm, id, offline); @@ -398,7 +409,22 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { return getUserSessionsCount(realm, client, false); } - protected long getUserSessionsCount(RealmModel realm, ClientModel client, boolean offline) { + @Override + public Map getActiveClientSessionStats(RealmModel realm, boolean offline) { + Cache> cache = getCache(offline); + cache = CacheDecorators.skipCacheLoaders(cache); + return cache.entrySet().stream() + .filter(UserSessionPredicate.create(realm.getId())) + .map(Mappers.authClientSessionSetMapper()) + .flatMap((Serializable & Function, Stream>)Mappers::toStream) + .collect( + CacheCollectors.serializableCollector( + () -> Collectors.groupingBy(Function.identity(), Collectors.counting()) + ) + ); + } + + protected long getUserSessionsCount(RealmModel realm, ClientModel client, boolean offline) { Cache> cache = getCache(offline); cache = CacheDecorators.skipCacheLoaders(cache); diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java index 177fd236df..50df448c26 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java @@ -17,6 +17,7 @@ package org.keycloak.models.sessions.infinispan.stream; +import org.infinispan.stream.SerializableSupplier; import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity; @@ -25,9 +26,15 @@ import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; import java.io.Serializable; +import java.util.Collection; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.function.Function; +import java.util.stream.Collector; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * @author Stian Thorgersen @@ -125,4 +132,22 @@ public class Mappers { } } + private static class AuthClientSessionSetMapper implements Function>, Set>, Serializable { + + @Override + public Set apply(Map.Entry> entry) { + UserSessionEntity entity = entry.getValue().getEntity(); + return entity.getAuthenticatedClientSessions().keySet(); + } + } + + public static Stream toStream(Collection collection) { + return collection.stream(); + } + + public static Function>, Set> authClientSessionSetMapper() { + return new AuthClientSessionSetMapper(); + } + + } diff --git a/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/InitializerStateTest.java b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/InitializerStateTest.java index 32210a0f02..d26f2b99fe 100644 --- a/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/InitializerStateTest.java +++ b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/InitializerStateTest.java @@ -20,6 +20,7 @@ package org.keycloak.models.sessions.infinispan.initializer; import org.junit.Assert; import org.junit.Test; import org.keycloak.models.cache.infinispan.UserCacheSession; +import org.keycloak.storage.CacheableStorageProviderModel; import java.text.DateFormat; import java.util.Calendar; @@ -65,13 +66,13 @@ public class InitializerStateTest { @Test public void testDailyTimeout() throws Exception { - Date date = new Date(UserCacheSession.dailyTimeout(10, 30)); + Date date = new Date(CacheableStorageProviderModel.dailyTimeout(10, 30)); System.out.println(DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(date)); - date = new Date(UserCacheSession.dailyTimeout(17, 45)); + date = new Date(CacheableStorageProviderModel.dailyTimeout(17, 45)); System.out.println(DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(date)); - date = new Date(UserCacheSession.weeklyTimeout(Calendar.MONDAY, 13, 45)); + date = new Date(CacheableStorageProviderModel.weeklyTimeout(Calendar.MONDAY, 13, 45)); System.out.println(DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(date)); - date = new Date(UserCacheSession.weeklyTimeout(Calendar.THURSDAY, 13, 45)); + date = new Date(CacheableStorageProviderModel.weeklyTimeout(Calendar.THURSDAY, 13, 45)); System.out.println(DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(date)); System.out.println("----"); Calendar cal = Calendar.getInstance(); @@ -80,7 +81,7 @@ public class InitializerStateTest { int min = cal.get(Calendar.MINUTE); date = new Date(cal.getTimeInMillis()); System.out.println(DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(date)); - date = new Date(UserCacheSession.dailyTimeout(hour, min)); + date = new Date(CacheableStorageProviderModel.dailyTimeout(hour, min)); System.out.println(DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(date)); cal = Calendar.getInstance(); cal.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceServerStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceServerStore.java index 207d4abe4a..5e79badca0 100644 --- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceServerStore.java +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceServerStore.java @@ -25,6 +25,8 @@ import org.keycloak.authorization.jpa.entities.ScopeEntity; import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.store.ResourceServerStore; +import org.keycloak.models.ModelException; +import org.keycloak.storage.StorageId; import javax.persistence.EntityManager; import javax.persistence.TypedQuery; @@ -46,6 +48,9 @@ public class JPAResourceServerStore implements ResourceServerStore { @Override public ResourceServer create(String clientId) { + if (!StorageId.isLocalStorage(clientId)) { + throw new ModelException("Creating resource server from federated ClientModel not supported"); + } ResourceServerEntity entity = new ResourceServerEntity(); entity.setId(clientId); 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 08f316425d..8eb8102672 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 @@ -45,7 +45,9 @@ import org.keycloak.models.jpa.entities.UserConsentRoleEntity; import org.keycloak.models.jpa.entities.UserEntity; import org.keycloak.models.utils.DefaultRoles; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.storage.StorageId; import org.keycloak.storage.UserStorageProvider; +import org.keycloak.storage.client.ClientStorageProvider; import javax.persistence.EntityManager; import javax.persistence.TypedQuery; @@ -194,7 +196,14 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { consentEntity = new UserConsentEntity(); consentEntity.setId(KeycloakModelUtils.generateId()); consentEntity.setUser(em.getReference(UserEntity.class, userId)); - consentEntity.setClientId(clientId); + StorageId clientStorageId = new StorageId(clientId); + if (clientStorageId.isLocal()) { + consentEntity.setClientId(clientId); + } else { + consentEntity.setClientStorageProvider(clientStorageId.getProviderId()); + consentEntity.setExternalClientId(clientStorageId.getExternalId()); + } + consentEntity.setCreatedDate(currentTime); consentEntity.setLastUpdatedDate(currentTime); em.persist(consentEntity); @@ -246,9 +255,16 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { private UserConsentEntity getGrantedConsentEntity(String userId, String clientId) { - TypedQuery query = em.createNamedQuery("userConsentByUserAndClient", UserConsentEntity.class); + StorageId clientStorageId = new StorageId(clientId); + String queryName = clientStorageId.isLocal() ? "userConsentByUserAndClient" : "userConsentByUserAndExternalClient"; + TypedQuery query = em.createNamedQuery(queryName, UserConsentEntity.class); query.setParameter("userId", userId); - query.setParameter("clientId", clientId); + if (clientStorageId.isLocal()) { + query.setParameter("clientId", clientId); + } else { + query.setParameter("clientStorageProvider", clientStorageId.getProviderId()); + query.setParameter("externalClientId", clientStorageId.getExternalId()); + } List results = query.getResultList(); if (results.size() > 1) { throw new ModelException("More results found for user [" + userId + "] and client [" + clientId + "]"); @@ -257,6 +273,7 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { } else { return null; } + } private UserConsentModel toConsentModel(RealmModel realm, UserConsentEntity entity) { @@ -264,9 +281,16 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { return null; } - ClientModel client = realm.getClientById(entity.getClientId()); + StorageId clientStorageId = null; + if ( entity.getClientId() == null) { + clientStorageId = new StorageId(entity.getClientStorageProvider(), entity.getExternalClientId()); + } else { + clientStorageId = new StorageId(entity.getClientId()); + } + + ClientModel client = realm.getClientById(clientStorageId.getId()); if (client == null) { - throw new ModelException("Client with id " + entity.getClientId() + " is not available"); + throw new ModelException("Client with id " + clientStorageId.getId() + " is not available"); } UserConsentModel model = new UserConsentModel(client); model.setCreatedDate(entity.getCreatedDate()); @@ -472,9 +496,32 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { @Override public void preRemove(RealmModel realm, ClientModel client) { - em.createNamedQuery("deleteUserConsentProtMappersByClient").setParameter("clientId", client.getId()).executeUpdate(); - em.createNamedQuery("deleteUserConsentRolesByClient").setParameter("clientId", client.getId()).executeUpdate(); - em.createNamedQuery("deleteUserConsentsByClient").setParameter("clientId", client.getId()).executeUpdate(); + StorageId clientStorageId = new StorageId(client.getId()); + if (clientStorageId.isLocal()) { + em.createNamedQuery("deleteUserConsentProtMappersByClient") + .setParameter("clientId", client.getId()) + .executeUpdate(); + em.createNamedQuery("deleteUserConsentRolesByClient") + .setParameter("clientId", client.getId()) + .executeUpdate(); + em.createNamedQuery("deleteUserConsentsByClient") + .setParameter("clientId", client.getId()) + .executeUpdate(); + } else { + em.createNamedQuery("deleteUserConsentProtMappersByExternalClient") + .setParameter("clientStorageProvider", clientStorageId.getProviderId()) + .setParameter("externalClientId",clientStorageId.getExternalId()) + .executeUpdate(); + em.createNamedQuery("deleteUserConsentRolesByExternalClient") + .setParameter("clientStorageProvider", clientStorageId.getProviderId()) + .setParameter("externalClientId", clientStorageId.getExternalId()) + .executeUpdate(); + em.createNamedQuery("deleteUserConsentsByExternalClient") + .setParameter("clientStorageProvider", clientStorageId.getProviderId()) + .setParameter("externalClientId", clientStorageId.getExternalId()) + .executeUpdate(); + + } } @Override @@ -806,8 +853,24 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { @Override public void preRemove(RealmModel realm, ComponentModel component) { - if (!component.getProviderType().equals(UserStorageProvider.class.getName())) return; - removeImportedUsers(realm, component.getId()); + if (component.getProviderType().equals(UserStorageProvider.class.getName())) { + removeImportedUsers(realm, component.getId()); + } + if (component.getProviderType().equals(ClientStorageProvider.class.getName())) { + removeConsentByClientStorageProvider(realm, component.getId()); + } + } + + protected void removeConsentByClientStorageProvider(RealmModel realm, String providerId) { + em.createNamedQuery("deleteUserConsentProtMappersByClientStorageProvider") + .setParameter("clientStorageProvider", providerId) + .executeUpdate(); + em.createNamedQuery("deleteUserConsentRolesByClientStorageProvider") + .setParameter("clientStorageProvider", providerId) + .executeUpdate(); + em.createNamedQuery("deleteUserConsentsByClientStorageProvider") + .setParameter("clientStorageProvider", providerId) + .executeUpdate(); } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java index 7f889777af..7ede55c743 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java @@ -125,9 +125,6 @@ public class ClientEntity { @CollectionTable(name="CLIENT_AUTH_FLOW_BINDINGS", joinColumns={ @JoinColumn(name="CLIENT_ID") }) protected Map authFlowBindings = new HashMap(); - @OneToMany(fetch = FetchType.LAZY, mappedBy = "client", cascade = CascadeType.REMOVE) - Collection identityProviders = new ArrayList(); - @OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "client") Collection protocolMappers = new ArrayList(); @@ -322,14 +319,6 @@ public class ClientEntity { this.frontchannelLogout = frontchannelLogout; } - public Collection getIdentityProviders() { - return this.identityProviders; - } - - public void setIdentityProviders(Collection identityProviders) { - this.identityProviders = identityProviders; - } - public Collection getProtocolMappers() { return protocolMappers; } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientIdentityProviderMappingEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientIdentityProviderMappingEntity.java deleted file mode 100755 index de625d7d2a..0000000000 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientIdentityProviderMappingEntity.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * 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.entities; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.Id; -import javax.persistence.IdClass; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.Table; -import java.io.Serializable; - -/** - * @author pedroigor - */ -@Table(name="CLIENT_IDENTITY_PROV_MAPPING") -@Entity -@IdClass(ClientIdentityProviderMappingEntity.Key.class) -public class ClientIdentityProviderMappingEntity { - - @Id - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "CLIENT_ID") - private ClientEntity client; - - @Id - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "IDENTITY_PROVIDER_ID") - private IdentityProviderEntity identityProvider; - - @Column(name = "RETRIEVE_TOKEN") - private boolean retrieveToken; - - public ClientEntity getClient() { - return this.client; - } - - public void setClient(ClientEntity client) { - this.client = client; - } - - public IdentityProviderEntity getIdentityProvider() { - return this.identityProvider; - } - - public void setIdentityProvider(IdentityProviderEntity identityProvider) { - this.identityProvider = identityProvider; - } - - public void setRetrieveToken(boolean retrieveToken) { - this.retrieveToken = retrieveToken; - } - - public boolean isRetrieveToken() { - return retrieveToken; - } - - public static class Key implements Serializable { - - private ClientEntity client; - private IdentityProviderEntity identityProvider; - - public Key() { - } - - public Key(ClientEntity client, IdentityProviderEntity identityProvider) { - this.client = client; - this.identityProvider = identityProvider; - } - - public ClientEntity getUser() { - return client; - } - - public IdentityProviderEntity getIdentityProvider() { - return identityProvider; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - Key key = (Key) o; - - if (identityProvider != null ? !identityProvider.getAlias().equals(key.identityProvider.getAlias()) : key.identityProvider != null) - return false; - if (client != null ? !client.getId().equals(key.client != null ? key.client.getId() : null) : key.client != null) return false; - - return true; - } - - @Override - public int hashCode() { - int result = client != null ? client.getId().hashCode() : 0; - result = 31 * result + (identityProvider != null ? identityProvider.hashCode() : 0); - return result; - } - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null) return false; - if (!(o instanceof ClientIdentityProviderMappingEntity)) return false; - - ClientIdentityProviderMappingEntity key = (ClientIdentityProviderMappingEntity) o; - - if (identityProvider != null ? !identityProvider.getAlias().equals(key.identityProvider.getAlias()) : key.identityProvider != null) - return false; - if (client != null ? !client.getId().equals(key.client != null ? key.client.getId() : null) : key.client != null) return false; - - return true; - } - - @Override - public int hashCode() { - int result = client != null ? client.getId().hashCode() : 0; - result = 31 * result + (identityProvider != null ? identityProvider.hashCode() : 0); - return result; - } - - -} diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentEntity.java index a29ab6925e..9772810b5e 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentEntity.java @@ -43,11 +43,14 @@ import java.util.Collection; }) @NamedQueries({ @NamedQuery(name="userConsentByUserAndClient", query="select consent from UserConsentEntity consent where consent.user.id = :userId and consent.clientId = :clientId"), + @NamedQuery(name="userConsentByUserAndExternalClient", query="select consent from UserConsentEntity consent where consent.user.id = :userId and consent.clientStorageProvider = :clientStorageProvider and consent.externalClientId = :externalClientId"), @NamedQuery(name="userConsentsByUser", query="select consent from UserConsentEntity consent where consent.user.id = :userId"), @NamedQuery(name="deleteUserConsentsByRealm", query="delete from UserConsentEntity consent where consent.user IN (select user from UserEntity user where user.realmId = :realmId)"), @NamedQuery(name="deleteUserConsentsByRealmAndLink", query="delete from UserConsentEntity consent where consent.user IN (select u from UserEntity u where u.realmId=:realmId and u.federationLink=:link)"), @NamedQuery(name="deleteUserConsentsByUser", query="delete from UserConsentEntity consent where consent.user = :user"), @NamedQuery(name="deleteUserConsentsByClient", query="delete from UserConsentEntity consent where consent.clientId = :clientId"), + @NamedQuery(name="deleteUserConsentsByExternalClient", query="delete from UserConsentEntity consent where consent.clientStorageProvider = :clientStorageProvider and consent.externalClientId = :externalClientId"), + @NamedQuery(name="deleteUserConsentsByClientStorageProvider", query="delete from UserConsentEntity consent where consent.clientStorageProvider = :clientStorageProvider"), }) public class UserConsentEntity { @@ -63,6 +66,12 @@ public class UserConsentEntity { @Column(name="CLIENT_ID") protected String clientId; + @Column(name="CLIENT_STORAGE_PROVIDER") + protected String clientStorageProvider; + + @Column(name="EXTERNAL_CLIENT_ID") + protected String externalClientId; + @OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "userConsent") Collection grantedRoles = new ArrayList(); @@ -91,14 +100,6 @@ public class UserConsentEntity { this.user = user; } - public String getClientId() { - return clientId; - } - - public void setClientId(String clientId) { - this.clientId = clientId; - } - public Collection getGrantedRoles() { return grantedRoles; } @@ -131,6 +132,30 @@ public class UserConsentEntity { this.lastUpdatedDate = lastUpdatedDate; } + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getClientStorageProvider() { + return clientStorageProvider; + } + + public void setClientStorageProvider(String clientStorageProvider) { + this.clientStorageProvider = clientStorageProvider; + } + + public String getExternalClientId() { + return externalClientId; + } + + public void setExternalClientId(String externalClientId) { + this.externalClientId = externalClientId; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentProtocolMapperEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentProtocolMapperEntity.java index 4c0dd5d74a..85df7598ee 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentProtocolMapperEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentProtocolMapperEntity.java @@ -38,7 +38,9 @@ import java.io.Serializable; @NamedQuery(name="deleteUserConsentProtMappersByUser", query="delete from UserConsentProtocolMapperEntity csm where csm.userConsent IN (select consent from UserConsentEntity consent where consent.user = :user)"), @NamedQuery(name="deleteUserConsentProtMappersByRealmAndLink", query="delete from UserConsentProtocolMapperEntity csm where csm.userConsent IN (select consent from UserConsentEntity consent where consent.user IN (select u from UserEntity u where u.realmId=:realmId and u.federationLink=:link))"), @NamedQuery(name="deleteUserConsentProtMappersByProtocolMapper", query="delete from UserConsentProtocolMapperEntity csm where csm.protocolMapperId = :protocolMapperId)"), - @NamedQuery(name="deleteUserConsentProtMappersByClient", query="delete from UserConsentProtocolMapperEntity csm where csm.userConsent IN (select consent from UserConsentEntity consent where consent.clientId = :clientId))"), + @NamedQuery(name="deleteUserConsentProtMappersByClient", query="delete from UserConsentProtocolMapperEntity csm where csm.userConsent IN (select consent from UserConsentEntity consent where consent.clientId = :clientId)"), + @NamedQuery(name="deleteUserConsentProtMappersByExternalClient", query="delete from UserConsentProtocolMapperEntity csm where csm.userConsent IN (select consent from UserConsentEntity consent where consent.clientStorageProvider = :clientStorageProvider and consent.externalClientId = :externalClientId)"), + @NamedQuery(name="deleteUserConsentProtMappersByClientStorageProvider", query="delete from UserConsentProtocolMapperEntity csm where csm.userConsent IN (select consent from UserConsentEntity consent where consent.clientStorageProvider = :clientStorageProvider)"), }) @Entity @Table(name="USER_CONSENT_PROT_MAPPER") diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentRoleEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentRoleEntity.java index 95d5f3e09a..c4818c783e 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentRoleEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentRoleEntity.java @@ -38,6 +38,8 @@ import java.io.Serializable; @NamedQuery(name="deleteUserConsentRolesByUser", query="delete from UserConsentRoleEntity grantedRole where grantedRole.userConsent IN (select consent from UserConsentEntity consent where consent.user = :user)"), @NamedQuery(name="deleteUserConsentRolesByRole", query="delete from UserConsentRoleEntity grantedRole where grantedRole.roleId = :roleId)"), @NamedQuery(name="deleteUserConsentRolesByClient", query="delete from UserConsentRoleEntity grantedRole where grantedRole.userConsent IN (select consent from UserConsentEntity consent where consent.clientId = :clientId)"), + @NamedQuery(name="deleteUserConsentRolesByExternalClient", query="delete from UserConsentRoleEntity grantedRole where grantedRole.userConsent IN (select consent from UserConsentEntity consent where consent.clientStorageProvider = :clientStorageProvider and consent.externalClientId = :externalClientId)"), + @NamedQuery(name="deleteUserConsentRolesByClientStorageProvider", query="delete from UserConsentRoleEntity grantedRole where grantedRole.userConsent IN (select consent from UserConsentEntity consent where consent.clientStorageProvider = :clientStorageProvider)"), }) @Entity @Table(name="USER_CONSENT_ROLE") diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java index a4c02de415..587ed7e960 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java @@ -30,6 +30,7 @@ import org.keycloak.models.session.PersistentClientSessionModel; import org.keycloak.models.session.PersistentUserSessionAdapter; import org.keycloak.models.session.PersistentUserSessionModel; import org.keycloak.models.session.UserSessionPersisterProvider; +import org.keycloak.storage.StorageId; import javax.persistence.EntityManager; import javax.persistence.Query; @@ -78,7 +79,17 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv PersistentClientSessionModel model = adapter.getUpdatedModel(); PersistentClientSessionEntity entity = new PersistentClientSessionEntity(); - entity.setClientId(clientSession.getClient().getId()); + StorageId clientStorageId = new StorageId(clientSession.getClient().getId()); + if (clientStorageId.isLocal()) { + entity.setClientId(clientStorageId.getId()); + entity.setClientStorageProvider(PersistentClientSessionEntity.LOCAL); + entity.setExternalClientId(PersistentClientSessionEntity.LOCAL); + + } else { + entity.setClientId(PersistentClientSessionEntity.EXTERNAL); + entity.setClientStorageProvider(clientStorageId.getProviderId()); + entity.setExternalClientId(clientStorageId.getExternalId()); + } entity.setTimestamp(clientSession.getTimestamp()); String offlineStr = offlineToString(offline); entity.setOffline(offlineStr); @@ -127,7 +138,18 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv @Override public void removeClientSession(String userSessionId, String clientUUID, boolean offline) { String offlineStr = offlineToString(offline); - PersistentClientSessionEntity sessionEntity = em.find(PersistentClientSessionEntity.class, new PersistentClientSessionEntity.Key(userSessionId, clientUUID, offlineStr)); + StorageId clientStorageId = new StorageId(clientUUID); + String clientId = PersistentClientSessionEntity.EXTERNAL; + String clientStorageProvider = PersistentClientSessionEntity.LOCAL; + String externalId = PersistentClientSessionEntity.LOCAL; + if (clientStorageId.isLocal()) { + clientId = clientUUID; + } else { + clientStorageProvider = clientStorageId.getProviderId(); + externalId = clientStorageId.getExternalId(); + + } + PersistentClientSessionEntity sessionEntity = em.find(PersistentClientSessionEntity.class, new PersistentClientSessionEntity.Key(userSessionId, clientId, clientStorageProvider, externalId, offlineStr)); if (sessionEntity != null) { em.remove(sessionEntity); @@ -168,7 +190,16 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv } private void onClientRemoved(String clientUUID) { - int num = em.createNamedQuery("deleteClientSessionsByClient").setParameter("clientId", clientUUID).executeUpdate(); + int num = 0; + StorageId clientStorageId = new StorageId(clientUUID); + if (clientStorageId.isLocal()) { + num = em.createNamedQuery("deleteClientSessionsByClient").setParameter("clientId", clientUUID).executeUpdate(); + } else { + num = em.createNamedQuery("deleteClientSessionsByExternalClient") + .setParameter("clientStorageProvider", clientStorageId.getProviderId()) + .setParameter("externalClientId", clientStorageId.getExternalId()) + .executeUpdate(); + } num = em.createNamedQuery("deleteDetachedUserSessions").executeUpdate(); } @@ -282,10 +313,14 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv } private PersistentAuthenticatedClientSessionAdapter toAdapter(RealmModel realm, PersistentUserSessionAdapter userSession, PersistentClientSessionEntity entity) { - ClientModel client = realm.getClientById(entity.getClientId()); + String clientId = entity.getClientId(); + if (!entity.getExternalClientId().equals("local")) { + clientId = new StorageId(entity.getClientId(), entity.getExternalClientId()).getId(); + } + ClientModel client = realm.getClientById(clientId); PersistentClientSessionModel model = new PersistentClientSessionModel(); - model.setClientId(entity.getClientId()); + model.setClientId(clientId); model.setUserSessionId(userSession.getId()); model.setUserId(userSession.getUserId()); model.setTimestamp(entity.getTimestamp()); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java index 3ae17b2448..44c3188c85 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java @@ -32,6 +32,8 @@ import java.io.Serializable; @NamedQueries({ @NamedQuery(name="deleteClientSessionsByRealm", query="delete from PersistentClientSessionEntity sess where sess.userSessionId IN (select u.userSessionId from PersistentUserSessionEntity u where u.realmId = :realmId)"), @NamedQuery(name="deleteClientSessionsByClient", query="delete from PersistentClientSessionEntity sess where sess.clientId = :clientId"), + @NamedQuery(name="deleteClientSessionsByExternalClient", query="delete from PersistentClientSessionEntity sess where sess.clientStorageProvider = :clientStorageProvider and sess.externalClientId = :externalClientId"), + @NamedQuery(name="deleteClientSessionsByClientStorageProvider", query="delete from PersistentClientSessionEntity sess where sess.clientStorageProvider = :clientStorageProvider"), @NamedQuery(name="deleteClientSessionsByUser", query="delete from PersistentClientSessionEntity sess where sess.userSessionId IN (select u.userSessionId from PersistentUserSessionEntity u where u.userId = :userId)"), @NamedQuery(name="deleteClientSessionsByUserSession", query="delete from PersistentClientSessionEntity sess where sess.userSessionId = :userSessionId and sess.offline = :offline"), @NamedQuery(name="deleteDetachedClientSessions", query="delete from PersistentClientSessionEntity sess where NOT EXISTS (select u.userSessionId from PersistentUserSessionEntity u where u.userSessionId = sess.userSessionId )"), @@ -44,6 +46,8 @@ import java.io.Serializable; @IdClass(PersistentClientSessionEntity.Key.class) public class PersistentClientSessionEntity { + public static final String LOCAL = "local"; + public static final String EXTERNAL = "external"; @Id @Column(name = "USER_SESSION_ID", length = 36) protected String userSessionId; @@ -52,6 +56,14 @@ public class PersistentClientSessionEntity { @Column(name="CLIENT_ID", length = 36) protected String clientId; + @Id + @Column(name="CLIENT_STORAGE_PROVIDER", length = 36) + protected String clientStorageProvider; + + @Id + @Column(name="EXTERNAL_CLIENT_ID", length = 255) + protected String externalClientId; + @Column(name="TIMESTAMP") protected int timestamp; @@ -78,6 +90,22 @@ public class PersistentClientSessionEntity { this.clientId = clientId; } + public String getClientStorageProvider() { + return clientStorageProvider; + } + + public void setClientStorageProvider(String clientStorageProvider) { + this.clientStorageProvider = clientStorageProvider; + } + + public String getExternalClientId() { + return externalClientId; + } + + public void setExternalClientId(String externalClientId) { + this.externalClientId = externalClientId; + } + public int getTimestamp() { return timestamp; } @@ -107,15 +135,19 @@ public class PersistentClientSessionEntity { protected String userSessionId; protected String clientId; + protected String clientStorageProvider; + protected String externalClientId; protected String offline; public Key() { } - public Key(String userSessionId, String clientId, String offline) { + public Key(String userSessionId, String clientId, String clientStorageProvider, String externalClientId, String offline) { this.userSessionId = userSessionId; this.clientId = clientId; + this.externalClientId = externalClientId; + this.clientStorageProvider = clientStorageProvider; this.offline = offline; } @@ -131,6 +163,14 @@ public class PersistentClientSessionEntity { return offline; } + public String getClientStorageProvider() { + return clientStorageProvider; + } + + public String getExternalClientId() { + return externalClientId; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -140,6 +180,8 @@ public class PersistentClientSessionEntity { if (this.userSessionId != null ? !this.userSessionId.equals(key.userSessionId) : key.userSessionId != null) return false; if (this.clientId != null ? !this.clientId.equals(key.clientId) : key.clientId != null) return false; + if (this.externalClientId != null ? !this.externalClientId.equals(key.clientId) : key.externalClientId != null) return false; + if (this.clientStorageProvider != null ? !this.clientStorageProvider.equals(key.clientId) : key.clientStorageProvider != null) return false; if (this.offline != null ? !this.offline.equals(key.offline) : key.offline != null) return false; return true; @@ -149,6 +191,8 @@ public class PersistentClientSessionEntity { public int hashCode() { int result = this.userSessionId != null ? this.userSessionId.hashCode() : 0; result = 37 * result + (this.clientId != null ? this.clientId.hashCode() : 0); + result = 37 * result + (this.externalClientId != null ? this.externalClientId.hashCode() : 0); + result = 37 * result + (this.clientStorageProvider != null ? this.clientStorageProvider.hashCode() : 0); result = 31 * result + (this.offline != null ? this.offline.hashCode() : 0); return result; } 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 f6de4312a3..7474155630 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 @@ -32,9 +32,11 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserModel; +import org.keycloak.models.jpa.entities.UserConsentEntity; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.storage.StorageId; import org.keycloak.storage.UserStorageProvider; +import org.keycloak.storage.client.ClientStorageProvider; import org.keycloak.storage.federated.UserFederatedStorageProvider; import org.keycloak.storage.jpa.entity.BrokerLinkEntity; import org.keycloak.storage.jpa.entity.FederatedUser; @@ -257,7 +259,13 @@ public class JpaUserFederatedStorageProvider implements consentEntity = new FederatedUserConsentEntity(); consentEntity.setId(KeycloakModelUtils.generateId()); consentEntity.setUserId(userId); - consentEntity.setClientId(clientId); + StorageId clientStorageId = new StorageId(clientId); + if (clientStorageId.isLocal()) { + consentEntity.setClientId(clientId); + } else { + consentEntity.setClientStorageProvider(clientStorageId.getProviderId()); + consentEntity.setExternalClientId(clientStorageId.getExternalId()); + } consentEntity.setRealmId(realm.getId()); consentEntity.setStorageProviderId(new StorageId(userId).getProviderId()); long currentTime = Time.currentTimeMillis(); @@ -315,9 +323,16 @@ public class JpaUserFederatedStorageProvider implements } private FederatedUserConsentEntity getGrantedConsentEntity(String userId, String clientId) { - TypedQuery query = em.createNamedQuery("userFederatedConsentByUserAndClient", FederatedUserConsentEntity.class); + StorageId clientStorageId = new StorageId(clientId); + String queryName = clientStorageId.isLocal() ? "userFederatedConsentByUserAndClient" : "userFederatedConsentByUserAndExternalClient"; + TypedQuery query = em.createNamedQuery(queryName, FederatedUserConsentEntity.class); query.setParameter("userId", userId); - query.setParameter("clientId", clientId); + if (clientStorageId.isLocal()) { + query.setParameter("clientId", clientId); + } else { + query.setParameter("clientStorageProvider", clientStorageId.getProviderId()); + query.setParameter("externalClientId", clientStorageId.getExternalId()); + } List results = query.getResultList(); if (results.size() > 1) { throw new ModelException("More results found for user [" + userId + "] and client [" + clientId + "]"); @@ -334,10 +349,14 @@ public class JpaUserFederatedStorageProvider implements return null; } - ClientModel client = realm.getClientById(entity.getClientId()); - if (client == null) { - throw new ModelException("Client with id " + entity.getClientId() + " is not available"); + StorageId clientStorageId = null; + if ( entity.getClientId() == null) { + clientStorageId = new StorageId(entity.getClientStorageProvider(), entity.getExternalClientId()); + } else { + clientStorageId = new StorageId(entity.getClientId()); } + + ClientModel client = realm.getClientById(clientStorageId.getId()); UserConsentModel model = new UserConsentModel(client); model.setCreatedDate(entity.getCreatedDate()); model.setLastUpdatedDate(entity.getLastUpdatedDate()); @@ -822,9 +841,26 @@ public class JpaUserFederatedStorageProvider implements @Override public void preRemove(RealmModel realm, ClientModel client) { - em.createNamedQuery("deleteFederatedUserConsentProtMappersByClient").setParameter("clientId", client.getId()).executeUpdate(); - em.createNamedQuery("deleteFederatedUserConsentRolesByClient").setParameter("clientId", client.getId()).executeUpdate(); - em.createNamedQuery("deleteFederatedUserConsentsByClient").setParameter("clientId", client.getId()).executeUpdate(); + StorageId clientStorageId = new StorageId(client.getId()); + if (clientStorageId.isLocal()) { + em.createNamedQuery("deleteFederatedUserConsentProtMappersByClient").setParameter("clientId", client.getId()).executeUpdate(); + em.createNamedQuery("deleteFederatedUserConsentRolesByClient").setParameter("clientId", client.getId()).executeUpdate(); + em.createNamedQuery("deleteFederatedUserConsentsByClient").setParameter("clientId", client.getId()).executeUpdate(); + } else { + em.createNamedQuery("deleteFederatedUserConsentProtMappersByExternalClient") + .setParameter("clientStorageProvider", clientStorageId.getProviderId()) + .setParameter("externalClientId",clientStorageId.getExternalId()) + .executeUpdate(); + em.createNamedQuery("deleteFederatedUserConsentRolesByExternalClient") + .setParameter("clientStorageProvider", clientStorageId.getProviderId()) + .setParameter("externalClientId",clientStorageId.getExternalId()) + .executeUpdate(); + em.createNamedQuery("deleteFederatedUserConsentsByExternalClient") + .setParameter("clientStorageProvider", clientStorageId.getProviderId()) + .setParameter("externalClientId",clientStorageId.getExternalId()) + .executeUpdate(); + + } } @Override @@ -885,41 +921,53 @@ public class JpaUserFederatedStorageProvider implements @Override public void preRemove(RealmModel realm, ComponentModel model) { - if (!model.getProviderType().equals(UserStorageProvider.class.getName())) return; + if (model.getProviderType().equals(UserStorageProvider.class.getName())) { - em.createNamedQuery("deleteBrokerLinkByStorageProvider") - .setParameter("storageProviderId", model.getId()) - .executeUpdate(); - em.createNamedQuery("deleteFederatedAttributesByStorageProvider") - .setParameter("storageProviderId", model.getId()) - .executeUpdate(); - em.createNamedQuery("deleteFederatedUserConsentProtMappersByStorageProvider") - .setParameter("storageProviderId", model.getId()) - .executeUpdate(); - em.createNamedQuery("deleteFederatedUserRoleMappingsByStorageProvider") - .setParameter("storageProviderId", model.getId()) - .executeUpdate(); - em.createNamedQuery("deleteFederatedUserConsentsByStorageProvider") - .setParameter("storageProviderId", model.getId()) - .executeUpdate(); - em.createNamedQuery("deleteFederatedCredentialAttributeByStorageProvider") - .setParameter("storageProviderId", model.getId()) - .executeUpdate(); - em.createNamedQuery("deleteFederatedUserCredentialsByStorageProvider") - .setParameter("storageProviderId", model.getId()) - .executeUpdate(); - em.createNamedQuery("deleteFederatedUserGroupMembershipByStorageProvider") - .setParameter("storageProviderId", model.getId()) - .executeUpdate(); - em.createNamedQuery("deleteFederatedUserRequiredActionsByStorageProvider") - .setParameter("storageProviderId", model.getId()) - .executeUpdate(); - em.createNamedQuery("deleteFederatedUserRoleMappingsByStorageProvider") - .setParameter("storageProviderId", model.getId()) - .executeUpdate(); - em.createNamedQuery("deleteFederatedUsersByStorageProvider") - .setParameter("storageProviderId", model.getId()) - .executeUpdate(); + em.createNamedQuery("deleteBrokerLinkByStorageProvider") + .setParameter("storageProviderId", model.getId()) + .executeUpdate(); + em.createNamedQuery("deleteFederatedAttributesByStorageProvider") + .setParameter("storageProviderId", model.getId()) + .executeUpdate(); + em.createNamedQuery("deleteFederatedUserConsentProtMappersByStorageProvider") + .setParameter("storageProviderId", model.getId()) + .executeUpdate(); + em.createNamedQuery("deleteFederatedUserRoleMappingsByStorageProvider") + .setParameter("storageProviderId", model.getId()) + .executeUpdate(); + em.createNamedQuery("deleteFederatedUserConsentsByStorageProvider") + .setParameter("storageProviderId", model.getId()) + .executeUpdate(); + em.createNamedQuery("deleteFederatedCredentialAttributeByStorageProvider") + .setParameter("storageProviderId", model.getId()) + .executeUpdate(); + em.createNamedQuery("deleteFederatedUserCredentialsByStorageProvider") + .setParameter("storageProviderId", model.getId()) + .executeUpdate(); + em.createNamedQuery("deleteFederatedUserGroupMembershipByStorageProvider") + .setParameter("storageProviderId", model.getId()) + .executeUpdate(); + em.createNamedQuery("deleteFederatedUserRequiredActionsByStorageProvider") + .setParameter("storageProviderId", model.getId()) + .executeUpdate(); + em.createNamedQuery("deleteFederatedUserRoleMappingsByStorageProvider") + .setParameter("storageProviderId", model.getId()) + .executeUpdate(); + em.createNamedQuery("deleteFederatedUsersByStorageProvider") + .setParameter("storageProviderId", model.getId()) + .executeUpdate(); + } else if (model.getProviderType().equals(ClientStorageProvider.class.getName())) { + em.createNamedQuery("deleteFederatedUserConsentProtMappersByClientStorageProvider") + .setParameter("clientStorageProvider", model.getId()) + .executeUpdate(); + em.createNamedQuery("deleteFederatedUserConsentRolesByClientStorageProvider") + .setParameter("clientStorageProvider", model.getId()) + .executeUpdate(); + em.createNamedQuery("deleteFederatedUserConsentsByClientStorageProvider") + .setParameter("clientStorageProvider", model.getId()) + .executeUpdate(); + + } } } diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentEntity.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentEntity.java index 225e80bd72..c2eac0ba39 100755 --- a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentEntity.java +++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentEntity.java @@ -40,11 +40,14 @@ import java.util.Collection; }) @NamedQueries({ @NamedQuery(name="userFederatedConsentByUserAndClient", query="select consent from FederatedUserConsentEntity consent where consent.userId = :userId and consent.clientId = :clientId"), + @NamedQuery(name="userFederatedConsentByUserAndExternalClient", query="select consent from FederatedUserConsentEntity consent where consent.userId = :userId and consent.clientStorageProvider = :clientStorageProvider and consent.externalClientId = :externalClientId"), @NamedQuery(name="userFederatedConsentsByUser", query="select consent from FederatedUserConsentEntity consent where consent.userId = :userId"), @NamedQuery(name="deleteFederatedUserConsentsByRealm", query="delete from FederatedUserConsentEntity consent where consent.realmId=:realmId"), @NamedQuery(name="deleteFederatedUserConsentsByStorageProvider", query="delete from FederatedUserConsentEntity e where e.storageProviderId=:storageProviderId"), @NamedQuery(name="deleteFederatedUserConsentsByUser", query="delete from FederatedUserConsentEntity consent where consent.userId = :userId and consent.realmId = :realmId"), @NamedQuery(name="deleteFederatedUserConsentsByClient", query="delete from FederatedUserConsentEntity consent where consent.clientId = :clientId"), + @NamedQuery(name="deleteFederatedUserConsentsByExternalClient", query="delete from FederatedUserConsentEntity consent where consent.clientStorageProvider = :clientStorageProvider and consent.externalClientId = :externalClientId"), + @NamedQuery(name="deleteFederatedUserConsentsByClientStorageProvider", query="delete from FederatedUserConsentEntity consent where consent.clientStorageProvider = :clientStorageProvider"), }) public class FederatedUserConsentEntity { @@ -65,6 +68,12 @@ public class FederatedUserConsentEntity { @Column(name="CLIENT_ID") protected String clientId; + @Column(name="CLIENT_STORAGE_PROVIDER") + protected String clientStorageProvider; + + @Column(name="EXTERNAL_CLIENT_ID") + protected String externalClientId; + @Column(name = "CREATED_DATE") private Long createdDate; @@ -119,6 +128,22 @@ public class FederatedUserConsentEntity { this.clientId = clientId; } + public String getClientStorageProvider() { + return clientStorageProvider; + } + + public void setClientStorageProvider(String clientStorageProvider) { + this.clientStorageProvider = clientStorageProvider; + } + + public String getExternalClientId() { + return externalClientId; + } + + public void setExternalClientId(String externalClientId) { + this.externalClientId = externalClientId; + } + public Collection getGrantedRoles() { return grantedRoles; } diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentProtocolMapperEntity.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentProtocolMapperEntity.java index f7da2cc310..a9de2c6b58 100755 --- a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentProtocolMapperEntity.java +++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentProtocolMapperEntity.java @@ -39,6 +39,8 @@ import java.io.Serializable; @NamedQuery(name="deleteFederatedUserConsentProtMappersByStorageProvider", query="delete from FederatedUserConsentProtocolMapperEntity csm where csm.userConsent IN (select consent from FederatedUserConsentEntity consent where consent.storageProviderId = :storageProviderId)"), @NamedQuery(name="deleteFederatedUserConsentProtMappersByProtocolMapper", query="delete from FederatedUserConsentProtocolMapperEntity csm where csm.protocolMapperId = :protocolMapperId"), @NamedQuery(name="deleteFederatedUserConsentProtMappersByClient", query="delete from FederatedUserConsentProtocolMapperEntity csm where csm.userConsent IN (select consent from FederatedUserConsentEntity consent where consent.clientId = :clientId)"), + @NamedQuery(name="deleteFederatedUserConsentProtMappersByExternalClient", query="delete from FederatedUserConsentProtocolMapperEntity csm where csm.userConsent IN (select consent from FederatedUserConsentEntity consent where consent.clientStorageProvider = :clientStorageProvider and consent.externalClientId = :externalClientId)"), + @NamedQuery(name="deleteFederatedUserConsentProtMappersByClientStorageProvider", query="delete from FederatedUserConsentProtocolMapperEntity csm where csm.userConsent IN (select consent from FederatedUserConsentEntity consent where consent.clientStorageProvider = :clientStorageProvider)"), }) @Entity @Table(name="FED_USER_CONSENT_PROT_MAPPER") diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentRoleEntity.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentRoleEntity.java index d74865d11d..0e0551d918 100755 --- a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentRoleEntity.java +++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentRoleEntity.java @@ -38,6 +38,8 @@ import java.io.Serializable; @NamedQuery(name="deleteFederatedUserConsentRolesByStorageProvider", query="delete from FederatedUserConsentRoleEntity grantedRole where grantedRole.userConsent IN (select consent from FederatedUserConsentEntity consent where consent.storageProviderId = :storageProviderId)"), @NamedQuery(name="deleteFederatedUserConsentRolesByRole", query="delete from FederatedUserConsentRoleEntity grantedRole where grantedRole.roleId = :roleId"), @NamedQuery(name="deleteFederatedUserConsentRolesByClient", query="delete from FederatedUserConsentRoleEntity grantedRole where grantedRole.userConsent IN (select consent from FederatedUserConsentEntity consent where consent.clientId = :clientId)"), + @NamedQuery(name="deleteFederatedUserConsentRolesByExternalClient", query="delete from FederatedUserConsentRoleEntity grantedRole where grantedRole.userConsent IN (select consent from FederatedUserConsentEntity consent where consent.clientStorageProvider = :clientStorageProvider and consent.externalClientId = :externalClientId)"), + @NamedQuery(name="deleteFederatedUserConsentRolesByClientStorageProvider", query="delete from FederatedUserConsentRoleEntity grantedRole where grantedRole.userConsent IN (select consent from FederatedUserConsentEntity consent where consent.clientStorageProvider = :clientStorageProvider)"), }) @Entity @Table(name="FED_USER_CONSENT_ROLE") diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-4.0.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-4.0.0.xml index f7ebc6838b..2987163198 100644 --- a/model/jpa/src/main/resources/META-INF/jpa-changelog-4.0.0.xml +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-4.0.0.xml @@ -29,4 +29,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/model/jpa/src/main/resources/META-INF/persistence.xml b/model/jpa/src/main/resources/META-INF/persistence.xml index f23198c387..36e3fb40d2 100755 --- a/model/jpa/src/main/resources/META-INF/persistence.xml +++ b/model/jpa/src/main/resources/META-INF/persistence.xml @@ -39,7 +39,6 @@ org.keycloak.models.jpa.entities.UserRoleMappingEntity org.keycloak.models.jpa.entities.IdentityProviderEntity org.keycloak.models.jpa.entities.IdentityProviderMapperEntity - org.keycloak.models.jpa.entities.ClientIdentityProviderMappingEntity org.keycloak.models.jpa.entities.ProtocolMapperEntity org.keycloak.models.jpa.entities.UserConsentEntity org.keycloak.models.jpa.entities.UserConsentRoleEntity diff --git a/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java b/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java index 61ae1beabc..ce71deef82 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java @@ -25,7 +25,7 @@ import org.keycloak.models.RealmProvider; */ public interface CacheRealmProvider extends RealmProvider { void clear(); - RealmProvider getDelegate(); + RealmProvider getRealmDelegate(); void registerRealmInvalidation(String id, String name); diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index 0dad16a48d..2e7d3ef92d 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -485,6 +485,8 @@ public class ModelToRepresentation { public static ClientRepresentation toRepresentation(ClientModel clientModel) { ClientRepresentation rep = new ClientRepresentation(); rep.setId(clientModel.getId()); + String providerId = StorageId.resolveProviderId(clientModel); + rep.setOrigin(providerId); rep.setClientId(clientModel.getClientId()); rep.setName(clientModel.getName()); rep.setDescription(clientModel.getDescription()); diff --git a/server-spi-private/src/main/java/org/keycloak/storage/client/AbstractClientStorageAdapter.java b/server-spi-private/src/main/java/org/keycloak/storage/client/AbstractClientStorageAdapter.java new file mode 100644 index 0000000000..db2a9adeac --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/storage/client/AbstractClientStorageAdapter.java @@ -0,0 +1,139 @@ +/* + * 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.client; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.storage.StorageId; + +import java.util.Collections; +import java.util.Map; + +/** + * Helper base class for ClientModel implementations for ClientStorageProvider implementations. + * + * Contains default implementations of some methods + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +public abstract class AbstractClientStorageAdapter extends UnsupportedOperationsClientStorageAdapter { + protected KeycloakSession session; + protected RealmModel realm; + protected ClientStorageProviderModel component; + private StorageId storageId; + + + public AbstractClientStorageAdapter(KeycloakSession session, RealmModel realm, ClientStorageProviderModel component) { + this.session = session; + this.realm = realm; + this.component = component; + } + + /** + * Creates federated id based on getClientId() method + * + * @return + */ + @Override + public String getId() { + if (storageId == null) { + storageId = new StorageId(component.getId(), getClientId()); + } + return storageId.getId(); + } + + @Override + public final RealmModel getRealm() { + return realm; + } + + + /** + * This method really isn't used by anybody anywhere. Legacy feature never supported. + * + * @return + */ + @Override + public boolean isSurrogateAuthRequired() { + return false; + } + + /** + * This method really isn't used by anybody anywhere. Legacy feature never supported. + * + * @return + */ + @Override + public void setSurrogateAuthRequired(boolean surrogateAuthRequired) { + // do nothing, we don't do anything with this. + } + + /** + * This is for logout. Empty implementation for now. Can override if you can store this information somewhere. + * + * @return + */ + @Override + public Map getRegisteredNodes() { + return Collections.EMPTY_MAP; + } + + /** + * This is for logout. Empty implementation for now. Can override if you can store this information somewhere. + * + * @return + */ + @Override + public void registerNode(String nodeHost, int registrationTime) { + // do nothing + } + + /** + * This is for logout. Empty implementation for now. Can override if you can store this information somewhere. + * + * @return + */ + @Override + public void unregisterNode(String nodeHost) { + // do nothing + } + + /** + * Overriding implementations should call super.updateClient() as this fires off an update event. + * + */ + @Override + public void updateClient() { + session.getKeycloakSessionFactory().publish(new RealmModel.ClientUpdatedEvent() { + + @Override + public ClientModel getUpdatedClient() { + return AbstractClientStorageAdapter.this; + } + + @Override + public KeycloakSession getKeycloakSession() { + return session; + } + }); + + } + + +} diff --git a/server-spi-private/src/main/java/org/keycloak/storage/client/AbstractReadOnlyClientStorageAdapter.java b/server-spi-private/src/main/java/org/keycloak/storage/client/AbstractReadOnlyClientStorageAdapter.java new file mode 100644 index 0000000000..d8e6bd456a --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/storage/client/AbstractReadOnlyClientStorageAdapter.java @@ -0,0 +1,280 @@ +/* + * 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.client; + +import org.keycloak.models.ClientTemplateModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.storage.ReadOnlyException; + +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public abstract class AbstractReadOnlyClientStorageAdapter extends AbstractClientStorageAdapter { + public AbstractReadOnlyClientStorageAdapter(KeycloakSession session, RealmModel realm, ClientStorageProviderModel component) { + super(session, realm, component); + } + + @Override + public void setClientId(String clientId) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void setName(String name) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void setDescription(String description) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void setEnabled(boolean enabled) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void setWebOrigins(Set webOrigins) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void addWebOrigin(String webOrigin) { + throw new ReadOnlyException("client is read only for this update"); + } + + @Override + public void removeWebOrigin(String webOrigin) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void setRedirectUris(Set redirectUris) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void addRedirectUri(String redirectUri) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void removeRedirectUri(String redirectUri) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void setManagementUrl(String url) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void setRootUrl(String url) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void setBaseUrl(String url) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void setBearerOnly(boolean only) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void setNodeReRegistrationTimeout(int timeout) { + + throw new ReadOnlyException("client is read only for this update"); + } + + @Override + public void setClientAuthenticatorType(String clientAuthenticatorType) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void setSecret(String secret) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void setRegistrationToken(String registrationToken) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void setProtocol(String protocol) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void setAttribute(String name, String value) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void removeAttribute(String name) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void removeAuthenticationFlowBindingOverride(String binding) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void setAuthenticationFlowBindingOverride(String binding, String flowId) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void setFrontchannelLogout(boolean flag) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void setPublicClient(boolean flag) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void setConsentRequired(boolean consentRequired) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void setStandardFlowEnabled(boolean standardFlowEnabled) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void setImplicitFlowEnabled(boolean implicitFlowEnabled) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void setDirectAccessGrantsEnabled(boolean directAccessGrantsEnabled) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void setClientTemplate(ClientTemplateModel template) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void setUseTemplateScope(boolean flag) { + + throw new ReadOnlyException("client is read only for this update"); + } + + @Override + public void setUseTemplateMappers(boolean flag) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void setUseTemplateConfig(boolean flag) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void setNotBefore(int notBefore) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public ProtocolMapperModel addProtocolMapper(ProtocolMapperModel model) { + throw new ReadOnlyException("client is read only for this update"); + } + + @Override + public void removeProtocolMapper(ProtocolMapperModel mapping) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void updateProtocolMapper(ProtocolMapperModel mapping) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void setFullScopeAllowed(boolean value) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void addScopeMapping(RoleModel role) { + throw new ReadOnlyException("client is read only for this update"); + + } + + @Override + public void deleteScopeMapping(RoleModel role) { + throw new ReadOnlyException("client is read only for this update"); + + } +} diff --git a/server-spi-private/src/main/java/org/keycloak/storage/client/ClientStorageProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/storage/client/ClientStorageProviderFactory.java new file mode 100755 index 0000000000..e9f8ee7caa --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/storage/client/ClientStorageProviderFactory.java @@ -0,0 +1,118 @@ +/* + * 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.client; + +import org.keycloak.Config; +import org.keycloak.component.ComponentFactory; +import org.keycloak.component.ComponentModel; +import org.keycloak.component.ComponentValidationException; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.provider.ProviderConfigProperty; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public interface ClientStorageProviderFactory extends ComponentFactory { + + + /** + * called per Keycloak transaction. + * + * @param session + * @param model + * @return + */ + T create(KeycloakSession session, ComponentModel model); + + /** + * This is the name of the provider and will be showed in the admin console as an option. + * + * @return + */ + @Override + String getId(); + + @Override + default void init(Config.Scope config) { + + } + + @Override + default void postInit(KeycloakSessionFactory factory) { + + } + + @Override + default void close() { + + } + + @Override + default String getHelpText() { + return ""; + } + + @Override + default List getConfigProperties() { + return Collections.EMPTY_LIST; + } + + @Override + default void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException { + + } + + /** + * Called when ClientStorageProviderModel is created. This allows you to do initialization of any additional configuration + * you need to add. + * + * @param session + * @param realm + * @param model + */ + @Override + default void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) { + + } + + /** + * configuration properties that are common across all UserStorageProvider implementations + * + * @return + */ + @Override + default + List getCommonProviderConfigProperties() { + return ClientStorageProviderSpi.commonConfig(); + } + + @Override + default + Map getTypeMetadata() { + Map metadata = new HashMap<>(); + return metadata; + } +} diff --git a/server-spi-private/src/main/java/org/keycloak/storage/client/ClientStorageProviderSpi.java b/server-spi-private/src/main/java/org/keycloak/storage/client/ClientStorageProviderSpi.java new file mode 100755 index 0000000000..bf1146d40e --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/storage/client/ClientStorageProviderSpi.java @@ -0,0 +1,83 @@ +/* + * 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.client; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ProviderConfigurationBuilder; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +import java.util.Collections; +import java.util.List; + +/** + * @author Stian Thorgersen + */ +public class ClientStorageProviderSpi implements Spi { + + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return "client-storage"; + } + + @Override + public Class getProviderClass() { + return ClientStorageProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return ClientStorageProviderFactory.class; + } + + private static final List commonConfig; + + static { + List config = ProviderConfigurationBuilder.create() + .property() + .name("enabled").type(ProviderConfigProperty.BOOLEAN_TYPE).add() + .property() + .name("priority").type(ProviderConfigProperty.STRING_TYPE).add() + .property() + .name("cachePolicy").type(ProviderConfigProperty.STRING_TYPE).add() + .property() + .name("maxLifespan").type(ProviderConfigProperty.STRING_TYPE).add() + .property() + .name("evictionHour").type(ProviderConfigProperty.STRING_TYPE).add() + .property() + .name("evictionMinute").type(ProviderConfigProperty.STRING_TYPE).add() + .property() + .name("evictionDay").type(ProviderConfigProperty.STRING_TYPE).add() + .property() + .name("cacheInvalidBefore").type(ProviderConfigProperty.STRING_TYPE).add() + .build(); + commonConfig = Collections.unmodifiableList(config); + } + + public static List commonConfig() { + return commonConfig; + + } + +} diff --git a/server-spi-private/src/main/java/org/keycloak/storage/client/UnsupportedOperationsClientStorageAdapter.java b/server-spi-private/src/main/java/org/keycloak/storage/client/UnsupportedOperationsClientStorageAdapter.java new file mode 100644 index 0000000000..4de10a60b6 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/storage/client/UnsupportedOperationsClientStorageAdapter.java @@ -0,0 +1,82 @@ +/* + * 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.client; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.ModelException; +import org.keycloak.models.RoleModel; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * Base helper class. Unsupported operations are implemented here that throw exception on invocation. + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +public abstract class UnsupportedOperationsClientStorageAdapter implements ClientModel { + @Override + public final RoleModel getRole(String name) { + return null; + } + + @Override + public final RoleModel addRole(String name) { + throw new ModelException("Unsupported operation"); + } + + @Override + public final RoleModel addRole(String id, String name) { + throw new ModelException("Unsupported operation"); + } + + @Override + public final boolean removeRole(RoleModel role) { + throw new ModelException("Unsupported operation"); + } + + @Override + public final Set getRoles() { + return Collections.EMPTY_SET; + } + + @Override + public final List getDefaultRoles() { + return Collections.EMPTY_LIST; + } + + @Override + public final void addDefaultRole(String name) { + throw new ModelException("Unsupported operation"); + + } + + @Override + public final void updateDefaultRoles(String... defaultRoles) { + throw new ModelException("Unsupported operation"); + + } + + @Override + public final void removeDefaultRoles(String... defaultRoles) { + throw new ModelException("Unsupported operation"); + } + + +} diff --git a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi index b781ca359d..a63b206527 100755 --- a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -70,4 +70,5 @@ org.keycloak.transaction.TransactionManagerLookupSpi org.keycloak.credential.hash.PasswordHashSpi org.keycloak.credential.CredentialSpi org.keycloak.keys.PublicKeyStorageSpi -org.keycloak.keys.KeySpi \ No newline at end of file +org.keycloak.keys.KeySpi +org.keycloak.storage.client.ClientStorageProviderSpi \ No newline at end of file diff --git a/server-spi/src/main/java/org/keycloak/models/ClientProvider.java b/server-spi/src/main/java/org/keycloak/models/ClientProvider.java new file mode 100644 index 0000000000..29dc415013 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/models/ClientProvider.java @@ -0,0 +1,45 @@ +/* + * 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.provider.Provider; +import org.keycloak.storage.client.ClientLookupProvider; + +import java.util.List; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public interface ClientProvider extends ClientLookupProvider, Provider { + List getClients(RealmModel realm); + + ClientModel addClient(RealmModel realm, String clientId); + + ClientModel addClient(RealmModel realm, String id, String clientId); + + RoleModel addClientRole(RealmModel realm, ClientModel client, String name); + + RoleModel addClientRole(RealmModel realm, ClientModel client, String id, String name); + + RoleModel getClientRole(RealmModel realm, ClientModel client, String name); + + Set getClientRoles(RealmModel realm, ClientModel client); + + boolean removeClient(String id, RealmModel realm); +} 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 c239fb2a32..72a0d4ca7c 100755 --- a/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java +++ b/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java @@ -124,6 +124,8 @@ public interface KeycloakSession { UserProvider users(); + ClientProvider clientStorageManager(); + /** * Un-cached view of all users in system including users loaded by UserStorageProviders * @@ -145,6 +147,15 @@ public interface KeycloakSession { */ UserProvider userLocalStorage(); + RealmProvider realmLocalStorage(); + + /** + * Keycloak specific local storage for clients. No cache in front, this api talks directly to database configured for Keycloak + * + * @return + */ + ClientProvider clientLocalStorage(); + /** * Hybrid storage for UserStorageProviders that can't store a specific piece of keycloak data in their external storage. * No cache in front. diff --git a/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java index 6d48425d7a..5eb18db8fe 100755 --- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java +++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java @@ -22,6 +22,8 @@ import org.keycloak.component.ComponentModel; import org.keycloak.provider.ProviderEvent; import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProviderModel; +import org.keycloak.storage.client.ClientStorageProvider; +import org.keycloak.storage.client.ClientStorageProviderModel; import java.util.*; @@ -341,6 +343,16 @@ public interface RealmModel extends RoleContainerModel { return list; } + default + List getClientStorageProviders() { + List list = new LinkedList<>(); + for (ComponentModel component : getComponents(getId(), ClientStorageProvider.class.getName())) { + list.add(new ClientStorageProviderModel(component)); + } + Collections.sort(list, ClientStorageProviderModel.comparator); + return list; + } + String getLoginTheme(); void setLoginTheme(String name); diff --git a/server-spi/src/main/java/org/keycloak/models/RealmProvider.java b/server-spi/src/main/java/org/keycloak/models/RealmProvider.java index d14f2d6256..6fed88af63 100755 --- a/server-spi/src/main/java/org/keycloak/models/RealmProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/RealmProvider.java @@ -19,6 +19,7 @@ package org.keycloak.models; import org.keycloak.migration.MigrationModel; import org.keycloak.provider.Provider; +import org.keycloak.storage.client.ClientLookupProvider; import java.util.List; import java.util.Set; @@ -27,7 +28,7 @@ import java.util.Set; * @author Bill Burke * @version $Revision: 1 $ */ -public interface RealmProvider extends Provider { +public interface RealmProvider extends Provider, ClientProvider { // Note: The reason there are so many query methods here is for layering a cache on top of an persistent KeycloakSession MigrationModel getMigrationModel(); @@ -58,15 +59,6 @@ public interface RealmProvider extends Provider { void addTopLevelGroup(RealmModel realm, GroupModel subGroup); - ClientModel addClient(RealmModel realm, String clientId); - - ClientModel addClient(RealmModel realm, String id, String clientId); - - List getClients(RealmModel realm); - - ClientModel getClientById(String id, RealmModel realm); - ClientModel getClientByClientId(String clientId, RealmModel realm); - RoleModel addRealmRole(RealmModel realm, String name); @@ -74,22 +66,12 @@ public interface RealmProvider extends Provider { RoleModel getRealmRole(RealmModel realm, String name); - RoleModel addClientRole(RealmModel realm, ClientModel client, String name); - - RoleModel addClientRole(RealmModel realm, ClientModel client, String id, String name); - Set getRealmRoles(RealmModel realm); - RoleModel getClientRole(RealmModel realm, ClientModel client, String name); - - Set getClientRoles(RealmModel realm, ClientModel client); - boolean removeRole(RealmModel realm, RoleModel role); RoleModel getRoleById(String id, RealmModel realm); - boolean removeClient(String id, RealmModel realm); - ClientTemplateModel getClientTemplateById(String id, RealmModel realm); GroupModel getGroupById(String id, RealmModel realm); diff --git a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java index fc67d4eb02..ac1c22409b 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java @@ -20,6 +20,7 @@ package org.keycloak.models; import org.keycloak.provider.Provider; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.function.Predicate; @@ -49,6 +50,15 @@ public interface UserSessionProvider extends Provider { long getActiveUserSessions(RealmModel realm, ClientModel client); + /** + * Returns a summary of client sessions key is client.getId() + * + * @param realm + * @param offline + * @return + */ + Map getActiveClientSessionStats(RealmModel realm, boolean offline); + /** This will remove attached ClientLoginSessionModels too **/ void removeUserSession(RealmModel realm, UserSessionModel session); void removeUserSessions(RealmModel realm, UserModel user); diff --git a/server-spi/src/main/java/org/keycloak/models/cache/CachedObject.java b/server-spi/src/main/java/org/keycloak/models/cache/CachedObject.java new file mode 100644 index 0000000000..f252db55fa --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/models/cache/CachedObject.java @@ -0,0 +1,25 @@ +/* + * 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; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public interface CachedObject { + long getCacheTimestamp(); +} diff --git a/server-spi/src/main/java/org/keycloak/storage/CacheableStorageProviderModel.java b/server-spi/src/main/java/org/keycloak/storage/CacheableStorageProviderModel.java new file mode 100644 index 0000000000..dd740f234c --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/storage/CacheableStorageProviderModel.java @@ -0,0 +1,264 @@ +/* + * 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; + +import org.keycloak.common.util.Time; +import org.keycloak.component.ComponentModel; +import org.keycloak.component.PrioritizedComponentModel; +import org.keycloak.models.cache.CachedObject; + +import java.util.Calendar; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class CacheableStorageProviderModel extends PrioritizedComponentModel { + public static final String CACHE_POLICY = "cachePolicy"; + public static final String MAX_LIFESPAN = "maxLifespan"; + public static final String EVICTION_HOUR = "evictionHour"; + public static final String EVICTION_MINUTE = "evictionMinute"; + public static final String EVICTION_DAY = "evictionDay"; + public static final String CACHE_INVALID_BEFORE = "cacheInvalidBefore"; + public static final String ENABLED = "enabled"; + + private transient CachePolicy cachePolicy; + private transient long maxLifespan = -1; + private transient int evictionHour = -1; + private transient int evictionMinute = -1; + private transient int evictionDay = -1; + private transient long cacheInvalidBefore = -1; + private transient Boolean enabled; + + public CacheableStorageProviderModel() { + } + + public CacheableStorageProviderModel(ComponentModel copy) { + super(copy); + } + + public CachePolicy getCachePolicy() { + if (cachePolicy == null) { + String str = getConfig().getFirst(CACHE_POLICY); + if (str == null) return null; + cachePolicy = CachePolicy.valueOf(str); + } + return cachePolicy; + } + + public void setCachePolicy(CachePolicy cachePolicy) { + this.cachePolicy = cachePolicy; + if (cachePolicy == null) { + getConfig().remove(CACHE_POLICY); + + } else { + getConfig().putSingle(CACHE_POLICY, cachePolicy.name()); + } + } + + public long getMaxLifespan() { + if (maxLifespan < 0) { + String str = getConfig().getFirst(MAX_LIFESPAN); + if (str == null) return -1; + maxLifespan = Long.valueOf(str); + } + return maxLifespan; + } + + public void setMaxLifespan(long maxLifespan) { + this.maxLifespan = maxLifespan; + getConfig().putSingle(MAX_LIFESPAN, Long.toString(maxLifespan)); + } + + public int getEvictionHour() { + if (evictionHour < 0) { + String str = getConfig().getFirst(EVICTION_HOUR); + if (str == null) return -1; + evictionHour = Integer.valueOf(str); + } + return evictionHour; + } + + public void setEvictionHour(int evictionHour) { + if (evictionHour > 23 || evictionHour < 0) throw new IllegalArgumentException("Must be between 0 and 23"); + this.evictionHour = evictionHour; + getConfig().putSingle(EVICTION_HOUR, Integer.toString(evictionHour)); + } + + public int getEvictionMinute() { + if (evictionMinute < 0) { + String str = getConfig().getFirst(EVICTION_MINUTE); + if (str == null) return -1; + evictionMinute = Integer.valueOf(str); + } + return evictionMinute; + } + + public void setEvictionMinute(int evictionMinute) { + if (evictionMinute > 59 || evictionMinute < 0) throw new IllegalArgumentException("Must be between 0 and 59"); + this.evictionMinute = evictionMinute; + getConfig().putSingle(EVICTION_MINUTE, Integer.toString(evictionMinute)); + } + + public int getEvictionDay() { + if (evictionDay < 0) { + String str = getConfig().getFirst(EVICTION_DAY); + if (str == null) return -1; + evictionDay = Integer.valueOf(str); + } + return evictionDay; + } + + public void setEvictionDay(int evictionDay) { + if (evictionDay > 7 || evictionDay < 1) throw new IllegalArgumentException("Must be between 1 and 7"); + this.evictionDay = evictionDay; + getConfig().putSingle(EVICTION_DAY, Integer.toString(evictionDay)); + } + + public long getCacheInvalidBefore() { + if (cacheInvalidBefore < 0) { + String str = getConfig().getFirst(CACHE_INVALID_BEFORE); + if (str == null) return -1; + cacheInvalidBefore = Long.valueOf(str); + } + return cacheInvalidBefore; + } + + public void setCacheInvalidBefore(long cacheInvalidBefore) { + this.cacheInvalidBefore = cacheInvalidBefore; + getConfig().putSingle(CACHE_INVALID_BEFORE, Long.toString(cacheInvalidBefore)); + } + + public void setEnabled(boolean flag) { + enabled = flag; + getConfig().putSingle(ENABLED, Boolean.toString(flag)); + } + + public boolean isEnabled() { + if (enabled == null) { + String val = getConfig().getFirst(ENABLED); + if (val == null) { + enabled = true; + } else { + enabled = Boolean.valueOf(val); + } + } + return enabled; + + } + + public long getLifespan() { + UserStorageProviderModel.CachePolicy policy = getCachePolicy(); + long lifespan = -1; + if (policy == null || policy == UserStorageProviderModel.CachePolicy.DEFAULT) { + lifespan = -1; + } else if (policy == CacheableStorageProviderModel.CachePolicy.EVICT_DAILY) { + if (getEvictionHour() > -1 && getEvictionMinute() > -1) { + lifespan = dailyTimeout(getEvictionHour(), getEvictionMinute()) - Time.currentTimeMillis(); + } + } else if (policy == CacheableStorageProviderModel.CachePolicy.EVICT_WEEKLY) { + if (getEvictionDay() > 0 && getEvictionHour() > -1 && getEvictionMinute() > -1) { + lifespan = weeklyTimeout(getEvictionDay(), getEvictionHour(), getEvictionMinute()) - Time.currentTimeMillis(); + } + } else if (policy == CacheableStorageProviderModel.CachePolicy.MAX_LIFESPAN) { + lifespan = getMaxLifespan(); + } + return lifespan; + } + + public boolean shouldInvalidate(CachedObject cached) { + boolean invalidate = false; + if (!isEnabled()) { + invalidate = true; + } else { + CacheableStorageProviderModel.CachePolicy policy = getCachePolicy(); + if (policy != null) { + //String currentTime = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(Time.currentTimeMillis())); + if (policy == CacheableStorageProviderModel.CachePolicy.NO_CACHE) { + invalidate = true; + } else if (cached.getCacheTimestamp() < getCacheInvalidBefore()) { + invalidate = true; + } else if (policy == CacheableStorageProviderModel.CachePolicy.MAX_LIFESPAN) { + if (cached.getCacheTimestamp() + getMaxLifespan() < Time.currentTimeMillis()) { + invalidate = true; + } + } else if (policy == CacheableStorageProviderModel.CachePolicy.EVICT_DAILY) { + long dailyTimeout = dailyTimeout(getEvictionHour(), getEvictionMinute()); + dailyTimeout = dailyTimeout - (24 * 60 * 60 * 1000); + //String timeout = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(dailyTimeout)); + //String stamp = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(cached.getCacheTimestamp())); + if (cached.getCacheTimestamp() <= dailyTimeout) { + invalidate = true; + } + } else if (policy == CacheableStorageProviderModel.CachePolicy.EVICT_WEEKLY) { + int oneWeek = 7 * 24 * 60 * 60 * 1000; + long weeklyTimeout = weeklyTimeout(getEvictionDay(), getEvictionHour(), getEvictionMinute()); + long lastTimeout = weeklyTimeout - oneWeek; + //String timeout = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(weeklyTimeout)); + //String stamp = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(cached.getCacheTimestamp())); + if (cached.getCacheTimestamp() <= lastTimeout) { + invalidate = true; + } + } + } + } + return invalidate; + } + + + public static long dailyTimeout(int hour, int minute) { + Calendar cal = Calendar.getInstance(); + Calendar cal2 = Calendar.getInstance(); + cal.setTimeInMillis(Time.currentTimeMillis()); + cal2.setTimeInMillis(Time.currentTimeMillis()); + cal2.set(Calendar.HOUR_OF_DAY, hour); + cal2.set(Calendar.MINUTE, minute); + if (cal2.getTimeInMillis() < cal.getTimeInMillis()) { + int add = (24 * 60 * 60 * 1000); + cal.add(Calendar.MILLISECOND, add); + } else { + cal.add(Calendar.MILLISECOND, (int)(cal2.getTimeInMillis() - cal.getTimeInMillis())); + } + return cal.getTimeInMillis(); + } + + public static long weeklyTimeout(int day, int hour, int minute) { + Calendar cal = Calendar.getInstance(); + Calendar cal2 = Calendar.getInstance(); + cal.setTimeInMillis(Time.currentTimeMillis()); + cal2.setTimeInMillis(Time.currentTimeMillis()); + cal2.set(Calendar.HOUR_OF_DAY, hour); + cal2.set(Calendar.MINUTE, minute); + cal2.set(Calendar.DAY_OF_WEEK, day); + if (cal2.getTimeInMillis() < cal.getTimeInMillis()) { + int add = (7 * 24 * 60 * 60 * 1000); + cal2.add(Calendar.MILLISECOND, add); + } + + return cal2.getTimeInMillis(); + } + + + + public enum CachePolicy { + NO_CACHE, + DEFAULT, + EVICT_DAILY, + EVICT_WEEKLY, + MAX_LIFESPAN + } +} diff --git a/server-spi/src/main/java/org/keycloak/storage/StorageId.java b/server-spi/src/main/java/org/keycloak/storage/StorageId.java index fbbc406857..3a2a14111f 100644 --- a/server-spi/src/main/java/org/keycloak/storage/StorageId.java +++ b/server-spi/src/main/java/org/keycloak/storage/StorageId.java @@ -17,6 +17,7 @@ package org.keycloak.storage; import org.keycloak.component.ComponentModel; +import org.keycloak.models.ClientModel; import org.keycloak.models.UserModel; import java.io.Serializable; @@ -75,8 +76,15 @@ public class StorageId implements Serializable { public static boolean isLocalStorage(UserModel user) { return new StorageId(user.getId()).getProviderId() == null; } - public static boolean isLocalStorage(String userId) { - return new StorageId(userId).getProviderId() == null; + public static boolean isLocalStorage(String id) { + return new StorageId(id).getProviderId() == null; + } + + public static String resolveProviderId(ClientModel client) { + return new StorageId(client.getId()).getProviderId(); + } + public static boolean isLocalStorage(ClientModel client) { + return new StorageId(client.getId()).getProviderId() == null; } public boolean isLocal() { return getProviderId() == null; diff --git a/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderModel.java b/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderModel.java index 1ec06a63e6..e145c40225 100755 --- a/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderModel.java +++ b/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderModel.java @@ -18,7 +18,6 @@ package org.keycloak.storage; import org.keycloak.component.ComponentModel; -import org.keycloak.component.PrioritizedComponentModel; /** * Stored configuration of a User Storage provider instance. @@ -26,27 +25,12 @@ import org.keycloak.component.PrioritizedComponentModel; * @author Marek Posolda * @author Bill Burke */ -public class UserStorageProviderModel extends PrioritizedComponentModel { +public class UserStorageProviderModel extends CacheableStorageProviderModel { - public static final String CACHE_POLICY = "cachePolicy"; - public static final String MAX_LIFESPAN = "maxLifespan"; - public static final String EVICTION_HOUR = "evictionHour"; - public static final String EVICTION_MINUTE = "evictionMinute"; - public static final String EVICTION_DAY = "evictionDay"; - public static final String CACHE_INVALID_BEFORE = "cacheInvalidBefore"; public static final String IMPORT_ENABLED = "importEnabled"; public static final String FULL_SYNC_PERIOD = "fullSyncPeriod"; public static final String CHANGED_SYNC_PERIOD = "changedSyncPeriod"; public static final String LAST_SYNC = "lastSync"; - public static final String ENABLED = "enabled"; - - public static enum CachePolicy { - NO_CACHE, - DEFAULT, - EVICT_DAILY, - EVICT_WEEKLY, - MAX_LIFESPAN - } public UserStorageProviderModel() { setProviderType(UserStorageProvider.class.getName()); @@ -60,105 +44,6 @@ public class UserStorageProviderModel extends PrioritizedComponentModel { private transient Integer changedSyncPeriod; private transient Integer lastSync; private transient Boolean importEnabled; - private transient Boolean enabled; - private transient CachePolicy cachePolicy; - private transient long maxLifespan = -1; - private transient int evictionHour = -1; - private transient int evictionMinute = -1; - private transient int evictionDay = -1; - private transient long cacheInvalidBefore = -1; - - public CachePolicy getCachePolicy() { - if (cachePolicy == null) { - String str = getConfig().getFirst(CACHE_POLICY); - if (str == null) return null; - cachePolicy = CachePolicy.valueOf(str); - } - return cachePolicy; - } - - public void setCachePolicy(CachePolicy cachePolicy) { - this.cachePolicy = cachePolicy; - if (cachePolicy == null) { - getConfig().remove(CACHE_POLICY); - - } else { - getConfig().putSingle(CACHE_POLICY, cachePolicy.name()); - } - } - - public long getMaxLifespan() { - if (maxLifespan < 0) { - String str = getConfig().getFirst(MAX_LIFESPAN); - if (str == null) return -1; - maxLifespan = Long.valueOf(str); - } - return maxLifespan; - } - - public void setMaxLifespan(long maxLifespan) { - this.maxLifespan = maxLifespan; - getConfig().putSingle(MAX_LIFESPAN, Long.toString(maxLifespan)); - } - - public int getEvictionHour() { - if (evictionHour < 0) { - String str = getConfig().getFirst(EVICTION_HOUR); - if (str == null) return -1; - evictionHour = Integer.valueOf(str); - } - return evictionHour; - } - - public void setEvictionHour(int evictionHour) { - if (evictionHour > 23 || evictionHour < 0) throw new IllegalArgumentException("Must be between 0 and 23"); - this.evictionHour = evictionHour; - getConfig().putSingle(EVICTION_HOUR, Integer.toString(evictionHour)); - } - - public int getEvictionMinute() { - if (evictionMinute < 0) { - String str = getConfig().getFirst(EVICTION_MINUTE); - if (str == null) return -1; - evictionMinute = Integer.valueOf(str); - } - return evictionMinute; - } - - public void setEvictionMinute(int evictionMinute) { - if (evictionMinute > 59 || evictionMinute < 0) throw new IllegalArgumentException("Must be between 0 and 59"); - this.evictionMinute = evictionMinute; - getConfig().putSingle(EVICTION_MINUTE, Integer.toString(evictionMinute)); - } - - public int getEvictionDay() { - if (evictionDay < 0) { - String str = getConfig().getFirst(EVICTION_DAY); - if (str == null) return -1; - evictionDay = Integer.valueOf(str); - } - return evictionDay; - } - - public void setEvictionDay(int evictionDay) { - if (evictionDay > 7 || evictionDay < 1) throw new IllegalArgumentException("Must be between 1 and 7"); - this.evictionDay = evictionDay; - getConfig().putSingle(EVICTION_DAY, Integer.toString(evictionDay)); - } - - public long getCacheInvalidBefore() { - if (cacheInvalidBefore < 0) { - String str = getConfig().getFirst(CACHE_INVALID_BEFORE); - if (str == null) return -1; - cacheInvalidBefore = Long.valueOf(str); - } - return cacheInvalidBefore; - } - - public void setCacheInvalidBefore(long cacheInvalidBefore) { - this.cacheInvalidBefore = cacheInvalidBefore; - getConfig().putSingle(CACHE_INVALID_BEFORE, Long.toString(cacheInvalidBefore)); - } public boolean isImportEnabled() { if (importEnabled == null) { @@ -178,24 +63,6 @@ public class UserStorageProviderModel extends PrioritizedComponentModel { getConfig().putSingle(IMPORT_ENABLED, Boolean.toString(flag)); } - public void setEnabled(boolean flag) { - enabled = flag; - getConfig().putSingle(ENABLED, Boolean.toString(flag)); - } - - - public boolean isEnabled() { - if (enabled == null) { - String val = getConfig().getFirst(ENABLED); - if (val == null) { - enabled = true; - } else { - enabled = Boolean.valueOf(val); - } - } - return enabled; - - } public int getFullSyncPeriod() { if (fullSyncPeriod == null) { diff --git a/server-spi/src/main/java/org/keycloak/storage/client/ClientLookupProvider.java b/server-spi/src/main/java/org/keycloak/storage/client/ClientLookupProvider.java new file mode 100644 index 0000000000..7f04e5f427 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/storage/client/ClientLookupProvider.java @@ -0,0 +1,31 @@ +/* + * 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.client; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.RealmModel; + +/** + * Abstraction interface for lookoup of clients by id and clientId. These methods required for participating in login flows. + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +public interface ClientLookupProvider { + ClientModel getClientById(String id, RealmModel realm); + ClientModel getClientByClientId(String clientId, RealmModel realm); +} diff --git a/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProvider.java b/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProvider.java new file mode 100644 index 0000000000..c0773a6188 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProvider.java @@ -0,0 +1,72 @@ +/* + * 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.client; + +import org.keycloak.models.GroupModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.provider.Provider; +import org.keycloak.storage.client.ClientLookupProvider; + +/** + * Base interface for components that want to provide an alternative storage mechanism for clients + * + * This is currently a private incomplete SPI. Please discuss on dev list if you want us to complete it or want to do the work yourself. + * This work is described in KEYCLOAK-6408 JIRA issue. + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +public interface ClientStorageProvider extends Provider, ClientLookupProvider { + + + /** + * Callback when a realm is removed. Implement this if, for example, you want to do some + * cleanup in your user storage when a realm is removed + * + * @param realm + */ + default + void preRemove(RealmModel realm) { + + } + + /** + * Callback when a group is removed. Allows you to do things like remove a user + * group mapping in your external store if appropriate + * + * @param realm + * @param group + */ + default + void preRemove(RealmModel realm, GroupModel group) { + + } + + /** + * Callback when a role is removed. Allows you to do things like remove a user + * role mapping in your external store if appropriate + + * @param realm + * @param role + */ + default + void preRemove(RealmModel realm, RoleModel role) { + + } +} + diff --git a/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProviderModel.java b/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProviderModel.java new file mode 100755 index 0000000000..54093afeb4 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProviderModel.java @@ -0,0 +1,60 @@ +/* + * 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.client; + +import org.keycloak.component.ComponentModel; +import org.keycloak.storage.CacheableStorageProviderModel; + +/** + * Stored configuration of a Client Storage provider instance. + * + * @author Bill Burke + */ +public class ClientStorageProviderModel extends CacheableStorageProviderModel { + + public static final String ENABLED = "enabled"; + + public ClientStorageProviderModel() { + setProviderType(ClientStorageProvider.class.getName()); + } + + public ClientStorageProviderModel(ComponentModel copy) { + super(copy); + } + + private transient Boolean enabled; + + public void setEnabled(boolean flag) { + enabled = flag; + getConfig().putSingle(ENABLED, Boolean.toString(flag)); + } + + + public boolean isEnabled() { + if (enabled == null) { + String val = getConfig().getFirst(ENABLED); + if (val == null) { + enabled = true; + } else { + enabled = Boolean.valueOf(val); + } + } + return enabled; + + } +} diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java index 0cc81c1876..6cabf76b7d 100644 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java @@ -20,6 +20,7 @@ import org.keycloak.component.ComponentFactory; import org.keycloak.component.ComponentModel; import org.keycloak.credential.UserCredentialStoreManager; import org.keycloak.keys.DefaultKeyManager; +import org.keycloak.models.ClientProvider; import org.keycloak.models.KeycloakContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; @@ -35,6 +36,7 @@ import org.keycloak.models.cache.UserCache; import org.keycloak.provider.Provider; import org.keycloak.provider.ProviderFactory; import org.keycloak.sessions.AuthenticationSessionProvider; +import org.keycloak.storage.ClientStorageManager; import org.keycloak.storage.UserStorageManager; import org.keycloak.storage.federated.UserFederatedStorageProvider; import org.keycloak.theme.DefaultThemeManager; @@ -58,6 +60,7 @@ public class DefaultKeycloakSession implements KeycloakSession { private final Map attributes = new HashMap<>(); private RealmProvider model; private UserStorageManager userStorageManager; + private ClientStorageManager clientStorageManager; private UserCredentialStoreManager userCredentialStorageManager; private UserSessionProvider sessionProvider; private AuthenticationSessionProvider authenticationSessionProvider; @@ -135,6 +138,23 @@ public class DefaultKeycloakSession implements KeycloakSession { return getProvider(UserProvider.class); } + @Override + public RealmProvider realmLocalStorage() { + return getProvider(RealmProvider.class); + } + + @Override + public ClientProvider clientLocalStorage() { + return realmLocalStorage(); + } + + @Override + public ClientProvider clientStorageManager() { + if (clientStorageManager == null) clientStorageManager = new ClientStorageManager(this); + return clientStorageManager; + } + + @Override public UserProvider userStorageManager() { if (userStorageManager == null) userStorageManager = new UserStorageManager(this); @@ -232,6 +252,7 @@ public class DefaultKeycloakSession implements KeycloakSession { return model; } + @Override public UserSessionProvider sessions() { if (sessionProvider == null) { diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientStorageProviderResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientStorageProviderResource.java new file mode 100644 index 0000000000..6c8561cc6d --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientStorageProviderResource.java @@ -0,0 +1,111 @@ +/* + * 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.services.resources.admin; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.annotations.cache.NoCache; +import org.jboss.resteasy.spi.NotFoundException; +import org.keycloak.common.ClientConnection; +import org.keycloak.component.ComponentModel; +import org.keycloak.events.admin.OperationType; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.services.ServicesLogger; +import org.keycloak.services.managers.UserStorageSyncManager; +import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; +import org.keycloak.storage.UserStorageProvider; +import org.keycloak.storage.UserStorageProviderModel; +import org.keycloak.storage.client.ClientStorageProvider; +import org.keycloak.storage.ldap.LDAPStorageProvider; +import org.keycloak.storage.ldap.mappers.LDAPStorageMapper; +import org.keycloak.storage.user.SynchronizationResult; + +import javax.ws.rs.BadRequestException; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriInfo; +import java.util.HashMap; +import java.util.Map; + +/** + * @resource User Storage Provider + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class ClientStorageProviderResource { + private static final Logger logger = Logger.getLogger(ClientStorageProviderResource.class); + + protected RealmModel realm; + + protected AdminPermissionEvaluator auth; + + protected AdminEventBuilder adminEvent; + + @Context + protected ClientConnection clientConnection; + + @Context + protected UriInfo uriInfo; + + @Context + protected KeycloakSession session; + + @Context + protected HttpHeaders headers; + + public ClientStorageProviderResource(RealmModel realm, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) { + this.auth = auth; + this.realm = realm; + this.adminEvent = adminEvent; + } + + /** + * Need this for admin console to display simple name of provider when displaying client detail + * + * KEYCLOAK-4328 + * + * @param id + * @return + */ + @GET + @Path("{id}/name") + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public Map getSimpleName(@PathParam("id") String id) { + auth.clients().requireList(); + + ComponentModel model = realm.getComponent(id); + if (model == null) { + throw new NotFoundException("Could not find component"); + } + if (!model.getProviderType().equals(ClientStorageProvider.class.getName())) { + throw new NotFoundException("found, but not a ClientStorageProvider"); + } + + Map data = new HashMap<>(); + data.put("id", model.getId()); + data.put("name", model.getName()); + return data; + } +} diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java index c0ea7df937..580a0bc1fe 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java @@ -98,7 +98,7 @@ public class ClientsResource { public List getClients(@QueryParam("clientId") String clientId, @QueryParam("viewableOnly") @DefaultValue("false") boolean viewableOnly) { List rep = new ArrayList<>(); - if (clientId == null) { + if (clientId == null || clientId.trim().equals("")) { List clientModels = realm.getClients(); auth.clients().requireList(); boolean view = auth.clients().canView(); 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 88af6f6548..3a82baa590 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 @@ -100,6 +100,7 @@ import java.security.cert.X509Certificate; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.LinkedList; @@ -504,17 +505,38 @@ public class RealmAdminResource { public List> getClientSessionStats() { auth.realm().requireViewRealm(); - List> data = new LinkedList>(); - for (ClientModel client : realm.getClients()) { - long size = session.sessions().getActiveUserSessions(client.getRealm(), client); - if (size == 0) continue; - Map map = new HashMap<>(); - map.put("id", client.getId()); - map.put("clientId", client.getClientId()); - map.put("active", size + ""); - data.add(map); + Map> data = new HashMap(); + { + Map activeCount =session.sessions().getActiveClientSessionStats(realm, false); + for (Map.Entry entry : activeCount.entrySet()) { + Map map = new HashMap<>(); + ClientModel client = realm.getClientById(entry.getKey()); + map.put("id", client.getId()); + map.put("clientId", client.getClientId()); + map.put("active", entry.getValue().toString()); + map.put("offline", "0"); + data.put(client.getId(), map); + + } } - return data; + { + Map offlineCount = session.sessions().getActiveClientSessionStats(realm, true); + for (Map.Entry entry : offlineCount.entrySet()) { + Map map = data.get(entry.getKey()); + if (map == null) { + map = new HashMap<>(); + ClientModel client = realm.getClientById(entry.getKey()); + map.put("id", client.getId()); + map.put("clientId", client.getClientId()); + map.put("active", "0"); + data.put(client.getId(), map); + } + map.put("offline", entry.getValue().toString()); + } + } + List> result = new LinkedList<>(); + for (Map item : data.values()) result.add(item); + return result; } /** diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java index 1c5978e88c..00726e413e 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java @@ -31,6 +31,7 @@ import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.services.ForbiddenException; +import org.keycloak.storage.StorageId; import java.util.Arrays; import java.util.Collection; @@ -634,8 +635,8 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionM public Map getAccess(ClientModel client) { Map map = new HashMap<>(); map.put("view", canView(client)); - map.put("manage", canManage(client)); - map.put("configure", canConfigure(client)); + map.put("manage", StorageId.isLocalStorage(client) && canManage(client)); + map.put("configure", StorageId.isLocalStorage(client) && canConfigure(client)); return map; } diff --git a/services/src/main/java/org/keycloak/storage/ClientStorageManager.java b/services/src/main/java/org/keycloak/storage/ClientStorageManager.java new file mode 100644 index 0000000000..d60d2f4a22 --- /dev/null +++ b/services/src/main/java/org/keycloak/storage/ClientStorageManager.java @@ -0,0 +1,222 @@ +/* + * 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; + +import org.jboss.logging.Logger; +import org.keycloak.common.util.reflections.Types; +import org.keycloak.component.ComponentModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ModelException; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.storage.client.ClientLookupProvider; +import org.keycloak.storage.client.ClientStorageProvider; +import org.keycloak.storage.client.ClientStorageProviderFactory; +import org.keycloak.storage.client.ClientStorageProviderModel; + +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class ClientStorageManager implements ClientProvider { + private static final Logger logger = Logger.getLogger(ClientStorageManager.class); + + protected KeycloakSession session; + + public static boolean isStorageProviderEnabled(RealmModel realm, String providerId) { + ClientStorageProviderModel model = getStorageProviderModel(realm, providerId); + return model.isEnabled(); + } + + public static ClientStorageProviderModel getStorageProviderModel(RealmModel realm, String componentId) { + ComponentModel model = realm.getComponent(componentId); + if (model == null) return null; + return new ClientStorageProviderModel(model); + } + + public static ClientStorageProvider getStorageProvider(KeycloakSession session, RealmModel realm, String componentId) { + ComponentModel model = realm.getComponent(componentId); + if (model == null) return null; + ClientStorageProviderModel storageModel = new ClientStorageProviderModel(model); + ClientStorageProviderFactory factory = (ClientStorageProviderFactory)session.getKeycloakSessionFactory().getProviderFactory(ClientStorageProvider.class, model.getProviderId()); + if (factory == null) { + throw new ModelException("Could not find ClientStorageProviderFactory for: " + model.getProviderId()); + } + return getStorageProviderInstance(session, storageModel, factory); + } + + + public static List getStorageProviders(RealmModel realm) { + return realm.getClientStorageProviders(); + } + + public static ClientStorageProvider getStorageProviderInstance(KeycloakSession session, ClientStorageProviderModel model, ClientStorageProviderFactory factory) { + ClientStorageProvider instance = (ClientStorageProvider)session.getAttribute(model.getId()); + if (instance != null) return instance; + instance = factory.create(session, model); + if (instance == null) { + throw new IllegalStateException("ClientStorageProvideFactory (of type " + factory.getClass().getName() + ") produced a null instance"); + } + session.enlistForClose(instance); + session.setAttribute(model.getId(), instance); + return instance; + } + + + public static List getStorageProviders(KeycloakSession session, RealmModel realm, Class type) { + List list = new LinkedList<>(); + for (ClientStorageProviderModel model : getStorageProviders(realm)) { + ClientStorageProviderFactory factory = (ClientStorageProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(ClientStorageProvider.class, model.getProviderId()); + if (factory == null) { + logger.warnv("Configured ClientStorageProvider {0} of provider id {1} does not exist in realm {2}", model.getName(), model.getProviderId(), realm.getName()); + continue; + } + if (Types.supports(type, factory, ClientStorageProviderFactory.class)) { + list.add(type.cast(getStorageProviderInstance(session, model, factory))); + } + + + } + return list; + } + + + public static List getEnabledStorageProviders(KeycloakSession session, RealmModel realm, Class type) { + List list = new LinkedList<>(); + for (ClientStorageProviderModel model : getStorageProviders(realm)) { + if (!model.isEnabled()) continue; + ClientStorageProviderFactory factory = (ClientStorageProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(ClientStorageProvider.class, model.getProviderId()); + if (factory == null) { + logger.warnv("Configured ClientStorageProvider {0} of provider id {1} does not exist in realm {2}", model.getName(), model.getProviderId(), realm.getName()); + continue; + } + if (Types.supports(type, factory, ClientStorageProviderFactory.class)) { + list.add(type.cast(getStorageProviderInstance(session, model, factory))); + } + + + } + return list; + } + + + public ClientStorageManager(KeycloakSession session) { + this.session = session; + } + + @Override + public ClientModel getClientById(String id, RealmModel realm) { + StorageId storageId = new StorageId(id); + if (storageId.getProviderId() == null) { + return session.clientLocalStorage().getClientById(id, realm); + } + ClientLookupProvider provider = (ClientLookupProvider)getStorageProvider(session, realm, storageId.getProviderId()); + if (provider == null) return null; + if (!isStorageProviderEnabled(realm, storageId.getProviderId())) return null; + return provider.getClientById(id, realm); + } + + @Override + public ClientModel getClientByClientId(String clientId, RealmModel realm) { + ClientModel client = session.clientLocalStorage().getClientByClientId(clientId, realm); + if (client != null) { + return client; + } + for (ClientLookupProvider provider : getEnabledStorageProviders(session, realm, ClientLookupProvider.class)) { + client = provider.getClientByClientId(clientId, realm); + if (client != null) return client; + } + return null; + } + + + @Override + public ClientModel addClient(RealmModel realm, String clientId) { + return session.clientLocalStorage().addClient(realm, clientId); + } + + @Override + public ClientModel addClient(RealmModel realm, String id, String clientId) { + return session.clientLocalStorage().addClient(realm, id, clientId); + } + + + + + @Override + public List getClients(RealmModel realm) { + return session.clientLocalStorage().getClients(realm); + } + + @Override + public RoleModel addClientRole(RealmModel realm, ClientModel client, String name) { + if (!StorageId.isLocalStorage(client.getId())) { + throw new RuntimeException("Federated clients do not support this operation"); + } + return session.clientLocalStorage().addClientRole(realm, client, name); + } + + @Override + public RoleModel addClientRole(RealmModel realm, ClientModel client, String id, String name) { + if (!StorageId.isLocalStorage(client.getId())) { + throw new RuntimeException("Federated clients do not support this operation"); + } + return session.clientLocalStorage().addClientRole(realm, client, id, name); + } + + @Override + public RoleModel getClientRole(RealmModel realm, ClientModel client, String name) { + if (!StorageId.isLocalStorage(client.getId())) { + //throw new RuntimeException("Federated clients do not support this operation"); + return null; + } + return session.clientLocalStorage().getClientRole(realm, client, name); + } + + @Override + public Set getClientRoles(RealmModel realm, ClientModel client) { + if (!StorageId.isLocalStorage(client.getId())) { + //throw new RuntimeException("Federated clients do not support this operation"); + return Collections.EMPTY_SET; + } + return session.clientLocalStorage().getClientRoles(realm, client); + } + + @Override + public void close() { + + } + + @Override + public boolean removeClient(String id, RealmModel realm) { + if (!StorageId.isLocalStorage(id)) { + throw new RuntimeException("Federated clients do not support this operation"); + } + return session.clientLocalStorage().removeClient(id, realm); + } + + + +} diff --git a/services/src/main/java/org/keycloak/storage/UserStorageManager.java b/services/src/main/java/org/keycloak/storage/UserStorageManager.java index 16d14e08f1..48015536d9 100755 --- a/services/src/main/java/org/keycloak/storage/UserStorageManager.java +++ b/services/src/main/java/org/keycloak/storage/UserStorageManager.java @@ -40,6 +40,7 @@ import org.keycloak.models.cache.UserCache; import org.keycloak.models.utils.ComponentUtil; import org.keycloak.models.utils.ReadOnlyUserModelDelegate; import org.keycloak.services.managers.UserStorageSyncManager; +import org.keycloak.storage.client.ClientStorageProvider; import org.keycloak.storage.federated.UserFederatedStorageProvider; import org.keycloak.storage.user.ImportedUserValidation; import org.keycloak.storage.user.UserBulkUpdateProvider; @@ -696,6 +697,11 @@ public class UserStorageManager implements UserProvider, OnUserCache, OnCreateCo @Override public void preRemove(RealmModel realm, ComponentModel component) { + if (component.getProviderType().equals(ClientStorageProvider.class.getName())) { + localStorage().preRemove(realm, component); + if (getFederatedStorage() != null) getFederatedStorage().preRemove(realm, component); + return; + } if (!component.getProviderType().equals(UserStorageProvider.class.getName())) return; localStorage().preRemove(realm, component); if (getFederatedStorage() != null) getFederatedStorage().preRemove(realm, component); diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProvider.java new file mode 100644 index 0000000000..672cb1e870 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProvider.java @@ -0,0 +1,282 @@ +/* + * 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.testsuite.federation; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientTemplateModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.storage.StorageId; +import org.keycloak.storage.client.AbstractReadOnlyClientStorageAdapter; +import org.keycloak.storage.client.ClientLookupProvider; +import org.keycloak.storage.client.ClientStorageProvider; +import org.keycloak.storage.client.ClientStorageProviderModel; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class HardcodedClientStorageProvider implements ClientStorageProvider, ClientLookupProvider { + protected KeycloakSession session; + protected ClientStorageProviderModel component; + protected String clientId; + protected String redirectUri; + protected boolean consent; + + public HardcodedClientStorageProvider(KeycloakSession session, ClientStorageProviderModel component) { + this.session = session; + this.component = component; + this.clientId = component.getConfig().getFirst(HardcodedClientStorageProviderFactory.CLIENT_ID); + this.redirectUri = component.getConfig().getFirst(HardcodedClientStorageProviderFactory.REDIRECT_URI); + this.consent = "true".equals(component.getConfig().getFirst(HardcodedClientStorageProviderFactory.CONSENT)); + } + + @Override + public ClientModel getClientById(String id, RealmModel realm) { + StorageId storageId = new StorageId(id); + final String clientId = storageId.getExternalId(); + if (this.clientId.equals(clientId)) return new ClientAdapter(realm); + return null; + } + + @Override + public ClientModel getClientByClientId(String clientId, RealmModel realm) { + if (this.clientId.equals(clientId)) return new ClientAdapter(realm); + return null; + } + + @Override + public void close() { + + } + + public class ClientAdapter extends AbstractReadOnlyClientStorageAdapter { + + public ClientAdapter(RealmModel realm) { + super(HardcodedClientStorageProvider.this.session, realm, HardcodedClientStorageProvider.this.component); + } + + @Override + public String getClientId() { + return clientId; + } + + @Override + public String getName() { + return "Federated Client"; + } + + @Override + public String getDescription() { + return "Pulled in from client storage provider"; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public Set getWebOrigins() { + return Collections.EMPTY_SET; + } + + @Override + public Set getRedirectUris() { + HashSet set = new HashSet<>(); + set.add(redirectUri); + return set; + } + + @Override + public String getManagementUrl() { + return null; + } + + @Override + public String getRootUrl() { + return null; + } + + @Override + public String getBaseUrl() { + return null; + } + + @Override + public boolean isBearerOnly() { + return false; + } + + @Override + public int getNodeReRegistrationTimeout() { + return 0; + } + + @Override + public String getClientAuthenticatorType() { + return null; + } + + @Override + public boolean validateSecret(String secret) { + return "password".equals(secret); + } + + @Override + public String getSecret() { + return "password"; + } + + @Override + public String getRegistrationToken() { + return null; + } + + @Override + public String getProtocol() { + return "openid-connect"; + } + + @Override + public String getAttribute(String name) { + return null; + } + + @Override + public Map getAttributes() { + return Collections.EMPTY_MAP; + } + + @Override + public String getAuthenticationFlowBindingOverride(String binding) { + return null; + } + + @Override + public Map getAuthenticationFlowBindingOverrides() { + return Collections.EMPTY_MAP; + } + + @Override + public boolean isFrontchannelLogout() { + return false; + } + + @Override + public boolean isPublicClient() { + return false; + } + + @Override + public boolean isConsentRequired() { + return consent; + } + + @Override + public boolean isStandardFlowEnabled() { + return true; + } + + @Override + public boolean isImplicitFlowEnabled() { + return true; + } + + @Override + public boolean isDirectAccessGrantsEnabled() { + return true; + } + + @Override + public boolean isServiceAccountsEnabled() { + return false; + } + + @Override + public ClientTemplateModel getClientTemplate() { + return null; + } + + @Override + public boolean useTemplateScope() { + return false; + } + + @Override + public boolean useTemplateMappers() { + return false; + } + + @Override + public boolean useTemplateConfig() { + return false; + } + + @Override + public int getNotBefore() { + return 0; + } + + @Override + public Set getProtocolMappers() { + return Collections.EMPTY_SET; + } + + @Override + public ProtocolMapperModel getProtocolMapperById(String id) { + return null; + } + + @Override + public ProtocolMapperModel getProtocolMapperByName(String protocol, String name) { + return null; + } + + @Override + public boolean isFullScopeAllowed() { + return false; + } + + @Override + public Set getScopeMappings() { + RoleModel offlineAccess = realm.getRole("offline_access"); + Set set = new HashSet<>(); + set.add(offlineAccess); + return set; + } + + @Override + public Set getRealmScopeMappings() { + return Collections.EMPTY_SET; + } + + @Override + public boolean hasScope(RoleModel role) { + return false; + } + } + + +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProviderFactory.java new file mode 100644 index 0000000000..67fcc0e52d --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProviderFactory.java @@ -0,0 +1,81 @@ +/* + * 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.testsuite.federation; + +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ProviderConfigurationBuilder; +import org.keycloak.storage.client.ClientStorageProviderFactory; +import org.keycloak.storage.client.ClientStorageProviderModel; + +import java.util.List; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class HardcodedClientStorageProviderFactory implements ClientStorageProviderFactory { + @Override + public HardcodedClientStorageProvider create(KeycloakSession session, ComponentModel model) { + return new HardcodedClientStorageProvider(session, new ClientStorageProviderModel(model)); + } + + + public static final String PROVIDER_ID = "hardcoded-client"; + + @Override + public String getId() { + return PROVIDER_ID; + } + + protected static final List CONFIG_PROPERTIES; + + public static final String CLIENT_ID = "client_id"; + + public static final String REDIRECT_URI = "redirect_uri"; + public static final String CONSENT = "consent"; + + static { + CONFIG_PROPERTIES = ProviderConfigurationBuilder.create() + .property().name(CLIENT_ID) + .type(ProviderConfigProperty.STRING_TYPE) + .label("Hardcoded Client Id") + .helpText("Only this client id is available for lookup") + .defaultValue("hardcoded-client") + .add() + .property().name(REDIRECT_URI) + .type(ProviderConfigProperty.STRING_TYPE) + .label("Redirect Uri") + .helpText("Valid redirect uri. Only one allowed") + .defaultValue("http://localhost:8180/*") + .add() + .property().name(CONSENT) + .type(ProviderConfigProperty.BOOLEAN_TYPE) + .label("Consent Required") + .helpText("Is consent required") + .defaultValue("false") + .add() + .build(); + } + + + @Override + public List getConfigProperties() { + return CONFIG_PROPERTIES; + } +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.storage.client.ClientStorageProviderFactory b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.storage.client.ClientStorageProviderFactory new file mode 100644 index 0000000000..0ed6376e2c --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.storage.client.ClientStorageProviderFactory @@ -0,0 +1 @@ +org.keycloak.testsuite.federation.HardcodedClientStorageProviderFactory \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/ClientStorageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/ClientStorageTest.java new file mode 100644 index 0000000000..e2517022d5 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/ClientStorageTest.java @@ -0,0 +1,481 @@ +/* + * Copyright 2017 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.testsuite.federation.storage; + +import org.apache.commons.io.FileUtils; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.graphene.page.Page; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.OAuth2Constants; +import org.keycloak.admin.client.resource.ClientsResource; +import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory; +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.component.ComponentModel; +import org.keycloak.events.Details; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.AuthenticationFlowBindings; +import org.keycloak.models.AuthenticationFlowModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.Constants; +import org.keycloak.models.RealmModel; +import org.keycloak.models.cache.infinispan.ClientAdapter; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.RefreshToken; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ComponentRepresentation; +import org.keycloak.representations.idm.EventRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.storage.CacheableStorageProviderModel; +import org.keycloak.storage.UserStorageProvider; +import org.keycloak.storage.client.ClientStorageProvider; +import org.keycloak.storage.client.ClientStorageProviderModel; +import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory; +import org.keycloak.testsuite.federation.HardcodedClientStorageProviderFactory; +import org.keycloak.testsuite.federation.UserMapStorageFactory; +import org.keycloak.testsuite.federation.UserPropertyFileStorageFactory; +import org.keycloak.testsuite.forms.UsernameOnlyAuthenticator; +import org.keycloak.testsuite.pages.AppPage; +import org.keycloak.testsuite.pages.ErrorPage; +import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.runonserver.RunOnServerDeployment; +import org.keycloak.testsuite.util.OAuthClient; +import org.keycloak.util.BasicAuthHelper; +import org.keycloak.util.TokenUtil; +import org.openqa.selenium.By; + +import javax.ws.rs.NotFoundException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Form; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Calendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static java.util.Calendar.DAY_OF_WEEK; +import static java.util.Calendar.HOUR_OF_DAY; +import static java.util.Calendar.MINUTE; +import static org.junit.Assert.assertEquals; +import static org.keycloak.storage.CacheableStorageProviderModel.CACHE_POLICY; +import static org.keycloak.storage.CacheableStorageProviderModel.EVICTION_DAY; +import static org.keycloak.storage.CacheableStorageProviderModel.EVICTION_HOUR; +import static org.keycloak.storage.CacheableStorageProviderModel.EVICTION_MINUTE; +import static org.keycloak.storage.CacheableStorageProviderModel.MAX_LIFESPAN; +import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername; + +/** + * Test that clients can override auth flows + * + * @author Bill Burke + */ +public class ClientStorageTest extends AbstractTestRealmKeycloakTest { + @Rule + public AssertEvents events = new AssertEvents(this); + + @Page + protected AppPage appPage; + + @Page + protected LoginPage loginPage; + + @Page + protected ErrorPage errorPage; + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + } + + protected String providerId; + + @Deployment + public static WebArchive deploy() { + return RunOnServerDeployment.create(UserResource.class) + .addPackages(true, "org.keycloak.testsuite"); + } + + protected String addComponent(ComponentRepresentation component) { + Response resp = adminClient.realm("test").components().add(component); + resp.close(); + String id = ApiUtil.getCreatedId(resp); + getCleanup().addComponentId(id); + return id; + } + + @Before + public void addProvidersBeforeTest() throws URISyntaxException, IOException { + ComponentRepresentation provider = new ComponentRepresentation(); + provider.setName("client-storage-hardcoded"); + provider.setProviderId(HardcodedClientStorageProviderFactory.PROVIDER_ID); + provider.setProviderType(ClientStorageProvider.class.getName()); + provider.setConfig(new MultivaluedHashMap<>()); + provider.getConfig().putSingle(HardcodedClientStorageProviderFactory.CLIENT_ID, "hardcoded-client"); + provider.getConfig().putSingle(HardcodedClientStorageProviderFactory.REDIRECT_URI, oauth.getRedirectUri()); + + providerId = addComponent(provider); + } + + protected String userId; + + @Before + public void clientConfiguration() { + userId = findUserByUsername(adminClient.realm("test"), "test-user@localhost").getId(); + oauth.clientId("hardcoded-client"); + } + + + + + + @Test + public void testClientStats() throws Exception { + testDirectGrant("hardcoded-client"); + testDirectGrant("hardcoded-client"); + testBrowser("test-app"); + offlineTokenDirectGrantFlowNoRefresh(); + List> list = adminClient.realm("test").getClientSessionStats(); + boolean hardTested = false; + boolean testAppTested = false; + for (Map entry : list) { + if (entry.get("clientId").equals("hardcoded-client")) { + Assert.assertEquals("3", entry.get("active")); + Assert.assertEquals("1", entry.get("offline")); + hardTested = true; + } else if (entry.get("clientId").equals("test-app")) { + Assert.assertEquals("1", entry.get("active")); + Assert.assertEquals("0", entry.get("offline")); + testAppTested = true; + } + } + Assert.assertTrue(hardTested && testAppTested); + } + + + @Test + public void testBrowser() throws Exception { + String clientId = "hardcoded-client"; + testBrowser(clientId); + //Thread.sleep(10000000); + } + + private void testBrowser(String clientId) { + oauth.clientId(clientId); + String loginFormUrl = oauth.getLoginFormUrl(); + //log.info("loginFormUrl: " + loginFormUrl); + + //Thread.sleep(10000000); + + driver.navigate().to(loginFormUrl); + + loginPage.assertCurrent(); + + // Fill username+password. I am successfully authenticated + oauth.fillLoginForm("test-user@localhost", "password"); + appPage.assertCurrent(); + + events.expectLogin().client(clientId).detail(Details.USERNAME, "test-user@localhost").assertEvent(); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); + Assert.assertNotNull(tokenResponse.getAccessToken()); + Assert.assertNotNull(tokenResponse.getRefreshToken()); + + events.clear(); + + } + + @Test + public void testGrantAccessTokenNoOverride() throws Exception { + testDirectGrant("hardcoded-client"); + } + + private void testDirectGrant(String clientId) { + Client httpClient = javax.ws.rs.client.ClientBuilder.newClient(); + String grantUri = oauth.getResourceOwnerPasswordCredentialGrantUrl(); + WebTarget grantTarget = httpClient.target(grantUri); + + { // test no password + String header = BasicAuthHelper.createHeader(clientId, "password"); + Form form = new Form(); + form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD); + form.param("username", "test-user@localhost"); + Response response = grantTarget.request() + .header(HttpHeaders.AUTHORIZATION, header) + .post(Entity.form(form)); + assertEquals(401, response.getStatus()); + response.close(); + } + + { // test invalid password + String header = BasicAuthHelper.createHeader(clientId, "password"); + Form form = new Form(); + form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD); + form.param("username", "test-user@localhost"); + form.param("password", "invalid"); + Response response = grantTarget.request() + .header(HttpHeaders.AUTHORIZATION, header) + .post(Entity.form(form)); + assertEquals(401, response.getStatus()); + response.close(); + } + + { // test valid password + String header = BasicAuthHelper.createHeader(clientId, "password"); + Form form = new Form(); + form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD); + form.param("username", "test-user@localhost"); + form.param("password", "password"); + Response response = grantTarget.request() + .header(HttpHeaders.AUTHORIZATION, header) + .post(Entity.form(form)); + assertEquals(200, response.getStatus()); + response.close(); + } + + httpClient.close(); + events.clear(); + } + + @Test + public void testDailyEviction() { + testIsCached(); + + testingClient.server().run(session -> { + RealmModel realm = session.realms().getRealmByName("test"); + ClientStorageProviderModel model = realm.getClientStorageProviders().get(0); + Calendar eviction = Calendar.getInstance(); + eviction.add(Calendar.HOUR, 1); + model.setCachePolicy(CacheableStorageProviderModel.CachePolicy.EVICT_DAILY); + model.setEvictionHour(eviction.get(HOUR_OF_DAY)); + model.setEvictionMinute(eviction.get(MINUTE)); + realm.updateComponent(model); + }); + testIsCached(); + setTimeOffset(2 * 60 * 60); // 2 hours in future + testNotCached(); + testIsCached(); + + setDefaultCachePolicy(); + testIsCached(); + + } + @Test + public void testWeeklyEviction() { + testIsCached(); + + testingClient.server().run(session -> { + RealmModel realm = session.realms().getRealmByName("test"); + ClientStorageProviderModel model = realm.getClientStorageProviders().get(0); + Calendar eviction = Calendar.getInstance(); + eviction.add(Calendar.HOUR, 4 * 24); + model.setCachePolicy(CacheableStorageProviderModel.CachePolicy.EVICT_WEEKLY); + model.setEvictionDay(eviction.get(DAY_OF_WEEK)); + model.setEvictionHour(eviction.get(HOUR_OF_DAY)); + model.setEvictionMinute(eviction.get(MINUTE)); + realm.updateComponent(model); + }); + testIsCached(); + setTimeOffset(2 * 24 * 60 * 60); // 2 days in future + testIsCached(); + setTimeOffset(5 * 24 * 60 * 60); // 5 days in future + testNotCached(); + testIsCached(); + + setDefaultCachePolicy(); + testIsCached(); + + } + @Test + public void testMaxLifespan() { + testIsCached(); + + testingClient.server().run(session -> { + RealmModel realm = session.realms().getRealmByName("test"); + ClientStorageProviderModel model = realm.getClientStorageProviders().get(0); + model.setCachePolicy(CacheableStorageProviderModel.CachePolicy.MAX_LIFESPAN); + model.setMaxLifespan(1 * 60 * 60 * 1000); + realm.updateComponent(model); + }); + testIsCached(); + + setTimeOffset(1/2 * 60 * 60); // 1/2 hour in future + + testIsCached(); + + setTimeOffset(2 * 60 * 60); // 2 hours in future + + testNotCached(); + testIsCached(); + + setDefaultCachePolicy(); + testIsCached(); + + } + + private void testNotCached() { + testingClient.server().run(session -> { + RealmModel realm = session.realms().getRealmByName("test"); + ClientModel hardcoded = realm.getClientByClientId("hardcoded-client"); + Assert.assertNotNull(hardcoded); + Assert.assertFalse(hardcoded instanceof ClientAdapter); + }); + } + + + @Test + public void testIsCached() { + testingClient.server().run(session -> { + RealmModel realm = session.realms().getRealmByName("test"); + ClientModel hardcoded = realm.getClientByClientId("hardcoded-client"); + Assert.assertNotNull(hardcoded); + Assert.assertTrue(hardcoded instanceof org.keycloak.models.cache.infinispan.ClientAdapter); + }); + } + + + @Test + public void testNoCache() { + testIsCached(); + + testingClient.server().run(session -> { + RealmModel realm = session.realms().getRealmByName("test"); + ClientStorageProviderModel model = realm.getClientStorageProviders().get(0); + model.setCachePolicy(CacheableStorageProviderModel.CachePolicy.NO_CACHE); + realm.updateComponent(model); + }); + + testNotCached(); + + // test twice because updating component should evict + testNotCached(); + + // set it back + setDefaultCachePolicy(); + testIsCached(); + + + } + + private void setDefaultCachePolicy() { + testingClient.server().run(session -> { + RealmModel realm = session.realms().getRealmByName("test"); + ClientStorageProviderModel model = realm.getClientStorageProviders().get(0); + model.setCachePolicy(CacheableStorageProviderModel.CachePolicy.DEFAULT); + realm.updateComponent(model); + }); + } + + @Test + public void offlineTokenDirectGrantFlow() throws Exception { + oauth.scope(OAuth2Constants.OFFLINE_ACCESS); + oauth.clientId("hardcoded-client"); + OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password"); + Assert.assertNull(tokenResponse.getErrorDescription()); + AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken()); + String offlineTokenString = tokenResponse.getRefreshToken(); + RefreshToken offlineToken = oauth.verifyRefreshToken(offlineTokenString); + + events.expectLogin() + .client("hardcoded-client") + .user(userId) + .session(token.getSessionState()) + .detail(Details.GRANT_TYPE, OAuth2Constants.PASSWORD) + .detail(Details.TOKEN_ID, token.getId()) + .detail(Details.REFRESH_TOKEN_ID, offlineToken.getId()) + .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE) + .detail(Details.USERNAME, "test-user@localhost") + .removeDetail(Details.CODE_ID) + .removeDetail(Details.REDIRECT_URI) + .removeDetail(Details.CONSENT) + .assertEvent(); + + Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType()); + Assert.assertEquals(0, offlineToken.getExpiration()); + + testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId); + + // Assert same token can be refreshed again + testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId); + } + public void offlineTokenDirectGrantFlowNoRefresh() throws Exception { + oauth.scope(OAuth2Constants.OFFLINE_ACCESS); + oauth.clientId("hardcoded-client"); + OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password"); + Assert.assertNull(tokenResponse.getErrorDescription()); + AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken()); + String offlineTokenString = tokenResponse.getRefreshToken(); + RefreshToken offlineToken = oauth.verifyRefreshToken(offlineTokenString); + } + + private String testRefreshWithOfflineToken(AccessToken oldToken, RefreshToken offlineToken, String offlineTokenString, + final String sessionId, String userId) { + // Change offset to big value to ensure userSession expired + setTimeOffset(99999); + Assert.assertFalse(oldToken.isActive()); + Assert.assertTrue(offlineToken.isActive()); + + // Assert userSession expired + testingClient.testing().removeExpired("test"); + try { + testingClient.testing().removeUserSession("test", sessionId); + } catch (NotFoundException nfe) { + // Ignore + } + + OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(offlineTokenString, "password"); + AccessToken refreshedToken = oauth.verifyToken(response.getAccessToken()); + Assert.assertEquals(200, response.getStatusCode()); + Assert.assertEquals(sessionId, refreshedToken.getSessionState()); + + // Assert new refreshToken in the response + String newRefreshToken = response.getRefreshToken(); + Assert.assertNotNull(newRefreshToken); + Assert.assertNotEquals(oldToken.getId(), refreshedToken.getId()); + + Assert.assertEquals(userId, refreshedToken.getSubject()); + + Assert.assertTrue(refreshedToken.getRealmAccess().isUserInRole(Constants.OFFLINE_ACCESS_ROLE)); + + + EventRepresentation refreshEvent = events.expectRefresh(offlineToken.getId(), sessionId) + .client("hardcoded-client") + .user(userId) + .removeDetail(Details.UPDATED_REFRESH_TOKEN_ID) + .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE) + .assertEvent(); + Assert.assertNotEquals(oldToken.getId(), refreshEvent.getDetails().get(Details.TOKEN_ID)); + + setTimeOffset(0); + return newRefreshToken; + } + + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/MapCollectTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/MapCollectTest.java new file mode 100644 index 0000000000..bf1e656c1f --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/MapCollectTest.java @@ -0,0 +1,110 @@ +/* + * 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.testsuite.federation.storage; + +import org.infinispan.stream.CacheCollectors; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class MapCollectTest { + + public static class UserSessionObject { + public String id; + public String realm; + public Set clients = new HashSet<>(); + + public UserSessionObject(String realm, String... clients) { + this.id = UUID.randomUUID().toString(); + this.realm = realm; + for (String c : clients) this.clients.add(c); + } + } + + public static class RealmFilter implements Predicate { + protected String realm; + + public RealmFilter(String realm) { + this.realm = realm; + } + + @Override + public boolean test(UserSessionObject entry) { + return entry.realm.equals(realm); + } + + public static RealmFilter create(String realm) { + return new RealmFilter(realm); + } + } + + public static Set clients(UserSessionObject s) { + return s.clients; + } + + + @Test + public void testMe() throws Exception { + + List list = Arrays.asList( + new UserSessionObject("realm1", "a", "b") + , new UserSessionObject("realm1", "a", "c") + , new UserSessionObject("realm1", "a", "d") + , new UserSessionObject("realm1", "a", "b") + , new UserSessionObject("realm2", "a", "b") + , new UserSessionObject("realm2", "a", "c") + , new UserSessionObject("realm2", "a", "b") + + ); + + Map result = list.stream().collect( + Collectors.groupingBy(s -> s.realm, Collectors.summingLong(i -> 1))); + + for (Map.Entry entry : result.entrySet()) { + System.out.println(entry.getKey() + ":" + entry.getValue()); + } + + result = list.stream() + .filter(RealmFilter.create("realm1")) + .map(s->s.clients) + .flatMap(c->c.stream()) + .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); + + for (Map.Entry entry : result.entrySet()) { + System.out.println(entry.getKey() + ":" + entry.getValue()); + } + + + + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java index c3a341b190..138ddcde0a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java @@ -41,7 +41,7 @@ import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.storage.UserStorageProvider; import static org.keycloak.storage.UserStorageProviderModel.CACHE_POLICY; -import org.keycloak.storage.UserStorageProviderModel.CachePolicy; +import org.keycloak.storage.CacheableStorageProviderModel.CachePolicy; import static org.keycloak.storage.UserStorageProviderModel.EVICTION_DAY; import static org.keycloak.storage.UserStorageProviderModel.EVICTION_HOUR; import static org.keycloak.storage.UserStorageProviderModel.EVICTION_MINUTE; diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java index 48228a24af..d446985339 100644 --- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java +++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java @@ -167,16 +167,36 @@ public class UserStorageTest { KeycloakSession session = keycloakRule.startSession(); RealmModel realm = session.realms().getRealmByName("test"); CachedUserModel thor = (CachedUserModel)session.users().getUserByUsername("thor", realm); - long thorTimestamp = thor.getCacheTimestamp(); + long lastTimestamp = thor.getCacheTimestamp(); realm.updateComponent(model); keycloakRule.stopSession(session, true); + session = keycloakRule.startSession(); + realm = session.realms().getRealmByName("test"); + thor = (CachedUserModel)session.users().getUserByUsername("thor", realm); + lastTimestamp = thor.getCacheTimestamp(); + realm.updateComponent(model); + keycloakRule.stopSession(session, true); + + // test is cached + session = keycloakRule.startSession(); + realm = session.realms().getRealmByName("test"); + thor = (CachedUserModel)session.users().getUserByUsername("thor", realm); + // thor should be evicted because we changed the model + Assert.assertTrue(thor.getCacheTimestamp() > lastTimestamp); + lastTimestamp = thor.getCacheTimestamp(); + keycloakRule.stopSession(session, true); + + Time.setOffset(60 * 2 * 60); // 2 hours session = keycloakRule.startSession(); realm = session.realms().getRealmByName("test"); UserModel thor2 = session.users().getUserByUsername("thor", realm); - Assert.assertFalse(thor2 instanceof CachedUserModel); + // thor should be evicted because we put it 2 hours in the future + if (thor2 instanceof CachedUserModel) { + Assert.assertTrue(((CachedUserModel)thor2).getCacheTimestamp() > lastTimestamp); + } model.getConfig().remove("cachePolicy"); model.getConfig().remove("evictionHour"); model.getConfig().remove("evictionMinute"); @@ -199,24 +219,46 @@ public class UserStorageTest { KeycloakSession session = keycloakRule.startSession(); RealmModel realm = session.realms().getRealmByName("test"); - CachedUserModel thor = (CachedUserModel)session.users().getUserByUsername("thor", realm); realm.updateComponent(model); keycloakRule.stopSession(session, true); + session = keycloakRule.startSession(); + realm = session.realms().getRealmByName("test"); + CachedUserModel thor = (CachedUserModel)session.users().getUserByUsername("thor", realm); + long lastTimestamp = thor.getCacheTimestamp(); + keycloakRule.stopSession(session, true); + + session = keycloakRule.startSession(); + realm = session.realms().getRealmByName("test"); + thor = (CachedUserModel)session.users().getUserByUsername("thor", realm); + lastTimestamp = thor.getCacheTimestamp(); + keycloakRule.stopSession(session, true); + + session = keycloakRule.startSession(); + realm = session.realms().getRealmByName("test"); + thor = (CachedUserModel)session.users().getUserByUsername("thor", realm); + lastTimestamp = thor.getCacheTimestamp(); + keycloakRule.stopSession(session, true); + Time.setOffset(60 * 60 * 24 * 2); // 2 days in future, should be cached still session = keycloakRule.startSession(); realm = session.realms().getRealmByName("test"); // test still - UserModel thor2 = session.users().getUserByUsername("thor", realm); - Assert.assertTrue(thor2 instanceof CachedUserModel); + thor = (CachedUserModel)session.users().getUserByUsername("thor", realm); + Assert.assertEquals(thor.getCacheTimestamp(), lastTimestamp); + lastTimestamp = thor.getCacheTimestamp(); keycloakRule.stopSession(session, true); + Time.setOffset(Time.getOffset() + 60 * 60 * 24 * 3); // 3 days into future, cache will be invalidated session = keycloakRule.startSession(); realm = session.realms().getRealmByName("test"); - thor2 = session.users().getUserByUsername("thor", realm); - Assert.assertFalse(thor2 instanceof CachedUserModel); + UserModel thor2 = session.users().getUserByUsername("thor", realm); + // thor should be evicted because we put it 2 hours in the future + if (thor2 instanceof CachedUserModel) { + Assert.assertTrue(((CachedUserModel)thor2).getCacheTimestamp() > lastTimestamp); + } model.getConfig().remove("cachePolicy"); model.getConfig().remove("evictionHour"); model.getConfig().remove("evictionMinute"); @@ -233,24 +275,44 @@ public class UserStorageTest { KeycloakSession session = keycloakRule.startSession(); RealmModel realm = session.realms().getRealmByName("test"); - CachedUserModel thor = (CachedUserModel)session.users().getUserByUsername("thor", realm); realm.updateComponent(model); keycloakRule.stopSession(session, true); - Time.setOffset(60 * 5); // 5 minutes in future, should be cached still + session = keycloakRule.startSession(); + realm = session.realms().getRealmByName("test"); + CachedUserModel thor = (CachedUserModel)session.users().getUserByUsername("thor", realm); + long lastTimestamp = thor.getCacheTimestamp(); + keycloakRule.stopSession(session, true); + session = keycloakRule.startSession(); realm = session.realms().getRealmByName("test"); - // test still - UserModel thor2 = session.users().getUserByUsername("thor", realm); - Assert.assertTrue(thor2 instanceof CachedUserModel); + thor = (CachedUserModel)session.users().getUserByUsername("thor", realm); + lastTimestamp = thor.getCacheTimestamp(); keycloakRule.stopSession(session, true); + + + Time.setOffset(60 * 5); // 5 minutes in future, should be cached still + session = keycloakRule.startSession(); + realm = session.realms().getRealmByName("test"); + thor = (CachedUserModel)session.users().getUserByUsername("thor", realm); + Assert.assertEquals(thor.getCacheTimestamp(), lastTimestamp); + lastTimestamp = thor.getCacheTimestamp(); + keycloakRule.stopSession(session, true); + Time.setOffset(60 * 20); // 20 minutes into future, cache will be invalidated session = keycloakRule.startSession(); realm = session.realms().getRealmByName("test"); - thor2 = session.users().getUserByUsername("thor", realm); - Assert.assertFalse(thor2 instanceof CachedUserModel); + UserModel thor2 = session.users().getUserByUsername("thor", realm); + // thor should be evicted because we put it 2 hours in the future + if (thor2 instanceof CachedUserModel) { + Assert.assertTrue(((CachedUserModel)thor2).getCacheTimestamp() > lastTimestamp); + } + keycloakRule.stopSession(session, true); + + session = keycloakRule.startSession(); + realm = session.realms().getRealmByName("test"); model.getConfig().remove("cachePolicy"); model.getConfig().remove("maxLifespan"); realm.updateComponent(model); diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserConsentModelTest.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserConsentModelTest.java index 9fe49f21da..ced16a0791 100644 --- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserConsentModelTest.java +++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserConsentModelTest.java @@ -20,6 +20,7 @@ package org.keycloak.testsuite.model; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.keycloak.component.ComponentModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ModelException; import org.keycloak.models.ProtocolMapperModel; @@ -30,6 +31,8 @@ import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserModel; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.mappers.UserPropertyMapper; +import org.keycloak.storage.client.ClientStorageProviderModel; +import org.keycloak.testsuite.federation.HardcodedClientStorageProviderFactory; import java.util.List; @@ -38,6 +41,8 @@ import java.util.List; */ public class UserConsentModelTest extends AbstractModelTest { + private ComponentModel clientStorageComponent; + @Before public void setupEnv() { RealmModel realm = realmManager.createRealm("original"); @@ -87,6 +92,22 @@ public class UserConsentModelTest extends AbstractModelTest { maryFooGrant.addGrantedProtocolMapper(fooMapper); realmManager.getSession().users().addConsent(realm, mary.getId(), maryFooGrant); + ClientStorageProviderModel clientStorage = new ClientStorageProviderModel(); + clientStorage.setProviderId(HardcodedClientStorageProviderFactory.PROVIDER_ID); + clientStorage.getConfig().putSingle(HardcodedClientStorageProviderFactory.CLIENT_ID, "hardcoded-client"); + clientStorage.getConfig().putSingle(HardcodedClientStorageProviderFactory.REDIRECT_URI, "http://localhost:8081/*"); + clientStorage.getConfig().putSingle(HardcodedClientStorageProviderFactory.CONSENT, "true"); + clientStorage.setParentId(realm.getId()); + clientStorageComponent = realm.addComponentModel(clientStorage); + + ClientModel hardcodedClient = session.realms().getClientByClientId("hardcoded-client", realm); + + Assert.assertNotNull(hardcodedClient); + + UserConsentModel maryHardcodedGrant = new UserConsentModel(hardcodedClient); + realmManager.getSession().users().addConsent(realm, mary.getId(), maryHardcodedGrant); + + commit(); } @@ -125,7 +146,15 @@ public class UserConsentModelTest extends AbstractModelTest { Assert.assertNotNull("Created Date should be set", maryConsent.getCreatedDate()); Assert.assertNotNull("Last Updated Date should be set", maryConsent.getLastUpdatedDate()); + ClientModel hardcodedClient = session.realms().getClientByClientId("hardcoded-client", realm); + UserConsentModel maryHardcodedConsent = realmManager.getSession().users().getConsentByClient(realm, mary.getId(), hardcodedClient.getId()); + Assert.assertEquals(maryHardcodedConsent.getGrantedRoles().size(), 0); + Assert.assertEquals(maryHardcodedConsent.getGrantedProtocolMappers().size(), 0); + Assert.assertNotNull("Created Date should be set", maryHardcodedConsent.getCreatedDate()); + Assert.assertNotNull("Last Updated Date should be set", maryHardcodedConsent.getLastUpdatedDate()); + Assert.assertNull(realmManager.getSession().users().getConsentByClient(realm, mary.getId(), barClient.getId())); + Assert.assertNull(realmManager.getSession().users().getConsentByClient(realm, john.getId(), hardcodedClient.getId())); } @Test @@ -139,14 +168,26 @@ public class UserConsentModelTest extends AbstractModelTest { List johnConsents = realmManager.getSession().users().getConsents(realm, john.getId()); Assert.assertEquals(2, johnConsents.size()); + ClientModel hardcodedClient = session.realms().getClientByClientId("hardcoded-client", realm); + List maryConsents = realmManager.getSession().users().getConsents(realm, mary.getId()); - Assert.assertEquals(1, maryConsents.size()); + Assert.assertEquals(2, maryConsents.size()); UserConsentModel maryConsent = maryConsents.get(0); + UserConsentModel maryHardcodedConsent = maryConsents.get(1); + if (maryConsents.get(0).getClient().getId().equals(hardcodedClient.getId())) { + maryConsent = maryConsents.get(1); + maryHardcodedConsent = maryConsents.get(0); + + } Assert.assertEquals(maryConsent.getClient().getId(), fooClient.getId()); Assert.assertEquals(maryConsent.getGrantedRoles().size(), 1); Assert.assertEquals(maryConsent.getGrantedProtocolMappers().size(), 1); Assert.assertTrue(isRoleGranted(realm, "realm-role", maryConsent)); Assert.assertTrue(isMapperGranted(fooClient, "foo", maryConsent)); + + Assert.assertEquals(maryHardcodedConsent.getClient().getId(), hardcodedClient.getId()); + Assert.assertEquals(maryHardcodedConsent.getGrantedRoles().size(), 0); + Assert.assertEquals(maryHardcodedConsent.getGrantedProtocolMappers().size(), 0); } @Test @@ -190,14 +231,19 @@ public class UserConsentModelTest extends AbstractModelTest { RealmModel realm = realmManager.getRealm("original"); ClientModel fooClient = realm.getClientByClientId("foo-client"); UserModel john = session.users().getUserByUsername("john", realm); + UserModel mary = session.users().getUserByUsername("mary", realm); realmManager.getSession().users().revokeConsentForClient(realm, john.getId(), fooClient.getId()); + ClientModel hardcodedClient = session.realms().getClientByClientId("hardcoded-client", realm); + realmManager.getSession().users().revokeConsentForClient(realm, mary.getId(), hardcodedClient.getId()); commit(); realm = realmManager.getRealm("original"); john = session.users().getUserByUsername("john", realm); Assert.assertNull(realmManager.getSession().users().getConsentByClient(realm, john.getId(), fooClient.getId())); + mary = session.users().getUserByUsername("mary", realm); + Assert.assertNull(realmManager.getSession().users().getConsentByClient(realm, mary.getId(), hardcodedClient.getId())); } @Test @@ -206,6 +252,8 @@ public class UserConsentModelTest extends AbstractModelTest { RealmModel realm = realmManager.getRealm("original"); UserModel john = session.users().getUserByUsername("john", realm); session.users().removeUser(realm, john); + UserModel mary = session.users().getUserByUsername("mary", realm); + session.users().removeUser(realm, mary); } @Test @@ -270,6 +318,24 @@ public class UserConsentModelTest extends AbstractModelTest { Assert.assertNull(realmManager.getSession().users().getConsentByClient(realm, john.getId(), barClient.getId())); } + @Test + public void deleteClientStorageTest() { + RealmModel realm = realmManager.getRealm("original"); + realm.removeComponent(clientStorageComponent); + commit(); + + + + realm = realmManager.getRealm("original"); + ClientModel hardcodedClient = session.realms().getClientByClientId("hardcoded-client", realm); + Assert.assertNull(hardcodedClient); + + UserModel mary = session.users().getUserByUsername("mary", realm); + + List maryConsents = realmManager.getSession().users().getConsents(realm, mary.getId()); + Assert.assertEquals(1, maryConsents.size()); + } + private boolean isRoleGranted(RoleContainerModel roleContainer, String roleName, UserConsentModel consentModel) { RoleModel role = roleContainer.getRole(roleName); return consentModel.isRoleGranted(role); diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserConsentWithUserStorageModelTest.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserConsentWithUserStorageModelTest.java index 6fd18bf946..04b5cdeb4c 100644 --- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserConsentWithUserStorageModelTest.java +++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserConsentWithUserStorageModelTest.java @@ -20,6 +20,7 @@ package org.keycloak.testsuite.model; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.keycloak.component.ComponentModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ModelException; import org.keycloak.models.ProtocolMapperModel; @@ -31,6 +32,8 @@ import org.keycloak.models.UserModel; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.mappers.UserPropertyMapper; import org.keycloak.storage.UserStorageProviderModel; +import org.keycloak.storage.client.ClientStorageProviderModel; +import org.keycloak.testsuite.federation.HardcodedClientStorageProviderFactory; import org.keycloak.testsuite.federation.storage.UserMapStorageFactory; import org.keycloak.testsuite.federation.storage.UserPropertyFileStorageFactory; @@ -41,6 +44,8 @@ import java.util.List; */ public class UserConsentWithUserStorageModelTest extends AbstractModelTest { + private ComponentModel clientStorageComponent; + @Before public void setupEnv() { RealmModel realm = realmManager.createRealm("original"); @@ -97,6 +102,22 @@ public class UserConsentWithUserStorageModelTest extends AbstractModelTest { maryFooGrant.addGrantedProtocolMapper(fooMapper); realmManager.getSession().users().addConsent(realm, mary.getId(), maryFooGrant); + ClientStorageProviderModel clientStorage = new ClientStorageProviderModel(); + clientStorage.setProviderId(HardcodedClientStorageProviderFactory.PROVIDER_ID); + clientStorage.getConfig().putSingle(HardcodedClientStorageProviderFactory.CLIENT_ID, "hardcoded-client"); + clientStorage.getConfig().putSingle(HardcodedClientStorageProviderFactory.REDIRECT_URI, "http://localhost:8081/*"); + clientStorage.getConfig().putSingle(HardcodedClientStorageProviderFactory.CONSENT, "true"); + clientStorage.setParentId(realm.getId()); + clientStorageComponent = realm.addComponentModel(clientStorage); + + ClientModel hardcodedClient = session.realms().getClientByClientId("hardcoded-client", realm); + + Assert.assertNotNull(hardcodedClient); + + UserConsentModel maryHardcodedGrant = new UserConsentModel(hardcodedClient); + realmManager.getSession().users().addConsent(realm, mary.getId(), maryHardcodedGrant); + + commit(); } @@ -135,7 +156,15 @@ public class UserConsentWithUserStorageModelTest extends AbstractModelTest { Assert.assertNotNull("Created Date should be set", maryConsent.getCreatedDate()); Assert.assertNotNull("Last Updated Date should be set", maryConsent.getLastUpdatedDate()); + ClientModel hardcodedClient = session.realms().getClientByClientId("hardcoded-client", realm); + UserConsentModel maryHardcodedConsent = realmManager.getSession().users().getConsentByClient(realm, mary.getId(), hardcodedClient.getId()); + Assert.assertEquals(maryHardcodedConsent.getGrantedRoles().size(), 0); + Assert.assertEquals(maryHardcodedConsent.getGrantedProtocolMappers().size(), 0); + Assert.assertNotNull("Created Date should be set", maryHardcodedConsent.getCreatedDate()); + Assert.assertNotNull("Last Updated Date should be set", maryHardcodedConsent.getLastUpdatedDate()); + Assert.assertNull(realmManager.getSession().users().getConsentByClient(realm, mary.getId(), barClient.getId())); + Assert.assertNull(realmManager.getSession().users().getConsentByClient(realm, john.getId(), hardcodedClient.getId())); } @Test @@ -149,14 +178,26 @@ public class UserConsentWithUserStorageModelTest extends AbstractModelTest { List johnConsents = realmManager.getSession().users().getConsents(realm, john.getId()); Assert.assertEquals(2, johnConsents.size()); + ClientModel hardcodedClient = session.realms().getClientByClientId("hardcoded-client", realm); + List maryConsents = realmManager.getSession().users().getConsents(realm, mary.getId()); - Assert.assertEquals(1, maryConsents.size()); + Assert.assertEquals(2, maryConsents.size()); UserConsentModel maryConsent = maryConsents.get(0); + UserConsentModel maryHardcodedConsent = maryConsents.get(1); + if (maryConsents.get(0).getClient().getId().equals(hardcodedClient.getId())) { + maryConsent = maryConsents.get(1); + maryHardcodedConsent = maryConsents.get(0); + + } Assert.assertEquals(maryConsent.getClient().getId(), fooClient.getId()); Assert.assertEquals(maryConsent.getGrantedRoles().size(), 1); Assert.assertEquals(maryConsent.getGrantedProtocolMappers().size(), 1); Assert.assertTrue(isRoleGranted(realm, "realm-role", maryConsent)); Assert.assertTrue(isMapperGranted(fooClient, "foo", maryConsent)); + + Assert.assertEquals(maryHardcodedConsent.getClient().getId(), hardcodedClient.getId()); + Assert.assertEquals(maryHardcodedConsent.getGrantedRoles().size(), 0); + Assert.assertEquals(maryHardcodedConsent.getGrantedProtocolMappers().size(), 0); } @Test @@ -200,14 +241,19 @@ public class UserConsentWithUserStorageModelTest extends AbstractModelTest { RealmModel realm = realmManager.getRealm("original"); ClientModel fooClient = realm.getClientByClientId("foo-client"); UserModel john = session.users().getUserByUsername("john", realm); + UserModel mary = session.users().getUserByUsername("mary", realm); realmManager.getSession().users().revokeConsentForClient(realm, john.getId(), fooClient.getId()); + ClientModel hardcodedClient = session.realms().getClientByClientId("hardcoded-client", realm); + realmManager.getSession().users().revokeConsentForClient(realm, mary.getId(), hardcodedClient.getId()); commit(); realm = realmManager.getRealm("original"); john = session.users().getUserByUsername("john", realm); Assert.assertNull(realmManager.getSession().users().getConsentByClient(realm, john.getId(), fooClient.getId())); + mary = session.users().getUserByUsername("mary", realm); + Assert.assertNull(realmManager.getSession().users().getConsentByClient(realm, mary.getId(), hardcodedClient.getId())); } @Test @@ -216,6 +262,8 @@ public class UserConsentWithUserStorageModelTest extends AbstractModelTest { RealmModel realm = realmManager.getRealm("original"); UserModel john = session.users().getUserByUsername("john", realm); session.users().removeUser(realm, john); + UserModel mary = session.users().getUserByUsername("mary", realm); + session.users().removeUser(realm, mary); } @Test @@ -280,6 +328,24 @@ public class UserConsentWithUserStorageModelTest extends AbstractModelTest { Assert.assertNull(realmManager.getSession().users().getConsentByClient(realm, john.getId(), barClient.getId())); } + @Test + public void deleteClientStorageTest() { + RealmModel realm = realmManager.getRealm("original"); + realm.removeComponent(clientStorageComponent); + commit(); + + + + realm = realmManager.getRealm("original"); + ClientModel hardcodedClient = session.realms().getClientByClientId("hardcoded-client", realm); + Assert.assertNull(hardcodedClient); + + UserModel mary = session.users().getUserByUsername("mary", realm); + + List maryConsents = realmManager.getSession().users().getConsents(realm, mary.getId()); + Assert.assertEquals(1, maryConsents.size()); + } + private boolean isRoleGranted(RoleContainerModel roleContainer, String roleName, UserConsentModel consentModel) { RoleModel role = roleContainer.getRole(roleName); return consentModel.isRoleGranted(role); diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index 3595dd982a..f8ff2d0726 100644 --- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -173,6 +173,7 @@ realm-sessions=Realm Sessions revocation=Revocation logout-all=Logout all active-sessions=Active Sessions +offline-sessions=Offline Sessions sessions=Sessions not-before=Not Before not-before.tooltip=Revoke any tokens issued before this date. @@ -1343,6 +1344,8 @@ userStorage.cachePolicy.maxLifespan.tooltip=Max lifespan of a user cache entry i user-origin-link=Storage Origin user-origin.tooltip=UserStorageProvider the user was loaded from user-link.tooltip=UserStorageProvider this locally stored user was imported from. +client-origin-link=Storage Origin +client-origin.tooltip=Provider the client was loaded from disable=Disable disableable-credential-types=Disableable Types diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js index 2e88f02596..16252184ad 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js @@ -764,6 +764,15 @@ module.controller('ClientListCtrl', function($scope, realm, Client, serverInfo, }); }; + $scope.searchClient = function() { + console.log('searchQuery!!! ' + $scope.search.clientId); + Client.query({realm: realm.realm, viewableOnly: true, clientId: $scope.search.clientId}).$promise.then(function(clients) { + $scope.numberOfPages = Math.ceil(clients.length/$scope.pageSize); + $scope.clients = clients; + }); + + }; + $scope.exportClient = function(client) { var clientCopy = angular.copy(client); delete clientCopy.id; @@ -819,7 +828,7 @@ module.controller('ClientInstallationCtrl', function($scope, realm, client, serv } }); -module.controller('ClientDetailCtrl', function($scope, realm, client, templates, $route, serverInfo, Client, ClientDescriptionConverter, $location, $modal, Dialog, Notifications) { +module.controller('ClientDetailCtrl', function($scope, realm, client, templates, $route, serverInfo, Client, ClientDescriptionConverter, Components, ClientStorageOperations, $location, $modal, Dialog, Notifications) { @@ -889,6 +898,25 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates, $scope.disableServiceAccountRolesTab = !client.serviceAccountsEnabled; $scope.disableCredentialsTab = client.publicClient; + if(client.origin) { + if ($scope.access.viewRealm) { + Components.get({realm: realm.realm, componentId: client.origin}, function (link) { + $scope.originName = link.name; + //$scope.originLink = "#/realms/" + realm.realm + "/user-storage/providers/" + link.providerId + "/" + link.id; + }) + } + else { + // KEYCLOAK-4328 + ClientStorageOperations.simpleName.get({realm: realm.realm, componentId: client.origin}, function (link) { + $scope.originName = link.name; + //$scope.originLink = $location.absUrl(); + }) + } + } else { + console.log("origin is null"); + } + + function updateProperties() { if (!$scope.client.attributes) { $scope.client.attributes = {}; diff --git a/themes/src/main/resources/theme/base/admin/resources/js/services.js b/themes/src/main/resources/theme/base/admin/resources/js/services.js index b1b6304f42..570079267e 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/services.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/services.js @@ -1813,6 +1813,16 @@ module.factory('UserStorageOperations', function($resource) { }); +module.factory('ClientStorageOperations', function($resource) { + var object = {} + object.simpleName = $resource(authUrl + '/admin/realms/:realm/client-storage/:componentId/name', { + realm : '@realm', + componentId : '@componentId' + }); + return object; +}); + + module.factory('ClientRegistrationPolicyProviders', function($resource) { return $resource(authUrl + '/admin/realms/:realm/client-registration-policy/providers', { realm : '@realm', diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html index c00601aa65..cbeb321a94 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html @@ -37,6 +37,13 @@ {{:: 'client.enabled.tooltip' | translate}} +
+ +
+ {{originName}} +
+ {{:: 'client-origin.tooltip' | translate}} +
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-list.html index 03ebf5cc39..744e3c484e 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/client-list.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-list.html @@ -11,9 +11,9 @@
- +
- +
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/session-realm.html b/themes/src/main/resources/theme/base/admin/resources/partials/session-realm.html index e724d952d1..98b228427d 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/session-realm.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/session-realm.html @@ -18,12 +18,14 @@ {{:: 'client' | translate}} {{:: 'active-sessions' | translate}} + {{:: 'offline-sessions' | translate}} {{data.clientId}} {{data.active}} + {{data.offline}} diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html index 40aab54633..c29469291b 100755 --- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html +++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html @@ -9,11 +9,11 @@