[KEYCLOAK-18424] GUI order for user profile attributes
This commit is contained in:
parent
b26b41332e
commit
f32447bcc1
8 changed files with 124 additions and 38 deletions
|
@ -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) {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
||||
|
|
|
@ -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.");
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue