KEYCLOAK-18552
* added group as attribute metadata * validation for groups and references to groups * adapted template to use show attribute groups * test and integration tests for attribute groups
This commit is contained in:
parent
579302f396
commit
9dff21d0a7
18 changed files with 727 additions and 18 deletions
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright 2021 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.userprofile;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Configuration of the attribute group.
|
||||
*
|
||||
* @author <a href="joerg.matysiak@bosch.io">Jörg Matysiak</a>
|
||||
*/
|
||||
public class AttributeGroupMetadata {
|
||||
|
||||
private String name;
|
||||
private String displayHeader;
|
||||
private String displayDescription;
|
||||
private Map<String, Object> annotations;
|
||||
|
||||
public AttributeGroupMetadata(String name, String displayHeader, String displayDescription, Map<String, Object> annotations) {
|
||||
this.name = name;
|
||||
this.displayHeader = displayHeader;
|
||||
this.displayDescription = displayDescription;
|
||||
if (annotations != null) {
|
||||
addAnnotations(annotations);
|
||||
}
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public AttributeGroupMetadata setName(String name) {
|
||||
this.name = name != null ? name.trim() : null;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getDisplayHeader() {
|
||||
return displayHeader;
|
||||
}
|
||||
|
||||
public AttributeGroupMetadata setDisplayHeader(String displayHeader) {
|
||||
this.displayHeader = displayHeader;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getDisplayDescription() {
|
||||
return displayDescription;
|
||||
}
|
||||
|
||||
public AttributeGroupMetadata setDisplayDescription(String displayDescription) {
|
||||
this.displayDescription = displayDescription;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Map<String, Object> getAnnotations() {
|
||||
return annotations;
|
||||
}
|
||||
|
||||
public AttributeGroupMetadata addAnnotations(Map<String, Object> annotations) {
|
||||
if(annotations != null) {
|
||||
if(this.annotations == null) {
|
||||
this.annotations = new HashMap<>();
|
||||
}
|
||||
|
||||
this.annotations.putAll(annotations);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public AttributeGroupMetadata clone() {
|
||||
return new AttributeGroupMetadata(name, displayHeader, displayDescription, annotations);
|
||||
}
|
||||
}
|
|
@ -43,6 +43,7 @@ public final class AttributeMetadata {
|
|||
|
||||
private final String attributeName;
|
||||
private String attributeDisplayName;
|
||||
private AttributeGroupMetadata attributeGroupMetadata;
|
||||
private final Predicate<AttributeContext> selector;
|
||||
private final Predicate<AttributeContext> writeAllowed;
|
||||
/** Predicate to decide if attribute is required, it is handled as required if predicate is null */
|
||||
|
@ -112,6 +113,10 @@ public final class AttributeMetadata {
|
|||
return this;
|
||||
}
|
||||
|
||||
public AttributeGroupMetadata getAttributeGroupMetadata() {
|
||||
return attributeGroupMetadata;
|
||||
}
|
||||
|
||||
public boolean isSelected(AttributeContext context) {
|
||||
return selector.test(context);
|
||||
}
|
||||
|
@ -184,6 +189,9 @@ public final class AttributeMetadata {
|
|||
cloned.addAnnotations(annotations);
|
||||
}
|
||||
cloned.setAttributeDisplayName(attributeDisplayName);
|
||||
if (attributeGroupMetadata != null) {
|
||||
cloned.setAttributeGroupMetadata(attributeGroupMetadata.clone());
|
||||
}
|
||||
return cloned;
|
||||
}
|
||||
|
||||
|
@ -199,6 +207,12 @@ public final class AttributeMetadata {
|
|||
return this;
|
||||
}
|
||||
|
||||
public AttributeMetadata setAttributeGroupMetadata(AttributeGroupMetadata attributeGroupMetadata) {
|
||||
if(attributeGroupMetadata != null)
|
||||
this.attributeGroupMetadata = attributeGroupMetadata;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
|
|
@ -44,7 +44,7 @@ public abstract class AbstractUserProfileBean {
|
|||
}
|
||||
|
||||
/**
|
||||
* Create UserProfile instance of the relevant type. Is called from {@link #init(KeycloakSession)}.
|
||||
* Create UserProfile instance of the relevant type. Is called from {@link #init(KeycloakSession, boolean)}.
|
||||
*
|
||||
* @param provider to create UserProfile from
|
||||
* @return user profile instance
|
||||
|
@ -158,6 +158,36 @@ public abstract class AbstractUserProfileBean {
|
|||
return metadata.getValidators().stream().collect(Collectors.toMap(AttributeValidatorMetadata::getValidatorId, AttributeValidatorMetadata::getValidatorConfig));
|
||||
}
|
||||
|
||||
public String getGroup() {
|
||||
if (metadata.getAttributeGroupMetadata() != null) {
|
||||
return metadata.getAttributeGroupMetadata().getName();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getGroupDisplayHeader() {
|
||||
if (metadata.getAttributeGroupMetadata() != null) {
|
||||
return metadata.getAttributeGroupMetadata().getDisplayHeader();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getGroupDisplayDescription() {
|
||||
if (metadata.getAttributeGroupMetadata() != null) {
|
||||
return metadata.getAttributeGroupMetadata().getDisplayDescription();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Map<String, Object> getGroupAnnotations() {
|
||||
|
||||
if ((metadata.getAttributeGroupMetadata() == null) || (metadata.getAttributeGroupMetadata().getAnnotations() == null)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
return metadata.getAttributeGroupMetadata().getAnnotations();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Attribute o) {
|
||||
return Integer.compare(metadata.getGuiOrder(), o.metadata.getGuiOrder());
|
||||
|
|
|
@ -229,7 +229,7 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
|
|||
* Sub-types can override this method to customize how contextual profile metadata is configured at runtime.
|
||||
*
|
||||
* @param metadata the profile metadata
|
||||
* @param metadata the current session
|
||||
* @param session the current session
|
||||
* @return the metadata
|
||||
*/
|
||||
protected UserProfileMetadata configureUserProfile(UserProfileMetadata metadata, KeycloakSession session) {
|
||||
|
|
|
@ -32,6 +32,7 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.component.AmphibianProviderFactory;
|
||||
|
@ -53,6 +54,7 @@ import org.keycloak.userprofile.config.UPAttributeRequired;
|
|||
import org.keycloak.userprofile.config.UPAttributeSelector;
|
||||
import org.keycloak.userprofile.config.UPConfig;
|
||||
import org.keycloak.userprofile.config.UPConfigUtils;
|
||||
import org.keycloak.userprofile.config.UPGroup;
|
||||
import org.keycloak.userprofile.validator.AttributeRequiredByMetadataValidator;
|
||||
import org.keycloak.userprofile.validator.BlankAttributeValidator;
|
||||
import org.keycloak.userprofile.validator.ImmutableAttributeValidator;
|
||||
|
@ -255,7 +257,9 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
|||
return decoratedMetadata;
|
||||
}
|
||||
|
||||
Map<String, UPGroup> groupsByName = asHashMap(parsedConfig.getGroups());
|
||||
int guiOrder = 0;
|
||||
|
||||
for (UPAttribute attrConfig : parsedConfig.getAttributes()) {
|
||||
String attributeName = attrConfig.getName();
|
||||
List<AttributeValidatorMetadata> validators = new ArrayList<>();
|
||||
|
@ -313,6 +317,8 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
|||
}
|
||||
|
||||
Map<String, Object> annotations = attrConfig.getAnnotations();
|
||||
String attributeGroup = attrConfig.getGroup();
|
||||
AttributeGroupMetadata groupMetadata = toAttributeGroupMeta(groupsByName.get(attributeGroup));
|
||||
|
||||
if (isUsernameOrEmailAttribute(attributeName)) {
|
||||
if (permissions == null) {
|
||||
|
@ -324,16 +330,26 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
|||
if (atts.isEmpty()) {
|
||||
// attribute metadata doesn't exist so we have to add it. We keep it optional as Abstract base
|
||||
// doesn't require it.
|
||||
decoratedMetadata.addAttribute(attributeName, guiOrder++, writeAllowed, validators).addAnnotations(annotations).setAttributeDisplayName(attrConfig.getDisplayName());
|
||||
decoratedMetadata.addAttribute(attributeName, guiOrder++, writeAllowed, validators)
|
||||
.addAnnotations(annotations)
|
||||
.setAttributeDisplayName(attrConfig.getDisplayName())
|
||||
.setAttributeGroupMetadata(groupMetadata);
|
||||
} else {
|
||||
final int localGuiOrder = guiOrder++;
|
||||
// only add configured validators and annotations if attribute metadata exist
|
||||
atts.stream().forEach(c -> c.addValidator(validators).addAnnotations(annotations).setAttributeDisplayName(attrConfig.getDisplayName()).setGuiOrder(localGuiOrder));
|
||||
atts.stream().forEach(c -> c.addValidator(validators)
|
||||
.addAnnotations(annotations)
|
||||
.setAttributeDisplayName(attrConfig.getDisplayName())
|
||||
.setGuiOrder(localGuiOrder)
|
||||
.setAttributeGroupMetadata(groupMetadata));
|
||||
}
|
||||
} else {
|
||||
// always add validation for imuttable/read-only attributes
|
||||
// always add validation for immutable/read-only attributes
|
||||
validators.add(new AttributeValidatorMetadata(ImmutableAttributeValidator.ID));
|
||||
decoratedMetadata.addAttribute(attributeName, guiOrder++, validators, selector, writeAllowed, required, readAllowed).addAnnotations(annotations).setAttributeDisplayName(attrConfig.getDisplayName());
|
||||
decoratedMetadata.addAttribute(attributeName, guiOrder++, validators, selector, writeAllowed, required, readAllowed)
|
||||
.addAnnotations(annotations)
|
||||
.setAttributeDisplayName(attrConfig.getDisplayName())
|
||||
.setAttributeGroupMetadata(groupMetadata);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -341,6 +357,17 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
|||
|
||||
}
|
||||
|
||||
private Map<String, UPGroup> asHashMap(List<UPGroup> groups) {
|
||||
return groups.stream().collect(Collectors.toMap(g -> g.getName(), g -> g));
|
||||
}
|
||||
|
||||
private AttributeGroupMetadata toAttributeGroupMeta(UPGroup group) {
|
||||
if (group == null) {
|
||||
return null;
|
||||
}
|
||||
return new AttributeGroupMetadata(group.getName(), group.getDisplayHeader(), group.getDisplayDescription(), group.getAnnotations());
|
||||
}
|
||||
|
||||
private boolean isUsernameOrEmailAttribute(String attributeName) {
|
||||
return UserModel.USERNAME.equals(attributeName) || UserModel.EMAIL.equals(attributeName);
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ public class UPAttribute {
|
|||
private UPAttributePermissions permissions;
|
||||
/** null means it is always selected */
|
||||
private UPAttributeSelector selector;
|
||||
private String group;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
|
@ -102,8 +103,16 @@ public class UPAttribute {
|
|||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
public void setGroup(String group) {
|
||||
this.group = group != null ? group.trim() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "UPAttribute [name=" + name + ", displayName=" + displayName + ", permissions=" + permissions + ", selector=" + selector + ", required=" + required + ", validations=" + validations + ", annotations=" + annotations + "]";
|
||||
return "UPAttribute [name=" + name + ", displayName=" + displayName + ", permissions=" + permissions + ", selector=" + selector + ", required=" + required + ", validations=" + validations + ", annotations=" + annotations + ", group=" + group + "]";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package org.keycloak.userprofile.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
|
@ -28,6 +29,7 @@ import java.util.List;
|
|||
public class UPConfig {
|
||||
|
||||
private List<UPAttribute> attributes;
|
||||
private List<UPGroup> groups;
|
||||
|
||||
public List<UPAttribute> getAttributes() {
|
||||
return attributes;
|
||||
|
@ -47,8 +49,29 @@ public class UPConfig {
|
|||
return this;
|
||||
}
|
||||
|
||||
public List<UPGroup> getGroups() {
|
||||
if (groups == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
public void setGroups(List<UPGroup> groups) {
|
||||
this.groups = groups;
|
||||
}
|
||||
|
||||
public UPConfig addGroup(UPGroup group) {
|
||||
if (groups == null) {
|
||||
groups = new ArrayList<>();
|
||||
}
|
||||
|
||||
groups.add(group);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "UPConfig [attributes=" + attributes + "]";
|
||||
return "UPConfig [attributes=" + attributes + ", groups=" + groups + "]";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,11 +22,13 @@ import java.io.IOException;
|
|||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.keycloak.common.util.StreamUtil;
|
||||
|
@ -62,7 +64,7 @@ public class UPConfigUtils {
|
|||
/**
|
||||
* Load configuration from JSON file.
|
||||
* <p>
|
||||
* Configuration is not validated, use {@link #validate(UPConfig)} to validate it and get list of errors.
|
||||
* Configuration is not validated, use {@link #validate(KeycloakSession, UPConfig)} to validate it and get list of errors.
|
||||
*
|
||||
* @param is JSON file to be loaded
|
||||
* @return object representation of the configuration
|
||||
|
@ -75,10 +77,12 @@ public class UPConfigUtils {
|
|||
/**
|
||||
* Validate object representation of the configuration. Validations:
|
||||
* <ul>
|
||||
* <li>defaultProfile is defined and exists in profiles
|
||||
* <li>parent exists for type
|
||||
* <li>type exists for attribute
|
||||
* <li>validator (from Validator SPI) exists for validation and it's config is correct
|
||||
* <li>defaultProfile is defined and exists in profiles</li>
|
||||
* <li>parent exists for type</li>
|
||||
* <li>type exists for attribute</li>
|
||||
* <li>validator (from Validator SPI) exists for validation and it's config is correct</li>
|
||||
* <li>if an attribute group is configured it is verified that this group exists</li>
|
||||
* <li>all groups have a name != null</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param session to be used for Validator SPI integration
|
||||
|
@ -86,11 +90,28 @@ public class UPConfigUtils {
|
|||
* @return list of errors, empty if no error found
|
||||
*/
|
||||
public static List<String> validate(KeycloakSession session, UPConfig config) {
|
||||
List<String> errors = new ArrayList<>();
|
||||
List<String> errors = validateAttributes(session, config);
|
||||
errors.addAll(validateAttributeGroups(config));
|
||||
return errors;
|
||||
}
|
||||
|
||||
private static List<String> validateAttributeGroups(UPConfig config) {
|
||||
long groupsWithoutName = config.getGroups().stream().filter(g -> g.getName() == null).collect(Collectors.counting());
|
||||
|
||||
if (groupsWithoutName > 0) {
|
||||
String errorMessage = "Name is mandatory for groups, found " + groupsWithoutName + " group(s) without name.";
|
||||
return Collections.singletonList(errorMessage);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private static List<String> validateAttributes(KeycloakSession session, UPConfig config) {
|
||||
List<String> errors = new ArrayList<>();
|
||||
Set<String> groups = config.getGroups().stream().map(g -> g.getName()).collect(Collectors.toSet());
|
||||
|
||||
if (config.getAttributes() != null) {
|
||||
Set<String> attNamesCache = new HashSet<>();
|
||||
config.getAttributes().forEach((attribute) -> validate(session, attribute, errors, attNamesCache));
|
||||
config.getAttributes().forEach((attribute) -> validateAttribute(session, attribute, groups, errors, attNamesCache));
|
||||
} else {
|
||||
errors.add("UserProfile configuration without 'attributes' section is not allowed");
|
||||
}
|
||||
|
@ -103,10 +124,11 @@ public class UPConfigUtils {
|
|||
*
|
||||
* @param session to be used for Validator SPI integration
|
||||
* @param attributeConfig config to be validated
|
||||
* @param groups set of groups that are configured
|
||||
* @param errors to add error message in if something is invalid
|
||||
* @param attNamesCache cache of already existing attribute names so we can check uniqueness
|
||||
*/
|
||||
private static void validate(KeycloakSession session, UPAttribute attributeConfig, List<String> errors, Set<String> attNamesCache) {
|
||||
private static void validateAttribute(KeycloakSession session, UPAttribute attributeConfig, Set<String> groups, List<String> errors, Set<String> attNamesCache) {
|
||||
String attributeName = attributeConfig.getName();
|
||||
if (isBlank(attributeName)) {
|
||||
errors.add("Attribute configuration without 'name' is not allowed");
|
||||
|
@ -138,6 +160,12 @@ public class UPConfigUtils {
|
|||
if (attributeConfig.getSelector() != null) {
|
||||
validateScopes(attributeConfig.getSelector().getScopes(), "selector.scopes", attributeName, errors, session);
|
||||
}
|
||||
|
||||
if (attributeConfig.getGroup() != null) {
|
||||
if (!groups.contains(attributeConfig.getGroup())) {
|
||||
errors.add("Attribute '" + attributeName + "' references unknown group '" + attributeConfig.getGroup() + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateScopes(Set<String> scopes, String propertyName, String attributeName, List<String> errors, KeycloakSession session) {
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright 2021 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.userprofile.config;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Configuration of the attribute group.
|
||||
*
|
||||
* @author <a href="joerg.matysiak@bosch.io">Jörg Matysiak</a>
|
||||
*/
|
||||
public class UPGroup {
|
||||
|
||||
private String name;
|
||||
private String displayHeader;
|
||||
private String displayDescription;
|
||||
private Map<String, Object> annotations;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name != null ? name.trim() : null;
|
||||
}
|
||||
|
||||
public String getDisplayHeader() {
|
||||
return displayHeader;
|
||||
}
|
||||
|
||||
public void setDisplayHeader(String displayHeader) {
|
||||
this.displayHeader = displayHeader;
|
||||
}
|
||||
|
||||
public String getDisplayDescription() {
|
||||
return displayDescription;
|
||||
}
|
||||
|
||||
public void setDisplayDescription(String displayDescription) {
|
||||
this.displayDescription = displayDescription;
|
||||
}
|
||||
|
||||
public Map<String, Object> getAnnotations() {
|
||||
return annotations;
|
||||
}
|
||||
|
||||
public void setAnnotations(Map<String, Object> annotations) {
|
||||
this.annotations = annotations;
|
||||
}
|
||||
}
|
|
@ -112,7 +112,71 @@ public class RequiredActionUpdateProfileWithUserProfileTest extends RequiredActi
|
|||
Assert.assertEquals("Department",updateProfilePage.getLabelForField("department"));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testAttributeGrouping() {
|
||||
|
||||
setUserProfileConfiguration("{\"attributes\": ["
|
||||
+ "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + "},"
|
||||
+ "{\"name\": \"username\", " + VerifyProfileTest.PERMISSIONS_ALL + "},"
|
||||
+ "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}},"
|
||||
+ "{\"name\": \"department\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\":{}, \"group\": \"company\"},"
|
||||
+ "{\"name\": \"email\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"group\": \"contact\"}"
|
||||
+ "], \"groups\": ["
|
||||
+ "{\"name\": \"company\", \"displayDescription\": \"Company field desc\" },"
|
||||
+ "{\"name\": \"contact\" }"
|
||||
+ "]}");
|
||||
|
||||
loginPage.open();
|
||||
loginPage.login(USERNAME1, PASSWORD);
|
||||
|
||||
updateProfilePage.assertCurrent();
|
||||
String htmlFormId="kc-update-profile-form";
|
||||
|
||||
//assert fields and groups location in form
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(1) > div:nth-child(2) > input#lastName")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(2) > div:nth-child(2) > input#username")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(3) > div:nth-child(2) > input#firstName")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(4) > div:nth-child(1) > label#header-company")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(4) > div:nth-child(2) > label#description-company")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(5) > div:nth-child(2) > input#department")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(6) > div:nth-child(1) > label#header-contact")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(7) > div:nth-child(2) > input#email")
|
||||
).isDisplayed()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testAttributeGuiOrder() {
|
||||
|
||||
|
|
|
@ -71,6 +71,74 @@ public class KcOidcFirstBrokerLoginWithUserProfileTest extends KcOidcFirstBroker
|
|||
// direct value in display name
|
||||
Assert.assertEquals("Department", updateAccountInformationPage.getLabelForField("department"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAttributeGrouping() {
|
||||
|
||||
updateExecutions(AbstractBrokerTest::enableUpdateProfileOnFirstLogin);
|
||||
|
||||
setUserProfileConfiguration("{\"attributes\": ["
|
||||
+ "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + "},"
|
||||
+ "{\"name\": \"username\", " + VerifyProfileTest.PERMISSIONS_ALL + "},"
|
||||
+ "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}},"
|
||||
+ "{\"name\": \"department\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\":{}, \"group\": \"company\"},"
|
||||
+ "{\"name\": \"email\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"group\": \"contact\"}"
|
||||
+ "], \"groups\": ["
|
||||
+ "{\"name\": \"company\", \"displayDescription\": \"Company field desc\" },"
|
||||
+ "{\"name\": \"contact\" }"
|
||||
+ "]}");
|
||||
|
||||
driver.navigate().to(getAccountUrl(getConsumerRoot(), bc.consumerRealmName()));
|
||||
logInWithBroker(bc);
|
||||
|
||||
waitForPage(driver, "update account information", false);
|
||||
updateAccountInformationPage.assertCurrent();
|
||||
|
||||
//assert fields location in form
|
||||
String htmlFormId = "kc-idp-review-profile-form";
|
||||
|
||||
//assert fields and groups location in form
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(1) > div:nth-child(2) > input#lastName")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(2) > div:nth-child(2) > input#username")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(3) > div:nth-child(2) > input#firstName")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(4) > div:nth-child(1) > label#header-company")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(4) > div:nth-child(2) > label#description-company")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(5) > div:nth-child(2) > input#department")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(6) > div:nth-child(1) > label#header-contact")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(7) > div:nth-child(2) > input#email")
|
||||
).isDisplayed()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAttributeGuiOrder() {
|
||||
|
|
|
@ -294,6 +294,79 @@ public class RegisterWithUserProfileTest extends RegisterTest {
|
|||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAttributeGrouping() {
|
||||
|
||||
setUserProfileConfiguration("{\"attributes\": ["
|
||||
+ "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + "},"
|
||||
+ "{\"name\": \"username\", " + VerifyProfileTest.PERMISSIONS_ALL + "},"
|
||||
+ "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}},"
|
||||
+ "{\"name\": \"department\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\":{}, \"group\": \"company\"},"
|
||||
+ "{\"name\": \"email\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"group\": \"contact\"}"
|
||||
+ "], \"groups\": ["
|
||||
+ "{\"name\": \"company\", \"displayDescription\": \"Company field desc\" },"
|
||||
+ "{\"name\": \"contact\" }"
|
||||
+ "]}");
|
||||
|
||||
loginPage.open();
|
||||
loginPage.clickRegister();
|
||||
|
||||
registerPage.assertCurrent();
|
||||
String htmlFormId="kc-register-form";
|
||||
|
||||
//assert fields and groups location in form
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(1) > div:nth-child(2) > input#lastName")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(2) > div:nth-child(2) > input#username")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#kc-register-form > div:nth-child(3) > div:nth-child(2) > input#password")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(4) > div:nth-child(2) > input#password-confirm")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(5) > div:nth-child(2) > input#firstName")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(6) > div:nth-child(1) > label#header-company")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(6) > div:nth-child(2) > label#description-company")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(7) > div:nth-child(2) > input#department")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(8) > div:nth-child(1) > label#header-contact")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(9) > div:nth-child(2) > input#email")
|
||||
).isDisplayed()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegisterUserSuccess_requiredReadOnlyAttributeNotRenderedAndNotBlockingRegistration() {
|
||||
|
||||
|
|
|
@ -180,6 +180,72 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest {
|
|||
Assert.assertEquals("Department",verifyProfilePage.getLabelForField("department"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAttributeGrouping() {
|
||||
|
||||
setUserProfileConfiguration(CONFIGURATION_FOR_USER_EDIT);
|
||||
updateUser(user5Id, "ExistingFirst", "ExistingLast", null);
|
||||
|
||||
setUserProfileConfiguration("{\"attributes\": ["
|
||||
+ "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + "},"
|
||||
+ "{\"name\": \"username\", " + VerifyProfileTest.PERMISSIONS_ALL + "},"
|
||||
+ "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}},"
|
||||
+ "{\"name\": \"department\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\":{}, \"group\": \"company\"},"
|
||||
+ "{\"name\": \"email\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"group\": \"contact\"}"
|
||||
+ "], \"groups\": ["
|
||||
+ "{\"name\": \"company\", \"displayDescription\": \"Company field desc\" },"
|
||||
+ "{\"name\": \"contact\" }"
|
||||
+ "]}");
|
||||
|
||||
loginPage.open();
|
||||
loginPage.login("login-test5", "password");
|
||||
|
||||
verifyProfilePage.assertCurrent();
|
||||
String htmlFormId="kc-update-profile-form";
|
||||
|
||||
//assert fields and groups location in form
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(1) > div:nth-child(2) > input#lastName")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(2) > div:nth-child(2) > input#username")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(3) > div:nth-child(2) > input#firstName")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(4) > div:nth-child(1) > label#header-company")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(4) > div:nth-child(2) > label#description-company")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(5) > div:nth-child(2) > input#department")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(6) > div:nth-child(1) > label#header-contact")
|
||||
).isDisplayed()
|
||||
);
|
||||
Assert.assertTrue(
|
||||
driver.findElement(
|
||||
By.cssSelector("form#"+htmlFormId+" > div:nth-child(7) > div:nth-child(2) > input#email")
|
||||
).isDisplayed()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAttributeGuiOrder() {
|
||||
|
||||
|
|
|
@ -54,6 +54,7 @@ import org.keycloak.representations.idm.RealmRepresentation;
|
|||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||
import org.keycloak.testsuite.runonserver.RunOnServer;
|
||||
import org.keycloak.userprofile.AttributeGroupMetadata;
|
||||
import org.keycloak.userprofile.DeclarativeUserProfileProvider;
|
||||
import org.keycloak.userprofile.config.UPAttribute;
|
||||
import org.keycloak.userprofile.config.UPAttributePermissions;
|
||||
|
@ -349,6 +350,67 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
assertNotNull(attributes.getFirstValue("address"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetProfileAttributeGroups() {
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testGetProfileAttributeGroups);
|
||||
}
|
||||
|
||||
private static void testGetProfileAttributeGroups(KeycloakSession session) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
UserModel user = session.users().addUser(realm, org.keycloak.models.utils.KeycloakModelUtils.generateId());
|
||||
UserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||
|
||||
String configuration = "{\n" +
|
||||
" \"attributes\": [\n" +
|
||||
" {\n" +
|
||||
" \"name\": \"address\",\n" +
|
||||
" \"group\": \"companyaddress\"\n" +
|
||||
" },\n" +
|
||||
" {\n" +
|
||||
" \"name\": \"second\",\n" +
|
||||
" \"group\": \"groupwithanno" + "\"\n" +
|
||||
" }\n" +
|
||||
" ],\n" +
|
||||
" \"groups\": [\n" +
|
||||
" {\n" +
|
||||
" \"name\": \"companyaddress\",\n" +
|
||||
" \"displayHeader\": \"header\",\n" +
|
||||
" \"displayDescription\": \"description\"\n" +
|
||||
" },\n" +
|
||||
" {\n" +
|
||||
" \"name\": \"groupwithanno\",\n" +
|
||||
" \"annotations\": {\n" +
|
||||
" \"anno1\": \"value1\",\n" +
|
||||
" \"anno2\": \"value2\"\n" +
|
||||
" }\n" +
|
||||
" }\n" +
|
||||
" ]\n" +
|
||||
"}\n";
|
||||
provider.setConfiguration(configuration);
|
||||
|
||||
UserProfile profile = provider.create(UserProfileContext.ACCOUNT, user);
|
||||
Attributes attributes = profile.getAttributes();
|
||||
|
||||
assertThat(attributes.nameSet(),
|
||||
containsInAnyOrder(UserModel.USERNAME, UserModel.EMAIL, UserModel.FIRST_NAME, UserModel.LAST_NAME, "address", "second"));
|
||||
|
||||
|
||||
AttributeGroupMetadata companyAddressGroup = attributes.getMetadata("address").getAttributeGroupMetadata();
|
||||
assertEquals("companyaddress", companyAddressGroup.getName());
|
||||
assertEquals("header", companyAddressGroup.getDisplayHeader());
|
||||
assertEquals("description", companyAddressGroup.getDisplayDescription());
|
||||
assertNull(companyAddressGroup.getAnnotations());
|
||||
|
||||
AttributeGroupMetadata groupwithannoGroup = attributes.getMetadata("second").getAttributeGroupMetadata();
|
||||
assertEquals("groupwithanno", groupwithannoGroup.getName());
|
||||
assertNull(groupwithannoGroup.getDisplayHeader());
|
||||
assertNull(groupwithannoGroup.getDisplayDescription());
|
||||
Map<String, Object> annotations = groupwithannoGroup.getAnnotations();
|
||||
assertEquals(2, annotations.size());
|
||||
assertEquals("value1", annotations.get("anno1"));
|
||||
assertEquals("value2", annotations.get("anno2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateAndUpdateUser() {
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testCreateAndUpdateUser);
|
||||
|
|
|
@ -42,9 +42,10 @@ import org.keycloak.userprofile.config.UPAttributePermissions;
|
|||
import org.keycloak.userprofile.config.UPAttributeRequired;
|
||||
import org.keycloak.userprofile.config.UPConfig;
|
||||
import org.keycloak.userprofile.config.UPConfigUtils;
|
||||
import org.keycloak.userprofile.config.UPGroup;
|
||||
|
||||
/**
|
||||
* Unit test for {@link UPConfigParser} functionality
|
||||
* Unit test for {@link UPConfigUtils} functionality
|
||||
*
|
||||
* @author Vlastimil Elias <velias@redhat.com>
|
||||
*
|
||||
|
@ -134,6 +135,19 @@ public class UPConfigParserTest extends AbstractTestRealmKeycloakTest {
|
|||
//displayName
|
||||
att = config.getAttributes().get(4);
|
||||
Assert.assertEquals("${profile.phone}", att.getDisplayName());
|
||||
|
||||
// group
|
||||
Assert.assertEquals("contact", att.getGroup());
|
||||
|
||||
// assert *** groups ***
|
||||
Assert.assertEquals(1, config.getGroups().size());
|
||||
|
||||
UPGroup group = config.getGroups().get(0);
|
||||
Assert.assertEquals("contact", group.getName());
|
||||
Assert.assertEquals("Contact information", group.getDisplayHeader());
|
||||
Assert.assertEquals("Required to contact you in case of emergency", group.getDisplayDescription());
|
||||
Assert.assertEquals(1, group.getAnnotations().size());
|
||||
Assert.assertEquals("value1", group.getAnnotations().get("contactanno1"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -309,4 +323,37 @@ public class UPConfigParserTest extends AbstractTestRealmKeycloakTest {
|
|||
errors = validate(session, config);
|
||||
Assert.assertEquals(1, errors.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateConfiguration_attributeGroupConfigurationErrors() {
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UPConfigParserTest::validateConfiguration_attributeGroupConfigurationErrors);
|
||||
}
|
||||
|
||||
private static void validateConfiguration_attributeGroupConfigurationErrors(KeycloakSession session) throws IOException {
|
||||
UPConfig config = loadValidConfig();
|
||||
|
||||
// add a group without name
|
||||
UPGroup groupWithoutName = new UPGroup();
|
||||
config.addGroup(groupWithoutName);
|
||||
List<String> errors = validate(session, config);
|
||||
Assert.assertEquals(1, errors.size());
|
||||
Assert.assertEquals("Name is mandatory for groups, found 1 group(s) without name.", errors.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateConfiguration_attributeGroupReferenceErrors() {
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UPConfigParserTest::validateConfiguration_attributeGroupReferenceErrors);
|
||||
}
|
||||
|
||||
private static void validateConfiguration_attributeGroupReferenceErrors(KeycloakSession session) throws IOException {
|
||||
UPConfig config = loadValidConfig();
|
||||
|
||||
// attribute references group that is not configured
|
||||
UPAttribute firstAttribute = config.getAttributes().get(0);
|
||||
firstAttribute.setGroup("non-existing-group");
|
||||
List<String> errors = validate(session, config);
|
||||
Assert.assertEquals(1, errors.size());
|
||||
Assert.assertEquals("Attribute 'username' references unknown group 'non-existing-group'", errors.get(0));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,4 +1,14 @@
|
|||
{
|
||||
"groups" : [
|
||||
{
|
||||
"name" : "contact",
|
||||
"displayHeader" : "Contact information",
|
||||
"displayDescription" : "Required to contact you in case of emergency",
|
||||
"annotations" : {
|
||||
"contactanno1" : "value1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"attributes": [
|
||||
{
|
||||
"name":"username",
|
||||
|
@ -46,6 +56,7 @@
|
|||
"validations": {
|
||||
"not-blank":{}
|
||||
},
|
||||
"group": "contact",
|
||||
"required": {
|
||||
"scopes" : ["phone-1", "phone-2"],
|
||||
"roles" : ["user", "admin"]
|
||||
|
|
|
@ -1,5 +1,35 @@
|
|||
<#macro userProfileFormFields>
|
||||
<#assign currentGroup="">
|
||||
|
||||
<#list profile.attributes as attribute>
|
||||
|
||||
<#assign groupName = attribute.group!"">
|
||||
<#if groupName != currentGroup>
|
||||
<#assign currentGroup=groupName>
|
||||
<#if currentGroup != "" >
|
||||
<div class="${properties.kcFormGroupClass!}">
|
||||
|
||||
<#assign groupDisplayHeader=attribute.groupDisplayHeader!"">
|
||||
<#if groupDisplayHeader != "">
|
||||
<#assign groupHeaderText=advancedMsg(attribute.groupDisplayHeader)!groupName>
|
||||
<#else>
|
||||
<#assign groupHeaderText=groupName>
|
||||
</#if>
|
||||
<div class="${properties.kcContentWrapperClass!}">
|
||||
<label id="header-${groupName}" class="${kcFormGroupHeader!}">${groupHeaderText}</label>
|
||||
</div>
|
||||
|
||||
<#assign groupDisplayDescription=attribute.groupDisplayDescription!"">
|
||||
<#if groupDisplayDescription != "">
|
||||
<#assign groupDescriptionText=advancedMsg(attribute.groupDisplayDescription)!"">
|
||||
<div class="${properties.kcLabelWrapperClass!}">
|
||||
<label id="description-${groupName}" class="${properties.kcLabelClass!}">${groupDescriptionText}</label>
|
||||
</div>
|
||||
</#if>
|
||||
</div>
|
||||
</#if>
|
||||
</#if>
|
||||
|
||||
<#nested "beforeField" attribute>
|
||||
<div class="${properties.kcFormGroupClass!}">
|
||||
<div class="${properties.kcLabelWrapperClass!}">
|
||||
|
|
|
@ -68,6 +68,9 @@ kcSignUpClass=login-pf-signup
|
|||
|
||||
kcInfoAreaClass=col-xs-12 col-sm-4 col-md-4 col-lg-5 details
|
||||
|
||||
### user-profile grouping
|
||||
kcFormGroupHeader=pf-c-form__group
|
||||
|
||||
##### css classes for form buttons
|
||||
# main class used for all buttons
|
||||
kcButtonClass=pf-c-button
|
||||
|
|
Loading…
Reference in a new issue