keycloak-aplcache
Changes
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java 5(+5 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java 4(+2 -2)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/AbstractRevisioned.java 4(+3 -1)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java 248(+162 -86)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java 101(+10 -91)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java 36(+31 -5)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java 25(+25 -0)
model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/InitializerStateTest.java 11(+6 -5)
model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientIdentityProviderMappingEntity.java 141(+0 -141)
model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentProtocolMapperEntity.java 4(+3 -1)
model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java 45(+40 -5)
model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java 46(+45 -1)
model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentProtocolMapperEntity.java 2(+2 -0)
model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentRoleEntity.java 2(+2 -0)
server-spi-private/src/main/java/org/keycloak/storage/client/AbstractClientStorageAdapter.java 139(+139 -0)
server-spi-private/src/main/java/org/keycloak/storage/client/AbstractReadOnlyClientStorageAdapter.java 280(+280 -0)
server-spi-private/src/main/java/org/keycloak/storage/client/ClientStorageProviderFactory.java 118(+118 -0)
server-spi-private/src/main/java/org/keycloak/storage/client/ClientStorageProviderSpi.java 83(+83 -0)
server-spi-private/src/main/java/org/keycloak/storage/client/UnsupportedOperationsClientStorageAdapter.java 82(+82 -0)
services/src/main/java/org/keycloak/services/resources/admin/ClientStorageProviderResource.java 111(+111 -0)
services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java 5(+3 -2)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProvider.java 282(+282 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProviderFactory.java 81(+81 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.storage.client.ClientStorageProviderFactory 1(+1 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/ClientStorageTest.java 481(+481 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/MapCollectTest.java 110(+110 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java 2(+1 -1)
testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java 88(+75 -13)
testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserConsentModelTest.java 68(+67 -1)
Details
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 77c2c45..83c4f8a 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<String, Boolean> access;
+ protected String origin;
public String getId() {
@@ -384,4 +385,19 @@ public class ClientRepresentation {
public void setAccess(Map<String, Boolean> 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/authorization/StoreFactoryCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java
index ab73c94..cb5d060 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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -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/ClientAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
index a5823ce..4c28ad6 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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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,12 +65,17 @@ 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 a521ef2..4d2ce42 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/entities/AbstractRevisioned.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/AbstractRevisioned.java
index 22fef96..8c88df7 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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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/cache/infinispan/GroupAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java
index 21bcc66..0e69c68 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 bfa00e0..dd62377 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/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
index 83bafb1..018213f 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<String, RealmAdapter> managedRealms = new HashMap<>();
- protected Map<String, ClientAdapter> managedApplications = new HashMap<>();
+ protected Map<String, ClientModel> managedApplications = new HashMap<>();
protected Map<String, ClientTemplateAdapter> managedClientTemplates = new HashMap<>();
protected Map<String, RoleAdapter> managedRoles = new HashMap<>();
protected Map<String, GroupAdapter> 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<RealmModel> getRealms() {
// Retrieve realms from backend
- List<RealmModel> backendRealms = getDelegate().getRealms();
+ List<RealmModel> backendRealms = getRealmDelegate().getRealms();
// Return cache delegates to ensure cache invalidated during write operations
List<RealmModel> cachedRealms = new LinkedList<RealmModel>();
@@ -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<ClientModel> model = getDelegate().getClients(realm);
+ List<ClientModel> model = getClientDelegate().getClients(realm);
if (model == null) return null;
Set<String> 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<RoleModel> model = getDelegate().getRealmRoles(realm);
+ Set<RoleModel> model = getRealmDelegate().getRealmRoles(realm);
if (model == null) return null;
Set<String> 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<RoleModel> model = getDelegate().getClientRoles(realm, client);
+ Set<RoleModel> model = getRealmDelegate().getClientRoles(realm, client);
if (model == null) return null;
Set<String> 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<GroupModel> model = getDelegate().getGroups(realm);
+ List<GroupModel> model = getRealmDelegate().getGroups(realm);
if (model == null) return null;
Set<String> 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<GroupModel> model = getDelegate().getTopLevelGroups(realm);
+ List<GroupModel> model = getRealmDelegate().getTopLevelGroups(realm);
if (model == null) return null;
Set<String> 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<GroupModel> model = getDelegate().getTopLevelGroups(realm, first, max);
+ List<GroupModel> model = getRealmDelegate().getTopLevelGroups(realm, first, max);
if (model == null) return null;
Set<String> 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<GroupModel> 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<ClientInitialAccessModel> 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 00e41f3..24ed6d9 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 4b5309a..be3d0ad 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/sessions/infinispan/InfinispanUserSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
index 4cfea1d..c0b1aa0 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<UserSessionModel> 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<UserSessionModel> getUserSessionModels(RealmModel realm, int firstResult, int maxResults, boolean offline, UserSessionPredicate predicate) {
Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = getCache(offline);
cache = CacheDecorators.skipCacheLoaders(cache);
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionCache = getClientSessionCache(offline);
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionCacheDecorated = CacheDecorators.skipCacheLoaders(clientSessionCache);
- final String clientUuid = client.getId();
-
Stream<UserSessionEntity> 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<UserSessionModel> 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<String, Long> getActiveClientSessionStats(RealmModel realm, boolean offline) {
+ Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = getCache(offline);
+ cache = CacheDecorators.skipCacheLoaders(cache);
+ return cache.entrySet().stream()
+ .filter(UserSessionPredicate.create(realm.getId()))
+ .map(Mappers.authClientSessionSetMapper())
+ .flatMap((Serializable & Function<Set<String>, Stream<? extends String>>)Mappers::toStream)
+ .collect(
+ CacheCollectors.serializableCollector(
+ () -> Collectors.groupingBy(Function.identity(), Collectors.counting())
+ )
+ );
+ }
+
+ protected long getUserSessionsCount(RealmModel realm, ClientModel client, boolean offline) {
Cache<String, SessionEntityWrapper<UserSessionEntity>> 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 177fd23..50df448 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -125,4 +132,22 @@ public class Mappers {
}
}
+ private static class AuthClientSessionSetMapper implements Function<Map.Entry<String, SessionEntityWrapper<UserSessionEntity>>, Set<String>>, Serializable {
+
+ @Override
+ public Set<String> apply(Map.Entry<String, SessionEntityWrapper<UserSessionEntity>> entry) {
+ UserSessionEntity entity = entry.getValue().getEntity();
+ return entity.getAuthenticatedClientSessions().keySet();
+ }
+ }
+
+ public static <T> Stream<T> toStream(Collection<T> collection) {
+ return collection.stream();
+ }
+
+ public static Function<Map.Entry<String, SessionEntityWrapper<UserSessionEntity>>, Set<String>> 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 32210a0..d26f2b9 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 207d4ab..5e79bad 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/entities/ClientEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java
index 7f88977..7ede55c 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<String, String> authFlowBindings = new HashMap<String, String>();
- @OneToMany(fetch = FetchType.LAZY, mappedBy = "client", cascade = CascadeType.REMOVE)
- Collection<ClientIdentityProviderMappingEntity> identityProviders = new ArrayList<ClientIdentityProviderMappingEntity>();
-
@OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "client")
Collection<ProtocolMapperEntity> protocolMappers = new ArrayList<ProtocolMapperEntity>();
@@ -322,14 +319,6 @@ public class ClientEntity {
this.frontchannelLogout = frontchannelLogout;
}
- public Collection<ClientIdentityProviderMappingEntity> getIdentityProviders() {
- return this.identityProviders;
- }
-
- public void setIdentityProviders(Collection<ClientIdentityProviderMappingEntity> identityProviders) {
- this.identityProviders = identityProviders;
- }
-
public Collection<ProtocolMapperEntity> getProtocolMappers() {
return protocolMappers;
}
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 a29ab69..9772810 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<UserConsentRoleEntity> grantedRoles = new ArrayList<UserConsentRoleEntity>();
@@ -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<UserConsentRoleEntity> 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 4c0dd5d..85df759 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 95d5f3e..c4818c7 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/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
index 08f3164..8eb8102 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<UserConsentEntity> query = em.createNamedQuery("userConsentByUserAndClient", UserConsentEntity.class);
+ StorageId clientStorageId = new StorageId(clientId);
+ String queryName = clientStorageId.isLocal() ? "userConsentByUserAndClient" : "userConsentByUserAndExternalClient";
+ TypedQuery<UserConsentEntity> 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<UserConsentEntity> 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/session/JpaUserSessionPersisterProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java
index a4c02de..587ed7e 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 3ae17b2..44c3188 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/entity/FederatedUserConsentEntity.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentEntity.java
index 225e80b..c2eac0b 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<FederatedUserConsentRoleEntity> 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 f7da2cc..a9de2c6 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 d74865d..0e0551d 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/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java
index f6de431..7474155 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<FederatedUserConsentEntity> query = em.createNamedQuery("userFederatedConsentByUserAndClient", FederatedUserConsentEntity.class);
+ StorageId clientStorageId = new StorageId(clientId);
+ String queryName = clientStorageId.isLocal() ? "userFederatedConsentByUserAndClient" : "userFederatedConsentByUserAndExternalClient";
+ TypedQuery<FederatedUserConsentEntity> 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<FederatedUserConsentEntity> 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();
+ } 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();
- 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();
+ }
}
}
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 f7ebc68..2987163 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 @@
</createTable>
<addPrimaryKey columnNames="CLIENT_ID, BINDING_NAME" constraintName="C_CLI_FLOW_BIND" tableName="CLIENT_AUTH_FLOW_BINDINGS"/>
</changeSet>
+ <changeSet author="bburke@redhat.com" id="4.0.0-CLEANUP-UNUSED-TABLE">
+ <dropIndex tableName="CLIENT_IDENTITY_PROV_MAPPING" indexName="IDX_CLIENT_ID_PROV_MAP_CLIENT"/>
+ <dropPrimaryKey tableName="CLIENT_IDENTITY_PROV_MAPPING" constraintName="CONSTR_CLIENT_IDEN_PROV_MAP"/>
+ <dropUniqueConstraint tableName="CLIENT_IDENTITY_PROV_MAPPING" constraintName="UK_7CAELWNIBJI49AVXSRTUF6XJ12"/>
+ <dropTable tableName="CLIENT_IDENTITY_PROV_MAPPING"/>
+ </changeSet>
+ <changeSet author="bburke@redhat.com" id="4.0.0-KEYCLOAK-6228">
+ <!-- Modifying some columns so that CLIENT_ID is 255. Drop foreign key constraints too that referenced CLIENT tablename.
+ This is needed for client storage SPI but only needed for tables that might reference a federated client -->
+
+ <!-- Modify USER_CONSENT -->
+ <dropUniqueConstraint constraintName="UK_JKUWUVD56ONTGSUHOGM8UEWRT" tableName="USER_CONSENT"/>
+ <dropNotNullConstraint tableName="USER_CONSENT" columnName="CLIENT_ID" columnDataType="VARCHAR(36)"/>
+ <addColumn tableName="USER_CONSENT">
+ <column name="CLIENT_STORAGE_PROVIDER" type="VARCHAR(36)">
+ <constraints nullable="true"/>
+ </column>
+ <column name="EXTERNAL_CLIENT_ID" type="VARCHAR(255)">
+ <constraints nullable="true"/>
+ </column>
+ </addColumn>
+ <addUniqueConstraint columnNames="CLIENT_ID, CLIENT_STORAGE_PROVIDER, EXTERNAL_CLIENT_ID, USER_ID" constraintName="UK_JKUWUVD56ONTGSUHOGM8UEWRT" tableName="USER_CONSENT"/>
+
+ <!-- FED_USER_CONSENT -->
+ <addColumn tableName="FED_USER_CONSENT">
+ <column name="CLIENT_STORAGE_PROVIDER" type="VARCHAR(36)">
+ <constraints nullable="true"/>
+ </column>
+ <column name="EXTERNAL_CLIENT_ID" type="VARCHAR(255)">
+ <constraints nullable="true"/>
+ </column>
+ </addColumn>
+ <dropNotNullConstraint tableName="FED_USER_CONSENT" columnName="CLIENT_ID" columnDataType="VARCHAR(36)"/>
+ <createIndex tableName="FED_USER_CONSENT" indexName="IDX_FU_CNSNT_EXT">
+ <column name="USER_ID" type="VARCHAR(255)" />
+ <column name="CLIENT_STORAGE_PROVIDER" type="VARCHAR(36)" />
+ <column name="EXTERNAL_CLIENT_ID" type="VARCHAR(255)" />
+ </createIndex>
+
+ <!-- Modify OFFLINE_CLIENT_SESSION -->
+ <addColumn tableName="OFFLINE_CLIENT_SESSION">
+ <column name="CLIENT_STORAGE_PROVIDER" type="VARCHAR(36)" defaultValue="local">
+ <constraints nullable="false"/>
+ </column>
+ <column name="EXTERNAL_CLIENT_ID" type="VARCHAR(255)" defaultValue="local">
+ <constraints nullable="false"/>
+ </column>
+ </addColumn>
+ <update tableName="OFFLINE_CLIENT_SESSION">
+ <column name="CLIENT_STORAGE_PROVIDER" value="local"/>
+ </update>
+ <update tableName="OFFLINE_CLIENT_SESSION">
+ <column name="EXTERNAL_CLIENT_ID" value="local"/>
+ </update>
+ <dropPrimaryKey tableName="OFFLINE_CLIENT_SESSION" constraintName="CONSTRAINT_OFFL_CL_SES_PK3"/>
+ <addPrimaryKey columnNames="USER_SESSION_ID,CLIENT_ID, CLIENT_STORAGE_PROVIDER, EXTERNAL_CLIENT_ID, OFFLINE_FLAG" constraintName="CONSTRAINT_OFFL_CL_SES_PK3" tableName="OFFLINE_CLIENT_SESSION"/>
+
+ </changeSet>
</databaseChangeLog>
diff --git a/model/jpa/src/main/resources/META-INF/persistence.xml b/model/jpa/src/main/resources/META-INF/persistence.xml
index f23198c..36e3fb4 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 @@
<class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
<class>org.keycloak.models.jpa.entities.IdentityProviderEntity</class>
<class>org.keycloak.models.jpa.entities.IdentityProviderMapperEntity</class>
- <class>org.keycloak.models.jpa.entities.ClientIdentityProviderMappingEntity</class>
<class>org.keycloak.models.jpa.entities.ProtocolMapperEntity</class>
<class>org.keycloak.models.jpa.entities.UserConsentEntity</class>
<class>org.keycloak.models.jpa.entities.UserConsentRoleEntity</class>
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 0000000..29dc415
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface ClientProvider extends ClientLookupProvider, Provider {
+ List<ClientModel> 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<RoleModel> 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 c239fb2..72a0d4c 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 6d48425..5eb18db 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<ClientStorageProviderModel> getClientStorageProviders() {
+ List<ClientStorageProviderModel> 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 d14f2d6..6fed88a 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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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<ClientModel> 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<RoleModel> getRealmRoles(RealmModel realm);
- RoleModel getClientRole(RealmModel realm, ClientModel client, String name);
-
- Set<RoleModel> 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 fc67d4e..ac1c224 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<String, Long> 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/storage/CacheableStorageProviderModel.java b/server-spi/src/main/java/org/keycloak/storage/CacheableStorageProviderModel.java
new file mode 100644
index 0000000..dd740f2
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @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/client/ClientLookupProvider.java b/server-spi/src/main/java/org/keycloak/storage/client/ClientLookupProvider.java
new file mode 100644
index 0000000..7f04e5f
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @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 0000000..c0773a6
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @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 0000000..54093af
--- /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 <a href="mailto:bburke@redhat.com">Bill Burke</a>
+ */
+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/server-spi/src/main/java/org/keycloak/storage/StorageId.java b/server-spi/src/main/java/org/keycloak/storage/StorageId.java
index fbbc406..3a2a141 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 1ec06a6..e145c40 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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
* @author <a href="mailto:bburke@redhat.com">Bill Burke</a>
*/
-public class UserStorageProviderModel 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 class UserStorageProviderModel extends CacheableStorageProviderModel {
+
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-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java b/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java
index 61ae1be..ce71dee 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 0dad16a..2e7d3ef 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 0000000..db2a9ad
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @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<String, Integer> 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 0000000..d8e6bd4
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @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<String> 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<String> 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 0000000..e9f8ee7
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface ClientStorageProviderFactory<T extends ClientStorageProvider> extends ComponentFactory<T, ClientStorageProvider> {
+
+
+ /**
+ * 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<ProviderConfigProperty> 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<ProviderConfigProperty> getCommonProviderConfigProperties() {
+ return ClientStorageProviderSpi.commonConfig();
+ }
+
+ @Override
+ default
+ Map<String, Object> getTypeMetadata() {
+ Map<String, Object> 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 0000000..bf1146d
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ClientStorageProviderSpi implements Spi {
+
+ @Override
+ public boolean isInternal() {
+ return true;
+ }
+
+ @Override
+ public String getName() {
+ return "client-storage";
+ }
+
+ @Override
+ public Class<? extends Provider> getProviderClass() {
+ return ClientStorageProvider.class;
+ }
+
+ @Override
+ public Class<? extends ProviderFactory> getProviderFactoryClass() {
+ return ClientStorageProviderFactory.class;
+ }
+
+ private static final List<ProviderConfigProperty> commonConfig;
+
+ static {
+ List<ProviderConfigProperty> 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<ProviderConfigProperty> 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 0000000..4de10a6
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @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<RoleModel> getRoles() {
+ return Collections.EMPTY_SET;
+ }
+
+ @Override
+ public final List<String> 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 b781ca3..a63b206 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/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java
index 0cc81c1..6cabf76 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<String, Object> attributes = new HashMap<>();
private RealmProvider model;
private UserStorageManager userStorageManager;
+ private ClientStorageManager clientStorageManager;
private UserCredentialStoreManager userCredentialStorageManager;
private UserSessionProvider sessionProvider;
private AuthenticationSessionProvider authenticationSessionProvider;
@@ -136,6 +139,23 @@ public class DefaultKeycloakSession implements KeycloakSession {
}
@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);
return userStorageManager;
@@ -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/ClientsResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java
index c0ea7df..580a0bc 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<ClientRepresentation> getClients(@QueryParam("clientId") String clientId, @QueryParam("viewableOnly") @DefaultValue("false") boolean viewableOnly) {
List<ClientRepresentation> rep = new ArrayList<>();
- if (clientId == null) {
+ if (clientId == null || clientId.trim().equals("")) {
List<ClientModel> clientModels = realm.getClients();
auth.clients().requireList();
boolean view = auth.clients().canView();
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 0000000..6c8561c
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @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<String, String> 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<String, String> 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/permissions/ClientPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java
index 1c5978e..00726e4 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<String, Boolean> getAccess(ClientModel client) {
Map<String, Boolean> 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/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index 88af6f6..3a82baa 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<Map<String, String>> getClientSessionStats() {
auth.realm().requireViewRealm();
- List<Map<String, String>> data = new LinkedList<Map<String, String>>();
- for (ClientModel client : realm.getClients()) {
- long size = session.sessions().getActiveUserSessions(client.getRealm(), client);
- if (size == 0) continue;
- Map<String, String> map = new HashMap<>();
- map.put("id", client.getId());
- map.put("clientId", client.getClientId());
- map.put("active", size + "");
- data.add(map);
- }
- return data;
+ Map<String, Map<String, String>> data = new HashMap();
+ {
+ Map<String, Long> activeCount =session.sessions().getActiveClientSessionStats(realm, false);
+ for (Map.Entry<String, Long> entry : activeCount.entrySet()) {
+ Map<String, String> 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);
+
+ }
+ }
+ {
+ Map<String, Long> offlineCount = session.sessions().getActiveClientSessionStats(realm, true);
+ for (Map.Entry<String, Long> entry : offlineCount.entrySet()) {
+ Map<String, String> 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<Map<String, String>> result = new LinkedList<>();
+ for (Map<String, String> item : data.values()) result.add(item);
+ return result;
}
/**
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 0000000..d60d2f4
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @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<ClientStorageProviderModel> 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 <T> List<T> getStorageProviders(KeycloakSession session, RealmModel realm, Class<T> type) {
+ List<T> 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 <T> List<T> getEnabledStorageProviders(KeycloakSession session, RealmModel realm, Class<T> type) {
+ List<T> 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<ClientModel> 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<RoleModel> 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 16d14e0..4801553 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 0000000..672cb1e
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @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<String> getWebOrigins() {
+ return Collections.EMPTY_SET;
+ }
+
+ @Override
+ public Set<String> getRedirectUris() {
+ HashSet<String> 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<String, String> getAttributes() {
+ return Collections.EMPTY_MAP;
+ }
+
+ @Override
+ public String getAuthenticationFlowBindingOverride(String binding) {
+ return null;
+ }
+
+ @Override
+ public Map<String, String> 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<ProtocolMapperModel> 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<RoleModel> getScopeMappings() {
+ RoleModel offlineAccess = realm.getRole("offline_access");
+ Set<RoleModel> set = new HashSet<>();
+ set.add(offlineAccess);
+ return set;
+ }
+
+ @Override
+ public Set<RoleModel> 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 0000000..67fcc0e
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class HardcodedClientStorageProviderFactory implements ClientStorageProviderFactory<HardcodedClientStorageProvider> {
+ @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<ProviderConfigProperty> 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<ProviderConfigProperty> 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 0000000..0ed6376
--- /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 0000000..e251702
--- /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 <a href="mailto:bburke@redhat.com">Bill Burke</a>
+ */
+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<Map<String, String>> list = adminClient.realm("test").getClientSessionStats();
+ boolean hardTested = false;
+ boolean testAppTested = false;
+ for (Map<String, String> 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 0000000..bf1e656
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class MapCollectTest {
+
+ public static class UserSessionObject {
+ public String id;
+ public String realm;
+ public Set<String> 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<UserSessionObject> {
+ 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<String> clients(UserSessionObject s) {
+ return s.clients;
+ }
+
+
+ @Test
+ public void testMe() throws Exception {
+
+ List<UserSessionObject> 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<String, Long> result = list.stream().collect(
+ Collectors.groupingBy(s -> s.realm, Collectors.summingLong(i -> 1)));
+
+ for (Map.Entry<String, Long> 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<String, Long> 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 c3a341b..138ddcd 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 48228a2..d446985 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);
+ 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);
+
+
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");
- // test still
UserModel thor2 = session.users().getUserByUsername("thor", realm);
- Assert.assertTrue(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);
+ }
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);
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 9fe49f2..ced16a0 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<UserConsentModel> johnConsents = realmManager.getSession().users().getConsents(realm, john.getId());
Assert.assertEquals(2, johnConsents.size());
+ ClientModel hardcodedClient = session.realms().getClientByClientId("hardcoded-client", realm);
+
List<UserConsentModel> 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<UserConsentModel> 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 6fd18bf..04b5cde 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<UserConsentModel> johnConsents = realmManager.getSession().users().getConsents(realm, john.getId());
Assert.assertEquals(2, johnConsents.size());
+ ClientModel hardcodedClient = session.realms().getClientByClientId("hardcoded-client", realm);
+
List<UserConsentModel> 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<UserConsentModel> 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 3595dd9..f8ff2d0 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 2e88f02..1625218 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 b1b6304..5700792 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 c00601a..cbeb321 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 @@
</div>
<kc-tooltip>{{:: 'client.enabled.tooltip' | translate}}</kc-tooltip>
</div>
+ <div class="form-group clearfix block" data-ng-show="client.origin">
+ <label class="col-md-2 control-label">{{:: 'client-origin-link' | translate}}</label>
+ <div class="col-md-6">
+ {{originName}}
+ </div>
+ <kc-tooltip>{{:: 'client-origin.tooltip' | translate}}</kc-tooltip>
+ </div>
<div class="form-group clearfix block" data-ng-show="protocol != 'docker-v2'">
<label class="col-md-2 control-label" for="consentRequired">{{:: 'consent-required' | translate}}</label>
<div class="col-sm-6">
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 03ebf5c..744e3c4 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 @@
<div class="form-inline">
<div class="form-group">
<div class="input-group">
- <input type="text" placeholder="{{:: 'search.placeholder' | translate}}" data-ng-model="search.clientId" class="form-control search" onkeyup="if(event.keyCode === 13){$(this).next('I').click();}">
+ <input type="text" placeholder="{{:: 'search.placeholder' | translate}}" data-ng-model="search.clientId" class="form-control search" onkeydown="if(event.keyCode === 13) document.getElementById('clientSearch').click()">
<div class="input-group-addon">
- <i class="fa fa-search" type="submit"></i>
+ <i class="fa fa-search" id="clientSearch" data-ng-click="searchClient()"></i>
</div>
</div>
</div>
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 e724d95..98b2284 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 @@
<tr>
<th>{{:: 'client' | translate}}</th>
<th>{{:: 'active-sessions' | translate}}</th>
+ <th>{{:: 'offline-sessions' | translate}}</th>
</tr>
</thead>
<tbody>
<tr data-ng-repeat="data in stats">
<td><a href="#/realms/{{realm.realm}}/clients/{{data.id}}/sessions">{{data.clientId}}</a></td>
<td>{{data.active}}</td>
+ <td>{{data.offline}}</td>
</tr>
</tbody>
</table>
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 40aab54..c294692 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 @@
<ul class="nav nav-tabs" data-ng-hide="create && !path[4]">
<li ng-class="{active: !path[4]}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{:: 'settings' | translate}}</a></li>
<li ng-class="{active: path[4] == 'credentials'}"
- data-ng-show="!client.publicClient && client.protocol == 'openid-connect'">
+ data-ng-show="!client.publicClient && client.protocol == 'openid-connect' && !client.origin">
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials">{{:: 'credentials' | translate}}</a>
</li>
<li ng-class="{active: path[4] == 'saml'}" data-ng-show="client.protocol == 'saml' && (client.attributes['saml.client.signature'] == 'true' || client.attributes['saml.encrypt'] == 'true')"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/saml/keys">{{:: 'saml-keys' | translate}}</a></li>
- <li ng-class="{active: path[4] == 'roles'}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/roles">{{:: 'roles' | translate}}</a></li>
+ <li ng-class="{active: path[4] == 'roles'}" data-ng-show="!client.origin"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/roles">{{:: 'roles' | translate}}</a></li>
<li ng-class="{active: path[4] == 'mappers'}" data-ng-show="!client.bearerOnly">
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/mappers">{{:: 'mappers' | translate}}</a>
<kc-tooltip>{{:: 'mappers.tooltip' | translate}}</kc-tooltip>
@@ -23,10 +23,10 @@
<kc-tooltip>{{:: 'scope.tooltip' | translate}}</kc-tooltip>
</li>
<li ng-class="{active: path[4] == 'authz'}"
- data-ng-show="serverInfo.featureEnabled('AUTHORIZATION') && !disableAuthorizationTab && client.authorizationServicesEnabled">
+ data-ng-show="serverInfo.featureEnabled('AUTHORIZATION') && !disableAuthorizationTab && client.authorizationServicesEnabled && !client.origin">
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">{{:: 'authz-authorization' |
translate}}</a></li>
- <li ng-class="{active: path[4] == 'revocation'}" data-ng-show="client.protocol != 'docker-v2' && client.protocol != 'saml'"><a
+ <li ng-class="{active: path[4] == 'revocation'}" data-ng-show="client.protocol != 'docker-v2' && client.protocol != 'saml' && !client.origin"><a
href="#/realms/{{realm.realm}}/clients/{{client.id}}/revocation">{{:: 'revocation' | translate}}</a>
</li>
<!-- <li ng-class="{active: path[4] == 'identity-provider'}" data-ng-show="realm.identityFederationEnabled"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/identity-provider">Identity Provider</a></li> -->
@@ -40,9 +40,9 @@
<kc-tooltip>{{:: 'offline-access.tooltip' | translate}}</kc-tooltip>
</li>
- <li ng-class="{active: path[4] == 'clustering'}" data-ng-show="!client.publicClient"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/clustering">{{:: 'clustering' | translate}}</a></li>
+ <li ng-class="{active: path[4] == 'clustering'}" data-ng-show="!client.publicClient && !client.origin"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/clustering">{{:: 'clustering' | translate}}</a></li>
- <li ng-class="{active: path[4] == 'installation'}">
+ <li ng-class="{active: path[4] == 'installation'}" data-ng-show="!client.origin">
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/installation">{{:: 'installation' | translate}}</a>
<kc-tooltip>{{:: 'installation.tooltip' | translate}}</kc-tooltip>
</li>