mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-10 15:32:05 -03:30
Rework getEligibleResourcesForInitialAction so it returns all resources that are eligible to be associated with a policy
Closes #42106 Signed-off-by: Stefan Guilhen <sguilhen@redhat.com>
This commit is contained in:
parent
360ff7050c
commit
8eb6ee619f
@ -23,6 +23,8 @@ import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
import jakarta.persistence.criteria.Subquery;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
@ -31,6 +33,7 @@ import org.keycloak.models.FederatedIdentityModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.jpa.entities.FederatedIdentityEntity;
|
||||
import org.keycloak.models.jpa.entities.UserEntity;
|
||||
|
||||
public abstract class AbstractUserResourcePolicyProvider implements ResourcePolicyProvider {
|
||||
@ -39,69 +42,67 @@ public abstract class AbstractUserResourcePolicyProvider implements ResourcePoli
|
||||
private final EntityManager em;
|
||||
private final KeycloakSession session;
|
||||
|
||||
private static final String BROKER_ALIASES = "broker-aliases";
|
||||
|
||||
public AbstractUserResourcePolicyProvider(KeycloakSession session, ComponentModel model) {
|
||||
this.policyModel = model;
|
||||
this.em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
public abstract Predicate timePredicate(long time, CriteriaBuilder cb, CriteriaQuery<String> query, Root<UserEntity> userRoot);
|
||||
|
||||
// For each user row, a subquery is executed to check if a corresponding record exists in
|
||||
// the state table. If no record is found, the condition is met -> user is eligible for initial action
|
||||
|
||||
@Override
|
||||
public List<String> getEligibleResourcesForInitialAction(long time) {
|
||||
public List<String> getEligibleResourcesForInitialAction() {
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaQuery<String> query = cb.createQuery(String.class);
|
||||
Root<UserEntity> userRoot = query.from(UserEntity.class);
|
||||
List<Predicate> predicates = new ArrayList<>();
|
||||
|
||||
// Subquery will find if a state record exists for the user and policy
|
||||
// SELECT 1 FROM ResourcePolicyStateEntity s WHERE s.resourceId = userRoot.id AND s.policyId = :policyId
|
||||
Subquery<Integer> subquery = query.subquery(Integer.class);
|
||||
Root<ResourcePolicyStateEntity> stateRoot = subquery.from(ResourcePolicyStateEntity.class);
|
||||
subquery.select(cb.literal(1)); // Select 1 for existence check
|
||||
subquery.select(cb.literal(1));
|
||||
subquery.where(
|
||||
cb.and(
|
||||
cb.equal(stateRoot.get("resourceId"), userRoot.get("id")),
|
||||
cb.equal(stateRoot.get("policyId"), policyModel.getId())
|
||||
)
|
||||
);
|
||||
|
||||
// Time-based condition
|
||||
Predicate timePredicate = timePredicate(time, cb, query, userRoot);
|
||||
|
||||
// NOT EXISTS condition
|
||||
Predicate notExistsPredicate = cb.not(cb.exists(subquery));
|
||||
predicates.add(notExistsPredicate);
|
||||
|
||||
query.where(cb.and(timePredicate, notExistsPredicate));
|
||||
query.select(userRoot.get("id"));
|
||||
|
||||
return em.createQuery(query).getResultList();
|
||||
}
|
||||
@Override
|
||||
public List<String> filterEligibleResources(List<String> candidateResourceIds, long time) {
|
||||
// If there are no candidates, return an empty list
|
||||
if (candidateResourceIds == null || candidateResourceIds.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
// origin-based condition
|
||||
Predicate originPredicate = buildOriginPredicate(cb, query, userRoot);
|
||||
if (originPredicate != null) {
|
||||
predicates.add(originPredicate);
|
||||
}
|
||||
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaQuery<String> query = cb.createQuery(String.class);
|
||||
Root<UserEntity> userRoot = query.from(UserEntity.class);
|
||||
|
||||
// Time-based condition
|
||||
Predicate timePredicate = timePredicate(time, cb, query, userRoot);
|
||||
|
||||
// IN clause with candidateResourceIds
|
||||
Predicate inClausePredicate = userRoot.get("id").in(candidateResourceIds);
|
||||
|
||||
query.where(cb.and(timePredicate, inClausePredicate));
|
||||
query.select(userRoot.get("id"));
|
||||
// todo: add relationship predicates (groups, roles, perhaps user attribute?)
|
||||
|
||||
query.select(userRoot.get("id")).where(predicates);
|
||||
return em.createQuery(query).getResultList();
|
||||
}
|
||||
|
||||
protected Predicate buildOriginPredicate(CriteriaBuilder cb, CriteriaQuery<String> query, Root<UserEntity> userRoot) {
|
||||
// As of now we check only if there are broker aliases configured for the policy. More complete approach should check
|
||||
// a "origin" attribute for the origin type, and add the predicate accordingly
|
||||
// e.g. "origin" = "broker", check broker aliases; "origin" = "any" no need to filter anything; "origin" = "fed-provider", check "fed-providers", etc
|
||||
if (!this.getBrokerAliases().isEmpty()) {
|
||||
Subquery<Integer> subquery = query.subquery(Integer.class);
|
||||
Root<FederatedIdentityEntity> from = subquery.from(FederatedIdentityEntity.class);
|
||||
|
||||
subquery.select(cb.literal(1));
|
||||
subquery.where(
|
||||
cb.and(
|
||||
cb.equal(from.get("user").get("id"), userRoot.get("id")),
|
||||
from.get("identityProvider").in(getBrokerAliases())
|
||||
)
|
||||
);
|
||||
return cb.exists(subquery);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the specified resource is in the scope of this policy. For example, a policy associated with a
|
||||
* broker is applicable only to users with a federated identity associated with the same broker.
|
||||
@ -172,6 +173,6 @@ public abstract class AbstractUserResourcePolicyProvider implements ResourcePoli
|
||||
}
|
||||
|
||||
protected List<String> getBrokerAliases() {
|
||||
return getModel().getConfig().getOrDefault("broker-aliases", List.of());
|
||||
return getModel().getConfig().getOrDefault(BROKER_ALIASES, List.of());
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,6 +89,20 @@ public class JpaResourcePolicyStateProvider implements ResourcePolicyStateProvid
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ScheduledAction> getScheduledActionsByPolicy(ResourcePolicy policy) {
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaQuery<ResourcePolicyStateEntity> query = cb.createQuery(ResourcePolicyStateEntity.class);
|
||||
Root<ResourcePolicyStateEntity> stateRoot = query.from(ResourcePolicyStateEntity.class);
|
||||
|
||||
Predicate byPolicy = cb.equal(stateRoot.get("policyId"), policy.getId());
|
||||
query.where(byPolicy);
|
||||
|
||||
return em.createQuery(query).getResultStream()
|
||||
.map(s -> new ScheduledAction(s.getPolicyId(), s.getScheduledActionId(), s.getResourceId()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ScheduledAction> getScheduledActionsByResource(String resourceId) {
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
@ -103,21 +117,6 @@ public class JpaResourcePolicyStateProvider implements ResourcePolicyStateProvid
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> findResourceIdsByScheduledAction(String policyId, String scheduledActionId) {
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaQuery<String> query = cb.createQuery(String.class);
|
||||
Root<ResourcePolicyStateEntity> stateRoot = query.from(ResourcePolicyStateEntity.class);
|
||||
|
||||
Predicate policyPredicate = cb.equal(stateRoot.get("policyId"), policyId);
|
||||
Predicate actionPredicate = cb.equal(stateRoot.get("scheduledActionId"), scheduledActionId);
|
||||
|
||||
query.select(stateRoot.get("resourceId"));
|
||||
query.where(cb.and(policyPredicate, actionPredicate));
|
||||
|
||||
return em.createQuery(query).getResultList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(String policyId, String policyProviderId, List<String> resourceIds, String newLastCompletedActionId) {
|
||||
for (String resourceId : resourceIds) {
|
||||
|
||||
@ -17,15 +17,8 @@
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.Expression;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.jpa.entities.UserEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -37,13 +30,6 @@ public class UserCreationTimeResourcePolicyProvider extends AbstractUserResource
|
||||
super(session, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Predicate timePredicate(long time, CriteriaBuilder cb, CriteriaQuery<String> query, Root<UserEntity> userRoot) {
|
||||
long currentTimeMillis = Time.currentTimeMillis();
|
||||
Expression<Long> timeMoment = cb.sum(userRoot.get("createdTimestamp"), cb.literal(time));
|
||||
return cb.lessThan(timeMoment, cb.literal(currentTimeMillis));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ResourceOperationType> getSupportedOperationsForScheduling() {
|
||||
return List.of(CREATE);
|
||||
|
||||
@ -17,19 +17,10 @@
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.Expression;
|
||||
import jakarta.persistence.criteria.Path;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.jpa.entities.UserEntity;
|
||||
|
||||
import static org.keycloak.models.policy.ResourceOperationType.CREATE;
|
||||
import static org.keycloak.models.policy.ResourceOperationType.LOGIN;
|
||||
@ -40,14 +31,6 @@ public class UserSessionRefreshTimeResourcePolicyProvider extends AbstractUserRe
|
||||
super(session, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Predicate timePredicate(long time, CriteriaBuilder cb, CriteriaQuery<String> query, Root<UserEntity> userRoot) {
|
||||
long currentTimeSeconds = Time.currentTime();
|
||||
Path<Long> lastSessionRefreshTime = userRoot.get("lastSessionRefreshTime");
|
||||
Expression<Long> lastSessionRefreshTimeExpiration = cb.sum(lastSessionRefreshTime, cb.literal(Duration.ofMillis(time).toSeconds()));
|
||||
return cb.and(cb.isNotNull(lastSessionRefreshTime), cb.lessThan(lastSessionRefreshTimeExpiration, cb.literal(currentTimeSeconds)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ResourceOperationType> getSupportedOperationsForScheduling() {
|
||||
return List.of(CREATE, LOGIN);
|
||||
|
||||
@ -26,11 +26,6 @@ import java.util.Set;
|
||||
*/
|
||||
public interface ResourcePolicyStateProvider extends Provider {
|
||||
|
||||
/**
|
||||
* Finds resource IDs scheduled to run the specified action within a policy.
|
||||
*/
|
||||
List<String> findResourceIdsByScheduledAction(String policyId, String scheduledActionId);
|
||||
|
||||
/**
|
||||
* Updates the state for a list of resources that have just completed a new action.
|
||||
* This will perform an update for existing states or an insert for new states.
|
||||
@ -71,6 +66,8 @@ public interface ResourcePolicyStateProvider extends Provider {
|
||||
|
||||
List<ScheduledAction> getScheduledActionsByResource(String resourceId);
|
||||
|
||||
List<ScheduledAction> getScheduledActionsByPolicy(ResourcePolicy policy);
|
||||
|
||||
List<ScheduledAction> getDueScheduledActions(ResourcePolicy policy);
|
||||
|
||||
record ScheduledAction (String policyId, String actionId, String resourceId) {};
|
||||
|
||||
@ -25,15 +25,9 @@ public interface ResourcePolicyProvider extends Provider {
|
||||
/**
|
||||
* Finds all resources that are eligible for the first action of a policy.
|
||||
*
|
||||
* @param time The time delay for the first action.
|
||||
* @return A list of eligible resource IDs.
|
||||
*/
|
||||
List<String> getEligibleResourcesForInitialAction(long time);
|
||||
|
||||
/**
|
||||
* This method checks a list of candidates and returns only those that are eligible based on time.
|
||||
*/
|
||||
List<String> filterEligibleResources(List<String> candidateResourceIds, long time);
|
||||
List<String> getEligibleResourcesForInitialAction();
|
||||
|
||||
/**
|
||||
* Checks if the provider supports resources of the specified type.
|
||||
|
||||
@ -19,8 +19,6 @@ package org.keycloak.models.policy;
|
||||
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@ -36,12 +34,13 @@ import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.policy.ResourcePolicyStateProvider.ScheduledAction;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
public class ResourcePolicyManager {
|
||||
|
||||
private static final Logger log = Logger.getLogger(ResourcePolicyManager.class);
|
||||
|
||||
private ResourcePolicyStateProvider policyStateProvider;
|
||||
|
||||
public static boolean isFeatureEnabled() {
|
||||
return Profile.isFeatureEnabled(Feature.RESOURCE_LIFECYCLE);
|
||||
}
|
||||
@ -50,6 +49,7 @@ public class ResourcePolicyManager {
|
||||
|
||||
public ResourcePolicyManager(KeycloakSession session) {
|
||||
this.session = session;
|
||||
this.policyStateProvider = session.getKeycloakSessionFactory().getProviderFactory(ResourcePolicyStateProvider.class).create(session);
|
||||
}
|
||||
|
||||
public ResourcePolicy addPolicy(String providerId) {
|
||||
@ -108,11 +108,10 @@ public class ResourcePolicyManager {
|
||||
// find which action IDs were deleted
|
||||
oldActionIds.removeAll(newActionIds); // The remaining IDs are the deleted ones
|
||||
|
||||
ResourcePolicyStateProvider stateProvider = getResourcePolicyStateProvider();
|
||||
// delete orphaned state records - this means that we actually reset the flow for users which completed the action which is being removed
|
||||
// it seems like the best way to handle this
|
||||
if (!oldActionIds.isEmpty()) {
|
||||
stateProvider.removeByCompletedActions(policy.getId(), oldActionIds);
|
||||
policyStateProvider.removeByCompletedActions(policy.getId(), oldActionIds);
|
||||
}
|
||||
|
||||
RealmModel realm = getRealm();
|
||||
@ -157,80 +156,8 @@ public class ResourcePolicyManager {
|
||||
.map(ResourceAction::new).sorted().toList();
|
||||
}
|
||||
|
||||
public void runPolicies() {
|
||||
List<ResourcePolicy> policies = getPolicies();
|
||||
|
||||
for (ResourcePolicy policy : policies) {
|
||||
runPolicy(policy);
|
||||
}
|
||||
}
|
||||
|
||||
private void runPolicy(ResourcePolicy policy) {
|
||||
log.tracev("Running policy {0}", policy.getProviderId());
|
||||
|
||||
// no actions -> skip
|
||||
List<ResourceAction> actions = getActions(policy);
|
||||
if (actions.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ResourcePolicyProvider policyProvider = getPolicyProvider(policy);
|
||||
ResourcePolicyStateProvider stateProvider = getResourcePolicyStateProvider();
|
||||
|
||||
// fetch all candidate lists for subsequent actions
|
||||
// need to load all candidates before creation a state record for initial action
|
||||
// if we don't do this, we risk executing more actions for single resource (user) in one run (in case the actions were modified by admin)
|
||||
Map<String, List<String>> candidatesForAction = new HashMap<>();
|
||||
for (int i = 1; i < actions.size(); i++) {
|
||||
ResourceAction previousAction = actions.get(i - 1);
|
||||
List<String> candidateIds = stateProvider.findResourceIdsByScheduledAction(policy.getId(), previousAction.getId());
|
||||
candidatesForAction.put(actions.get(i).getId(), candidateIds);
|
||||
}
|
||||
|
||||
// Process the Initial action (State Zero) - look for eligable users NOT present in the state table.
|
||||
ResourceAction initialAction = actions.get(0);
|
||||
ResourceActionProvider actionProvider = getActionProvider(initialAction);
|
||||
log.tracev("Initial action {0}", initialAction.getProviderId());
|
||||
|
||||
List<String> newResourceIds = policyProvider.getEligibleResourcesForInitialAction(initialAction.getAfter());
|
||||
log.tracev("Eligable resource IDs for initial action {0}", newResourceIds);
|
||||
// <comment> todo: do we want to wrap it into separate tx? So we have more granular approach for handling errors & possible retries??
|
||||
if (!newResourceIds.isEmpty()) {
|
||||
// run action
|
||||
runAction(actionProvider, newResourceIds);
|
||||
|
||||
// create state record
|
||||
stateProvider.update(policy.getId(), policy.getProviderId(), newResourceIds, initialAction.getId());
|
||||
}
|
||||
// </comment>
|
||||
|
||||
// Process the rest of the actions
|
||||
for (ResourceAction action : actions) {
|
||||
// Find all resources that have completed the PREVIOUS action.
|
||||
List<String> candidateIds = candidatesForAction.getOrDefault(action.getId(), Collections.emptyList());
|
||||
|
||||
if (candidateIds.isEmpty()) {
|
||||
continue; // No users are at this stage yet.
|
||||
}
|
||||
|
||||
// Ask the policyProvider to filter these candidates based on time.
|
||||
List<String> eligibleIds = policyProvider.filterEligibleResources(candidateIds, action.getAfter());
|
||||
|
||||
// <comment> todo: do we want to wrap it into separate tx? So we have more granular approach for handling errors & possible retries??
|
||||
if (!eligibleIds.isEmpty()) {
|
||||
// Get the action provider and run the action on the eligible users.
|
||||
actionProvider = getActionProvider(action);
|
||||
runAction(actionProvider, eligibleIds);
|
||||
|
||||
// Update the state for the users that were processed.
|
||||
stateProvider.update(policy.getId(), policy.getProviderId(), eligibleIds, action.getId());
|
||||
}
|
||||
// </comment>
|
||||
}
|
||||
}
|
||||
|
||||
private void runAction(ResourceActionProvider actionProvider, List<String> newResourceIds) {
|
||||
actionProvider.run(newResourceIds == null ? List.of() : newResourceIds);
|
||||
private ResourceAction getFirstAction(ResourcePolicy policy) {
|
||||
return getActions(policy).get(0);
|
||||
}
|
||||
|
||||
private ResourcePolicyProvider getPolicyProvider(ResourcePolicy policy) {
|
||||
@ -245,11 +172,6 @@ public class ResourcePolicyManager {
|
||||
return (ResourceActionProvider) actionFactory.create(session, getRealm().getComponent(action.getId()));
|
||||
}
|
||||
|
||||
private ResourcePolicyStateProvider getResourcePolicyStateProvider() {
|
||||
ProviderFactory<ResourcePolicyStateProvider> providerFactory = session.getKeycloakSessionFactory().getProviderFactory(ResourcePolicyStateProvider.class);
|
||||
return providerFactory.create(session);
|
||||
}
|
||||
|
||||
private RealmModel getRealm() {
|
||||
return session.getContext().getRealm();
|
||||
}
|
||||
@ -296,10 +218,18 @@ public class ResourcePolicyManager {
|
||||
});
|
||||
}
|
||||
|
||||
public void scheduleAllEligibleResources(ResourcePolicy policy) {
|
||||
ResourcePolicyProvider provider = getPolicyProvider(policy);
|
||||
ResourceAction firstAction = getFirstAction(policy);
|
||||
provider.getEligibleResourcesForInitialAction().forEach(resourceId -> {
|
||||
// TODO run each scheduling task in a separate tx as other txs might schedule an action while this is running.
|
||||
this.policyStateProvider.scheduleAction(policy, firstAction, resourceId);
|
||||
});
|
||||
}
|
||||
|
||||
public void processEvent(ResourcePolicyEvent event) {
|
||||
|
||||
ResourcePolicyStateProvider state = getResourcePolicyStateProvider();
|
||||
List<String> currentlyAssignedPolicies = state.getScheduledActionsByResource(event.getResourceId())
|
||||
List<String> currentlyAssignedPolicies = policyStateProvider.getScheduledActionsByResource(event.getResourceId())
|
||||
.stream().map(ScheduledAction::policyId).toList();
|
||||
List<ResourcePolicy> policies = this.getPolicies();
|
||||
|
||||
@ -311,40 +241,35 @@ public class ResourcePolicyManager {
|
||||
if (!currentlyAssignedPolicies.contains(policy.getId())) {
|
||||
// if policy is not assigned, check if the provider allows assigning based on the event
|
||||
if (provider.scheduleOnEvent(event)) {
|
||||
state.scheduleAction(policy, getFirstAction(policy), event.getResourceId());
|
||||
policyStateProvider.scheduleAction(policy, getFirstAction(policy), event.getResourceId());
|
||||
}
|
||||
} else {
|
||||
if (provider.resetOnEvent(event)) {
|
||||
state.scheduleAction(policy, getFirstAction(policy), event.getResourceId());
|
||||
policyStateProvider.scheduleAction(policy, getFirstAction(policy), event.getResourceId());
|
||||
}
|
||||
// TODO add a removeOnEvent to allow policies to detach from resources on specific events (e.g. unlinking an identity)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ResourceAction getFirstAction(ResourcePolicy policy) {
|
||||
return getActions(policy).get(0);
|
||||
}
|
||||
|
||||
public void runScheduledTasks() {
|
||||
for (ResourcePolicy policy : getPolicies()) {
|
||||
ResourcePolicyStateProvider state = getResourcePolicyStateProvider();
|
||||
|
||||
for (ScheduledAction scheduled : state.getDueScheduledActions(policy)) {
|
||||
for (ScheduledAction scheduled : policyStateProvider.getDueScheduledActions(policy)) {
|
||||
List<ResourceAction> actions = getActions(policy);
|
||||
|
||||
for (int i = 0; i < actions.size(); i++) {
|
||||
ResourceAction currentAction = actions.get(i);
|
||||
|
||||
if (currentAction.getId().equals(scheduled.actionId())) {
|
||||
runAction(getActionProvider(currentAction), List.of(scheduled.resourceId()));
|
||||
getActionProvider(currentAction).run(List.of(scheduled.resourceId()));
|
||||
|
||||
if (actions.size() > i + 1) {
|
||||
// schedule the next action using the time offset difference between the actions.
|
||||
ResourceAction nextAction = actions.get(i + 1);
|
||||
state.scheduleAction(policy, nextAction,nextAction.getAfter() - currentAction.getAfter(), scheduled.resourceId());
|
||||
policyStateProvider.scheduleAction(policy, nextAction,nextAction.getAfter() - currentAction.getAfter(), scheduled.resourceId());
|
||||
} else {
|
||||
state.remove(policy.getId(), scheduled.resourceId());
|
||||
policyStateProvider.remove(policy.getId(), scheduled.resourceId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,22 +18,19 @@
|
||||
package org.keycloak.tests.admin.model.policy;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.connections.jpa.JpaConnectionProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
@ -42,7 +39,6 @@ import org.keycloak.models.policy.NotifyUserActionProviderFactory;
|
||||
import org.keycloak.models.policy.ResourceAction;
|
||||
import org.keycloak.models.policy.ResourcePolicy;
|
||||
import org.keycloak.models.policy.ResourcePolicyManager;
|
||||
import org.keycloak.models.policy.ResourcePolicyStateEntity;
|
||||
import org.keycloak.models.policy.ResourcePolicyStateProvider;
|
||||
import org.keycloak.models.policy.UserActionBuilder;
|
||||
import org.keycloak.models.policy.UserCreationTimeResourcePolicyProviderFactory;
|
||||
@ -54,6 +50,7 @@ import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.realm.ManagedUser;
|
||||
import org.keycloak.testframework.realm.UserConfig;
|
||||
import org.keycloak.testframework.realm.UserConfigBuilder;
|
||||
import org.keycloak.testframework.remote.providers.runonserver.RunOnServer;
|
||||
import org.keycloak.testframework.remote.runonserver.InjectRunOnServer;
|
||||
import org.keycloak.testframework.remote.runonserver.RunOnServerClient;
|
||||
|
||||
@ -130,82 +127,6 @@ public class ResourcePolicyManagementTest {
|
||||
// em.flush();
|
||||
// }
|
||||
|
||||
runOnServer.run(session -> {
|
||||
EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
||||
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
ResourcePolicyStateProvider stateProvider = session.getProvider(ResourcePolicyStateProvider.class);
|
||||
UserModel user = session.users().addUser(realm, "test");
|
||||
|
||||
// Create a policy with two actions
|
||||
ResourcePolicy policy = manager.addPolicy(new ResourcePolicy(UserCreationTimeResourcePolicyProviderFactory.ID));
|
||||
ResourceAction notify = UserActionBuilder.builder(NotifyUserActionProviderFactory.ID).after(Duration.ofDays(5)).build();
|
||||
ResourceAction disable = UserActionBuilder.builder(DisableUserActionProviderFactory.ID).after(Duration.ofDays(10)).build();
|
||||
manager.updateActions(policy, List.of(notify, disable));
|
||||
|
||||
// Get the created actions to access their IDs
|
||||
List<ResourceAction> createdActions = manager.getActions(policy);
|
||||
ResourceAction createdNotifyAction = createdActions.get(0);
|
||||
ResourceAction createdDisableAction = createdActions.get(1);
|
||||
|
||||
// --- SIMULATE USER PROGRESS ---
|
||||
// Manually set the user's state to have completed 'notify'
|
||||
stateProvider.update(policy.getId(), policy.getProviderId(), List.of(user.getId()), createdNotifyAction.getId());
|
||||
ResourcePolicyStateEntity.PrimaryKey pk = new ResourcePolicyStateEntity.PrimaryKey(user.getId(), policy.getId());
|
||||
assertNotNull(em.find(ResourcePolicyStateEntity.class, pk), "State should exist before the update.");
|
||||
|
||||
// Admin deletes 'notify' by updating the policy with only 'disable'
|
||||
manager.updateActions(policy, List.of(createdDisableAction));
|
||||
|
||||
//need to flush and clear the persistance context cache to get correct result in next call
|
||||
em.flush();
|
||||
em.clear();
|
||||
|
||||
// The user's state record should have been deleted because its last_completed_action (notify) no longer exists.
|
||||
assertNull(em.find(ResourcePolicyStateEntity.class, pk), "State record should be deleted when its completed action is removed.");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPolicyDoesNotFallThroughActionsInSingleRun() {
|
||||
runOnServer.run(session -> {
|
||||
EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
||||
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
UserModel user = session.users().addUser(realm, "testuser");
|
||||
user.setEnabled(true);
|
||||
|
||||
// Create a policy with notify (5 days) and disable (10 days) actions
|
||||
ResourcePolicy policy = manager.addPolicy(new ResourcePolicy(UserCreationTimeResourcePolicyProviderFactory.ID));
|
||||
ResourceAction notifyAction = UserActionBuilder.builder(NotifyUserActionProviderFactory.ID).after(Duration.ofDays(5)).build();
|
||||
ResourceAction disableAction = UserActionBuilder.builder(DisableUserActionProviderFactory.ID).after(Duration.ofDays(10)).build();
|
||||
manager.updateActions(policy, List.of(notifyAction, disableAction));
|
||||
|
||||
ResourceAction createdNotifyAction = manager.getActions(policy).get(0);
|
||||
|
||||
try {
|
||||
// Simulate the user being 12 days old, making them eligible for both actions' time conditions.
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(12).toSeconds()));
|
||||
manager.runPolicies();
|
||||
|
||||
user = session.users().getUserById(realm, user.getId());
|
||||
|
||||
// Verify that ONLY the first action (notify) was executed.
|
||||
assertNotNull(user.getAttributes().get("message"), "The first action (notify) should have run.");
|
||||
assertTrue(user.isEnabled(), "The second action (disable) should NOT have run.");
|
||||
|
||||
// Verify that the user's state is correctly paused after the first action.
|
||||
ResourcePolicyStateEntity.PrimaryKey pk = new ResourcePolicyStateEntity.PrimaryKey(user.getId(), policy.getId());
|
||||
ResourcePolicyStateEntity state = em.find(ResourcePolicyStateEntity.class, pk);
|
||||
|
||||
assertNotNull(state, "A state record should have been created for the user.");
|
||||
assertEquals(createdNotifyAction.getId(), state.getScheduledActionId(), "The user's state should be at the first action.");
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -230,6 +151,173 @@ public class ResourcePolicyManagementTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPolicyDoesNotFallThroughActionsInSingleRun() {
|
||||
// register policy to notify user in 5 days and disable in 10 days
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
configureSessionContext(session);
|
||||
PolicyBuilder.create()
|
||||
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
|
||||
.withActions(
|
||||
UserActionBuilder.builder(NotifyUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build(),
|
||||
UserActionBuilder.builder(DisableUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(10))
|
||||
.build()
|
||||
).build(session);
|
||||
});
|
||||
|
||||
// create a new user - should bind the user to the policy and setup the first action
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").build());
|
||||
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
UserModel user = session.users().getUserByUsername(realm,"testuser");
|
||||
|
||||
List<ResourcePolicy> registeredPolicies = manager.getPolicies();
|
||||
assertEquals(1, registeredPolicies.size());
|
||||
|
||||
ResourcePolicy policy = registeredPolicies.get(0);
|
||||
assertEquals(2, manager.getActions(policy).size());
|
||||
ResourceAction notifyAction = manager.getActions(policy).get(0);
|
||||
|
||||
ResourcePolicyStateProvider stateProvider = session.getKeycloakSessionFactory().getProviderFactory(ResourcePolicyStateProvider.class).create(session);
|
||||
ResourcePolicyStateProvider.ScheduledAction scheduledAction = stateProvider.getScheduledAction(policy.getId(), user.getId());
|
||||
assertNotNull(scheduledAction, "An action should have been scheduled for the user " + user.getUsername());
|
||||
assertEquals(notifyAction.getId(), scheduledAction.actionId());
|
||||
|
||||
try {
|
||||
// Simulate the user being 12 days old, making them eligible for both actions' time conditions.
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(12).toSeconds()));
|
||||
manager.runScheduledTasks();
|
||||
|
||||
user = session.users().getUserById(realm, user.getId());
|
||||
// Verify that ONLY the first action (notify) was executed.
|
||||
assertNotNull(user.getAttributes().get("message"), "The first action (notify) should have run.");
|
||||
assertTrue(user.isEnabled(), "The second action (disable) should NOT have run.");
|
||||
|
||||
// Verify that the next action was scheduled for the user
|
||||
ResourceAction disableAction = manager.getActions(policy).get(1);
|
||||
scheduledAction = stateProvider.getScheduledAction(policy.getId(), user.getId());
|
||||
assertNotNull(scheduledAction, "An action should have been scheduled for the user " + user.getUsername());
|
||||
assertEquals(disableAction.getId(), scheduledAction.actionId(), "The second action should have been scheduled");
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAssignPolicyToExistingResources() {
|
||||
// create some realm users
|
||||
for (int i = 0; i < 10; i++) {
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("user-" + i).build());
|
||||
}
|
||||
|
||||
// create some users associated with a federated identity
|
||||
for (int i = 0; i < 10; i++) {
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("idp-user-" + i)
|
||||
.federatedLink("someidp", UUID.randomUUID().toString(), "idp-user-" + i).build());
|
||||
}
|
||||
|
||||
// register a policy to notify user in 5 days
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
configureSessionContext(session);
|
||||
PolicyBuilder.create()
|
||||
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
|
||||
.withConfig("broker-aliases", "someidp")
|
||||
.withActions(
|
||||
UserActionBuilder.builder(NotifyUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build(),
|
||||
UserActionBuilder.builder(DisableUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(10))
|
||||
.build()
|
||||
).build(session);
|
||||
});
|
||||
|
||||
// now with the policy in place, let's create a couple more idp users - these will be attached to the policy on
|
||||
// creation.
|
||||
for (int i = 0; i < 3; i++) {
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("new-idp-user-" + i)
|
||||
.federatedLink("someidp", UUID.randomUUID().toString(), "new-idp-user-" + i).build());
|
||||
}
|
||||
|
||||
// new realm users created after the policy - these should not be attached to the policy because they are not idp users.
|
||||
for (int i = 0; i < 3; i++) {
|
||||
managedRealm.admin().users().create(UserConfigBuilder.create().username("new-user-" + i).build());
|
||||
}
|
||||
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager policyManager = new ResourcePolicyManager(session);
|
||||
List<ResourcePolicy> registeredPolicies = policyManager.getPolicies();
|
||||
assertEquals(1, registeredPolicies.size());
|
||||
ResourcePolicy policy = registeredPolicies.get(0);
|
||||
|
||||
assertEquals(2, policyManager.getActions(policy).size());
|
||||
ResourceAction notifyAction = policyManager.getActions(policy).get(0);
|
||||
|
||||
// check no policies are yet attached to the previous users, only to the ones created after the policy was in place
|
||||
ResourcePolicyStateProvider stateProvider = session.getKeycloakSessionFactory().getProviderFactory(ResourcePolicyStateProvider.class).create(session);
|
||||
List<ResourcePolicyStateProvider.ScheduledAction> scheduledActions = stateProvider.getScheduledActionsByPolicy(policy);
|
||||
assertEquals(3, scheduledActions.size());
|
||||
scheduledActions.forEach(scheduledAction -> {
|
||||
assertEquals(notifyAction.getId(), scheduledAction.actionId());
|
||||
UserModel user = session.users().getUserById(realm, scheduledAction.resourceId());
|
||||
assertNotNull(user);
|
||||
assertTrue(user.getUsername().startsWith("new-idp-user-"));
|
||||
});
|
||||
|
||||
try {
|
||||
// let's run the schedule actions for the new users so they transition to the next one.
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds()));
|
||||
policyManager.runScheduledTasks();
|
||||
|
||||
// check the same users are now scheduled to run the second action.
|
||||
ResourceAction disableAction = policyManager.getActions(policy).get(1);
|
||||
scheduledActions = stateProvider.getScheduledActionsByPolicy(policy);
|
||||
assertEquals(3, scheduledActions.size());
|
||||
scheduledActions.forEach(scheduledAction -> {
|
||||
assertEquals(disableAction.getId(), scheduledAction.actionId());
|
||||
UserModel user = session.users().getUserById(realm, scheduledAction.resourceId());
|
||||
assertNotNull(user);
|
||||
assertTrue(user.getUsername().startsWith("new-idp-user-"));
|
||||
});
|
||||
|
||||
// assign the policy to the eligible users - i.e. only users from the same idp who are not yet assigned to the policy.
|
||||
policyManager.scheduleAllEligibleResources(policy);
|
||||
|
||||
// check policy was correctly assigned to the old users, not affecting users already associated with the policy.
|
||||
scheduledActions = stateProvider.getScheduledActionsByPolicy(policy);
|
||||
assertEquals(13, scheduledActions.size());
|
||||
|
||||
List<ResourcePolicyStateProvider.ScheduledAction> scheduledToNotify = scheduledActions.stream()
|
||||
.filter(action -> notifyAction.getId().equals(action.actionId())).toList();
|
||||
assertEquals(10, scheduledToNotify.size());
|
||||
scheduledToNotify.forEach(scheduledAction -> {
|
||||
UserModel user = session.users().getUserById(realm, scheduledAction.resourceId());
|
||||
assertNotNull(user);
|
||||
assertTrue(user.getUsername().startsWith("idp-user-"));
|
||||
});
|
||||
|
||||
List<ResourcePolicyStateProvider.ScheduledAction> scheduledToDisable = scheduledActions.stream()
|
||||
.filter(action -> disableAction.getId().equals(action.actionId())).toList();
|
||||
assertEquals(3, scheduledToDisable.size());
|
||||
scheduledToDisable.forEach(scheduledAction -> {
|
||||
UserModel user = session.users().getUserById(realm, scheduledAction.resourceId());
|
||||
assertNotNull(user);
|
||||
assertTrue(user.getUsername().startsWith("new-idp-user-"));
|
||||
});
|
||||
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static RealmModel configureSessionContext(KeycloakSession session) {
|
||||
RealmModel realm = session.realms().getRealmByName(REALM_NAME);
|
||||
session.getContext().setRealm(realm);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user