keycloak-aplcache

done 1st iteration

1/27/2018 12:47:16 PM

Changes

model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientIdentityProviderMappingEntity.java 141(+0 -141)

Details

diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java
index 77c2c45..83c4f8a 100755
--- a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java
@@ -67,6 +67,7 @@ public class ClientRepresentation {
     private Boolean useTemplateMappers;
     private ResourceServerRepresentation authorizationSettings;
     private Map<String, Boolean> access;
+    protected String origin;
 
 
     public String getId() {
@@ -384,4 +385,19 @@ public class ClientRepresentation {
     public void setAccess(Map<String, Boolean> access) {
         this.access = access;
     }
+
+
+    /**
+     * Returns id of ClientStorageProvider that loaded this user
+     *
+     * @return NULL if user stored locally
+     */
+    public String getOrigin() {
+        return origin;
+    }
+
+    public void setOrigin(String origin) {
+        this.origin = origin;
+    }
+
 }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java
index ab73c94..cb5d060 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java
@@ -41,6 +41,7 @@ import org.keycloak.authorization.store.ScopeStore;
 import org.keycloak.authorization.store.StoreFactory;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakTransaction;
+import org.keycloak.models.ModelException;
 import org.keycloak.models.cache.authorization.CachedStoreFactoryProvider;
 import org.keycloak.models.cache.infinispan.authorization.entities.CachedPolicy;
 import org.keycloak.models.cache.infinispan.authorization.entities.CachedResource;
@@ -64,6 +65,7 @@ import org.keycloak.models.cache.infinispan.authorization.events.ScopeRemovedEve
 import org.keycloak.models.cache.infinispan.authorization.events.ScopeUpdatedEvent;
 import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
 import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
+import org.keycloak.storage.StorageId;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -348,6 +350,9 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
     protected class ResourceServerCache implements ResourceServerStore {
         @Override
         public ResourceServer create(String clientId) {
+            if (!StorageId.isLocalStorage(clientId)) {
+                throw new ModelException("Creating resource server from federated ClientModel not supported");
+            }
             ResourceServer server = getResourceServerStoreDelegate().create(clientId);
             registerResourceServerInvalidation(server.getId());
             return server;
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
index 6ab385c..289e3a1 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
@@ -479,13 +479,13 @@ public class RealmCacheSession implements CacheRealmProvider {
 
     @Override
     public ClientModel addClient(RealmModel realm, String clientId) {
-        ClientModel client = getClientDelegate().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 = getClientDelegate().addClient(realm, id, clientId);
+        ClientModel client = getRealmDelegate().addClient(realm, id, clientId);
         return addedClient(realm, client);
     }
 
@@ -550,7 +550,7 @@ public class RealmCacheSession implements CacheRealmProvider {
             if (client == null) {
                 // TODO: Handle with cluster invalidations too
                 invalidations.add(cacheKey);
-                return getClientDelegate().getClients(realm);
+                return getRealmDelegate().getClients(realm);
             }
             list.add(client);
         }
@@ -573,7 +573,7 @@ public class RealmCacheSession implements CacheRealmProvider {
         for (RoleModel role : client.getRoles()) {
             roleRemovalInvalidations(role.getId(), role.getName(), client.getId());
         }
-        return getClientDelegate().removeClient(id, realm);
+        return getRealmDelegate().removeClient(id, realm);
     }
 
 
@@ -636,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 getClientDelegate().getClientRoles(realm, client);
+            return getRealmDelegate().getClientRoles(realm, client);
         }
 
         RoleListQuery query = cache.get(cacheKey, RoleListQuery.class);
@@ -646,7 +646,7 @@ public class RealmCacheSession implements CacheRealmProvider {
 
         if (query == null) {
             Long loaded = cache.getCurrentRevision(cacheKey);
-            Set<RoleModel> model = getClientDelegate().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());
@@ -660,7 +660,7 @@ public class RealmCacheSession implements CacheRealmProvider {
             RoleModel role = session.realms().getRoleById(id, realm);
             if (role == null) {
                 invalidations.add(cacheKey);
-                return getClientDelegate().getClientRoles(realm, client);
+                return getRealmDelegate().getClientRoles(realm, client);
             }
             list.add(role);
         }
@@ -674,7 +674,7 @@ public class RealmCacheSession implements CacheRealmProvider {
 
     @Override
     public RoleModel addClientRole(RealmModel realm, ClientModel client, String id, String name) {
-        RoleModel role = getClientDelegate().addClientRole(realm, client, id, name);
+        RoleModel role = getRealmDelegate().addClientRole(realm, client, id, name);
         addedRole(role.getId(), client.getId());
         return role;
     }
@@ -714,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 getClientDelegate().getClientRole(realm, client, name);
+            return getRealmDelegate().getClientRole(realm, client, name);
         }
 
         RoleListQuery query = cache.get(cacheKey, RoleListQuery.class);
@@ -724,7 +724,7 @@ public class RealmCacheSession implements CacheRealmProvider {
 
         if (query == null) {
             Long loaded = cache.getCurrentRevision(cacheKey);
-            RoleModel model = getClientDelegate().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);
@@ -734,7 +734,7 @@ public class RealmCacheSession implements CacheRealmProvider {
         RoleModel role = getRoleById(query.getRoles().iterator().next(), realm);
         if (role == null) {
             invalidations.add(cacheKey);
-            return getClientDelegate().getClientRole(realm, client, name);
+            return getRealmDelegate().getClientRole(realm, client, name);
         }
         return role;
     }
@@ -1025,7 +1025,7 @@ public class RealmCacheSession implements CacheRealmProvider {
             logger.tracev("adding client by id cache miss: {0}", cached.getClientId());
             cache.addRevisioned(cached, startupRevision);
         } else if (invalidations.contains(id)) {
-            return getClientDelegate().getClientById(id, realm);
+            return getRealmDelegate().getClientById(id, realm);
         } else if (managedApplications.containsKey(id)) {
             return managedApplications.get(id);
         }
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceServerStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceServerStore.java
index 207d4ab..5e79bad 100644
--- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceServerStore.java
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceServerStore.java
@@ -25,6 +25,8 @@ import org.keycloak.authorization.jpa.entities.ScopeEntity;
 import org.keycloak.authorization.model.Resource;
 import org.keycloak.authorization.model.ResourceServer;
 import org.keycloak.authorization.store.ResourceServerStore;
+import org.keycloak.models.ModelException;
+import org.keycloak.storage.StorageId;
 
 import javax.persistence.EntityManager;
 import javax.persistence.TypedQuery;
@@ -46,6 +48,9 @@ public class JPAResourceServerStore implements ResourceServerStore {
 
     @Override
     public ResourceServer create(String clientId) {
+        if (!StorageId.isLocalStorage(clientId)) {
+            throw new ModelException("Creating resource server from federated ClientModel not supported");
+        }
         ResourceServerEntity entity = new ResourceServerEntity();
 
         entity.setId(clientId);
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java
index 7f88977..7ede55c 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java
@@ -125,9 +125,6 @@ public class ClientEntity {
     @CollectionTable(name="CLIENT_AUTH_FLOW_BINDINGS", joinColumns={ @JoinColumn(name="CLIENT_ID") })
     protected Map<String, String> authFlowBindings = new HashMap<String, String>();
 
-    @OneToMany(fetch = FetchType.LAZY, mappedBy = "client", cascade = CascadeType.REMOVE)
-    Collection<ClientIdentityProviderMappingEntity> identityProviders = new ArrayList<ClientIdentityProviderMappingEntity>();
-
     @OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "client")
     Collection<ProtocolMapperEntity> protocolMappers = new ArrayList<ProtocolMapperEntity>();
 
@@ -322,14 +319,6 @@ public class ClientEntity {
         this.frontchannelLogout = frontchannelLogout;
     }
 
-    public Collection<ClientIdentityProviderMappingEntity> getIdentityProviders() {
-        return this.identityProviders;
-    }
-
-    public void setIdentityProviders(Collection<ClientIdentityProviderMappingEntity> identityProviders) {
-        this.identityProviders = identityProviders;
-    }
-
     public Collection<ProtocolMapperEntity> getProtocolMappers() {
         return protocolMappers;
     }
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-4.0.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-4.0.0.xml
index f7ebc68..859f2a7 100644
--- a/model/jpa/src/main/resources/META-INF/jpa-changelog-4.0.0.xml
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-4.0.0.xml
@@ -29,4 +29,36 @@
         </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"/>
+        <modifyDataType tableName="USER_CONSENT" columnName="CLIENT_ID" newDataType="VARCHAR(255)"/>
+        <addUniqueConstraint columnNames="CLIENT_ID, USER_ID" constraintName="UK_JKUWUVD56ONTGSUHOGM8UEWRT" tableName="USER_CONSENT"/>
+
+        <!-- Modify CLIENT_NODE_REGISTRATIONS -->
+        <dropForeignKeyConstraint constraintName="FK4129723BA992F594" baseTableName="CLIENT"/>
+        <modifyDataType tableName="CLIENT_NODE_REGISTRATIONS" columnName="CLIENT_ID" newDataType="VARCHAR(255)"/>
+
+        <!-- Modify OFFLINE_CLIENT_SESSION -->
+        <dropPrimaryKey tableName="OFFLINE_CLIENT_SESSION" constraintName="CONSTRAINT_OFFL_CL_SES_PK3"/>
+        <modifyDataType tableName="OFFLINE_CLIENT_SESSION" columnName="CLIENT_ID" newDataType="VARCHAR(255)"/>
+        <addPrimaryKey columnNames="USER_SESSION_ID,CLIENT_ID, OFFLINE_FLAG" constraintName="CONSTRAINT_OFFL_CL_SES_PK3" tableName="OFFLINE_CLIENT_SESSION"/>
+
+        <!-- FED_USER_CONSENT -->
+        <dropIndex tableName="FED_USER_CONSENT" indexName="IDX_FU_CONSENT"/>
+        <modifyDataType tableName="FED_USER_CONSENT" columnName="CLIENT_ID" newDataType="VARCHAR(255)"/>
+        <createIndex tableName="FED_USER_CONSENT" indexName="IDX_FU_CONSENT">
+            <column name="USER_ID" type="VARCHAR(255)" />
+            <column name="CLIENT_ID" type="VARCHAR(36)" />
+        </createIndex>
+    </changeSet>
 </databaseChangeLog>
diff --git a/model/jpa/src/main/resources/META-INF/persistence.xml b/model/jpa/src/main/resources/META-INF/persistence.xml
index f23198c..36e3fb4 100755
--- a/model/jpa/src/main/resources/META-INF/persistence.xml
+++ b/model/jpa/src/main/resources/META-INF/persistence.xml
@@ -39,7 +39,6 @@
         <class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
         <class>org.keycloak.models.jpa.entities.IdentityProviderEntity</class>
         <class>org.keycloak.models.jpa.entities.IdentityProviderMapperEntity</class>
-        <class>org.keycloak.models.jpa.entities.ClientIdentityProviderMappingEntity</class>
         <class>org.keycloak.models.jpa.entities.ProtocolMapperEntity</class>
         <class>org.keycloak.models.jpa.entities.UserConsentEntity</class>
         <class>org.keycloak.models.jpa.entities.UserConsentRoleEntity</class>
diff --git a/server-spi/src/main/java/org/keycloak/storage/CacheableStorageProviderModel.java b/server-spi/src/main/java/org/keycloak/storage/CacheableStorageProviderModel.java
index 355821c..8263bfe 100644
--- a/server-spi/src/main/java/org/keycloak/storage/CacheableStorageProviderModel.java
+++ b/server-spi/src/main/java/org/keycloak/storage/CacheableStorageProviderModel.java
@@ -137,7 +137,7 @@ public class CacheableStorageProviderModel extends PrioritizedComponentModel {
         getConfig().putSingle(CACHE_INVALID_BEFORE, Long.toString(cacheInvalidBefore));
     }
 
-    public static enum CachePolicy {
+    public enum CachePolicy {
         NO_CACHE,
         DEFAULT,
         EVICT_DAILY,
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index 0dad16a..2e7d3ef 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -485,6 +485,8 @@ public class ModelToRepresentation {
     public static ClientRepresentation toRepresentation(ClientModel clientModel) {
         ClientRepresentation rep = new ClientRepresentation();
         rep.setId(clientModel.getId());
+        String providerId = StorageId.resolveProviderId(clientModel);
+        rep.setOrigin(providerId);
         rep.setClientId(clientModel.getClientId());
         rep.setName(clientModel.getName());
         rep.setDescription(clientModel.getDescription());
diff --git a/server-spi-private/src/main/java/org/keycloak/storage/client/AbstractClientStorageAdapter.java b/server-spi-private/src/main/java/org/keycloak/storage/client/AbstractClientStorageAdapter.java
new file mode 100644
index 0000000..db2a9ad
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/storage/client/AbstractClientStorageAdapter.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.storage.client;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.storage.StorageId;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Helper base class for ClientModel implementations for ClientStorageProvider implementations.
+ *
+ * Contains default implementations of some methods
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public abstract class AbstractClientStorageAdapter extends UnsupportedOperationsClientStorageAdapter {
+    protected KeycloakSession session;
+    protected RealmModel realm;
+    protected ClientStorageProviderModel component;
+    private StorageId storageId;
+
+
+    public AbstractClientStorageAdapter(KeycloakSession session, RealmModel realm, ClientStorageProviderModel component) {
+        this.session = session;
+        this.realm = realm;
+        this.component = component;
+    }
+
+    /**
+     * Creates federated id based on getClientId() method
+     *
+     * @return
+     */
+    @Override
+    public String getId() {
+        if (storageId == null) {
+            storageId = new StorageId(component.getId(), getClientId());
+        }
+        return storageId.getId();
+    }
+
+    @Override
+    public final RealmModel getRealm() {
+        return realm;
+    }
+
+
+    /**
+     * This method really isn't used by anybody anywhere.  Legacy feature never supported.
+     *
+     * @return
+     */
+    @Override
+    public boolean isSurrogateAuthRequired() {
+        return false;
+    }
+
+    /**
+     * This method really isn't used by anybody anywhere.  Legacy feature never supported.
+     *
+     * @return
+     */
+    @Override
+    public void setSurrogateAuthRequired(boolean surrogateAuthRequired) {
+        // do nothing, we don't do anything with this.
+    }
+
+    /**
+     * This is for logout.  Empty implementation for now.  Can override if you can store this information somewhere.
+     *
+     * @return
+     */
+    @Override
+    public Map<String, Integer> getRegisteredNodes() {
+        return Collections.EMPTY_MAP;
+    }
+
+    /**
+     * This is for logout.  Empty implementation for now.  Can override if you can store this information somewhere.
+     *
+     * @return
+     */
+    @Override
+    public void registerNode(String nodeHost, int registrationTime) {
+        // do nothing
+    }
+
+    /**
+     * This is for logout.  Empty implementation for now.  Can override if you can store this information somewhere.
+     *
+     * @return
+     */
+    @Override
+    public void unregisterNode(String nodeHost) {
+        // do nothing
+    }
+
+    /**
+     * Overriding implementations should call super.updateClient() as this fires off an update event.
+     *
+     */
+    @Override
+    public void updateClient() {
+        session.getKeycloakSessionFactory().publish(new RealmModel.ClientUpdatedEvent() {
+
+            @Override
+            public ClientModel getUpdatedClient() {
+                return AbstractClientStorageAdapter.this;
+            }
+
+            @Override
+            public KeycloakSession getKeycloakSession() {
+                return session;
+            }
+        });
+
+    }
+
+
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/storage/client/AbstractReadOnlyClientStorageAdapter.java b/server-spi-private/src/main/java/org/keycloak/storage/client/AbstractReadOnlyClientStorageAdapter.java
new file mode 100644
index 0000000..d8e6bd4
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/storage/client/AbstractReadOnlyClientStorageAdapter.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.storage.client;
+
+import org.keycloak.models.ClientTemplateModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.storage.ReadOnlyException;
+
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public abstract class AbstractReadOnlyClientStorageAdapter extends AbstractClientStorageAdapter {
+    public AbstractReadOnlyClientStorageAdapter(KeycloakSession session, RealmModel realm, ClientStorageProviderModel component) {
+        super(session, realm, component);
+    }
+
+    @Override
+    public void setClientId(String clientId) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setName(String name) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setDescription(String description) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setEnabled(boolean enabled) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setWebOrigins(Set<String> webOrigins) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void addWebOrigin(String webOrigin) {
+        throw new ReadOnlyException("client is read only for this update");
+    }
+
+    @Override
+    public void removeWebOrigin(String webOrigin) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setRedirectUris(Set<String> redirectUris) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void addRedirectUri(String redirectUri) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void removeRedirectUri(String redirectUri) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setManagementUrl(String url) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setRootUrl(String url) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setBaseUrl(String url) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setBearerOnly(boolean only) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setNodeReRegistrationTimeout(int timeout) {
+
+        throw new ReadOnlyException("client is read only for this update");
+    }
+
+    @Override
+    public void setClientAuthenticatorType(String clientAuthenticatorType) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setSecret(String secret) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setRegistrationToken(String registrationToken) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setProtocol(String protocol) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setAttribute(String name, String value) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void removeAttribute(String name) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void removeAuthenticationFlowBindingOverride(String binding) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setAuthenticationFlowBindingOverride(String binding, String flowId) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setFrontchannelLogout(boolean flag) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setPublicClient(boolean flag) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setConsentRequired(boolean consentRequired) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setStandardFlowEnabled(boolean standardFlowEnabled) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setImplicitFlowEnabled(boolean implicitFlowEnabled) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setDirectAccessGrantsEnabled(boolean directAccessGrantsEnabled) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setClientTemplate(ClientTemplateModel template) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setUseTemplateScope(boolean flag) {
+
+        throw new ReadOnlyException("client is read only for this update");
+    }
+
+    @Override
+    public void setUseTemplateMappers(boolean flag) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setUseTemplateConfig(boolean flag) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setNotBefore(int notBefore) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public ProtocolMapperModel addProtocolMapper(ProtocolMapperModel model) {
+        throw new ReadOnlyException("client is read only for this update");
+    }
+
+    @Override
+    public void removeProtocolMapper(ProtocolMapperModel mapping) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void updateProtocolMapper(ProtocolMapperModel mapping) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setFullScopeAllowed(boolean value) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void addScopeMapping(RoleModel role) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void deleteScopeMapping(RoleModel role) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/storage/client/UnsupportedOperationsClientStorageAdapter.java b/server-spi-private/src/main/java/org/keycloak/storage/client/UnsupportedOperationsClientStorageAdapter.java
new file mode 100644
index 0000000..4de10a6
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/storage/client/UnsupportedOperationsClientStorageAdapter.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.storage.client;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.RoleModel;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Base helper class.  Unsupported operations are implemented here that throw exception on invocation.
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public abstract class UnsupportedOperationsClientStorageAdapter implements ClientModel {
+    @Override
+    public final RoleModel getRole(String name) {
+        return null;
+    }
+
+    @Override
+    public final RoleModel addRole(String name) {
+        throw new ModelException("Unsupported operation");
+    }
+
+    @Override
+    public final RoleModel addRole(String id, String name) {
+        throw new ModelException("Unsupported operation");
+    }
+
+    @Override
+    public final boolean removeRole(RoleModel role) {
+        throw new ModelException("Unsupported operation");
+    }
+
+    @Override
+    public final Set<RoleModel> getRoles() {
+        return Collections.EMPTY_SET;
+    }
+
+    @Override
+    public final List<String> getDefaultRoles() {
+        return Collections.EMPTY_LIST;
+    }
+
+    @Override
+    public final void addDefaultRole(String name) {
+        throw new ModelException("Unsupported operation");
+
+    }
+
+    @Override
+    public final void updateDefaultRoles(String... defaultRoles) {
+        throw new ModelException("Unsupported operation");
+
+    }
+
+    @Override
+    public final void removeDefaultRoles(String... defaultRoles) {
+        throw new ModelException("Unsupported operation");
+    }
+
+
+}
diff --git a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index b781ca3..a63b206 100755
--- a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -70,4 +70,5 @@ org.keycloak.transaction.TransactionManagerLookupSpi
 org.keycloak.credential.hash.PasswordHashSpi
 org.keycloak.credential.CredentialSpi
 org.keycloak.keys.PublicKeyStorageSpi
-org.keycloak.keys.KeySpi
\ No newline at end of file
+org.keycloak.keys.KeySpi
+org.keycloak.storage.client.ClientStorageProviderSpi
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/storage/ClientStorageManager.java b/services/src/main/java/org/keycloak/storage/ClientStorageManager.java
index 0a4112f..53f38cc 100644
--- a/services/src/main/java/org/keycloak/storage/ClientStorageManager.java
+++ b/services/src/main/java/org/keycloak/storage/ClientStorageManager.java
@@ -32,6 +32,7 @@ import org.keycloak.storage.client.ClientStorageProviderFactory;
 import org.keycloak.storage.client.ClientStorageProviderModel;
 import org.keycloak.storage.user.UserLookupProvider;
 
+import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
@@ -186,7 +187,8 @@ public class ClientStorageManager implements ClientProvider {
     @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");
+            //throw new RuntimeException("Federated clients do not support this operation");
+            return null;
         }
         return session.clientLocalStorage().getClientRole(realm, client, name);
     }
@@ -194,12 +196,18 @@ public class ClientStorageManager implements ClientProvider {
     @Override
     public Set<RoleModel> getClientRoles(RealmModel realm, ClientModel client) {
         if (!StorageId.isLocalStorage(client.getId())) {
-            throw new RuntimeException("Federated clients do not support this operation");
+            //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");
@@ -207,4 +215,6 @@ public class ClientStorageManager implements ClientProvider {
         return session.clientLocalStorage().removeClient(id, realm);
     }
 
+
+
 }
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProvider.java
new file mode 100644
index 0000000..198014f
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProvider.java
@@ -0,0 +1,278 @@
+/*
+ * 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.AbstractClientStorageAdapter;
+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;
+
+    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);
+    }
+
+    @Override
+    public ClientModel getClientById(String id, RealmModel realm) {
+        StorageId storageId = new StorageId(id);
+        final String clientId = storageId.getExternalId();
+        if (clientId.equals(clientId)) return new ClientAdapter(realm);
+        return null;
+    }
+
+    @Override
+    public ClientModel getClientByClientId(String clientId, RealmModel realm) {
+        if (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 null;
+        }
+
+        @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 false;
+        }
+
+        @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() {
+            return Collections.EMPTY_SET;
+        }
+
+        @Override
+        public Set<RoleModel> getRealmScopeMappings() {
+            return Collections.EMPTY_SET;
+        }
+
+        @Override
+        public boolean hasScope(RoleModel role) {
+            return false;
+        }
+    }
+
+
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProviderFactory.java
new file mode 100644
index 0000000..6b2e480
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProviderFactory.java
@@ -0,0 +1,74 @@
+/*
+ * 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";
+
+    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.BOOLEAN_TYPE)
+                .label("Redirect Uri")
+                .helpText("Valid redirect uri.  Only one allowed")
+                .defaultValue("http://localhost:8180/*")
+                .add()
+                .build();
+    }
+
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return CONFIG_PROPERTIES;
+    }
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.storage.client.ClientStorageProviderFactory b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.storage.client.ClientStorageProviderFactory
new file mode 100644
index 0000000..0ed6376
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.storage.client.ClientStorageProviderFactory
@@ -0,0 +1 @@
+org.keycloak.testsuite.federation.HardcodedClientStorageProviderFactory
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/ClientStorageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/ClientStorageTest.java
new file mode 100644
index 0000000..0684007
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/ClientStorageTest.java
@@ -0,0 +1,214 @@
+/*
+ * 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.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.RealmModel;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.ComponentRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.storage.UserStorageProvider;
+import org.keycloak.storage.client.ClientStorageProvider;
+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.openqa.selenium.By;
+
+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.List;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * 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) {
+    }
+
+    @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());
+
+        String providerId = addComponent(provider);
+    }
+
+
+
+
+    //@Test
+    public void testRunConsole() throws Exception {
+        Thread.sleep(10000000);
+    }
+
+
+    @Test
+    public void testBrowser() throws Exception {
+        String clientId = "hardcoded-client";
+        testBrowser(clientId);
+    }
+
+    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();
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java
index c3a341b..138ddcd 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java
@@ -41,7 +41,7 @@ import org.keycloak.representations.idm.RoleRepresentation;
 import org.keycloak.representations.idm.UserRepresentation;
 import org.keycloak.storage.UserStorageProvider;
 import static org.keycloak.storage.UserStorageProviderModel.CACHE_POLICY;
-import org.keycloak.storage.UserStorageProviderModel.CachePolicy;
+import org.keycloak.storage.CacheableStorageProviderModel.CachePolicy;
 import static org.keycloak.storage.UserStorageProviderModel.EVICTION_DAY;
 import static org.keycloak.storage.UserStorageProviderModel.EVICTION_HOUR;
 import static org.keycloak.storage.UserStorageProviderModel.EVICTION_MINUTE;