From bb77ab4a812c87b5faa25383a637c034321da0b1 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Fri, 27 Jan 2017 17:37:08 -0500 Subject: [PATCH 1/2] account link tests --- ...ssThroughFederatedUserStorageProvider.java | 170 ++++++++++++++++ ...ghFederatedUserStorageProviderFactory.java | 40 ++++ ...eycloak.storage.UserStorageProviderFactory | 3 +- .../testsuite/broker/AccountLinkTest.java | 186 ++++++++++++++++++ .../testsuite/broker/BrokerTestTools.java | 51 +++++ 5 files changed, 449 insertions(+), 1 deletion(-) create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/PassThroughFederatedUserStorageProvider.java create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/PassThroughFederatedUserStorageProviderFactory.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AccountLinkTest.java 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 0000000000..2e993855e9 --- /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 Bill Burke + * @version $Revision: 1 $ + */ +public class PassThroughFederatedUserStorageProvider implements + UserStorageProvider, + UserLookupProvider, + CredentialInputValidator, + CredentialInputUpdater +{ + + public static final Set 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 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 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 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 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 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 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 0000000000..1b3cb55950 --- /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 Bill Burke + * @version $Revision: 1 $ + */ +public class PassThroughFederatedUserStorageProviderFactory implements UserStorageProviderFactory { + + 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 a97dd1ebb6..a9ae823e92 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 0000000000..7e796cd8a9 --- /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 Bill Burke + * @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 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 f021a366b9..c5b7b315af 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 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); + + + + } } From 0d308e2b69b8f2efaaa157e7b15cb6f6b1e2282b Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Tue, 31 Jan 2017 15:15:49 -0500 Subject: [PATCH 2/2] KEYCLOAK-4218 --- .../org/keycloak/models/jpa/RealmAdapter.java | 3 +- .../mongo/keycloak/adapters/RealmAdapter.java | 7 +- .../keycloak/models/utils/ComponentUtil.java | 10 +- .../keycloak/storage/OnUpdateComponent.java | 32 +++++ .../keycloak/storage/UserStorageManager.java | 14 +- .../federation/sync/SyncFederationTest.java | 131 ++++++++++++++++-- 6 files changed, 179 insertions(+), 18 deletions(-) create mode 100644 server-spi-private/src/main/java/org/keycloak/storage/OnUpdateComponent.java 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 32f34a5727..1a1c1e9253 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 { 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 { 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 62d3be8fb3..3854989a87 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 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 0d103124dc..6bd0a37639 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 0000000000..057a45b388 --- /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 Bill Burke + * @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 1134b86071..92e17bbeeb 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 Bill Burke * @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 4967bcc6c1..0394ced6fd 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,11 @@ public class SyncFederationTest { }); - KeycloakSession session = keycloakRule.startSession(); + 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 @@ -117,8 +122,8 @@ public class SyncFederationTest { // 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); } @@ -134,8 +139,114 @@ public class SyncFederationTest { }); } + /** + * Test that period sync is triggered when updating a synchronized User Storage Provider to have a non-negative sync period + * + */ @Test - public void test02ConcurrentSync() throws Exception { + 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 { + + // 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()); + 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); + } 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 + public void test03ConcurrentSync() throws Exception { SyncDummyUserFederationProviderFactory.restartLatches(); // Enable timer for SyncDummyUserFederationProvider