[KEYCLOAK-18424] GUI order for user profile attributes

This commit is contained in:
Vlastimil Elias 2021-06-30 14:22:50 +02:00 committed by Pedro Igor
parent b26b41332e
commit f32447bcc1
8 changed files with 124 additions and 38 deletions

View file

@ -50,21 +50,23 @@ public final class AttributeMetadata {
private final Predicate<AttributeContext> readAllowed;
private List<AttributeValidatorMetadata> validators;
private Map<String, Object> annotations;
private int guiOrder;
AttributeMetadata(String attributeName) {
this(attributeName, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE);
AttributeMetadata(String attributeName, int guiOrder) {
this(attributeName, guiOrder, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE);
}
AttributeMetadata(String attributeName, Predicate<AttributeContext> writeAllowed, Predicate<AttributeContext> required) {
this(attributeName, ALWAYS_TRUE, writeAllowed, required, ALWAYS_TRUE);
AttributeMetadata(String attributeName, int guiOrder, Predicate<AttributeContext> writeAllowed, Predicate<AttributeContext> required) {
this(attributeName, guiOrder, ALWAYS_TRUE, writeAllowed, required, ALWAYS_TRUE);
}
AttributeMetadata(String attributeName, Predicate<AttributeContext> selector) {
this(attributeName, selector, ALWAYS_FALSE, ALWAYS_TRUE, ALWAYS_TRUE);
AttributeMetadata(String attributeName, int guiOrder, Predicate<AttributeContext> selector) {
this(attributeName, guiOrder, selector, ALWAYS_FALSE, ALWAYS_TRUE, ALWAYS_TRUE);
}
AttributeMetadata(String attributeName, List<String> scopes, Predicate<AttributeContext> writeAllowed, Predicate<AttributeContext> required) {
this(attributeName, context -> {
AttributeMetadata(String attributeName, int guiOrder, List<String> scopes, Predicate<AttributeContext> writeAllowed, Predicate<AttributeContext> required) {
this(attributeName, guiOrder, context -> {
KeycloakSession session = context.getSession();
AuthenticationSessionModel authSession = session.getContext().getAuthenticationSession();
@ -86,7 +88,7 @@ public final class AttributeMetadata {
}, writeAllowed, required, ALWAYS_TRUE);
}
AttributeMetadata(String attributeName, Predicate<AttributeContext> selector, Predicate<AttributeContext> writeAllowed,
AttributeMetadata(String attributeName, int guiOrder, Predicate<AttributeContext> selector, Predicate<AttributeContext> writeAllowed,
Predicate<AttributeContext> required,
Predicate<AttributeContext> readAllowed) {
this.attributeName = attributeName;
@ -94,12 +96,22 @@ public final class AttributeMetadata {
this.writeAllowed = writeAllowed;
this.required = required;
this.readAllowed = readAllowed;
this.guiOrder = guiOrder;
}
public String getName() {
return attributeName;
}
public int getGuiOrder() {
return guiOrder;
}
public AttributeMetadata setGuiOrder(int guiOrder) {
this.guiOrder = guiOrder;
return this;
}
public boolean isSelected(AttributeContext context) {
return selector.test(context);
}
@ -157,7 +169,7 @@ public final class AttributeMetadata {
@Override
public AttributeMetadata clone() {
AttributeMetadata cloned = new AttributeMetadata(attributeName, selector, writeAllowed, required, readAllowed);
AttributeMetadata cloned = new AttributeMetadata(attributeName, guiOrder, selector, writeAllowed, required, readAllowed);
// we clone validators list to allow adding or removing validators. Validators
// itself are not cloned as we do not expect them to be reconfigured.
if (validators != null) {

View file

@ -56,24 +56,24 @@ public final class UserProfileMetadata implements Cloneable {
return metadata;
}
public AttributeMetadata addAttribute(String name, AttributeValidatorMetadata... validator) {
return addAttribute(name, Arrays.asList(validator));
public AttributeMetadata addAttribute(String name, int guiOrder, AttributeValidatorMetadata... validator) {
return addAttribute(name, guiOrder, Arrays.asList(validator));
}
public AttributeMetadata addAttribute(String name, Predicate<AttributeContext> writeAllowed, Predicate<AttributeContext> readAllowed, AttributeValidatorMetadata... validator) {
return addAttribute(new AttributeMetadata(name, ALWAYS_TRUE, writeAllowed, ALWAYS_TRUE, readAllowed).addValidator(Arrays.asList(validator)));
public AttributeMetadata addAttribute(String name, int guiOrder, Predicate<AttributeContext> writeAllowed, Predicate<AttributeContext> readAllowed, AttributeValidatorMetadata... validator) {
return addAttribute(new AttributeMetadata(name, guiOrder, ALWAYS_TRUE, writeAllowed, ALWAYS_TRUE, readAllowed).addValidator(Arrays.asList(validator)));
}
public AttributeMetadata addAttribute(String name, Predicate<AttributeContext> writeAllowed, List<AttributeValidatorMetadata> validators) {
return addAttribute(new AttributeMetadata(name, ALWAYS_TRUE, writeAllowed, ALWAYS_TRUE, ALWAYS_TRUE).addValidator(validators));
public AttributeMetadata addAttribute(String name, int guiOrder, Predicate<AttributeContext> writeAllowed, List<AttributeValidatorMetadata> validators) {
return addAttribute(new AttributeMetadata(name, guiOrder, ALWAYS_TRUE, writeAllowed, ALWAYS_TRUE, ALWAYS_TRUE).addValidator(validators));
}
public AttributeMetadata addAttribute(String name, List<AttributeValidatorMetadata> validators) {
return addAttribute(new AttributeMetadata(name).addValidator(validators));
public AttributeMetadata addAttribute(String name, int guiOrder, List<AttributeValidatorMetadata> validators) {
return addAttribute(new AttributeMetadata(name, guiOrder).addValidator(validators));
}
public AttributeMetadata addAttribute(String name, List<AttributeValidatorMetadata> validator, Predicate<AttributeContext> selector, Predicate<AttributeContext> writeAllowed, Predicate<AttributeContext> required, Predicate<AttributeContext> readAllowed) {
return addAttribute(new AttributeMetadata(name, selector, writeAllowed, required, readAllowed).addValidator(validator));
public AttributeMetadata addAttribute(String name, int guiOrder, List<AttributeValidatorMetadata> validator, Predicate<AttributeContext> selector, Predicate<AttributeContext> writeAllowed, Predicate<AttributeContext> required, Predicate<AttributeContext> readAllowed) {
return addAttribute(new AttributeMetadata(name, guiOrder, selector, writeAllowed, required, readAllowed).addValidator(validator));
}
/**

View file

@ -159,7 +159,7 @@ public abstract class AbstractUserProfileBean {
@Override
public int compareTo(Attribute o) {
return getName().compareTo(o.getName());
return Integer.compare(metadata.getGuiOrder(), o.metadata.getGuiOrder());
}
}
}

View file

@ -267,11 +267,11 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
private UserProfileMetadata createRegistrationUserCreationProfile() {
UserProfileMetadata metadata = new UserProfileMetadata(REGISTRATION_USER_CREATION);
metadata.addAttribute(UserModel.USERNAME, new AttributeValidatorMetadata(RegistrationEmailAsUsernameUsernameValueValidator.ID), new AttributeValidatorMetadata(RegistrationUsernameExistsValidator.ID));
metadata.addAttribute(UserModel.USERNAME, -2, new AttributeValidatorMetadata(RegistrationEmailAsUsernameUsernameValueValidator.ID), new AttributeValidatorMetadata(RegistrationUsernameExistsValidator.ID));
metadata.addAttribute(UserModel.EMAIL, new AttributeValidatorMetadata(RegistrationEmailAsUsernameEmailValueValidator.ID));
metadata.addAttribute(UserModel.EMAIL, -1, new AttributeValidatorMetadata(RegistrationEmailAsUsernameEmailValueValidator.ID));
metadata.addAttribute(READ_ONLY_ATTRIBUTE_KEY, createReadOnlyAttributeUnchangedValidator(readOnlyAttributesPattern));
metadata.addAttribute(READ_ONLY_ATTRIBUTE_KEY, 1000, createReadOnlyAttributeUnchangedValidator(readOnlyAttributesPattern));
return metadata;
}
@ -279,13 +279,13 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
private UserProfileMetadata createDefaultProfile(UserProfileContext context, AttributeValidatorMetadata readOnlyValidator) {
UserProfileMetadata metadata = new UserProfileMetadata(context);
metadata.addAttribute(UserModel.USERNAME, AbstractUserProfileProvider::editUsernameCondition,
metadata.addAttribute(UserModel.USERNAME, -2, AbstractUserProfileProvider::editUsernameCondition,
AbstractUserProfileProvider::editUsernameCondition,
new AttributeValidatorMetadata(UsernameHasValueValidator.ID),
new AttributeValidatorMetadata(DuplicateUsernameValidator.ID),
new AttributeValidatorMetadata(UsernameMutationValidator.ID)).setAttributeDisplayName("${username}");
metadata.addAttribute(UserModel.EMAIL, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_EMAIL)),
metadata.addAttribute(UserModel.EMAIL, -1, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_EMAIL)),
new AttributeValidatorMetadata(EmailValidator.ID, ValidatorConfig.builder().config(EmailValidator.IGNORE_EMPTY_VALUE, true).build()),
new AttributeValidatorMetadata(DuplicateEmailValidator.ID),
new AttributeValidatorMetadata(EmailExistsAsUsernameValidator.ID)).setAttributeDisplayName("${email}");
@ -298,7 +298,7 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
readonlyValidators.add(readOnlyValidator);
}
metadata.addAttribute(READ_ONLY_ATTRIBUTE_KEY, readonlyValidators);
metadata.addAttribute(READ_ONLY_ATTRIBUTE_KEY, 1000, readonlyValidators);
return metadata;
}
@ -306,9 +306,9 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
private UserProfileMetadata createBrokeringProfile(AttributeValidatorMetadata readOnlyValidator) {
UserProfileMetadata metadata = new UserProfileMetadata(IDP_REVIEW);
metadata.addAttribute(UserModel.USERNAME, new AttributeValidatorMetadata(BrokeringFederatedUsernameHasValueValidator.ID)).setAttributeDisplayName("${username}");
metadata.addAttribute(UserModel.USERNAME, -2, new AttributeValidatorMetadata(BrokeringFederatedUsernameHasValueValidator.ID)).setAttributeDisplayName("${username}");
metadata.addAttribute(UserModel.EMAIL, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_EMAIL)),
metadata.addAttribute(UserModel.EMAIL, -1, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_EMAIL)),
new AttributeValidatorMetadata(EmailValidator.ID)).setAttributeDisplayName("${email}");
List<AttributeValidatorMetadata> readonlyValidators = new ArrayList<>();
@ -319,7 +319,7 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
readonlyValidators.add(readOnlyValidator);
}
metadata.addAttribute(READ_ONLY_ATTRIBUTE_KEY, readonlyValidators);
metadata.addAttribute(READ_ONLY_ATTRIBUTE_KEY, 1000, readonlyValidators);
return metadata;
}
@ -335,7 +335,7 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
readonlyValidators.add(createReadOnlyAttributeUnchangedValidator(adminReadOnlyAttributesPattern));
metadata.addAttribute(READ_ONLY_ATTRIBUTE_KEY, readonlyValidators);
metadata.addAttribute(READ_ONLY_ATTRIBUTE_KEY, 1000, readonlyValidators);
return metadata;
}

View file

@ -133,9 +133,9 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
if (!isEnabled(session)) {
if(!context.equals(UserProfileContext.USER_API) && !context.equals(UserProfileContext.REGISTRATION_USER_CREATION)) {
decoratedMetadata.addAttribute(UserModel.FIRST_NAME, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(
decoratedMetadata.addAttribute(UserModel.FIRST_NAME, 1, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(
Messages.MISSING_FIRST_NAME))).setAttributeDisplayName("${firstName}");
decoratedMetadata.addAttribute(UserModel.LAST_NAME, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_LAST_NAME))).setAttributeDisplayName("${lastName}");
decoratedMetadata.addAttribute(UserModel.LAST_NAME, 2, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_LAST_NAME))).setAttributeDisplayName("${lastName}");
return decoratedMetadata;
}
}
@ -254,6 +254,7 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
return decoratedMetadata;
}
int guiOrder = 0;
for (UPAttribute attrConfig : parsedConfig.getAttributes()) {
String attributeName = attrConfig.getName();
List<AttributeValidatorMetadata> validators = new ArrayList<>();
@ -322,15 +323,16 @@ 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, writeAllowed, validators).addAnnotations(annotations).setAttributeDisplayName(attrConfig.getDisplayName());
decoratedMetadata.addAttribute(attributeName, guiOrder++, writeAllowed, validators).addAnnotations(annotations).setAttributeDisplayName(attrConfig.getDisplayName());
} 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()));
atts.stream().forEach(c -> c.addValidator(validators).addAnnotations(annotations).setAttributeDisplayName(attrConfig.getDisplayName()).setGuiOrder(localGuiOrder));
}
} else {
// always add validation for imuttable/read-only attributes
validators.add(new AttributeValidatorMetadata(ImmutableAttributeValidator.ID));
decoratedMetadata.addAttribute(attributeName, validators, selector, writeAllowed, required, readAllowed).addAnnotations(annotations).setAttributeDisplayName(attrConfig.getDisplayName());
decoratedMetadata.addAttribute(attributeName, guiOrder++, validators, selector, writeAllowed, required, readAllowed).addAnnotations(annotations).setAttributeDisplayName(attrConfig.getDisplayName());
}
}

View file

@ -46,6 +46,7 @@ import org.keycloak.testsuite.pages.VerifyEmailPage;
import org.keycloak.testsuite.util.ClientScopeBuilder;
import org.keycloak.testsuite.util.GreenMailRule;
import org.keycloak.testsuite.util.KeycloakModelUtils;
import org.openqa.selenium.By;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -267,6 +268,60 @@ public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest {
Assert.assertEquals("Department",registerPage.getLabelForField("department"));
}
@Test
public void testAttributeGuiOrder() {
setUserProfileConfiguration("{\"attributes\": ["
+ "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + "},"
+ "{\"name\": \"department\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\":{}},"
+ "{\"name\": \"username\", " + VerifyProfileTest.PERMISSIONS_ALL + "},"
+ "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}},"
+ "{\"name\": \"email\", " + VerifyProfileTest.PERMISSIONS_ALL + "}"
+ "]}");
loginPage.open();
loginPage.clickRegister();
registerPage.assertCurrent();
//assert fields location in form
Assert.assertTrue(
driver.findElement(
By.cssSelector("form#kc-register-form > div:nth-child(1) > div:nth-child(2) > input#lastName")
).isDisplayed()
);
Assert.assertTrue(
driver.findElement(
By.cssSelector("form#kc-register-form > div:nth-child(2) > div:nth-child(2) > input#department")
).isDisplayed()
);
Assert.assertTrue(
driver.findElement(
By.cssSelector("form#kc-register-form > div:nth-child(3) > div:nth-child(2) > input#username")
).isDisplayed()
);
Assert.assertTrue(
driver.findElement(
By.cssSelector("form#kc-register-form > div:nth-child(4) > div:nth-child(2) > input#password")
).isDisplayed()
);
Assert.assertTrue(
driver.findElement(
By.cssSelector("form#kc-register-form > div:nth-child(5) > div:nth-child(2) > input#password-confirm")
).isDisplayed()
);
Assert.assertTrue(
driver.findElement(
By.cssSelector("form#kc-register-form > div:nth-child(6) > div:nth-child(2) > input#firstName")
).isDisplayed()
);
Assert.assertTrue(
driver.findElement(
By.cssSelector("form#kc-register-form > div:nth-child(7) > div:nth-child(2) > input#email")
).isDisplayed()
);
}
@Test
public void testRegisterUserSuccess_requiredReadOnlyAttributeNotRenderedAndNotBlockingRegistration() {

View file

@ -1536,6 +1536,19 @@ module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, clientS
return attributeName != "username" && attributeName != "email";
};
$scope.guiOrderUp = function(index) {
$scope.moveAttribute(index, index - 1);
};
$scope.guiOrderDown = function(index) {
$scope.moveAttribute(index, index + 1);
};
$scope.moveAttribute = function(old_index, new_index){
$scope.config.attributes.splice(new_index, 0, $scope.config.attributes.splice(old_index, 1)[0]);
$scope.save();
}
$scope.removeAttribute = function(attribute) {
Dialog.confirmDelete(attribute.name, 'attribute', function() {
let newAttributes = [];
@ -1737,7 +1750,7 @@ module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, clientS
delete $scope.canUserEdit;
delete $scope.canAdminEdit;
$route.reload();
Notifications.success("The attribute has been added.");
Notifications.success("User Profile configuration has been saved.");
});
};

View file

@ -30,7 +30,11 @@
</thead>
<tbody>
<tr ng-repeat="attribute in config.attributes">
<td><a href="" data-ng-click="edit(attribute)">{{attribute.name}}</a></td>
<td class="kc-sorter">
<button data-ng-hide="flow.builtIn" data-ng-disabled="$first" class="btn btn-default btn-sm" data-ng-click="guiOrderUp($index)"><i class="fa fa-angle-up"></i></button>
<button data-ng-hide="flow.builtIn" data-ng-disabled="$last" class="btn btn-default btn-sm" data-ng-click="guiOrderDown($index)"><i class="fa fa-angle-down"></i></button>
<span><a href="" data-ng-click="edit(attribute)">{{attribute.name}}</a></span>
</td>
<td>{{attribute.displayName}}</td>
<td class="kc-action-cell" data-ng-click="edit(attribute)">{{:: 'edit' | translate}}</td>
<td class="kc-action-cell" data-ng-click="removeAttribute(attribute)">{{:: 'delete' | translate}}</td>