Refactoring how policies are activated based on user-defined events and conditions

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor 2025-09-01 16:20:28 -03:00
parent cee9b6803b
commit b65356f3c8
6 changed files with 58 additions and 93 deletions

View File

@ -19,10 +19,7 @@ package org.keycloak.models.policy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import jakarta.persistence.EntityManager;
import jakarta.persistence.criteria.CriteriaBuilder;
@ -33,22 +30,18 @@ import jakarta.persistence.criteria.Subquery;
import org.keycloak.component.ComponentModel;
import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.jpa.entities.UserEntity;
import org.keycloak.models.policy.conditions.IdentityProviderPolicyConditionFactory;
import org.keycloak.models.policy.conditions.IdentityProviderPolicyConditionProvider;
public abstract class AbstractUserResourcePolicyProvider implements ResourcePolicyProvider {
public abstract class AbstractUserResourcePolicyProvider extends EventBasedResourcePolicyProvider {
private final ComponentModel policyModel;
private final EntityManager em;
private final KeycloakSession session;
public AbstractUserResourcePolicyProvider(KeycloakSession session, ComponentModel model) {
this.policyModel = model;
super(session, model);
this.em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
this.session = session;
}
@Override
@ -66,7 +59,7 @@ public abstract class AbstractUserResourcePolicyProvider implements ResourcePoli
subquery.where(
cb.and(
cb.equal(stateRoot.get("resourceId"), userRoot.get("id")),
cb.equal(stateRoot.get("policyId"), policyModel.getId())
cb.equal(stateRoot.get("policyId"), getModel().getId())
)
);
Predicate notExistsPredicate = cb.not(cb.exists(subquery));
@ -80,7 +73,7 @@ public abstract class AbstractUserResourcePolicyProvider implements ResourcePoli
}
private List<Predicate> getConditionsPredicate(CriteriaBuilder cb, CriteriaQuery<String> query, Root<UserEntity> path) {
List<String> conditions = policyModel.getConfig().getOrDefault("conditions", List.of());
List<String> conditions = getModel().getConfig().getOrDefault("conditions", List.of());
if (conditions.isEmpty()) {
return List.of();
@ -100,23 +93,6 @@ public abstract class AbstractUserResourcePolicyProvider implements ResourcePoli
return predicates;
}
@Override
public boolean supports(ResourceType type) {
return ResourceType.USERS.equals(type);
}
@Override
public boolean activateOnEvent(ResourcePolicyEvent event) {
boolean b = this.supports(event.getResourceType())
&& this.getSupportedOperationsForActivation().contains(event.getOperation());
if (!b) {
return false;
}
return evaluate(event);
}
@Override
public boolean resetOnEvent(ResourcePolicyEvent event) {
boolean b = this.supports(event.getResourceType())
@ -140,20 +116,6 @@ public abstract class AbstractUserResourcePolicyProvider implements ResourcePoli
return !evaluate(event);
}
private boolean evaluate(ResourcePolicyEvent event) {
List<String> conditions = policyModel.getConfig().getOrDefault("conditions", List.of());
for (String providerId : conditions) {
ResourcePolicyConditionProvider condition = resolveCondition(providerId);
if (!condition.evaluate(event)) {
return false;
}
}
return true;
}
@Override
public void close() {
// no-op
@ -179,20 +141,12 @@ public abstract class AbstractUserResourcePolicyProvider implements ResourcePoli
return em;
}
protected ComponentModel getModel() {
return policyModel;
}
protected KeycloakSession getSession() {
return session;
}
protected RealmModel getRealm() {
return getSession().getContext().getRealm();
}
protected List<String> getBrokerAliases() {
List<String> conditions = policyModel.getConfig().getOrDefault("conditions", List.of());
List<String> conditions = getModel().getConfig().getOrDefault("conditions", List.of());
for (String providerId : conditions) {
ResourcePolicyConditionProvider condition = resolveCondition(providerId);
@ -204,29 +158,4 @@ public abstract class AbstractUserResourcePolicyProvider implements ResourcePoli
return List.of();
}
private ResourcePolicyConditionProvider resolveCondition(String providerId) {
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
ResourcePolicyConditionProviderFactory<ResourcePolicyConditionProvider> providerFactory = (ResourcePolicyConditionProviderFactory<ResourcePolicyConditionProvider>) sessionFactory.getProviderFactory(ResourcePolicyConditionProvider.class, providerId);
if (providerFactory == null) {
throw new IllegalStateException("Could not find condition provider: " + providerId);
}
Map<String, List<String>> config = new HashMap<>();
for (Entry<String, List<String>> configEntry : policyModel.getConfig().entrySet()) {
if (configEntry.getKey().startsWith(providerId)) {
config.put(configEntry.getKey().substring(providerId.length() + 1), configEntry.getValue());
}
}
ResourcePolicyConditionProvider condition = providerFactory.create(session, config);
if (condition == null) {
throw new IllegalStateException("Factory " + providerFactory.getClass() + " returned a null provider");
}
return condition;
}
}

View File

@ -31,6 +31,10 @@ public class EventBasedResourcePolicyProvider implements ResourcePolicyProvider
@Override
public boolean activateOnEvent(ResourcePolicyEvent event) {
if (!supports(event.getResourceType())) {
return false;
}
List<String> events = model.getConfig().getOrDefault("events", List.of());
ResourceOperationType operation = event.getOperation();
@ -38,7 +42,34 @@ public class EventBasedResourcePolicyProvider implements ResourcePolicyProvider
return false;
}
List<String> conditions = model.getConfig().getOrDefault("conditions", List.of());
return evaluate(event);
}
@Override
public boolean deactivateOnEvent(ResourcePolicyEvent event) {
if (!supports(event.getResourceType())) {
return false;
}
return false;
}
@Override
public boolean resetOnEvent(ResourcePolicyEvent event) {
if (!supports(event.getResourceType())) {
return false;
}
return false;
}
@Override
public void close() {
}
protected boolean evaluate(ResourcePolicyEvent event) {
List<String> conditions = getModel().getConfig().getOrDefault("conditions", List.of());
for (String providerId : conditions) {
ResourcePolicyConditionProvider condition = resolveCondition(providerId);
@ -51,22 +82,7 @@ public class EventBasedResourcePolicyProvider implements ResourcePolicyProvider
return true;
}
@Override
public boolean deactivateOnEvent(ResourcePolicyEvent event) {
return false;
}
@Override
public boolean resetOnEvent(ResourcePolicyEvent event) {
return false;
}
@Override
public void close() {
}
private ResourcePolicyConditionProvider resolveCondition(String providerId) {
protected ResourcePolicyConditionProvider resolveCondition(String providerId) {
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
ResourcePolicyConditionProviderFactory<ResourcePolicyConditionProvider> providerFactory = (ResourcePolicyConditionProviderFactory<ResourcePolicyConditionProvider>) sessionFactory.getProviderFactory(ResourcePolicyConditionProvider.class, providerId);
@ -90,4 +106,12 @@ public class EventBasedResourcePolicyProvider implements ResourcePolicyProvider
return condition;
}
protected ComponentModel getModel() {
return model;
}
protected KeycloakSession getSession() {
return session;
}
}

View File

@ -34,6 +34,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.policy.DeleteUserActionProviderFactory;
import org.keycloak.models.policy.ResourceOperationType;
import org.keycloak.models.policy.ResourcePolicy;
import org.keycloak.models.policy.ResourcePolicyManager;
import org.keycloak.models.policy.ResourcePolicyStateProvider;
@ -119,6 +120,7 @@ public class BrokeredUserSessionRefreshTimePolicyTest {
public void tesRunActionOnFederatedUser() {
consumerRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
.of(UserSessionRefreshTimeResourcePolicyProviderFactory.ID)
.onEvent(ResourceOperationType.LOGIN.toString())
.onCoditions(ResourcePolicyConditionRepresentation.create()
.of(IdentityProviderPolicyConditionFactory.ID)
.withConfig(IdentityProviderPolicyConditionFactory.EXPECTED_ALIASES, IDP_OIDC_ALIAS)
@ -189,6 +191,7 @@ public class BrokeredUserSessionRefreshTimePolicyTest {
public void testAddRemoveFedIdentityAffectsPolicyAssociation() {
consumerRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
.of(UserSessionRefreshTimeResourcePolicyProviderFactory.ID)
.onEvent(ResourceOperationType.ADD_FEDERATED_IDENTITY.toString())
.onCoditions(ResourcePolicyConditionRepresentation.create()
.of(IdentityProviderPolicyConditionFactory.ID)
.withConfig(IdentityProviderPolicyConditionFactory.EXPECTED_ALIASES, IDP_OIDC_ALIAS)

View File

@ -196,6 +196,7 @@ public class ResourcePolicyManagementTest {
public void testPolicyDoesNotFallThroughActionsInSingleRun() {
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
.onEvent(ResourceOperationType.CREATE.toString())
.withActions(
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
.after(Duration.ofDays(5))
@ -360,6 +361,7 @@ public class ResourcePolicyManagementTest {
// create a test policy
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
.onEvent(ResourceOperationType.CREATE.toString())
.name("test-policy")
.withConfig("enabled", "true")
.withActions(
@ -467,6 +469,7 @@ public class ResourcePolicyManagementTest {
public void testRecurringPolicy() {
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
.onEvent(ResourceOperationType.CREATE.toString())
.withConfig("recurring", "true")
.withActions(
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)

View File

@ -7,6 +7,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.policy.DisableUserActionProviderFactory;
import org.keycloak.models.policy.NotifyUserActionProviderFactory;
import org.keycloak.models.policy.ResourceOperationType;
import org.keycloak.models.policy.ResourcePolicyManager;
import org.keycloak.models.policy.UserCreationTimeResourcePolicyProviderFactory;
import org.keycloak.representations.idm.CredentialRepresentation;
@ -57,6 +58,7 @@ public class UserCreationTimePolicyTest {
public void testDisableUserBasedOnCreationDate() {
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
.onEvent(ResourceOperationType.CREATE.toString())
.withActions(
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
.after(Duration.ofDays(5))

View File

@ -33,6 +33,7 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.models.policy.DisableUserActionProviderFactory;
import org.keycloak.models.policy.NotifyUserActionProviderFactory;
import org.keycloak.models.policy.ResourceOperationType;
import org.keycloak.models.policy.ResourcePolicyManager;
import org.keycloak.models.policy.UserSessionRefreshTimeResourcePolicyProviderFactory;
import org.keycloak.representations.resources.policies.ResourcePolicyActionRepresentation;
@ -86,6 +87,7 @@ public class UserSessionRefreshTimePolicyTest {
public void testDisabledUserAfterInactivityPeriod() {
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
.of(UserSessionRefreshTimeResourcePolicyProviderFactory.ID)
.onEvent(ResourceOperationType.LOGIN.toString())
.withActions(
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
.after(Duration.ofDays(5))
@ -164,12 +166,14 @@ public class UserSessionRefreshTimePolicyTest {
public void testMultiplePolicies() {
managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create()
.of(UserSessionRefreshTimeResourcePolicyProviderFactory.ID)
.onEvent(ResourceOperationType.LOGIN.toString())
.withActions(
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
.after(Duration.ofDays(5))
.withConfig("message_key", "notifier1")
.build()
).of(UserSessionRefreshTimeResourcePolicyProviderFactory.ID)
.onEvent(ResourceOperationType.LOGIN.toString())
.withActions(
ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID)
.after(Duration.ofDays(10))