Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ public final class AttributeMetadata {
private String attributeDisplayName;
private AttributeGroupMetadata attributeGroupMetadata;
private final Predicate<AttributeContext> selector;
private final Predicate<AttributeContext> writeAllowed;
private final List<Predicate<AttributeContext>> writeAllowed = new ArrayList<>();
/** Predicate to decide if attribute is required, it is handled as required if predicate is null */
private final Predicate<AttributeContext> required;
private final Predicate<AttributeContext> readAllowed;
private final List<Predicate<AttributeContext>> readAllowed = new ArrayList<>();
private List<AttributeValidatorMetadata> validators;
private Map<String, Object> annotations;
private int guiOrder;
Expand Down Expand Up @@ -93,11 +93,22 @@ public final class AttributeMetadata {
Predicate<AttributeContext> required,
Predicate<AttributeContext> readAllowed) {
this.attributeName = attributeName;
this.guiOrder = guiOrder;
this.selector = selector;
this.writeAllowed = writeAllowed;
addWriteCondition(writeAllowed);
this.required = required;
this.readAllowed = readAllowed;
addReadCondition(readAllowed);
}

AttributeMetadata(String attributeName, int guiOrder, Predicate<AttributeContext> selector, List<Predicate<AttributeContext>> writeAllowed,
Predicate<AttributeContext> required,
List<Predicate<AttributeContext>> readAllowed) {
this.attributeName = attributeName;
this.guiOrder = guiOrder;
this.selector = selector;
this.writeAllowed.addAll(writeAllowed);
this.required = required;
this.readAllowed.addAll(readAllowed);
}

public String getName() {
Expand All @@ -116,21 +127,34 @@ public AttributeMetadata setGuiOrder(int guiOrder) {
public AttributeGroupMetadata getAttributeGroupMetadata() {
return attributeGroupMetadata;
}

public boolean isSelected(AttributeContext context) {
return selector.test(context);
}

private boolean allConditionsMet(List<Predicate<AttributeContext>> predicates, AttributeContext context) {
return predicates.stream().allMatch(p -> p.test(context));
}

public AttributeMetadata addReadCondition(Predicate<AttributeContext> readAllowed) {
this.readAllowed.add(readAllowed);
return this;
}

public AttributeMetadata addWriteCondition(Predicate<AttributeContext> writeAllowed) {
this.writeAllowed.add(writeAllowed);
return this;
}
public boolean isReadOnly(AttributeContext context) {
return !writeAllowed.test(context);
return !canEdit(context);
}

public boolean canView(AttributeContext context) {
return readAllowed.test(context);
return allConditionsMet(readAllowed, context);
}

public boolean canEdit(AttributeContext context) {
return writeAllowed.test(context);
return allConditionsMet(writeAllowed, context);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ protected UserProfileMetadata decorateUserProfileForCache(UserProfileMetadata de
}

Map<String, UPGroup> groupsByName = asHashMap(parsedConfig.getGroups());
RealmModel realm = session.getContext().getRealm();
int guiOrder = 0;

for (UPAttribute attrConfig : parsedConfig.getAttributes()) {
Expand Down Expand Up @@ -343,10 +344,19 @@ protected UserProfileMetadata decorateUserProfileForCache(UserProfileMetadata de
// make sure username and email are writable if permissions are not set
if (permissions == null || permissions.isEmpty()) {
writeAllowed = AttributeMetadata.ALWAYS_TRUE;
readAllowed = AttributeMetadata.ALWAYS_TRUE;
}

List<AttributeMetadata> atts = decoratedMetadata.getAttribute(attributeName);

// Add ImmutableAttributeValidator to ensure that attributes that are configured
// as read-only are marked as such.
// Skip this for username in realms with username = email to allow change of email
// address on initial login with profile via idp
if (!realm.isRegistrationEmailAsUsername() || !UserModel.USERNAME.equals(attributeName)) {
validators.add(new AttributeValidatorMetadata(ImmutableAttributeValidator.ID));
}

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.
Expand All @@ -356,12 +366,17 @@ protected UserProfileMetadata decorateUserProfileForCache(UserProfileMetadata de
.setAttributeGroupMetadata(groupMetadata);
} else {
final int localGuiOrder = guiOrder++;
// only add configured validators and annotations if attribute metadata exist
Predicate<AttributeContext> readAllowedFinal = readAllowed;
Predicate<AttributeContext> writeAllowedFinal = writeAllowed;

// add configured validators and annotations to existing attribute metadata
atts.stream().forEach(c -> c.addValidator(validators)
.addAnnotations(annotations)
.setAttributeDisplayName(attrConfig.getDisplayName())
.setGuiOrder(localGuiOrder)
.setAttributeGroupMetadata(groupMetadata));
.addAnnotations(annotations)
.setAttributeDisplayName(attrConfig.getDisplayName())
.setGuiOrder(localGuiOrder)
.setAttributeGroupMetadata(groupMetadata)
.addReadCondition(readAllowedFinal)
.addWriteCondition(writeAllowedFinal));
}
} else {
// always add validation for immutable/read-only attributes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,14 @@ public boolean isUsernamePresent() {
}
}

public boolean isUsernameEnabled() {
try {
return driver.findElement(By.id("username")).isEnabled();
} catch (NoSuchElementException nse) {
return false;
}
}

public boolean isDepartmentPresent() {
try {
isDepartmentEnabled();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ protected boolean isDeclarativeUserProfile() {
return true;
}

private static String UP_CONFIG_FOR_METADATA = "{\"attributes\": ["
private final static String UP_CONFIG_FOR_METADATA = "{\"attributes\": ["
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {\"scopes\":[\"profile\"]}, \"displayName\": \"${profile.firstName}\", \"validations\": {\"length\": { \"max\": 255 }}},"
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + ", \"required\": {}, \"displayName\": \"Last name\", \"annotations\": {\"formHintKey\" : \"userEmailFormFieldHint\", \"anotherKey\" : 10, \"yetAnotherKey\" : \"some value\"}},"
+ "{\"name\": \"attr_with_scope_selector\"," + PERMISSIONS_ALL + ", \"selector\": {\"scopes\": [\"profile\"]}},"
Expand All @@ -78,20 +78,26 @@ protected boolean isDeclarativeUserProfile() {
+ "{\"name\": \"attr_no_permission\"," + PERMISSIONS_ADMIN_ONLY + "}"
+ "]}";

private static String UP_CONFIG_NO_ACCESS_TO_NAME_FIELDS = "{\"attributes\": ["
private final static String UP_CONFIG_NO_ACCESS_TO_NAME_FIELDS = "{\"attributes\": ["
+ "{\"name\": \"firstName\"," + PERMISSIONS_ADMIN_ONLY + ", \"required\": {}, \"displayName\": \"${profile.firstName}\", \"validations\": {\"length\": { \"max\": 255 }}},"
+ "{\"name\": \"lastName\"," + PERMISSIONS_ADMIN_ONLY + ", \"required\": {}, \"displayName\": \"Last name\", \"annotations\": {\"formHintKey\" : \"userEmailFormFieldHint\", \"anotherKey\" : 10, \"yetAnotherKey\" : \"some value\"}},"
+ "{\"name\": \"attr_readonly\"," + PERMISSIONS_ADMIN_EDITABLE + "},"
+ "{\"name\": \"attr_no_permission\"," + PERMISSIONS_ADMIN_ONLY + "}"
+ "]}";

private static String UP_CONFIG_RO_ACCESS_TO_NAME_FIELDS = "{\"attributes\": ["
private final static String UP_CONFIG_RO_ACCESS_TO_NAME_FIELDS = "{\"attributes\": ["
+ "{\"name\": \"firstName\"," + PERMISSIONS_ADMIN_EDITABLE + ", \"required\": {}, \"displayName\": \"${profile.firstName}\", \"validations\": {\"length\": { \"max\": 255 }}},"
+ "{\"name\": \"lastName\"," + PERMISSIONS_ADMIN_EDITABLE + ", \"required\": {}, \"displayName\": \"Last name\", \"annotations\": {\"formHintKey\" : \"userEmailFormFieldHint\", \"anotherKey\" : 10, \"yetAnotherKey\" : \"some value\"}},"
+ "{\"name\": \"attr_readonly\"," + PERMISSIONS_ADMIN_EDITABLE + "},"
+ "{\"name\": \"attr_no_permission\"," + PERMISSIONS_ADMIN_ONLY + "}"
+ "]}";

private final static String UP_CONFIG_RO_USERNAME_AND_EMAIL = "{\"attributes\": ["
+ "{\"name\": \"email\"," + PERMISSIONS_ADMIN_EDITABLE + ", \"required\": {}, \"displayName\": \"${email}\", \"annotations\": {\"formHintKey\" : \"userEmailFormFieldHint\", \"anotherKey\" : 10, \"yetAnotherKey\" : \"some value\"}},"
+ "{\"name\": \"attr_readonly\"," + PERMISSIONS_ADMIN_EDITABLE + "},"
+ "{\"name\": \"attr_no_permission\"," + PERMISSIONS_ADMIN_ONLY + "}"
+ "]}";


@Test
@Override
Expand Down Expand Up @@ -187,6 +193,31 @@ public void testGetUserProfileMetadata_RoAccessToNameFields() throws IOException
}
}

@Test
public void testGetUserProfileMetadata_RoAccessToUsernameAndEmail() throws IOException {

try {
RealmRepresentation realmRep = adminClient.realm("test").toRepresentation();
realmRep.setEditUsernameAllowed(false);
adminClient.realm("test").update(realmRep);

setUserProfileConfiguration(UP_CONFIG_RO_USERNAME_AND_EMAIL);

UserRepresentation user = getUser();
assertNotNull(user.getUserProfileMetadata());

assertUserProfileAttributeMetadata(user, "username", "${username}", true, true);
assertUserProfileAttributeMetadata(user, "email", "${email}", true, true);

assertUserProfileAttributeMetadata(user, "attr_readonly", "attr_readonly", false, true);
assertNull(getUserProfileAttributeMetadata(user, "attr_no_permission"));
} finally {
RealmRepresentation realmRep = testRealm().toRepresentation();
realmRep.setEditUsernameAllowed(true);
testRealm().update(realmRep);
}
}


@Test
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,63 @@ public void testRequiredReadOnlyAttribute() {
assertEquals("Last", user.getLastName());
}

@Test
public void testAdminOnlyAttributeNotVisibleToUser() {

setUserProfileConfiguration("{\"attributes\": ["
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
+ "{\"name\": \"department\"," + PERMISSIONS_ADMIN_ONLY + "},"
+ "{\"name\": \"requiredAttrToTriggerVerifyPage\"," + PERMISSIONS_ALL + ", \"required\": {}}"
+ "]}");

loginPage.open();
loginPage.login("login-test6", "password");

verifyProfilePage.assertCurrent();
Assert.assertEquals("ExistingLast", verifyProfilePage.getLastName());
Assert.assertFalse("Admin-only attribute should not be visible for user", verifyProfilePage.isDepartmentPresent());
}


@Test
public void testUsernameReadOnlyInProfile() {

setUserProfileConfiguration("{\"attributes\": ["
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
+ "{\"name\": \"username\"," + PERMISSIONS_ADMIN_EDITABLE + "},"
+ "{\"name\": \"requiredAttrToTriggerVerifyPage\"," + PERMISSIONS_ALL + ", \"required\": {}}"
+ "]}");

loginPage.open();
loginPage.login("login-test6", "password");

verifyProfilePage.assertCurrent();
Assert.assertEquals("ExistingLast", verifyProfilePage.getLastName());

Assert.assertFalse("username should not be editable by user", verifyProfilePage.isUsernameEnabled());
}

@Test
public void testUsernameReadNotVisibleInProfile() {

setUserProfileConfiguration("{\"attributes\": ["
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
+ "{\"name\": \"username\"," + PERMISSIONS_ADMIN_ONLY + "},"
+ "{\"name\": \"requiredAttrToTriggerVerifyPage\"," + PERMISSIONS_ALL + ", \"required\": {}}"
+ "]}");

loginPage.open();
loginPage.login("login-test6", "password");

verifyProfilePage.assertCurrent();
Assert.assertEquals("ExistingLast", verifyProfilePage.getLastName());

Assert.assertFalse("username should not be shown to user", verifyProfilePage.isUsernamePresent());
}

@Test
public void testAttributeNotVisible() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,85 @@ private static void testReadonlyUpdates(KeycloakSession session) {
assertTrue(profile.getAttributes().isReadOnly("department"));
}

@Test
public void testReadonlyEmailCannotBeUpdated() {
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testReadonlyEmailCannotBeUpdated);
}

private static void testReadonlyEmailCannotBeUpdated(KeycloakSession session) {
Map<String, Object> attributes = new HashMap<>();

attributes.put(UserModel.USERNAME, org.keycloak.models.utils.KeycloakModelUtils.generateId());
attributes.put(UserModel.EMAIL, "[email protected]");

UserProfileProvider provider = getDynamicUserProfileProvider(session);

// configure email r/o for user
provider.setConfiguration("{\"attributes\": [{\"name\": \"email\", \"permissions\": {\"edit\": [ \"admin\"]}}]}");

UserProfile profile = provider.create(UserProfileContext.ACCOUNT, attributes);
UserModel user = profile.create();

assertThat(profile.getAttributes().nameSet(),
containsInAnyOrder(UserModel.USERNAME, UserModel.EMAIL));

profile = provider.create(UserProfileContext.USER_API, attributes, user);

Set<String> attributesUpdated = new HashSet<>();

profile.update((attributeName, userModel, oldValue) -> assertTrue(attributesUpdated.add(attributeName)));

attributes.put(UserModel.EMAIL, "[email protected]");

profile = provider.create(UserProfileContext.ACCOUNT, attributes, user);

try {
profile.update();
fail("Should fail since email is read only");
} catch (ValidationException ve) {
assertTrue(ve.isAttributeOnError("email"));
}

assertEquals("E-Mail address shouldn't be changed", "[email protected]", user.getEmail());
}

@Test
public void testUpdateEmail() {
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testUpdateEmail);
}

private static void testUpdateEmail(KeycloakSession session) {
Map<String, Object> attributes = new HashMap<>();

attributes.put(UserModel.USERNAME, org.keycloak.models.utils.KeycloakModelUtils.generateId());
attributes.put(UserModel.EMAIL, "[email protected]");

UserProfileProvider provider = getDynamicUserProfileProvider(session);

// configure email r/w for user
provider.setConfiguration("{\"attributes\": [{\"name\": \"email\", \"permissions\": {\"edit\": [ \"user\", \"admin\"]}}]}");

UserProfile profile = provider.create(UserProfileContext.ACCOUNT, attributes);
UserModel user = profile.create();

assertThat(profile.getAttributes().nameSet(),
containsInAnyOrder(UserModel.USERNAME, UserModel.EMAIL));

profile = provider.create(UserProfileContext.USER_API, attributes, user);

Set<String> attributesUpdated = new HashSet<>();

profile.update((attributeName, userModel, oldValue) -> assertTrue(attributesUpdated.add(attributeName)));

attributes.put("email", "[email protected]");

profile = provider.create(UserProfileContext.ACCOUNT, attributes, user);

profile.update();

assertEquals("E-Mail address should have been changed!", "[email protected]", user.getEmail());
}

@Test
public void testDoNotUpdateUndefinedAttributes() {
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testDoNotUpdateUndefinedAttributes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@
<input type="hidden" ui-select2="isRequiredScopes" id="isRequiredScopes" data-ng-model="requiredScopes" data-placeholder="Select a scope..." multiple/>
</div>
</div>
<fieldset class="border-top" data-ng-show="isNotUsernameOrEmail(currentAttribute.name)">
<fieldset class="border-top">
<legend collapsed><span class="text">{{:: 'user.profile.attribute.permission' | translate}}</span></legend>
<div class="form-group">
<label class="col-md-2 control-label" for="canUserView">{{:: 'user.profile.attribute.canUserView' | translate}}</label>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ export class AccountPage extends React.Component<AccountPageProps, AccountPageSt
onSubmit={(event) => this.handleSubmit(event)}
className="personal-info-form"
>
{!this.isRegistrationEmailAsUsername && (
{!this.isRegistrationEmailAsUsername && fields.username != undefined && (
<FormGroup
label={Msg.localize("username")}
fieldId="user-name"
Expand Down