Allow fetching roles when evaluating role licies
Closes #20736 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
4fa940a31e
commit
d12711e858
9 changed files with 200 additions and 105 deletions
|
@ -27,8 +27,10 @@ import org.keycloak.authorization.model.Policy;
|
|||
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 class RolePolicyProvider implements PolicyProvider {
|
|||
@Override
|
||||
public void evaluate(Evaluation evaluation) {
|
||||
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();
|
||||
RealmModel realm = authorizationProvider.getKeycloakSession().getContext().getRealm();
|
||||
Identity identity = evaluation.getContext().getIdentity();
|
||||
|
@ -56,7 +59,7 @@ public class RolePolicyProvider implements PolicyProvider {
|
|||
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 class RolePolicyProvider implements PolicyProvider {
|
|||
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());
|
||||
|
|
|
@ -36,6 +36,7 @@ import org.keycloak.models.RoleModel;
|
|||
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 class RolePolicyProviderFactory implements PolicyProviderFactory<RolePoli
|
|||
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 class RolePolicyProviderFactory implements PolicyProviderFactory<RolePoli
|
|||
} 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 class RolePolicyProviderFactory implements PolicyProviderFactory<RolePoli
|
|||
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());
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import java.util.Set;
|
|||
public class RolePolicyRepresentation extends AbstractPolicyRepresentation {
|
||||
|
||||
private Set<RoleDefinition> roles;
|
||||
private boolean fetchRoles;
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
|
@ -58,6 +59,14 @@ public class RolePolicyRepresentation extends AbstractPolicyRepresentation {
|
|||
addRole(clientId + "/" + name, required);
|
||||
}
|
||||
|
||||
public boolean isFetchRoles() {
|
||||
return fetchRoles;
|
||||
}
|
||||
|
||||
public void setFetchRoles(boolean fetchRoles) {
|
||||
this.fetchRoles = fetchRoles;
|
||||
}
|
||||
|
||||
public static class RoleDefinition {
|
||||
|
||||
private String id;
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 42 KiB |
|
@ -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.
|
||||
|
|
|
@ -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.
|
|
@ -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 (
|
||||
<FormGroup
|
||||
label={t("roles")}
|
||||
labelIcon={
|
||||
<HelpItem helpText={t("policyRolesHelp")} fieldLabelId="roles" />
|
||||
}
|
||||
fieldId="roles"
|
||||
helperTextInvalid={t("requiredRoles")}
|
||||
validated={errors.roles ? "error" : "default"}
|
||||
isRequired
|
||||
>
|
||||
<Controller
|
||||
name="roles"
|
||||
control={control}
|
||||
defaultValue={[]}
|
||||
rules={{
|
||||
validate: (value?: RequiredIdValue[]) =>
|
||||
value && value.filter((c) => c.id).length > 0,
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<>
|
||||
{open && (
|
||||
<AddRoleMappingModal
|
||||
id="role"
|
||||
type="roles"
|
||||
onAssign={(rows) => {
|
||||
field.onChange([
|
||||
...(field.value || []),
|
||||
...rows.map((row) => ({ id: row.role.id })),
|
||||
]);
|
||||
setSelectedRoles([...selectedRoles, ...rows]);
|
||||
setOpen(false);
|
||||
<>
|
||||
<FormGroup
|
||||
label={t("roles")}
|
||||
labelIcon={
|
||||
<HelpItem helpText={t("policyRolesHelp")} fieldLabelId="roles" />
|
||||
}
|
||||
fieldId="roles"
|
||||
helperTextInvalid={t("requiredRoles")}
|
||||
validated={errors.roles ? "error" : "default"}
|
||||
isRequired
|
||||
>
|
||||
<Controller
|
||||
name="roles"
|
||||
control={control}
|
||||
defaultValue={[]}
|
||||
rules={{
|
||||
validate: (value?: RequiredIdValue[]) =>
|
||||
value && value.filter((c) => c.id).length > 0,
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<>
|
||||
{open && (
|
||||
<AddRoleMappingModal
|
||||
id="role"
|
||||
type="roles"
|
||||
onAssign={(rows) => {
|
||||
field.onChange([
|
||||
...(field.value || []),
|
||||
...rows.map((row) => ({ id: row.role.id })),
|
||||
]);
|
||||
setSelectedRoles([...selectedRoles, ...rows]);
|
||||
setOpen(false);
|
||||
}}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
isLDAPmapper
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
data-testid="select-role-button"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
isLDAPmapper
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
data-testid="select-role-button"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
{t("addRoles")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
{selectedRoles.length > 0 && (
|
||||
<TableComposable variant="compact">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{t("roles")}</Th>
|
||||
<Th>{t("required")}</Th>
|
||||
<Th aria-hidden="true" />
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{selectedRoles.map((row, index) => (
|
||||
<Tr key={row.role.id}>
|
||||
<Td>
|
||||
<ServiceRole role={row.role} client={row.client} />
|
||||
</Td>
|
||||
<Td>
|
||||
<Controller
|
||||
name={`roles.${index}.required`}
|
||||
defaultValue={false}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
id="required"
|
||||
data-testid="standard"
|
||||
name="required"
|
||||
isChecked={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
<Button
|
||||
variant="link"
|
||||
className="keycloak__client-authorization__policy-row-remove"
|
||||
icon={<MinusCircleIcon />}
|
||||
onClick={() => {
|
||||
setValue("roles", [
|
||||
...(values || []).filter((s) => s.id !== row.role.id),
|
||||
]);
|
||||
setSelectedRoles([
|
||||
...selectedRoles.filter(
|
||||
(s) => s.role.id !== row.role.id,
|
||||
),
|
||||
]);
|
||||
}}
|
||||
/>
|
||||
</Td>
|
||||
>
|
||||
{t("addRoles")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
{selectedRoles.length > 0 && (
|
||||
<TableComposable variant="compact">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{t("roles")}</Th>
|
||||
<Th>{t("required")}</Th>
|
||||
<Th aria-hidden="true" />
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</TableComposable>
|
||||
)}
|
||||
</FormGroup>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{selectedRoles.map((row, index) => (
|
||||
<Tr key={row.role.id}>
|
||||
<Td>
|
||||
<ServiceRole role={row.role} client={row.client} />
|
||||
</Td>
|
||||
<Td>
|
||||
<Controller
|
||||
name={`roles.${index}.required`}
|
||||
defaultValue={false}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
id="required"
|
||||
data-testid="standard"
|
||||
name="required"
|
||||
isChecked={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
<Button
|
||||
variant="link"
|
||||
className="keycloak__client-authorization__policy-row-remove"
|
||||
icon={<MinusCircleIcon />}
|
||||
onClick={() => {
|
||||
setValue("roles", [
|
||||
...(values || []).filter((s) => s.id !== row.role.id),
|
||||
]);
|
||||
setSelectedRoles([
|
||||
...selectedRoles.filter(
|
||||
(s) => s.role.id !== row.role.id,
|
||||
),
|
||||
]);
|
||||
}}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</TableComposable>
|
||||
)}
|
||||
</FormGroup>
|
||||
<DefaultSwitchControl
|
||||
name="fetchRoles"
|
||||
label={t("fetchRoles")}
|
||||
labelIcon={t("fetchRolesHelp")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -30,6 +30,9 @@ import org.keycloak.admin.client.resource.ClientsResource;
|
|||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.authorization.client.AuthorizationDeniedException;
|
||||
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.RealmRepresentation;
|
||||
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.ResourceRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.util.ClientBuilder;
|
||||
import org.keycloak.testsuite.util.GroupBuilder;
|
||||
import org.keycloak.testsuite.util.RealmBuilder;
|
||||
|
@ -164,6 +168,30 @@ public class RolePolicyTest extends AbstractAuthzTest {
|
|||
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) {
|
||||
RolePolicyRepresentation policy = new RolePolicyRepresentation();
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ import org.keycloak.admin.client.resource.PolicyResource;
|
|||
import org.keycloak.admin.client.resource.RolePoliciesResource;
|
||||
import org.keycloak.admin.client.resource.RolePolicyResource;
|
||||
import org.keycloak.admin.client.resource.RolesResource;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.DecisionStrategy;
|
||||
|
@ -73,6 +74,19 @@ public class RolePolicyManagementTest extends AbstractPolicyManagementTest {
|
|||
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
|
||||
public void testCreateClientRolePolicy() {
|
||||
ClientResource client = getClient();
|
||||
|
@ -115,6 +129,7 @@ public class RolePolicyManagementTest extends AbstractPolicyManagementTest {
|
|||
|
||||
representation.setName("changed");
|
||||
representation.setDescription("changed");
|
||||
representation.setFetchRoles(true);
|
||||
representation.setDecisionStrategy(DecisionStrategy.AFFIRMATIVE);
|
||||
representation.setLogic(Logic.POSITIVE);
|
||||
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())
|
||||
.findFirst().isPresent())
|
||||
.count());
|
||||
assertEquals(representation.isFetchRoles(), actual.isFetchRoles());
|
||||
}
|
||||
|
||||
private String getRoleName(String id) {
|
||||
|
|
Loading…
Reference in a new issue