diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProvider.java index e8db89d651e0..499acba9a838 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProvider.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProvider.java @@ -27,8 +27,10 @@ import org.keycloak.authorization.policy.evaluation.Evaluation; import org.keycloak.authorization.policy.provider.PolicyProvider; import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; import org.keycloak.representations.idm.authorization.RolePolicyRepresentation; /** @@ -47,7 +49,8 @@ public RolePolicyProvider(BiFunction roleIds = representationFunction.apply(policy, evaluation.getAuthorizationProvider()).getRoles(); + RolePolicyRepresentation policyRep = representationFunction.apply(policy, evaluation.getAuthorizationProvider()); + Set roleIds = policyRep.getRoles(); AuthorizationProvider authorizationProvider = evaluation.getAuthorizationProvider(); RealmModel realm = authorizationProvider.getKeycloakSession().getContext().getRealm(); Identity identity = evaluation.getContext().getIdentity(); @@ -56,7 +59,7 @@ public void evaluate(Evaluation evaluation) { RoleModel role = realm.getRoleById(roleDefinition.getId()); if (role != null) { - boolean hasRole = hasRole(identity, role, realm); + boolean hasRole = hasRole(identity, role, realm, authorizationProvider, policyRep.isFetchRoles()); if (!hasRole && roleDefinition.isRequired()) { evaluation.deny(); @@ -69,7 +72,12 @@ public void evaluate(Evaluation evaluation) { logger.debugv("policy {} evaluated with status {} on identity {}", policy.getName(), evaluation.getEffect(), identity.getId()); } - private boolean hasRole(Identity identity, RoleModel role, RealmModel realm) { + private boolean hasRole(Identity identity, RoleModel role, RealmModel realm, AuthorizationProvider authorizationProvider, boolean fetchRoles) { + if (fetchRoles) { + KeycloakSession session = authorizationProvider.getKeycloakSession(); + UserModel user = session.users().getUserById(realm, identity.getId()); + return user.hasRole(role); + } String roleName = role.getName(); if (role.isClientRole()) { ClientModel clientModel = realm.getClientById(role.getContainerId()); diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java index f82e9db9c89b..bd00d0b9fda4 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java @@ -36,6 +36,7 @@ import org.keycloak.representations.idm.authorization.PolicyRepresentation; import org.keycloak.representations.idm.authorization.RolePolicyRepresentation; import org.keycloak.util.JsonSerialization; +import org.keycloak.utils.StringUtil; import java.io.IOException; import java.util.ArrayList; @@ -87,6 +88,12 @@ public RolePolicyRepresentation toRepresentation(Policy policy, AuthorizationPro representation.setRoles(new HashSet<>( Arrays.asList(JsonSerialization.readValue(roles, RolePolicyRepresentation.RoleDefinition[].class)))); } + + String fetchRoles = policy.getConfig().get("fetchRoles"); + + if (StringUtil.isNotBlank(fetchRoles)) { + representation.setFetchRoles(Boolean.parseBoolean(fetchRoles)); + } } catch (IOException cause) { throw new RuntimeException("Failed to deserialize roles", cause); } @@ -116,6 +123,11 @@ public void onImport(Policy policy, PolicyRepresentation representation, Authori } catch (IOException cause) { throw new RuntimeException("Failed to deserialize roles during import", cause); } + String fetchRoles = representation.getConfig().get("fetchRoles"); + + if (StringUtil.isNotBlank(fetchRoles)) { + policy.putConfig("fetchRoles", fetchRoles); + } } @Override @@ -139,10 +151,17 @@ public void onExport(Policy policy, PolicyRepresentation representation, Authori throw new RuntimeException("Failed to export role policy [" + policy.getName() + "]", cause); } + String fetchRoles = policy.getConfig().get("fetchRoles"); + + if (StringUtil.isNotBlank(fetchRoles)) { + config.put("fetchRoles", fetchRoles); + } + representation.setConfig(config); } private void updateRoles(Policy policy, RolePolicyRepresentation representation, AuthorizationProvider authorization) { + policy.putConfig("fetchRoles", String.valueOf(representation.isFetchRoles())); updateRoles(policy, authorization, representation.getRoles()); } diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/RolePolicyRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/RolePolicyRepresentation.java index a4f29738f40a..623587efdb50 100644 --- a/core/src/main/java/org/keycloak/representations/idm/authorization/RolePolicyRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/authorization/RolePolicyRepresentation.java @@ -25,6 +25,7 @@ public class RolePolicyRepresentation extends AbstractPolicyRepresentation { private Set roles; + private boolean fetchRoles; @Override public String getType() { @@ -58,6 +59,14 @@ public void addClientRole(String clientId, String name, boolean required) { addRole(clientId + "/" + name, required); } + public boolean isFetchRoles() { + return fetchRoles; + } + + public void setFetchRoles(boolean fetchRoles) { + this.fetchRoles = fetchRoles; + } + public static class RoleDefinition { private String id; diff --git a/docs/documentation/authorization_services/images/policy/create-role.png b/docs/documentation/authorization_services/images/policy/create-role.png index 0aece01caefc..a0aa2ef1c633 100644 Binary files a/docs/documentation/authorization_services/images/policy/create-role.png and b/docs/documentation/authorization_services/images/policy/create-role.png differ diff --git a/docs/documentation/authorization_services/topics/policy-role-policy.adoc b/docs/documentation/authorization_services/topics/policy-role-policy.adoc index 06510d3be6ae..4c3729afa572 100644 --- a/docs/documentation/authorization_services/topics/policy-role-policy.adoc +++ b/docs/documentation/authorization_services/topics/policy-role-policy.adoc @@ -31,6 +31,10 @@ Specifies which *realm* roles are permitted by this policy. + Specifies which *client* roles are permitted by this policy. To enable this field must first select a `Client`. + +* *Fetch Roles* ++ +By default, only the roles available from the token sent with the authorization requests are used to check if the user is granted with a role. If this setting is enabled, the policy will ignore roles from the token and check any role associated with the user instead. ++ * *Logic* + The logic of this policy to apply after the other conditions have been evaluated. diff --git a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties index 2f69e963083f..f57add052dab 100644 --- a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties +++ b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties @@ -3097,3 +3097,5 @@ addTranslationDialogHelperText=The translation based on the default language is noLanguagesSearchResultsInstructions=Click on the search bar above to search for languages addTranslationDialogOkBtn=Ok translationError=Please add translations before saving +fetchRoles=Fetch Roles +fetchRolesHelp=By default, only the roles available from the token sent with the authorization requests are used to check if the user is granted with a role. If this setting is enabled, the policy will ignore roles from the token and check any role associated with the user instead. \ No newline at end of file diff --git a/js/apps/admin-ui/src/clients/authorization/policy/Role.tsx b/js/apps/admin-ui/src/clients/authorization/policy/Role.tsx index 3ef9a640aa1e..1374cc9b7a7e 100644 --- a/js/apps/admin-ui/src/clients/authorization/policy/Role.tsx +++ b/js/apps/admin-ui/src/clients/authorization/policy/Role.tsx @@ -18,6 +18,7 @@ import { AddRoleMappingModal } from "../../../components/role-mapping/AddRoleMap import { Row, ServiceRole } from "../../../components/role-mapping/RoleMapping"; import { useFetch } from "../../../utils/useFetch"; import type { RequiredIdValue } from "./ClientScope"; +import { DefaultSwitchControl } from "../../../components/SwitchControl"; export const Role = () => { const { t } = useTranslation(); @@ -28,6 +29,7 @@ export const Role = () => { formState: { errors }, } = useFormContext<{ roles?: RequiredIdValue[]; + fetchRoles?: boolean; }>(); const values = getValues("roles"); @@ -58,109 +60,116 @@ export const Role = () => { ); return ( - - } - fieldId="roles" - helperTextInvalid={t("requiredRoles")} - validated={errors.roles ? "error" : "default"} - isRequired - > - - value && value.filter((c) => c.id).length > 0, - }} - render={({ field }) => ( - <> - {open && ( - { - field.onChange([ - ...(field.value || []), - ...rows.map((row) => ({ id: row.role.id })), - ]); - setSelectedRoles([...selectedRoles, ...rows]); - setOpen(false); - }} - onClose={() => { - setOpen(false); + <> + + } + fieldId="roles" + helperTextInvalid={t("requiredRoles")} + validated={errors.roles ? "error" : "default"} + isRequired + > + + value && value.filter((c) => c.id).length > 0, + }} + render={({ field }) => ( + <> + {open && ( + { + field.onChange([ + ...(field.value || []), + ...rows.map((row) => ({ id: row.role.id })), + ]); + setSelectedRoles([...selectedRoles, ...rows]); + setOpen(false); + }} + onClose={() => { + setOpen(false); + }} + isLDAPmapper + /> + )} + - + > + {t("addRoles")} + + + )} + /> + {selectedRoles.length > 0 && ( + + + + {t("roles")} + {t("required")} + + + + + {selectedRoles.map((row, index) => ( + + + + + + ( + + )} + /> + + +