Merge pull request #4949 from patriot1burke/client-storage-spi

KEYCLOAK-6228
This commit is contained in:
Bill Burke 2018-02-01 08:59:02 -05:00 committed by GitHub
commit 8f09efab9d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
72 changed files with 3547 additions and 715 deletions

View file

@ -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;
}
}

View file

@ -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,11 +65,16 @@ public class ClientAdapter implements ClientModel {
protected boolean isUpdated() {
if (updated != null) return true;
if (!invalidated) return false;
updated = cacheSession.getDelegate().getClientById(cached.getId(), cachedRealm);
updated = cacheSession.getRealmDelegate().getClientById(cached.getId(), cachedRealm);
if (updated == null) throw new IllegalStateException("Not found in database");
return true;
}
@Override
public long getCacheTimestamp() {
return cached.getCacheTimestamp();
}
@Override
public void updateClient() {
if (updated != null) updated.updateClient();

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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);
}

View file

@ -1,81 +0,0 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.cache.infinispan;
import org.keycloak.models.cache.infinispan.entities.CachedClient;
import org.keycloak.models.cache.infinispan.entities.CachedClientTemplate;
import org.keycloak.models.cache.infinispan.entities.CachedGroup;
import org.keycloak.models.cache.infinispan.entities.CachedRealm;
import org.keycloak.models.cache.infinispan.entities.CachedRole;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface RealmCache {
void clear();
CachedRealm getRealm(String id);
void invalidateRealm(CachedRealm realm);
void addRealm(CachedRealm realm);
CachedRealm getRealmByName(String name);
void invalidateRealmById(String id);
CachedClient getClient(String id);
void invalidateClient(CachedClient app);
void evictClientById(String id);
void addClient(CachedClient app);
void invalidateClientById(String id);
CachedRole getRole(String id);
void invalidateRole(CachedRole role);
void evictRoleById(String id);
void addRole(CachedRole role);
void invalidateRoleById(String id);
CachedGroup getGroup(String id);
void invalidateGroup(CachedGroup role);
void addGroup(CachedGroup role);
void invalidateGroupById(String id);
CachedClientTemplate getClientTemplate(String id);
void invalidateClientTemplate(CachedClientTemplate app);
void evictClientTemplateById(String id);
void addClientTemplate(CachedClientTemplate app);
void invalidateClientTemplateById(String id);
}

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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,27 +338,12 @@ 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);
} 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();
}
long lifespan = model.getLifespan();
if (lifespan > 0) {
cache.addRevisioned(cached, startupRevision, lifespan);
} else {
cache.addRevisioned(cached, startupRevision);
}
}
} else {
cached = new CachedUser(revision, realm, delegate, notBefore);
adapter = new UserAdapter(cached, this, session, realm);
@ -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);

View file

@ -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;

View file

@ -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;
}

View file

@ -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,6 +409,21 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
return getUserSessionsCount(realm, client, false);
}
@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);

View file

@ -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();
}
}

View file

@ -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);

View file

@ -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);

View file

@ -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));
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);
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;
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();
}

View file

@ -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;
}

View file

@ -1,141 +0,0 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.jpa.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import java.io.Serializable;
/**
* @author pedroigor
*/
@Table(name="CLIENT_IDENTITY_PROV_MAPPING")
@Entity
@IdClass(ClientIdentityProviderMappingEntity.Key.class)
public class ClientIdentityProviderMappingEntity {
@Id
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CLIENT_ID")
private ClientEntity client;
@Id
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "IDENTITY_PROVIDER_ID")
private IdentityProviderEntity identityProvider;
@Column(name = "RETRIEVE_TOKEN")
private boolean retrieveToken;
public ClientEntity getClient() {
return this.client;
}
public void setClient(ClientEntity client) {
this.client = client;
}
public IdentityProviderEntity getIdentityProvider() {
return this.identityProvider;
}
public void setIdentityProvider(IdentityProviderEntity identityProvider) {
this.identityProvider = identityProvider;
}
public void setRetrieveToken(boolean retrieveToken) {
this.retrieveToken = retrieveToken;
}
public boolean isRetrieveToken() {
return retrieveToken;
}
public static class Key implements Serializable {
private ClientEntity client;
private IdentityProviderEntity identityProvider;
public Key() {
}
public Key(ClientEntity client, IdentityProviderEntity identityProvider) {
this.client = client;
this.identityProvider = identityProvider;
}
public ClientEntity getUser() {
return client;
}
public IdentityProviderEntity getIdentityProvider() {
return identityProvider;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Key key = (Key) o;
if (identityProvider != null ? !identityProvider.getAlias().equals(key.identityProvider.getAlias()) : key.identityProvider != null)
return false;
if (client != null ? !client.getId().equals(key.client != null ? key.client.getId() : null) : key.client != null) return false;
return true;
}
@Override
public int hashCode() {
int result = client != null ? client.getId().hashCode() : 0;
result = 31 * result + (identityProvider != null ? identityProvider.hashCode() : 0);
return result;
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
if (!(o instanceof ClientIdentityProviderMappingEntity)) return false;
ClientIdentityProviderMappingEntity key = (ClientIdentityProviderMappingEntity) o;
if (identityProvider != null ? !identityProvider.getAlias().equals(key.identityProvider.getAlias()) : key.identityProvider != null)
return false;
if (client != null ? !client.getId().equals(key.client != null ? key.client.getId() : null) : key.client != null) return false;
return true;
}
@Override
public int hashCode() {
int result = client != null ? client.getId().hashCode() : 0;
result = 31 * result + (identityProvider != null ? identityProvider.hashCode() : 0);
return result;
}
}

View file

@ -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;

View file

@ -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")

View file

@ -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")

View file

@ -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());

View file

@ -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;
}

View file

@ -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);
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);
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) {
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,7 +921,7 @@ 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())
@ -920,6 +956,18 @@ public class JpaUserFederatedStorageProvider implements
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();
}
}
}

View file

@ -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;
}

View file

@ -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")

View file

@ -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")

View file

@ -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>

View file

@ -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>

View file

@ -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);

View file

@ -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());

View file

@ -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;
}
});
}
}

View file

@ -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");
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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");
}
}

View file

@ -71,3 +71,4 @@ org.keycloak.credential.hash.PasswordHashSpi
org.keycloak.credential.CredentialSpi
org.keycloak.keys.PublicKeyStorageSpi
org.keycloak.keys.KeySpi
org.keycloak.storage.client.ClientStorageProviderSpi

View file

@ -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);
}

View file

@ -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.

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -0,0 +1,25 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.cache;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface CachedObject {
long getCacheTimestamp();
}

View file

@ -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
}
}

View file

@ -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;

View file

@ -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 class UserStorageProviderModel extends CacheableStorageProviderModel {
public static final String CACHE_POLICY = "cachePolicy";
public static final String MAX_LIFESPAN = "maxLifespan";
public static final String EVICTION_HOUR = "evictionHour";
public static final String EVICTION_MINUTE = "evictionMinute";
public static final String EVICTION_DAY = "evictionDay";
public static final String CACHE_INVALID_BEFORE = "cacheInvalidBefore";
public static final String IMPORT_ENABLED = "importEnabled";
public static final String FULL_SYNC_PERIOD = "fullSyncPeriod";
public static final String CHANGED_SYNC_PERIOD = "changedSyncPeriod";
public static final String LAST_SYNC = "lastSync";
public static final String ENABLED = "enabled";
public static enum CachePolicy {
NO_CACHE,
DEFAULT,
EVICT_DAILY,
EVICT_WEEKLY,
MAX_LIFESPAN
}
public UserStorageProviderModel() {
setProviderType(UserStorageProvider.class.getName());
@ -60,105 +44,6 @@ public class UserStorageProviderModel extends PrioritizedComponentModel {
private transient Integer changedSyncPeriod;
private transient Integer lastSync;
private transient Boolean importEnabled;
private transient Boolean enabled;
private transient CachePolicy cachePolicy;
private transient long maxLifespan = -1;
private transient int evictionHour = -1;
private transient int evictionMinute = -1;
private transient int evictionDay = -1;
private transient long cacheInvalidBefore = -1;
public CachePolicy getCachePolicy() {
if (cachePolicy == null) {
String str = getConfig().getFirst(CACHE_POLICY);
if (str == null) return null;
cachePolicy = CachePolicy.valueOf(str);
}
return cachePolicy;
}
public void setCachePolicy(CachePolicy cachePolicy) {
this.cachePolicy = cachePolicy;
if (cachePolicy == null) {
getConfig().remove(CACHE_POLICY);
} else {
getConfig().putSingle(CACHE_POLICY, cachePolicy.name());
}
}
public long getMaxLifespan() {
if (maxLifespan < 0) {
String str = getConfig().getFirst(MAX_LIFESPAN);
if (str == null) return -1;
maxLifespan = Long.valueOf(str);
}
return maxLifespan;
}
public void setMaxLifespan(long maxLifespan) {
this.maxLifespan = maxLifespan;
getConfig().putSingle(MAX_LIFESPAN, Long.toString(maxLifespan));
}
public int getEvictionHour() {
if (evictionHour < 0) {
String str = getConfig().getFirst(EVICTION_HOUR);
if (str == null) return -1;
evictionHour = Integer.valueOf(str);
}
return evictionHour;
}
public void setEvictionHour(int evictionHour) {
if (evictionHour > 23 || evictionHour < 0) throw new IllegalArgumentException("Must be between 0 and 23");
this.evictionHour = evictionHour;
getConfig().putSingle(EVICTION_HOUR, Integer.toString(evictionHour));
}
public int getEvictionMinute() {
if (evictionMinute < 0) {
String str = getConfig().getFirst(EVICTION_MINUTE);
if (str == null) return -1;
evictionMinute = Integer.valueOf(str);
}
return evictionMinute;
}
public void setEvictionMinute(int evictionMinute) {
if (evictionMinute > 59 || evictionMinute < 0) throw new IllegalArgumentException("Must be between 0 and 59");
this.evictionMinute = evictionMinute;
getConfig().putSingle(EVICTION_MINUTE, Integer.toString(evictionMinute));
}
public int getEvictionDay() {
if (evictionDay < 0) {
String str = getConfig().getFirst(EVICTION_DAY);
if (str == null) return -1;
evictionDay = Integer.valueOf(str);
}
return evictionDay;
}
public void setEvictionDay(int evictionDay) {
if (evictionDay > 7 || evictionDay < 1) throw new IllegalArgumentException("Must be between 1 and 7");
this.evictionDay = evictionDay;
getConfig().putSingle(EVICTION_DAY, Integer.toString(evictionDay));
}
public long getCacheInvalidBefore() {
if (cacheInvalidBefore < 0) {
String str = getConfig().getFirst(CACHE_INVALID_BEFORE);
if (str == null) return -1;
cacheInvalidBefore = Long.valueOf(str);
}
return cacheInvalidBefore;
}
public void setCacheInvalidBefore(long cacheInvalidBefore) {
this.cacheInvalidBefore = cacheInvalidBefore;
getConfig().putSingle(CACHE_INVALID_BEFORE, Long.toString(cacheInvalidBefore));
}
public boolean isImportEnabled() {
if (importEnabled == null) {
@ -178,24 +63,6 @@ public class UserStorageProviderModel extends PrioritizedComponentModel {
getConfig().putSingle(IMPORT_ENABLED, Boolean.toString(flag));
}
public void setEnabled(boolean flag) {
enabled = flag;
getConfig().putSingle(ENABLED, Boolean.toString(flag));
}
public boolean isEnabled() {
if (enabled == null) {
String val = getConfig().getFirst(ENABLED);
if (val == null) {
enabled = true;
} else {
enabled = Boolean.valueOf(val);
}
}
return enabled;
}
public int getFullSyncPeriod() {
if (fullSyncPeriod == null) {

View file

@ -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);
}

View file

@ -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) {
}
}

View file

@ -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;
}
}

View file

@ -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;
@ -135,6 +138,23 @@ public class DefaultKeycloakSession implements KeycloakSession {
return getProvider(UserProvider.class);
}
@Override
public RealmProvider realmLocalStorage() {
return getProvider(RealmProvider.class);
}
@Override
public ClientProvider clientLocalStorage() {
return realmLocalStorage();
}
@Override
public ClientProvider clientStorageManager() {
if (clientStorageManager == null) clientStorageManager = new ClientStorageManager(this);
return clientStorageManager;
}
@Override
public UserProvider userStorageManager() {
if (userStorageManager == null) userStorageManager = new UserStorageManager(this);
@ -232,6 +252,7 @@ public class DefaultKeycloakSession implements KeycloakSession {
return model;
}
@Override
public UserSessionProvider sessions() {
if (sessionProvider == null) {

View file

@ -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;
}
}

View file

@ -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();

View file

@ -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, 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", size + "");
data.add(map);
map.put("active", entry.getValue().toString());
map.put("offline", "0");
data.put(client.getId(), map);
}
return data;
}
{
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;
}
/**

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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());
}
}
}

View file

@ -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;

View file

@ -167,16 +167,36 @@ public class UserStorageTest {
KeycloakSession session = keycloakRule.startSession();
RealmModel realm = session.realms().getRealmByName("test");
CachedUserModel thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
long thorTimestamp = thor.getCacheTimestamp();
long lastTimestamp = thor.getCacheTimestamp();
realm.updateComponent(model);
keycloakRule.stopSession(session, true);
session = keycloakRule.startSession();
realm = session.realms().getRealmByName("test");
thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
lastTimestamp = thor.getCacheTimestamp();
realm.updateComponent(model);
keycloakRule.stopSession(session, true);
// test is cached
session = keycloakRule.startSession();
realm = session.realms().getRealmByName("test");
thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
// thor should be evicted because we changed the model
Assert.assertTrue(thor.getCacheTimestamp() > lastTimestamp);
lastTimestamp = thor.getCacheTimestamp();
keycloakRule.stopSession(session, true);
Time.setOffset(60 * 2 * 60); // 2 hours
session = keycloakRule.startSession();
realm = session.realms().getRealmByName("test");
UserModel thor2 = session.users().getUserByUsername("thor", realm);
Assert.assertFalse(thor2 instanceof CachedUserModel);
// thor should be evicted because we put it 2 hours in the future
if (thor2 instanceof CachedUserModel) {
Assert.assertTrue(((CachedUserModel)thor2).getCacheTimestamp() > lastTimestamp);
}
model.getConfig().remove("cachePolicy");
model.getConfig().remove("evictionHour");
model.getConfig().remove("evictionMinute");
@ -199,24 +219,46 @@ public class UserStorageTest {
KeycloakSession session = keycloakRule.startSession();
RealmModel realm = session.realms().getRealmByName("test");
CachedUserModel thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
realm.updateComponent(model);
keycloakRule.stopSession(session, true);
session = keycloakRule.startSession();
realm = session.realms().getRealmByName("test");
CachedUserModel thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
long lastTimestamp = thor.getCacheTimestamp();
keycloakRule.stopSession(session, true);
session = keycloakRule.startSession();
realm = session.realms().getRealmByName("test");
thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
lastTimestamp = thor.getCacheTimestamp();
keycloakRule.stopSession(session, true);
session = keycloakRule.startSession();
realm = session.realms().getRealmByName("test");
thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
lastTimestamp = thor.getCacheTimestamp();
keycloakRule.stopSession(session, true);
Time.setOffset(60 * 60 * 24 * 2); // 2 days in future, should be cached still
session = keycloakRule.startSession();
realm = session.realms().getRealmByName("test");
// test still
UserModel thor2 = session.users().getUserByUsername("thor", realm);
Assert.assertTrue(thor2 instanceof CachedUserModel);
thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
Assert.assertEquals(thor.getCacheTimestamp(), lastTimestamp);
lastTimestamp = thor.getCacheTimestamp();
keycloakRule.stopSession(session, true);
Time.setOffset(Time.getOffset() + 60 * 60 * 24 * 3); // 3 days into future, cache will be invalidated
session = keycloakRule.startSession();
realm = session.realms().getRealmByName("test");
thor2 = session.users().getUserByUsername("thor", realm);
Assert.assertFalse(thor2 instanceof CachedUserModel);
UserModel thor2 = session.users().getUserByUsername("thor", realm);
// thor should be evicted because we put it 2 hours in the future
if (thor2 instanceof CachedUserModel) {
Assert.assertTrue(((CachedUserModel)thor2).getCacheTimestamp() > lastTimestamp);
}
model.getConfig().remove("cachePolicy");
model.getConfig().remove("evictionHour");
model.getConfig().remove("evictionMinute");
@ -233,24 +275,44 @@ public class UserStorageTest {
KeycloakSession session = keycloakRule.startSession();
RealmModel realm = session.realms().getRealmByName("test");
CachedUserModel thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
realm.updateComponent(model);
keycloakRule.stopSession(session, true);
Time.setOffset(60 * 5); // 5 minutes in future, should be cached still
session = keycloakRule.startSession();
realm = session.realms().getRealmByName("test");
CachedUserModel thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
long lastTimestamp = thor.getCacheTimestamp();
keycloakRule.stopSession(session, true);
session = keycloakRule.startSession();
realm = session.realms().getRealmByName("test");
// test still
UserModel thor2 = session.users().getUserByUsername("thor", realm);
Assert.assertTrue(thor2 instanceof CachedUserModel);
thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
lastTimestamp = thor.getCacheTimestamp();
keycloakRule.stopSession(session, true);
Time.setOffset(60 * 5); // 5 minutes in future, should be cached still
session = keycloakRule.startSession();
realm = session.realms().getRealmByName("test");
thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
Assert.assertEquals(thor.getCacheTimestamp(), lastTimestamp);
lastTimestamp = thor.getCacheTimestamp();
keycloakRule.stopSession(session, true);
Time.setOffset(60 * 20); // 20 minutes into future, cache will be invalidated
session = keycloakRule.startSession();
realm = session.realms().getRealmByName("test");
thor2 = session.users().getUserByUsername("thor", realm);
Assert.assertFalse(thor2 instanceof CachedUserModel);
UserModel thor2 = session.users().getUserByUsername("thor", realm);
// thor should be evicted because we put it 2 hours in the future
if (thor2 instanceof CachedUserModel) {
Assert.assertTrue(((CachedUserModel)thor2).getCacheTimestamp() > lastTimestamp);
}
keycloakRule.stopSession(session, true);
session = keycloakRule.startSession();
realm = session.realms().getRealmByName("test");
model.getConfig().remove("cachePolicy");
model.getConfig().remove("maxLifespan");
realm.updateComponent(model);

View file

@ -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);

View file

@ -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);

View file

@ -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

View file

@ -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 = {};

View file

@ -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',

View file

@ -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">

View file

@ -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>

View file

@ -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>

View file

@ -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>