From 5a015a65180746e7082f8abf03d312d3952ad394 Mon Sep 17 00:00:00 2001 From: mposolda Date: Wed, 7 Sep 2016 15:34:31 +0200 Subject: [PATCH] KEYCLOAK-3494 Input elements backed by user attributes fail to update in themes --- .../org/keycloak/models/jpa/UserAdapter.java | 34 ++-- .../jpa/entities/UserAttributeEntity.java | 1 + .../java/org/keycloak/models/Constants.java | 2 +- .../account/freemarker/model/AccountBean.java | 5 +- .../common/keycloak-server-subsystem.xsl | 13 ++ .../resources/META-INF/keycloak-themes.json | 6 + .../theme/address/account/account.ftl | 114 +++++++++++++ .../theme/address/account/theme.properties | 18 +++ .../resources/partials/user-attributes.html | 72 +++++++++ .../theme/address/admin/theme.properties | 18 +++ .../address/login/login-update-profile.ftl | 95 +++++++++++ .../theme/address/login/register.ftl | 131 +++++++++++++++ .../theme/address/login/theme.properties | 18 +++ .../pages/AccountUpdateProfilePage.java | 20 +++ .../account/custom/CustomThemeTest.java | 86 ++++++++++ testsuite/integration/pom.xml | 8 + .../DummyUserFederationProvider.java | 150 ------------------ .../DummyUserFederationProviderFactory.java | 132 --------------- .../AbstractKeycloakIdentityProviderTest.java | 2 +- ...yncDummyUserFederationProviderFactory.java | 2 +- .../federation/sync/SyncFederationTest.java | 2 +- .../model/ConcurrentTransactionsTest.java | 18 ++- .../testsuite/model/UserModelTest.java | 25 +++ ...cloak.models.UserFederationProviderFactory | 1 - 24 files changed, 669 insertions(+), 304 deletions(-) create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/keycloak-themes.json create mode 100755 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/account/account.ftl create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/account/theme.properties create mode 100755 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/admin/resources/partials/user-attributes.html create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/admin/theme.properties create mode 100755 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/login/login-update-profile.ftl create mode 100755 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/login/register.ftl create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/login/theme.properties create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomThemeTest.java delete mode 100755 testsuite/integration/src/test/java/org/keycloak/testsuite/DummyUserFederationProvider.java delete mode 100755 testsuite/integration/src/test/java/org/keycloak/testsuite/DummyUserFederationProviderFactory.java diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java index 2ea12fd839..49bcac633b 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java @@ -125,29 +125,32 @@ public class UserAdapter implements UserModel, JpaModel { @Override public void setSingleAttribute(String name, String value) { - boolean found = false; + String firstExistingAttrId = null; List toRemove = new ArrayList<>(); for (UserAttributeEntity attr : user.getAttributes()) { if (attr.getName().equals(name)) { - if (!found) { + if (firstExistingAttrId == null) { attr.setValue(value); - found = true; + firstExistingAttrId = attr.getId(); } else { toRemove.add(attr); } } } - for (UserAttributeEntity attr : toRemove) { - em.remove(attr); - user.getAttributes().remove(attr); - } + if (firstExistingAttrId != null) { + // Remove attributes through HQL to avoid StaleUpdateException + Query query = em.createNamedQuery("deleteUserAttributesOtherThan"); + query.setParameter("attrId", firstExistingAttrId); + query.setParameter("userId", user.getId()); + int numUpdated = query.executeUpdate(); - if (found) { - return; - } + // Remove attribute from local entity + user.getAttributes().removeAll(toRemove); + } else { - persistAttributeValue(name, value); + persistAttributeValue(name, value); + } } @Override @@ -178,6 +181,15 @@ public class UserAdapter implements UserModel, JpaModel { query.setParameter("name", name); query.setParameter("userId", user.getId()); int numUpdated = query.executeUpdate(); + + // KEYCLOAK-3494 : Also remove attributes from local user entity + List toRemove = new ArrayList<>(); + for (UserAttributeEntity attr : user.getAttributes()) { + if (attr.getName().equals(name)) { + toRemove.add(attr); + } + } + user.getAttributes().removeAll(toRemove); } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserAttributeEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserAttributeEntity.java index 2b5d35cc63..16b155b75b 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserAttributeEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserAttributeEntity.java @@ -43,6 +43,7 @@ import java.util.Set; @NamedQuery(name="getAttributesByNameAndValue", query="select attr from UserAttributeEntity attr where attr.name = :name and attr.value = :value"), @NamedQuery(name="deleteUserAttributesByRealm", query="delete from UserAttributeEntity attr where attr.user IN (select u from UserEntity u where u.realmId=:realmId)"), @NamedQuery(name="deleteUserAttributesByNameAndUser", query="delete from UserAttributeEntity attr where attr.user.id = :userId and attr.name = :name"), + @NamedQuery(name="deleteUserAttributesOtherThan", query="delete from UserAttributeEntity attr where attr.user.id = :userId and attr.id <> :attrId"), @NamedQuery(name="deleteUserAttributesByRealmAndLink", query="delete from UserAttributeEntity attr where attr.user IN (select u from UserEntity u where u.realmId=:realmId and u.federationLink=:link)") }) @Table(name="USER_ATTRIBUTE") diff --git a/server-spi/src/main/java/org/keycloak/models/Constants.java b/server-spi/src/main/java/org/keycloak/models/Constants.java index 916565aaa4..42982f6ace 100755 --- a/server-spi/src/main/java/org/keycloak/models/Constants.java +++ b/server-spi/src/main/java/org/keycloak/models/Constants.java @@ -50,5 +50,5 @@ public interface Constants { String KEY = "key"; // Prefix for user attributes used in various "context"data maps - public static final String USER_ATTRIBUTES_PREFIX = "user.attributes."; + String USER_ATTRIBUTES_PREFIX = "user.attributes."; } diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/model/AccountBean.java b/services/src/main/java/org/keycloak/forms/account/freemarker/model/AccountBean.java index acb1514388..857bfc04d9 100755 --- a/services/src/main/java/org/keycloak/forms/account/freemarker/model/AccountBean.java +++ b/services/src/main/java/org/keycloak/forms/account/freemarker/model/AccountBean.java @@ -18,6 +18,7 @@ package org.keycloak.forms.account.freemarker.model; import org.jboss.logging.Logger; +import org.keycloak.models.Constants; import org.keycloak.models.UserModel; import javax.ws.rs.core.MultivaluedMap; @@ -55,8 +56,8 @@ public class AccountBean { if (profileFormData != null) { for (String key : profileFormData.keySet()) { - if (key.startsWith("user.attributes.")) { - String attribute = key.substring("user.attributes.".length()); + if (key.startsWith(Constants.USER_ATTRIBUTES_PREFIX)) { + String attribute = key.substring(Constants.USER_ATTRIBUTES_PREFIX.length()); attributes.put(attribute, profileFormData.getFirst(key)); } } diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/keycloak-server-subsystem.xsl b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keycloak-server-subsystem.xsl index f32c036600..664eecb1ae 100644 --- a/testsuite/integration-arquillian/servers/auth-server/jboss/common/keycloak-server-subsystem.xsl +++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keycloak-server-subsystem.xsl @@ -36,6 +36,11 @@ + + + org.keycloak.testsuite.integration-arquillian-testsuite-providers + + @@ -46,6 +51,14 @@ module:org.keycloak.testsuite.integration-arquillian-testsuite-providers + + + + + + + + diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/keycloak-themes.json b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/keycloak-themes.json new file mode 100644 index 0000000000..03978db442 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/keycloak-themes.json @@ -0,0 +1,6 @@ +{ + "themes": [{ + "name" : "address", + "types": [ "admin", "account", "login" ] + }] +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/account/account.ftl b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/account/account.ftl new file mode 100755 index 0000000000..d2a6af16e0 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/account/account.ftl @@ -0,0 +1,114 @@ +<#import "template.ftl" as layout> +<@layout.mainLayout active='account' bodyClass='user'; section> + +
+
+

${msg("editAccountHtmlTtile")}

+
+
+ * ${msg("requiredFields")} +
+
+ +
+ + + +
+
+ <#if realm.editUsernameAllowed>* +
+ +
+ disabled="disabled" value="${(account.username!'')?html}"/> +
+
+ +
+
+ * +
+ +
+ +
+
+ +
+
+ * +
+ +
+ +
+
+ +
+
+ * +
+ +
+ +
+
+ +
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+ + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/account/theme.properties b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/account/theme.properties new file mode 100644 index 0000000000..3e50437b9a --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/account/theme.properties @@ -0,0 +1,18 @@ +# +# 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. +# + +parent=keycloak \ No newline at end of file diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/admin/resources/partials/user-attributes.html b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/admin/resources/partials/user-attributes.html new file mode 100755 index 0000000000..af512de26e --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/admin/resources/partials/user-attributes.html @@ -0,0 +1,72 @@ + + +
+ + + + +
+
+ +
+ +
+ Street address. +
+
+ +
+ +
+ City or locality. +
+
+ +
+ +
+ State, province, prefecture, or region. +
+
+ +
+ +
+ Zip code or postal code. +
+
+ +
+ +
+ Country name. +
+ +
+
+ + +
+
+
+
+ + diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/admin/theme.properties b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/admin/theme.properties new file mode 100644 index 0000000000..3e50437b9a --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/admin/theme.properties @@ -0,0 +1,18 @@ +# +# 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. +# + +parent=keycloak \ No newline at end of file diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/login/login-update-profile.ftl b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/login/login-update-profile.ftl new file mode 100755 index 0000000000..e02a340405 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/login/login-update-profile.ftl @@ -0,0 +1,95 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout; section> + <#if section = "title"> + ${msg("loginProfileTitle")} + <#elseif section = "header"> + ${msg("loginProfileTitle")} + <#elseif section = "form"> +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+ + +
+
+
+
+
+ +
+ +
+
+
+ + \ No newline at end of file diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/login/register.ftl b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/login/register.ftl new file mode 100755 index 0000000000..3247305cf9 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/login/register.ftl @@ -0,0 +1,131 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout; section> + <#if section = "title"> + ${msg("registerWithTitle",(realm.name!''))} + <#elseif section = "header"> + ${msg("registerWithTitleHtml",(realm.name!''))} + <#elseif section = "form"> +
+ <#if !realm.registrationEmailAsUsername> +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + <#if passwordRequired> +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+ <#if recaptchaRequired??> +
+
+
+
+
+ + +
+ + +
+ +
+
+
+ + \ No newline at end of file diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/login/theme.properties b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/login/theme.properties new file mode 100644 index 0000000000..3e50437b9a --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/theme/address/login/theme.properties @@ -0,0 +1,18 @@ +# +# 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. +# + +parent=keycloak \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java index 2159c2f6ff..2a77847fac 100755 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java @@ -17,7 +17,9 @@ package org.keycloak.testsuite.pages; +import org.keycloak.models.Constants; import org.keycloak.services.resources.RealmsResource; +import org.openqa.selenium.By; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; @@ -96,6 +98,14 @@ public class AccountUpdateProfilePage extends AbstractAccountPage { submitButton.click(); } + public void updateAttribute(String attrName, String attrValue) { + WebElement attrElement = findAttributeInputElement(attrName); + attrElement.clear(); + attrElement.sendKeys(attrValue); + submitButton.click(); + } + + public void clickCancel() { cancelButton.click(); } @@ -117,6 +127,11 @@ public class AccountUpdateProfilePage extends AbstractAccountPage { return emailInput.getAttribute("value"); } + public String getAttribute(String attrName) { + WebElement attrElement = findAttributeInputElement(attrName); + return attrElement.getAttribute("value"); + } + public boolean isCurrent() { return driver.getTitle().contains("Account Management") && driver.getPageSource().contains("Edit Account"); } @@ -140,4 +155,9 @@ public class AccountUpdateProfilePage extends AbstractAccountPage { public boolean isPasswordUpdateSupported() { return driver.getPageSource().contains(getPath() + "/password"); } + + private WebElement findAttributeInputElement(String attrName) { + String attrId = Constants.USER_ATTRIBUTES_PREFIX + attrName; + return driver.findElement(By.id(attrId)); + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomThemeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomThemeTest.java new file mode 100644 index 0000000000..810d9c2084 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomThemeTest.java @@ -0,0 +1,86 @@ +/* + * 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.account.custom; + +import org.jboss.arquillian.graphene.page.Page; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.events.Details; +import org.keycloak.events.EventType; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.TestRealmKeycloakTest; +import org.keycloak.testsuite.account.AccountTest; +import org.keycloak.testsuite.pages.AccountUpdateProfilePage; +import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.util.RealmBuilder; +import org.keycloak.testsuite.util.UserBuilder; + +/** + * @author Marek Posolda + */ +public class CustomThemeTest extends TestRealmKeycloakTest { + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + testRealm.setAccountTheme("address"); + + UserRepresentation user2 = UserBuilder.create() + .enabled(true) + .username("test-user-no-access@localhost") + .email("test-user-no-access@localhost") + .password("password") + .build(); + + RealmBuilder.edit(testRealm) + .user(user2); + } + + @Rule + public AssertEvents events = new AssertEvents(this); + + @Page + protected LoginPage loginPage; + + @Page + protected AccountUpdateProfilePage profilePage; + + // KEYCLOAK-3494 + @Test + public void changeProfile() throws Exception { + profilePage.open(); + loginPage.login("test-user@localhost", "password"); + + events.expectLogin().client("account").detail(Details.REDIRECT_URI, AccountTest.ACCOUNT_REDIRECT).assertEvent(); + + Assert.assertEquals("test-user@localhost", profilePage.getEmail()); + Assert.assertEquals("", profilePage.getAttribute("street")); + + profilePage.updateAttribute("street", "Elm 1"); + Assert.assertEquals("Elm 1", profilePage.getAttribute("street")); + + profilePage.updateAttribute("street", "Elm 2"); + Assert.assertEquals("Elm 2", profilePage.getAttribute("street")); + + events.expectAccount(EventType.UPDATE_PROFILE).assertEvent(); + } + + +} diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml index aca1d78278..92187bc456 100755 --- a/testsuite/integration/pom.xml +++ b/testsuite/integration/pom.xml @@ -166,6 +166,14 @@ org.keycloak federation-properties-example + + + + org.keycloak.testsuite + integration-arquillian-testsuite-providers + ${project.version} + + org.jboss.logging jboss-logging diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/DummyUserFederationProvider.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/DummyUserFederationProvider.java deleted file mode 100755 index 0669da47db..0000000000 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/DummyUserFederationProvider.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.testsuite; - -import org.keycloak.models.CredentialValidationOutput; -import org.keycloak.models.GroupModel; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RoleModel; -import org.keycloak.models.UserCredentialModel; -import org.keycloak.models.UserFederationProvider; -import org.keycloak.models.UserModel; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class DummyUserFederationProvider implements UserFederationProvider { - - private final Map users; - - public DummyUserFederationProvider(Map users) { - this.users = users; - } - - @Override - public UserModel validateAndProxy(RealmModel realm, UserModel local) { - return local; - } - - @Override - public boolean synchronizeRegistrations() { - return true; - } - - @Override - public UserModel register(RealmModel realm, UserModel user) { - users.put(user.getUsername(), user); - return user; - } - - @Override - public boolean removeUser(RealmModel realm, UserModel user) { - return users.remove(user.getUsername()) != null; - } - - @Override - public UserModel getUserByUsername(RealmModel realm, String username) { - return users.get(username); - } - - @Override - public UserModel getUserByEmail(RealmModel realm, String email) { - return null; - } - - @Override - public List searchByAttributes(Map attributes, RealmModel realm, int maxResults) { - return Collections.emptyList(); - } - - @Override - public List getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) { - return Collections.emptyList(); - } - - @Override - public void preRemove(RealmModel realm) { - - } - - @Override - public void preRemove(RealmModel realm, RoleModel role) { - - } - - @Override - public void preRemove(RealmModel realm, GroupModel group) { - - } - - @Override - public boolean isValid(RealmModel realm, UserModel local) { - String username = local.getUsername(); - return users.containsKey(username); - } - - @Override - public Set getSupportedCredentialTypes(UserModel user) { - // Just user "test-user" is able to validate password with this federationProvider - if (user.getUsername().equals("test-user")) { - return Collections.singleton(UserCredentialModel.PASSWORD); - } else { - return Collections.emptySet(); - } - } - - @Override - public Set getSupportedCredentialTypes() { - return Collections.singleton(UserCredentialModel.PASSWORD); - } - - @Override - public boolean validCredentials(RealmModel realm, UserModel user, List input) { - if (user.getUsername().equals("test-user") && input.size() == 1) { - UserCredentialModel password = input.get(0); - if (password.getType().equals(UserCredentialModel.PASSWORD)) { - return "secret".equals(password.getValue()); - } - } - return false; - } - - @Override - public boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input) { - return validCredentials(realm, user, Arrays.asList(input)); - } - - @Override - public CredentialValidationOutput validCredentials(RealmModel realm, UserCredentialModel credential) { - return CredentialValidationOutput.failed(); - } - - @Override - public void close() { - - } -} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/DummyUserFederationProviderFactory.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/DummyUserFederationProviderFactory.java deleted file mode 100755 index 4b49499137..0000000000 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/DummyUserFederationProviderFactory.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.testsuite; - -import org.jboss.logging.Logger; -import org.keycloak.Config; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.models.UserFederationProvider; -import org.keycloak.models.UserFederationProviderFactory; -import org.keycloak.models.UserFederationProviderModel; -import org.keycloak.models.UserFederationSyncResult; -import org.keycloak.models.UserModel; -import org.keycloak.provider.ConfiguredProvider; -import org.keycloak.provider.ProviderConfigProperty; - -import java.util.*; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class DummyUserFederationProviderFactory implements UserFederationProviderFactory, ConfiguredProvider { - - private static final Logger logger = Logger.getLogger(DummyUserFederationProviderFactory.class); - public static final String PROVIDER_NAME = "dummy"; - - private AtomicInteger fullSyncCounter = new AtomicInteger(); - private AtomicInteger changedSyncCounter = new AtomicInteger(); - - private Map users = new HashMap(); - - @Override - public UserFederationProvider getInstance(KeycloakSession session, UserFederationProviderModel model) { - return new DummyUserFederationProvider(users); - } - - @Override - public Set getConfigurationOptions() { - Set list = new HashSet(); - list.add("important.config"); - return list; - } - - @Override - public UserFederationProvider create(KeycloakSession session) { - return new DummyUserFederationProvider(users); - } - - @Override - public void init(Config.Scope config) { - - } - - @Override - public void postInit(KeycloakSessionFactory factory) { - - } - - @Override - public void close() { - - } - - @Override - public String getId() { - return PROVIDER_NAME; - } - - @Override - public UserFederationSyncResult syncAllUsers(KeycloakSessionFactory sessionFactory, String realmId, UserFederationProviderModel model) { - logger.info("syncAllUsers invoked"); - fullSyncCounter.incrementAndGet(); - return UserFederationSyncResult.empty(); - } - - @Override - public UserFederationSyncResult syncChangedUsers(KeycloakSessionFactory sessionFactory, String realmId, UserFederationProviderModel model, Date lastSync) { - logger.info("syncChangedUsers invoked"); - changedSyncCounter.incrementAndGet(); - return UserFederationSyncResult.empty(); - } - - public int getFullSyncCounter() { - return fullSyncCounter.get(); - } - - public int getChangedSyncCounter() { - return changedSyncCounter.get(); - } - - @Override - public String getHelpText() { - return "Dummy User Federation Provider Help Text"; - } - - @Override - public List getConfigProperties() { - - ProviderConfigProperty prop1 = new ProviderConfigProperty(); - prop1.setName("prop1"); - prop1.setLabel("Prop1"); - prop1.setDefaultValue("prop1Default"); - prop1.setHelpText("Prop1 HelpText"); - prop1.setType(ProviderConfigProperty.STRING_TYPE); - - ProviderConfigProperty prop2 = new ProviderConfigProperty(); - prop2.setName("prop2"); - prop2.setLabel("Prop2"); - prop2.setDefaultValue("true"); - prop2.setHelpText("Prop2 HelpText"); - prop2.setType(ProviderConfigProperty.BOOLEAN_TYPE); - - return Arrays.asList(prop1, prop2); - } -} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractKeycloakIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractKeycloakIdentityProviderTest.java index 7683d0c0f4..f6a85e46d1 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractKeycloakIdentityProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractKeycloakIdentityProviderTest.java @@ -45,8 +45,8 @@ import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserModel; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.services.Urls; -import org.keycloak.testsuite.DummyUserFederationProviderFactory; import org.keycloak.testsuite.broker.util.UserSessionStatusServlet; +import org.keycloak.testsuite.federation.DummyUserFederationProviderFactory; import org.openqa.selenium.By; import org.openqa.selenium.NoSuchElementException; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/sync/SyncDummyUserFederationProviderFactory.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/sync/SyncDummyUserFederationProviderFactory.java index 5d327f4bbd..3831787aa1 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/sync/SyncDummyUserFederationProviderFactory.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/sync/SyncDummyUserFederationProviderFactory.java @@ -32,7 +32,7 @@ import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserFederationSyncResult; import org.keycloak.models.UserModel; import org.keycloak.models.utils.KeycloakModelUtils; -import org.keycloak.testsuite.DummyUserFederationProviderFactory; +import org.keycloak.testsuite.federation.DummyUserFederationProviderFactory; /** * @author Marek Posolda 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 84a901a171..efa688c5a2 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 @@ -36,7 +36,7 @@ import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserFederationSyncResult; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.UsersSyncManager; -import org.keycloak.testsuite.DummyUserFederationProviderFactory; +import org.keycloak.testsuite.federation.DummyUserFederationProviderFactory; import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.timer.TimerProvider; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ConcurrentTransactionsTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ConcurrentTransactionsTest.java index b3ad0c4b5b..2fb2aaf7eb 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ConcurrentTransactionsTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ConcurrentTransactionsTest.java @@ -17,6 +17,7 @@ package org.keycloak.testsuite.model; +import java.util.Arrays; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; @@ -151,14 +152,17 @@ public class ConcurrentTransactionsTest extends AbstractModelTest { } - // KEYCLOAK-3296 + // KEYCLOAK-3296 , KEYCLOAK-3494 @Test public void removeUserAttribute() throws Exception { RealmModel realm = realmManager.createRealm("original"); KeycloakSession session = realmManager.getSession(); - UserModel user = session.users().addUser(realm, "john"); - user.setSingleAttribute("foo", "val1"); + UserModel john = session.users().addUser(realm, "john"); + john.setSingleAttribute("foo", "val1"); + + UserModel john2 = session.users().addUser(realm, "john2"); + john2.setAttribute("foo", Arrays.asList("val1", "val2")); final KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); commit(); @@ -182,12 +186,18 @@ public class ConcurrentTransactionsTest extends AbstractModelTest { UserModel john = session.users().getUserByUsername("john", realm); String attrVal = john.getFirstAttribute("foo"); + UserModel john2 = session.users().getUserByUsername("john2", realm); + String attrVal2 = john2.getFirstAttribute("foo"); + // Wait until it's read in both threads readAttrLatch.countDown(); readAttrLatch.await(); - // Remove user attribute in both threads + // KEYCLOAK-3296 : Remove user attribute in both threads john.removeAttribute("foo"); + + // KEYCLOAK-3494 : Set single attribute in both threads + john2.setSingleAttribute("foo", "bar"); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java index 4b3f78ef12..e2af2416d3 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java @@ -211,6 +211,31 @@ public class UserModelTest extends AbstractModelTest { Assert.assertEquals("val23", attrVals.get(0)); } + // KEYCLOAK-3494 + @Test + public void testUpdateUserAttribute() throws Exception { + RealmModel realm = realmManager.createRealm("original"); + UserModel user = session.users().addUser(realm, "user"); + + user.setSingleAttribute("key1", "value1"); + + commit(); + + realm = realmManager.getRealmByName("original"); + user = session.users().getUserByUsername("user", realm); + + // Update attribute + List attrVals = new ArrayList<>(Arrays.asList( "val2" )); + user.setAttribute("key1", attrVals); + Map> allAttrVals = user.getAttributes(); + + // Ensure same transaction is able to see updated value + Assert.assertEquals(1, allAttrVals.size()); + Assert.assertEquals(allAttrVals.get("key1"), Arrays.asList("val2")); + + commit(); + } + @Test public void testSearchByString() { RealmModel realm = realmManager.createRealm("original"); diff --git a/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.models.UserFederationProviderFactory b/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.models.UserFederationProviderFactory index 1b4de7323d..d4a16d36c8 100755 --- a/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.models.UserFederationProviderFactory +++ b/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.models.UserFederationProviderFactory @@ -15,5 +15,4 @@ # limitations under the License. # -org.keycloak.testsuite.DummyUserFederationProviderFactory org.keycloak.testsuite.federation.sync.SyncDummyUserFederationProviderFactory \ No newline at end of file