From 6b84b9b4b6804923e8fc3f32a55cdd685043db1d Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Sat, 27 Jan 2018 09:47:16 -0500 Subject: [PATCH] done 1st iteration --- .../idm/ClientRepresentation.java | 16 + .../cache/infinispan/RealmCacheSession.java | 24 +- .../StoreFactoryCacheSession.java | 5 + .../jpa/store/JPAResourceServerStore.java | 5 + .../models/jpa/entities/ClientEntity.java | 11 - .../ClientIdentityProviderMappingEntity.java | 141 --------- .../META-INF/jpa-changelog-4.0.0.xml | 32 ++ .../main/resources/META-INF/persistence.xml | 1 - .../models/utils/ModelToRepresentation.java | 2 + .../client/AbstractClientStorageAdapter.java | 139 +++++++++ .../AbstractReadOnlyClientStorageAdapter.java | 280 ++++++++++++++++++ .../client/ClientStorageProviderFactory.java | 0 .../client/ClientStorageProviderSpi.java | 2 +- ...pportedOperationsClientStorageAdapter.java | 82 +++++ .../services/org.keycloak.provider.Spi | 3 +- .../CacheableStorageProviderModel.java | 2 +- .../storage/ClientStorageManager.java | 14 +- .../HardcodedClientStorageProvider.java | 278 +++++++++++++++++ ...HardcodedClientStorageProviderFactory.java | 74 +++++ ...torage.client.ClientStorageProviderFactory | 1 + .../federation/storage/ClientStorageTest.java | 214 +++++++++++++ .../federation/storage/UserStorageTest.java | 2 +- 22 files changed, 1157 insertions(+), 171 deletions(-) delete mode 100755 model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientIdentityProviderMappingEntity.java create mode 100644 server-spi-private/src/main/java/org/keycloak/storage/client/AbstractClientStorageAdapter.java create mode 100644 server-spi-private/src/main/java/org/keycloak/storage/client/AbstractReadOnlyClientStorageAdapter.java rename {server-spi => server-spi-private}/src/main/java/org/keycloak/storage/client/ClientStorageProviderFactory.java (100%) rename {server-spi => server-spi-private}/src/main/java/org/keycloak/storage/client/ClientStorageProviderSpi.java (99%) create mode 100644 server-spi-private/src/main/java/org/keycloak/storage/client/UnsupportedOperationsClientStorageAdapter.java create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProvider.java create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProviderFactory.java create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.storage.client.ClientStorageProviderFactory create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/ClientStorageTest.java 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 77c2c45bd7..83c4f8abd9 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 access; + protected String origin; public String getId() { @@ -384,4 +385,19 @@ public class ClientRepresentation { public void setAccess(Map 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/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java index 6ab385cda4..289e3a14e1 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 model = getClientDelegate().getClientRoles(realm, client); + Set model = getRealmDelegate().getClientRoles(realm, client); if (model == null) return null; Set 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/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 ab73c949d7..cb5d0606d6 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 Bill Burke @@ -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/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 207d4abe4a..5e79badca0 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 7f889777af..7ede55c743 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 authFlowBindings = new HashMap(); - @OneToMany(fetch = FetchType.LAZY, mappedBy = "client", cascade = CascadeType.REMOVE) - Collection identityProviders = new ArrayList(); - @OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "client") Collection protocolMappers = new ArrayList(); @@ -322,14 +319,6 @@ public class ClientEntity { this.frontchannelLogout = frontchannelLogout; } - public Collection getIdentityProviders() { - return this.identityProviders; - } - - public void setIdentityProviders(Collection identityProviders) { - this.identityProviders = identityProviders; - } - public Collection getProtocolMappers() { return protocolMappers; } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientIdentityProviderMappingEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientIdentityProviderMappingEntity.java deleted file mode 100755 index de625d7d2a..0000000000 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientIdentityProviderMappingEntity.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.models.jpa.entities; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.Id; -import javax.persistence.IdClass; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.Table; -import java.io.Serializable; - -/** - * @author pedroigor - */ -@Table(name="CLIENT_IDENTITY_PROV_MAPPING") -@Entity -@IdClass(ClientIdentityProviderMappingEntity.Key.class) -public class ClientIdentityProviderMappingEntity { - - @Id - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "CLIENT_ID") - private ClientEntity client; - - @Id - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "IDENTITY_PROVIDER_ID") - private IdentityProviderEntity identityProvider; - - @Column(name = "RETRIEVE_TOKEN") - private boolean retrieveToken; - - public ClientEntity getClient() { - return this.client; - } - - public void setClient(ClientEntity client) { - this.client = client; - } - - public IdentityProviderEntity getIdentityProvider() { - return this.identityProvider; - } - - public void setIdentityProvider(IdentityProviderEntity identityProvider) { - this.identityProvider = identityProvider; - } - - public void setRetrieveToken(boolean retrieveToken) { - this.retrieveToken = retrieveToken; - } - - public boolean isRetrieveToken() { - return retrieveToken; - } - - public static class Key implements Serializable { - - private ClientEntity client; - private IdentityProviderEntity identityProvider; - - public Key() { - } - - public Key(ClientEntity client, IdentityProviderEntity identityProvider) { - this.client = client; - this.identityProvider = identityProvider; - } - - public ClientEntity getUser() { - return client; - } - - public IdentityProviderEntity getIdentityProvider() { - return identityProvider; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - Key key = (Key) o; - - if (identityProvider != null ? !identityProvider.getAlias().equals(key.identityProvider.getAlias()) : key.identityProvider != null) - return false; - if (client != null ? !client.getId().equals(key.client != null ? key.client.getId() : null) : key.client != null) return false; - - return true; - } - - @Override - public int hashCode() { - int result = client != null ? client.getId().hashCode() : 0; - result = 31 * result + (identityProvider != null ? identityProvider.hashCode() : 0); - return result; - } - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null) return false; - if (!(o instanceof ClientIdentityProviderMappingEntity)) return false; - - ClientIdentityProviderMappingEntity key = (ClientIdentityProviderMappingEntity) o; - - if (identityProvider != null ? !identityProvider.getAlias().equals(key.identityProvider.getAlias()) : key.identityProvider != null) - return false; - if (client != null ? !client.getId().equals(key.client != null ? key.client.getId() : null) : key.client != null) return false; - - return true; - } - - @Override - public int hashCode() { - int result = client != null ? client.getId().hashCode() : 0; - result = 31 * result + (identityProvider != null ? identityProvider.hashCode() : 0); - return result; - } - - -} 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 f7ebc6838b..859f2a7b85 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/model/jpa/src/main/resources/META-INF/persistence.xml b/model/jpa/src/main/resources/META-INF/persistence.xml index f23198c387..36e3fb40d2 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 @@ org.keycloak.models.jpa.entities.UserRoleMappingEntity org.keycloak.models.jpa.entities.IdentityProviderEntity org.keycloak.models.jpa.entities.IdentityProviderMapperEntity - org.keycloak.models.jpa.entities.ClientIdentityProviderMappingEntity org.keycloak.models.jpa.entities.ProtocolMapperEntity org.keycloak.models.jpa.entities.UserConsentEntity org.keycloak.models.jpa.entities.UserConsentRoleEntity 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 0dad16a48d..2e7d3ef92d 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 0000000000..db2a9adeac --- /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 Bill Burke + * @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 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 0000000000..d8e6bd456a --- /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 Bill Burke + * @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 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 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/src/main/java/org/keycloak/storage/client/ClientStorageProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/storage/client/ClientStorageProviderFactory.java similarity index 100% rename from server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProviderFactory.java rename to server-spi-private/src/main/java/org/keycloak/storage/client/ClientStorageProviderFactory.java diff --git a/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProviderSpi.java b/server-spi-private/src/main/java/org/keycloak/storage/client/ClientStorageProviderSpi.java similarity index 99% rename from server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProviderSpi.java rename to server-spi-private/src/main/java/org/keycloak/storage/client/ClientStorageProviderSpi.java index fd0c5a1892..bf1146d40e 100755 --- a/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProviderSpi.java +++ b/server-spi-private/src/main/java/org/keycloak/storage/client/ClientStorageProviderSpi.java @@ -33,7 +33,7 @@ public class ClientStorageProviderSpi implements Spi { @Override public boolean isInternal() { - return false; + return true; } @Override 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 0000000000..4de10a60b6 --- /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 Bill Burke + * @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 getRoles() { + return Collections.EMPTY_SET; + } + + @Override + public final List 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 b781ca359d..a63b206527 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/server-spi/src/main/java/org/keycloak/storage/CacheableStorageProviderModel.java b/server-spi/src/main/java/org/keycloak/storage/CacheableStorageProviderModel.java index 355821c90c..8263bfe153 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/services/src/main/java/org/keycloak/storage/ClientStorageManager.java b/services/src/main/java/org/keycloak/storage/ClientStorageManager.java index 0a4112f689..53f38cc0de 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,11 +196,17 @@ public class ClientStorageManager implements ClientProvider { @Override public Set 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)) { @@ -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 0000000000..198014f561 --- /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 Bill Burke + * @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 getWebOrigins() { + return Collections.EMPTY_SET; + } + + @Override + public Set getRedirectUris() { + HashSet 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 getAttributes() { + return Collections.EMPTY_MAP; + } + + @Override + public String getAuthenticationFlowBindingOverride(String binding) { + return null; + } + + @Override + public Map 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 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 getScopeMappings() { + return Collections.EMPTY_SET; + } + + @Override + public Set 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 0000000000..6b2e480ffa --- /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 Bill Burke + * @version $Revision: 1 $ + */ +public class HardcodedClientStorageProviderFactory implements ClientStorageProviderFactory { + @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 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 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 0000000000..0ed6376e2c --- /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 0000000000..06840076d5 --- /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 Bill Burke + */ +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 c3a341b190..138ddcde0a 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;