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.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());
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 |
|
@ -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.
|
||||||
|
|
|
@ -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.
|
|
@ -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")}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in a new issue