mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-10 15:32:05 -03:30
Add a policy condition based on user roles (#42487)
Closes #42117 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
d05c3b5a9e
commit
1b17a3c9a6
@ -31,6 +31,8 @@ import org.keycloak.models.MembershipMetadata;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.RoleModel.RoleGrantedEvent;
|
||||
import org.keycloak.models.RoleModel.RoleRevokedEvent;
|
||||
import org.keycloak.models.SubjectCredentialManager;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.jpa.entities.UserAttributeEntity;
|
||||
@ -38,7 +40,6 @@ import org.keycloak.models.jpa.entities.UserEntity;
|
||||
import org.keycloak.models.jpa.entities.UserGroupMembershipEntity;
|
||||
import org.keycloak.models.jpa.entities.UserRequiredActionEntity;
|
||||
import org.keycloak.models.jpa.entities.UserRoleMappingEntity;
|
||||
import org.keycloak.models.policy.ResourcePolicyManager;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.RoleUtils;
|
||||
|
||||
@ -507,6 +508,7 @@ public class UserAdapter implements UserModel, JpaModel<UserEntity> {
|
||||
public void grantRole(RoleModel role) {
|
||||
if (hasDirectRole(role)) return;
|
||||
grantRoleImpl(role);
|
||||
RoleGrantedEvent.fire(role, this, session);
|
||||
}
|
||||
|
||||
public void grantRoleImpl(RoleModel role) {
|
||||
@ -545,6 +547,7 @@ public class UserAdapter implements UserModel, JpaModel<UserEntity> {
|
||||
em.remove(entity);
|
||||
}
|
||||
em.flush();
|
||||
RoleRevokedEvent.fire(role, this, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
package org.keycloak.models.policy.conditions;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.policy.ResourcePolicyConditionProviderFactory;
|
||||
|
||||
public class RolePolicyConditionFactory implements ResourcePolicyConditionProviderFactory<RolePolicyConditionProvider> {
|
||||
|
||||
public static final String ID = "role-condition";
|
||||
public static final String EXPECTED_ROLES = "roles";
|
||||
|
||||
@Override
|
||||
public RolePolicyConditionProvider create(KeycloakSession session, Map<String, List<String>> config) {
|
||||
return new RolePolicyConditionProvider(session, config.get(EXPECTED_ROLES));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(org.keycloak.Config.Scope config) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
package org.keycloak.models.policy.conditions;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
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.models.policy.ResourcePolicyConditionProvider;
|
||||
import org.keycloak.models.policy.ResourcePolicyEvent;
|
||||
import org.keycloak.models.policy.ResourceType;
|
||||
import org.keycloak.models.utils.RoleUtils;
|
||||
|
||||
public class RolePolicyConditionProvider implements ResourcePolicyConditionProvider {
|
||||
|
||||
private final List<String> expectedRoles;
|
||||
private final KeycloakSession session;
|
||||
|
||||
public RolePolicyConditionProvider(KeycloakSession session, List<String> expectedRoles) {
|
||||
this.session = session;
|
||||
this.expectedRoles = expectedRoles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evaluate(ResourcePolicyEvent event) {
|
||||
if (!ResourceType.USERS.equals(event.getResourceType())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String userId = event.getResourceId();
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
UserModel user = session.users().getUserById(realm, userId);
|
||||
|
||||
if (user == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Set<RoleModel> roles = user.getRoleMappingsStream().collect(Collectors.toSet());
|
||||
|
||||
for (String name : expectedRoles) {
|
||||
RoleModel expectedRole = getRole(name, realm);
|
||||
|
||||
if (expectedRole == null || !RoleUtils.hasRole(roles, expectedRole)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private RoleModel getRole(String expectedRole, RealmModel realm) {
|
||||
boolean isClientRole = expectedRole.indexOf('/') != -1;
|
||||
|
||||
if (isClientRole) {
|
||||
String[] parts = expectedRole.split("/");
|
||||
|
||||
if (parts.length != 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String clientId = parts[0];
|
||||
String roleName = parts[1];
|
||||
|
||||
ClientModel client = session.clients().getClientByClientId(realm, clientId);
|
||||
|
||||
if (client == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return client.getRole(roleName);
|
||||
}
|
||||
|
||||
return realm.getRole(expectedRole);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
}
|
||||
@ -17,4 +17,5 @@
|
||||
|
||||
org.keycloak.models.policy.conditions.GroupMembershipPolicyConditionFactory
|
||||
org.keycloak.models.policy.conditions.IdentityProviderPolicyConditionFactory
|
||||
org.keycloak.models.policy.conditions.UserAttributePolicyConditionFactory
|
||||
org.keycloak.models.policy.conditions.UserAttributePolicyConditionFactory
|
||||
org.keycloak.models.policy.conditions.RolePolicyConditionFactory
|
||||
@ -1,7 +1,6 @@
|
||||
package org.keycloak.models.policy;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.events.admin.OperationType;
|
||||
@ -9,6 +8,9 @@ import org.keycloak.models.FederatedIdentityModel;
|
||||
import org.keycloak.models.FederatedIdentityModel.FederatedIdentityCreatedEvent;
|
||||
import org.keycloak.models.FederatedIdentityModel.FederatedIdentityRemovedEvent;
|
||||
import org.keycloak.models.GroupModel.GroupMemberJoinEvent;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.RoleModel.RoleGrantedEvent;
|
||||
import org.keycloak.models.RoleModel.RoleRevokedEvent;
|
||||
import org.keycloak.provider.ProviderEvent;
|
||||
|
||||
public enum ResourceOperationType {
|
||||
@ -17,7 +19,8 @@ public enum ResourceOperationType {
|
||||
LOGIN(EventType.LOGIN),
|
||||
ADD_FEDERATED_IDENTITY(new Class[] {FederatedIdentityCreatedEvent.class}, new Class[] {FederatedIdentityRemovedEvent.class}),
|
||||
REMOVE_FEDERATED_IDENTITY(FederatedIdentityRemovedEvent.class),
|
||||
GROUP_MEMBERSHIP_JOIN(GroupMemberJoinEvent.class);
|
||||
GROUP_MEMBERSHIP_JOIN(GroupMemberJoinEvent.class),
|
||||
ROLE_GRANTED(new Class[] {RoleGrantedEvent.class}, new Class[] {RoleRevokedEvent.class});
|
||||
|
||||
private final List<Object> types;
|
||||
private final List<Object> deactivationTypes;
|
||||
@ -71,6 +74,12 @@ public enum ResourceOperationType {
|
||||
if (event instanceof FederatedIdentityModel.FederatedIdentityRemovedEvent fie) {
|
||||
return fie.getUser().getId();
|
||||
}
|
||||
if (event instanceof RoleModel.RoleGrantedEvent rge) {
|
||||
return rge.getUser().getId();
|
||||
}
|
||||
if (event instanceof RoleModel.RoleRevokedEvent rre) {
|
||||
return rre.getUser().getId();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@ -40,6 +40,68 @@ public interface RoleModel {
|
||||
KeycloakSession getKeycloakSession();
|
||||
}
|
||||
|
||||
interface RoleEvent extends ProviderEvent {
|
||||
RealmModel getRealm();
|
||||
RoleModel getRole();
|
||||
KeycloakSession getKeycloakSession();
|
||||
}
|
||||
|
||||
interface RoleGrantedEvent extends RoleModel.RoleEvent {
|
||||
static void fire(RoleModel role, UserModel user, KeycloakSession session) {
|
||||
session.getKeycloakSessionFactory().publish(new RoleModel.RoleGrantedEvent() {
|
||||
@Override
|
||||
public RealmModel getRealm() {
|
||||
return session.getContext().getRealm();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RoleModel getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserModel getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeycloakSession getKeycloakSession() {
|
||||
return session;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
UserModel getUser();
|
||||
}
|
||||
|
||||
interface RoleRevokedEvent extends RoleModel.RoleEvent {
|
||||
static void fire(RoleModel role, UserModel user, KeycloakSession session) {
|
||||
session.getKeycloakSessionFactory().publish(new RoleModel.RoleRevokedEvent() {
|
||||
@Override
|
||||
public RealmModel getRealm() {
|
||||
return session.getContext().getRealm();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RoleModel getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserModel getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeycloakSession getKeycloakSession() {
|
||||
return session;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
UserModel getUser();
|
||||
}
|
||||
|
||||
String getName();
|
||||
|
||||
String getDescription();
|
||||
|
||||
@ -0,0 +1,207 @@
|
||||
package org.keycloak.tests.admin.model.policy;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.keycloak.models.policy.conditions.RolePolicyConditionFactory.EXPECTED_ROLES;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.admin.client.resource.RealmResourcePolicies;
|
||||
import org.keycloak.admin.client.resource.RolesResource;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.policy.EventBasedResourcePolicyProviderFactory;
|
||||
import org.keycloak.models.policy.ResourceOperationType;
|
||||
import org.keycloak.models.policy.ResourcePolicyManager;
|
||||
import org.keycloak.models.policy.SetUserAttributeActionProviderFactory;
|
||||
import org.keycloak.models.policy.conditions.RolePolicyConditionFactory;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyActionRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyConditionRepresentation;
|
||||
import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.injection.LifeCycle;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.realm.RoleConfigBuilder;
|
||||
import org.keycloak.testframework.realm.UserConfigBuilder;
|
||||
import org.keycloak.testframework.remote.runonserver.InjectRunOnServer;
|
||||
import org.keycloak.testframework.remote.runonserver.RunOnServerClient;
|
||||
import org.keycloak.testframework.util.ApiUtil;
|
||||
|
||||
@KeycloakIntegrationTest(config = RLMServerConfig.class)
|
||||
public class RolePolicyConditionTest {
|
||||
|
||||
private static final String REALM_NAME = "default";
|
||||
|
||||
@InjectRunOnServer(permittedPackages = "org.keycloak.tests")
|
||||
RunOnServerClient runOnServer;
|
||||
|
||||
@InjectRealm(lifecycle = LifeCycle.METHOD)
|
||||
ManagedRealm managedRealm;
|
||||
|
||||
@BeforeEach
|
||||
public void onBefore() {
|
||||
UPConfig upConfig = managedRealm.admin().users().userProfile().getConfiguration();
|
||||
upConfig.setUnmanagedAttributePolicy(UnmanagedAttributePolicy.ENABLED);
|
||||
managedRealm.admin().users().userProfile().update(upConfig);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConditionForSingleRole() {
|
||||
String expected = "realm-role-1";
|
||||
createPolicy(expected);
|
||||
assertUserRoles("user-1", false);
|
||||
assertUserRoles("user-2", false, "not-valid-role");
|
||||
assertUserRoles("user-3", true, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConditionForMultipleRole() {
|
||||
List<String> expected = List.of("realm-role-1", "realm-role-2", "client-a/client-role-1");
|
||||
createPolicy(expected);
|
||||
assertUserRoles("user-1", false, List.of("realm-role-1", "realm-role-2"));
|
||||
assertUserRoles("user-2", false, List.of("realm-role-1", "realm-role-2", "client-b/client-role-1"));
|
||||
assertUserRoles("user-3", true, expected);
|
||||
}
|
||||
|
||||
private void assertUserRoles(String username, boolean shouldExist, String... roles) {
|
||||
assertUserRoles(username, shouldExist, List.of(roles));
|
||||
}
|
||||
|
||||
private void assertUserRoles(String username, boolean shouldExist, List<String> roles) {
|
||||
try (Response response = managedRealm.admin().users().create(UserConfigBuilder.create()
|
||||
.username(username)
|
||||
.email(username + "@example.com")
|
||||
.build())) {
|
||||
String id = ApiUtil.getCreatedId(response);
|
||||
|
||||
for (String roleName : roles) {
|
||||
RoleRepresentation role = createRoleIfNotExists(roleName);
|
||||
|
||||
if (role.getClientRole()) {
|
||||
managedRealm.admin().users().get(id).roles().clientLevel(role.getContainerId()).add(List.of(role));
|
||||
} else {
|
||||
managedRealm.admin().users().get(id).roles().realmLevel().add(List.of(role));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runOnServer.run((session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
|
||||
try {
|
||||
// set offset to 7 days - notify action should run now
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds()));
|
||||
new ResourcePolicyManager(session).runScheduledActions();
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
|
||||
UserModel user = session.users().getUserByUsername(realm, username);
|
||||
assertNotNull(user);
|
||||
|
||||
if (shouldExist) {
|
||||
assertTrue(user.getAttributes().containsKey("notified"));
|
||||
} else {
|
||||
assertFalse(user.getAttributes().containsKey("notified"));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private void createPolicy(String... expectedValues) {
|
||||
createPolicy(Map.of(EXPECTED_ROLES, List.of(expectedValues)));
|
||||
}
|
||||
|
||||
private void createPolicy(List<String> expectedValues) {
|
||||
createPolicy(Map.of(EXPECTED_ROLES, expectedValues));
|
||||
}
|
||||
|
||||
private void createPolicy(Map<String, List<String>> attributes) {
|
||||
for (String roleName : attributes.getOrDefault(EXPECTED_ROLES, List.of())) {
|
||||
createRoleIfNotExists(roleName);
|
||||
}
|
||||
|
||||
List<ResourcePolicyRepresentation> expectedPolicies = ResourcePolicyRepresentation.create()
|
||||
.of(EventBasedResourcePolicyProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.ROLE_GRANTED.name())
|
||||
.recurring()
|
||||
.onCoditions(ResourcePolicyConditionRepresentation.create()
|
||||
.of(RolePolicyConditionFactory.ID)
|
||||
.withConfig(attributes)
|
||||
.build())
|
||||
.withActions(
|
||||
ResourcePolicyActionRepresentation.create()
|
||||
.of(SetUserAttributeActionProviderFactory.ID)
|
||||
.withConfig("notified", "true")
|
||||
.after(Duration.ofDays(5))
|
||||
.build()
|
||||
).build();
|
||||
|
||||
RealmResourcePolicies policies = managedRealm.admin().resources().policies();
|
||||
|
||||
try (Response response = policies.create(expectedPolicies)) {
|
||||
assertThat(response.getStatus(), is(Status.CREATED.getStatusCode()));
|
||||
}
|
||||
}
|
||||
|
||||
private RoleRepresentation createRoleIfNotExists(String roleName) {
|
||||
if (roleName.indexOf('/') != -1) {
|
||||
String[] parts = roleName.split("/");
|
||||
String clientId = parts[0];
|
||||
String clientRoleName = parts[1];
|
||||
List<ClientRepresentation> clients = managedRealm.admin().clients().findByClientId(clientId);
|
||||
|
||||
if (clients.isEmpty()) {
|
||||
ClientRepresentation client = new ClientRepresentation();
|
||||
client.setClientId(clientId);
|
||||
client.setName(clientId);
|
||||
client.setProtocol("openid-connect");
|
||||
managedRealm.admin().clients().create(client).close();
|
||||
clients = managedRealm.admin().clients().findByClientId(clientId);
|
||||
}
|
||||
|
||||
assertThat(clients.isEmpty(), is(false));
|
||||
|
||||
RolesResource roles = managedRealm.admin().clients().get(clients.get(0).getId()).roles();
|
||||
|
||||
if (roles.list(clientRoleName, -1, -1).isEmpty()) {
|
||||
roles.create(RoleConfigBuilder.create()
|
||||
.name(clientRoleName)
|
||||
.build());
|
||||
}
|
||||
|
||||
return roles.get(clientRoleName).toRepresentation();
|
||||
} else {
|
||||
RolesResource roles = managedRealm.admin().roles();
|
||||
|
||||
if (roles.list(roleName, -1, -1).isEmpty()) {
|
||||
roles.create(RoleConfigBuilder.create()
|
||||
.name(roleName)
|
||||
.build());
|
||||
}
|
||||
|
||||
return roles.get(roleName).toRepresentation();
|
||||
}
|
||||
}
|
||||
|
||||
private static RealmModel configureSessionContext(KeycloakSession session) {
|
||||
RealmModel realm = session.realms().getRealmByName(REALM_NAME);
|
||||
session.getContext().setRealm(realm);
|
||||
return realm;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user