[KEYCLOAK-18426] - Support required by role and scopes in Admin UI

This commit is contained in:
Pedro Igor 2021-06-23 09:49:21 -03:00
parent 52ced98f92
commit faadb896ea
15 changed files with 329 additions and 104 deletions

View file

@ -32,6 +32,7 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
import org.keycloak.common.Profile; import org.keycloak.common.Profile;
@ -41,6 +42,8 @@ import org.keycloak.component.AmphibianProviderFactory;
import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException; import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.ClientScopeModel.ClientScopeRemovedEvent;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -48,6 +51,7 @@ import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.provider.EnvironmentDependentProviderFactory; import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderEvent;
import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.userprofile.AttributeContext; import org.keycloak.userprofile.AttributeContext;
import org.keycloak.userprofile.AttributeMetadata; import org.keycloak.userprofile.AttributeMetadata;
@ -85,7 +89,7 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
* @param configuredScopes to be evaluated * @param configuredScopes to be evaluated
* @return * @return
*/ */
private static boolean requestedScopePredicate(AttributeContext context, List<String> configuredScopes) { private static boolean requestedScopePredicate(AttributeContext context, Set<String> configuredScopes) {
KeycloakSession session = context.getSession(); KeycloakSession session = context.getSession();
AuthenticationSessionModel authenticationSession = session.getContext().getAuthenticationSession(); AuthenticationSessionModel authenticationSession = session.getContext().getAuthenticationSession();
@ -277,13 +281,13 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
UPAttributePermissions permissions = attrConfig.getPermissions(); UPAttributePermissions permissions = attrConfig.getPermissions();
if (permissions != null) { if (permissions != null) {
List<String> editRoles = permissions.getEdit(); Set<String> editRoles = permissions.getEdit();
if (!editRoles.isEmpty()) { if (!editRoles.isEmpty()) {
writeAllowed = ac -> UPConfigUtils.isRoleForContext(ac.getContext(), editRoles); writeAllowed = ac -> UPConfigUtils.isRoleForContext(ac.getContext(), editRoles);
} }
List<String> viewRoles = permissions.getView(); Set<String> viewRoles = permissions.getView();
if (viewRoles.isEmpty()) { if (viewRoles.isEmpty()) {
readAllowed = writeAllowed; readAllowed = writeAllowed;
@ -333,7 +337,7 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
} }
private Predicate<AttributeContext> createViewAllowedPredicate(Predicate<AttributeContext> canEdit, private Predicate<AttributeContext> createViewAllowedPredicate(Predicate<AttributeContext> canEdit,
List<String> viewRoles) { Set<String> viewRoles) {
return ac -> UPConfigUtils.isRoleForContext(ac.getContext(), viewRoles) || canEdit.test(ac); return ac -> UPConfigUtils.isRoleForContext(ac.getContext(), viewRoles) || canEdit.test(ac);
} }

View file

@ -17,7 +17,7 @@
package org.keycloak.userprofile.config; package org.keycloak.userprofile.config;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.Set;
/** /**
* Configuration of permissions for the attribute * Configuration of permissions for the attribute
@ -27,22 +27,22 @@ import java.util.List;
*/ */
public class UPAttributePermissions { public class UPAttributePermissions {
private List<String> view = Collections.emptyList(); private Set<String> view = Collections.emptySet();
private List<String> edit = Collections.emptyList(); private Set<String> edit = Collections.emptySet();
public List<String> getView() { public Set<String> getView() {
return view; return view;
} }
public void setView(List<String> view) { public void setView(Set<String> view) {
this.view = view; this.view = view;
} }
public List<String> getEdit() { public Set<String> getEdit() {
return edit; return edit;
} }
public void setEdit(List<String> edit) { public void setEdit(Set<String> edit) {
this.edit = edit; this.edit = edit;
} }

View file

@ -16,7 +16,7 @@
*/ */
package org.keycloak.userprofile.config; package org.keycloak.userprofile.config;
import java.util.List; import java.util.Set;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
@ -28,8 +28,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
*/ */
public class UPAttributeRequired { public class UPAttributeRequired {
private List<String> roles; private Set<String> roles;
private List<String> scopes; private Set<String> scopes;
/** /**
* Check if this config means that the attribute is ALWAYS required. * Check if this config means that the attribute is ALWAYS required.
@ -41,19 +41,19 @@ public class UPAttributeRequired {
return (roles == null || roles.isEmpty()) && (scopes == null || scopes.isEmpty()); return (roles == null || roles.isEmpty()) && (scopes == null || scopes.isEmpty());
} }
public List<String> getRoles() { public Set<String> getRoles() {
return roles; return roles;
} }
public void setRoles(List<String> roles) { public void setRoles(Set<String> roles) {
this.roles = roles; this.roles = roles;
} }
public List<String> getScopes() { public Set<String> getScopes() {
return scopes; return scopes;
} }
public void setScopes(List<String> scopes) { public void setScopes(Set<String> scopes) {
this.scopes = scopes; this.scopes = scopes;
} }

View file

@ -16,7 +16,7 @@
*/ */
package org.keycloak.userprofile.config; package org.keycloak.userprofile.config;
import java.util.List; import java.util.Set;
/** /**
* Config of the rules when attribute is selected. * Config of the rules when attribute is selected.
@ -26,13 +26,13 @@ import java.util.List;
*/ */
public class UPAttributeSelector { public class UPAttributeSelector {
private List<String> scopes; private Set<String> scopes;
public List<String> getScopes() { public Set<String> getScopes() {
return scopes; return scopes;
} }
public void setScopes(List<String> scopes) { public void setScopes(Set<String> scopes) {
this.scopes = scopes; this.scopes = scopes;
} }

View file

@ -26,8 +26,11 @@ 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.Stream;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.userprofile.UserProfileContext; import org.keycloak.userprofile.UserProfileContext;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import org.keycloak.validate.ValidationResult; import org.keycloak.validate.ValidationResult;
@ -127,6 +130,25 @@ public class UPConfigUtils {
} }
if (attributeConfig.getRequired() != null) { if (attributeConfig.getRequired() != null) {
validateRoles(attributeConfig.getRequired().getRoles(), "required.roles", errors, attributeName); validateRoles(attributeConfig.getRequired().getRoles(), "required.roles", errors, attributeName);
validateScopes(attributeConfig.getRequired().getScopes(), "required.scopes", attributeName, errors, session);
}
if (attributeConfig.getSelector() != null) {
validateScopes(attributeConfig.getSelector().getScopes(), "selector.scopes", attributeName, errors, session);
}
}
private static void validateScopes(Set<String> scopes, String propertyName, String attributeName, List<String> errors, KeycloakSession session) {
if (scopes == null) {
return;
}
for (String scope : scopes) {
RealmModel realm = session.getContext().getRealm();
Stream<ClientScopeModel> realmScopes = realm.getClientScopesStream();
if (!realmScopes.anyMatch(cs -> cs.getName().equals(scope))) {
errors.add(new StringBuilder("'").append(propertyName).append("' configuration for attribute '").append(attributeName).append("' contains unsupported scope '").append(scope).append("'").toString());
}
} }
} }
@ -146,7 +168,7 @@ public class UPConfigUtils {
* @param errors to ass error message into * @param errors to ass error message into
* @param attributeName we are validating for use in erorr messages * @param attributeName we are validating for use in erorr messages
*/ */
private static void validateRoles(List<String> roles, String fieldName, List<String> errors, String attributeName) { private static void validateRoles(Set<String> roles, String fieldName, List<String> errors, String attributeName) {
if (roles != null) { if (roles != null) {
for (String role : roles) { for (String role : roles) {
if (!PSEUDOROLES.contains(role)) { if (!PSEUDOROLES.contains(role)) {
@ -223,7 +245,7 @@ public class UPConfigUtils {
* @param roles to be inspected * @param roles to be inspected
* @return true if roles list contains role representing checked context * @return true if roles list contains role representing checked context
*/ */
public static boolean isRoleForContext(UserProfileContext context, List<String> roles) { public static boolean isRoleForContext(UserProfileContext context, Set<String> roles) {
if (roles == null) if (roles == null)
return false; return false;
if (context == UserProfileContext.USER_API) if (context == UserProfileContext.USER_API)

View file

@ -1,13 +1,16 @@
{ {
"attributes": [ "attributes": [
{ {
"name": "username" "name": "username",
"displayName": "${username}"
}, },
{ {
"name": "email" "name": "email",
"displayName": "${email}"
}, },
{ {
"name": "firstName", "name": "firstName",
"displayName": "${firstName}",
"required": {"roles" : ["user"]}, "required": {"roles" : ["user"]},
"permissions": { "permissions": {
"view": ["admin", "user"], "view": ["admin", "user"],
@ -16,6 +19,7 @@
}, },
{ {
"name": "lastName", "name": "lastName",
"displayName": "${lastName}",
"required": {"roles" : ["user"]}, "required": {"roles" : ["user"]},
"permissions": { "permissions": {
"view": ["admin", "user"], "view": ["admin", "user"],

View file

@ -126,7 +126,9 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest {
actions.add(action); actions.add(action);
testRealm.setRequiredActions(actions); testRealm.setRequiredActions(actions);
testRealm.setClientScopes(Collections.singletonList(ClientScopeBuilder.create().name(SCOPE_DEPARTMENT).protocol("openid-connect").build())); testRealm.setClientScopes(new ArrayList<>());
testRealm.getClientScopes().add(ClientScopeBuilder.create().name(SCOPE_DEPARTMENT).protocol("openid-connect").build());
testRealm.getClientScopes().add(ClientScopeBuilder.create().name("profile").protocol("openid-connect").build());
client_scope_default = KeycloakModelUtils.createClient(testRealm, "client-a"); client_scope_default = KeycloakModelUtils.createClient(testRealm, "client-a");
client_scope_default.setDefaultClientScopes(Collections.singletonList(SCOPE_DEPARTMENT)); client_scope_default.setDefaultClientScopes(Collections.singletonList(SCOPE_DEPARTMENT));
client_scope_default.setRedirectUris(Collections.singletonList("*")); client_scope_default.setRedirectUris(Collections.singletonList("*"));

View file

@ -46,6 +46,7 @@ import org.junit.Test;
import org.keycloak.common.Profile; import org.keycloak.common.Profile;
import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException; import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
@ -61,6 +62,7 @@ import org.keycloak.userprofile.config.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;
import org.keycloak.userprofile.config.UPAttributeRequired; import org.keycloak.userprofile.config.UPAttributeRequired;
import org.keycloak.userprofile.config.UPAttributeSelector;
import org.keycloak.userprofile.config.UPConfig; import org.keycloak.userprofile.config.UPConfig;
import org.keycloak.testsuite.util.ClientScopeBuilder; import org.keycloak.testsuite.util.ClientScopeBuilder;
import org.keycloak.testsuite.util.KeycloakModelUtils; import org.keycloak.testsuite.util.KeycloakModelUtils;
@ -87,7 +89,9 @@ public class UserProfileTest extends AbstractUserProfileTest {
@Override @Override
public void configureTestRealm(RealmRepresentation testRealm) { public void configureTestRealm(RealmRepresentation testRealm) {
testRealm.setClientScopes(Collections.singletonList(ClientScopeBuilder.create().name("customer").protocol("openid-connect").build())); testRealm.setClientScopes(new ArrayList<>());
testRealm.getClientScopes().add(ClientScopeBuilder.create().name("customer").protocol("openid-connect").build());
testRealm.getClientScopes().add(ClientScopeBuilder.create().name("client-a").protocol("openid-connect").build());
ClientRepresentation client = KeycloakModelUtils.createClient(testRealm, "client-a"); ClientRepresentation client = KeycloakModelUtils.createClient(testRealm, "client-a");
client.setDefaultClientScopes(Collections.singletonList("customer")); client.setDefaultClientScopes(Collections.singletonList("customer"));
KeycloakModelUtils.createClient(testRealm, "client-b"); KeycloakModelUtils.createClient(testRealm, "client-b");
@ -284,7 +288,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
attribute.setRequired(requirements); attribute.setRequired(requirements);
UPAttributePermissions permissions = new UPAttributePermissions(); UPAttributePermissions permissions = new UPAttributePermissions();
permissions.setEdit(Collections.singletonList(ROLE_USER)); permissions.setEdit(Collections.singleton(ROLE_USER));
attribute.setPermissions(permissions); attribute.setPermissions(permissions);
config.addAttribute(attribute); config.addAttribute(attribute);
@ -709,7 +713,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
attribute.setRequired(requirements); attribute.setRequired(requirements);
UPAttributePermissions permissions = new UPAttributePermissions(); UPAttributePermissions permissions = new UPAttributePermissions();
permissions.setEdit(Collections.singletonList(ROLE_USER)); permissions.setEdit(Collections.singleton(ROLE_USER));
attribute.setPermissions(permissions); attribute.setPermissions(permissions);
config.addAttribute(attribute); config.addAttribute(attribute);
@ -820,14 +824,12 @@ public class UserProfileTest extends AbstractUserProfileTest {
UPAttributeRequired requirements = new UPAttributeRequired(); UPAttributeRequired requirements = new UPAttributeRequired();
List<String> roles = new ArrayList<>(); requirements.setRoles(Collections.singleton(ROLE_USER));
roles.add(ROLE_USER);
requirements.setRoles(roles);
attribute.setRequired(requirements); attribute.setRequired(requirements);
UPAttributePermissions permissions = new UPAttributePermissions(); UPAttributePermissions permissions = new UPAttributePermissions();
permissions.setEdit(Collections.singletonList(ROLE_USER)); permissions.setEdit(Collections.singleton(ROLE_USER));
attribute.setPermissions(permissions); attribute.setPermissions(permissions);
config.addAttribute(attribute); config.addAttribute(attribute);
@ -889,12 +891,12 @@ public class UserProfileTest extends AbstractUserProfileTest {
UPAttributeRequired requirements = new UPAttributeRequired(); UPAttributeRequired requirements = new UPAttributeRequired();
requirements.setRoles(Collections.singletonList(ROLE_ADMIN)); requirements.setRoles(Collections.singleton(ROLE_ADMIN));
attribute.setRequired(requirements); attribute.setRequired(requirements);
UPAttributePermissions permissions = new UPAttributePermissions(); UPAttributePermissions permissions = new UPAttributePermissions();
permissions.setEdit(Collections.singletonList(UPConfigUtils.ROLE_ADMIN)); permissions.setEdit(Collections.singleton(UPConfigUtils.ROLE_ADMIN));
attribute.setPermissions(permissions); attribute.setPermissions(permissions);
config.addAttribute(attribute); config.addAttribute(attribute);
@ -946,7 +948,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
attribute.setRequired(requirements); attribute.setRequired(requirements);
UPAttributePermissions permissions = new UPAttributePermissions(); UPAttributePermissions permissions = new UPAttributePermissions();
permissions.setEdit(Collections.singletonList(UPConfigUtils.ROLE_ADMIN)); permissions.setEdit(Collections.singleton(UPConfigUtils.ROLE_ADMIN));
attribute.setPermissions(permissions); attribute.setPermissions(permissions);
config.addAttribute(attribute); config.addAttribute(attribute);
@ -994,9 +996,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
attribute.setRequired(requirements); attribute.setRequired(requirements);
UPAttributePermissions permissions = new UPAttributePermissions(); UPAttributePermissions permissions = new UPAttributePermissions();
List<String> roles = new ArrayList<>(); permissions.setEdit(Collections.singleton(UPConfigUtils.ROLE_USER));
roles.add(UPConfigUtils.ROLE_USER);
permissions.setEdit(roles);
attribute.setPermissions(permissions); attribute.setPermissions(permissions);
config.addAttribute(attribute); config.addAttribute(attribute);
@ -1039,14 +1039,12 @@ public class UserProfileTest extends AbstractUserProfileTest {
UPAttributeRequired requirements = new UPAttributeRequired(); UPAttributeRequired requirements = new UPAttributeRequired();
List<String> scopes = new ArrayList<>(); requirements.setScopes(Collections.singleton("client-a"));
scopes.add("client-a");
requirements.setScopes(scopes);
attribute.setRequired(requirements); attribute.setRequired(requirements);
UPAttributePermissions permissions = new UPAttributePermissions(); UPAttributePermissions permissions = new UPAttributePermissions();
permissions.setEdit(Collections.singletonList("user")); permissions.setEdit(Collections.singleton("user"));
attribute.setPermissions(permissions); attribute.setPermissions(permissions);
config.addAttribute(attribute); config.addAttribute(attribute);
@ -1113,4 +1111,40 @@ public class UserProfileTest extends AbstractUserProfileTest {
} }
} }
@Test
public void testConfigurationInvalidScope() {
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testConfigurationInvalidScope);
}
private static void testConfigurationInvalidScope(KeycloakSession session) throws IOException {
RealmModel realm = session.getContext().getRealm();
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
ComponentModel component = provider.getComponentModel();
assertNotNull(component);
UPConfig config = new UPConfig();
UPAttribute attribute = new UPAttribute();
attribute.setName(ATT_ADDRESS);
UPAttributeRequired requirements = new UPAttributeRequired();
requirements.setScopes(Collections.singleton("invalid"));
attribute.setRequired(requirements);
attribute.setSelector(new UPAttributeSelector());
attribute.getSelector().setScopes(Collections.singleton("invalid"));
config.addAttribute(attribute);
try {
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
Assert.fail("Expected to fail due to invalid client scope");
} catch (ComponentValidationException cve) {
//ignore
}
}
} }

View file

@ -26,6 +26,7 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
@ -35,6 +36,7 @@ import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.runonserver.RunOnServer; import org.keycloak.testsuite.runonserver.RunOnServer;
import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonMappingException;
import org.keycloak.testsuite.util.ClientScopeBuilder;
import org.keycloak.userprofile.config.UPAttribute; import org.keycloak.userprofile.config.UPAttribute;
import org.keycloak.userprofile.config.UPAttributePermissions; import org.keycloak.userprofile.config.UPAttributePermissions;
import org.keycloak.userprofile.config.UPAttributeRequired; import org.keycloak.userprofile.config.UPAttributeRequired;
@ -51,6 +53,12 @@ public class UPConfigParserTest extends AbstractTestRealmKeycloakTest {
@Override @Override
public void configureTestRealm(RealmRepresentation testRealm) { public void configureTestRealm(RealmRepresentation testRealm) {
testRealm.setClientScopes(new ArrayList<>());
testRealm.getClientScopes().add(ClientScopeBuilder.create().name("phone-1-sel").build());
testRealm.getClientScopes().add(ClientScopeBuilder.create().name("phone-1").build());
testRealm.getClientScopes().add(ClientScopeBuilder.create().name("phone-2-sel").build());
testRealm.getClientScopes().add(ClientScopeBuilder.create().name("phone-2").build());
testRealm.getClientScopes().add(ClientScopeBuilder.create().name("phone-3-sel").build());
} }
@Test @Test
@ -158,39 +166,51 @@ public class UPConfigParserTest extends AbstractTestRealmKeycloakTest {
} }
@Test @Test
public void validateConfiguration_OK() throws IOException { public void validateConfiguration_OK() {
List<String> errors = validate(null, loadValidConfig()); getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UPConfigParserTest::validateConfiguration_OK);
}
public static void validateConfiguration_OK(KeycloakSession session) throws IOException {
List<String> errors = validate(session, loadValidConfig());
Assert.assertTrue(errors.isEmpty()); Assert.assertTrue(errors.isEmpty());
} }
@Test @Test
public void validateConfiguration_attributeNameErrors() throws IOException { public void validateConfiguration_attributeNameErrors() {
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UPConfigParserTest::validateConfiguration_attributeNameErrors);
}
public static void validateConfiguration_attributeNameErrors(KeycloakSession session) throws IOException {
UPConfig config = loadValidConfig(); UPConfig config = loadValidConfig();
//we run this test without KeycloakSession so validator configs are not validated here //we run this test without KeycloakSession so validator configs are not validated here
UPAttribute attConfig = config.getAttributes().get(1); UPAttribute attConfig = config.getAttributes().get(1);
attConfig.setName(null); attConfig.setName(null);
List<String> errors = validate(null, config); List<String> errors = validate(session, config);
Assert.assertEquals(1, errors.size()); Assert.assertEquals(1, errors.size());
attConfig.setName(" "); attConfig.setName(" ");
errors = validate(null, config); errors = validate(session, config);
Assert.assertEquals(1, errors.size()); Assert.assertEquals(1, errors.size());
// duplicate attribute name // duplicate attribute name
attConfig.setName("firstName"); attConfig.setName("firstName");
errors = validate(null, config); errors = validate(session, config);
Assert.assertEquals(1, errors.size()); Assert.assertEquals(1, errors.size());
// attribute name format error - unallowed character // attribute name format error - unallowed character
attConfig.setName("ema il"); attConfig.setName("ema il");
errors = validate(null, config); errors = validate(session, config);
Assert.assertEquals(1, errors.size()); Assert.assertEquals(1, errors.size());
} }
@Test @Test
public void validateConfiguration_attributePermissionsErrors() throws IOException { public void validateConfiguration_attributePermissionsErrors() {
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UPConfigParserTest::validateConfiguration_attributePermissionsErrors);
}
public static void validateConfiguration_attributePermissionsErrors(KeycloakSession session) throws IOException {
UPConfig config = loadValidConfig(); UPConfig config = loadValidConfig();
//we run this test without KeycloakSession so validator configs are not validated here //we run this test without KeycloakSession so validator configs are not validated here
@ -198,38 +218,41 @@ public class UPConfigParserTest extends AbstractTestRealmKeycloakTest {
// no permissions configures at all // no permissions configures at all
attConfig.setPermissions(null); attConfig.setPermissions(null);
List<String> errors = validate(null, config); List<String> errors = validate(session, config);
Assert.assertEquals(0, errors.size()); Assert.assertEquals(0, errors.size());
// no permissions structure fields configured // no permissions structure fields configured
UPAttributePermissions permsConfig = new UPAttributePermissions(); UPAttributePermissions permsConfig = new UPAttributePermissions();
attConfig.setPermissions(permsConfig); attConfig.setPermissions(permsConfig);
errors = validate(null, config); errors = validate(session, config);
Assert.assertTrue(errors.isEmpty()); Assert.assertTrue(errors.isEmpty());
// valid if both are present, even empty // valid if both are present, even empty
permsConfig.setEdit(Collections.emptyList()); permsConfig.setEdit(Collections.emptySet());
permsConfig.setView(Collections.emptyList()); permsConfig.setView(Collections.emptySet());
attConfig.setPermissions(permsConfig); attConfig.setPermissions(permsConfig);
errors = validate(null, config); errors = validate(session, config);
Assert.assertEquals(0, errors.size()); Assert.assertEquals(0, errors.size());
List<String> withInvRole = new ArrayList<>(); Set<String> withInvRole = Collections.singleton("invalid");
withInvRole.add("invalid");
// invalid role used for view // invalid role used for view
permsConfig.setView(withInvRole); permsConfig.setView(withInvRole);
errors = validate(null, config); errors = validate(session, config);
Assert.assertEquals(1, errors.size()); Assert.assertEquals(1, errors.size());
// invalid role used for edit also // invalid role used for edit also
permsConfig.setEdit(withInvRole); permsConfig.setEdit(withInvRole);
errors = validate(null, config); errors = validate(session, config);
Assert.assertEquals(2, errors.size()); Assert.assertEquals(2, errors.size());
} }
@Test @Test
public void validateConfiguration_attributeRequirementsErrors() throws IOException { public void validateConfiguration_attributeRequirementsErrors() {
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UPConfigParserTest::validateConfiguration_attributeRequirementsErrors);
}
public static void validateConfiguration_attributeRequirementsErrors(KeycloakSession session) throws IOException {
UPConfig config = loadValidConfig(); UPConfig config = loadValidConfig();
//we run this test without KeycloakSession so validator configs are not validated here //we run this test without KeycloakSession so validator configs are not validated here
@ -237,22 +260,19 @@ public class UPConfigParserTest extends AbstractTestRealmKeycloakTest {
// it is OK without requirements configures at all // it is OK without requirements configures at all
attConfig.setRequired(null); attConfig.setRequired(null);
List<String> errors = validate(null, config); List<String> errors = validate(session, config);
Assert.assertEquals(0, errors.size()); Assert.assertEquals(0, errors.size());
// it is OK with empty config as it means ALWAYS required // it is OK with empty config as it means ALWAYS required
UPAttributeRequired reqConfig = new UPAttributeRequired(); UPAttributeRequired reqConfig = new UPAttributeRequired();
attConfig.setRequired(reqConfig); attConfig.setRequired(reqConfig);
errors = validate(null, config); errors = validate(session, config);
Assert.assertEquals(0, errors.size()); Assert.assertEquals(0, errors.size());
Assert.assertTrue(reqConfig.isAlways()); Assert.assertTrue(reqConfig.isAlways());
List<String> withInvRole = new ArrayList<>();
withInvRole.add("invalid");
// invalid role used // invalid role used
reqConfig.setRoles(withInvRole);; reqConfig.setRoles(Collections.singleton("invalid"));
errors = validate(null, config); errors = validate(session, config);
Assert.assertEquals(1, errors.size()); Assert.assertEquals(1, errors.size());
Assert.assertFalse(reqConfig.isAlways()); Assert.assertFalse(reqConfig.isAlways());
@ -260,7 +280,7 @@ public class UPConfigParserTest extends AbstractTestRealmKeycloakTest {
@Test @Test
public void validateConfiguration_attributeValidationsErrors() { public void validateConfiguration_attributeValidationsErrors() {
getTestingClient().server().run((RunOnServer) UPConfigParserTest::validateConfiguration_attributeValidationsErrors); getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UPConfigParserTest::validateConfiguration_attributeValidationsErrors);
} }
private static void validateConfiguration_attributeValidationsErrors(KeycloakSession session) throws IOException { private static void validateConfiguration_attributeValidationsErrors(KeycloakSession session) throws IOException {
@ -274,8 +294,8 @@ public class UPConfigParserTest extends AbstractTestRealmKeycloakTest {
//add validation config for one attribute for testing purposes //add validation config for one attribute for testing purposes
Map<String, Map<String, Object>> validationConfig = new HashMap<>(); Map<String, Map<String, Object>> validationConfig = new HashMap<>();
config.getAttributes().get(1).setValidations(validationConfig); config.getAttributes().get(1).setValidations(validationConfig);
// empty validator name // empty validator name
validationConfig.put(" ",null); validationConfig.put(" ",null);
List<String> errors = validate(session, config); List<String> errors = validate(session, config);
Assert.assertEquals(1, errors.size()); Assert.assertEquals(1, errors.size());
@ -288,9 +308,5 @@ public class UPConfigParserTest extends AbstractTestRealmKeycloakTest {
validationConfig.put("length", vc ); validationConfig.put("length", vc );
errors = validate(session, config); errors = validate(session, config);
Assert.assertEquals(1, errors.size()); Assert.assertEquals(1, errors.size());
} }
} }

View file

@ -19,8 +19,9 @@ package org.keycloak.testsuite.user.profile.config;
import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_ADMIN; import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_ADMIN;
import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_USER; import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_USER;
import java.util.ArrayList; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
@ -52,14 +53,14 @@ public class UPConfigUtilsTest {
Assert.assertFalse(UPConfigUtils.isRoleForContext(UserProfileContext.ACCOUNT, null)); Assert.assertFalse(UPConfigUtils.isRoleForContext(UserProfileContext.ACCOUNT, null));
List<String> roles = new ArrayList<>(); Set<String> roles = new HashSet<>();
roles.add(ROLE_ADMIN); roles.add(ROLE_ADMIN);
Assert.assertTrue(UPConfigUtils.isRoleForContext(UserProfileContext.USER_API, roles)); Assert.assertTrue(UPConfigUtils.isRoleForContext(UserProfileContext.USER_API, roles));
Assert.assertFalse(UPConfigUtils.isRoleForContext(UserProfileContext.ACCOUNT, roles)); Assert.assertFalse(UPConfigUtils.isRoleForContext(UserProfileContext.ACCOUNT, roles));
Assert.assertFalse(UPConfigUtils.isRoleForContext(UserProfileContext.ACCOUNT_OLD, roles)); Assert.assertFalse(UPConfigUtils.isRoleForContext(UserProfileContext.ACCOUNT_OLD, roles));
Assert.assertFalse(UPConfigUtils.isRoleForContext(UserProfileContext.UPDATE_PROFILE, roles)); Assert.assertFalse(UPConfigUtils.isRoleForContext(UserProfileContext.UPDATE_PROFILE, roles));
roles = new ArrayList<>(); roles = new HashSet<>();
roles.add(ROLE_USER); roles.add(ROLE_USER);
Assert.assertFalse(UPConfigUtils.isRoleForContext(UserProfileContext.USER_API, roles)); Assert.assertFalse(UPConfigUtils.isRoleForContext(UserProfileContext.USER_API, roles));
Assert.assertTrue(UPConfigUtils.isRoleForContext(UserProfileContext.ACCOUNT, roles)); Assert.assertTrue(UPConfigUtils.isRoleForContext(UserProfileContext.ACCOUNT, roles));

View file

@ -9,8 +9,8 @@
"name":"email ", "name":"email ",
"validations": { "validations": {
"length" : { "max": 255 }, "length" : { "max": 255 },
"emailFormat": {}, "email": {},
"emailDomainDenyList": {} "not-blank": {}
}, },
"required": { "required": {
"roles" : ["user", "admin"] "roles" : ["user", "admin"]
@ -44,7 +44,7 @@
"name":"phone", "name":"phone",
"displayName" : "${profile.phone}", "displayName" : "${profile.phone}",
"validations": { "validations": {
"phoneNumberFormatInternational":{} "not-blank":{}
}, },
"required": { "required": {
"scopes" : ["phone-1", "phone-2"], "scopes" : ["phone-1", "phone-2"],

View file

@ -1896,8 +1896,14 @@ user.profile.attribute.name=Name
user.profile.attribute.name.tooltip=The name of the attribute. user.profile.attribute.name.tooltip=The name of the attribute.
user.profile.attribute.displayName=Display name user.profile.attribute.displayName=Display name
user.profile.attribute.displayName.tooltip=Display name for the attribute. Supports keys for localized values as well. For example\: ${profile.attribute.phoneNumber} user.profile.attribute.displayName.tooltip=Display name for the attribute. Supports keys for localized values as well. For example\: ${profile.attribute.phoneNumber}
user.profile.attribute.selector.scopes=Enabled when scope
user.profile.attribute.selector.scopes.tooltip=Set the attribute as enabled only when a set of one or more scopes are requested by clients. This constraint only applies to flows where clients are able to ask for scopes (e.g.: during login or registration).
user.profile.attribute.required=Required user.profile.attribute.required=Required
user.profile.attribute.required.tooltip=Set the attribute as required. If enabled, the attribute must be set by users and administrators. Otherwise, the attribute is optional. user.profile.attribute.required.tooltip=Set the attribute as required. If enabled, the attribute must be set by users and administrators. Otherwise, the attribute is optional.
user.profile.attribute.required.roles=Required for roles
user.profile.attribute.required.roles.tooltip=Set the attribute as required for specific types of users. If set to 'user', the attribute is required for users. If set to 'admin' the attribute is required only for administrators.
user.profile.attribute.required.scopes=Required for scopes
user.profile.attribute.required.scopes.tooltip=Set the attribute as required only when a set of one or more scopes are requested by clients. This constraint only applies to flows where clients are able to ask for scopes (e.g.: during login or registration).
user.profile.attribute.permission=Permission user.profile.attribute.permission=Permission
user.profile.attribute.canUserView=Can user view? user.profile.attribute.canUserView=Can user view?
user.profile.attribute.canUserView.tooltip=If enabled, users can view the attribute. Otherwise, users don't have access to the attribute. user.profile.attribute.canUserView.tooltip=If enabled, users can view the attribute. Otherwise, users don't have access to the attribute.

View file

@ -268,7 +268,10 @@ module.config([ '$routeProvider', function($routeProvider) {
}, },
realm : function(RealmLoader) { realm : function(RealmLoader) {
return RealmLoader(); return RealmLoader();
} },
clientScopes : function(ClientScopeListLoader) {
return ClientScopeListLoader();
},
}, },
controller : 'RealmUserProfileCtrl' controller : 'RealmUserProfileCtrl'
}) })

View file

@ -1401,7 +1401,7 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
}; };
}); });
module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, $http, $location, $route, UserProfile, Dialog, Notifications, serverInfo) { module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, clientScopes, $http, $location, $route, UserProfile, Dialog, Notifications, serverInfo) {
$scope.realm = realm; $scope.realm = realm;
$scope.validatorProviders = serverInfo.componentTypes['org.keycloak.validate.Validator']; $scope.validatorProviders = serverInfo.componentTypes['org.keycloak.validate.Validator'];
@ -1420,14 +1420,29 @@ module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, $http,
$scope.showJsonEditor = function() { $scope.showJsonEditor = function() {
$scope.isShowAttributes = false; $scope.isShowAttributes = false;
delete $scope.currentAttribute;
} }
$scope.canViewPermission = { $scope.isRequiredRoles = {
minimumInputLength: 0, minimumInputLength: 0,
delay: 500, delay: 500,
allowClear: true, allowClear: true,
id: function(e) { return e; },
query: function (query) { query: function (query) {
query.callback({results: ['user', 'admin']}); var expectedRoles = ['user', 'admin'];
var roles = [];
if ('' == query.term.trim()) {
roles = expectedRoles;
} else {
for (var i = 0; i < expectedRoles.length; i++) {
if (expectedRoles[i].indexOf(query.term.trim()) != -1) {
roles.push(expectedRoles[i]);
}
}
}
query.callback({results: roles});
}, },
formatResult: function(object, container, query) { formatResult: function(object, container, query) {
return object; return object;
@ -1437,18 +1452,57 @@ module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, $http,
} }
}; };
$scope.canEditPermission = { $scope.isRequiredScopes = {
minimumInputLength: 0, minimumInputLength: 1,
delay: 500, delay: 500,
allowClear: true, allowClear: true,
query: function (query) { query: function (query) {
query.callback({results: ['user', 'admin']}); var scopes = [];
if ('' == query.term.trim()) {
scopes = clientScopes;
} else {
for (var i = 0; i < clientScopes.length; i++) {
if (clientScopes[i].name.indexOf(query.term.trim()) != -1) {
scopes.push(clientScopes[i]);
}
}
}
query.callback({results: scopes});
}, },
formatResult: function(object, container, query) { formatResult: function(object, container, query) {
return object; return object.name;
}, },
formatSelection: function(object, container, query) { formatSelection: function(object, container, query) {
return object; return object.name;
}
};
$scope.selectorByScopeSelect = {
minimumInputLength: 1,
delay: 500,
allowClear: true,
query: function (query) {
var scopes = [];
if ('' == query.term.trim()) {
scopes = clientScopes;
} else {
for (var i = 0; i < clientScopes.length; i++) {
if (clientScopes[i].name.indexOf(query.term.trim()) != -1) {
scopes.push(clientScopes[i]);
}
}
}
query.callback({results: scopes});
},
formatResult: function(object, container, query) {
return object.name;
},
formatSelection: function(object, container, query) {
return object.name;
} }
}; };
@ -1461,6 +1515,13 @@ module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, $http,
$scope.create = function() { $scope.create = function() {
$scope.isCreate = true; $scope.isCreate = true;
$scope.currentAttribute = { $scope.currentAttribute = {
selector: {
scopes: []
},
required: {
roles: [],
scopes: []
},
permissions: { permissions: {
view: [], view: [],
edit: [] edit: []
@ -1503,6 +1564,35 @@ module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, $http,
}; };
} }
if (attribute.selector == null) {
attribute.selector = {
scopes: []
};
}
if (attribute.required) {
if (attribute.required.roles) {
$scope.requiredRoles = attribute.required.roles;
}
if (attribute.required.scopes) {
for (var i = 0; i < attribute.required.scopes.length; i++) {
$scope.requiredScopes.push({
id: attribute.required.scopes[i],
name: attribute.required.scopes[i]
});
}
}
}
if (attribute.selector && attribute.selector.scopes) {
for (var i = 0; i < attribute.selector.scopes.length; i++) {
$scope.selectorByScope.push({
id: attribute.selector.scopes[i],
name: attribute.selector.scopes[i]
});
}
}
$scope.isRequired = attribute.required != null; $scope.isRequired = attribute.required != null;
$scope.canUserView = attribute.permissions.view.includes('user'); $scope.canUserView = attribute.permissions.view.includes('user');
$scope.canAdminView = attribute.permissions.view.includes('admin'); $scope.canAdminView = attribute.permissions.view.includes('admin');
@ -1514,27 +1604,33 @@ module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, $http,
$scope.$watch('isRequired', function() { $scope.$watch('isRequired', function() {
if ($scope.isRequired) { if ($scope.isRequired) {
$scope.currentAttribute.required = {}; $scope.currentAttribute.required = {
} else { roles: [],
scopes: []
};
} else if ($scope.currentAttribute) {
delete $scope.currentAttribute.required; delete $scope.currentAttribute.required;
} }
}, true); }, true);
handlePermission = function(permission, role, allowed) { handlePermission = function(permission, role, allowed) {
let attribute = $scope.currentAttribute; let attribute = $scope.currentAttribute;
let roles = [];
for (let r of attribute.permissions[permission]) { if (attribute && attribute.permissions) {
if (r != role) { let roles = [];
roles.push(r);
for (let r of attribute.permissions[permission]) {
if (r != role) {
roles.push(r);
}
} }
}
if (allowed) { if (allowed) {
roles.push(role); roles.push(role);
} }
attribute.permissions[permission] = roles; attribute.permissions[permission] = roles;
}
} }
$scope.$watch('canUserView', function() { $scope.$watch('canUserView', function() {
@ -1603,8 +1699,24 @@ module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, $http,
$scope.config = JSON.parse($scope.rawConfig); $scope.config = JSON.parse($scope.rawConfig);
} }
if ($scope.isCreate && $scope.currentAttribute) { if ($scope.currentAttribute) {
$scope.config['attributes'].push($scope.currentAttribute); if ($scope.isRequired) {
$scope.currentAttribute.required.roles = $scope.requiredRoles;
for (var i = 0; i < $scope.requiredScopes.length; i++) {
$scope.currentAttribute.required.scopes.push($scope.requiredScopes[i].name);
}
}
$scope.currentAttribute.selector = {scopes: []};
for (var i = 0; i < $scope.selectorByScope.length; i++) {
$scope.currentAttribute.selector.scopes.push($scope.selectorByScope[i].name);
}
if ($scope.isCreate) {
$scope.config['attributes'].push($scope.currentAttribute);
}
} }
UserProfile.update({realm: realm.realm}, UserProfile.update({realm: realm.realm},

View file

@ -85,6 +85,13 @@
type="text" class="form-control"/> type="text" class="form-control"/>
</div> </div>
</div> </div>
<div class="form-group">
<label class="col-md-2 control-label" for="selectorByScopeSelect">{{:: 'user.profile.attribute.selector.scopes' | translate}}</label>
<kc-tooltip>{{:: 'user.profile.attribute.selector.scopes.tooltip' | translate}}</kc-tooltip>
<div class="col-md-6">
<input type="hidden" ui-select2="selectorByScopeSelect" id="selectorByScopeSelect" data-ng-model="selectorByScope" data-placeholder="Select a scope..." multiple/>
</div>
</div>
<div class="form-group"> <div class="form-group">
<label class="col-md-2 control-label" for="isRequired">{{:: 'user.profile.attribute.required' | translate}}</label> <label class="col-md-2 control-label" for="isRequired">{{:: 'user.profile.attribute.required' | translate}}</label>
<kc-tooltip>{{:: 'user.profile.attribute.required.tooltip' | translate}}</kc-tooltip> <kc-tooltip>{{:: 'user.profile.attribute.required.tooltip' | translate}}</kc-tooltip>
@ -93,6 +100,20 @@
on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/> on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div> </div>
</div> </div>
<div class="form-group" data-ng-show="isRequired">
<label class="col-md-2 control-label" for="isRequiredRoles">{{:: 'user.profile.attribute.required.roles' | translate}}</label>
<kc-tooltip>{{:: 'user.profile.attribute.required.roles.tooltip' | translate}}</kc-tooltip>
<div class="col-md-6">
<input type="hidden" ui-select2="isRequiredRoles" id="isRequiredRoles" data-ng-model="requiredRoles" data-placeholder="Select a role..." multiple/>
</div>
</div>
<div class="form-group" data-ng-show="isRequired">
<label class="col-md-2 control-label" for="isRequiredScopes">{{:: 'user.profile.attribute.required.scopes' | translate}}</label>
<kc-tooltip>{{:: 'user.profile.attribute.required.scopes.tooltip' | translate}}</kc-tooltip>
<div class="col-md-6">
<input type="hidden" ui-select2="isRequiredScopes" id="isRequiredScopes" data-ng-model="requiredScopes" data-placeholder="Select a scope..." multiple/>
</div>
</div>
<fieldset class="border-top"> <fieldset class="border-top">
<legend collapsed><span class="text">{{:: 'user.profile.attribute.permission' | translate}}</span></legend> <legend collapsed><span class="text">{{:: 'user.profile.attribute.permission' | translate}}</span></legend>
<div class="form-group"> <div class="form-group">