keycloak-aplcache

initial work

1/25/2018 1:08:26 PM

Changes

Details

diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
index 14f4c0f..2207153 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
@@ -54,7 +54,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,7 +66,7 @@ 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;
     }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java
index a521ef2..4d2ce42 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java
@@ -50,7 +50,7 @@ public class ClientTemplateAdapter implements ClientTemplateModel {
     private void getDelegateForUpdate() {
         if (updated == null) {
             cacheSession.registerClientTemplateInvalidation(cached.getId());
-            updated = cacheSession.getDelegate().getClientTemplateById(cached.getId(), cachedRealm);
+            updated = cacheSession.getRealmDelegate().getClientTemplateById(cached.getId(), cachedRealm);
             if (updated == null) throw new IllegalStateException("Not found in database");
         }
     }
@@ -63,7 +63,7 @@ public class ClientTemplateAdapter implements ClientTemplateModel {
     protected boolean isUpdated() {
         if (updated != null) return true;
         if (!invalidated) return false;
-        updated = cacheSession.getDelegate().getClientTemplateById(cached.getId(), cachedRealm);
+        updated = cacheSession.getRealmDelegate().getClientTemplateById(cached.getId(), cachedRealm);
         if (updated == null) throw new IllegalStateException("Not found in database");
         return true;
     }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java
index 21bcc66..0e69c68 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java
@@ -51,7 +51,7 @@ public class GroupAdapter implements GroupModel {
     protected void getDelegateForUpdate() {
         if (updated == null) {
             cacheSession.registerGroupInvalidation(cached.getId());
-            updated = cacheSession.getDelegate().getGroupById(cached.getId(), realm);
+            updated = cacheSession.getRealmDelegate().getGroupById(cached.getId(), realm);
             if (updated == null) throw new IllegalStateException("Not found in database");
         }
     }
@@ -64,7 +64,7 @@ public class GroupAdapter implements GroupModel {
     protected boolean isUpdated() {
         if (updated != null) return true;
         if (!invalidated) return false;
-        updated = cacheSession.getDelegate().getGroupById(cached.getId(), realm);
+        updated = cacheSession.getRealmDelegate().getGroupById(cached.getId(), realm);
         if (updated == null) throw new IllegalStateException("Not found in database");
         return true;
     }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
index bfa00e0..b80bbe3 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
@@ -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;
     }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
index 83bafb1..6ab385c 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
@@ -94,7 +94,8 @@ 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;
 
@@ -134,16 +135,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) {
@@ -319,7 +329,7 @@ public class RealmCacheSession implements CacheRealmProvider {
             @Override
             public void commit() {
                 try {
-                    if (delegate == null) return;
+                    if (realmDelegate == null) return;
                     if (clearAll) {
                         cache.clear();
                     }
@@ -360,14 +370,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 +391,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 +430,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 +454,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>();
@@ -463,19 +473,19 @@ public class RealmCacheSession implements CacheRealmProvider {
         cache.invalidateObject(id);
         invalidationEvents.add(RealmRemovedEvent.create(id, realm.getName()));
         cache.realmRemoval(id, realm.getName(), invalidations);
-        return getDelegate().removeRealm(id);
+        return getRealmDelegate().removeRealm(id);
     }
 
 
     @Override
     public ClientModel addClient(RealmModel realm, String clientId) {
-        ClientModel client = getDelegate().addClient(realm, clientId);
+        ClientModel client = getClientDelegate().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 = getClientDelegate().addClient(realm, id, clientId);
         return addedClient(realm, client);
     }
 
@@ -515,7 +525,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 +535,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 +550,7 @@ public class RealmCacheSession implements CacheRealmProvider {
             if (client == null) {
                 // TODO: Handle with cluster invalidations too
                 invalidations.add(cacheKey);
-                return getDelegate().getClients(realm);
+                return getClientDelegate().getClients(realm);
             }
             list.add(client);
         }
@@ -563,13 +573,14 @@ public class RealmCacheSession implements CacheRealmProvider {
         for (RoleModel role : client.getRoles()) {
             roleRemovalInvalidations(role.getId(), role.getName(), client.getId());
         }
-        return getDelegate().removeClient(id, realm);
+        return getClientDelegate().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 +590,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 +600,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 +610,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 +624,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 +636,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 getClientDelegate().getClientRoles(realm, client);
         }
 
         RoleListQuery query = cache.get(cacheKey, RoleListQuery.class);
@@ -635,7 +646,7 @@ public class RealmCacheSession implements CacheRealmProvider {
 
         if (query == null) {
             Long loaded = cache.getCurrentRevision(cacheKey);
-            Set<RoleModel> model = getDelegate().getClientRoles(realm, client);
+            Set<RoleModel> model = getClientDelegate().getClientRoles(realm, client);
             if (model == null) return null;
             Set<String> ids = new HashSet<>();
             for (RoleModel role : model) ids.add(role.getId());
@@ -649,7 +660,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 getClientDelegate().getClientRoles(realm, client);
             }
             list.add(role);
         }
@@ -663,7 +674,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 = getClientDelegate().addClientRole(realm, client, id, name);
         addedRole(role.getId(), client.getId());
         return role;
     }
@@ -673,7 +684,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 +694,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 +704,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 +714,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 getClientDelegate().getClientRole(realm, client, name);
         }
 
         RoleListQuery query = cache.get(cacheKey, RoleListQuery.class);
@@ -713,7 +724,7 @@ public class RealmCacheSession implements CacheRealmProvider {
 
         if (query == null) {
             Long loaded = cache.getCurrentRevision(cacheKey);
-            RoleModel model = getDelegate().getClientRole(realm, client, name);
+            RoleModel model = getClientDelegate().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 +734,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 getClientDelegate().getClientRole(realm, client, name);
         }
         return role;
     }
@@ -736,7 +747,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 +759,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 +770,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 +788,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 +811,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 +819,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 +829,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 +843,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 +855,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 +868,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 +878,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 +892,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 +907,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 +917,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 +931,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 +943,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 +957,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 +976,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 +989,7 @@ public class RealmCacheSession implements CacheRealmProvider {
 
         addGroupEventIfAbsent(GroupMovedEvent.create(subGroup, null, realm.getId()));
 
-        getDelegate().addTopLevelGroup(realm, subGroup);
+        getRealmDelegate().addTopLevelGroup(realm, subGroup);
 
     }
 
@@ -1007,14 +1018,14 @@ 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);
         } else if (invalidations.contains(id)) {
-            return getDelegate().getClientById(id, realm);
+            return getClientDelegate().getClientById(id, realm);
         } else if (managedApplications.containsKey(id)) {
             return managedApplications.get(id);
         }
@@ -1035,7 +1046,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 +1054,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 +1077,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 +1095,31 @@ public class RealmCacheSession implements CacheRealmProvider {
     // Don't cache ClientInitialAccessModel for now
     @Override
     public ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count) {
-        return getDelegate().createClientInitialAccessModel(realm, expiration, count);
+        return getRealmDelegate().createClientInitialAccessModel(realm, expiration, count);
     }
 
     @Override
     public ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id) {
-        return getDelegate().getClientInitialAccessModel(realm, id);
+        return getRealmDelegate().getClientInitialAccessModel(realm, id);
     }
 
     @Override
     public void removeClientInitialAccessModel(RealmModel realm, String id) {
-        getDelegate().removeClientInitialAccessModel(realm, id);
+        getRealmDelegate().removeClientInitialAccessModel(realm, id);
     }
 
     @Override
     public List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm) {
-        return getDelegate().listClientInitialAccess(realm);
+        return getRealmDelegate().listClientInitialAccess(realm);
     }
 
     @Override
     public void removeExpiredClientInitialAccess() {
-        getDelegate().removeExpiredClientInitialAccess();
+        getRealmDelegate().removeExpiredClientInitialAccess();
     }
 
     @Override
     public void decreaseRemainingCount(RealmModel realm, ClientInitialAccessModel clientInitialAccess) {
-        getDelegate().decreaseRemainingCount(realm, clientInitialAccess);
+        getRealmDelegate().decreaseRemainingCount(realm, clientInitialAccess);
     }
 }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java
index 00e41f3..24ed6d9 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java
@@ -49,7 +49,7 @@ public class RoleAdapter implements RoleModel {
     protected void getDelegateForUpdate() {
         if (updated == null) {
             cacheSession.registerRoleInvalidation(cached.getId(), cached.getName(), getContainerId());
-            updated = cacheSession.getDelegate().getRoleById(cached.getId(), realm);
+            updated = cacheSession.getRealmDelegate().getRoleById(cached.getId(), realm);
             if (updated == null) throw new IllegalStateException("Not found in database");
         }
     }
@@ -62,7 +62,7 @@ public class RoleAdapter implements RoleModel {
     protected boolean isUpdated() {
         if (updated != null) return true;
         if (!invalidated) return false;
-        updated = cacheSession.getDelegate().getRoleById(cached.getId(), realm);
+        updated = cacheSession.getRealmDelegate().getRoleById(cached.getId(), realm);
         if (updated == null) throw new IllegalStateException("Not found in database");
         return true;
     }
diff --git a/server-spi/src/main/java/org/keycloak/models/ClientProvider.java b/server-spi/src/main/java/org/keycloak/models/ClientProvider.java
new file mode 100644
index 0000000..b999485
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/models/ClientProvider.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.models;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.storage.client.ClientLookupProvider;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface ClientProvider extends ClientLookupProvider, Provider {
+    ClientModel addClient(RealmModel realm, String clientId);
+
+    ClientModel addClient(RealmModel realm, String id, String clientId);
+
+    List<ClientModel> getClients(RealmModel realm);
+
+    RoleModel addClientRole(RealmModel realm, ClientModel client, String name);
+
+    RoleModel addClientRole(RealmModel realm, ClientModel client, String id, String name);
+
+    RoleModel getClientRole(RealmModel realm, ClientModel client, String name);
+
+    Set<RoleModel> getClientRoles(RealmModel realm, ClientModel client);
+
+    boolean removeClient(String id, RealmModel realm);
+}
diff --git a/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java b/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java
index c239fb2..72a0d4c 100755
--- a/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java
+++ b/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java
@@ -124,6 +124,8 @@ public interface KeycloakSession {
     UserProvider users();
 
 
+    ClientProvider clientStorageManager();
+
     /**
      * Un-cached view of all users in system including users loaded by UserStorageProviders
      *
@@ -145,6 +147,15 @@ public interface KeycloakSession {
      */
     UserProvider userLocalStorage();
 
+    RealmProvider realmLocalStorage();
+
+    /**
+     * Keycloak specific local storage for clients.  No cache in front, this api talks directly to database configured for Keycloak
+     *
+     * @return
+     */
+    ClientProvider clientLocalStorage();
+
     /**
      * Hybrid storage for UserStorageProviders that can't store a specific piece of keycloak data in their external storage.
      * No cache in front.
diff --git a/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
index 6d48425..5eb18db 100755
--- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
@@ -22,6 +22,8 @@ import org.keycloak.component.ComponentModel;
 import org.keycloak.provider.ProviderEvent;
 import org.keycloak.storage.UserStorageProvider;
 import org.keycloak.storage.UserStorageProviderModel;
+import org.keycloak.storage.client.ClientStorageProvider;
+import org.keycloak.storage.client.ClientStorageProviderModel;
 
 import java.util.*;
 
@@ -341,6 +343,16 @@ public interface RealmModel extends RoleContainerModel {
         return list;
     }
 
+    default
+    List<ClientStorageProviderModel> getClientStorageProviders() {
+        List<ClientStorageProviderModel> list = new LinkedList<>();
+        for (ComponentModel component : getComponents(getId(), ClientStorageProvider.class.getName())) {
+            list.add(new ClientStorageProviderModel(component));
+        }
+        Collections.sort(list, ClientStorageProviderModel.comparator);
+        return list;
+    }
+
     String getLoginTheme();
 
     void setLoginTheme(String name);
diff --git a/server-spi/src/main/java/org/keycloak/models/RealmProvider.java b/server-spi/src/main/java/org/keycloak/models/RealmProvider.java
index d14f2d6..6fed88a 100755
--- a/server-spi/src/main/java/org/keycloak/models/RealmProvider.java
+++ b/server-spi/src/main/java/org/keycloak/models/RealmProvider.java
@@ -19,6 +19,7 @@ package org.keycloak.models;
 
 import org.keycloak.migration.MigrationModel;
 import org.keycloak.provider.Provider;
+import org.keycloak.storage.client.ClientLookupProvider;
 
 import java.util.List;
 import java.util.Set;
@@ -27,7 +28,7 @@ import java.util.Set;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public interface RealmProvider extends Provider {
+public interface RealmProvider extends Provider, ClientProvider {
 
     // Note: The reason there are so many query methods here is for layering a cache on top of an persistent KeycloakSession
     MigrationModel getMigrationModel();
@@ -58,15 +59,6 @@ public interface RealmProvider extends Provider {
 
     void addTopLevelGroup(RealmModel realm, GroupModel subGroup);
 
-    ClientModel addClient(RealmModel realm, String clientId);
-
-    ClientModel addClient(RealmModel realm, String id, String clientId);
-
-    List<ClientModel> getClients(RealmModel realm);
-
-    ClientModel getClientById(String id, RealmModel realm);
-    ClientModel getClientByClientId(String clientId, RealmModel realm);
-
 
     RoleModel addRealmRole(RealmModel realm, String name);
 
@@ -74,22 +66,12 @@ public interface RealmProvider extends Provider {
 
     RoleModel getRealmRole(RealmModel realm, String name);
 
-    RoleModel addClientRole(RealmModel realm, ClientModel client, String name);
-
-    RoleModel addClientRole(RealmModel realm, ClientModel client, String id, String name);
-
     Set<RoleModel> getRealmRoles(RealmModel realm);
 
-    RoleModel getClientRole(RealmModel realm, ClientModel client, String name);
-
-    Set<RoleModel> getClientRoles(RealmModel realm, ClientModel client);
-
     boolean removeRole(RealmModel realm, RoleModel role);
 
     RoleModel getRoleById(String id, RealmModel realm);
 
-    boolean removeClient(String id, RealmModel realm);
-
     ClientTemplateModel getClientTemplateById(String id, RealmModel realm);
     GroupModel getGroupById(String id, RealmModel realm);
 
diff --git a/server-spi/src/main/java/org/keycloak/storage/CacheableStorageProviderModel.java b/server-spi/src/main/java/org/keycloak/storage/CacheableStorageProviderModel.java
new file mode 100644
index 0000000..355821c
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/storage/CacheableStorageProviderModel.java
@@ -0,0 +1,147 @@
+/*
+ * 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.component.ComponentModel;
+import org.keycloak.component.PrioritizedComponentModel;
+
+/**
+ * @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";
+
+    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 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 static enum CachePolicy {
+        NO_CACHE,
+        DEFAULT,
+        EVICT_DAILY,
+        EVICT_WEEKLY,
+        MAX_LIFESPAN
+    }
+}
diff --git a/server-spi/src/main/java/org/keycloak/storage/client/ClientLookupProvider.java b/server-spi/src/main/java/org/keycloak/storage/client/ClientLookupProvider.java
new file mode 100644
index 0000000..7f04e5f
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/storage/client/ClientLookupProvider.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.storage.client;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.RealmModel;
+
+/**
+ * Abstraction interface for lookoup of clients by id and clientId.  These methods required for participating in login flows.
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface ClientLookupProvider {
+    ClientModel getClientById(String id, RealmModel realm);
+    ClientModel getClientByClientId(String clientId, RealmModel realm);
+}
diff --git a/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProvider.java b/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProvider.java
new file mode 100644
index 0000000..6cf80d9
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProvider.java
@@ -0,0 +1,69 @@
+/*
+ * 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
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface ClientStorageProvider extends Provider, ClientLookupProvider {
+
+
+    /**
+     * Callback when a realm is removed.  Implement this if, for example, you want to do some
+     * cleanup in your user storage when a realm is removed
+     *
+     * @param realm
+     */
+    default
+    void preRemove(RealmModel realm) {
+
+    }
+
+    /**
+     * Callback when a group is removed.  Allows you to do things like remove a user
+     * group mapping in your external store if appropriate
+     *
+     * @param realm
+     * @param group
+     */
+    default
+    void preRemove(RealmModel realm, GroupModel group) {
+
+    }
+
+    /**
+     * Callback when a role is removed.  Allows you to do things like remove a user
+     * role mapping in your external store if appropriate
+
+     * @param realm
+     * @param role
+     */
+    default
+    void preRemove(RealmModel realm, RoleModel role) {
+
+    }
+}
+
diff --git a/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProviderFactory.java b/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProviderFactory.java
new file mode 100755
index 0000000..e9f8ee7
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProviderFactory.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.storage.client;
+
+import org.keycloak.Config;
+import org.keycloak.component.ComponentFactory;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.component.ComponentValidationException;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface ClientStorageProviderFactory<T extends ClientStorageProvider> extends ComponentFactory<T, ClientStorageProvider> {
+
+
+    /**
+     * called per Keycloak transaction.
+     *
+     * @param session
+     * @param model
+     * @return
+     */
+    T create(KeycloakSession session, ComponentModel model);
+
+    /**
+     * This is the name of the provider and will be showed in the admin console as an option.
+     *
+     * @return
+     */
+    @Override
+    String getId();
+
+    @Override
+    default void init(Config.Scope config) {
+
+    }
+
+    @Override
+    default void postInit(KeycloakSessionFactory factory) {
+
+    }
+
+    @Override
+    default void close() {
+
+    }
+
+    @Override
+    default String getHelpText() {
+        return "";
+    }
+
+    @Override
+    default List<ProviderConfigProperty> getConfigProperties() {
+        return Collections.EMPTY_LIST;
+    }
+
+    @Override
+    default void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
+
+    }
+
+    /**
+     * Called when ClientStorageProviderModel is created.  This allows you to do initialization of any additional configuration
+     * you need to add.
+     *
+     * @param session
+     * @param realm
+     * @param model
+     */
+    @Override
+    default void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) {
+
+    }
+
+    /**
+     * configuration properties that are common across all UserStorageProvider implementations
+     *
+     * @return
+     */
+    @Override
+    default
+    List<ProviderConfigProperty> getCommonProviderConfigProperties() {
+        return ClientStorageProviderSpi.commonConfig();
+    }
+
+    @Override
+    default
+    Map<String, Object> getTypeMetadata() {
+        Map<String, Object> metadata = new HashMap<>();
+        return metadata;
+    }
+}
diff --git a/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProviderModel.java b/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProviderModel.java
new file mode 100755
index 0000000..54093af
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProviderModel.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.storage.client;
+
+import org.keycloak.component.ComponentModel;
+import org.keycloak.storage.CacheableStorageProviderModel;
+
+/**
+ * Stored configuration of a Client Storage provider instance.
+ *
+ * @author <a href="mailto:bburke@redhat.com">Bill Burke</a>
+ */
+public class ClientStorageProviderModel extends CacheableStorageProviderModel {
+
+    public static final String ENABLED = "enabled";
+
+    public ClientStorageProviderModel() {
+        setProviderType(ClientStorageProvider.class.getName());
+    }
+
+    public ClientStorageProviderModel(ComponentModel copy) {
+        super(copy);
+    }
+
+    private transient Boolean enabled;
+
+     public void setEnabled(boolean flag) {
+        enabled = flag;
+        getConfig().putSingle(ENABLED, Boolean.toString(flag));
+    }
+
+
+    public boolean isEnabled() {
+        if (enabled == null) {
+            String val = getConfig().getFirst(ENABLED);
+            if (val == null) {
+                enabled = true;
+            } else {
+                enabled = Boolean.valueOf(val);
+            }
+        }
+        return enabled;
+
+    }
+}
diff --git a/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProviderSpi.java b/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProviderSpi.java
new file mode 100755
index 0000000..fd0c5a1
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProviderSpi.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.storage.client;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.provider.ProviderConfigurationBuilder;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ClientStorageProviderSpi implements Spi {
+
+    @Override
+    public boolean isInternal() {
+        return false;
+    }
+
+    @Override
+    public String getName() {
+        return "client-storage";
+    }
+
+    @Override
+    public Class<? extends Provider> getProviderClass() {
+        return ClientStorageProvider.class;
+    }
+
+    @Override
+    public Class<? extends ProviderFactory> getProviderFactoryClass() {
+        return ClientStorageProviderFactory.class;
+    }
+
+    private static final List<ProviderConfigProperty> commonConfig;
+
+    static {
+        List<ProviderConfigProperty> config = ProviderConfigurationBuilder.create()
+                .property()
+                .name("enabled").type(ProviderConfigProperty.BOOLEAN_TYPE).add()
+                .property()
+                .name("priority").type(ProviderConfigProperty.STRING_TYPE).add()
+                 .property()
+                .name("cachePolicy").type(ProviderConfigProperty.STRING_TYPE).add()
+                .property()
+                .name("maxLifespan").type(ProviderConfigProperty.STRING_TYPE).add()
+                .property()
+                .name("evictionHour").type(ProviderConfigProperty.STRING_TYPE).add()
+                .property()
+                .name("evictionMinute").type(ProviderConfigProperty.STRING_TYPE).add()
+                .property()
+                .name("evictionDay").type(ProviderConfigProperty.STRING_TYPE).add()
+                .property()
+                .name("cacheInvalidBefore").type(ProviderConfigProperty.STRING_TYPE).add()
+                .build();
+        commonConfig = Collections.unmodifiableList(config);
+    }
+
+    public static List<ProviderConfigProperty> commonConfig() {
+        return commonConfig;
+
+    }
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/storage/StorageId.java b/server-spi/src/main/java/org/keycloak/storage/StorageId.java
index fbbc406..3a2a141 100644
--- a/server-spi/src/main/java/org/keycloak/storage/StorageId.java
+++ b/server-spi/src/main/java/org/keycloak/storage/StorageId.java
@@ -17,6 +17,7 @@
 package org.keycloak.storage;
 
 import org.keycloak.component.ComponentModel;
+import org.keycloak.models.ClientModel;
 import org.keycloak.models.UserModel;
 
 import java.io.Serializable;
@@ -75,8 +76,15 @@ public class StorageId implements Serializable {
     public static boolean isLocalStorage(UserModel user) {
         return new StorageId(user.getId()).getProviderId() == null;
     }
-    public static boolean isLocalStorage(String userId) {
-        return new StorageId(userId).getProviderId() == null;
+    public static boolean isLocalStorage(String id) {
+        return new StorageId(id).getProviderId() == null;
+    }
+
+    public static String resolveProviderId(ClientModel client) {
+        return new StorageId(client.getId()).getProviderId();
+    }
+    public static boolean isLocalStorage(ClientModel client) {
+        return new StorageId(client.getId()).getProviderId() == null;
     }
     public boolean isLocal() {
         return getProviderId() == null;
diff --git a/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderModel.java b/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderModel.java
index 1ec06a6..0b32b94 100755
--- a/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderModel.java
+++ b/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderModel.java
@@ -26,28 +26,14 @@ import org.keycloak.component.PrioritizedComponentModel;
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  * @author <a href="mailto:bburke@redhat.com">Bill Burke</a>
  */
-public class UserStorageProviderModel extends PrioritizedComponentModel {
-
-    public static final String CACHE_POLICY = "cachePolicy";
-    public static final String MAX_LIFESPAN = "maxLifespan";
-    public static final String EVICTION_HOUR = "evictionHour";
-    public static final String EVICTION_MINUTE = "evictionMinute";
-    public static final String EVICTION_DAY = "evictionDay";
-    public static final String CACHE_INVALID_BEFORE = "cacheInvalidBefore";
+public class UserStorageProviderModel extends CacheableStorageProviderModel {
+
     public static final String IMPORT_ENABLED = "importEnabled";
     public static final String FULL_SYNC_PERIOD = "fullSyncPeriod";
     public static final String CHANGED_SYNC_PERIOD = "changedSyncPeriod";
     public static final String LAST_SYNC = "lastSync";
     public static final String ENABLED = "enabled";
 
-    public static enum CachePolicy {
-        NO_CACHE,
-        DEFAULT,
-        EVICT_DAILY,
-        EVICT_WEEKLY,
-        MAX_LIFESPAN
-    }
-
     public UserStorageProviderModel() {
         setProviderType(UserStorageProvider.class.getName());
     }
@@ -61,104 +47,6 @@ public class UserStorageProviderModel extends PrioritizedComponentModel {
     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) {
diff --git a/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java b/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java
index 61ae1be..ce71dee 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java
@@ -25,7 +25,7 @@ import org.keycloak.models.RealmProvider;
  */
 public interface CacheRealmProvider extends RealmProvider {
     void clear();
-    RealmProvider getDelegate();
+    RealmProvider getRealmDelegate();
 
     void registerRealmInvalidation(String id, String name);
 
diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java
index 0cc81c1..6cabf76 100644
--- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java
+++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java
@@ -20,6 +20,7 @@ import org.keycloak.component.ComponentFactory;
 import org.keycloak.component.ComponentModel;
 import org.keycloak.credential.UserCredentialStoreManager;
 import org.keycloak.keys.DefaultKeyManager;
+import org.keycloak.models.ClientProvider;
 import org.keycloak.models.KeycloakContext;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
@@ -35,6 +36,7 @@ import org.keycloak.models.cache.UserCache;
 import org.keycloak.provider.Provider;
 import org.keycloak.provider.ProviderFactory;
 import org.keycloak.sessions.AuthenticationSessionProvider;
+import org.keycloak.storage.ClientStorageManager;
 import org.keycloak.storage.UserStorageManager;
 import org.keycloak.storage.federated.UserFederatedStorageProvider;
 import org.keycloak.theme.DefaultThemeManager;
@@ -58,6 +60,7 @@ public class DefaultKeycloakSession implements KeycloakSession {
     private final Map<String, Object> attributes = new HashMap<>();
     private RealmProvider model;
     private UserStorageManager userStorageManager;
+    private ClientStorageManager clientStorageManager;
     private UserCredentialStoreManager userCredentialStorageManager;
     private UserSessionProvider sessionProvider;
     private AuthenticationSessionProvider authenticationSessionProvider;
@@ -136,6 +139,23 @@ public class DefaultKeycloakSession implements KeycloakSession {
     }
 
     @Override
+    public RealmProvider realmLocalStorage() {
+        return getProvider(RealmProvider.class);
+    }
+
+    @Override
+    public ClientProvider clientLocalStorage() {
+        return realmLocalStorage();
+    }
+
+    @Override
+    public ClientProvider clientStorageManager() {
+        if (clientStorageManager == null) clientStorageManager = new ClientStorageManager(this);
+        return clientStorageManager;
+    }
+
+
+    @Override
     public UserProvider userStorageManager() {
         if (userStorageManager == null) userStorageManager = new UserStorageManager(this);
         return userStorageManager;
@@ -232,6 +252,7 @@ public class DefaultKeycloakSession implements KeycloakSession {
         return model;
     }
 
+
     @Override
     public UserSessionProvider sessions() {
         if (sessionProvider == null) {
diff --git a/services/src/main/java/org/keycloak/storage/ClientStorageManager.java b/services/src/main/java/org/keycloak/storage/ClientStorageManager.java
new file mode 100644
index 0000000..0a4112f
--- /dev/null
+++ b/services/src/main/java/org/keycloak/storage/ClientStorageManager.java
@@ -0,0 +1,210 @@
+/*
+ * 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.models.UserModel;
+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 org.keycloak.storage.user.UserLookupProvider;
+
+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 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 session.clientLocalStorage().getClientRoles(realm, client);
+    }
+
+    @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);
+    }
+
+}