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 final String attributeName;
|
||||||
private String attributeDisplayName;
|
private String attributeDisplayName;
|
||||||
|
private AttributeGroupMetadata attributeGroupMetadata;
|
||||||
private final Predicate<AttributeContext> selector;
|
private final Predicate<AttributeContext> selector;
|
||||||
private final Predicate<AttributeContext> writeAllowed;
|
private final Predicate<AttributeContext> writeAllowed;
|
||||||
/** Predicate to decide if attribute is required, it is handled as required if predicate is null */
|
/** 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;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AttributeGroupMetadata getAttributeGroupMetadata() {
|
||||||
|
return attributeGroupMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isSelected(AttributeContext context) {
|
public boolean isSelected(AttributeContext context) {
|
||||||
return selector.test(context);
|
return selector.test(context);
|
||||||
}
|
}
|
||||||
|
@ -184,6 +189,9 @@ public final class AttributeMetadata {
|
||||||
cloned.addAnnotations(annotations);
|
cloned.addAnnotations(annotations);
|
||||||
}
|
}
|
||||||
cloned.setAttributeDisplayName(attributeDisplayName);
|
cloned.setAttributeDisplayName(attributeDisplayName);
|
||||||
|
if (attributeGroupMetadata != null) {
|
||||||
|
cloned.setAttributeGroupMetadata(attributeGroupMetadata.clone());
|
||||||
|
}
|
||||||
return cloned;
|
return cloned;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,6 +207,12 @@ public final class AttributeMetadata {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AttributeMetadata setAttributeGroupMetadata(AttributeGroupMetadata attributeGroupMetadata) {
|
||||||
|
if(attributeGroupMetadata != null)
|
||||||
|
this.attributeGroupMetadata = attributeGroupMetadata;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (this == o) return true;
|
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
|
* @param provider to create UserProfile from
|
||||||
* @return user profile instance
|
* @return user profile instance
|
||||||
|
@ -158,6 +158,36 @@ public abstract class AbstractUserProfileBean {
|
||||||
return metadata.getValidators().stream().collect(Collectors.toMap(AttributeValidatorMetadata::getValidatorId, AttributeValidatorMetadata::getValidatorConfig));
|
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
|
@Override
|
||||||
public int compareTo(Attribute o) {
|
public int compareTo(Attribute o) {
|
||||||
return Integer.compare(metadata.getGuiOrder(), o.metadata.getGuiOrder());
|
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.
|
* Sub-types can override this method to customize how contextual profile metadata is configured at runtime.
|
||||||
*
|
*
|
||||||
* @param metadata the profile metadata
|
* @param metadata the profile metadata
|
||||||
* @param metadata the current session
|
* @param session the current session
|
||||||
* @return the metadata
|
* @return the metadata
|
||||||
*/
|
*/
|
||||||
protected UserProfileMetadata configureUserProfile(UserProfileMetadata metadata, KeycloakSession session) {
|
protected UserProfileMetadata configureUserProfile(UserProfileMetadata metadata, KeycloakSession session) {
|
||||||
|
|
|
@ -32,6 +32,7 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.keycloak.common.util.MultivaluedHashMap;
|
import org.keycloak.common.util.MultivaluedHashMap;
|
||||||
import org.keycloak.component.AmphibianProviderFactory;
|
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.UPAttributeSelector;
|
||||||
import org.keycloak.userprofile.config.UPConfig;
|
import org.keycloak.userprofile.config.UPConfig;
|
||||||
import org.keycloak.userprofile.config.UPConfigUtils;
|
import org.keycloak.userprofile.config.UPConfigUtils;
|
||||||
|
import org.keycloak.userprofile.config.UPGroup;
|
||||||
import org.keycloak.userprofile.validator.AttributeRequiredByMetadataValidator;
|
import org.keycloak.userprofile.validator.AttributeRequiredByMetadataValidator;
|
||||||
import org.keycloak.userprofile.validator.BlankAttributeValidator;
|
import org.keycloak.userprofile.validator.BlankAttributeValidator;
|
||||||
import org.keycloak.userprofile.validator.ImmutableAttributeValidator;
|
import org.keycloak.userprofile.validator.ImmutableAttributeValidator;
|
||||||
|
@ -255,7 +257,9 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
return decoratedMetadata;
|
return decoratedMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, UPGroup> groupsByName = asHashMap(parsedConfig.getGroups());
|
||||||
int guiOrder = 0;
|
int guiOrder = 0;
|
||||||
|
|
||||||
for (UPAttribute attrConfig : parsedConfig.getAttributes()) {
|
for (UPAttribute attrConfig : parsedConfig.getAttributes()) {
|
||||||
String attributeName = attrConfig.getName();
|
String attributeName = attrConfig.getName();
|
||||||
List<AttributeValidatorMetadata> validators = new ArrayList<>();
|
List<AttributeValidatorMetadata> validators = new ArrayList<>();
|
||||||
|
@ -313,6 +317,8 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Object> annotations = attrConfig.getAnnotations();
|
Map<String, Object> annotations = attrConfig.getAnnotations();
|
||||||
|
String attributeGroup = attrConfig.getGroup();
|
||||||
|
AttributeGroupMetadata groupMetadata = toAttributeGroupMeta(groupsByName.get(attributeGroup));
|
||||||
|
|
||||||
if (isUsernameOrEmailAttribute(attributeName)) {
|
if (isUsernameOrEmailAttribute(attributeName)) {
|
||||||
if (permissions == null) {
|
if (permissions == null) {
|
||||||
|
@ -324,16 +330,26 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
if (atts.isEmpty()) {
|
if (atts.isEmpty()) {
|
||||||
// attribute metadata doesn't exist so we have to add it. We keep it optional as Abstract base
|
// attribute metadata doesn't exist so we have to add it. We keep it optional as Abstract base
|
||||||
// doesn't require it.
|
// 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 {
|
} else {
|
||||||
final int localGuiOrder = guiOrder++;
|
final int localGuiOrder = guiOrder++;
|
||||||
// only add configured validators and annotations if attribute metadata exist
|
// 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 {
|
} else {
|
||||||
// always add validation for imuttable/read-only attributes
|
// always add validation for immutable/read-only attributes
|
||||||
validators.add(new AttributeValidatorMetadata(ImmutableAttributeValidator.ID));
|
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) {
|
private boolean isUsernameOrEmailAttribute(String attributeName) {
|
||||||
return UserModel.USERNAME.equals(attributeName) || UserModel.EMAIL.equals(attributeName);
|
return UserModel.USERNAME.equals(attributeName) || UserModel.EMAIL.equals(attributeName);
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@ public class UPAttribute {
|
||||||
private UPAttributePermissions permissions;
|
private UPAttributePermissions permissions;
|
||||||
/** null means it is always selected */
|
/** null means it is always selected */
|
||||||
private UPAttributeSelector selector;
|
private UPAttributeSelector selector;
|
||||||
|
private String group;
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return name;
|
return name;
|
||||||
|
@ -102,8 +103,16 @@ public class UPAttribute {
|
||||||
this.displayName = displayName;
|
this.displayName = displayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getGroup() {
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGroup(String group) {
|
||||||
|
this.group = group != null ? group.trim() : null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
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;
|
package org.keycloak.userprofile.config;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,6 +29,7 @@ import java.util.List;
|
||||||
public class UPConfig {
|
public class UPConfig {
|
||||||
|
|
||||||
private List<UPAttribute> attributes;
|
private List<UPAttribute> attributes;
|
||||||
|
private List<UPGroup> groups;
|
||||||
|
|
||||||
public List<UPAttribute> getAttributes() {
|
public List<UPAttribute> getAttributes() {
|
||||||
return attributes;
|
return attributes;
|
||||||
|
@ -47,8 +49,29 @@ public class UPConfig {
|
||||||
return this;
|
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
|
@Override
|
||||||
public String toString() {
|
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.io.InputStream;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.keycloak.common.util.StreamUtil;
|
import org.keycloak.common.util.StreamUtil;
|
||||||
|
@ -62,7 +64,7 @@ public class UPConfigUtils {
|
||||||
/**
|
/**
|
||||||
* Load configuration from JSON file.
|
* Load configuration from JSON file.
|
||||||
* <p>
|
* <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
|
* @param is JSON file to be loaded
|
||||||
* @return object representation of the configuration
|
* @return object representation of the configuration
|
||||||
|
@ -75,10 +77,12 @@ public class UPConfigUtils {
|
||||||
/**
|
/**
|
||||||
* Validate object representation of the configuration. Validations:
|
* Validate object representation of the configuration. Validations:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>defaultProfile is defined and exists in profiles
|
* <li>defaultProfile is defined and exists in profiles</li>
|
||||||
* <li>parent exists for type
|
* <li>parent exists for type</li>
|
||||||
* <li>type exists for attribute
|
* <li>type exists for attribute</li>
|
||||||
* <li>validator (from Validator SPI) exists for validation and it's config is correct
|
* <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>
|
* </ul>
|
||||||
*
|
*
|
||||||
* @param session to be used for Validator SPI integration
|
* @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
|
* @return list of errors, empty if no error found
|
||||||
*/
|
*/
|
||||||
public static List<String> validate(KeycloakSession session, UPConfig config) {
|
public static List<String> validate(KeycloakSession session, UPConfig config) {
|
||||||
|
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<>();
|
List<String> errors = new ArrayList<>();
|
||||||
|
Set<String> groups = config.getGroups().stream().map(g -> g.getName()).collect(Collectors.toSet());
|
||||||
|
|
||||||
if (config.getAttributes() != null) {
|
if (config.getAttributes() != null) {
|
||||||
Set<String> attNamesCache = new HashSet<>();
|
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 {
|
} else {
|
||||||
errors.add("UserProfile configuration without 'attributes' section is not allowed");
|
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 session to be used for Validator SPI integration
|
||||||
* @param attributeConfig config to be validated
|
* @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 errors to add error message in if something is invalid
|
||||||
* @param attNamesCache cache of already existing attribute names so we can check uniqueness
|
* @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();
|
String attributeName = attributeConfig.getName();
|
||||||
if (isBlank(attributeName)) {
|
if (isBlank(attributeName)) {
|
||||||
errors.add("Attribute configuration without 'name' is not allowed");
|
errors.add("Attribute configuration without 'name' is not allowed");
|
||||||
|
@ -138,6 +160,12 @@ public class UPConfigUtils {
|
||||||
if (attributeConfig.getSelector() != null) {
|
if (attributeConfig.getSelector() != null) {
|
||||||
validateScopes(attributeConfig.getSelector().getScopes(), "selector.scopes", attributeName, errors, session);
|
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) {
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -113,6 +113,70 @@ public class RequiredActionUpdateProfileWithUserProfileTest extends RequiredActi
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
@Test
|
||||||
public void testAttributeGuiOrder() {
|
public void testAttributeGuiOrder() {
|
||||||
|
|
||||||
|
|
|
@ -72,6 +72,74 @@ public class KcOidcFirstBrokerLoginWithUserProfileTest extends KcOidcFirstBroker
|
||||||
Assert.assertEquals("Department", updateAccountInformationPage.getLabelForField("department"));
|
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
|
@Test
|
||||||
public void testAttributeGuiOrder() {
|
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
|
@Test
|
||||||
public void testRegisterUserSuccess_requiredReadOnlyAttributeNotRenderedAndNotBlockingRegistration() {
|
public void testRegisterUserSuccess_requiredReadOnlyAttributeNotRenderedAndNotBlockingRegistration() {
|
||||||
|
|
||||||
|
|
|
@ -180,6 +180,72 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest {
|
||||||
Assert.assertEquals("Department",verifyProfilePage.getLabelForField("department"));
|
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
|
@Test
|
||||||
public void testAttributeGuiOrder() {
|
public void testAttributeGuiOrder() {
|
||||||
|
|
||||||
|
|
|
@ -54,6 +54,7 @@ import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||||
import org.keycloak.testsuite.runonserver.RunOnServer;
|
import org.keycloak.testsuite.runonserver.RunOnServer;
|
||||||
|
import org.keycloak.userprofile.AttributeGroupMetadata;
|
||||||
import org.keycloak.userprofile.DeclarativeUserProfileProvider;
|
import org.keycloak.userprofile.DeclarativeUserProfileProvider;
|
||||||
import org.keycloak.userprofile.config.UPAttribute;
|
import org.keycloak.userprofile.config.UPAttribute;
|
||||||
import org.keycloak.userprofile.config.UPAttributePermissions;
|
import org.keycloak.userprofile.config.UPAttributePermissions;
|
||||||
|
@ -349,6 +350,67 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
assertNotNull(attributes.getFirstValue("address"));
|
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
|
@Test
|
||||||
public void testCreateAndUpdateUser() {
|
public void testCreateAndUpdateUser() {
|
||||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::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.UPAttributeRequired;
|
||||||
import org.keycloak.userprofile.config.UPConfig;
|
import org.keycloak.userprofile.config.UPConfig;
|
||||||
import org.keycloak.userprofile.config.UPConfigUtils;
|
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>
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
*
|
*
|
||||||
|
@ -134,6 +135,19 @@ public class UPConfigParserTest extends AbstractTestRealmKeycloakTest {
|
||||||
//displayName
|
//displayName
|
||||||
att = config.getAttributes().get(4);
|
att = config.getAttributes().get(4);
|
||||||
Assert.assertEquals("${profile.phone}", att.getDisplayName());
|
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);
|
errors = validate(session, config);
|
||||||
Assert.assertEquals(1, errors.size());
|
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": [
|
"attributes": [
|
||||||
{
|
{
|
||||||
"name":"username",
|
"name":"username",
|
||||||
|
@ -46,6 +56,7 @@
|
||||||
"validations": {
|
"validations": {
|
||||||
"not-blank":{}
|
"not-blank":{}
|
||||||
},
|
},
|
||||||
|
"group": "contact",
|
||||||
"required": {
|
"required": {
|
||||||
"scopes" : ["phone-1", "phone-2"],
|
"scopes" : ["phone-1", "phone-2"],
|
||||||
"roles" : ["user", "admin"]
|
"roles" : ["user", "admin"]
|
||||||
|
|
|
@ -1,5 +1,35 @@
|
||||||
<#macro userProfileFormFields>
|
<#macro userProfileFormFields>
|
||||||
|
<#assign currentGroup="">
|
||||||
|
|
||||||
<#list profile.attributes as attribute>
|
<#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>
|
<#nested "beforeField" attribute>
|
||||||
<div class="${properties.kcFormGroupClass!}">
|
<div class="${properties.kcFormGroupClass!}">
|
||||||
<div class="${properties.kcLabelWrapperClass!}">
|
<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
|
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
|
##### css classes for form buttons
|
||||||
# main class used for all buttons
|
# main class used for all buttons
|
||||||
kcButtonClass=pf-c-button
|
kcButtonClass=pf-c-button
|
||||||
|
|
Loading…
Reference in a new issue