Allow fetching roles when evaluating role licies

Closes #20736

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor 2024-03-01 11:34:42 -03:00 committed by Marek Posolda
parent 4fa940a31e
commit d12711e858
9 changed files with 200 additions and 105 deletions

View file

@ -27,8 +27,10 @@ import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.policy.evaluation.Evaluation; import org.keycloak.authorization.policy.evaluation.Evaluation;
import org.keycloak.authorization.policy.provider.PolicyProvider; import org.keycloak.authorization.policy.provider.PolicyProvider;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.authorization.RolePolicyRepresentation; import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
/** /**
@ -47,7 +49,8 @@ public class RolePolicyProvider implements PolicyProvider {
@Override @Override
public void evaluate(Evaluation evaluation) { public void evaluate(Evaluation evaluation) {
Policy policy = evaluation.getPolicy(); Policy policy = evaluation.getPolicy();
Set<RolePolicyRepresentation.RoleDefinition> roleIds = representationFunction.apply(policy, evaluation.getAuthorizationProvider()).getRoles(); RolePolicyRepresentation policyRep = representationFunction.apply(policy, evaluation.getAuthorizationProvider());
Set<RolePolicyRepresentation.RoleDefinition> roleIds = policyRep.getRoles();
AuthorizationProvider authorizationProvider = evaluation.getAuthorizationProvider(); AuthorizationProvider authorizationProvider = evaluation.getAuthorizationProvider();
RealmModel realm = authorizationProvider.getKeycloakSession().getContext().getRealm(); RealmModel realm = authorizationProvider.getKeycloakSession().getContext().getRealm();
Identity identity = evaluation.getContext().getIdentity(); Identity identity = evaluation.getContext().getIdentity();
@ -56,7 +59,7 @@ public class RolePolicyProvider implements PolicyProvider {
RoleModel role = realm.getRoleById(roleDefinition.getId()); RoleModel role = realm.getRoleById(roleDefinition.getId());
if (role != null) { if (role != null) {
boolean hasRole = hasRole(identity, role, realm); boolean hasRole = hasRole(identity, role, realm, authorizationProvider, policyRep.isFetchRoles());
if (!hasRole && roleDefinition.isRequired()) { if (!hasRole && roleDefinition.isRequired()) {
evaluation.deny(); evaluation.deny();
@ -69,7 +72,12 @@ public class RolePolicyProvider implements PolicyProvider {
logger.debugv("policy {} evaluated with status {} on identity {}", policy.getName(), evaluation.getEffect(), identity.getId()); 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(); String roleName = role.getName();
if (role.isClientRole()) { if (role.isClientRole()) {
ClientModel clientModel = realm.getClientById(role.getContainerId()); ClientModel clientModel = realm.getClientById(role.getContainerId());

View file

@ -36,6 +36,7 @@ import org.keycloak.models.RoleModel;
import org.keycloak.representations.idm.authorization.PolicyRepresentation; import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.RolePolicyRepresentation; import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import org.keycloak.utils.StringUtil;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -87,6 +88,12 @@ public class RolePolicyProviderFactory implements PolicyProviderFactory<RolePoli
representation.setRoles(new HashSet<>( representation.setRoles(new HashSet<>(
Arrays.asList(JsonSerialization.readValue(roles, RolePolicyRepresentation.RoleDefinition[].class)))); 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) { } catch (IOException cause) {
throw new RuntimeException("Failed to deserialize roles", cause); throw new RuntimeException("Failed to deserialize roles", cause);
} }
@ -116,6 +123,11 @@ public class RolePolicyProviderFactory implements PolicyProviderFactory<RolePoli
} catch (IOException cause) { } catch (IOException cause) {
throw new RuntimeException("Failed to deserialize roles during import", 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 @Override
@ -139,10 +151,17 @@ public class RolePolicyProviderFactory implements PolicyProviderFactory<RolePoli
throw new RuntimeException("Failed to export role policy [" + policy.getName() + "]", cause); 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); representation.setConfig(config);
} }
private void updateRoles(Policy policy, RolePolicyRepresentation representation, AuthorizationProvider authorization) { private void updateRoles(Policy policy, RolePolicyRepresentation representation, AuthorizationProvider authorization) {
policy.putConfig("fetchRoles", String.valueOf(representation.isFetchRoles()));
updateRoles(policy, authorization, representation.getRoles()); updateRoles(policy, authorization, representation.getRoles());
} }

View file

@ -25,6 +25,7 @@ import java.util.Set;
public class RolePolicyRepresentation extends AbstractPolicyRepresentation { public class RolePolicyRepresentation extends AbstractPolicyRepresentation {
private Set<RoleDefinition> roles; private Set<RoleDefinition> roles;
private boolean fetchRoles;
@Override @Override
public String getType() { public String getType() {
@ -58,6 +59,14 @@ public class RolePolicyRepresentation extends AbstractPolicyRepresentation {
addRole(clientId + "/" + name, required); addRole(clientId + "/" + name, required);
} }
public boolean isFetchRoles() {
return fetchRoles;
}
public void setFetchRoles(boolean fetchRoles) {
this.fetchRoles = fetchRoles;
}
public static class RoleDefinition { public static class RoleDefinition {
private String id; private String id;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View file

@ -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`. 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* * *Logic*
+ +
The logic of this policy to apply after the other conditions have been evaluated. The logic of this policy to apply after the other conditions have been evaluated.

View file

@ -3097,3 +3097,5 @@ addTranslationDialogHelperText=The translation based on the default language is
noLanguagesSearchResultsInstructions=Click on the search bar above to search for languages noLanguagesSearchResultsInstructions=Click on the search bar above to search for languages
addTranslationDialogOkBtn=Ok addTranslationDialogOkBtn=Ok
translationError=Please add translations before saving 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.

View file

@ -18,6 +18,7 @@ import { AddRoleMappingModal } from "../../../components/role-mapping/AddRoleMap
import { Row, ServiceRole } from "../../../components/role-mapping/RoleMapping"; import { Row, ServiceRole } from "../../../components/role-mapping/RoleMapping";
import { useFetch } from "../../../utils/useFetch"; import { useFetch } from "../../../utils/useFetch";
import type { RequiredIdValue } from "./ClientScope"; import type { RequiredIdValue } from "./ClientScope";
import { DefaultSwitchControl } from "../../../components/SwitchControl";
export const Role = () => { export const Role = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -28,6 +29,7 @@ export const Role = () => {
formState: { errors }, formState: { errors },
} = useFormContext<{ } = useFormContext<{
roles?: RequiredIdValue[]; roles?: RequiredIdValue[];
fetchRoles?: boolean;
}>(); }>();
const values = getValues("roles"); const values = getValues("roles");
@ -58,6 +60,7 @@ export const Role = () => {
); );
return ( return (
<>
<FormGroup <FormGroup
label={t("roles")} label={t("roles")}
labelIcon={ labelIcon={
@ -162,5 +165,11 @@ export const Role = () => {
</TableComposable> </TableComposable>
)} )}
</FormGroup> </FormGroup>
<DefaultSwitchControl
name="fetchRoles"
label={t("fetchRoles")}
labelIcon={t("fetchRolesHelp")}
/>
</>
); );
}; };

View file

@ -30,6 +30,9 @@ import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.authorization.client.AuthorizationDeniedException; import org.keycloak.authorization.client.AuthorizationDeniedException;
import org.keycloak.authorization.client.AuthzClient; import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.keycloak.representations.idm.GroupRepresentation; import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.RoleRepresentation;
@ -40,6 +43,7 @@ import org.keycloak.representations.idm.authorization.PermissionRequest;
import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation; import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation; import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.RolePolicyRepresentation; import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.GroupBuilder; import org.keycloak.testsuite.util.GroupBuilder;
import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.RealmBuilder;
@ -164,6 +168,30 @@ public class RolePolicyTest extends AbstractAuthzTest {
assertNotNull(authzClient.authorization("alice", "password").authorize(new AuthorizationRequest(ticket))); assertNotNull(authzClient.authorization("alice", "password").authorize(new AuthorizationRequest(ticket)));
} }
@Test
public void testFetchRoles() {
AuthzClient authzClient = getAuthzClient();
RealmResource realm = getRealm();
ClientsResource clients = realm.clients();
ClientRepresentation client = clients.findByClientId(authzClient.getConfiguration().getResource()).get(0);
ClientScopeRepresentation rolesScope = ApiUtil.findClientScopeByName(realm, OIDCLoginProtocolFactory.ROLES_SCOPE).toRepresentation();
ClientResource clientResource = clients.get(client.getId());
clientResource.removeDefaultClientScope(rolesScope.getId());
getCleanup().addCleanup(() -> clientResource.addDefaultClientScope(rolesScope.getId()));
PermissionRequest request = new PermissionRequest("Resource B");
String ticket = authzClient.protection().permission().create(request).getTicket();
try {
authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
fail("Should fail because no role is available from the token");
} catch (AuthorizationDeniedException ignore) {
}
RolePolicyRepresentation roleRep = clientResource.authorization().policies().role().findByName("Role B Policy");
roleRep.setFetchRoles(true);
clientResource.authorization().policies().role().findById(roleRep.getId()).update(roleRep);
assertNotNull(authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket)));
}
private void createRealmRolePolicy(String name, String... roles) { private void createRealmRolePolicy(String name, String... roles) {
RolePolicyRepresentation policy = new RolePolicyRepresentation(); RolePolicyRepresentation policy = new RolePolicyRepresentation();

View file

@ -34,6 +34,7 @@ import org.keycloak.admin.client.resource.PolicyResource;
import org.keycloak.admin.client.resource.RolePoliciesResource; import org.keycloak.admin.client.resource.RolePoliciesResource;
import org.keycloak.admin.client.resource.RolePolicyResource; import org.keycloak.admin.client.resource.RolePolicyResource;
import org.keycloak.admin.client.resource.RolesResource; import org.keycloak.admin.client.resource.RolesResource;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.authorization.DecisionStrategy; import org.keycloak.representations.idm.authorization.DecisionStrategy;
@ -73,6 +74,19 @@ public class RolePolicyManagementTest extends AbstractPolicyManagementTest {
assertCreated(authorization, representation); assertCreated(authorization, representation);
} }
@Test
public void testCreateFetchRoles() {
AuthorizationResource authorization = getClient().authorization();
RolePolicyRepresentation representation = new RolePolicyRepresentation();
representation.setName(KeycloakModelUtils.generateId());
representation.setFetchRoles(true);
representation.addRole("Role A", false);
representation.addRole("Role B", true);
assertCreated(authorization, representation);
}
@Test @Test
public void testCreateClientRolePolicy() { public void testCreateClientRolePolicy() {
ClientResource client = getClient(); ClientResource client = getClient();
@ -115,6 +129,7 @@ public class RolePolicyManagementTest extends AbstractPolicyManagementTest {
representation.setName("changed"); representation.setName("changed");
representation.setDescription("changed"); representation.setDescription("changed");
representation.setFetchRoles(true);
representation.setDecisionStrategy(DecisionStrategy.AFFIRMATIVE); representation.setDecisionStrategy(DecisionStrategy.AFFIRMATIVE);
representation.setLogic(Logic.POSITIVE); representation.setLogic(Logic.POSITIVE);
representation.setRoles(representation.getRoles().stream().filter(roleDefinition -> !roleDefinition.getId().equals("Resource A")).collect(Collectors.toSet())); representation.setRoles(representation.getRoles().stream().filter(roleDefinition -> !roleDefinition.getId().equals("Resource A")).collect(Collectors.toSet()));
@ -208,6 +223,7 @@ public class RolePolicyManagementTest extends AbstractPolicyManagementTest {
.filter(roleDefinition -> (getRoleName(actualDefinition.getId()).equals(roleDefinition.getId()) || (clientRep.getClientId() + "/" + getRoleName(actualDefinition.getId())).equals(roleDefinition.getId())) && actualDefinition.isRequired() == roleDefinition.isRequired()) .filter(roleDefinition -> (getRoleName(actualDefinition.getId()).equals(roleDefinition.getId()) || (clientRep.getClientId() + "/" + getRoleName(actualDefinition.getId())).equals(roleDefinition.getId())) && actualDefinition.isRequired() == roleDefinition.isRequired())
.findFirst().isPresent()) .findFirst().isPresent())
.count()); .count());
assertEquals(representation.isFetchRoles(), actual.isFetchRoles());
} }
private String getRoleName(String id) { private String getRoleName(String id) {