mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 15:02:05 -03:30
Allow setting default value for userprofile attribute
Closes #36160 Signed-off-by: huyenvu2101 <vhuyen2101@gmail.com>
This commit is contained in:
parent
1af235c4f1
commit
5436f9781c
@ -31,13 +31,14 @@ public class UserProfileAttributeMetadata {
|
||||
private Map<String, Map<String, Object>> validators;
|
||||
private String group;
|
||||
private boolean multivalued;
|
||||
private String defaultValue;
|
||||
|
||||
public UserProfileAttributeMetadata() {
|
||||
|
||||
}
|
||||
|
||||
public UserProfileAttributeMetadata(String name, String displayName, boolean required, boolean readOnly, String group, Map<String, Object> annotations,
|
||||
Map<String, Map<String, Object>> validators, boolean multivalued) {
|
||||
Map<String, Map<String, Object>> validators, boolean multivalued, String defaultValue) {
|
||||
this.name = name;
|
||||
this.displayName = displayName;
|
||||
this.required = required;
|
||||
@ -46,12 +47,17 @@ public class UserProfileAttributeMetadata {
|
||||
this.validators = validators;
|
||||
this.group = group;
|
||||
this.multivalued = multivalued;
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getDefaultValue() {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return display name, either direct string to display, or construct for i18n like <code>${i18nkey}</code>
|
||||
*/
|
||||
|
||||
@ -44,6 +44,7 @@ public class UPAttribute implements Cloneable {
|
||||
private UPAttributeSelector selector;
|
||||
private String group;
|
||||
private boolean multivalued;
|
||||
private String defaultValue;
|
||||
|
||||
public UPAttribute() {
|
||||
}
|
||||
@ -152,13 +153,21 @@ public class UPAttribute implements Cloneable {
|
||||
this.multivalued = multivalued;
|
||||
}
|
||||
|
||||
public String getDefaultValue() {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public void setDefaultValue(String defaultValue) {
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
public boolean isMultivalued() {
|
||||
return multivalued;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "UPAttribute [name=" + name + ", displayName=" + displayName + ", permissions=" + permissions + ", selector=" + selector + ", required=" + required + ", validations=" + validations + ", annotations=" + annotations + ", group=" + group + ", multivalued=" + multivalued + "]";
|
||||
return "UPAttribute [name=" + name + ", displayName=" + displayName + ", permissions=" + permissions + ", selector=" + selector + ", required=" + required + ", validations=" + validations + ", annotations=" + annotations + ", group=" + group + ", multivalued=" + multivalued + ", defaultValue=" + defaultValue + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -184,6 +193,7 @@ public class UPAttribute implements Cloneable {
|
||||
attr.setSelector(this.selector == null ? null : this.selector.clone());
|
||||
attr.setGroup(this.group);
|
||||
attr.setMultivalued(this.multivalued);
|
||||
attr.setDefaultValue(this.defaultValue);
|
||||
return attr;
|
||||
}
|
||||
|
||||
@ -209,6 +219,7 @@ public class UPAttribute implements Cloneable {
|
||||
&& Objects.equals(this.required, other.required)
|
||||
&& Objects.equals(this.permissions, other.permissions)
|
||||
&& Objects.equals(this.selector, other.selector)
|
||||
&& Objects.equals(this.multivalued, other.multivalued);
|
||||
&& Objects.equals(this.multivalued, other.multivalued)
|
||||
&& Objects.equals(this.defaultValue, other.defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
@ -160,6 +160,10 @@ Multivalued::
|
||||
If enabled, the attribute supports multiple values and UIs are rendered accordingly to allow setting many values. When enabling this
|
||||
setting, make sure to add a validator to set a hard limit to the number of values.
|
||||
|
||||
Default Value::
|
||||
Defines the value that will be automatically assigned to the attribute if the user does not provide one.
|
||||
Ensure that this default value complies with all configured validators for the attribute.
|
||||
|
||||
Attribute Group::
|
||||
The attribute group to which the attribute belongs to, if any.
|
||||
|
||||
|
||||
@ -87,6 +87,7 @@ export interface UserProfileAttributeMetadata {
|
||||
annotations?: { [index: string]: any };
|
||||
validators: { [index: string]: { [index: string]: any } };
|
||||
multivalued: boolean;
|
||||
defaultValue: string;
|
||||
}
|
||||
|
||||
export interface UserProfileMetadata {
|
||||
|
||||
@ -3156,6 +3156,8 @@ bruteForceMode=Brute Force Mode
|
||||
error-invalid-multivalued-size=Attribute {{0}} must have at least {{1}} and at most {{2}} value(s).
|
||||
multivalued=Multivalued
|
||||
multivaluedHelp=If this attribute supports multiple values. This setting is an indicator and does not enable any validation.
|
||||
defaultValue=Default value
|
||||
defaultValueHelp=Default value when attribute value is not specified.
|
||||
to the attribute. For that, make sure to use any of the built-in validators to properly validate the size and the values.
|
||||
sendIdTokenOnLogout=Send 'id_token_hint' in logout requests
|
||||
sendIdTokenOnLogoutHelp=If the 'id_token_hint' parameter should be sent in logout requests.
|
||||
|
||||
@ -149,6 +149,7 @@ export default function NewAttributeSettings() {
|
||||
selector,
|
||||
required,
|
||||
multivalued,
|
||||
defaultValue,
|
||||
...values
|
||||
} = config.attributes!.find(
|
||||
(attribute) => attribute.name === attributeName,
|
||||
@ -180,6 +181,7 @@ export default function NewAttributeSettings() {
|
||||
);
|
||||
form.setValue("isRequired", required !== undefined);
|
||||
form.setValue("multivalued", multivalued === true);
|
||||
form.setValue("defaultValue", defaultValue);
|
||||
},
|
||||
[],
|
||||
);
|
||||
@ -229,6 +231,7 @@ export default function NewAttributeSettings() {
|
||||
annotations,
|
||||
validations,
|
||||
},
|
||||
formFields.defaultValue ? { defaultValue: formFields.defaultValue } : { defaultValue: null },
|
||||
formFields.isRequired ? { required: formFields.required } : undefined,
|
||||
formFields.group ? { group: formFields.group } : { group: null },
|
||||
);
|
||||
@ -247,6 +250,7 @@ export default function NewAttributeSettings() {
|
||||
annotations,
|
||||
validations,
|
||||
},
|
||||
formFields.defaultValue ? { defaultValue: formFields.defaultValue } : { defaultValue: null },
|
||||
formFields.isRequired ? { required: formFields.required } : undefined,
|
||||
formFields.group ? { group: formFields.group } : undefined,
|
||||
),
|
||||
|
||||
@ -139,6 +139,11 @@ export const AttributeGeneralSettings = () => {
|
||||
label={t("multivalued")}
|
||||
labelIcon={t("multivaluedHelp")}
|
||||
/>
|
||||
<TextControl
|
||||
name="defaultValue"
|
||||
label={t("defaultValue")}
|
||||
labelIcon={t("defaultValueHelp")}
|
||||
/>
|
||||
<SelectControl
|
||||
name="group"
|
||||
label={t("attributeGroup")}
|
||||
|
||||
@ -15,6 +15,7 @@ export interface UserProfileAttribute {
|
||||
displayName?: string;
|
||||
group?: string;
|
||||
multivalued?: boolean;
|
||||
defaultValue?: string;
|
||||
}
|
||||
export interface UserProfileAttributeRequired {
|
||||
roles?: string[];
|
||||
@ -43,6 +44,7 @@ export interface UserProfileAttributeMetadata {
|
||||
annotations?: Record<string, unknown>;
|
||||
validators?: Record<string, Record<string, unknown>>;
|
||||
multivalued?: boolean;
|
||||
defaultValue?: string;
|
||||
}
|
||||
|
||||
export interface UserProfileAttributeGroupMetadata {
|
||||
|
||||
@ -29,6 +29,7 @@ export const MultiInputComponent = ({
|
||||
form={form}
|
||||
aria-label={labelAttribute(t, attribute)}
|
||||
name={fieldName(attribute.name)!}
|
||||
defaultValue={[attribute.defaultValue || ""]}
|
||||
addButtonLabel={t("addMultivaluedLabel", {
|
||||
fieldLabel: labelAttribute(t, attribute),
|
||||
})}
|
||||
|
||||
@ -27,7 +27,7 @@ export const OptionComponent = (props: UserProfileFieldProps) => {
|
||||
<Controller
|
||||
name={fieldName(attribute.name)}
|
||||
control={form.control}
|
||||
defaultValue=""
|
||||
defaultValue={attribute.defaultValue}
|
||||
render={({ field }) => (
|
||||
<>
|
||||
{options.map((option) => (
|
||||
|
||||
@ -66,7 +66,7 @@ export const SelectComponent = (props: UserProfileFieldProps) => {
|
||||
<UserProfileGroup {...props}>
|
||||
<Controller
|
||||
name={fieldName(attribute.name)}
|
||||
defaultValue=""
|
||||
defaultValue={attribute.defaultValue}
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<KeycloakSelect
|
||||
|
||||
@ -17,6 +17,7 @@ export const TextAreaComponent = (props: UserProfileFieldProps) => {
|
||||
rows={attribute.annotations?.["inputTypeRows"] as number}
|
||||
readOnly={attribute.readOnly}
|
||||
isRequired={isRequired}
|
||||
value={attribute.defaultValue}
|
||||
/>
|
||||
</UserProfileGroup>
|
||||
);
|
||||
|
||||
@ -31,6 +31,7 @@ export const TextComponent = (props: UserProfileFieldProps) => {
|
||||
}
|
||||
isDisabled={attribute.readOnly}
|
||||
isRequired={isRequired}
|
||||
value={attribute.defaultValue}
|
||||
{...form.register(fieldName(attribute.name))}
|
||||
/>
|
||||
</UserProfileGroup>
|
||||
|
||||
@ -410,14 +410,17 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
||||
}
|
||||
}
|
||||
|
||||
// the profile should always hold all attributes defined in the config
|
||||
// the profile should always hold all attributes defined in the config and set default value if needed
|
||||
for (String attributeName : metadataByAttribute.keySet()) {
|
||||
if (!isSupportedAttribute(attributeName) || newAttributes.containsKey(attributeName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<String> values = EMPTY_VALUE;
|
||||
AttributeMetadata metadata = metadataByAttribute.get(attributeName);
|
||||
List<String> values = EMPTY_VALUE;
|
||||
if (metadata.getDefaultValue() != null) {
|
||||
values = Collections.singletonList(metadata.getDefaultValue());
|
||||
}
|
||||
|
||||
if (user != null && isIncludeAttributeIfNotProvided(metadata)) {
|
||||
values = normalizeAttributeValues(attributeName, user.getAttributes().getOrDefault(attributeName, EMPTY_VALUE));
|
||||
|
||||
@ -164,7 +164,8 @@ public class UserProfileUtil {
|
||||
group,
|
||||
attributes.getAnnotations(am.getName()),
|
||||
toValidatorMetadata(am, session),
|
||||
am.isMultivalued());
|
||||
am.isMultivalued(),
|
||||
am.getDefaultValue());
|
||||
}
|
||||
|
||||
private static Map<String, Map<String, Object>> toValidatorMetadata(AttributeMetadata am, KeycloakSession session){
|
||||
|
||||
@ -53,6 +53,7 @@ public class AttributeMetadata {
|
||||
private Map<String, Object> annotations;
|
||||
private int guiOrder;
|
||||
private boolean multivalued;
|
||||
private String defaultValue;
|
||||
private Function<AttributeContext, Map<String, Object>> annotationDecorator = (c) -> c.getMetadata().getAnnotations();
|
||||
|
||||
AttributeMetadata(String attributeName, int guiOrder) {
|
||||
@ -116,6 +117,10 @@ public class AttributeMetadata {
|
||||
return attributeName;
|
||||
}
|
||||
|
||||
public String getDefaultValue() {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public int getGuiOrder() {
|
||||
return guiOrder;
|
||||
}
|
||||
@ -226,6 +231,7 @@ public class AttributeMetadata {
|
||||
cloned.setAttributeGroupMetadata(attributeGroupMetadata.clone());
|
||||
}
|
||||
cloned.setMultivalued(multivalued);
|
||||
cloned.setDefaultValue(defaultValue);
|
||||
cloned.setAnnotationDecorator(annotationDecorator);
|
||||
return cloned;
|
||||
}
|
||||
@ -273,6 +279,11 @@ public class AttributeMetadata {
|
||||
return this;
|
||||
}
|
||||
|
||||
public AttributeMetadata setDefaultValue(String defaultValue) {
|
||||
this.defaultValue = defaultValue;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Map<String, Object> getAnnotations(AttributeContext context) {
|
||||
return annotationDecorator.apply(context);
|
||||
}
|
||||
|
||||
@ -151,6 +151,10 @@ public abstract class AbstractUserProfileBean {
|
||||
return metadata.isMultivalued();
|
||||
}
|
||||
|
||||
public String getDefaultValue() {
|
||||
return metadata.getDefaultValue();
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
List<String> v = getValues();
|
||||
if (v == null || v.isEmpty()) {
|
||||
|
||||
@ -398,6 +398,7 @@ public class DeclarativeUserProfileProvider implements UserProfileProvider {
|
||||
.addWriteCondition(writeAllowed)
|
||||
.addValidators(validators)
|
||||
.setRequired(required)
|
||||
.setDefaultValue(attrConfig.getDefaultValue())
|
||||
.setMultivalued(attrConfig.isMultivalued());
|
||||
}
|
||||
} else {
|
||||
@ -405,6 +406,7 @@ public class DeclarativeUserProfileProvider implements UserProfileProvider {
|
||||
.addAnnotations(annotations)
|
||||
.setAttributeDisplayName(attrConfig.getDisplayName())
|
||||
.setAttributeGroupMetadata(groupMetadata)
|
||||
.setDefaultValue(attrConfig.getDefaultValue())
|
||||
.setMultivalued(attrConfig.isMultivalued());
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,6 +45,7 @@ import org.keycloak.representations.userprofile.config.UPAttribute;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig;
|
||||
import org.keycloak.userprofile.UserProfileConstants;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.validate.ValidationContext;
|
||||
import org.keycloak.validate.ValidationResult;
|
||||
import org.keycloak.validate.ValidatorConfig;
|
||||
import org.keycloak.validate.Validators;
|
||||
@ -184,6 +185,15 @@ public class UPConfigUtils {
|
||||
}
|
||||
if (attributeConfig.getValidations() != null) {
|
||||
attributeConfig.getValidations().forEach((validator, validatorConfig) -> validateValidationConfig(session, validator, validatorConfig, attributeName, errors));
|
||||
|
||||
if (attributeConfig.getDefaultValue() != null) {
|
||||
attributeConfig.getValidations().forEach((validator, validatorConfig) -> {
|
||||
ValidationContext context = Validators.validator(session, validator).validate(attributeConfig.getDefaultValue(), attributeName, ValidatorConfig.configFromMap(validatorConfig));
|
||||
if (!context.isValid()) {
|
||||
errors.add("Default value is invalid");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (attributeConfig.getPermissions() != null) {
|
||||
if (attributeConfig.getPermissions().getView() != null) {
|
||||
|
||||
@ -2396,6 +2396,48 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testMultivalued);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultValue() {
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testInvalidConfigDefaultValue);
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testDefaultValue);
|
||||
}
|
||||
|
||||
private static void testInvalidConfigDefaultValue(KeycloakSession session) {
|
||||
UserProfileProvider provider = getUserProfileProvider(session);
|
||||
UPConfig upConfig = UPConfigUtils.parseSystemDefaultConfig();
|
||||
provider.setConfiguration(upConfig);
|
||||
|
||||
UPAttribute foo = new UPAttribute("foo", new UPAttributePermissions(Set.of(), Set.of(UserProfileConstants.ROLE_ADMIN)));
|
||||
foo.setDefaultValue("def");
|
||||
foo.setValidations(Map.of("length", Map.of("min", "5", "max", "15")));
|
||||
upConfig.addOrReplaceAttribute(foo);
|
||||
|
||||
try {
|
||||
provider.setConfiguration(upConfig);
|
||||
fail("Should fail because default value is not reach min length");
|
||||
} catch (ComponentValidationException cve) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
private static void testDefaultValue(KeycloakSession session) {
|
||||
UserProfileProvider provider = getUserProfileProvider(session);
|
||||
UPConfig upConfig = UPConfigUtils.parseSystemDefaultConfig();
|
||||
UPAttribute foo = new UPAttribute("foo", new UPAttributePermissions(Set.of(), Set.of(UserProfileConstants.ROLE_ADMIN)));
|
||||
foo.setDefaultValue("def");
|
||||
upConfig.addOrReplaceAttribute(foo);
|
||||
provider.setConfiguration(upConfig);
|
||||
|
||||
String userName = org.keycloak.models.utils.KeycloakModelUtils.generateId();
|
||||
Map<String, List<String>> attributes = new HashMap<>();
|
||||
attributes.put(UserModel.USERNAME, List.of(userName));
|
||||
UserProfile profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||
UserModel user = profile.create();
|
||||
List<String> actualValue = user.getAttributes().get("foo");
|
||||
List<String> expectedValue = List.of("def");
|
||||
assertThat(actualValue, Matchers.equalTo(expectedValue));
|
||||
}
|
||||
|
||||
private static void testMultivalued(KeycloakSession session) {
|
||||
UserProfileProvider provider = getUserProfileProvider(session);
|
||||
UPConfig upConfig = UPConfigUtils.parseSystemDefaultConfig();
|
||||
|
||||
@ -81,7 +81,7 @@
|
||||
<@inputTag attribute=attribute value=value!''/>
|
||||
</#list>
|
||||
<#else>
|
||||
<@inputTag attribute=attribute value=attribute.value!''/>
|
||||
<@inputTag attribute=attribute value=(attribute.value!attribute.defaultValue!'')/>
|
||||
</#if>
|
||||
</#switch>
|
||||
</#macro>
|
||||
@ -129,7 +129,7 @@
|
||||
<#if attribute.annotations.inputTypeCols??>cols="${attribute.annotations.inputTypeCols}"</#if>
|
||||
<#if attribute.annotations.inputTypeRows??>rows="${attribute.annotations.inputTypeRows}"</#if>
|
||||
<#if attribute.annotations.inputTypeMaxlength??>maxlength="${attribute.annotations.inputTypeMaxlength}"</#if>
|
||||
>${(attribute.value!'')}</textarea>
|
||||
>${(attribute.value!attribute.defaultValue!'')}</textarea>
|
||||
</span>
|
||||
</#macro>
|
||||
|
||||
@ -153,8 +153,13 @@
|
||||
<#assign options=[]>
|
||||
</#if>
|
||||
|
||||
<#assign selectedValues = attribute.values![]>
|
||||
<#if !selectedValues?has_content && (attribute.defaultValue??)>
|
||||
<#assign selectedValues = [attribute.defaultValue]>
|
||||
</#if>
|
||||
|
||||
<#list options as option>
|
||||
<option value="${option}" <#if attribute.values?seq_contains(option)>selected</#if>><@selectOptionLabelText attribute=attribute option=option/></option>
|
||||
<option value="${option}" <#if selectedValues?seq_contains(option)>selected</#if>><@selectOptionLabelText attribute=attribute option=option/></option>
|
||||
</#list>
|
||||
</select>
|
||||
<span class="${properties.kcFormControlUtilClass}">
|
||||
@ -200,16 +205,21 @@
|
||||
<#assign options=[]>
|
||||
</#if>
|
||||
|
||||
<#list options as option>
|
||||
<div class="${classDiv}">
|
||||
<input type="${inputType}" id="${attribute.name}-${option}" name="${attribute.name}" value="${option}" class="${classInput}"
|
||||
aria-invalid="<#if messagesPerField.existsError('${attribute.name}')>true</#if>"
|
||||
<#if attribute.readOnly>disabled</#if>
|
||||
<#if attribute.values?seq_contains(option)>checked</#if>
|
||||
/>
|
||||
<label for="${attribute.name}-${option}" class="${classLabel}<#if attribute.readOnly> ${properties.kcInputClassRadioCheckboxLabelDisabled!}</#if>"><@selectOptionLabelText attribute=attribute option=option/></label>
|
||||
</div>
|
||||
</#list>
|
||||
<#assign selectedValues = attribute.values![]>
|
||||
<#if !selectedValues?has_content && (attribute.defaultValue??)>
|
||||
<#assign selectedValues = [attribute.defaultValue]>
|
||||
</#if>
|
||||
|
||||
<#list options as option>
|
||||
<div class="${classDiv}">
|
||||
<input type="${inputType}" id="${attribute.name}-${option}" name="${attribute.name}" value="${option}" class="${classInput}"
|
||||
aria-invalid="<#if messagesPerField.existsError('${attribute.name}')>true</#if>"
|
||||
<#if attribute.readOnly>disabled</#if>
|
||||
<#if selectedValues?seq_contains(option)>checked</#if>
|
||||
/>
|
||||
<label for="${attribute.name}-${option}" class="${classLabel}<#if attribute.readOnly> ${properties.kcInputClassRadioCheckboxLabelDisabled!}</#if>"><@selectOptionLabelText attribute=attribute option=option/></label>
|
||||
</div>
|
||||
</#list>
|
||||
</#macro>
|
||||
|
||||
<#macro selectOptionLabelText attribute option>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user