keycloak-uncached

KEYCLOAK-3494 Input elements backed by user attributes fail

9/7/2016 10:34:31 AM

Changes

testsuite/integration/src/test/java/org/keycloak/testsuite/DummyUserFederationProvider.java 150(+0 -150)

testsuite/integration/src/test/java/org/keycloak/testsuite/DummyUserFederationProviderFactory.java 132(+0 -132)

Details

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 2b5d35c..16b155b 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/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
index 2ea12fd..49bcac6 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<UserEntity> {
 
     @Override
     public void setSingleAttribute(String name, String value) {
-        boolean found = false;
+        String firstExistingAttrId = null;
         List<UserAttributeEntity> 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<UserEntity> {
         query.setParameter("name", name);
         query.setParameter("userId", user.getId());
         int numUpdated = query.executeUpdate();
+
+        // KEYCLOAK-3494 : Also remove attributes from local user entity
+        List<UserAttributeEntity> toRemove = new ArrayList<>();
+        for (UserAttributeEntity attr : user.getAttributes()) {
+            if (attr.getName().equals(name)) {
+                toRemove.add(attr);
+            }
+        }
+        user.getAttributes().removeAll(toRemove);
     }
 
     @Override
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 916565a..42982f6 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 acb1514..857bfc0 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/pom.xml b/testsuite/integration/pom.xml
index aca1d78..92187bc 100755
--- a/testsuite/integration/pom.xml
+++ b/testsuite/integration/pom.xml
@@ -166,6 +166,14 @@
             <groupId>org.keycloak</groupId>
             <artifactId>federation-properties-example</artifactId>
         </dependency>
+
+        <!-- Dependency on services from integration-arquillian -->
+        <dependency>
+            <groupId>org.keycloak.testsuite</groupId>
+            <artifactId>integration-arquillian-testsuite-providers</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
         <dependency>
             <groupId>org.jboss.logging</groupId>
             <artifactId>jboss-logging</artifactId>
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 7683d0c..f6a85e4 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 5d327f4..3831787 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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
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 84a901a..efa688c 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 b3ad0c4..2fb2aaf 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 4b3f78e..e2af241 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<String> attrVals = new ArrayList<>(Arrays.asList( "val2" ));
+        user.setAttribute("key1", attrVals);
+        Map<String, List<String>> 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 1b4de73..d4a16d3 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
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 f32c036..664eecb 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 @@
             </provider>
         </spi>
     </xsl:variable>
+    <xsl:variable name="themeModuleDefinition">
+        <modules>
+            <module>org.keycloak.testsuite.integration-arquillian-testsuite-providers</module>
+        </modules>
+    </xsl:variable>
     
     <!--inject provider-->
     <xsl:template match="//*[local-name()='providers']/*[local-name()='provider']">
@@ -46,6 +51,14 @@
             <xsl:text>module:org.keycloak.testsuite.integration-arquillian-testsuite-providers</xsl:text>
         </provider>
     </xsl:template>
+
+    <!--inject provider for themes -->
+    <xsl:template match="//*[local-name()='theme']">
+        <xsl:copy>
+            <xsl:apply-templates select="@*|node()" />
+            <xsl:copy-of select="$themeModuleDefinition"/>
+        </xsl:copy>
+    </xsl:template>
     
     <!--inject truststore-->
     <xsl:template match="//*[local-name()='subsystem' and starts-with(namespace-uri(), $nsKS)]">
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 0000000..03978db
--- /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 0000000..d2a6af1
--- /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>
+
+    <div class="row">
+        <div class="col-md-10">
+            <h2>${msg("editAccountHtmlTtile")}</h2>
+        </div>
+        <div class="col-md-2 subtitle">
+            <span class="subtitle"><span class="required">*</span> ${msg("requiredFields")}</span>
+        </div>
+    </div>
+
+    <form action="${url.accountUrl}" class="form-horizontal" method="post">
+
+        <input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
+
+        <div class="form-group ${messagesPerField.printIfExists('username','has-error')}">
+            <div class="col-sm-2 col-md-2">
+                <label for="username" class="control-label">${msg("username")}</label> <#if realm.editUsernameAllowed><span class="required">*</span></#if>
+            </div>
+
+            <div class="col-sm-10 col-md-10">
+                <input type="text" class="form-control" id="username" name="username" <#if !realm.editUsernameAllowed>disabled="disabled"</#if> value="${(account.username!'')?html}"/>
+            </div>
+        </div>
+
+        <div class="form-group ${messagesPerField.printIfExists('email','has-error')}">
+            <div class="col-sm-2 col-md-2">
+            <label for="email" class="control-label">${msg("email")}</label> <span class="required">*</span>
+            </div>
+
+            <div class="col-sm-10 col-md-10">
+                <input type="text" class="form-control" id="email" name="email" autofocus value="${(account.email!'')?html}"/>
+            </div>
+        </div>
+
+        <div class="form-group ${messagesPerField.printIfExists('firstName','has-error')}">
+            <div class="col-sm-2 col-md-2">
+                <label for="firstName" class="control-label">${msg("firstName")}</label> <span class="required">*</span>
+            </div>
+
+            <div class="col-sm-10 col-md-10">
+                <input type="text" class="form-control" id="firstName" name="firstName" value="${(account.firstName!'')?html}"/>
+            </div>
+        </div>
+
+        <div class="form-group ${messagesPerField.printIfExists('lastName','has-error')}">
+            <div class="col-sm-2 col-md-2">
+                <label for="lastName" class="control-label">${msg("lastName")}</label> <span class="required">*</span>
+            </div>
+
+            <div class="col-sm-10 col-md-10">
+                <input type="text" class="form-control" id="lastName" name="lastName" value="${(account.lastName!'')?html}"/>
+            </div>
+        </div>
+
+        <div class="form-group">
+            <div class="col-sm-2 col-md-2">
+                <label for="user.attributes.street" class="control-label">${msg("street")}</label>
+            </div>
+
+            <div class="col-sm-10 col-md-10">
+                <input type="text" class="form-control" id="user.attributes.street" name="user.attributes.street" value="${(account.attributes.street!'')?html}"/>
+            </div>
+        </div>
+        <div class="form-group">
+            <div class="col-sm-2 col-md-2">
+                <label for="user.attributes.locality" class="control-label">${msg("locality")}</label>
+            </div>
+
+            <div class="col-sm-10 col-md-10">
+                <input type="text" class="form-control" id="user.attributes.locality" name="user.attributes.locality" value="${(account.attributes.locality!'')?html}"/>
+            </div>
+        </div>
+        <div class="form-group">
+            <div class="col-sm-2 col-md-2">
+                <label for="user.attributes.region" class="control-label">${msg("region")}</label>
+            </div>
+
+            <div class="col-sm-10 col-md-10">
+                <input type="text" class="form-control" id="user.attributes.region" name="user.attributes.region" value="${(account.attributes.region!'')?html}"/>
+            </div>
+        </div>
+        <div class="form-group">
+            <div class="col-sm-2 col-md-2">
+                <label for="user.attributes.postal_code" class="control-label">${msg("postal_code")}</label>
+            </div>
+
+            <div class="col-sm-10 col-md-10">
+                <input type="text" class="form-control" id="user.attributes.postal_code" name="user.attributes.postal_code" value="${(account.attributes.postal_code!'')?html}"/>
+            </div>
+        </div>
+        <div class="form-group">
+            <div class="col-sm-2 col-md-2">
+                <label for="user.attributes.country" class="control-label">${msg("country")}</label>
+            </div>
+
+            <div class="col-sm-10 col-md-10">
+                <input type="text" class="form-control" id="user.attributes.country" name="user.attributes.country" value="${(account.attributes.country!'')?html}"/>
+            </div>
+        </div>
+
+        <div class="form-group">
+            <div id="kc-form-buttons" class="col-md-offset-2 col-md-10 submit">
+                <div class="">
+                    <#if url.referrerURI??><a href="${url.referrerURI}">${msg("backToApplication")}/a></#if>
+                    <button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="submitAction" value="Save">${msg("doSave")}</button>
+                    <button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" name="submitAction" value="Cancel">${msg("doCancel")}</button>
+                </div>
+            </div>
+        </div>
+    </form>
+
+</@layout.mainLayout>
\ 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 0000000..3e50437
--- /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 0000000..af512de
--- /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 @@
+<!--
+  ~ 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.
+  -->
+
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+    <ol class="breadcrumb">
+        <li><a href="#/realms/{{realm.realm}}/users">Users</a></li>
+        <li>{{user.username}}</li>
+    </ol>
+
+    <kc-tabs-user></kc-tabs-user>
+
+    <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageUsers">
+        <div class="form-group clearfix block">
+            <label class="col-md-2 control-label" for="street">Street</label>
+            <div class="col-md-6">
+                <input ng-model="user.attributes.street" class="form-control" type="text" name="street" id="street" />
+            </div>
+            <kc-tooltip>Street address.</kc-tooltip>
+        </div>
+        <div class="form-group clearfix block">
+            <label class="col-md-2 control-label" for="locality">City or Locality</label>
+            <div class="col-md-6">
+                <input ng-model="user.attributes.locality" class="form-control" type="text" name="locality" id="locality" />
+            </div>
+            <kc-tooltip>City or locality.</kc-tooltip>
+        </div>
+        <div class="form-group clearfix block">
+            <label class="col-md-2 control-label" for="region">State, Province, or Region</label>
+            <div class="col-md-6">
+                <input ng-model="user.attributes.region" class="form-control" type="text" name="region" id="region" />
+            </div>
+            <kc-tooltip>State, province, prefecture, or region.</kc-tooltip>
+        </div>
+        <div class="form-group clearfix block">
+            <label class="col-md-2 control-label" for="postal_code">Zip or Postal code</label>
+            <div class="col-md-6">
+                <input ng-model="user.attributes.postal_code" class="form-control" type="text" name="postal_code" id="postal_code" />
+            </div>
+            <kc-tooltip>Zip code or postal code.</kc-tooltip>
+        </div>
+        <div class="form-group clearfix block">
+            <label class="col-md-2 control-label" for="country">Country</label>
+            <div class="col-md-6">
+                <input ng-model="user.attributes.country" class="form-control" type="text" name="country" id="country" />
+            </div>
+            <kc-tooltip>Country name.</kc-tooltip>
+        </div>
+
+        <div class="form-group" data-ng-show="access.manageUsers">
+            <div class="col-md-10 col-md-offset-2">
+                <button kc-save  data-ng-disabled="!changed">Save</button>
+                <button kc-reset data-ng-disabled="!changed">Cancel</button>
+            </div>
+        </div>
+    </form>
+</div>
+
+<kc-menu></kc-menu>
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 0000000..3e50437
--- /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 0000000..e02a340
--- /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">
+        <form id="kc-update-profile-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
+            <div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('email',properties.kcFormGroupErrorClass!)}">
+                <div class="${properties.kcLabelWrapperClass!}">
+                    <label for="email" class="${properties.kcLabelClass!}">${msg("email")}</label>
+                </div>
+                <div class="${properties.kcInputWrapperClass!}">
+                    <input type="text" id="email" name="email" value="${(user.email!'')?html}" class="${properties.kcInputClass!}" />
+                </div>
+            </div>
+
+            <div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('firstName',properties.kcFormGroupErrorClass!)}">
+                <div class="${properties.kcLabelWrapperClass!}">
+                    <label for="firstName" class="${properties.kcLabelClass!}">${msg("firstName")}</label>
+                </div>
+                <div class="${properties.kcInputWrapperClass!}">
+                    <input type="text" id="firstName" name="firstName" value="${(user.firstName!'')?html}" class="${properties.kcInputClass!}" />
+                </div>
+            </div>
+
+            <div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('lastName',properties.kcFormGroupErrorClass!)}">
+                <div class="${properties.kcLabelWrapperClass!}">
+                    <label for="lastName" class="${properties.kcLabelClass!}">${msg("lastName")}</label>
+                </div>
+                <div class="${properties.kcInputWrapperClass!}">
+                    <input type="text" id="lastName" name="lastName" value="${(user.lastName!'')?html}" class="${properties.kcInputClass!}" />
+                </div>
+            </div>
+            
+            <div class="form-group">
+                <div class="${properties.kcLabelWrapperClass!}">
+                    <label for="user.attributes.street" class="${properties.kcLabelClass!}">${msg("street")}</label>
+                </div>
+
+                <div class="${properties.kcInputWrapperClass!}">
+                    <input type="text" class="${properties.kcInputClass!}"  id="user.attributes.street" name="user.attributes.street" value="${(user.attributes.street!'')?html}"/>
+                </div>
+            </div>
+            <div class="form-group">
+                <div class="${properties.kcLabelWrapperClass!}">
+                    <label for="user.attributes.locality" class="${properties.kcLabelClass!}">${msg("locality")}</label>
+                </div>
+
+                <div class="${properties.kcInputWrapperClass!}">
+                    <input type="text" class="${properties.kcInputClass!}"  id="user.attributes.locality" name="user.attributes.locality" value="${(user.attributes.locality!'')?html}"/>
+                </div>
+            </div>
+            <div class="form-group">
+                <div class="${properties.kcLabelWrapperClass!}">
+                    <label for="user.attributes.region" class="${properties.kcLabelClass!}">${msg("region")}</label>
+                </div>
+
+                <div class="${properties.kcInputWrapperClass!}">
+                    <input type="text" class="${properties.kcInputClass!}"  id="user.attributes.region" name="user.attributes.region" value="${(user.attributes.region!'')?html}"/>
+                </div>
+            </div>
+            <div class="form-group">
+                <div class="${properties.kcLabelWrapperClass!}">
+                    <label for="user.attributes.postal_code" class="${properties.kcLabelClass!}">${msg("postal_code")}</label>
+                </div>
+
+                <div class="${properties.kcInputWrapperClass!}">
+                    <input type="text" class="${properties.kcInputClass!}"  id="user.attributes.postal_code" name="user.attributes.postal_code" value="${(user.attributes.postal_code!'')?html}"/>
+                </div>
+            </div>
+            <div class="form-group">
+                <div class="${properties.kcLabelWrapperClass!}">
+                    <label for="user.attributes.country" class="${properties.kcLabelClass!}">${msg("country")}</label>
+                </div>
+
+                <div class="${properties.kcInputWrapperClass!}">
+                    <input type="text" class="${properties.kcInputClass!}"  id="user.attributes.country" name="user.attributes.country" value="${(user.attributes.country!'')?html}"/>
+                </div>
+            </div>
+            
+
+            <div class="${properties.kcFormGroupClass!}">
+                <div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
+                    <div class="${properties.kcFormOptionsWrapperClass!}">
+                    </div>
+                </div>
+
+                <div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
+                    <input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}" />
+                </div>
+            </div>
+        </form>
+    </#if>
+</@layout.registrationLayout>
\ 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 0000000..3247305
--- /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">
+        <form id="kc-register-form" class="${properties.kcFormClass!}" action="${url.registrationAction}" method="post">
+          <#if !realm.registrationEmailAsUsername>
+            <div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('username',properties.kcFormGroupErrorClass!)}">
+                <div class="${properties.kcLabelWrapperClass!}">
+                    <label for="username" class="${properties.kcLabelClass!}">${msg("username")}</label>
+                </div>
+                <div class="${properties.kcInputWrapperClass!}">
+                    <input type="text" id="username" class="${properties.kcInputClass!}" name="username" value="${(register.formData.username!'')?html}" />
+                </div>
+            </div>
+          </#if>
+            <div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('firstName',properties.kcFormGroupErrorClass!)}">
+                <div class="${properties.kcLabelWrapperClass!}">
+                    <label for="firstName" class="${properties.kcLabelClass!}">${msg("firstName")}</label>
+                </div>
+                <div class="${properties.kcInputWrapperClass!}">
+                    <input type="text" id="firstName" class="${properties.kcInputClass!}" name="firstName" value="${(register.formData.firstName!'')?html}" />
+                </div>
+            </div>
+
+            <div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('lastName',properties.kcFormGroupErrorClass!)}">
+                <div class="${properties.kcLabelWrapperClass!}">
+                    <label for="lastName" class="${properties.kcLabelClass!}">${msg("lastName")}</label>
+                </div>
+                <div class="${properties.kcInputWrapperClass!}">
+                    <input type="text" id="lastName" class="${properties.kcInputClass!}" name="lastName" value="${(register.formData.lastName!'')?html}" />
+                </div>
+            </div>
+
+            <div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('email',properties.kcFormGroupErrorClass!)}">
+                <div class="${properties.kcLabelWrapperClass!}">
+                    <label for="email" class="${properties.kcLabelClass!}">${msg("email")}</label>
+                </div>
+                <div class="${properties.kcInputWrapperClass!}">
+                    <input type="text" id="email" class="${properties.kcInputClass!}" name="email" value="${(register.formData.email!'')?html}" />
+                </div>
+            </div>
+
+            <#if passwordRequired>
+            <div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('password',properties.kcFormGroupErrorClass!)}">
+                <div class="${properties.kcLabelWrapperClass!}">
+                    <label for="password" class="${properties.kcLabelClass!}">${msg("password")}</label>
+                </div>
+                <div class="${properties.kcInputWrapperClass!}">
+                    <input type="password" id="password" class="${properties.kcInputClass!}" name="password" />
+                </div>
+            </div>
+
+            <div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('password-confirm',properties.kcFormGroupErrorClass!)}">
+                <div class="${properties.kcLabelWrapperClass!}">
+                    <label for="password-confirm" class="${properties.kcLabelClass!}">${msg("passwordConfirm")}</label>
+                </div>
+                <div class="${properties.kcInputWrapperClass!}">
+                    <input type="password" id="password-confirm" class="${properties.kcInputClass!}" name="password-confirm" />
+                </div>
+            </div>
+            </#if>
+            <div class="form-group">
+                <div class="${properties.kcLabelWrapperClass!}">
+                    <label for="user.attributes.street" class="${properties.kcLabelClass!}">${msg("street")}</label>
+                </div>
+
+                <div class="${properties.kcInputWrapperClass!}">
+                    <input type="text" class="${properties.kcInputClass!}"  id="user.attributes.street" name="user.attributes.street" value="${(register.formData['user.attributes.street']!'')?html}"/>
+                </div>
+            </div>
+            <div class="form-group">
+                <div class="${properties.kcLabelWrapperClass!}">
+                    <label for="user.attributes.locality" class="${properties.kcLabelClass!}">${msg("locality")}</label>
+                </div>
+
+                <div class="${properties.kcInputWrapperClass!}">
+                    <input type="text" class="${properties.kcInputClass!}"  id="user.attributes.locality" name="user.attributes.locality" value="${(register.formData['user.attributes.locality']!'')?html}"/>
+                </div>
+            </div>
+            <div class="form-group">
+                <div class="${properties.kcLabelWrapperClass!}">
+                    <label for="user.attributes.region" class="${properties.kcLabelClass!}">${msg("region")}</label>
+                </div>
+
+                <div class="${properties.kcInputWrapperClass!}">
+                    <input type="text" class="${properties.kcInputClass!}"  id="user.attributes.region" name="user.attributes.region" value="${(register.formData['user.attributes.region']!'')?html}"/>
+                </div>
+            </div>
+            <div class="form-group">
+                <div class="${properties.kcLabelWrapperClass!}">
+                    <label for="user.attributes.postal_code" class="${properties.kcLabelClass!}">${msg("postal_code")}</label>
+                </div>
+
+                <div class="${properties.kcInputWrapperClass!}">
+                    <input type="text" class="${properties.kcInputClass!}"  id="user.attributes.postal_code" name="user.attributes.postal_code" value="${(register.formData['user.attributes.postal_code']!'')?html}"/>
+                </div>
+            </div>
+            <div class="form-group">
+                <div class="${properties.kcLabelWrapperClass!}">
+                    <label for="user.attributes.country" class="${properties.kcLabelClass!}">${msg("country")}</label>
+                </div>
+
+                <div class="${properties.kcInputWrapperClass!}">
+                    <input type="text" class="${properties.kcInputClass!}"  id="user.attributes.country" name="user.attributes.country" value="${(register.formData['user.attributes.country']!'')?html}"/>
+                </div>
+            </div>
+            <#if recaptchaRequired??>
+            <div class="form-group">
+                <div class="${properties.kcInputWrapperClass!}">
+                    <div class="g-recaptcha" data-size="compact" data-sitekey="${recaptchaSiteKey}"></div>
+                </div>
+            </div>
+            </#if>
+
+            <div class="${properties.kcFormGroupClass!}">
+                <div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
+                    <div class="${properties.kcFormOptionsWrapperClass!}">
+                        <span><a href="${url.loginUrl}">${msg("backToLogin")}</a></span>
+                    </div>
+                </div>
+
+                <div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
+                    <input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doRegister")}"/>
+                </div>
+            </div>
+        </form>
+    </#if>
+</@layout.registrationLayout>
\ 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 0000000..3e50437
--- /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 2159c2f..2a77847 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 0000000..810d9c2
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+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();
+    }
+
+
+}