keycloak-aplcache
Changes
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java 5(+5 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java 24(+12 -12)
model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientIdentityProviderMappingEntity.java 141(+0 -141)
server-spi-private/src/main/java/org/keycloak/storage/client/AbstractClientStorageAdapter.java 139(+139 -0)
server-spi-private/src/main/java/org/keycloak/storage/client/AbstractReadOnlyClientStorageAdapter.java 280(+280 -0)
server-spi-private/src/main/java/org/keycloak/storage/client/ClientStorageProviderFactory.java 0(+0 -0)
server-spi-private/src/main/java/org/keycloak/storage/client/UnsupportedOperationsClientStorageAdapter.java 82(+82 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProvider.java 278(+278 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProviderFactory.java 74(+74 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.storage.client.ClientStorageProviderFactory 1(+1 -0)
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;