diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java index 9d188df09c..af8a06386f 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java @@ -20,6 +20,7 @@ package org.keycloak.models.cache.infinispan; import org.jboss.logging.Logger; import org.keycloak.cluster.ClusterProvider; import org.keycloak.models.ClientScopeModel; +import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.cache.infinispan.events.InvalidationEvent; import org.keycloak.common.constants.ServiceAccountConstants; import org.keycloak.component.ComponentModel; @@ -48,6 +49,7 @@ import org.keycloak.models.cache.infinispan.events.UserFederationLinkRemovedEven import org.keycloak.models.cache.infinispan.events.UserFederationLinkUpdatedEvent; import org.keycloak.models.cache.infinispan.events.UserFullInvalidationEvent; import org.keycloak.models.cache.infinispan.events.UserUpdatedEvent; +import org.keycloak.models.cache.infinispan.stream.InIdentityProviderPredicate; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.ReadOnlyUserModelDelegate; import org.keycloak.storage.CacheableStorageProviderModel; @@ -844,6 +846,12 @@ public class UserCacheSession implements UserCache { return getDelegate().removeFederatedIdentity(realm, user, socialProvider); } + @Override + public void preRemove(RealmModel realm, IdentityProviderModel provider) { + cache.addInvalidations(InIdentityProviderPredicate.create().provider(provider.getAlias()), invalidations); + getDelegate().preRemove(realm, provider); + } + @Override public void grantToAllUsers(RealmModel realm, RoleModel role) { addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedFederatedIdentityLinks.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedFederatedIdentityLinks.java index 795969539d..26f465bd8b 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedFederatedIdentityLinks.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedFederatedIdentityLinks.java @@ -29,7 +29,7 @@ import java.util.Set; * * @author Marek Posolda */ -public class CachedFederatedIdentityLinks extends AbstractRevisioned implements InRealm { +public class CachedFederatedIdentityLinks extends AbstractRevisioned implements InRealm, InIdentityProvider { private final String realmId; private final Set federatedIdentities = new HashSet<>(); @@ -48,4 +48,10 @@ public class CachedFederatedIdentityLinks extends AbstractRevisioned implements public Set getFederatedIdentities() { return federatedIdentities; } + + @Override + public boolean contains(String alias) { + return federatedIdentities.stream().anyMatch( + federatedIdentityModel -> federatedIdentityModel.getIdentityProvider().equals(alias)); + } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/InIdentityProvider.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/InIdentityProvider.java new file mode 100755 index 0000000000..8ba0181166 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/InIdentityProvider.java @@ -0,0 +1,11 @@ +package org.keycloak.models.cache.infinispan.entities; + +import java.util.List; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public interface InIdentityProvider extends Revisioned { + boolean contains(String providerId); +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/InIdentityProviderPredicate.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/InIdentityProviderPredicate.java new file mode 100755 index 0000000000..aef0b488fc --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/stream/InIdentityProviderPredicate.java @@ -0,0 +1,70 @@ +package org.keycloak.models.cache.infinispan.stream; + +import org.keycloak.models.cache.infinispan.entities.InIdentityProvider; +import org.keycloak.models.cache.infinispan.entities.InRealm; +import org.keycloak.models.cache.infinispan.entities.Revisioned; + +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import java.io.Serializable; +import java.util.Map; +import java.util.function.Predicate; +import org.infinispan.commons.marshall.Externalizer; +import org.infinispan.commons.marshall.MarshallUtil; +import org.infinispan.commons.marshall.SerializeWith; + +/** + * @author Pedro Igor + */ +@SerializeWith(InIdentityProviderPredicate.ExternalizerImpl.class) +public class InIdentityProviderPredicate implements Predicate>, Serializable { + private String id; + + public static InIdentityProviderPredicate create() { + return new InIdentityProviderPredicate(); + } + + public InIdentityProviderPredicate provider(String id) { + this.id = id; + return this; + } + + @Override + public boolean test(Map.Entry entry) { + Object value = entry.getValue(); + if (value == null) return false; + if (!(value instanceof InIdentityProvider)) return false; + + return ((InIdentityProvider)value).contains(id); + } + + public static class ExternalizerImpl implements Externalizer { + + private static final int VERSION_1 = 1; + + @Override + public void writeObject(ObjectOutput output, InIdentityProviderPredicate obj) throws IOException { + output.writeByte(VERSION_1); + + MarshallUtil.marshallString(obj.id, output); + } + + @Override + public InIdentityProviderPredicate readObject(ObjectInput input) throws IOException, ClassNotFoundException { + switch (input.readByte()) { + case VERSION_1: + return readObjectVersion1(input); + default: + throw new IOException("Unknown version"); + } + } + + public InIdentityProviderPredicate readObjectVersion1(ObjectInput input) throws IOException, ClassNotFoundException { + InIdentityProviderPredicate res = new InIdentityProviderPredicate(); + res.id = MarshallUtil.unmarshallString(input); + + return res; + } + } +} diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java index ba72a4d4a5..4a411071c4 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java @@ -26,6 +26,7 @@ import org.keycloak.models.ClientModel; import org.keycloak.models.ClientScopeModel; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.GroupModel; +import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelException; @@ -191,6 +192,13 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { } } + @Override + public void preRemove(RealmModel realm, IdentityProviderModel provider) { + em.createNamedQuery("deleteFederatedIdentityByProvider") + .setParameter("realmId", realm.getId()) + .setParameter("providerAlias", provider.getAlias()).executeUpdate(); + } + @Override public void addConsent(RealmModel realm, String userId, UserConsentModel consent) { String clientId = consent.getClient().getId(); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/FederatedIdentityEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/FederatedIdentityEntity.java index 4bafe7cae8..88394d4982 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/FederatedIdentityEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/FederatedIdentityEntity.java @@ -38,6 +38,7 @@ import java.io.Serializable; @NamedQuery(name= "findFederatedIdentityByUserAndProvider", query="select link from FederatedIdentityEntity link where link.user = :user and link.identityProvider = :identityProvider"), @NamedQuery(name= "findUserByFederatedIdentityAndRealm", query="select link.user from FederatedIdentityEntity link where link.realmId = :realmId and link.identityProvider = :identityProvider and link.userId = :userId"), @NamedQuery(name= "deleteFederatedIdentityByRealm", query="delete from FederatedIdentityEntity social where social.user IN (select u from UserEntity u where realmId=:realmId)"), + @NamedQuery(name= "deleteFederatedIdentityByProvider", query="delete from FederatedIdentityEntity fdi where fdi.realmId = :realmId and fdi.identityProvider = :providerAlias "), @NamedQuery(name= "deleteFederatedIdentityByRealmAndLink", query="delete from FederatedIdentityEntity social where social.user IN (select u from UserEntity u where realmId=:realmId and u.federationLink=:link)"), @NamedQuery(name= "deleteFederatedIdentityByUser", query="delete from FederatedIdentityEntity social where social.user = :user") }) diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java index 9ba0e34a29..e327a39c5d 100644 --- a/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java +++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java @@ -27,6 +27,7 @@ import org.keycloak.models.ClientModel; import org.keycloak.models.ClientScopeModel; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.GroupModel; +import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelException; @@ -210,6 +211,13 @@ public class JpaUserFederatedStorageProvider implements return true; } + @Override + public void preRemove(RealmModel realm, IdentityProviderModel provider) { + em.createNamedQuery("deleteBrokerLinkByIdentityProvider") + .setParameter("realmId", realm.getId()) + .setParameter("providerAlias", provider.getAlias()); + } + private BrokerLinkEntity getBrokerLinkEntity(RealmModel realm, String userId, String socialProvider) { TypedQuery query = em.createNamedQuery("findBrokerLinkByUserAndProvider", BrokerLinkEntity.class) .setParameter("userId", userId) diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/BrokerLinkEntity.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/BrokerLinkEntity.java index a32acb668f..585bb942a3 100755 --- a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/BrokerLinkEntity.java +++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/BrokerLinkEntity.java @@ -38,6 +38,7 @@ import java.io.Serializable; @NamedQuery(name= "findUserByBrokerLinkAndRealm", query="select link.userId from BrokerLinkEntity link where link.realmId = :realmId and link.identityProvider = :identityProvider and link.brokerUserId = :brokerUserId"), @NamedQuery(name= "deleteBrokerLinkByStorageProvider", query="delete from BrokerLinkEntity social where social.storageProviderId = :storageProviderId"), @NamedQuery(name= "deleteBrokerLinkByRealm", query="delete from BrokerLinkEntity social where social.realmId = :realmId"), + @NamedQuery(name= "deleteBrokerLinkByIdentityProvider", query="delete from BrokerLinkEntity b where b.realmId = :realmId and b.identityProvider = :providerAlias"), @NamedQuery(name= "deleteBrokerLinkByRealmAndLink", query="delete from BrokerLinkEntity social where social.userId IN (select u.id from UserEntity u where realmId=:realmId and u.federationLink=:link)"), @NamedQuery(name= "deleteBrokerLinkByUser", query="delete from BrokerLinkEntity social where social.userId = :userId and social.realmId = :realmId") }) diff --git a/server-spi/src/main/java/org/keycloak/models/UserProvider.java b/server-spi/src/main/java/org/keycloak/models/UserProvider.java index 934b456071..c3c5bb1939 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/UserProvider.java @@ -40,6 +40,7 @@ public interface UserProvider extends Provider, void addFederatedIdentity(RealmModel realm, UserModel user, FederatedIdentityModel socialLink); boolean removeFederatedIdentity(RealmModel realm, UserModel user, String socialProvider); + void preRemove(RealmModel realm, IdentityProviderModel provider); void updateFederatedIdentity(RealmModel realm, UserModel federatedUser, FederatedIdentityModel federatedIdentityModel); Set getFederatedIdentities(UserModel user, RealmModel realm); FederatedIdentityModel getFederatedIdentity(UserModel user, String socialProvider, RealmModel realm); @@ -97,5 +98,4 @@ public interface UserProvider extends Provider, void close(); void preRemove(RealmModel realm, ComponentModel component); - } diff --git a/server-spi/src/main/java/org/keycloak/storage/federated/UserBrokerLinkFederatedStorage.java b/server-spi/src/main/java/org/keycloak/storage/federated/UserBrokerLinkFederatedStorage.java index 36726dd112..ff5acc8737 100644 --- a/server-spi/src/main/java/org/keycloak/storage/federated/UserBrokerLinkFederatedStorage.java +++ b/server-spi/src/main/java/org/keycloak/storage/federated/UserBrokerLinkFederatedStorage.java @@ -17,6 +17,7 @@ package org.keycloak.storage.federated; import org.keycloak.models.FederatedIdentityModel; +import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.RealmModel; import java.util.Set; @@ -29,6 +30,7 @@ public interface UserBrokerLinkFederatedStorage { String getUserByFederatedIdentity(FederatedIdentityModel socialLink, RealmModel realm); void addFederatedIdentity(RealmModel realm, String userId, FederatedIdentityModel socialLink); boolean removeFederatedIdentity(RealmModel realm, String userId, String socialProvider); + void preRemove(RealmModel realm, IdentityProviderModel provider); void updateFederatedIdentity(RealmModel realm, String userId, FederatedIdentityModel federatedIdentityModel); Set getFederatedIdentities(String userId, RealmModel realm); FederatedIdentityModel getFederatedIdentity(String userId, String socialProvider, RealmModel realm); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java index 64195a83be..9aeb0fd19a 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java @@ -130,6 +130,7 @@ public class IdentityProviderResource { } String alias = this.identityProviderModel.getAlias(); + session.users().preRemove(realm, identityProviderModel); this.realm.removeIdentityProviderByAlias(alias); Set mappers = this.realm.getIdentityProviderMappersByAlias(alias); diff --git a/services/src/main/java/org/keycloak/storage/UserStorageManager.java b/services/src/main/java/org/keycloak/storage/UserStorageManager.java index 0cc2756a1a..19cce15194 100755 --- a/services/src/main/java/org/keycloak/storage/UserStorageManager.java +++ b/services/src/main/java/org/keycloak/storage/UserStorageManager.java @@ -25,6 +25,7 @@ import org.keycloak.models.ClientModel; import org.keycloak.models.ClientScopeModel; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.GroupModel; +import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionTask; import org.keycloak.models.ModelException; @@ -224,6 +225,13 @@ public class UserStorageManager implements UserProvider, OnUserCache, OnCreateCo return getFederatedStorage().removeFederatedIdentity(realm, user.getId(), socialProvider); } } + + @Override + public void preRemove(RealmModel realm, IdentityProviderModel provider) { + localStorage().preRemove(realm, provider); + getFederatedStorage().preRemove(realm, provider); + } + @Override public void addConsent(RealmModel realm, String userId, UserConsentModel consent) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AccountLinkTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AccountLinkTest.java index 769b37f0b9..ed6e654601 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AccountLinkTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AccountLinkTest.java @@ -20,8 +20,14 @@ import org.jboss.arquillian.graphene.page.Page; import org.junit.Before; import org.junit.Test; import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.UsersResource; import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.models.FederatedIdentityModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; import org.keycloak.representations.idm.ComponentRepresentation; +import org.keycloak.representations.idm.FederatedIdentityRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.storage.UserStorageProvider; @@ -33,7 +39,10 @@ import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.UpdateAccountInformationPage; import java.util.List; +import java.util.Set; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.keycloak.testsuite.admin.ApiUtil.createUserAndResetPasswordWithAdminClient; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; @@ -120,7 +129,6 @@ public class AccountLinkTest extends AbstractKeycloakTest { String childIdp = CHILD_IDP; testAccountLink(childUsername, childPassword, childIdp); - } @Test @@ -133,7 +141,46 @@ public class AccountLinkTest extends AbstractKeycloakTest { } + @Test + public void testDeleteIdentityOnProviderRemoval() { + String childUsername = "child"; + String childPassword = "password"; + String childIdp = CHILD_IDP; + + assertFederatedIdentity(childUsername, childPassword, childIdp); + + RealmResource realm = adminClient.realm(CHILD_IDP); + UsersResource users = realm.users(); + List search = users.search(childUsername); + assertFalse(search.isEmpty()); + String userId = search.get(0).getId(); + List identities = users.get(userId).getFederatedIdentity(); + assertFalse(identities.isEmpty()); + + realm.identityProviders().get(PARENT_IDP).remove(); + + identities = users.get(userId).getFederatedIdentity(); + assertTrue(identities.isEmpty()); + + getTestingClient().server(CHILD_IDP).run(AccountLinkTest::checkEmptyFederatedIdentities); + } + + private static void checkEmptyFederatedIdentities(KeycloakSession session) { + RealmModel realm = session.getContext().getRealm(); + UserModel user = session.users().getUserByUsername("child", realm); + Set identities1 = session.users() + .getFederatedIdentities(user, realm); + assertTrue(identities1.isEmpty()); + assertNull(session.users().getFederatedIdentity(user, PARENT_IDP, realm)); + } + protected void testAccountLink(String childUsername, String childPassword, String childIdp) { + assertFederatedIdentity(childUsername, childPassword, childIdp); + assertRemoveFederatedIdentity(); + + } + + private void assertFederatedIdentity(String childUsername, String childPassword, String childIdp) { accountFederatedIdentityPage.realm(childIdp); accountFederatedIdentityPage.open(); loginPage.isCurrent(); @@ -161,12 +208,13 @@ public class AccountLinkTest extends AbstractKeycloakTest { System.out.println(driver.getPageSource()); assertTrue(accountFederatedIdentityPage.isCurrent()); assertTrue(driver.getPageSource().contains("id=\"remove-link-" + PARENT_IDP + "\"")); + } + private void assertRemoveFederatedIdentity() { // Unlink my "test-user" accountFederatedIdentityPage.clickRemoveProvider(PARENT_IDP); assertTrue(driver.getPageSource().contains("id=\"add-link-" + PARENT_IDP + "\"")); - // Logout from account management accountFederatedIdentityPage.logout();