KEYCLOAK-3494 Input elements backed by user attributes fail to update in themes

This commit is contained in:
mposolda 2016-09-07 15:34:31 +02:00
parent 7912bd4dae
commit 5a015a6518
24 changed files with 669 additions and 304 deletions

View file

@ -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

View file

@ -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")

View file

@ -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.";
}

View file

@ -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));
}
}

View file

@ -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']">
@ -47,6 +52,14 @@
</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)]">
<xsl:copy>

View file

@ -0,0 +1,6 @@
{
"themes": [{
"name" : "address",
"types": [ "admin", "account", "login" ]
}]
}

View file

@ -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>

View file

@ -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

View file

@ -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>

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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));
}
}

View file

@ -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();
}
}

View file

@ -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>

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class DummyUserFederationProvider implements UserFederationProvider {
private final Map<String, UserModel> users;
public DummyUserFederationProvider(Map<String, UserModel> 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<UserModel> searchByAttributes(Map<String, String> attributes, RealmModel realm, int maxResults) {
return Collections.emptyList();
}
@Override
public List<UserModel> 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<String> 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<String> getSupportedCredentialTypes() {
return Collections.singleton(UserCredentialModel.PASSWORD);
}
@Override
public boolean validCredentials(RealmModel realm, UserModel user, List<UserCredentialModel> 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() {
}
}

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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<String, UserModel> users = new HashMap<String, UserModel>();
@Override
public UserFederationProvider getInstance(KeycloakSession session, UserFederationProviderModel model) {
return new DummyUserFederationProvider(users);
}
@Override
public Set<String> getConfigurationOptions() {
Set<String> list = new HashSet<String>();
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<ProviderConfigProperty> 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);
}
}

View file

@ -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;

View file

@ -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>

View file

@ -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;

View file

@ -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);
}

View file

@ -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");

View file

@ -15,5 +15,4 @@
# limitations under the License.
#
org.keycloak.testsuite.DummyUserFederationProviderFactory
org.keycloak.testsuite.federation.sync.SyncDummyUserFederationProviderFactory