keycloak-memoizeit

Details

diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 32f34a5..1a1c1e9 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -1845,6 +1845,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
 
         ComponentEntity c = em.find(ComponentEntity.class, component.getId());
         if (c == null) return;
+        ComponentModel old = entityToModel(c);
         c.setName(component.getName());
         c.setProviderId(component.getProviderId());
         c.setProviderType(component.getProviderType());
@@ -1853,7 +1854,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
         em.createNamedQuery("deleteComponentConfigByComponent").setParameter("component", c).executeUpdate();
         em.flush();
         setConfig(component, c);
-        ComponentUtil.notifyUpdated(session, this, component);
+        ComponentUtil.notifyUpdated(session, this, old, component);
 
 
     }
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
index 62d3be8..3854989 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -1748,14 +1748,17 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
     public void updateComponent(ComponentModel model) {
         ComponentUtil.getComponentFactory(session, model).validateConfiguration(session, this, model);
 
+        ComponentModel old = null;
         for (ComponentEntity entity : realm.getComponentEntities()) {
             if (entity.getId().equals(model.getId())) {
+                old = entityToModel(entity);
                 updateComponentEntity(entity, model);
-
+                break;
             }
         }
+        if (old == null) return; // wasn't updated
         updateRealm();
-        ComponentUtil.notifyUpdated(session, this, model);
+        ComponentUtil.notifyUpdated(session, this, old, model);
 
     }
 
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java
index 0d10312..6bd0a37 100644
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java
@@ -26,6 +26,7 @@ import org.keycloak.provider.ProviderConfigProperty;
 import org.keycloak.provider.ProviderFactory;
 import org.keycloak.representations.idm.ComponentRepresentation;
 import org.keycloak.storage.OnCreateComponent;
+import org.keycloak.storage.OnUpdateComponent;
 import org.keycloak.storage.UserStorageProviderFactory;
 
 import java.util.HashMap;
@@ -94,9 +95,12 @@ public class ComponentUtil {
             ((OnCreateComponent)session.userStorageManager()).onCreate(session, realm, model);
         }
     }
-    public static void notifyUpdated(KeycloakSession session, RealmModel realm, ComponentModel model) {
-        ComponentFactory factory = getComponentFactory(session, model);
-        factory.onUpdate(session, realm, model);
+    public static void notifyUpdated(KeycloakSession session, RealmModel realm, ComponentModel oldModel, ComponentModel newModel) {
+        ComponentFactory factory = getComponentFactory(session, newModel);
+        factory.onUpdate(session, realm, newModel);
+        if (factory instanceof UserStorageProviderFactory) {
+            ((OnUpdateComponent)session.userStorageManager()).onUpdate(session, realm, oldModel, newModel);
+        }
     }
 
 }
diff --git a/server-spi-private/src/main/java/org/keycloak/storage/OnUpdateComponent.java b/server-spi-private/src/main/java/org/keycloak/storage/OnUpdateComponent.java
new file mode 100644
index 0000000..057a45b
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/storage/OnUpdateComponent.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.storage;
+
+import org.keycloak.component.ComponentModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+
+/**
+ * Callback for component update.  Only hardcoded classes like UserStorageManager implement it.  In future we
+ * may allow anybody to implement this interface.
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface OnUpdateComponent {
+    void onUpdate(KeycloakSession session, RealmModel realm, ComponentModel oldModel, ComponentModel newModel);
+}
diff --git a/services/src/main/java/org/keycloak/storage/UserStorageManager.java b/services/src/main/java/org/keycloak/storage/UserStorageManager.java
index 1134b86..92e17bb 100755
--- a/services/src/main/java/org/keycloak/storage/UserStorageManager.java
+++ b/services/src/main/java/org/keycloak/storage/UserStorageManager.java
@@ -60,7 +60,7 @@ import static org.keycloak.models.utils.KeycloakModelUtils.runJobInTransaction;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public class UserStorageManager implements UserProvider, OnUserCache, OnCreateComponent {
+public class UserStorageManager implements UserProvider, OnUserCache, OnCreateComponent, OnUpdateComponent {
 
     private static final Logger logger = Logger.getLogger(UserStorageManager.class);
 
@@ -640,6 +640,16 @@ public class UserStorageManager implements UserProvider, OnUserCache, OnCreateCo
 
     }
 
+    @Override
+    public void onUpdate(KeycloakSession session, RealmModel realm, ComponentModel oldModel, ComponentModel newModel) {
+        ComponentFactory factory = ComponentUtil.getComponentFactory(session, newModel);
+        if (!(factory instanceof UserStorageProviderFactory)) return;
+        UserStorageProviderModel old = new UserStorageProviderModel(oldModel);
+        UserStorageProviderModel newP= new UserStorageProviderModel(newModel);
+        if (old.getChangedSyncPeriod() != newP.getChangedSyncPeriod() || old.getFullSyncPeriod() != newP.getFullSyncPeriod()
+                || old.isImportEnabled() != newP.isImportEnabled()) {
+            new UserStorageSyncManager().notifyToRefreshPeriodicSync(session, realm, new UserStorageProviderModel(newModel), false);
+        }
 
-
+    }
 }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/sync/SyncFederationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/sync/SyncFederationTest.java
index 4967bcc..0394ced 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/sync/SyncFederationTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/sync/SyncFederationTest.java
@@ -61,9 +61,19 @@ public class SyncFederationTest {
     });
 
 
+    /**
+     * Test that period sync is triggered when creating a synchronized User Storage Provider
+     *
+     */
     @Test
-    public void test01PeriodicSync() {
+    public void test01PeriodicSyncOnCreate() {
 
+        KeycloakSession session = keycloakRule.startSession();
+        KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
+        DummyUserFederationProviderFactory dummyFedFactory = (DummyUserFederationProviderFactory) sessionFactory.getProviderFactory(UserStorageProvider.class, DummyUserFederationProviderFactory.PROVIDER_NAME);
+        int full = dummyFedFactory.getFullSyncCounter();
+        int changed = dummyFedFactory.getChangedSyncCounter();
+        keycloakRule.stopSession(session, false);
         // Enable timer for SyncDummyUserFederationProvider
         keycloakRule.update(new KeycloakRule.KeycloakSetup() {
 
@@ -81,16 +91,114 @@ public class SyncFederationTest {
 
         });
 
+        session = keycloakRule.startSession();
+        try {
+
+            // Assert that after some period was DummyUserFederationProvider triggered
+            UserStorageSyncManager usersSyncManager = new UserStorageSyncManager();
+            sleep(1800);
+
+            // Cancel timer
+            RealmModel appRealm = session.realms().getRealmByName("test");
+            usersSyncManager.notifyToRefreshPeriodicSync(session, appRealm, dummyModel, true);
+            log.infof("Notified sync manager about cancel periodic sync");
+
+            // This sync is here just to ensure that we have lock (doublecheck that periodic sync, which was possibly triggered before canceling timer is finished too)
+            while (true) {
+                SynchronizationResult result = usersSyncManager.syncChangedUsers(session.getKeycloakSessionFactory(), appRealm.getId(), dummyModel);
+                if (result.isIgnored()) {
+                    log.infof("Still waiting for lock before periodic sync is finished", result.toString());
+                    sleep(1000);
+                } else {
+                    break;
+                }
+            }
+
+            // Assert that DummyUserFederationProviderFactory.syncChangedUsers was invoked at least 2 times (once periodically and once for us)
+            int newChanged = dummyFedFactory.getChangedSyncCounter();
+            Assert.assertEquals(full, dummyFedFactory.getFullSyncCounter());
+            Assert.assertTrue("Assertion failed. newChanged=" + newChanged + ", changed=" + changed, newChanged > (changed + 1));
+
+            // Assert that dummy provider won't be invoked anymore
+            sleep(1800);
+            Assert.assertEquals(full, dummyFedFactory.getFullSyncCounter());
+            int newestChanged = dummyFedFactory.getChangedSyncCounter();
+            Assert.assertEquals("Assertion failed. newChanged=" + newChanged + ", newestChanged=" + newestChanged, newChanged, newestChanged);
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+
+        // remove dummyProvider
+        keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+
+            @Override
+            public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+                appRealm.removeComponent(dummyModel);
+            }
+
+        });
+    }
+
+    /**
+     * Test that period sync is triggered when updating a synchronized User Storage Provider to have a non-negative sync period
+     *
+     */
+    @Test
+    public void test02PeriodicSyncOnUpdate() {
+
         KeycloakSession session = keycloakRule.startSession();
+        KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
+        DummyUserFederationProviderFactory dummyFedFactory = (DummyUserFederationProviderFactory) sessionFactory.getProviderFactory(UserStorageProvider.class, DummyUserFederationProviderFactory.PROVIDER_NAME);
+        int full = dummyFedFactory.getFullSyncCounter();
+        int changed = dummyFedFactory.getChangedSyncCounter();
+        keycloakRule.stopSession(session, false);
+        // Enable timer for SyncDummyUserFederationProvider
+        keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+
+            @Override
+            public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+                UserStorageProviderModel model = new UserStorageProviderModel();
+                model.setProviderId(DummyUserFederationProviderFactory.PROVIDER_NAME);
+                model.setPriority(1);
+                model.setName("test-sync-dummy");
+                model.setFullSyncPeriod(-1);
+                model.setChangedSyncPeriod(-1);
+                model.setLastSync(0);
+                dummyModel = new UserStorageProviderModel(appRealm.addComponentModel(model));
+            }
+
+        });
+
+        session = keycloakRule.startSession();
+        try {
+
+            // Assert that after some period was DummyUserFederationProvider triggered
+            UserStorageSyncManager usersSyncManager = new UserStorageSyncManager();
+            // Assert that dummy provider won't be invoked anymore
+            sleep(1800);
+            Assert.assertEquals(full, dummyFedFactory.getFullSyncCounter());
+            int newestChanged = dummyFedFactory.getChangedSyncCounter();
+            Assert.assertEquals("Assertion failed. newChanged=" + changed + ", newestChanged=" + newestChanged, changed, newestChanged);
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+
+        keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+
+            @Override
+            public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+                dummyModel.setChangedSyncPeriod(1);
+                appRealm.updateComponent(dummyModel);
+            }
+
+        });
+
+
+        session = keycloakRule.startSession();
         try {
-            KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
-            DummyUserFederationProviderFactory dummyFedFactory = (DummyUserFederationProviderFactory)sessionFactory.getProviderFactory(UserStorageProvider.class, DummyUserFederationProviderFactory.PROVIDER_NAME);
-            int full = dummyFedFactory.getFullSyncCounter();
-            int changed = dummyFedFactory.getChangedSyncCounter();
 
             // Assert that after some period was DummyUserFederationProvider triggered
             UserStorageSyncManager usersSyncManager = new UserStorageSyncManager();
-            usersSyncManager.bootstrapPeriodic(sessionFactory, session.getProvider(TimerProvider.class));
             sleep(1800);
 
             // Cancel timer
@@ -112,17 +220,19 @@ public class SyncFederationTest {
             // Assert that DummyUserFederationProviderFactory.syncChangedUsers was invoked at least 2 times (once periodically and once for us)
             int newChanged = dummyFedFactory.getChangedSyncCounter();
             Assert.assertEquals(full, dummyFedFactory.getFullSyncCounter());
+            log.info("Asserting. newChanged=" + newChanged + " > changed=" + changed);
             Assert.assertTrue("Assertion failed. newChanged=" + newChanged + ", changed=" + changed, newChanged > (changed + 1));
 
             // Assert that dummy provider won't be invoked anymore
             sleep(1800);
             Assert.assertEquals(full, dummyFedFactory.getFullSyncCounter());
-            int newestChanged =  dummyFedFactory.getChangedSyncCounter();
-            Assert.assertEquals("Assertion failed. newChanged=" + newChanged + ", newestChanged="  + newestChanged, newChanged, newestChanged);
+            int newestChanged = dummyFedFactory.getChangedSyncCounter();
+            Assert.assertEquals("Assertion failed. newChanged=" + newChanged + ", newestChanged=" + newestChanged, newChanged, newestChanged);
         } finally {
             keycloakRule.stopSession(session, true);
         }
 
+
         // remove dummyProvider
         keycloakRule.update(new KeycloakRule.KeycloakSetup() {
 
@@ -134,8 +244,9 @@ public class SyncFederationTest {
         });
     }
 
+
     @Test
-    public void test02ConcurrentSync() throws Exception {
+    public void test03ConcurrentSync() throws Exception {
         SyncDummyUserFederationProviderFactory.restartLatches();
 
         // Enable timer for SyncDummyUserFederationProvider
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/PassThroughFederatedUserStorageProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/PassThroughFederatedUserStorageProvider.java
new file mode 100644
index 0000000..2e99385
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/PassThroughFederatedUserStorageProvider.java
@@ -0,0 +1,170 @@
+/*
+ * 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.credential.CredentialInput;
+import org.keycloak.credential.CredentialInputUpdater;
+import org.keycloak.credential.CredentialInputValidator;
+import org.keycloak.credential.CredentialModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.storage.StorageId;
+import org.keycloak.storage.UserStorageProvider;
+import org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage;
+import org.keycloak.storage.user.UserLookupProvider;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Provides one user where everything is stored in user federated storage
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class PassThroughFederatedUserStorageProvider implements
+        UserStorageProvider,
+        UserLookupProvider,
+        CredentialInputValidator,
+        CredentialInputUpdater
+{
+
+    public static final Set<String> CREDENTIAL_TYPES = Collections.singleton(UserCredentialModel.PASSWORD);
+    public static final String PASSTHROUGH_USERNAME = "passthrough";
+    public static final String INITIAL_PASSWORD = "secret";
+    private KeycloakSession session;
+    private ComponentModel component;
+
+    public PassThroughFederatedUserStorageProvider(KeycloakSession session, ComponentModel component) {
+        this.session = session;
+        this.component = component;
+    }
+
+    public Set<String> getSupportedCredentialTypes() {
+        return CREDENTIAL_TYPES;
+    }
+
+    @Override
+    public boolean supportsCredentialType(String credentialType) {
+        return getSupportedCredentialTypes().contains(credentialType);
+    }
+
+    @Override
+    public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
+        if (!CredentialModel.PASSWORD.equals(credentialType)) return false;
+        return true;
+    }
+
+    @Override
+    public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
+        UserCredentialModel password = (UserCredentialModel)input;
+        if (password.getType().equals(UserCredentialModel.PASSWORD)) {
+             if (INITIAL_PASSWORD.equals(password.getValue())) {
+                 return true;
+             }
+            List<CredentialModel> existing = session.userFederatedStorage().getStoredCredentialsByType(realm, user.getId(), "CLEAR_TEXT_PASSWORD");
+            if (existing.isEmpty()) return false;
+            return existing.get(0).getConfig().getFirst("VALUE").equals(password.getValue());
+        }
+        return false;
+    }
+
+    @Override
+    public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
+        // testing federated credential attributes
+        UserCredentialModel password = (UserCredentialModel)input;
+        if (password.getType().equals(UserCredentialModel.PASSWORD)) {
+            List<CredentialModel> existing = session.userFederatedStorage().getStoredCredentialsByType(realm, user.getId(), "CLEAR_TEXT_PASSWORD");
+            if (existing.isEmpty()) {
+                CredentialModel model = new CredentialModel();
+                model.setType("CLEAR_TEXT_PASSWORD");
+                model.getConfig().putSingle("VALUE", password.getValue());
+                session.userFederatedStorage().createCredential(realm, user.getId(), model);
+            } else {
+                CredentialModel model = existing.get(0);
+                model.setType("CLEAR_TEXT_PASSWORD");
+                model.getConfig().putSingle("VALUE", password.getValue());
+                session.userFederatedStorage().updateCredential(realm, user.getId(), model);
+
+            }
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {
+        List<CredentialModel> existing = session.userFederatedStorage().getStoredCredentialsByType(realm, user.getId(), "CLEAR_TEXT_PASSWORD");
+        for (CredentialModel model : existing) {
+            session.userFederatedStorage().removeStoredCredential(realm, user.getId(), model.getId());
+        }
+    }
+
+    @Override
+    public Set<String> getDisableableCredentialTypes(RealmModel realm, UserModel user) {
+        return CREDENTIAL_TYPES;
+    }
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public UserModel getUserById(String id, RealmModel realm) {
+        if (!StorageId.externalId(id).equals(PASSTHROUGH_USERNAME)) return null;
+        return getUserModel(realm);
+    }
+
+    @Override
+    public UserModel getUserByUsername(String username, RealmModel realm) {
+        if  (!PASSTHROUGH_USERNAME.equals(username)) return null;
+
+        return getUserModel(realm);
+    }
+
+    @Override
+    public UserModel getUserByEmail(String email, RealmModel realm) {
+        List<String> list = session.userFederatedStorage().getUsersByUserAttribute(realm, AbstractUserAdapterFederatedStorage.EMAIL_ATTRIBUTE, email);
+        for (String user : list) {
+            StorageId storageId = new StorageId(user);
+            if (!storageId.getExternalId().equals(PASSTHROUGH_USERNAME)) continue;
+            if (!storageId.getProviderId().equals(component.getId())) continue;
+            return getUserModel(realm);
+
+        }
+        return null;
+    }
+
+    private UserModel getUserModel(final RealmModel realm) {
+        return new AbstractUserAdapterFederatedStorage(session, realm, component) {
+            @Override
+            public String getUsername() {
+                return PASSTHROUGH_USERNAME;
+            }
+
+            @Override
+            public void setUsername(String username) {
+
+            }
+        };
+    }
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/PassThroughFederatedUserStorageProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/PassThroughFederatedUserStorageProviderFactory.java
new file mode 100644
index 0000000..1b3cb55
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/PassThroughFederatedUserStorageProviderFactory.java
@@ -0,0 +1,40 @@
+/*
+ * 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.storage.UserStorageProviderFactory;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class PassThroughFederatedUserStorageProviderFactory implements UserStorageProviderFactory<PassThroughFederatedUserStorageProvider> {
+
+    public static final String PROVIDER_ID = "pass-through-federated";
+
+    @Override
+    public PassThroughFederatedUserStorageProvider create(KeycloakSession session, ComponentModel model) {
+        return new PassThroughFederatedUserStorageProvider(session, model);
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory
index a97dd1e..a9ae823 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory
@@ -1 +1,2 @@
-org.keycloak.testsuite.federation.DummyUserFederationProviderFactory
\ No newline at end of file
+org.keycloak.testsuite.federation.DummyUserFederationProviderFactory
+org.keycloak.testsuite.federation.PassThroughFederatedUserStorageProviderFactory
\ No newline at end of file
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
new file mode 100644
index 0000000..7e796cd
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AccountLinkTest.java
@@ -0,0 +1,186 @@
+/*
+ * 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.broker;
+
+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.common.util.MultivaluedHashMap;
+import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.representations.idm.ComponentRepresentation;
+import org.keycloak.representations.idm.IdentityProviderRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.storage.UserStorageProvider;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.federation.PassThroughFederatedUserStorageProvider;
+import org.keycloak.testsuite.federation.PassThroughFederatedUserStorageProviderFactory;
+import org.keycloak.testsuite.pages.AccountFederatedIdentityPage;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.UpdateAccountInformationPage;
+
+import java.util.List;
+
+import static org.junit.Assert.assertTrue;
+import static org.keycloak.testsuite.admin.ApiUtil.createUserAndResetPasswordWithAdminClient;
+import static org.keycloak.testsuite.admin.ApiUtil.createUserWithAdminClient;
+import static org.keycloak.testsuite.broker.BrokerTestConstants.REALM_PROV_NAME;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class AccountLinkTest extends AbstractKeycloakTest {
+    public static final String CHILD_IDP = "child";
+    public static final String PARENT_IDP = "parent-idp";
+    public static final String PARENT_USERNAME = "parent";
+
+    @Page
+    protected AccountFederatedIdentityPage accountFederatedIdentityPage;
+
+    @Page
+    protected UpdateAccountInformationPage profilePage;
+
+    @Page
+    protected LoginPage loginPage;
+
+    @Override
+    public void addTestRealms(List<RealmRepresentation> testRealms) {
+        RealmRepresentation realm = new RealmRepresentation();
+        realm.setRealm(CHILD_IDP);
+        realm.setEnabled(true);
+        testRealms.add(realm);
+
+        realm = new RealmRepresentation();
+        realm.setRealm(PARENT_IDP);
+        realm.setEnabled(true);
+
+        testRealms.add(realm);
+
+    }
+
+    @Before
+    public void addIdpUser() {
+        RealmResource realm = adminClient.realms().realm(PARENT_IDP);
+        UserRepresentation user = new UserRepresentation();
+        user.setUsername(PARENT_USERNAME);
+        user.setEnabled(true);
+        String userId = createUserAndResetPasswordWithAdminClient(realm, user, "password");
+
+    }
+
+    @Before
+    public void addChildUser() {
+        RealmResource realm = adminClient.realms().realm(CHILD_IDP);
+        UserRepresentation user = new UserRepresentation();
+        user.setUsername("child");
+        user.setEnabled(true);
+        String userId = createUserAndResetPasswordWithAdminClient(realm, user, "password");
+
+    }
+
+    @Before
+    public void setupUserStorageProvider() {
+        ComponentRepresentation provider = new ComponentRepresentation();
+        provider.setName("passthrough");
+        provider.setProviderId(PassThroughFederatedUserStorageProviderFactory.PROVIDER_ID);
+        provider.setProviderType(UserStorageProvider.class.getName());
+        provider.setConfig(new MultivaluedHashMap<>());
+        provider.getConfig().putSingle("priority", Integer.toString(1));
+
+        RealmResource realm = adminClient.realms().realm(CHILD_IDP);
+        realm.components().add(provider);
+
+
+
+
+    }
+
+    @Before
+    public void createBroker() {
+        createParentChild();
+    }
+
+    public void createParentChild() {
+        BrokerTestTools.createKcOidcBroker(adminClient, CHILD_IDP, PARENT_IDP, suiteContext);
+    }
+
+    @Test
+    public void testAccountLink() {
+        String childUsername = "child";
+        String childPassword = "password";
+        String childIdp = CHILD_IDP;
+
+        testAccountLink(childUsername, childPassword, childIdp);
+
+    }
+
+    @Test
+    public void testAccountLinkWithUserStorageProvider() {
+        String childUsername = PassThroughFederatedUserStorageProvider.PASSTHROUGH_USERNAME;
+        String childPassword = PassThroughFederatedUserStorageProvider.INITIAL_PASSWORD;
+        String childIdp = CHILD_IDP;
+
+        testAccountLink(childUsername, childPassword, childIdp);
+
+    }
+
+    protected void testAccountLink(String childUsername, String childPassword, String childIdp) {
+        accountFederatedIdentityPage.realm(childIdp);
+        accountFederatedIdentityPage.open();
+        loginPage.isCurrent();
+        loginPage.login(childUsername, childPassword);
+        assertTrue(accountFederatedIdentityPage.isCurrent());
+
+        accountFederatedIdentityPage.clickAddProvider(PARENT_IDP);
+
+        this.loginPage.isCurrent();
+        loginPage.login(PARENT_USERNAME, "password");
+
+        // Assert identity linked in account management
+        assertTrue(accountFederatedIdentityPage.isCurrent());
+        assertTrue(driver.getPageSource().contains("id=\"remove-" + PARENT_IDP + "\""));
+
+        // Logout from account management
+        accountFederatedIdentityPage.logout();
+
+        // Assert I am logged immediately to account management due to previously linked "test-user" identity
+        loginPage.isCurrent();
+        loginPage.clickSocial(PARENT_IDP);
+        loginPage.login(PARENT_USERNAME, "password");
+        System.out.println(driver.getCurrentUrl());
+        System.out.println("--------------------------------");
+        System.out.println(driver.getPageSource());
+        assertTrue(accountFederatedIdentityPage.isCurrent());
+        assertTrue(driver.getPageSource().contains("id=\"remove-" + PARENT_IDP + "\""));
+
+        // Unlink my "test-user"
+        accountFederatedIdentityPage.clickRemoveProvider(PARENT_IDP);
+        assertTrue(driver.getPageSource().contains("id=\"add-" + PARENT_IDP + "\""));
+
+
+        // Logout from account management
+        accountFederatedIdentityPage.logout();
+
+        this.loginPage.clickSocial(PARENT_IDP);
+        this.loginPage.login(PARENT_USERNAME, "password");
+        this.profilePage.assertCurrent();
+    }
+
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/BrokerTestTools.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/BrokerTestTools.java
index f021a36..c5b7b31 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/BrokerTestTools.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/BrokerTestTools.java
@@ -1,17 +1,29 @@
 package org.keycloak.testsuite.broker;
 
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.representations.idm.ClientRepresentation;
 import org.keycloak.representations.idm.IdentityProviderRepresentation;
 import org.keycloak.testsuite.arquillian.SuiteContext;
 
 import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+
 import org.openqa.selenium.By;
 import org.openqa.selenium.WebDriver;
 import org.openqa.selenium.WebElement;
 import org.openqa.selenium.support.ui.ExpectedCondition;
 import org.openqa.selenium.support.ui.WebDriverWait;
 
+import static org.keycloak.testsuite.broker.BrokerTestConstants.CLIENT_ID;
+import static org.keycloak.testsuite.broker.BrokerTestConstants.CLIENT_SECRET;
+import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_ALIAS;
+import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_PROVIDER_ID;
+import static org.keycloak.testsuite.broker.BrokerTestConstants.REALM_CONS_NAME;
+import static org.keycloak.testsuite.broker.BrokerTestConstants.REALM_PROV_NAME;
+
 /**
  *
  * @author hmlnarik
@@ -62,4 +74,43 @@ public class BrokerTestTools {
 
         return result;
     }
+
+    /**
+     * Expects a child idp and parent idp running on same Keycloak instance.  Links the two with non-signature checks.
+     *
+     * @param adminClient
+     * @param childRealm
+     * @param idpRealm
+     * @param suiteContext
+     */
+    public static void createKcOidcBroker(Keycloak adminClient, String childRealm, String idpRealm, SuiteContext suiteContext) {
+        IdentityProviderRepresentation idp = createIdentityProvider(idpRealm, IDP_OIDC_PROVIDER_ID);
+        Map<String, String> config = idp.getConfig();
+
+        config.put("clientId", childRealm);
+        config.put("clientSecret", childRealm);
+        config.put("prompt", "login");
+        config.put("authorizationUrl", getAuthRoot(suiteContext) + "/auth/realms/" + idpRealm + "/protocol/openid-connect/auth");
+        config.put("tokenUrl", getAuthRoot(suiteContext) + "/auth/realms/" + idpRealm + "/protocol/openid-connect/token");
+        config.put("logoutUrl", getAuthRoot(suiteContext) + "/auth/realms/" + idpRealm + "/protocol/openid-connect/logout");
+        config.put("userInfoUrl", getAuthRoot(suiteContext) + "/auth/realms/" + idpRealm + "/protocol/openid-connect/userinfo");
+        config.put("backchannelSupported", "true");
+        adminClient.realm(childRealm).identityProviders().create(idp);
+
+        ClientRepresentation client = new ClientRepresentation();
+        client.setClientId(childRealm);
+        client.setName(childRealm);
+        client.setSecret(childRealm);
+        client.setEnabled(true);
+
+        client.setRedirectUris(Collections.singletonList(getAuthRoot(suiteContext) +
+                "/auth/realms/" + childRealm + "/broker/" + idpRealm + "/endpoint/*"));
+
+        client.setAdminUrl(getAuthRoot(suiteContext) +
+                "/auth/realms/" + childRealm + "/broker/" + idpRealm + "/endpoint");
+        adminClient.realm(idpRealm).clients().create(client);
+
+
+
+    }
 }