[KEYCLOAK-18426] - Support required by role and scopes in Admin UI
This commit is contained in:
parent
52ced98f92
commit
faadb896ea
15 changed files with 329 additions and 104 deletions
|
@ -32,6 +32,7 @@ import java.util.Collections;
|
|||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.keycloak.common.Profile;
|
||||
|
@ -41,6 +42,8 @@ import org.keycloak.component.AmphibianProviderFactory;
|
|||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.component.ComponentValidationException;
|
||||
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.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
@ -48,6 +51,7 @@ import org.keycloak.models.UserModel;
|
|||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.provider.ProviderEvent;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.userprofile.AttributeContext;
|
||||
import org.keycloak.userprofile.AttributeMetadata;
|
||||
|
@ -85,7 +89,7 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
|||
* @param configuredScopes to be evaluated
|
||||
* @return
|
||||
*/
|
||||
private static boolean requestedScopePredicate(AttributeContext context, List<String> configuredScopes) {
|
||||
private static boolean requestedScopePredicate(AttributeContext context, Set<String> configuredScopes) {
|
||||
KeycloakSession session = context.getSession();
|
||||
AuthenticationSessionModel authenticationSession = session.getContext().getAuthenticationSession();
|
||||
|
||||
|
@ -277,13 +281,13 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
|||
UPAttributePermissions permissions = attrConfig.getPermissions();
|
||||
|
||||
if (permissions != null) {
|
||||
List<String> editRoles = permissions.getEdit();
|
||||
Set<String> editRoles = permissions.getEdit();
|
||||
|
||||
if (!editRoles.isEmpty()) {
|
||||
writeAllowed = ac -> UPConfigUtils.isRoleForContext(ac.getContext(), editRoles);
|
||||
}
|
||||
|
||||
List<String> viewRoles = permissions.getView();
|
||||
Set<String> viewRoles = permissions.getView();
|
||||
|
||||
if (viewRoles.isEmpty()) {
|
||||
readAllowed = writeAllowed;
|
||||
|
@ -333,7 +337,7 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
|||
}
|
||||
|
||||
private Predicate<AttributeContext> createViewAllowedPredicate(Predicate<AttributeContext> canEdit,
|
||||
List<String> viewRoles) {
|
||||
Set<String> viewRoles) {
|
||||
return ac -> UPConfigUtils.isRoleForContext(ac.getContext(), viewRoles) || canEdit.test(ac);
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
package org.keycloak.userprofile.config;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Configuration of permissions for the attribute
|
||||
|
@ -27,22 +27,22 @@ import java.util.List;
|
|||
*/
|
||||
public class UPAttributePermissions {
|
||||
|
||||
private List<String> view = Collections.emptyList();
|
||||
private List<String> edit = Collections.emptyList();
|
||||
private Set<String> view = Collections.emptySet();
|
||||
private Set<String> edit = Collections.emptySet();
|
||||
|
||||
public List<String> getView() {
|
||||
public Set<String> getView() {
|
||||
return view;
|
||||
}
|
||||
|
||||
public void setView(List<String> view) {
|
||||
public void setView(Set<String> view) {
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
public List<String> getEdit() {
|
||||
public Set<String> getEdit() {
|
||||
return edit;
|
||||
}
|
||||
|
||||
public void setEdit(List<String> edit) {
|
||||
public void setEdit(Set<String> edit) {
|
||||
this.edit = edit;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
*/
|
||||
package org.keycloak.userprofile.config;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
|
@ -28,8 +28,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
|
|||
*/
|
||||
public class UPAttributeRequired {
|
||||
|
||||
private List<String> roles;
|
||||
private List<String> scopes;
|
||||
private Set<String> roles;
|
||||
private Set<String> scopes;
|
||||
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
|
||||
public List<String> getRoles() {
|
||||
public Set<String> getRoles() {
|
||||
return roles;
|
||||
}
|
||||
|
||||
public void setRoles(List<String> roles) {
|
||||
public void setRoles(Set<String> roles) {
|
||||
this.roles = roles;
|
||||
}
|
||||
|
||||
public List<String> getScopes() {
|
||||
public Set<String> getScopes() {
|
||||
return scopes;
|
||||
}
|
||||
|
||||
public void setScopes(List<String> scopes) {
|
||||
public void setScopes(Set<String> scopes) {
|
||||
this.scopes = scopes;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
*/
|
||||
package org.keycloak.userprofile.config;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Config of the rules when attribute is selected.
|
||||
|
@ -26,13 +26,13 @@ import java.util.List;
|
|||
*/
|
||||
public class UPAttributeSelector {
|
||||
|
||||
private List<String> scopes;
|
||||
private Set<String> scopes;
|
||||
|
||||
public List<String> getScopes() {
|
||||
public Set<String> getScopes() {
|
||||
return scopes;
|
||||
}
|
||||
|
||||
public void setScopes(List<String> scopes) {
|
||||
public void setScopes(Set<String> scopes) {
|
||||
this.scopes = scopes;
|
||||
}
|
||||
|
||||
|
|
|
@ -26,8 +26,11 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.validate.ValidationResult;
|
||||
|
@ -127,6 +130,25 @@ public class UPConfigUtils {
|
|||
}
|
||||
if (attributeConfig.getRequired() != null) {
|
||||
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 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) {
|
||||
for (String role : roles) {
|
||||
if (!PSEUDOROLES.contains(role)) {
|
||||
|
@ -223,7 +245,7 @@ public class UPConfigUtils {
|
|||
* @param roles to be inspected
|
||||
* @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)
|
||||
return false;
|
||||
if (context == UserProfileContext.USER_API)
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"name": "username"
|
||||
"name": "username",
|
||||
"displayName": "${username}"
|
||||
},
|
||||
{
|
||||
"name": "email"
|
||||
"name": "email",
|
||||
"displayName": "${email}"
|
||||
},
|
||||
{
|
||||
"name": "firstName",
|
||||
"displayName": "${firstName}",
|
||||
"required": {"roles" : ["user"]},
|
||||
"permissions": {
|
||||
"view": ["admin", "user"],
|
||||
|
@ -16,6 +19,7 @@
|
|||
},
|
||||
{
|
||||
"name": "lastName",
|
||||
"displayName": "${lastName}",
|
||||
"required": {"roles" : ["user"]},
|
||||
"permissions": {
|
||||
"view": ["admin", "user"],
|
||||
|
|
|
@ -126,7 +126,9 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest {
|
|||
actions.add(action);
|
||||
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.setDefaultClientScopes(Collections.singletonList(SCOPE_DEPARTMENT));
|
||||
client_scope_default.setRedirectUris(Collections.singletonList("*"));
|
||||
|
|
|
@ -46,6 +46,7 @@ import org.junit.Test;
|
|||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.component.ComponentValidationException;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
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.UPAttributePermissions;
|
||||
import org.keycloak.userprofile.config.UPAttributeRequired;
|
||||
import org.keycloak.userprofile.config.UPAttributeSelector;
|
||||
import org.keycloak.userprofile.config.UPConfig;
|
||||
import org.keycloak.testsuite.util.ClientScopeBuilder;
|
||||
import org.keycloak.testsuite.util.KeycloakModelUtils;
|
||||
|
@ -87,7 +89,9 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
|
||||
@Override
|
||||
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");
|
||||
client.setDefaultClientScopes(Collections.singletonList("customer"));
|
||||
KeycloakModelUtils.createClient(testRealm, "client-b");
|
||||
|
@ -284,7 +288,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
attribute.setRequired(requirements);
|
||||
|
||||
UPAttributePermissions permissions = new UPAttributePermissions();
|
||||
permissions.setEdit(Collections.singletonList(ROLE_USER));
|
||||
permissions.setEdit(Collections.singleton(ROLE_USER));
|
||||
attribute.setPermissions(permissions);
|
||||
|
||||
config.addAttribute(attribute);
|
||||
|
@ -709,7 +713,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
attribute.setRequired(requirements);
|
||||
|
||||
UPAttributePermissions permissions = new UPAttributePermissions();
|
||||
permissions.setEdit(Collections.singletonList(ROLE_USER));
|
||||
permissions.setEdit(Collections.singleton(ROLE_USER));
|
||||
attribute.setPermissions(permissions);
|
||||
|
||||
config.addAttribute(attribute);
|
||||
|
@ -820,14 +824,12 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
|
||||
UPAttributeRequired requirements = new UPAttributeRequired();
|
||||
|
||||
List<String> roles = new ArrayList<>();
|
||||
roles.add(ROLE_USER);
|
||||
requirements.setRoles(roles);
|
||||
requirements.setRoles(Collections.singleton(ROLE_USER));
|
||||
|
||||
attribute.setRequired(requirements);
|
||||
|
||||
UPAttributePermissions permissions = new UPAttributePermissions();
|
||||
permissions.setEdit(Collections.singletonList(ROLE_USER));
|
||||
permissions.setEdit(Collections.singleton(ROLE_USER));
|
||||
attribute.setPermissions(permissions);
|
||||
|
||||
config.addAttribute(attribute);
|
||||
|
@ -889,12 +891,12 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
|
||||
UPAttributeRequired requirements = new UPAttributeRequired();
|
||||
|
||||
requirements.setRoles(Collections.singletonList(ROLE_ADMIN));
|
||||
requirements.setRoles(Collections.singleton(ROLE_ADMIN));
|
||||
|
||||
attribute.setRequired(requirements);
|
||||
|
||||
UPAttributePermissions permissions = new UPAttributePermissions();
|
||||
permissions.setEdit(Collections.singletonList(UPConfigUtils.ROLE_ADMIN));
|
||||
permissions.setEdit(Collections.singleton(UPConfigUtils.ROLE_ADMIN));
|
||||
attribute.setPermissions(permissions);
|
||||
|
||||
config.addAttribute(attribute);
|
||||
|
@ -946,7 +948,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
attribute.setRequired(requirements);
|
||||
|
||||
UPAttributePermissions permissions = new UPAttributePermissions();
|
||||
permissions.setEdit(Collections.singletonList(UPConfigUtils.ROLE_ADMIN));
|
||||
permissions.setEdit(Collections.singleton(UPConfigUtils.ROLE_ADMIN));
|
||||
attribute.setPermissions(permissions);
|
||||
|
||||
config.addAttribute(attribute);
|
||||
|
@ -994,9 +996,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
attribute.setRequired(requirements);
|
||||
|
||||
UPAttributePermissions permissions = new UPAttributePermissions();
|
||||
List<String> roles = new ArrayList<>();
|
||||
roles.add(UPConfigUtils.ROLE_USER);
|
||||
permissions.setEdit(roles);
|
||||
permissions.setEdit(Collections.singleton(UPConfigUtils.ROLE_USER));
|
||||
attribute.setPermissions(permissions);
|
||||
|
||||
config.addAttribute(attribute);
|
||||
|
@ -1039,14 +1039,12 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
|
||||
UPAttributeRequired requirements = new UPAttributeRequired();
|
||||
|
||||
List<String> scopes = new ArrayList<>();
|
||||
scopes.add("client-a");
|
||||
requirements.setScopes(scopes);
|
||||
requirements.setScopes(Collections.singleton("client-a"));
|
||||
|
||||
attribute.setRequired(requirements);
|
||||
|
||||
UPAttributePermissions permissions = new UPAttributePermissions();
|
||||
permissions.setEdit(Collections.singletonList("user"));
|
||||
permissions.setEdit(Collections.singleton("user"));
|
||||
attribute.setPermissions(permissions);
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import java.util.Collections;
|
|||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
@ -35,6 +36,7 @@ import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
|||
import org.keycloak.testsuite.runonserver.RunOnServer;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
import org.keycloak.testsuite.util.ClientScopeBuilder;
|
||||
import org.keycloak.userprofile.config.UPAttribute;
|
||||
import org.keycloak.userprofile.config.UPAttributePermissions;
|
||||
import org.keycloak.userprofile.config.UPAttributeRequired;
|
||||
|
@ -51,6 +53,12 @@ public class UPConfigParserTest extends AbstractTestRealmKeycloakTest {
|
|||
|
||||
@Override
|
||||
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
|
||||
|
@ -158,39 +166,51 @@ public class UPConfigParserTest extends AbstractTestRealmKeycloakTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void validateConfiguration_OK() throws IOException {
|
||||
List<String> errors = validate(null, loadValidConfig());
|
||||
public void validateConfiguration_OK() {
|
||||
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());
|
||||
}
|
||||
|
||||
@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();
|
||||
//we run this test without KeycloakSession so validator configs are not validated here
|
||||
|
||||
UPAttribute attConfig = config.getAttributes().get(1);
|
||||
|
||||
attConfig.setName(null);
|
||||
List<String> errors = validate(null, config);
|
||||
List<String> errors = validate(session, config);
|
||||
Assert.assertEquals(1, errors.size());
|
||||
|
||||
attConfig.setName(" ");
|
||||
errors = validate(null, config);
|
||||
errors = validate(session, config);
|
||||
Assert.assertEquals(1, errors.size());
|
||||
|
||||
// duplicate attribute name
|
||||
attConfig.setName("firstName");
|
||||
errors = validate(null, config);
|
||||
errors = validate(session, config);
|
||||
Assert.assertEquals(1, errors.size());
|
||||
|
||||
// attribute name format error - unallowed character
|
||||
attConfig.setName("ema il");
|
||||
errors = validate(null, config);
|
||||
errors = validate(session, config);
|
||||
Assert.assertEquals(1, errors.size());
|
||||
}
|
||||
|
||||
@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();
|
||||
//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
|
||||
attConfig.setPermissions(null);
|
||||
List<String> errors = validate(null, config);
|
||||
List<String> errors = validate(session, config);
|
||||
Assert.assertEquals(0, errors.size());
|
||||
|
||||
// no permissions structure fields configured
|
||||
UPAttributePermissions permsConfig = new UPAttributePermissions();
|
||||
attConfig.setPermissions(permsConfig);
|
||||
errors = validate(null, config);
|
||||
errors = validate(session, config);
|
||||
Assert.assertTrue(errors.isEmpty());
|
||||
|
||||
// valid if both are present, even empty
|
||||
permsConfig.setEdit(Collections.emptyList());
|
||||
permsConfig.setView(Collections.emptyList());
|
||||
permsConfig.setEdit(Collections.emptySet());
|
||||
permsConfig.setView(Collections.emptySet());
|
||||
attConfig.setPermissions(permsConfig);
|
||||
errors = validate(null, config);
|
||||
errors = validate(session, config);
|
||||
Assert.assertEquals(0, errors.size());
|
||||
|
||||
List<String> withInvRole = new ArrayList<>();
|
||||
withInvRole.add("invalid");
|
||||
Set<String> withInvRole = Collections.singleton("invalid");
|
||||
|
||||
// invalid role used for view
|
||||
permsConfig.setView(withInvRole);
|
||||
errors = validate(null, config);
|
||||
errors = validate(session, config);
|
||||
Assert.assertEquals(1, errors.size());
|
||||
|
||||
// invalid role used for edit also
|
||||
permsConfig.setEdit(withInvRole);
|
||||
errors = validate(null, config);
|
||||
errors = validate(session, config);
|
||||
Assert.assertEquals(2, errors.size());
|
||||
}
|
||||
|
||||
@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();
|
||||
//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
|
||||
attConfig.setRequired(null);
|
||||
List<String> errors = validate(null, config);
|
||||
List<String> errors = validate(session, config);
|
||||
Assert.assertEquals(0, errors.size());
|
||||
|
||||
// it is OK with empty config as it means ALWAYS required
|
||||
UPAttributeRequired reqConfig = new UPAttributeRequired();
|
||||
attConfig.setRequired(reqConfig);
|
||||
errors = validate(null, config);
|
||||
errors = validate(session, config);
|
||||
Assert.assertEquals(0, errors.size());
|
||||
Assert.assertTrue(reqConfig.isAlways());
|
||||
|
||||
List<String> withInvRole = new ArrayList<>();
|
||||
withInvRole.add("invalid");
|
||||
|
||||
// invalid role used
|
||||
reqConfig.setRoles(withInvRole);;
|
||||
errors = validate(null, config);
|
||||
reqConfig.setRoles(Collections.singleton("invalid"));
|
||||
errors = validate(session, config);
|
||||
Assert.assertEquals(1, errors.size());
|
||||
Assert.assertFalse(reqConfig.isAlways());
|
||||
|
||||
|
@ -260,7 +280,7 @@ public class UPConfigParserTest extends AbstractTestRealmKeycloakTest {
|
|||
|
||||
@Test
|
||||
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 {
|
||||
|
@ -274,8 +294,8 @@ public class UPConfigParserTest extends AbstractTestRealmKeycloakTest {
|
|||
//add validation config for one attribute for testing purposes
|
||||
Map<String, Map<String, Object>> validationConfig = new HashMap<>();
|
||||
config.getAttributes().get(1).setValidations(validationConfig);
|
||||
|
||||
// empty validator name
|
||||
|
||||
// empty validator name
|
||||
validationConfig.put(" ",null);
|
||||
List<String> errors = validate(session, config);
|
||||
Assert.assertEquals(1, errors.size());
|
||||
|
@ -288,9 +308,5 @@ public class UPConfigParserTest extends AbstractTestRealmKeycloakTest {
|
|||
validationConfig.put("length", vc );
|
||||
errors = validate(session, config);
|
||||
Assert.assertEquals(1, errors.size());
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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_USER;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
@ -52,14 +53,14 @@ public class UPConfigUtilsTest {
|
|||
|
||||
Assert.assertFalse(UPConfigUtils.isRoleForContext(UserProfileContext.ACCOUNT, null));
|
||||
|
||||
List<String> roles = new ArrayList<>();
|
||||
Set<String> roles = new HashSet<>();
|
||||
roles.add(ROLE_ADMIN);
|
||||
Assert.assertTrue(UPConfigUtils.isRoleForContext(UserProfileContext.USER_API, roles));
|
||||
Assert.assertFalse(UPConfigUtils.isRoleForContext(UserProfileContext.ACCOUNT, roles));
|
||||
Assert.assertFalse(UPConfigUtils.isRoleForContext(UserProfileContext.ACCOUNT_OLD, roles));
|
||||
Assert.assertFalse(UPConfigUtils.isRoleForContext(UserProfileContext.UPDATE_PROFILE, roles));
|
||||
|
||||
roles = new ArrayList<>();
|
||||
roles = new HashSet<>();
|
||||
roles.add(ROLE_USER);
|
||||
Assert.assertFalse(UPConfigUtils.isRoleForContext(UserProfileContext.USER_API, roles));
|
||||
Assert.assertTrue(UPConfigUtils.isRoleForContext(UserProfileContext.ACCOUNT, roles));
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
"name":"email ",
|
||||
"validations": {
|
||||
"length" : { "max": 255 },
|
||||
"emailFormat": {},
|
||||
"emailDomainDenyList": {}
|
||||
"email": {},
|
||||
"not-blank": {}
|
||||
},
|
||||
"required": {
|
||||
"roles" : ["user", "admin"]
|
||||
|
@ -44,7 +44,7 @@
|
|||
"name":"phone",
|
||||
"displayName" : "${profile.phone}",
|
||||
"validations": {
|
||||
"phoneNumberFormatInternational":{}
|
||||
"not-blank":{}
|
||||
},
|
||||
"required": {
|
||||
"scopes" : ["phone-1", "phone-2"],
|
||||
|
|
|
@ -1896,8 +1896,14 @@ user.profile.attribute.name=Name
|
|||
user.profile.attribute.name.tooltip=The name of the attribute.
|
||||
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.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.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.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.
|
||||
|
|
|
@ -268,7 +268,10 @@ module.config([ '$routeProvider', function($routeProvider) {
|
|||
},
|
||||
realm : function(RealmLoader) {
|
||||
return RealmLoader();
|
||||
}
|
||||
},
|
||||
clientScopes : function(ClientScopeListLoader) {
|
||||
return ClientScopeListLoader();
|
||||
},
|
||||
},
|
||||
controller : 'RealmUserProfileCtrl'
|
||||
})
|
||||
|
|
|
@ -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.validatorProviders = serverInfo.componentTypes['org.keycloak.validate.Validator'];
|
||||
|
||||
|
@ -1420,14 +1420,29 @@ module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, $http,
|
|||
|
||||
$scope.showJsonEditor = function() {
|
||||
$scope.isShowAttributes = false;
|
||||
delete $scope.currentAttribute;
|
||||
}
|
||||
|
||||
$scope.canViewPermission = {
|
||||
$scope.isRequiredRoles = {
|
||||
minimumInputLength: 0,
|
||||
delay: 500,
|
||||
allowClear: true,
|
||||
id: function(e) { return e; },
|
||||
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) {
|
||||
return object;
|
||||
|
@ -1437,18 +1452,57 @@ module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, $http,
|
|||
}
|
||||
};
|
||||
|
||||
$scope.canEditPermission = {
|
||||
minimumInputLength: 0,
|
||||
$scope.isRequiredScopes = {
|
||||
minimumInputLength: 1,
|
||||
delay: 500,
|
||||
allowClear: true,
|
||||
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) {
|
||||
return object;
|
||||
return object.name;
|
||||
},
|
||||
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.isCreate = true;
|
||||
$scope.currentAttribute = {
|
||||
selector: {
|
||||
scopes: []
|
||||
},
|
||||
required: {
|
||||
roles: [],
|
||||
scopes: []
|
||||
},
|
||||
permissions: {
|
||||
view: [],
|
||||
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.canUserView = attribute.permissions.view.includes('user');
|
||||
$scope.canAdminView = attribute.permissions.view.includes('admin');
|
||||
|
@ -1514,27 +1604,33 @@ module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, $http,
|
|||
|
||||
$scope.$watch('isRequired', function() {
|
||||
if ($scope.isRequired) {
|
||||
$scope.currentAttribute.required = {};
|
||||
} else {
|
||||
$scope.currentAttribute.required = {
|
||||
roles: [],
|
||||
scopes: []
|
||||
};
|
||||
} else if ($scope.currentAttribute) {
|
||||
delete $scope.currentAttribute.required;
|
||||
}
|
||||
}, true);
|
||||
|
||||
handlePermission = function(permission, role, allowed) {
|
||||
let attribute = $scope.currentAttribute;
|
||||
let roles = [];
|
||||
|
||||
for (let r of attribute.permissions[permission]) {
|
||||
if (r != role) {
|
||||
roles.push(r);
|
||||
if (attribute && attribute.permissions) {
|
||||
let roles = [];
|
||||
|
||||
for (let r of attribute.permissions[permission]) {
|
||||
if (r != role) {
|
||||
roles.push(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allowed) {
|
||||
roles.push(role);
|
||||
}
|
||||
if (allowed) {
|
||||
roles.push(role);
|
||||
}
|
||||
|
||||
attribute.permissions[permission] = roles;
|
||||
attribute.permissions[permission] = roles;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.$watch('canUserView', function() {
|
||||
|
@ -1603,8 +1699,24 @@ module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, $http,
|
|||
$scope.config = JSON.parse($scope.rawConfig);
|
||||
}
|
||||
|
||||
if ($scope.isCreate && $scope.currentAttribute) {
|
||||
$scope.config['attributes'].push($scope.currentAttribute);
|
||||
if ($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},
|
||||
|
|
|
@ -85,6 +85,13 @@
|
|||
type="text" class="form-control"/>
|
||||
</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">
|
||||
<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>
|
||||
|
@ -93,6 +100,20 @@
|
|||
on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
|
||||
</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">
|
||||
<legend collapsed><span class="text">{{:: 'user.profile.attribute.permission' | translate}}</span></legend>
|
||||
<div class="form-group">
|
||||
|
|
Loading…
Reference in a new issue