More info on UserProfileContext

closes #25691

Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
mposolda 2023-12-19 10:01:29 +01:00 committed by Pedro Igor
parent 810ebf4efd
commit eb184a8554
10 changed files with 114 additions and 72 deletions

View file

@ -95,7 +95,6 @@ import org.keycloak.storage.user.UserRegistrationProvider;
import org.keycloak.userprofile.AttributeContext;
import org.keycloak.userprofile.AttributeGroupMetadata;
import org.keycloak.userprofile.AttributeMetadata;
import org.keycloak.userprofile.UserProfileContext;
import org.keycloak.userprofile.UserProfileDecorator;
import org.keycloak.userprofile.UserProfileMetadata;
import org.keycloak.userprofile.UserProfileUtil;
@ -1085,7 +1084,7 @@ public class LDAPStorageProvider implements UserStorageProvider,
}
});
Predicate<AttributeContext> onlyAdminCondition = context -> metadata.getContext() == UserProfileContext.USER_API;
Predicate<AttributeContext> onlyAdminCondition = context -> metadata.getContext().isAdminContext();
int guiOrder = (int) metadata.getAttributes().stream()
.map(AttributeMetadata::getName)

View file

@ -35,7 +35,6 @@ import org.keycloak.storage.user.ImportedUserValidation;
import org.keycloak.storage.user.UserLookupProvider;
import org.keycloak.userprofile.AttributeContext;
import org.keycloak.userprofile.AttributeMetadata;
import org.keycloak.userprofile.UserProfileContext;
import org.keycloak.userprofile.UserProfileDecorator;
import org.keycloak.userprofile.UserProfileMetadata;
@ -228,7 +227,7 @@ public class SSSDFederationProvider implements UserStorageProvider,
attributeContext.getUser() != null && model.getId().equals(attributeContext.getUser().getFederationLink());
// condition to view only by admin
Predicate<AttributeContext> onlyAdminCondition = context -> metadata.getContext() == UserProfileContext.USER_API;
Predicate<AttributeContext> onlyAdminCondition = context -> metadata.getContext().isAdminContext();
// guiOrder if new attributes are needed
int guiOrder = (int) metadata.getAttributes().stream()

View file

@ -122,7 +122,7 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
case ENABLED:
return true;
case ADMIN_EDIT:
return UserProfileContext.USER_API.equals(context);
return context.isAdminContext();
}
return false;
@ -436,7 +436,7 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
case ADMIN_EDIT:
case ADMIN_VIEW:
// unmanaged attributes only available through the admin context
return UserProfileContext.USER_API.equals(context);
return context.isAdminContext();
}
// allow unmanaged attributes if enabled to all contexts
@ -524,13 +524,13 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
@Override
public boolean canView(AttributeContext context) {
return canEdit(context)
|| (UnmanagedAttributePolicy.ADMIN_VIEW.equals(unmanagedAttributePolicy) && UserProfileContext.USER_API.equals(context.getContext()));
|| (UnmanagedAttributePolicy.ADMIN_VIEW.equals(unmanagedAttributePolicy) && context.getContext().isAdminContext());
}
@Override
public boolean canEdit(AttributeContext context) {
return UnmanagedAttributePolicy.ENABLED.equals(unmanagedAttributePolicy)
|| (UnmanagedAttributePolicy.ADMIN_EDIT.equals(unmanagedAttributePolicy) && UserProfileContext.USER_API.equals(context.getContext()));
|| (UnmanagedAttributePolicy.ADMIN_EDIT.equals(unmanagedAttributePolicy) && context.getContext().isAdminContext());
}
};
}

View file

@ -257,7 +257,7 @@ public final class DefaultUserProfile implements UserProfile {
UserProfileContext context = metadata.getContext();
R rep;
if (UserProfileContext.USER_API.equals(context)) {
if (context.isAdminContext()) {
RealmModel realm = session.getContext().getRealm();
rep = (R) ModelToRepresentation.toRepresentation(session, realm, user);
} else {

View file

@ -81,7 +81,7 @@ public class UserProfileUtil {
return false;
} else {
logger.tracef("Adding metadata attribute '%s' to user profile by user storage provider '%s' for user profile context '%s'.", attrName, storageProviderName, metadata.getContext().toString());
Predicate<AttributeContext> onlyAdminCondition = context -> metadata.getContext() == UserProfileContext.USER_API;
Predicate<AttributeContext> onlyAdminCondition = context -> metadata.getContext().isAdminContext();
AttributeMetadata attributeMetadata = metadata.addAttribute(attrName, guiOrder, Collections.emptyList())
.addWriteCondition(AttributeMetadata.ALWAYS_FALSE) // Not writable for anyone
.addReadCondition(onlyAdminCondition) // Read-only for administrators

View file

@ -0,0 +1,31 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.keycloak.userprofile;
/**
* Constants related to user profile
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class UserProfileConstants {
public static final String ROLE_USER = "user";
public static final String ROLE_ADMIN = "admin";
}

View file

@ -19,6 +19,11 @@
package org.keycloak.userprofile;
import java.util.Set;
import static org.keycloak.userprofile.UserProfileConstants.ROLE_ADMIN;
import static org.keycloak.userprofile.UserProfileConstants.ROLE_USER;
/**
* <p>This interface represents the different contexts from where user profiles are managed. The core contexts are already
* available here representing the different areas in Keycloak where user profiles are managed.
@ -33,44 +38,78 @@ public enum UserProfileContext {
/**
* In this context, a user profile is managed by themselves during an authentication flow such as when updating the user profile.
*/
UPDATE_PROFILE(true),
UPDATE_PROFILE(false, true, true),
/**
* In this context, a user profile is managed through the management interface such as the Admin API.
*/
USER_API(false),
USER_API(true, false, false),
/**
* In this context, a user profile is managed by themselves through the account console.
*/
ACCOUNT(true),
ACCOUNT(false, false, true),
/**
* In this context, a user profile is managed by themselves when authenticating through a broker.
*/
IDP_REVIEW(false),
IDP_REVIEW(false, true, false),
/**
* In this context, a user profile is managed by themselves when registering to a realm.
*/
REGISTRATION(false),
REGISTRATION(false, true, false),
/**
* In this context, a user profile is managed by themselves when updating their email through an application initiated action.
*/
UPDATE_EMAIL(false);
UPDATE_EMAIL(false, true, false);
private boolean resetEmailVerified;
private final boolean resetEmailVerified;
private final boolean adminContext;
private final boolean authFlowContext;
UserProfileContext(boolean resetEmailVerified){
UserProfileContext(boolean adminContext, boolean authFlowContext, boolean resetEmailVerified){
this.adminContext = adminContext;
this.authFlowContext = authFlowContext;
this.resetEmailVerified = resetEmailVerified;
}
/**
* @return true means that this context is applicable to administrators. False means that this context is applicable to regular users
*/
public boolean isAdminContext() {
return adminContext;
}
/**
* @return true if context CAN BE part of the authentication flow
*/
public boolean canBeAuthFlowContext() {
return authFlowContext;
}
/**
* @return true means that UserModel.emailVerified flag must be reset to false in this context when email address is updated
*/
public boolean isResetEmailVerified() {
return resetEmailVerified;
}
/**
* Check if roles configuration contains role for this context.
*
* @param roles to be inspected
* @return true if roles list contains role representing checked context
*/
public boolean isRoleForContext(Set<String> roles) {
if (roles == null)
return false;
return roles.contains(getContextRole());
}
private String getContextRole() {
return isAdminContext() ? ROLE_ADMIN : ROLE_USER;
}
}

View file

@ -294,20 +294,20 @@ public class DeclarativeUserProfileProvider implements UserProfileProvider {
Predicate<AttributeContext> required = AttributeMetadata.ALWAYS_FALSE;
if (rc != null) {
if (rc.isAlways() || UPConfigUtils.isRoleForContext(context, rc.getRoles())) {
if (rc.isAlways() || context.isRoleForContext(rc.getRoles())) {
required = AttributeMetadata.ALWAYS_TRUE;
// If scopes are configured, we will use scope-based selector and require the attribute just if scope is
// in current authenticationSession (either default scope or by 'scope' parameter)
if (rc.getScopes() != null && !rc.getScopes().isEmpty()) {
if (UPConfigUtils.canBeAuthFlowContext(context)) {
if (context.canBeAuthFlowContext()) {
required = (c) -> requestedScopePredicate(c, rc.getScopes());
} else {
// Scopes not available for admin and account contexts
required = AttributeMetadata.ALWAYS_FALSE;
}
}
} else if (UPConfigUtils.canBeAuthFlowContext(context) && rc.getScopes() != null && !rc.getScopes().isEmpty()) {
} else if (context.canBeAuthFlowContext() && rc.getScopes() != null && !rc.getScopes().isEmpty()) {
// for contexts executed from auth flow and with configured scopes requirement
// we have to create required validation with scopes based selector
required = (c) -> requestedScopePredicate(c, rc.getScopes());
@ -322,7 +322,7 @@ public class DeclarativeUserProfileProvider implements UserProfileProvider {
Set<String> editRoles = permissions.getEdit();
if (!editRoles.isEmpty()) {
writeAllowed = ac -> UPConfigUtils.isRoleForContext(ac.getContext(), editRoles);
writeAllowed = ac -> ac.getContext().isRoleForContext(editRoles);
}
Set<String> viewRoles = permissions.getView();
@ -336,7 +336,7 @@ public class DeclarativeUserProfileProvider implements UserProfileProvider {
Predicate<AttributeContext> selector = AttributeMetadata.ALWAYS_TRUE;
UPAttributeSelector sc = attrConfig.getSelector();
if (sc != null && !isBuiltInAttribute(attributeName) && UPConfigUtils.canBeAuthFlowContext(context) && sc.getScopes() != null && !sc.getScopes().isEmpty()) {
if (sc != null && !isBuiltInAttribute(attributeName) && context.canBeAuthFlowContext() && sc.getScopes() != null && !sc.getScopes().isEmpty()) {
// for contexts executed from auth flow and with configured scopes selector
// we have to create correct predicate
selector = (c) -> requestedScopePredicate(c, sc.getScopes());
@ -368,7 +368,7 @@ public class DeclarativeUserProfileProvider implements UserProfileProvider {
}
if (UserModel.EMAIL.equals(attributeName)) {
if (UserProfileContext.USER_API.equals(context)) {
if (context.isAdminContext()) {
required = new Predicate<AttributeContext>() {
@Override
public boolean test(AttributeContext context) {
@ -447,7 +447,7 @@ public class DeclarativeUserProfileProvider implements UserProfileProvider {
private Predicate<AttributeContext> createViewAllowedPredicate(Predicate<AttributeContext> canEdit,
Set<String> viewRoles) {
return ac -> UPConfigUtils.isRoleForContext(ac.getContext(), viewRoles) || canEdit.test(ac);
return ac -> ac.getContext().isRoleForContext(viewRoles) || canEdit.test(ac);
}
/**

View file

@ -40,7 +40,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.representations.userprofile.config.UPAttribute;
import org.keycloak.representations.userprofile.config.UPConfig;
import org.keycloak.userprofile.UserProfileContext;
import org.keycloak.userprofile.UserProfileConstants;
import org.keycloak.util.JsonSerialization;
import org.keycloak.validate.ValidationResult;
import org.keycloak.validate.ValidatorConfig;
@ -55,8 +55,8 @@ import org.keycloak.validate.Validators;
public class UPConfigUtils {
private static final String SYSTEM_DEFAULT_CONFIG_RESOURCE = "keycloak-default-user-profile.json";
public static final String ROLE_USER = "user";
public static final String ROLE_ADMIN = "admin";
public static final String ROLE_USER = UserProfileConstants.ROLE_USER;
public static final String ROLE_ADMIN = UserProfileConstants.ROLE_ADMIN;
private static final Set<String> PSEUDOROLES = new HashSet<>();
@ -287,32 +287,6 @@ public class UPConfigUtils {
}
}
/**
* Check if context CAN BE part of the AuthenticationFlow.
*
* @param context to check
* @return true if context CAN BE part of the auth flow
*/
public static boolean canBeAuthFlowContext(UserProfileContext context) {
return context != UserProfileContext.USER_API && context != UserProfileContext.ACCOUNT;
}
/**
* Check if roles configuration contains role given current context.
*
* @param context to be checked
* @param roles to be inspected
* @return true if roles list contains role representing checked context
*/
public static boolean isRoleForContext(UserProfileContext context, Set<String> roles) {
if (roles == null)
return false;
if (context == UserProfileContext.USER_API)
return roles.contains(ROLE_ADMIN);
else
return roles.contains(ROLE_USER);
}
public static String capitalizeFirstLetter(String str) {
if (str == null || str.isEmpty())
return str;

View file

@ -37,38 +37,38 @@ public class UPConfigUtilsTest {
@Test
public void canBeAuthFlowContext() {
Assert.assertFalse(UPConfigUtils.canBeAuthFlowContext(UserProfileContext.ACCOUNT));
Assert.assertFalse(UPConfigUtils.canBeAuthFlowContext(UserProfileContext.USER_API));
Assert.assertFalse(UserProfileContext.ACCOUNT.canBeAuthFlowContext());
Assert.assertFalse(UserProfileContext.USER_API.canBeAuthFlowContext());
Assert.assertTrue(UPConfigUtils.canBeAuthFlowContext(UserProfileContext.IDP_REVIEW));
Assert.assertTrue(UPConfigUtils.canBeAuthFlowContext(UserProfileContext.REGISTRATION));
Assert.assertTrue(UPConfigUtils.canBeAuthFlowContext(UserProfileContext.UPDATE_PROFILE));
Assert.assertTrue(UserProfileContext.IDP_REVIEW.canBeAuthFlowContext());
Assert.assertTrue(UserProfileContext.REGISTRATION.canBeAuthFlowContext());
Assert.assertTrue(UserProfileContext.UPDATE_PROFILE.canBeAuthFlowContext());
}
@Test
public void isRoleForContext() {
Assert.assertFalse(UPConfigUtils.isRoleForContext(UserProfileContext.ACCOUNT, null));
Assert.assertFalse(UserProfileContext.ACCOUNT.isRoleForContext( null));
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.UPDATE_PROFILE, roles));
Assert.assertTrue(UserProfileContext.USER_API.isRoleForContext(roles));
Assert.assertFalse(UserProfileContext.ACCOUNT.isRoleForContext(roles));
Assert.assertFalse(UserProfileContext.UPDATE_PROFILE.isRoleForContext(roles));
roles = new HashSet<>();
roles.add(ROLE_USER);
Assert.assertFalse(UPConfigUtils.isRoleForContext(UserProfileContext.USER_API, roles));
Assert.assertTrue(UPConfigUtils.isRoleForContext(UserProfileContext.ACCOUNT, roles));
Assert.assertTrue(UPConfigUtils.isRoleForContext(UserProfileContext.IDP_REVIEW, roles));
Assert.assertTrue(UPConfigUtils.isRoleForContext(UserProfileContext.REGISTRATION, roles));
Assert.assertFalse(UserProfileContext.USER_API.isRoleForContext(roles));
Assert.assertTrue(UserProfileContext.ACCOUNT.isRoleForContext(roles));
Assert.assertTrue(UserProfileContext.IDP_REVIEW.isRoleForContext(roles));
Assert.assertTrue(UserProfileContext.REGISTRATION.isRoleForContext(roles));
// both in roles
roles.add(ROLE_ADMIN);
Assert.assertTrue(UPConfigUtils.isRoleForContext(UserProfileContext.USER_API, roles));
Assert.assertTrue(UPConfigUtils.isRoleForContext(UserProfileContext.ACCOUNT, roles));
Assert.assertTrue(UPConfigUtils.isRoleForContext(UserProfileContext.IDP_REVIEW, roles));
Assert.assertTrue(UPConfigUtils.isRoleForContext(UserProfileContext.REGISTRATION, roles));
Assert.assertTrue(UserProfileContext.USER_API.isRoleForContext(roles));
Assert.assertTrue(UserProfileContext.ACCOUNT.isRoleForContext(roles));
Assert.assertTrue(UserProfileContext.IDP_REVIEW.isRoleForContext(roles));
Assert.assertTrue(UserProfileContext.REGISTRATION.isRoleForContext(roles));
}
@Test