From d0e83cc05ef7208528cf32927e2ca4d12e8669da Mon Sep 17 00:00:00 2001 From: vramik Date: Wed, 10 Sep 2025 16:55:45 +0200 Subject: [PATCH] Rename RLM to Workflows Closes #42512 Signed-off-by: vramik --- .../java/org/keycloak/common/Profile.java | 2 +- .../WorkflowConditionRepresentation.java} | 18 +- .../WorkflowRepresentation.java} | 64 +- .../WorkflowStepRepresentation.java} | 50 +- .../admin/client/resource/RealmResource.java | 4 +- .../resource/RealmResourcesResource.java | 9 - ...ourcePolicy.java => WorkflowResource.java} | 8 +- ...cePolicies.java => WorkflowsResource.java} | 16 +- .../models/cache/infinispan/UserAdapter.java | 1 - .../JpaResourcePolicyStateProvider.java | 181 ---- .../AbstractUserWorkflowProvider.java} | 18 +- .../EventBasedWorkflowProvider.java} | 28 +- .../EventBasedWorkflowProviderFactory.java} | 10 +- .../workflow/JpaWorkflowStateProvider.java | 180 ++++ .../JpaWorkflowStateProviderFactory.java} | 12 +- .../UserCreationTimeWorkflowProvider.java} | 12 +- ...rCreationTimeWorkflowProviderFactory.java} | 10 +- ...erSessionRefreshTimeWorkflowProvider.java} | 14 +- ...onRefreshTimeWorkflowProviderFactory.java} | 10 +- .../WorkflowStateEntity.java} | 82 +- ...upMembershipWorkflowConditionFactory.java} | 10 +- ...pMembershipWorkflowConditionProvider.java} | 18 +- ...tityProviderWorkflowConditionFactory.java} | 10 +- ...ityProviderWorkflowConditionProvider.java} | 18 +- .../RoleWorkflowConditionFactory.java} | 10 +- .../RoleWorkflowConditionProvider.java} | 20 +- ...serAttributeWorkflowConditionFactory.java} | 10 +- ...erAttributeWorkflowConditionProvider.java} | 14 +- .../META-INF/jpa-changelog-26.4.0.xml | 32 +- ...workflow.WorkflowConditionProviderFactory} | 8 +- ...k.models.workflow.WorkflowProviderFactory} | 7 +- ...els.workflow.WorkflowStateProviderFactory} | 2 +- .../main/resources/default-persistence.xml | 4 +- .../WorkflowStateProvider.java} | 36 +- .../WorkflowStateProviderFactory.java} | 6 +- .../WorkflowStateSpi.java} | 10 +- .../services/org.keycloak.provider.Spi | 2 +- .../ResourcePolicyInvalidStateException.java | 10 - .../ResourceOperationType.java | 2 +- .../{policy => workflow}/ResourceType.java | 16 +- .../Workflow.java} | 12 +- .../WorkflowConditionProvider.java} | 8 +- .../WorkflowConditionProviderFactory.java} | 6 +- .../WorkflowConditionSpi.java} | 10 +- .../WorkflowEvent.java} | 6 +- .../WorkflowInvalidStateException.java | 10 + .../WorkflowProvider.java} | 46 +- .../WorkflowProviderFactory.java} | 6 +- .../WorkflowSpi.java} | 10 +- .../WorkflowStep.java} | 33 +- .../WorkflowStepProvider.java} | 5 +- .../WorkflowStepProviderFactory.java} | 6 +- .../WorkflowStepSpi.java} | 10 +- .../services/org.keycloak.provider.Spi | 6 +- .../policy/AdhocResourcePolicyEvent.java | 8 - .../policy/AggregatedActionProvider.java | 47 - ...esourcePolicyActionRunnerSuccessEvent.java | 17 - .../models/policy/ResourcePolicyManager.java | 369 -------- .../models/policy/UserActionBuilder.java | 48 - .../AddRequiredActionStepProvider.java} | 26 +- ...AddRequiredActionStepProviderFactory.java} | 8 +- .../workflow/AdhocResourcePolicyEvent.java | 8 + .../workflow/AggregatedStepProvider.java | 42 + .../AggregatedStepProviderFactory.java} | 10 +- .../DeleteUserStepProvider.java} | 21 +- .../DeleteUserStepProviderFactory.java} | 10 +- .../DisableUserStepProvider.java} | 20 +- .../DisableUserStepProviderFactory.java} | 10 +- .../NotifyUserStepProvider.java} | 110 ++- .../NotifyUserStepProviderFactory.java} | 12 +- .../SetUserAttributeStepProvider.java} | 31 +- .../SetUserAttributeStepProviderFactory.java} | 12 +- .../WorkflowEventListener.java} | 18 +- .../WorkflowRunnerScheduledTask.java} | 20 +- .../WorkflowStepRunnerSuccessEvent.java | 7 + .../WorkflowsEventListenerFactory.java} | 24 +- .../models/workflow/WorkflowsManager.java | 365 ++++++++ .../RealmResourcePoliciesResource.java | 61 -- .../resource/RealmResourcesResource.java | 24 - .../resources/admin/RealmAdminResource.java | 8 +- .../admin/resource/WorkflowResource.java} | 36 +- .../admin/resource/WorkflowsResource.java | 66 ++ ...ycloak.events.EventListenerProviderFactory | 2 +- ...dels.workflow.WorkflowStepProviderFactory} | 13 +- .../model/policy/AddRequiredActionTest.java | 64 -- .../policy/RLMScheduledTaskServerConfig.java | 13 - .../policy/ResourcePolicyManagementTest.java | 871 ------------------ .../model/workflow/AddRequiredActionTest.java | 52 ++ .../AdhocWorkflowTest.java} | 118 +-- .../AggregatedStepTest.java} | 82 +- ...edUserSessionRefreshTimeWorkflowTest.java} | 136 +-- .../GroupMembershipJoinWorkflowTest.java} | 89 +- .../RoleWorkflowConditionTest.java} | 62 +- .../StepRunnerScheduledTaskTest.java} | 44 +- .../UserAttributeWorkflowConditionTest.java} | 62 +- .../UserCreationTimeWorkflowTest.java} | 60 +- .../UserSessionRefreshTimeWorkflowTest.java} | 108 +-- .../workflow/WorkflowManagementTest.java | 852 +++++++++++++++++ .../WorkflowsScheduledTaskServerConfig.java | 13 + .../WorkflowsServerConfig.java} | 6 +- ...fication.ftl => workflow-notification.ftl} | 0 ...fication.ftl => workflow-notification.ftl} | 0 102 files changed, 2529 insertions(+), 2704 deletions(-) rename core/src/main/java/org/keycloak/representations/{resources/policies/ResourcePolicyConditionRepresentation.java => workflows/WorkflowConditionRepresentation.java} (73%) rename core/src/main/java/org/keycloak/representations/{resources/policies/ResourcePolicyRepresentation.java => workflows/WorkflowRepresentation.java} (59%) rename core/src/main/java/org/keycloak/representations/{resources/policies/ResourcePolicyActionRepresentation.java => workflows/WorkflowStepRepresentation.java} (56%) delete mode 100644 integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResourcesResource.java rename integration/admin-client/src/main/java/org/keycloak/admin/client/resource/{RealmResourcePolicy.java => WorkflowResource.java} (79%) rename integration/admin-client/src/main/java/org/keycloak/admin/client/resource/{RealmResourcePolicies.java => WorkflowsResource.java} (57%) delete mode 100644 model/jpa/src/main/java/org/keycloak/models/policy/JpaResourcePolicyStateProvider.java rename model/jpa/src/main/java/org/keycloak/models/{policy/AbstractUserResourcePolicyProvider.java => workflow/AbstractUserWorkflowProvider.java} (82%) rename model/jpa/src/main/java/org/keycloak/models/{policy/EventBasedResourcePolicyProvider.java => workflow/EventBasedWorkflowProvider.java} (72%) rename model/jpa/src/main/java/org/keycloak/models/{policy/EventBasedResourcePolicyProviderFactory.java => workflow/EventBasedWorkflowProviderFactory.java} (66%) create mode 100644 model/jpa/src/main/java/org/keycloak/models/workflow/JpaWorkflowStateProvider.java rename model/jpa/src/main/java/org/keycloak/models/{policy/JpaResourcePolicyStateProviderFactory.java => workflow/JpaWorkflowStateProviderFactory.java} (79%) rename model/jpa/src/main/java/org/keycloak/models/{policy/UserCreationTimeResourcePolicyProvider.java => workflow/UserCreationTimeWorkflowProvider.java} (69%) rename model/jpa/src/main/java/org/keycloak/models/{policy/UserSessionRefreshTimeResourcePolicyProviderFactory.java => workflow/UserCreationTimeWorkflowProviderFactory.java} (76%) rename model/jpa/src/main/java/org/keycloak/models/{policy/UserSessionRefreshTimeResourcePolicyProvider.java => workflow/UserSessionRefreshTimeWorkflowProvider.java} (66%) rename model/jpa/src/main/java/org/keycloak/models/{policy/UserCreationTimeResourcePolicyProviderFactory.java => workflow/UserSessionRefreshTimeWorkflowProviderFactory.java} (79%) rename model/jpa/src/main/java/org/keycloak/models/{policy/ResourcePolicyStateEntity.java => workflow/WorkflowStateEntity.java} (57%) rename model/jpa/src/main/java/org/keycloak/models/{policy/conditions/GroupMembershipPolicyConditionFactory.java => workflow/conditions/GroupMembershipWorkflowConditionFactory.java} (54%) rename model/jpa/src/main/java/org/keycloak/models/{policy/conditions/GroupMembershipPolicyConditionProvider.java => workflow/conditions/GroupMembershipWorkflowConditionProvider.java} (64%) rename model/jpa/src/main/java/org/keycloak/models/{policy/conditions/IdentityProviderPolicyConditionFactory.java => workflow/conditions/IdentityProviderWorkflowConditionFactory.java} (53%) rename model/jpa/src/main/java/org/keycloak/models/{policy/conditions/IdentityProviderPolicyConditionProvider.java => workflow/conditions/IdentityProviderWorkflowConditionProvider.java} (75%) rename model/jpa/src/main/java/org/keycloak/models/{policy/conditions/RolePolicyConditionFactory.java => workflow/conditions/RoleWorkflowConditionFactory.java} (55%) rename model/jpa/src/main/java/org/keycloak/models/{policy/conditions/RolePolicyConditionProvider.java => workflow/conditions/RoleWorkflowConditionProvider.java} (74%) rename model/jpa/src/main/java/org/keycloak/models/{policy/conditions/UserAttributePolicyConditionFactory.java => workflow/conditions/UserAttributeWorkflowConditionFactory.java} (52%) rename model/jpa/src/main/java/org/keycloak/models/{policy/conditions/UserAttributePolicyConditionProvider.java => workflow/conditions/UserAttributeWorkflowConditionProvider.java} (73%) rename model/jpa/src/main/resources/META-INF/services/{org.keycloak.models.policy.ResourcePolicyConditionProviderFactory => org.keycloak.models.workflow.WorkflowConditionProviderFactory} (68%) rename model/jpa/src/main/resources/META-INF/services/{org.keycloak.models.policy.ResourcePolicyProviderFactory => org.keycloak.models.workflow.WorkflowProviderFactory} (75%) rename model/jpa/src/main/resources/META-INF/services/{org.keycloak.models.policy.ResourcePolicyStateProviderFactory => org.keycloak.models.workflow.WorkflowStateProviderFactory} (91%) rename model/storage-private/src/main/java/org/keycloak/models/{policy/ResourcePolicyStateProvider.java => workflow/WorkflowStateProvider.java} (51%) rename model/storage-private/src/main/java/org/keycloak/models/{policy/ResourcePolicyStateProviderFactory.java => workflow/WorkflowStateProviderFactory.java} (78%) rename model/storage-private/src/main/java/org/keycloak/models/{policy/ResourcePolicyStateSpi.java => workflow/WorkflowStateSpi.java} (81%) delete mode 100644 server-spi-private/src/main/java/org/keycloak/models/policy/ResourcePolicyInvalidStateException.java rename server-spi-private/src/main/java/org/keycloak/models/{policy => workflow}/ResourceOperationType.java (98%) rename server-spi-private/src/main/java/org/keycloak/models/{policy => workflow}/ResourceType.java (85%) rename server-spi-private/src/main/java/org/keycloak/models/{policy/ResourcePolicy.java => workflow/Workflow.java} (88%) rename server-spi-private/src/main/java/org/keycloak/models/{policy/ResourcePolicyConditionProvider.java => workflow/WorkflowConditionProvider.java} (62%) rename server-spi-private/src/main/java/org/keycloak/models/{policy/ResourcePolicyConditionProviderFactory.java => workflow/WorkflowConditionProviderFactory.java} (69%) rename server-spi-private/src/main/java/org/keycloak/models/{policy/ResourcePolicyConditionSpi.java => workflow/WorkflowConditionSpi.java} (62%) rename server-spi-private/src/main/java/org/keycloak/models/{policy/ResourcePolicyEvent.java => workflow/WorkflowEvent.java} (75%) create mode 100644 server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowInvalidStateException.java rename server-spi-private/src/main/java/org/keycloak/models/{policy/ResourcePolicyProvider.java => workflow/WorkflowProvider.java} (53%) rename server-spi-private/src/main/java/org/keycloak/models/{policy/ResourcePolicyProviderFactory.java => workflow/WorkflowProviderFactory.java} (76%) rename server-spi-private/src/main/java/org/keycloak/models/{policy/ResourceActionSpi.java => workflow/WorkflowSpi.java} (83%) rename server-spi-private/src/main/java/org/keycloak/models/{policy/ResourceAction.java => workflow/WorkflowStep.java} (73%) rename server-spi-private/src/main/java/org/keycloak/models/{policy/ResourceActionProvider.java => workflow/WorkflowStepProvider.java} (86%) rename server-spi-private/src/main/java/org/keycloak/models/{policy/ResourceActionProviderFactory.java => workflow/WorkflowStepProviderFactory.java} (77%) rename server-spi-private/src/main/java/org/keycloak/models/{policy/ResourcePolicySpi.java => workflow/WorkflowStepSpi.java} (82%) delete mode 100644 services/src/main/java/org/keycloak/models/policy/AdhocResourcePolicyEvent.java delete mode 100644 services/src/main/java/org/keycloak/models/policy/AggregatedActionProvider.java delete mode 100644 services/src/main/java/org/keycloak/models/policy/ResourcePolicyActionRunnerSuccessEvent.java delete mode 100644 services/src/main/java/org/keycloak/models/policy/ResourcePolicyManager.java delete mode 100644 services/src/main/java/org/keycloak/models/policy/UserActionBuilder.java rename services/src/main/java/org/keycloak/models/{policy/AddRequiredActionProvider.java => workflow/AddRequiredActionStepProvider.java} (63%) rename services/src/main/java/org/keycloak/models/{policy/AddRequiredActionProviderFactory.java => workflow/AddRequiredActionStepProviderFactory.java} (79%) create mode 100644 services/src/main/java/org/keycloak/models/workflow/AdhocResourcePolicyEvent.java create mode 100644 services/src/main/java/org/keycloak/models/workflow/AggregatedStepProvider.java rename services/src/main/java/org/keycloak/models/{policy/AggregatedActionProviderFactory.java => workflow/AggregatedStepProviderFactory.java} (69%) rename services/src/main/java/org/keycloak/models/{policy/DeleteUserActionProvider.java => workflow/DeleteUserStepProvider.java} (73%) rename services/src/main/java/org/keycloak/models/{policy/DeleteUserActionProviderFactory.java => workflow/DeleteUserStepProviderFactory.java} (80%) rename services/src/main/java/org/keycloak/models/{policy/DisableUserActionProvider.java => workflow/DisableUserStepProvider.java} (73%) rename services/src/main/java/org/keycloak/models/{policy/DisableUserActionProviderFactory.java => workflow/DisableUserStepProviderFactory.java} (80%) rename services/src/main/java/org/keycloak/models/{policy/NotifyUserActionProvider.java => workflow/NotifyUserStepProvider.java} (60%) rename services/src/main/java/org/keycloak/models/{policy/NotifyUserActionProviderFactory.java => workflow/NotifyUserStepProviderFactory.java} (82%) rename services/src/main/java/org/keycloak/models/{policy/SetUserAttributeActionProvider.java => workflow/SetUserAttributeStepProvider.java} (74%) rename services/src/main/java/org/keycloak/models/{policy/SetUserAttributeActionProviderFactory.java => workflow/SetUserAttributeStepProviderFactory.java} (76%) rename services/src/main/java/org/keycloak/models/{policy/ResourcePolicyEventListener.java => workflow/WorkflowEventListener.java} (73%) rename services/src/main/java/org/keycloak/models/{policy/ResourceActionRunnerScheduledTask.java => workflow/WorkflowRunnerScheduledTask.java} (58%) create mode 100644 services/src/main/java/org/keycloak/models/workflow/WorkflowStepRunnerSuccessEvent.java rename services/src/main/java/org/keycloak/models/{policy/ResourcePolicyEventListenerFactory.java => workflow/WorkflowsEventListenerFactory.java} (69%) create mode 100644 services/src/main/java/org/keycloak/models/workflow/WorkflowsManager.java delete mode 100644 services/src/main/java/org/keycloak/realm/resources/policies/admin/resource/RealmResourcePoliciesResource.java delete mode 100644 services/src/main/java/org/keycloak/realm/resources/policies/admin/resource/RealmResourcesResource.java rename services/src/main/java/org/keycloak/{realm/resources/policies/admin/resource/RealmResourcePolicyResource.java => workflow/admin/resource/WorkflowResource.java} (50%) create mode 100644 services/src/main/java/org/keycloak/workflow/admin/resource/WorkflowsResource.java rename services/src/main/resources/META-INF/services/{org.keycloak.models.policy.ResourceActionProviderFactory => org.keycloak.models.workflow.WorkflowStepProviderFactory} (64%) delete mode 100644 tests/base/src/test/java/org/keycloak/tests/admin/model/policy/AddRequiredActionTest.java delete mode 100644 tests/base/src/test/java/org/keycloak/tests/admin/model/policy/RLMScheduledTaskServerConfig.java delete mode 100644 tests/base/src/test/java/org/keycloak/tests/admin/model/policy/ResourcePolicyManagementTest.java create mode 100644 tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/AddRequiredActionTest.java rename tests/base/src/test/java/org/keycloak/tests/admin/model/{policy/AdhocPolicyTest.java => workflow/AdhocWorkflowTest.java} (58%) rename tests/base/src/test/java/org/keycloak/tests/admin/model/{policy/AggregatedActionTest.java => workflow/AggregatedStepTest.java} (60%) rename tests/base/src/test/java/org/keycloak/tests/admin/model/{policy/BrokeredUserSessionRefreshTimePolicyTest.java => workflow/BrokeredUserSessionRefreshTimeWorkflowTest.java} (73%) rename tests/base/src/test/java/org/keycloak/tests/admin/model/{policy/GroupMembershipJoinPolicyTest.java => workflow/GroupMembershipJoinWorkflowTest.java} (58%) rename tests/base/src/test/java/org/keycloak/tests/admin/model/{policy/RolePolicyConditionTest.java => workflow/RoleWorkflowConditionTest.java} (76%) rename tests/base/src/test/java/org/keycloak/tests/admin/model/{policy/ActionRunnerScheduledTaskTest.java => workflow/StepRunnerScheduledTaskTest.java} (74%) rename tests/base/src/test/java/org/keycloak/tests/admin/model/{policy/UserAttributePolicyConditionTest.java => workflow/UserAttributeWorkflowConditionTest.java} (71%) rename tests/base/src/test/java/org/keycloak/tests/admin/model/{policy/UserCreationTimePolicyTest.java => workflow/UserCreationTimeWorkflowTest.java} (70%) rename tests/base/src/test/java/org/keycloak/tests/admin/model/{policy/UserSessionRefreshTimePolicyTest.java => workflow/UserSessionRefreshTimeWorkflowTest.java} (69%) create mode 100644 tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/WorkflowManagementTest.java create mode 100644 tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/WorkflowsScheduledTaskServerConfig.java rename tests/base/src/test/java/org/keycloak/tests/admin/model/{policy/RLMServerConfig.java => workflow/WorkflowsServerConfig.java} (85%) rename themes/src/main/resources/theme/base/email/html/{resource-policy-notification.ftl => workflow-notification.ftl} (100%) rename themes/src/main/resources/theme/base/email/text/{resource-policy-notification.ftl => workflow-notification.ftl} (100%) diff --git a/common/src/main/java/org/keycloak/common/Profile.java b/common/src/main/java/org/keycloak/common/Profile.java index 37b1f067e81..7792ae2ed68 100755 --- a/common/src/main/java/org/keycloak/common/Profile.java +++ b/common/src/main/java/org/keycloak/common/Profile.java @@ -140,7 +140,7 @@ public class Profile { ROLLING_UPDATES_V1("Rolling Updates", Type.DEFAULT, 1), ROLLING_UPDATES_V2("Rolling Updates for patch releases", Type.PREVIEW, 2), - RESOURCE_LIFECYCLE("Resource lifecycle management", Type.EXPERIMENTAL), + WORKFLOWS("Workflows", Type.EXPERIMENTAL), LOG_MDC("Mapped Diagnostic Context (MDC) information in logs", Type.PREVIEW), diff --git a/core/src/main/java/org/keycloak/representations/resources/policies/ResourcePolicyConditionRepresentation.java b/core/src/main/java/org/keycloak/representations/workflows/WorkflowConditionRepresentation.java similarity index 73% rename from core/src/main/java/org/keycloak/representations/resources/policies/ResourcePolicyConditionRepresentation.java rename to core/src/main/java/org/keycloak/representations/workflows/WorkflowConditionRepresentation.java index 8c5752603ca..405d124370f 100644 --- a/core/src/main/java/org/keycloak/representations/resources/policies/ResourcePolicyConditionRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/workflows/WorkflowConditionRepresentation.java @@ -1,11 +1,11 @@ -package org.keycloak.representations.resources.policies; +package org.keycloak.representations.workflows; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -public class ResourcePolicyConditionRepresentation { +public class WorkflowConditionRepresentation { public static Builder create() { return new Builder(); @@ -15,19 +15,19 @@ public class ResourcePolicyConditionRepresentation { private String providerId; private Map> config; - public ResourcePolicyConditionRepresentation() { + public WorkflowConditionRepresentation() { // reflection } - public ResourcePolicyConditionRepresentation(String providerId) { + public WorkflowConditionRepresentation(String providerId) { this(providerId, null); } - public ResourcePolicyConditionRepresentation(String providerId, Map> config) { + public WorkflowConditionRepresentation(String providerId, Map> config) { this(null, providerId, config); } - public ResourcePolicyConditionRepresentation(String id, String providerId, Map> config) { + public WorkflowConditionRepresentation(String id, String providerId, Map> config) { this.id = id; this.providerId = providerId; this.config = config; @@ -65,10 +65,10 @@ public class ResourcePolicyConditionRepresentation { public static class Builder { - private ResourcePolicyConditionRepresentation action; + private WorkflowConditionRepresentation action; public Builder of(String providerId) { - this.action = new ResourcePolicyConditionRepresentation(providerId); + this.action = new WorkflowConditionRepresentation(providerId); return this; } @@ -87,7 +87,7 @@ public class ResourcePolicyConditionRepresentation { return this; } - public ResourcePolicyConditionRepresentation build() { + public WorkflowConditionRepresentation build() { return action; } } diff --git a/core/src/main/java/org/keycloak/representations/resources/policies/ResourcePolicyRepresentation.java b/core/src/main/java/org/keycloak/representations/workflows/WorkflowRepresentation.java similarity index 59% rename from core/src/main/java/org/keycloak/representations/resources/policies/ResourcePolicyRepresentation.java rename to core/src/main/java/org/keycloak/representations/workflows/WorkflowRepresentation.java index 3529080c0a9..b65b8b63297 100644 --- a/core/src/main/java/org/keycloak/representations/resources/policies/ResourcePolicyRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/workflows/WorkflowRepresentation.java @@ -1,4 +1,4 @@ -package org.keycloak.representations.resources.policies; +package org.keycloak.representations.workflows; import java.util.ArrayList; import java.util.Arrays; @@ -11,7 +11,7 @@ import java.util.Optional; import org.keycloak.common.util.MultivaluedHashMap; -public class ResourcePolicyRepresentation { +public class WorkflowRepresentation { public static Builder create() { return new Builder(); @@ -20,22 +20,22 @@ public class ResourcePolicyRepresentation { private String id; private String providerId; private MultivaluedHashMap config; - private List actions; - private List conditions; + private List steps; + private List conditions; - public ResourcePolicyRepresentation() { + public WorkflowRepresentation() { // reflection } - public ResourcePolicyRepresentation(String providerId) { + public WorkflowRepresentation(String providerId) { this(providerId, null); } - public ResourcePolicyRepresentation(String providerId, Map> config) { + public WorkflowRepresentation(String providerId, Map> config) { this(null, providerId, config); } - public ResourcePolicyRepresentation(String id, String providerId, Map> config) { + public WorkflowRepresentation(String id, String providerId, Map> config) { this.id = id; this.providerId = providerId; this.config = new MultivaluedHashMap<>(config); @@ -68,38 +68,38 @@ public class ResourcePolicyRepresentation { this.config.putSingle("name", name); } - public void setConditions(List conditions) { + public void setConditions(List conditions) { this.conditions = conditions; } - public List getConditions() { + public List getConditions() { return conditions; } - public void setActions(List actions) { - this.actions = actions; + public void setSteps(List steps) { + this.steps = steps; } - public List getActions() { - return actions; + public List getSteps() { + return steps; } public MultivaluedHashMap getConfig() { return config; } - public void addAction(ResourcePolicyActionRepresentation action) { - if (actions == null) { - actions = new ArrayList<>(); + public void addStep(WorkflowStepRepresentation step) { + if (steps == null) { + steps = new ArrayList<>(); } - actions.add(action); + steps.add(step); } public static class Builder { private String providerId; - private Map> config = new HashMap<>(); - private List conditions = new ArrayList<>(); - private final Map> actions = new HashMap<>(); + private final Map> config = new HashMap<>(); + private List conditions = new ArrayList<>(); + private final Map> steps = new HashMap<>(); private List builders = new ArrayList<>(); private Builder() { @@ -124,7 +124,7 @@ public class ResourcePolicyRepresentation { return this; } - public Builder onConditions(ResourcePolicyConditionRepresentation... condition) { + public Builder onConditions(WorkflowConditionRepresentation... condition) { if (conditions == null) { conditions = new ArrayList<>(); } @@ -132,8 +132,8 @@ public class ResourcePolicyRepresentation { return this; } - public Builder withActions(ResourcePolicyActionRepresentation... actions) { - this.actions.computeIfAbsent(providerId, (k) -> new ArrayList<>()).addAll(Arrays.asList(actions)); + public Builder withSteps(WorkflowStepRepresentation... steps) { + this.steps.computeIfAbsent(providerId, (k) -> new ArrayList<>()).addAll(Arrays.asList(steps)); return this; } @@ -159,21 +159,21 @@ public class ResourcePolicyRepresentation { return withConfig("recurring", "true"); } - public List build() { - List policies = new ArrayList<>(); + public List build() { + List workflows = new ArrayList<>(); for (Builder builder : builders) { - for (Entry> entry : builder.actions.entrySet()) { - ResourcePolicyRepresentation policy = new ResourcePolicyRepresentation(entry.getKey(), builder.config); + for (Entry> entry : builder.steps.entrySet()) { + WorkflowRepresentation workflow = new WorkflowRepresentation(entry.getKey(), builder.config); - policy.setActions(entry.getValue()); - policy.setConditions(builder.conditions); + workflow.setSteps(entry.getValue()); + workflow.setConditions(builder.conditions); - policies.add(policy); + workflows.add(workflow); } } - return policies; + return workflows; } } } diff --git a/core/src/main/java/org/keycloak/representations/resources/policies/ResourcePolicyActionRepresentation.java b/core/src/main/java/org/keycloak/representations/workflows/WorkflowStepRepresentation.java similarity index 56% rename from core/src/main/java/org/keycloak/representations/resources/policies/ResourcePolicyActionRepresentation.java rename to core/src/main/java/org/keycloak/representations/workflows/WorkflowStepRepresentation.java index 1bad736332a..183411b7e3d 100644 --- a/core/src/main/java/org/keycloak/representations/resources/policies/ResourcePolicyActionRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/workflows/WorkflowStepRepresentation.java @@ -1,4 +1,4 @@ -package org.keycloak.representations.resources.policies; +package org.keycloak.representations.workflows; import java.time.Duration; import java.util.Arrays; @@ -7,7 +7,7 @@ import java.util.List; import org.keycloak.common.util.MultivaluedHashMap; -public class ResourcePolicyActionRepresentation { +public class WorkflowStepRepresentation { private static final String AFTER_KEY = "after"; @@ -18,25 +18,25 @@ public class ResourcePolicyActionRepresentation { private String id; private String providerId; private MultivaluedHashMap config; - private List actions; + private List steps; - public ResourcePolicyActionRepresentation() { + public WorkflowStepRepresentation() { // reflection } - public ResourcePolicyActionRepresentation(String providerId) { + public WorkflowStepRepresentation(String providerId) { this(providerId, null); } - public ResourcePolicyActionRepresentation(String providerId, MultivaluedHashMap config) { + public WorkflowStepRepresentation(String providerId, MultivaluedHashMap config) { this(null, providerId, config, null); } - public ResourcePolicyActionRepresentation(String id, String providerId, MultivaluedHashMap config, List actions) { + public WorkflowStepRepresentation(String id, String providerId, MultivaluedHashMap config, List steps) { this.id = id; this.providerId = providerId; this.config = config; - this.actions = actions; + this.steps = steps; } public String getId() { @@ -74,54 +74,54 @@ public class ResourcePolicyActionRepresentation { setConfig(AFTER_KEY, String.valueOf(ms)); } - public List getActions() { - return actions; + public List getSteps() { + return steps; } - public void setActions(List actions) { - this.actions = actions; + public void setSteps(List steps) { + this.steps = steps; } public static class Builder { - private ResourcePolicyActionRepresentation action; + private WorkflowStepRepresentation step; public Builder of(String providerId) { - this.action = new ResourcePolicyActionRepresentation(providerId); + this.step = new WorkflowStepRepresentation(providerId); return this; } public Builder after(Duration duration) { - action.setAfter(duration.toMillis()); + step.setAfter(duration.toMillis()); return this; } - public Builder before(ResourcePolicyActionRepresentation targetAction, Duration timeBeforeTarget) { - // Calculate absolute time: targetAction.after - timeBeforeTarget - String targetAfter = targetAction.getConfig().get(AFTER_KEY).get(0); + public Builder before(WorkflowStepRepresentation targetStep, Duration timeBeforeTarget) { + // Calculate absolute time: targetStep.after - timeBeforeTarget + String targetAfter = targetStep.getConfig().get(AFTER_KEY).get(0); long targetTime = Long.parseLong(targetAfter); long thisTime = targetTime - timeBeforeTarget.toMillis(); - action.setAfter(thisTime); + step.setAfter(thisTime); return this; } public Builder withConfig(String key, String value) { - action.setConfig(key, value); + step.setConfig(key, value); return this; } - public Builder withActions(ResourcePolicyActionRepresentation... actions) { - action.setActions(Arrays.asList(actions)); + public Builder withSteps(WorkflowStepRepresentation... steps) { + step.setSteps(Arrays.asList(steps)); return this; } public Builder withConfig(String key, List values) { - action.setConfig(key, values); + step.setConfig(key, values); return this; } - public ResourcePolicyActionRepresentation build() { - return action; + public WorkflowStepRepresentation build() { + return step; } } } diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java index 7d4e614a4ec..583a7afc5e2 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java @@ -421,6 +421,6 @@ public interface RealmResource { @Path("client-types") ClientTypesResource clientTypes(); - @Path("resources") - RealmResourcesResource resources(); + @Path("workflows") + WorkflowsResource workflows(); } diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResourcesResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResourcesResource.java deleted file mode 100644 index 1d271f560e0..00000000000 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResourcesResource.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.keycloak.admin.client.resource; - -import jakarta.ws.rs.Path; - -public interface RealmResourcesResource { - - @Path("policies") - RealmResourcePolicies policies(); -} diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResourcePolicy.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/WorkflowResource.java similarity index 79% rename from integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResourcePolicy.java rename to integration/admin-client/src/main/java/org/keycloak/admin/client/resource/WorkflowResource.java index 7424083405c..14420b704bd 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResourcePolicy.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/WorkflowResource.java @@ -12,20 +12,20 @@ import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation; +import org.keycloak.representations.workflows.WorkflowRepresentation; -public interface RealmResourcePolicy { +public interface WorkflowResource { @DELETE Response delete(); @PUT @Consumes(APPLICATION_JSON) - Response update(ResourcePolicyRepresentation policy); + Response update(WorkflowRepresentation workflow); @GET @Produces(APPLICATION_JSON) - ResourcePolicyRepresentation toRepresentation(); + WorkflowRepresentation toRepresentation(); @Path("bind/{type}/{resourceId}") @POST diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResourcePolicies.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/WorkflowsResource.java similarity index 57% rename from integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResourcePolicies.java rename to integration/admin-client/src/main/java/org/keycloak/admin/client/resource/WorkflowsResource.java index b586d9a82bf..ea966aa039d 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResourcePolicies.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/WorkflowsResource.java @@ -1,7 +1,5 @@ package org.keycloak.admin.client.resource; -import java.util.List; - import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; @@ -10,22 +8,24 @@ import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation; +import org.keycloak.representations.workflows.WorkflowRepresentation; -public interface RealmResourcePolicies { +import java.util.List; + +public interface WorkflowsResource { @POST @Consumes(MediaType.APPLICATION_JSON) - Response create(ResourcePolicyRepresentation representation); + Response create(WorkflowRepresentation representation); @POST @Consumes(MediaType.APPLICATION_JSON) - Response create(List representation); + Response create(List representation); @GET @Produces(MediaType.APPLICATION_JSON) - List list(); + List list(); @Path("{id}") - RealmResourcePolicy policy(@PathParam("id") String id); + WorkflowResource workflow(@PathParam("id") String id); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java index b9019dcc135..a3a72aca549 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java @@ -29,7 +29,6 @@ import org.keycloak.models.SubjectCredentialManager; import org.keycloak.models.UserModel; import org.keycloak.models.cache.CachedUserModel; import org.keycloak.models.cache.infinispan.entities.CachedUser; -import org.keycloak.models.policy.ResourcePolicyManager; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.RoleUtils; diff --git a/model/jpa/src/main/java/org/keycloak/models/policy/JpaResourcePolicyStateProvider.java b/model/jpa/src/main/java/org/keycloak/models/policy/JpaResourcePolicyStateProvider.java deleted file mode 100644 index 72584b61cda..00000000000 --- a/model/jpa/src/main/java/org/keycloak/models/policy/JpaResourcePolicyStateProvider.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright 2025 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.models.policy; - -import jakarta.persistence.EntityManager; -import jakarta.persistence.criteria.CriteriaBuilder; -import jakarta.persistence.criteria.CriteriaDelete; -import jakarta.persistence.criteria.CriteriaQuery; -import jakarta.persistence.criteria.Predicate; -import jakarta.persistence.criteria.Root; -import org.jboss.logging.Logger; -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.utils.StringUtil; - -import java.util.List; - -public class JpaResourcePolicyStateProvider implements ResourcePolicyStateProvider { - - private final EntityManager em; - private static final Logger LOGGER = Logger.getLogger(JpaResourcePolicyStateProvider.class); - private final KeycloakSession session; - - public JpaResourcePolicyStateProvider(KeycloakSession session) { - this.session = session; - this.em = session.getProvider(JpaConnectionProvider.class).getEntityManager(); - } - - @Override - public ScheduledAction getScheduledAction(String policyId, String resourceId) { - ResourcePolicyStateEntity.PrimaryKey pk = new ResourcePolicyStateEntity.PrimaryKey(resourceId, policyId); - ResourcePolicyStateEntity entity = em.find(ResourcePolicyStateEntity.class, pk); - if (entity != null) { - return new ScheduledAction(entity.getPolicyId(), entity.getScheduledActionId(), entity.getResourceId()); - } - return null; - } - - @Override - public void scheduleAction(ResourcePolicy policy, ResourceAction action, String resourceId) { - ResourcePolicyStateEntity.PrimaryKey pk = new ResourcePolicyStateEntity.PrimaryKey(resourceId, policy.getId()); - ResourcePolicyStateEntity entity = em.find(ResourcePolicyStateEntity.class, pk); - if (entity == null) { - entity = new ResourcePolicyStateEntity(); - entity.setResourceId(resourceId); - entity.setPolicyId(policy.getId()); - entity.setPolicyProviderId(policy.getProviderId()); - entity.setScheduledActionId(action.getId()); - entity.setScheduledActionTimestamp(Time.currentTimeMillis() + action.getAfter()); - em.persist(entity); - } - else { - entity.setScheduledActionId(action.getId()); - entity.setScheduledActionTimestamp(Time.currentTimeMillis() + action.getAfter()); - } - } - - @Override - public List getDueScheduledActions(ResourcePolicy policy) { - CriteriaBuilder cb = em.getCriteriaBuilder(); - CriteriaQuery query = cb.createQuery(ResourcePolicyStateEntity.class); - Root stateRoot = query.from(ResourcePolicyStateEntity.class); - - Predicate byPolicy = cb.equal(stateRoot.get("policyId"), policy.getId()); - Predicate isExpired = cb.lessThan(stateRoot.get("scheduledActionTimestamp"), Time.currentTimeMillis()); - - query.where(cb.and(byPolicy, isExpired)); - - return em.createQuery(query).getResultStream() - .map(s -> new ScheduledAction(s.getPolicyId(), s.getScheduledActionId(), s.getResourceId())) - .toList(); - } - - @Override - public List getScheduledActionsByPolicy(String id) { - if (StringUtil.isBlank(id)) { - return List.of(); - } - - CriteriaBuilder cb = em.getCriteriaBuilder(); - CriteriaQuery query = cb.createQuery(ResourcePolicyStateEntity.class); - Root stateRoot = query.from(ResourcePolicyStateEntity.class); - - Predicate byPolicy = cb.equal(stateRoot.get("policyId"), id); - query.where(byPolicy); - - return em.createQuery(query).getResultStream() - .map(s -> new ScheduledAction(s.getPolicyId(), s.getScheduledActionId(), s.getResourceId())) - .toList(); - } - - @Override - public List getScheduledActionsByResource(String resourceId) { - CriteriaBuilder cb = em.getCriteriaBuilder(); - CriteriaQuery query = cb.createQuery(ResourcePolicyStateEntity.class); - Root stateRoot = query.from(ResourcePolicyStateEntity.class); - - Predicate byResource = cb.equal(stateRoot.get("resourceId"), resourceId); - query.where(byResource); - - return em.createQuery(query).getResultStream() - .map(s -> new ScheduledAction(s.getPolicyId(), s.getScheduledActionId(), s.getResourceId())) - .toList(); - } - - @Override - public void removeByResource(String resourceId) { - CriteriaBuilder cb = em.getCriteriaBuilder(); - CriteriaDelete delete = cb.createCriteriaDelete(ResourcePolicyStateEntity.class); - Root root = delete.from(ResourcePolicyStateEntity.class); - delete.where(cb.equal(root.get("resourceId"), resourceId)); - int deletedCount = em.createQuery(delete).executeUpdate(); - - if (LOGGER.isTraceEnabled()) { - if (deletedCount > 0) { - LOGGER.tracev("Deleted {0} orphaned state records for resource {1}", deletedCount, resourceId); - } - } - } - - @Override - public void remove(String policyId, String resourceId) { - ResourcePolicyStateEntity.PrimaryKey pk = new ResourcePolicyStateEntity.PrimaryKey(resourceId, policyId); - ResourcePolicyStateEntity entity = em.find(ResourcePolicyStateEntity.class, pk); - if (entity != null) { - em.remove(entity); - } - } - - @Override - public void remove(String policyId) { - CriteriaBuilder cb = em.getCriteriaBuilder(); - CriteriaDelete delete = cb.createCriteriaDelete(ResourcePolicyStateEntity.class); - Root root = delete.from(ResourcePolicyStateEntity.class); - delete.where(cb.equal(root.get("policyId"), policyId)); - int deletedCount = em.createQuery(delete).executeUpdate(); - - if (LOGGER.isTraceEnabled()) { - if (deletedCount > 0) { - RealmModel realm = session.getContext().getRealm(); - LOGGER.tracev("Deleted {0} state records for realm {1}", deletedCount, realm.getId()); - } - } - } - - @Override - public void removeAll() { - CriteriaBuilder cb = em.getCriteriaBuilder(); - CriteriaDelete delete = cb.createCriteriaDelete(ResourcePolicyStateEntity.class); - int deletedCount = em.createQuery(delete).executeUpdate(); - - if (LOGGER.isTraceEnabled()) { - if (deletedCount > 0) { - RealmModel realm = session.getContext().getRealm(); - LOGGER.tracev("Deleted {0} state records for realm {1}", deletedCount, realm.getId()); - } - } - } - - @Override - public void close() { - } - -} diff --git a/model/jpa/src/main/java/org/keycloak/models/policy/AbstractUserResourcePolicyProvider.java b/model/jpa/src/main/java/org/keycloak/models/workflow/AbstractUserWorkflowProvider.java similarity index 82% rename from model/jpa/src/main/java/org/keycloak/models/policy/AbstractUserResourcePolicyProvider.java rename to model/jpa/src/main/java/org/keycloak/models/workflow/AbstractUserWorkflowProvider.java index 66b5363c710..5a765fe90ae 100644 --- a/model/jpa/src/main/java/org/keycloak/models/policy/AbstractUserResourcePolicyProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/AbstractUserWorkflowProvider.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import java.util.ArrayList; import java.util.List; @@ -32,31 +32,31 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.jpa.entities.UserEntity; -public abstract class AbstractUserResourcePolicyProvider extends EventBasedResourcePolicyProvider { +public abstract class AbstractUserWorkflowProvider extends EventBasedWorkflowProvider { private final EntityManager em; - public AbstractUserResourcePolicyProvider(KeycloakSession session, ComponentModel model) { + public AbstractUserWorkflowProvider(KeycloakSession session, ComponentModel model) { super(session, model); this.em = session.getProvider(JpaConnectionProvider.class).getEntityManager(); } @Override - public List getEligibleResourcesForInitialAction() { + public List getEligibleResourcesForInitialStep() { CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(String.class); Root userRoot = query.from(UserEntity.class); List 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 will find if a state record exists for the user and workflow + // SELECT 1 FROM WorkflowActionStateEntity s WHERE s.resourceId = userRoot.id AND s.workflowId = :workflowId Subquery subquery = query.subquery(Integer.class); - Root stateRoot = subquery.from(ResourcePolicyStateEntity.class); + Root stateRoot = subquery.from(WorkflowStateEntity.class); subquery.select(cb.literal(1)); subquery.where( cb.and( cb.equal(stateRoot.get("resourceId"), userRoot.get("id")), - cb.equal(stateRoot.get("policyId"), getModel().getId()) + cb.equal(stateRoot.get("workflowId"), getModel().getId()) ) ); Predicate notExistsPredicate = cb.not(cb.exists(subquery)); @@ -79,7 +79,7 @@ public abstract class AbstractUserResourcePolicyProvider extends EventBasedResou List predicates = new ArrayList<>(); for (String providerId : conditions) { - ResourcePolicyConditionProvider condition = resolveCondition(providerId); + WorkflowConditionProvider condition = resolveCondition(providerId); Predicate predicate = condition.toPredicate(cb, query, path); if (predicate != null) { diff --git a/model/jpa/src/main/java/org/keycloak/models/policy/EventBasedResourcePolicyProvider.java b/model/jpa/src/main/java/org/keycloak/models/workflow/EventBasedWorkflowProvider.java similarity index 72% rename from model/jpa/src/main/java/org/keycloak/models/policy/EventBasedResourcePolicyProvider.java rename to model/jpa/src/main/java/org/keycloak/models/workflow/EventBasedWorkflowProvider.java index 22d90c88f89..0fe14e54fd4 100644 --- a/model/jpa/src/main/java/org/keycloak/models/policy/EventBasedResourcePolicyProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/EventBasedWorkflowProvider.java @@ -1,4 +1,4 @@ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import java.util.HashMap; import java.util.List; @@ -9,18 +9,18 @@ import org.keycloak.component.ComponentModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; -public class EventBasedResourcePolicyProvider implements ResourcePolicyProvider { +public class EventBasedWorkflowProvider implements WorkflowProvider { private final KeycloakSession session; private final ComponentModel model; - public EventBasedResourcePolicyProvider(KeycloakSession session, ComponentModel model) { + public EventBasedWorkflowProvider(KeycloakSession session, ComponentModel model) { this.session = session; this.model = model; } @Override - public List getEligibleResourcesForInitialAction() { + public List getEligibleResourcesForInitialStep() { return List.of(); } @@ -30,7 +30,7 @@ public class EventBasedResourcePolicyProvider implements ResourcePolicyProvider } @Override - public boolean activateOnEvent(ResourcePolicyEvent event) { + public boolean activateOnEvent(WorkflowEvent event) { if (!supports(event.getResourceType())) { return false; } @@ -42,7 +42,7 @@ public class EventBasedResourcePolicyProvider implements ResourcePolicyProvider return evaluate(event); } - protected boolean isActivationEvent(ResourcePolicyEvent event) { + protected boolean isActivationEvent(WorkflowEvent event) { ResourceOperationType operation = event.getOperation(); if (ResourceOperationType.AD_HOC.equals(operation)) { @@ -55,7 +55,7 @@ public class EventBasedResourcePolicyProvider implements ResourcePolicyProvider } @Override - public boolean deactivateOnEvent(ResourcePolicyEvent event) { + public boolean deactivateOnEvent(WorkflowEvent event) { if (!supports(event.getResourceType())) { return false; } @@ -74,11 +74,11 @@ public class EventBasedResourcePolicyProvider implements ResourcePolicyProvider } @Override - public boolean resetOnEvent(ResourcePolicyEvent event) { + public boolean resetOnEvent(WorkflowEvent event) { return isResetEvent(event) && evaluate(event); } - protected boolean isResetEvent(ResourcePolicyEvent event) { + protected boolean isResetEvent(WorkflowEvent event) { boolean resetEventEnabled = Boolean.parseBoolean(getModel().getConfig().getFirstOrDefault("reset-event-enabled", Boolean.FALSE.toString())); return resetEventEnabled && isActivationEvent(event); } @@ -88,11 +88,11 @@ public class EventBasedResourcePolicyProvider implements ResourcePolicyProvider } - protected boolean evaluate(ResourcePolicyEvent event) { + protected boolean evaluate(WorkflowEvent event) { List conditions = getModel().getConfig().getOrDefault("conditions", List.of()); for (String providerId : conditions) { - ResourcePolicyConditionProvider condition = resolveCondition(providerId); + WorkflowConditionProvider condition = resolveCondition(providerId); if (!condition.evaluate(event)) { return false; @@ -102,9 +102,9 @@ public class EventBasedResourcePolicyProvider implements ResourcePolicyProvider return true; } - protected ResourcePolicyConditionProvider resolveCondition(String providerId) { + protected WorkflowConditionProvider resolveCondition(String providerId) { KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); - ResourcePolicyConditionProviderFactory providerFactory = (ResourcePolicyConditionProviderFactory) sessionFactory.getProviderFactory(ResourcePolicyConditionProvider.class, providerId); + WorkflowConditionProviderFactory providerFactory = (WorkflowConditionProviderFactory) sessionFactory.getProviderFactory(WorkflowConditionProvider.class, providerId); if (providerFactory == null) { throw new IllegalStateException("Could not find condition provider: " + providerId); @@ -118,7 +118,7 @@ public class EventBasedResourcePolicyProvider implements ResourcePolicyProvider } } - ResourcePolicyConditionProvider condition = providerFactory.create(session, config); + WorkflowConditionProvider condition = providerFactory.create(session, config); if (condition == null) { throw new IllegalStateException("Factory " + providerFactory.getClass() + " returned a null provider"); diff --git a/model/jpa/src/main/java/org/keycloak/models/policy/EventBasedResourcePolicyProviderFactory.java b/model/jpa/src/main/java/org/keycloak/models/workflow/EventBasedWorkflowProviderFactory.java similarity index 66% rename from model/jpa/src/main/java/org/keycloak/models/policy/EventBasedResourcePolicyProviderFactory.java rename to model/jpa/src/main/java/org/keycloak/models/workflow/EventBasedWorkflowProviderFactory.java index 8c80e26f292..5b28e688844 100644 --- a/model/jpa/src/main/java/org/keycloak/models/policy/EventBasedResourcePolicyProviderFactory.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/EventBasedWorkflowProviderFactory.java @@ -1,4 +1,4 @@ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import java.util.List; @@ -8,13 +8,13 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.provider.ProviderConfigProperty; -public class EventBasedResourcePolicyProviderFactory implements ResourcePolicyProviderFactory { +public class EventBasedWorkflowProviderFactory implements WorkflowProviderFactory { - public static final String ID = "event-based-resource-policy"; + public static final String ID = "event-based-workflow"; @Override - public ResourcePolicyProvider create(KeycloakSession session, ComponentModel model) { - return new EventBasedResourcePolicyProvider(session, model); + public WorkflowProvider create(KeycloakSession session, ComponentModel model) { + return new EventBasedWorkflowProvider(session, model); } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/workflow/JpaWorkflowStateProvider.java b/model/jpa/src/main/java/org/keycloak/models/workflow/JpaWorkflowStateProvider.java new file mode 100644 index 00000000000..d47fedc13f7 --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/JpaWorkflowStateProvider.java @@ -0,0 +1,180 @@ +/* + * Copyright 2025 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.workflow; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaDelete; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; +import org.jboss.logging.Logger; +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.utils.StringUtil; + +import java.util.List; + +public class JpaWorkflowStateProvider implements WorkflowStateProvider { + + private final EntityManager em; + private static final Logger LOGGER = Logger.getLogger(JpaWorkflowStateProvider.class); + private final KeycloakSession session; + + public JpaWorkflowStateProvider(KeycloakSession session) { + this.session = session; + this.em = session.getProvider(JpaConnectionProvider.class).getEntityManager(); + } + + @Override + public ScheduledStep getScheduledStep(String workflowId, String resourceId) { + WorkflowStateEntity.PrimaryKey pk = new WorkflowStateEntity.PrimaryKey(resourceId, workflowId); + WorkflowStateEntity entity = em.find(WorkflowStateEntity.class, pk); + if (entity != null) { + return new ScheduledStep(entity.getWorkflowId(), entity.getScheduledStepId(), entity.getResourceId()); + } + return null; + } + + @Override + public void scheduleStep(Workflow workflow, WorkflowStep step, String resourceId) { + WorkflowStateEntity.PrimaryKey pk = new WorkflowStateEntity.PrimaryKey(resourceId, workflow.getId()); + WorkflowStateEntity entity = em.find(WorkflowStateEntity.class, pk); + if (entity == null) { + entity = new WorkflowStateEntity(); + entity.setResourceId(resourceId); + entity.setWorkflowId(workflow.getId()); + entity.setWorkflowProviderId(workflow.getProviderId()); + entity.setScheduledStepId(step.getId()); + entity.setScheduledStepTimestamp(Time.currentTimeMillis() + step.getAfter()); + em.persist(entity); + } else { + entity.setScheduledStepId(step.getId()); + entity.setScheduledStepTimestamp(Time.currentTimeMillis() + step.getAfter()); + } + } + + @Override + public List getDueScheduledSteps(Workflow workflow) { + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery(WorkflowStateEntity.class); + Root stateRoot = query.from(WorkflowStateEntity.class); + + Predicate byWorkflow = cb.equal(stateRoot.get("workflowId"), workflow.getId()); + Predicate isExpired = cb.lessThan(stateRoot.get("scheduledStepTimestamp"), Time.currentTimeMillis()); + + query.where(cb.and(byWorkflow, isExpired)); + + return em.createQuery(query).getResultStream() + .map(s -> new ScheduledStep(s.getWorkflowId(), s.getScheduledStepId(), s.getResourceId())) + .toList(); + } + + @Override + public List getScheduledStepsByWorkflow(String workflowId) { + if (StringUtil.isBlank(workflowId)) { + return List.of(); + } + + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery(WorkflowStateEntity.class); + Root stateRoot = query.from(WorkflowStateEntity.class); + + Predicate byWorkflow = cb.equal(stateRoot.get("workflowId"), workflowId); + query.where(byWorkflow); + + return em.createQuery(query).getResultStream() + .map(s -> new ScheduledStep(s.getWorkflowId(), s.getScheduledStepId(), s.getResourceId())) + .toList(); + } + + @Override + public List getScheduledStepsByResource(String resourceId) { + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery(WorkflowStateEntity.class); + Root stateRoot = query.from(WorkflowStateEntity.class); + + Predicate byResource = cb.equal(stateRoot.get("resourceId"), resourceId); + query.where(byResource); + + return em.createQuery(query).getResultStream() + .map(s -> new ScheduledStep(s.getWorkflowId(), s.getScheduledStepId(), s.getResourceId())) + .toList(); + } + + @Override + public void removeByResource(String resourceId) { + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaDelete delete = cb.createCriteriaDelete(WorkflowStateEntity.class); + Root root = delete.from(WorkflowStateEntity.class); + delete.where(cb.equal(root.get("resourceId"), resourceId)); + int deletedCount = em.createQuery(delete).executeUpdate(); + + if (LOGGER.isTraceEnabled()) { + if (deletedCount > 0) { + LOGGER.tracev("Deleted {0} orphaned state records for resource {1}", deletedCount, resourceId); + } + } + } + + @Override + public void remove(String workflowId, String resourceId) { + WorkflowStateEntity.PrimaryKey pk = new WorkflowStateEntity.PrimaryKey(resourceId, workflowId); + WorkflowStateEntity entity = em.find(WorkflowStateEntity.class, pk); + if (entity != null) { + em.remove(entity); + } + } + + @Override + public void remove(String workflowId) { + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaDelete delete = cb.createCriteriaDelete(WorkflowStateEntity.class); + Root root = delete.from(WorkflowStateEntity.class); + delete.where(cb.equal(root.get("workflowId"), workflowId)); + int deletedCount = em.createQuery(delete).executeUpdate(); + + if (LOGGER.isTraceEnabled()) { + if (deletedCount > 0) { + RealmModel realm = session.getContext().getRealm(); + LOGGER.tracev("Deleted {0} state records for realm {1}", deletedCount, realm.getId()); + } + } + } + + @Override + public void removeAll() { + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaDelete delete = cb.createCriteriaDelete(WorkflowStateEntity.class); + int deletedCount = em.createQuery(delete).executeUpdate(); + + if (LOGGER.isTraceEnabled()) { + if (deletedCount > 0) { + RealmModel realm = session.getContext().getRealm(); + LOGGER.tracev("Deleted {0} state records for realm {1}", deletedCount, realm.getId()); + } + } + } + + @Override + public void close() { + } + +} diff --git a/model/jpa/src/main/java/org/keycloak/models/policy/JpaResourcePolicyStateProviderFactory.java b/model/jpa/src/main/java/org/keycloak/models/workflow/JpaWorkflowStateProviderFactory.java similarity index 79% rename from model/jpa/src/main/java/org/keycloak/models/policy/JpaResourcePolicyStateProviderFactory.java rename to model/jpa/src/main/java/org/keycloak/models/workflow/JpaWorkflowStateProviderFactory.java index 0669aae3b40..a7e244abb6f 100644 --- a/model/jpa/src/main/java/org/keycloak/models/policy/JpaResourcePolicyStateProviderFactory.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/JpaWorkflowStateProviderFactory.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import org.keycloak.Config; import org.keycloak.models.KeycloakSession; @@ -23,7 +23,7 @@ import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel.RealmRemovedEvent; import org.keycloak.models.UserModel.UserRemovedEvent; -public class JpaResourcePolicyStateProviderFactory implements ResourcePolicyStateProviderFactory { +public class JpaWorkflowStateProviderFactory implements WorkflowStateProviderFactory { public static final String PROVIDER_ID = "jpa"; @@ -48,8 +48,8 @@ public class JpaResourcePolicyStateProviderFactory implements ResourcePolicyStat } @Override - public ResourcePolicyStateProvider create(KeycloakSession session) { - return new JpaResourcePolicyStateProvider(session); + public WorkflowStateProvider create(KeycloakSession session) { + return new JpaWorkflowStateProvider(session); } @Override @@ -58,13 +58,13 @@ public class JpaResourcePolicyStateProviderFactory implements ResourcePolicyStat private void onRealmRemovedEvent(RealmRemovedEvent event) { KeycloakSession session = event.getKeycloakSession(); - ResourcePolicyStateProvider provider = session.getProvider(ResourcePolicyStateProvider.class); + WorkflowStateProvider provider = session.getProvider(WorkflowStateProvider.class); provider.removeAll(); } private void onUserRemovedEvent(UserRemovedEvent event) { KeycloakSession session = event.getKeycloakSession(); - ResourcePolicyStateProvider provider = session.getProvider(ResourcePolicyStateProvider.class); + WorkflowStateProvider provider = session.getProvider(WorkflowStateProvider.class); provider.removeByResource(event.getUser().getId()); } } diff --git a/model/jpa/src/main/java/org/keycloak/models/policy/UserCreationTimeResourcePolicyProvider.java b/model/jpa/src/main/java/org/keycloak/models/workflow/UserCreationTimeWorkflowProvider.java similarity index 69% rename from model/jpa/src/main/java/org/keycloak/models/policy/UserCreationTimeResourcePolicyProvider.java rename to model/jpa/src/main/java/org/keycloak/models/workflow/UserCreationTimeWorkflowProvider.java index 5dc196e7997..6baaaac7f74 100644 --- a/model/jpa/src/main/java/org/keycloak/models/policy/UserCreationTimeResourcePolicyProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/UserCreationTimeWorkflowProvider.java @@ -15,23 +15,21 @@ * limitations under the License. */ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import org.keycloak.component.ComponentModel; import org.keycloak.models.KeycloakSession; -import java.util.List; +import static org.keycloak.models.workflow.ResourceOperationType.CREATE; -import static org.keycloak.models.policy.ResourceOperationType.CREATE; +public class UserCreationTimeWorkflowProvider extends AbstractUserWorkflowProvider { -public class UserCreationTimeResourcePolicyProvider extends AbstractUserResourcePolicyProvider { - - public UserCreationTimeResourcePolicyProvider(KeycloakSession session, ComponentModel model) { + public UserCreationTimeWorkflowProvider(KeycloakSession session, ComponentModel model) { super(session, model); } @Override - protected boolean isActivationEvent(ResourcePolicyEvent event) { + protected boolean isActivationEvent(WorkflowEvent event) { return super.isActivationEvent(event) || CREATE.equals(event.getOperation()); } } diff --git a/model/jpa/src/main/java/org/keycloak/models/policy/UserSessionRefreshTimeResourcePolicyProviderFactory.java b/model/jpa/src/main/java/org/keycloak/models/workflow/UserCreationTimeWorkflowProviderFactory.java similarity index 76% rename from model/jpa/src/main/java/org/keycloak/models/policy/UserSessionRefreshTimeResourcePolicyProviderFactory.java rename to model/jpa/src/main/java/org/keycloak/models/workflow/UserCreationTimeWorkflowProviderFactory.java index 460ff9d69e6..67de1c6aa65 100644 --- a/model/jpa/src/main/java/org/keycloak/models/policy/UserSessionRefreshTimeResourcePolicyProviderFactory.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/UserCreationTimeWorkflowProviderFactory.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import java.util.List; @@ -25,13 +25,13 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.provider.ProviderConfigProperty; -public class UserSessionRefreshTimeResourcePolicyProviderFactory implements ResourcePolicyProviderFactory { +public class UserCreationTimeWorkflowProviderFactory implements WorkflowProviderFactory { - public static final String ID = "user-refresh-time-resource-policy"; + public static final String ID = "user-creation-time-workflow"; @Override - public UserSessionRefreshTimeResourcePolicyProvider create(KeycloakSession session, ComponentModel model) { - return new UserSessionRefreshTimeResourcePolicyProvider(session, model); + public UserCreationTimeWorkflowProvider create(KeycloakSession session, ComponentModel model) { + return new UserCreationTimeWorkflowProvider(session, model); } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/policy/UserSessionRefreshTimeResourcePolicyProvider.java b/model/jpa/src/main/java/org/keycloak/models/workflow/UserSessionRefreshTimeWorkflowProvider.java similarity index 66% rename from model/jpa/src/main/java/org/keycloak/models/policy/UserSessionRefreshTimeResourcePolicyProvider.java rename to model/jpa/src/main/java/org/keycloak/models/workflow/UserSessionRefreshTimeWorkflowProvider.java index 20617fd6be2..382e295169f 100644 --- a/model/jpa/src/main/java/org/keycloak/models/policy/UserSessionRefreshTimeResourcePolicyProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/UserSessionRefreshTimeWorkflowProvider.java @@ -15,29 +15,29 @@ * limitations under the License. */ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; -import static org.keycloak.models.policy.ResourceOperationType.CREATE; -import static org.keycloak.models.policy.ResourceOperationType.LOGIN; +import static org.keycloak.models.workflow.ResourceOperationType.CREATE; +import static org.keycloak.models.workflow.ResourceOperationType.LOGIN; import java.util.List; import org.keycloak.component.ComponentModel; import org.keycloak.models.KeycloakSession; -public class UserSessionRefreshTimeResourcePolicyProvider extends AbstractUserResourcePolicyProvider { +public class UserSessionRefreshTimeWorkflowProvider extends AbstractUserWorkflowProvider { - public UserSessionRefreshTimeResourcePolicyProvider(KeycloakSession session, ComponentModel model) { + public UserSessionRefreshTimeWorkflowProvider(KeycloakSession session, ComponentModel model) { super(session, model); } @Override - protected boolean isActivationEvent(ResourcePolicyEvent event) { + protected boolean isActivationEvent(WorkflowEvent event) { return super.isActivationEvent(event) || List.of(CREATE, LOGIN).contains(event.getOperation()); } @Override - protected boolean isResetEvent(ResourcePolicyEvent event) { + protected boolean isResetEvent(WorkflowEvent event) { return LOGIN.equals(event.getOperation()); } } diff --git a/model/jpa/src/main/java/org/keycloak/models/policy/UserCreationTimeResourcePolicyProviderFactory.java b/model/jpa/src/main/java/org/keycloak/models/workflow/UserSessionRefreshTimeWorkflowProviderFactory.java similarity index 79% rename from model/jpa/src/main/java/org/keycloak/models/policy/UserCreationTimeResourcePolicyProviderFactory.java rename to model/jpa/src/main/java/org/keycloak/models/workflow/UserSessionRefreshTimeWorkflowProviderFactory.java index df308c1da9e..c5cfe254503 100644 --- a/model/jpa/src/main/java/org/keycloak/models/policy/UserCreationTimeResourcePolicyProviderFactory.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/UserSessionRefreshTimeWorkflowProviderFactory.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import java.util.List; @@ -25,13 +25,13 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.provider.ProviderConfigProperty; -public class UserCreationTimeResourcePolicyProviderFactory implements ResourcePolicyProviderFactory { +public class UserSessionRefreshTimeWorkflowProviderFactory implements WorkflowProviderFactory { - public static final String ID = "user-creation-time-resource-policy"; + public static final String ID = "user-refresh-time-workflow"; @Override - public UserCreationTimeResourcePolicyProvider create(KeycloakSession session, ComponentModel model) { - return new UserCreationTimeResourcePolicyProvider(session, model); + public UserSessionRefreshTimeWorkflowProvider create(KeycloakSession session, ComponentModel model) { + return new UserSessionRefreshTimeWorkflowProvider(session, model); } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/policy/ResourcePolicyStateEntity.java b/model/jpa/src/main/java/org/keycloak/models/workflow/WorkflowStateEntity.java similarity index 57% rename from model/jpa/src/main/java/org/keycloak/models/policy/ResourcePolicyStateEntity.java rename to model/jpa/src/main/java/org/keycloak/models/workflow/WorkflowStateEntity.java index 3aa35ae65da..8e68e005d30 100644 --- a/model/jpa/src/main/java/org/keycloak/models/policy/ResourcePolicyStateEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/WorkflowStateEntity.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -26,32 +26,32 @@ import java.io.Serializable; import java.util.Objects; /** - * Represents the state of a resource within a time-based policy flow. + * Represents the state of a resource within a time-based workflow. */ @Entity -@Table(name = "RESOURCE_POLICY_STATE") -@IdClass(ResourcePolicyStateEntity.PrimaryKey.class) -public class ResourcePolicyStateEntity { +@Table(name = "WORKFLOW_STATE") +@IdClass(WorkflowStateEntity.PrimaryKey.class) +public class WorkflowStateEntity { @Id @Column(name = "RESOURCE_ID") private String resourceId; @Id - @Column(name = "POLICY_ID") - private String policyId; + @Column(name = "WORKFLOW_ID") + private String workflowId; @Column(name = "RESOURCE_TYPE") private String resourceType; // do we want/need to store this? - @Column(name = "POLICY_PROVIDER_ID") - private String policyProviderId; + @Column(name = "WORKFLOW_PROVIDER_ID") + private String workflowProviderId; - @Column(name = "SCHEDULED_ACTION_ID") - private String scheduledActionId; + @Column(name = "SCHEDULED_STEP_ID") + private String scheduledStepId; - @Column(name = "SCHEDULED_ACTION_TIMESTAMP") - private long scheduledActionTimestamp; + @Column(name = "SCHEDULED_STEP_TIMESTAMP") + private long scheduledStepTimestamp; public String getResourceId() { return resourceId; @@ -61,20 +61,20 @@ public class ResourcePolicyStateEntity { this.resourceId = resourceId; } - public String getPolicyId() { - return policyId; + public String getWorkflowId() { + return workflowId; } - public void setPolicyId(String policyId) { - this.policyId = policyId; + public void setWorkflowId(String workflowId) { + this.workflowId = workflowId; } - public String getPolicyProviderId() { - return policyProviderId; + public String getWorkflowProviderId() { + return workflowProviderId; } - public void setPolicyProviderId(String policyProviderId) { - this.policyProviderId = policyProviderId; + public void setWorkflowProviderId(String workflowProviderId) { + this.workflowProviderId = workflowProviderId; } public String getResourceType() { @@ -85,33 +85,33 @@ public class ResourcePolicyStateEntity { this.resourceType = resourceType; } - public String getScheduledActionId() { - return scheduledActionId; + public String getScheduledStepId() { + return scheduledStepId; } - public void setScheduledActionId(String scheduledActionId) { - this.scheduledActionId = scheduledActionId; + public void setScheduledStepId(String scheduledStepId) { + this.scheduledStepId = scheduledStepId; } - public long getScheduledActionTimestamp() { - return scheduledActionTimestamp; + public long getScheduledStepTimestamp() { + return scheduledStepTimestamp; } - public void setScheduledActionTimestamp(long scheduledActionTimestamp) { - this.scheduledActionTimestamp = scheduledActionTimestamp; + public void setScheduledStepTimestamp(long scheduledStepTimestamp) { + this.scheduledStepTimestamp = scheduledStepTimestamp; } public static class PrimaryKey implements Serializable { private String resourceId; - private String policyId; + private String workflowId; public PrimaryKey() { } - public PrimaryKey(String resourceId, String policyId) { + public PrimaryKey(String resourceId, String workflowId) { this.resourceId = resourceId; - this.policyId = policyId; + this.workflowId = workflowId; } public String getResourceId() { @@ -122,12 +122,12 @@ public class ResourcePolicyStateEntity { this.resourceId = resourceId; } - public String getPolicyId() { - return policyId; + public String getWorkflowId() { + return workflowId; } - public void setPolicyId(String policyId) { - this.policyId = policyId; + public void setWorkflowId(String workflowId) { + this.workflowId = workflowId; } @Override @@ -135,12 +135,12 @@ public class ResourcePolicyStateEntity { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; PrimaryKey that = (PrimaryKey) o; - return Objects.equals(resourceId, that.resourceId) && Objects.equals(policyId, that.policyId); + return Objects.equals(resourceId, that.resourceId) && Objects.equals(workflowId, that.workflowId); } @Override public int hashCode() { - return Objects.hash(resourceId, policyId); + return Objects.hash(resourceId, workflowId); } } @@ -148,13 +148,13 @@ public class ResourcePolicyStateEntity { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - ResourcePolicyStateEntity that = (ResourcePolicyStateEntity) o; - return Objects.equals(resourceId, that.resourceId) && Objects.equals(policyId, that.policyId); + WorkflowStateEntity that = (WorkflowStateEntity) o; + return Objects.equals(resourceId, that.resourceId) && Objects.equals(workflowId, that.workflowId); } @Override public int hashCode() { - return Objects.hash(resourceId, policyId); + return Objects.hash(resourceId, workflowId); } } diff --git a/model/jpa/src/main/java/org/keycloak/models/policy/conditions/GroupMembershipPolicyConditionFactory.java b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/GroupMembershipWorkflowConditionFactory.java similarity index 54% rename from model/jpa/src/main/java/org/keycloak/models/policy/conditions/GroupMembershipPolicyConditionFactory.java rename to model/jpa/src/main/java/org/keycloak/models/workflow/conditions/GroupMembershipWorkflowConditionFactory.java index 981120513e0..69e371210ea 100644 --- a/model/jpa/src/main/java/org/keycloak/models/policy/conditions/GroupMembershipPolicyConditionFactory.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/GroupMembershipWorkflowConditionFactory.java @@ -1,20 +1,20 @@ -package org.keycloak.models.policy.conditions; +package org.keycloak.models.workflow.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; +import org.keycloak.models.workflow.WorkflowConditionProviderFactory; -public class GroupMembershipPolicyConditionFactory implements ResourcePolicyConditionProviderFactory { +public class GroupMembershipWorkflowConditionFactory implements WorkflowConditionProviderFactory { public static final String ID = "group-membership-condition"; public static final String EXPECTED_GROUPS = "groups"; @Override - public GroupMembershipPolicyConditionProvider create(KeycloakSession session, Map> config) { - return new GroupMembershipPolicyConditionProvider(session, config.get(EXPECTED_GROUPS)); + public GroupMembershipWorkflowConditionProvider create(KeycloakSession session, Map> config) { + return new GroupMembershipWorkflowConditionProvider(session, config.get(EXPECTED_GROUPS)); } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/policy/conditions/GroupMembershipPolicyConditionProvider.java b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/GroupMembershipWorkflowConditionProvider.java similarity index 64% rename from model/jpa/src/main/java/org/keycloak/models/policy/conditions/GroupMembershipPolicyConditionProvider.java rename to model/jpa/src/main/java/org/keycloak/models/workflow/conditions/GroupMembershipWorkflowConditionProvider.java index 754d9718452..ef8a2631f03 100644 --- a/model/jpa/src/main/java/org/keycloak/models/policy/conditions/GroupMembershipPolicyConditionProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/GroupMembershipWorkflowConditionProvider.java @@ -1,4 +1,4 @@ -package org.keycloak.models.policy.conditions; +package org.keycloak.models.workflow.conditions; import java.util.List; @@ -6,23 +6,23 @@ import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; -import org.keycloak.models.policy.ResourcePolicyConditionProvider; -import org.keycloak.models.policy.ResourcePolicyEvent; -import org.keycloak.models.policy.ResourcePolicyInvalidStateException; -import org.keycloak.models.policy.ResourceType; +import org.keycloak.models.workflow.WorkflowConditionProvider; +import org.keycloak.models.workflow.WorkflowEvent; +import org.keycloak.models.workflow.WorkflowInvalidStateException; +import org.keycloak.models.workflow.ResourceType; -public class GroupMembershipPolicyConditionProvider implements ResourcePolicyConditionProvider { +public class GroupMembershipWorkflowConditionProvider implements WorkflowConditionProvider { private final List expectedGroups; private final KeycloakSession session; - public GroupMembershipPolicyConditionProvider(KeycloakSession session, List expectedGroups) { + public GroupMembershipWorkflowConditionProvider(KeycloakSession session, List expectedGroups) { this.session = session; this.expectedGroups = expectedGroups;; } @Override - public boolean evaluate(ResourcePolicyEvent event) { + public boolean evaluate(WorkflowEvent event) { if (!ResourceType.USERS.equals(event.getResourceType())) { return false; } @@ -48,7 +48,7 @@ public class GroupMembershipPolicyConditionProvider implements ResourcePolicyCon public void validate() { expectedGroups.forEach(id -> { if (session.groups().getGroupById(session.getContext().getRealm(), id) == null) { - throw new ResourcePolicyInvalidStateException(String.format("Group with id %s does not exist.", id)); + throw new WorkflowInvalidStateException(String.format("Group with id %s does not exist.", id)); } }); } diff --git a/model/jpa/src/main/java/org/keycloak/models/policy/conditions/IdentityProviderPolicyConditionFactory.java b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/IdentityProviderWorkflowConditionFactory.java similarity index 53% rename from model/jpa/src/main/java/org/keycloak/models/policy/conditions/IdentityProviderPolicyConditionFactory.java rename to model/jpa/src/main/java/org/keycloak/models/workflow/conditions/IdentityProviderWorkflowConditionFactory.java index ec38eb872c4..4e866464736 100644 --- a/model/jpa/src/main/java/org/keycloak/models/policy/conditions/IdentityProviderPolicyConditionFactory.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/IdentityProviderWorkflowConditionFactory.java @@ -1,20 +1,20 @@ -package org.keycloak.models.policy.conditions; +package org.keycloak.models.workflow.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; +import org.keycloak.models.workflow.WorkflowConditionProviderFactory; -public class IdentityProviderPolicyConditionFactory implements ResourcePolicyConditionProviderFactory { +public class IdentityProviderWorkflowConditionFactory implements WorkflowConditionProviderFactory { public static final String ID = "identity-provider-condition"; public static final String EXPECTED_ALIASES = "alias"; @Override - public IdentityProviderPolicyConditionProvider create(KeycloakSession session, Map> config) { - return new IdentityProviderPolicyConditionProvider(session, config.get(EXPECTED_ALIASES)); + public IdentityProviderWorkflowConditionProvider create(KeycloakSession session, Map> config) { + return new IdentityProviderWorkflowConditionProvider(session, config.get(EXPECTED_ALIASES)); } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/policy/conditions/IdentityProviderPolicyConditionProvider.java b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/IdentityProviderWorkflowConditionProvider.java similarity index 75% rename from model/jpa/src/main/java/org/keycloak/models/policy/conditions/IdentityProviderPolicyConditionProvider.java rename to model/jpa/src/main/java/org/keycloak/models/workflow/conditions/IdentityProviderWorkflowConditionProvider.java index bdbf7b8decb..d556ef7adac 100644 --- a/model/jpa/src/main/java/org/keycloak/models/policy/conditions/IdentityProviderPolicyConditionProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/IdentityProviderWorkflowConditionProvider.java @@ -1,4 +1,4 @@ -package org.keycloak.models.policy.conditions; +package org.keycloak.models.workflow.conditions; import java.util.List; import java.util.stream.Stream; @@ -13,23 +13,23 @@ 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.policy.ResourcePolicyConditionProvider; -import org.keycloak.models.policy.ResourcePolicyEvent; -import org.keycloak.models.policy.ResourcePolicyInvalidStateException; -import org.keycloak.models.policy.ResourceType; +import org.keycloak.models.workflow.WorkflowConditionProvider; +import org.keycloak.models.workflow.WorkflowEvent; +import org.keycloak.models.workflow.WorkflowInvalidStateException; +import org.keycloak.models.workflow.ResourceType; -public class IdentityProviderPolicyConditionProvider implements ResourcePolicyConditionProvider { +public class IdentityProviderWorkflowConditionProvider implements WorkflowConditionProvider { private final List expectedAliases; private final KeycloakSession session; - public IdentityProviderPolicyConditionProvider(KeycloakSession session, List expectedAliases) { + public IdentityProviderWorkflowConditionProvider(KeycloakSession session, List expectedAliases) { this.session = session; this.expectedAliases = expectedAliases;; } @Override - public boolean evaluate(ResourcePolicyEvent event) { + public boolean evaluate(WorkflowEvent event) { if (!ResourceType.USERS.equals(event.getResourceType())) { return false; } @@ -66,7 +66,7 @@ public class IdentityProviderPolicyConditionProvider implements ResourcePolicyCo public void validate() { expectedAliases.forEach(alias -> { if (session.identityProviders().getByAlias(alias) == null) { - throw new ResourcePolicyInvalidStateException(String.format("Identity provider %s does not exist.", alias)); + throw new WorkflowInvalidStateException(String.format("Identity provider %s does not exist.", alias)); } }); } diff --git a/model/jpa/src/main/java/org/keycloak/models/policy/conditions/RolePolicyConditionFactory.java b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/RoleWorkflowConditionFactory.java similarity index 55% rename from model/jpa/src/main/java/org/keycloak/models/policy/conditions/RolePolicyConditionFactory.java rename to model/jpa/src/main/java/org/keycloak/models/workflow/conditions/RoleWorkflowConditionFactory.java index 05e34e4db66..f50bddaaa9e 100644 --- a/model/jpa/src/main/java/org/keycloak/models/policy/conditions/RolePolicyConditionFactory.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/RoleWorkflowConditionFactory.java @@ -1,20 +1,20 @@ -package org.keycloak.models.policy.conditions; +package org.keycloak.models.workflow.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; +import org.keycloak.models.workflow.WorkflowConditionProviderFactory; -public class RolePolicyConditionFactory implements ResourcePolicyConditionProviderFactory { +public class RoleWorkflowConditionFactory implements WorkflowConditionProviderFactory { public static final String ID = "role-condition"; public static final String EXPECTED_ROLES = "roles"; @Override - public RolePolicyConditionProvider create(KeycloakSession session, Map> config) { - return new RolePolicyConditionProvider(session, config.get(EXPECTED_ROLES)); + public RoleWorkflowConditionProvider create(KeycloakSession session, Map> config) { + return new RoleWorkflowConditionProvider(session, config.get(EXPECTED_ROLES)); } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/policy/conditions/RolePolicyConditionProvider.java b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/RoleWorkflowConditionProvider.java similarity index 74% rename from model/jpa/src/main/java/org/keycloak/models/policy/conditions/RolePolicyConditionProvider.java rename to model/jpa/src/main/java/org/keycloak/models/workflow/conditions/RoleWorkflowConditionProvider.java index eb8015220d6..ab5e0395b9d 100644 --- a/model/jpa/src/main/java/org/keycloak/models/policy/conditions/RolePolicyConditionProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/RoleWorkflowConditionProvider.java @@ -1,4 +1,4 @@ -package org.keycloak.models.policy.conditions; +package org.keycloak.models.workflow.conditions; import java.util.List; import java.util.Set; @@ -9,24 +9,24 @@ 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.ResourcePolicyInvalidStateException; -import org.keycloak.models.policy.ResourceType; +import org.keycloak.models.workflow.WorkflowConditionProvider; +import org.keycloak.models.workflow.WorkflowEvent; +import org.keycloak.models.workflow.WorkflowInvalidStateException; +import org.keycloak.models.workflow.ResourceType; import org.keycloak.models.utils.RoleUtils; -public class RolePolicyConditionProvider implements ResourcePolicyConditionProvider { +public class RoleWorkflowConditionProvider implements WorkflowConditionProvider { private final List expectedRoles; private final KeycloakSession session; - public RolePolicyConditionProvider(KeycloakSession session, List expectedRoles) { + public RoleWorkflowConditionProvider(KeycloakSession session, List expectedRoles) { this.session = session; this.expectedRoles = expectedRoles; } @Override - public boolean evaluate(ResourcePolicyEvent event) { + public boolean evaluate(WorkflowEvent event) { if (!ResourceType.USERS.equals(event.getResourceType())) { return false; } @@ -53,10 +53,10 @@ public class RolePolicyConditionProvider implements ResourcePolicyConditionProvi } @Override - public void validate() throws ResourcePolicyInvalidStateException { + public void validate() throws WorkflowInvalidStateException { expectedRoles.forEach(id -> { if (session.roles().getRoleById(session.getContext().getRealm(), id) == null) { - throw new ResourcePolicyInvalidStateException(String.format("Role with id %s does not exist.", id)); + throw new WorkflowInvalidStateException(String.format("Role with id %s does not exist.", id)); } }); } diff --git a/model/jpa/src/main/java/org/keycloak/models/policy/conditions/UserAttributePolicyConditionFactory.java b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/UserAttributeWorkflowConditionFactory.java similarity index 52% rename from model/jpa/src/main/java/org/keycloak/models/policy/conditions/UserAttributePolicyConditionFactory.java rename to model/jpa/src/main/java/org/keycloak/models/workflow/conditions/UserAttributeWorkflowConditionFactory.java index bc22afdca44..5cd63ebd230 100644 --- a/model/jpa/src/main/java/org/keycloak/models/policy/conditions/UserAttributePolicyConditionFactory.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/UserAttributeWorkflowConditionFactory.java @@ -1,19 +1,19 @@ -package org.keycloak.models.policy.conditions; +package org.keycloak.models.workflow.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; +import org.keycloak.models.workflow.WorkflowConditionProviderFactory; -public class UserAttributePolicyConditionFactory implements ResourcePolicyConditionProviderFactory { +public class UserAttributeWorkflowConditionFactory implements WorkflowConditionProviderFactory { public static final String ID = "user-attribute-condition"; @Override - public UserAttributePolicyConditionProvider create(KeycloakSession session, Map> config) { - return new UserAttributePolicyConditionProvider(session, config); + public UserAttributeWorkflowConditionProvider create(KeycloakSession session, Map> config) { + return new UserAttributeWorkflowConditionProvider(session, config); } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/policy/conditions/UserAttributePolicyConditionProvider.java b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/UserAttributeWorkflowConditionProvider.java similarity index 73% rename from model/jpa/src/main/java/org/keycloak/models/policy/conditions/UserAttributePolicyConditionProvider.java rename to model/jpa/src/main/java/org/keycloak/models/workflow/conditions/UserAttributeWorkflowConditionProvider.java index 2eda72220a2..82daee9a340 100644 --- a/model/jpa/src/main/java/org/keycloak/models/policy/conditions/UserAttributePolicyConditionProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/workflow/conditions/UserAttributeWorkflowConditionProvider.java @@ -1,4 +1,4 @@ -package org.keycloak.models.policy.conditions; +package org.keycloak.models.workflow.conditions; import static org.keycloak.common.util.CollectionUtil.collectionEquals; @@ -9,22 +9,22 @@ import java.util.Map.Entry; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; 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.workflow.WorkflowConditionProvider; +import org.keycloak.models.workflow.WorkflowEvent; +import org.keycloak.models.workflow.ResourceType; -public class UserAttributePolicyConditionProvider implements ResourcePolicyConditionProvider { +public class UserAttributeWorkflowConditionProvider implements WorkflowConditionProvider { private final Map> expectedAttributes; private final KeycloakSession session; - public UserAttributePolicyConditionProvider(KeycloakSession session, Map> expectedAttributes) { + public UserAttributeWorkflowConditionProvider(KeycloakSession session, Map> expectedAttributes) { this.session = session; this.expectedAttributes = expectedAttributes;; } @Override - public boolean evaluate(ResourcePolicyEvent event) { + public boolean evaluate(WorkflowEvent event) { if (!ResourceType.USERS.equals(event.getResourceType())) { return false; } diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-26.4.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-26.4.0.xml index 257a8d7a947..41fe6fc7bd6 100644 --- a/model/jpa/src/main/resources/META-INF/jpa-changelog-26.4.0.xml +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-26.4.0.xml @@ -28,35 +28,35 @@ - - + + - + - + - - + + + constraintName="PK_WORKFLOW_STEP_STATE" + tableName="WORKFLOW_STATE" + columnNames="RESOURCE_ID, WORKFLOW_ID" /> - - - + + + - + - + diff --git a/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.policy.ResourcePolicyConditionProviderFactory b/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.workflow.WorkflowConditionProviderFactory similarity index 68% rename from model/jpa/src/main/resources/META-INF/services/org.keycloak.models.policy.ResourcePolicyConditionProviderFactory rename to model/jpa/src/main/resources/META-INF/services/org.keycloak.models.workflow.WorkflowConditionProviderFactory index 6ceadd8a5b9..b551bb564b7 100644 --- a/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.policy.ResourcePolicyConditionProviderFactory +++ b/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.workflow.WorkflowConditionProviderFactory @@ -15,7 +15,7 @@ # limitations under the License. # -org.keycloak.models.policy.conditions.GroupMembershipPolicyConditionFactory -org.keycloak.models.policy.conditions.IdentityProviderPolicyConditionFactory -org.keycloak.models.policy.conditions.UserAttributePolicyConditionFactory -org.keycloak.models.policy.conditions.RolePolicyConditionFactory \ No newline at end of file +org.keycloak.models.workflow.conditions.GroupMembershipWorkflowConditionFactory +org.keycloak.models.workflow.conditions.IdentityProviderWorkflowConditionFactory +org.keycloak.models.workflow.conditions.UserAttributeWorkflowConditionFactory +org.keycloak.models.workflow.conditions.RoleWorkflowConditionFactory \ No newline at end of file diff --git a/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.policy.ResourcePolicyProviderFactory b/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.workflow.WorkflowProviderFactory similarity index 75% rename from model/jpa/src/main/resources/META-INF/services/org.keycloak.models.policy.ResourcePolicyProviderFactory rename to model/jpa/src/main/resources/META-INF/services/org.keycloak.models.workflow.WorkflowProviderFactory index 5d57446a2f9..7192cc2e484 100644 --- a/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.policy.ResourcePolicyProviderFactory +++ b/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.workflow.WorkflowProviderFactory @@ -15,6 +15,7 @@ # limitations under the License. # -org.keycloak.models.policy.UserCreationTimeResourcePolicyProviderFactory -org.keycloak.models.policy.UserSessionRefreshTimeResourcePolicyProviderFactory -org.keycloak.models.policy.EventBasedResourcePolicyProviderFactory \ No newline at end of file +org.keycloak.models.workflow.UserCreationTimeWorkflowProviderFactory +org.keycloak.models.workflow.UserSessionRefreshTimeWorkflowProviderFactory +org.keycloak.models.workflow.EventBasedWorkflowProviderFactory + diff --git a/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.policy.ResourcePolicyStateProviderFactory b/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.workflow.WorkflowStateProviderFactory similarity index 91% rename from model/jpa/src/main/resources/META-INF/services/org.keycloak.models.policy.ResourcePolicyStateProviderFactory rename to model/jpa/src/main/resources/META-INF/services/org.keycloak.models.workflow.WorkflowStateProviderFactory index 66f641dcd55..0edc484b4ee 100644 --- a/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.policy.ResourcePolicyStateProviderFactory +++ b/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.workflow.WorkflowStateProviderFactory @@ -15,4 +15,4 @@ # limitations under the License. # -org.keycloak.models.policy.JpaResourcePolicyStateProviderFactory +org.keycloak.models.workflow.JpaWorkflowStateProviderFactory diff --git a/model/jpa/src/main/resources/default-persistence.xml b/model/jpa/src/main/resources/default-persistence.xml index c2fd76046de..ed192c9a4d2 100644 --- a/model/jpa/src/main/resources/default-persistence.xml +++ b/model/jpa/src/main/resources/default-persistence.xml @@ -92,8 +92,8 @@ org.keycloak.storage.configuration.jpa.entity.ServerConfigEntity - - org.keycloak.models.policy.ResourcePolicyStateEntity + + org.keycloak.models.workflow.WorkflowStateEntity true diff --git a/model/storage-private/src/main/java/org/keycloak/models/policy/ResourcePolicyStateProvider.java b/model/storage-private/src/main/java/org/keycloak/models/workflow/WorkflowStateProvider.java similarity index 51% rename from model/storage-private/src/main/java/org/keycloak/models/policy/ResourcePolicyStateProvider.java rename to model/storage-private/src/main/java/org/keycloak/models/workflow/WorkflowStateProvider.java index 7629392716c..a284c3bf25f 100644 --- a/model/storage-private/src/main/java/org/keycloak/models/policy/ResourcePolicyStateProvider.java +++ b/model/storage-private/src/main/java/org/keycloak/models/workflow/WorkflowStateProvider.java @@ -15,16 +15,16 @@ * limitations under the License. */ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import org.keycloak.provider.Provider; import java.util.List; /** - * Interface serves as state check for policy actions. + * Interface serves as state check for workflow actions. */ -public interface ResourcePolicyStateProvider extends Provider { +public interface WorkflowStateProvider extends Provider { /** * Deletes the state records associated with the given {@code resourceId}. @@ -34,40 +34,40 @@ public interface ResourcePolicyStateProvider extends Provider { void removeByResource(String resourceId); /** - * Removes the record identified by the specified {@code policyId} and {@code resourceId}. - * @param policyId the id of the policy. + * Removes the record identified by the specified {@code workflowId} and {@code resourceId}. + * @param workflowId the id of the workflow. * @param resourceId the id of the resource. */ - void remove(String policyId, String resourceId); + void remove(String workflowId, String resourceId); /** - * Removes any record identified by the specified {@code policyId}. - * @param policyId the id of the policy. + * Removes any record identified by the specified {@code workflowId}. + * @param workflowId the id of the workflow. */ - void remove(String policyId); + void remove(String workflowId); /** * Deletes all state records associated with the current realm bound to the session. */ void removeAll(); - void scheduleAction(ResourcePolicy policy, ResourceAction action, String resourceId); + void scheduleStep(Workflow workflow, WorkflowStep step, String resourceId); - ScheduledAction getScheduledAction(String policyId, String resourceId); + ScheduledStep getScheduledStep(String workflowId, String resourceId); - List getScheduledActionsByResource(String resourceId); + List getScheduledStepsByResource(String resourceId); - List getScheduledActionsByPolicy(String policy); + List getScheduledStepsByWorkflow(String workflowId); - default List getScheduledActionsByPolicy(ResourcePolicy policy) { - if (policy == null) { + default List getScheduledStepsByWorkflow(Workflow workflow) { + if (workflow == null) { return List.of(); } - return getScheduledActionsByPolicy(policy.getId()); + return getScheduledStepsByWorkflow(workflow.getId()); } - List getDueScheduledActions(ResourcePolicy policy); + List getDueScheduledSteps(Workflow workflow); - record ScheduledAction (String policyId, String actionId, String resourceId) {}; + record ScheduledStep(String workflowId, String stepId, String resourceId) {} } diff --git a/model/storage-private/src/main/java/org/keycloak/models/policy/ResourcePolicyStateProviderFactory.java b/model/storage-private/src/main/java/org/keycloak/models/workflow/WorkflowStateProviderFactory.java similarity index 78% rename from model/storage-private/src/main/java/org/keycloak/models/policy/ResourcePolicyStateProviderFactory.java rename to model/storage-private/src/main/java/org/keycloak/models/workflow/WorkflowStateProviderFactory.java index 518a187d0cd..04fb1ac4c8d 100644 --- a/model/storage-private/src/main/java/org/keycloak/models/policy/ResourcePolicyStateProviderFactory.java +++ b/model/storage-private/src/main/java/org/keycloak/models/workflow/WorkflowStateProviderFactory.java @@ -15,18 +15,18 @@ * limitations under the License. */ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import org.keycloak.Config; import org.keycloak.common.Profile; import org.keycloak.provider.EnvironmentDependentProviderFactory; import org.keycloak.provider.ProviderFactory; -public interface ResourcePolicyStateProviderFactory extends ProviderFactory, EnvironmentDependentProviderFactory { +public interface WorkflowStateProviderFactory extends ProviderFactory, EnvironmentDependentProviderFactory { @Override default boolean isSupported(Config.Scope config) { - return Profile.isFeatureEnabled(Profile.Feature.RESOURCE_LIFECYCLE); + return Profile.isFeatureEnabled(Profile.Feature.WORKFLOWS); } } diff --git a/model/storage-private/src/main/java/org/keycloak/models/policy/ResourcePolicyStateSpi.java b/model/storage-private/src/main/java/org/keycloak/models/workflow/WorkflowStateSpi.java similarity index 81% rename from model/storage-private/src/main/java/org/keycloak/models/policy/ResourcePolicyStateSpi.java rename to model/storage-private/src/main/java/org/keycloak/models/workflow/WorkflowStateSpi.java index ed590b634cd..6f75af55fb6 100644 --- a/model/storage-private/src/main/java/org/keycloak/models/policy/ResourcePolicyStateSpi.java +++ b/model/storage-private/src/main/java/org/keycloak/models/workflow/WorkflowStateSpi.java @@ -15,15 +15,15 @@ * limitations under the License. */ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import org.keycloak.provider.Provider; import org.keycloak.provider.ProviderFactory; import org.keycloak.provider.Spi; -public class ResourcePolicyStateSpi implements Spi { +public class WorkflowStateSpi implements Spi { - public static final String NAME = "resource-policy-state"; + public static final String NAME = "workflow-state"; @Override public boolean isInternal() { @@ -37,11 +37,11 @@ public class ResourcePolicyStateSpi implements Spi { @Override public Class getProviderClass() { - return ResourcePolicyStateProvider.class; + return WorkflowStateProvider.class; } @Override public Class getProviderFactoryClass() { - return ResourcePolicyStateProviderFactory.class; + return WorkflowStateProviderFactory.class; } } diff --git a/model/storage-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/model/storage-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi index 5589d6b415a..997e7bcb481 100644 --- a/model/storage-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/model/storage-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -26,4 +26,4 @@ org.keycloak.storage.clientscope.ClientScopeStorageProviderSpi org.keycloak.models.session.UserSessionPersisterSpi org.keycloak.models.session.RevokedTokenPersisterSpi org.keycloak.cluster.ClusterSpi -org.keycloak.models.policy.ResourcePolicyStateSpi +org.keycloak.models.workflow.WorkflowStateSpi diff --git a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourcePolicyInvalidStateException.java b/server-spi-private/src/main/java/org/keycloak/models/policy/ResourcePolicyInvalidStateException.java deleted file mode 100644 index efa05e8401c..00000000000 --- a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourcePolicyInvalidStateException.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.keycloak.models.policy; - -import org.keycloak.models.ModelValidationException; - -public class ResourcePolicyInvalidStateException extends ModelValidationException { - - public ResourcePolicyInvalidStateException(String message) { - super(message); - } -} diff --git a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourceOperationType.java b/server-spi-private/src/main/java/org/keycloak/models/workflow/ResourceOperationType.java similarity index 98% rename from server-spi-private/src/main/java/org/keycloak/models/policy/ResourceOperationType.java rename to server-spi-private/src/main/java/org/keycloak/models/workflow/ResourceOperationType.java index ff7b1372f5d..76a0cf83e65 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourceOperationType.java +++ b/server-spi-private/src/main/java/org/keycloak/models/workflow/ResourceOperationType.java @@ -1,4 +1,4 @@ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import java.util.List; diff --git a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourceType.java b/server-spi-private/src/main/java/org/keycloak/models/workflow/ResourceType.java similarity index 85% rename from server-spi-private/src/main/java/org/keycloak/models/policy/ResourceType.java rename to server-spi-private/src/main/java/org/keycloak/models/workflow/ResourceType.java index 8f8ccb0aab6..7ac2435c39a 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourceType.java +++ b/server-spi-private/src/main/java/org/keycloak/models/workflow/ResourceType.java @@ -15,9 +15,9 @@ * limitations under the License. */ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; -import static org.keycloak.models.policy.ResourceOperationType.toOperationType; +import static org.keycloak.models.workflow.ResourceOperationType.toOperationType; import org.keycloak.events.Event; import org.keycloak.events.EventType; @@ -54,32 +54,32 @@ public enum ResourceType { this.resourceResolver = resourceResolver; } - public ResourcePolicyEvent toEvent(AdminEvent event) { + public WorkflowEvent toEvent(AdminEvent event) { if (Objects.equals(this.supportedAdminResourceType, event.getResourceType()) && this.supportedAdminOperationTypes.contains(event.getOperationType())) { ResourceOperationType resourceOperationType = toOperationType(event.getOperationType()); if (resourceOperationType != null) { - return new ResourcePolicyEvent(this, resourceOperationType, event.getResourceId(), event); + return new WorkflowEvent(this, resourceOperationType, event.getResourceId(), event); } } return null; } - public ResourcePolicyEvent toEvent(Event event) { + public WorkflowEvent toEvent(Event event) { if (this.supportedEventTypes.contains(event.getType())) { ResourceOperationType resourceOperationType = toOperationType(event.getType()); String resourceId = switch (this) { case USERS -> event.getUserId(); }; if (resourceOperationType != null && resourceId != null) { - return new ResourcePolicyEvent(this, resourceOperationType, event.getUserId(), event); + return new WorkflowEvent(this, resourceOperationType, event.getUserId(), event); } } return null; } - public ResourcePolicyEvent toEvent(ProviderEvent event) { + public WorkflowEvent toEvent(ProviderEvent event) { ResourceOperationType resourceOperationType = toOperationType(event.getClass()); if (resourceOperationType == null) { @@ -92,7 +92,7 @@ public enum ResourceType { return null; } - return new ResourcePolicyEvent(this, resourceOperationType, resourceId, event); + return new WorkflowEvent(this, resourceOperationType, resourceId, event); } public Object resolveResource(KeycloakSession session, String id) { diff --git a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourcePolicy.java b/server-spi-private/src/main/java/org/keycloak/models/workflow/Workflow.java similarity index 88% rename from server-spi-private/src/main/java/org/keycloak/models/policy/ResourcePolicy.java rename to server-spi-private/src/main/java/org/keycloak/models/workflow/Workflow.java index 670c13ae366..a63bb0361c9 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourcePolicy.java +++ b/server-spi-private/src/main/java/org/keycloak/models/workflow/Workflow.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import java.util.List; import java.util.Map; @@ -23,30 +23,30 @@ import java.util.Map; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.component.ComponentModel; -public class ResourcePolicy { +public class Workflow { private MultivaluedHashMap config; private String providerId; private String id; private Long notBefore; - public ResourcePolicy() { + public Workflow() { // reflection } - public ResourcePolicy(String providerId) { + public Workflow(String providerId) { this.providerId = providerId; this.id = null; this.config = null; } - public ResourcePolicy(ComponentModel c) { + public Workflow(ComponentModel c) { this.id = c.getId(); this.providerId = c.getProviderId(); this.config = c.getConfig(); } - public ResourcePolicy(String providerId, Map> config) { + public Workflow(String providerId, Map> config) { this.providerId = providerId; MultivaluedHashMap c = new MultivaluedHashMap<>(); config.forEach(c::addAll); diff --git a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourcePolicyConditionProvider.java b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowConditionProvider.java similarity index 62% rename from server-spi-private/src/main/java/org/keycloak/models/policy/ResourcePolicyConditionProvider.java rename to server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowConditionProvider.java index 61dff33a407..650fafcdd5c 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourcePolicyConditionProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowConditionProvider.java @@ -1,4 +1,4 @@ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; @@ -6,13 +6,13 @@ import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; import org.keycloak.provider.Provider; -public interface ResourcePolicyConditionProvider extends Provider { +public interface WorkflowConditionProvider extends Provider { - boolean evaluate(ResourcePolicyEvent event); + boolean evaluate(WorkflowEvent event); default Predicate toPredicate(CriteriaBuilder cb, CriteriaQuery query, Root userRoot) { return null; } - void validate() throws ResourcePolicyInvalidStateException; + void validate() throws WorkflowInvalidStateException; } diff --git a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourcePolicyConditionProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowConditionProviderFactory.java similarity index 69% rename from server-spi-private/src/main/java/org/keycloak/models/policy/ResourcePolicyConditionProviderFactory.java rename to server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowConditionProviderFactory.java index c24ea415f1f..d87f53a0881 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourcePolicyConditionProviderFactory.java +++ b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowConditionProviderFactory.java @@ -1,4 +1,4 @@ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import java.util.List; import java.util.Map; @@ -9,7 +9,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.provider.EnvironmentDependentProviderFactory; import org.keycloak.provider.ProviderFactory; -public interface ResourcePolicyConditionProviderFactory

extends ProviderFactory

, EnvironmentDependentProviderFactory { +public interface WorkflowConditionProviderFactory

extends ProviderFactory

, EnvironmentDependentProviderFactory { P create(KeycloakSession session, Map> config); @@ -20,6 +20,6 @@ public interface ResourcePolicyConditionProviderFactory

getProviderClass() { - return ResourcePolicyConditionProvider.class; + return WorkflowConditionProvider.class; } @Override public Class getProviderFactoryClass() { - return ResourcePolicyConditionProviderFactory.class; + return WorkflowConditionProviderFactory.class; } } diff --git a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourcePolicyEvent.java b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowEvent.java similarity index 75% rename from server-spi-private/src/main/java/org/keycloak/models/policy/ResourcePolicyEvent.java rename to server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowEvent.java index 78751060d5d..4d788a782c5 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourcePolicyEvent.java +++ b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowEvent.java @@ -1,13 +1,13 @@ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; -public class ResourcePolicyEvent { +public class WorkflowEvent { private final ResourceType type; private final ResourceOperationType operation; private final String resourceId; private final Object event; - public ResourcePolicyEvent(ResourceType type, ResourceOperationType operation, String resourceId, Object event) { + public WorkflowEvent(ResourceType type, ResourceOperationType operation, String resourceId, Object event) { this.type = type; this.operation = operation; this.resourceId = resourceId; diff --git a/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowInvalidStateException.java b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowInvalidStateException.java new file mode 100644 index 00000000000..6abd5317191 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowInvalidStateException.java @@ -0,0 +1,10 @@ +package org.keycloak.models.workflow; + +import org.keycloak.models.ModelValidationException; + +public class WorkflowInvalidStateException extends ModelValidationException { + + public WorkflowInvalidStateException(String message) { + super(message); + } +} diff --git a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourcePolicyProvider.java b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowProvider.java similarity index 53% rename from server-spi-private/src/main/java/org/keycloak/models/policy/ResourcePolicyProvider.java rename to server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowProvider.java index f6ee6e5aa25..b34c37944f9 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourcePolicyProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowProvider.java @@ -15,19 +15,19 @@ * limitations under the License. */ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import java.util.List; import org.keycloak.provider.Provider; -public interface ResourcePolicyProvider extends Provider { +public interface WorkflowProvider extends Provider { /** - * Finds all resources that are eligible for the first action of a policy. + * Finds all resources that are eligible for the first action of a workflow. * * @return A list of eligible resource IDs. */ - List getEligibleResourcesForInitialAction(); + List getEligibleResourcesForInitialStep(); /** * Checks if the provider supports resources of the specified type. @@ -38,45 +38,45 @@ public interface ResourcePolicyProvider extends Provider { boolean supports(ResourceType type); /** - * Indicates whether the policy supports being activated for a resource based on the event or not. If {@code true}, the - * policy will be activated for the resource. For scheduled policies, this means the first action will be scheduled. For - * immediate policies, this means all actions will be executed right away. + * Indicates whether the workflow supports being activated for a resource based on the event or not. If {@code true}, the + * workflow will be activated for the resource. For scheduled workflows, this means the first action will be scheduled. For + * immediate workflows, this means all actions will be executed right away. * - * At the very least, implementations should validate the event's resource type and operation to ensure the policy will + * At the very least, implementations should validate the event's resource type and operation to ensure the workflow will * only be activated on expected operations being performed on the expected type. * - * @param event a {@link ResourcePolicyEvent} containing details of the event that was triggered such as operation + * @param event a {@link WorkflowEvent} containing details of the event that was triggered such as operation * (CREATE, LOGIN, etc.), the resource type, and the resource id. - * @return {@code true} if the policy can be activated based on the received event; {@code false} otherwise. + * @return {@code true} if the workflow can be activated based on the received event; {@code false} otherwise. */ - boolean activateOnEvent(ResourcePolicyEvent event); + boolean activateOnEvent(WorkflowEvent event); /** - * Indicates whether the policy supports being reset (i.e. go back to the first action) based on the event received or not. - * By default, this method returns false as most policies won't support this kind of flow, but specific policies such + * Indicates whether the workflow supports being reset (i.e. go back to the first action) based on the event received or not. + * By default, this method returns false as most workflows won't support this kind of flow, but specific workflows such * as one based on a resource's last updated time, or last used time, can signal that they expect the process to start * over once the timestamp they are based on is updated. * - * At the very least, implementations should validate the event's resource type and operation to ensure the policy will + * At the very least, implementations should validate the event's resource type and operation to ensure the workflow will * only be reset on expected operations being performed on the expected type. * - * @param event a {@link ResourcePolicyEvent} containing details of the event that was triggered such as operation + * @param event a {@link WorkflowEvent} containing details of the event that was triggered such as operation * (CREATE, LOGIN, etc.), the resource type, and the resource id. - * @return {@code true} if the policy supports resetting the flow based on the received event; {@code false} otherwise. + * @return {@code true} if the workflow supports resetting the flow based on the received event; {@code false} otherwise. */ - boolean resetOnEvent(ResourcePolicyEvent event); + boolean resetOnEvent(WorkflowEvent event); /** - * Indicates whether the policy supports being deactivated for a resource based on the event or not. If {@code true}, the - * policy will be deactivated for the resource, meaning any existing scheduled actions will be removed and no further + * Indicates whether the workflow supports being deactivated for a resource based on the event or not. If {@code true}, the + * workflow will be deactivated for the resource, meaning any existing scheduled actions will be removed and no further * actions will be executed. * - * At the very least, implementations should validate the event's resource type and operation to ensure the policy will + * At the very least, implementations should validate the event's resource type and operation to ensure the workflow will * only be deactivated on expected operations being performed on the expected type. * - * @param event a {@link ResourcePolicyEvent} containing details of the event that was triggered such as operation + * @param event a {@link WorkflowEvent} containing details of the event that was triggered such as operation * (CREATE, LOGIN, etc.), the resource type, and the resource id. - * @return {@code true} if the policy can be deactivated based on the received event; {@code false} otherwise. + * @return {@code true} if the workflow can be deactivated based on the received event; {@code false} otherwise. */ - boolean deactivateOnEvent(ResourcePolicyEvent event); + boolean deactivateOnEvent(WorkflowEvent event); } diff --git a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourcePolicyProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowProviderFactory.java similarity index 76% rename from server-spi-private/src/main/java/org/keycloak/models/policy/ResourcePolicyProviderFactory.java rename to server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowProviderFactory.java index 0b1961142a2..b76d8732d05 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourcePolicyProviderFactory.java +++ b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowProviderFactory.java @@ -15,17 +15,17 @@ * limitations under the License. */ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import org.keycloak.Config; import org.keycloak.common.Profile; import org.keycloak.component.ComponentFactory; import org.keycloak.provider.EnvironmentDependentProviderFactory; -public interface ResourcePolicyProviderFactory

extends ComponentFactory, EnvironmentDependentProviderFactory { +public interface WorkflowProviderFactory

extends ComponentFactory, EnvironmentDependentProviderFactory { @Override default boolean isSupported(Config.Scope config) { - return Profile.isFeatureEnabled(Profile.Feature.RESOURCE_LIFECYCLE); + return Profile.isFeatureEnabled(Profile.Feature.WORKFLOWS); } } diff --git a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourceActionSpi.java b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowSpi.java similarity index 83% rename from server-spi-private/src/main/java/org/keycloak/models/policy/ResourceActionSpi.java rename to server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowSpi.java index 30f9b595e72..d58c0b826e6 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourceActionSpi.java +++ b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowSpi.java @@ -15,15 +15,15 @@ * limitations under the License. */ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import org.keycloak.provider.Provider; import org.keycloak.provider.ProviderFactory; import org.keycloak.provider.Spi; -public class ResourceActionSpi implements Spi { +public class WorkflowSpi implements Spi { - public static String NAME = "rlm-action"; + public static final String NAME = "workflow"; @Override public boolean isInternal() { @@ -37,11 +37,11 @@ public class ResourceActionSpi implements Spi { @Override public Class getProviderClass() { - return ResourceActionProvider.class; + return WorkflowProvider.class; } @Override public Class getProviderFactoryClass() { - return ResourceActionProviderFactory.class; + return WorkflowProviderFactory.class; } } diff --git a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourceAction.java b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowStep.java similarity index 73% rename from server-spi-private/src/main/java/org/keycloak/models/policy/ResourceAction.java rename to server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowStep.java index 8fff06427e2..e53a1664b72 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourceAction.java +++ b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowStep.java @@ -15,14 +15,14 @@ * limitations under the License. */ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import java.util.List; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.component.ComponentModel; -public class ResourceAction implements Comparable { +public class WorkflowStep implements Comparable { public static final String AFTER_KEY = "after"; public static final String PRIORITY_KEY = "priority"; @@ -30,23 +30,19 @@ public class ResourceAction implements Comparable { private String id; private String providerId; private MultivaluedHashMap config; - private List actions = List.of(); + private List steps = List.of(); - public ResourceAction() { + public WorkflowStep() { // reflection } - public ResourceAction(String providerId) { - this.providerId = providerId; - } - - public ResourceAction(String providerId, MultivaluedHashMap config, List actions) { + public WorkflowStep(String providerId, MultivaluedHashMap config, List steps) { this.providerId = providerId; this.config = config; - this.actions = actions; + this.steps = steps; } - public ResourceAction(ComponentModel model) { + public WorkflowStep(ComponentModel model) { this.id = model.getId(); this.providerId = model.getProviderId(); this.config = model.getConfig(); @@ -60,7 +56,7 @@ public class ResourceAction implements Comparable { return providerId; } - public ResourceAction setConfig(String key, String value) { + public WorkflowStep setConfig(String key, String value) { if (config == null) { config = new MultivaluedHashMap<>(); } @@ -79,7 +75,6 @@ public class ResourceAction implements Comparable { setConfig(PRIORITY_KEY, String.valueOf(priority)); } - // todo, do not expose the priority to user?? in export etc. the order of actions should define the priority public int getPriority() { String value = getConfig().getFirst(PRIORITY_KEY); if (value == null) { @@ -100,19 +95,19 @@ public class ResourceAction implements Comparable { return Long.valueOf(getConfig().getFirstOrDefault(AFTER_KEY, "0")); } - public List getActions() { - if (actions == null) { + public List getSteps() { + if (steps == null) { return List.of(); } - return actions; + return steps; } - public void setActions(List actions) { - this.actions = actions; + public void setSteps(List steps) { + this.steps = steps; } @Override - public int compareTo(ResourceAction other) { + public int compareTo(WorkflowStep other) { return Integer.compare(this.getPriority(), other.getPriority()); } } diff --git a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourceActionProvider.java b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowStepProvider.java similarity index 86% rename from server-spi-private/src/main/java/org/keycloak/models/policy/ResourceActionProvider.java rename to server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowStepProvider.java index 9890fbfba41..59f4c004e4f 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourceActionProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowStepProvider.java @@ -15,14 +15,13 @@ * limitations under the License. */ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import java.util.List; import org.keycloak.provider.Provider; -public interface ResourceActionProvider extends Provider { +public interface WorkflowStepProvider extends Provider { void run(List resourceIds); - boolean isRunnable(); } diff --git a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourceActionProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowStepProviderFactory.java similarity index 77% rename from server-spi-private/src/main/java/org/keycloak/models/policy/ResourceActionProviderFactory.java rename to server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowStepProviderFactory.java index 4051efeaf62..3112ab7f3ca 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourceActionProviderFactory.java +++ b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowStepProviderFactory.java @@ -15,19 +15,19 @@ * limitations under the License. */ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import org.keycloak.Config; import org.keycloak.common.Profile; import org.keycloak.component.ComponentFactory; import org.keycloak.provider.EnvironmentDependentProviderFactory; -public interface ResourceActionProviderFactory

extends ComponentFactory, EnvironmentDependentProviderFactory { +public interface WorkflowStepProviderFactory

extends ComponentFactory, EnvironmentDependentProviderFactory { ResourceType getType(); @Override default boolean isSupported(Config.Scope config) { - return Profile.isFeatureEnabled(Profile.Feature.RESOURCE_LIFECYCLE); + return Profile.isFeatureEnabled(Profile.Feature.WORKFLOWS); } } diff --git a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourcePolicySpi.java b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowStepSpi.java similarity index 82% rename from server-spi-private/src/main/java/org/keycloak/models/policy/ResourcePolicySpi.java rename to server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowStepSpi.java index 7af7b54852c..26d67559560 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourcePolicySpi.java +++ b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowStepSpi.java @@ -15,15 +15,15 @@ * limitations under the License. */ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import org.keycloak.provider.Provider; import org.keycloak.provider.ProviderFactory; import org.keycloak.provider.Spi; -public class ResourcePolicySpi implements Spi { +public class WorkflowStepSpi implements Spi { - public static final String NAME = "rlm-policy"; + public static String NAME = "workflow-step"; @Override public boolean isInternal() { @@ -37,11 +37,11 @@ public class ResourcePolicySpi implements Spi { @Override public Class getProviderClass() { - return ResourcePolicyProvider.class; + return WorkflowStepProvider.class; } @Override public Class getProviderFactoryClass() { - return ResourcePolicyProviderFactory.class; + return WorkflowStepProviderFactory.class; } } diff --git a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi index 4737acd0bd6..bd11d1c0d69 100755 --- a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -105,7 +105,7 @@ org.keycloak.cookie.CookieSpi org.keycloak.organization.OrganizationSpi org.keycloak.securityprofile.SecurityProfileSpi org.keycloak.logging.MappedDiagnosticContextSpi -org.keycloak.models.policy.ResourceActionSpi -org.keycloak.models.policy.ResourcePolicySpi -org.keycloak.models.policy.ResourcePolicyConditionSpi +org.keycloak.models.workflow.WorkflowStepSpi +org.keycloak.models.workflow.WorkflowSpi +org.keycloak.models.workflow.WorkflowConditionSpi org.keycloak.protocol.oidc.rar.AuthorizationDetailsProcessorSpi diff --git a/services/src/main/java/org/keycloak/models/policy/AdhocResourcePolicyEvent.java b/services/src/main/java/org/keycloak/models/policy/AdhocResourcePolicyEvent.java deleted file mode 100644 index d13b8dc49c0..00000000000 --- a/services/src/main/java/org/keycloak/models/policy/AdhocResourcePolicyEvent.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.keycloak.models.policy; - -final class AdhocResourcePolicyEvent extends ResourcePolicyEvent { - - AdhocResourcePolicyEvent(ResourceType type, String resourceId) { - super(type, ResourceOperationType.AD_HOC, resourceId, null); - } -} diff --git a/services/src/main/java/org/keycloak/models/policy/AggregatedActionProvider.java b/services/src/main/java/org/keycloak/models/policy/AggregatedActionProvider.java deleted file mode 100644 index 229fb7ba804..00000000000 --- a/services/src/main/java/org/keycloak/models/policy/AggregatedActionProvider.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.keycloak.models.policy; - -import java.util.List; - -import org.jboss.logging.Logger; -import org.keycloak.component.ComponentModel; -import org.keycloak.models.KeycloakSession; - -public class AggregatedActionProvider implements ResourceActionProvider { - - private final KeycloakSession session; - private final ComponentModel model; - private final Logger log = Logger.getLogger(AggregatedActionProvider.class); - - public AggregatedActionProvider(KeycloakSession session, ComponentModel model) { - this.session = session; - this.model = model; - } - - @Override - public void close() { - } - - @Override - public void run(List userIds) { - ResourcePolicyManager manager = new ResourcePolicyManager(session); - List actions = manager.getActionById(session, model.getId()) - .getActions().stream() - .map(manager::getActionProvider) - .toList(); - - for (String userId : userIds) { - for (ResourceActionProvider action : actions) { - try { - action.run(List.of(userId)); - } catch (Exception e) { - log.errorf(e, "Failed to execute action %s for user %s", model.getProviderId(), userId); - } - } - } - } - - @Override - public boolean isRunnable() { - return true; - } -} diff --git a/services/src/main/java/org/keycloak/models/policy/ResourcePolicyActionRunnerSuccessEvent.java b/services/src/main/java/org/keycloak/models/policy/ResourcePolicyActionRunnerSuccessEvent.java deleted file mode 100644 index 90a667593ed..00000000000 --- a/services/src/main/java/org/keycloak/models/policy/ResourcePolicyActionRunnerSuccessEvent.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.keycloak.models.policy; - -import org.keycloak.models.KeycloakSession; -import org.keycloak.provider.ProviderEvent; - -public final class ResourcePolicyActionRunnerSuccessEvent implements ProviderEvent { - - private final KeycloakSession session; - - public ResourcePolicyActionRunnerSuccessEvent(KeycloakSession session) { - this.session = session; - } - - public KeycloakSession getSession() { - return session; - } -} diff --git a/services/src/main/java/org/keycloak/models/policy/ResourcePolicyManager.java b/services/src/main/java/org/keycloak/models/policy/ResourcePolicyManager.java deleted file mode 100644 index f3f70ae7eaa..00000000000 --- a/services/src/main/java/org/keycloak/models/policy/ResourcePolicyManager.java +++ /dev/null @@ -1,369 +0,0 @@ -/* - * Copyright 2025 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.models.policy; - -import static java.util.Optional.ofNullable; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Stream; - -import jakarta.ws.rs.BadRequestException; -import org.jboss.logging.Logger; -import org.keycloak.common.Profile; -import org.keycloak.common.Profile.Feature; -import org.keycloak.common.util.MultivaluedHashMap; -import org.keycloak.component.ComponentFactory; -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.models.utils.KeycloakModelUtils; -import org.keycloak.representations.resources.policies.ResourcePolicyActionRepresentation; -import org.keycloak.representations.resources.policies.ResourcePolicyConditionRepresentation; -import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation; - -public class ResourcePolicyManager { - - private static final Logger log = Logger.getLogger(ResourcePolicyManager.class); - - private final KeycloakSession session; - private final ResourcePolicyStateProvider policyStateProvider; - - public static boolean isFeatureEnabled() { - return Profile.isFeatureEnabled(Feature.RESOURCE_LIFECYCLE); - } - - public ResourcePolicyManager(KeycloakSession session) { - this.session = session; - this.policyStateProvider = session.getKeycloakSessionFactory().getProviderFactory(ResourcePolicyStateProvider.class).create(session); - } - - public ResourcePolicy addPolicy(String providerId) { - return addPolicy(new ResourcePolicy(providerId)); - } - - public ResourcePolicy addPolicy(String providerId, Map> config) { - return addPolicy(new ResourcePolicy(providerId, config)); - } - - public ResourcePolicy addPolicy(ResourcePolicy policy) { - RealmModel realm = getRealm(); - ComponentModel model = new ComponentModel(); - - model.setParentId(realm.getId()); - model.setProviderId(policy.getProviderId()); - model.setProviderType(ResourcePolicyProvider.class.getName()); - - MultivaluedHashMap config = policy.getConfig(); - - if (config != null) { - model.setConfig(config); - } - - return new ResourcePolicy(realm.addComponentModel(model)); - } - - // This method takes an ordered list of actions. First action in the list has the highest priority, last action has the lowest priority - public void createActions(ResourcePolicy policy, List actions) { - for (int i = 0; i < actions.size(); i++) { - ResourceAction action = actions.get(i); - - // assign priority based on index. - action.setPriority(i + 1); - - List subActions = Optional.ofNullable(action.getActions()).orElse(List.of()); - - // persist the new action component. - action = addAction(policy.getId(), action); - - for (int j = 0; j < subActions.size(); j++) { - ResourceAction subAction = subActions.get(j); - // assign priority based on index. - subAction.setPriority(j + 1); - addAction(action.getId(), subAction); - } - } - } - - private ResourceAction addAction(String parentId, ResourceAction action) { - RealmModel realm = getRealm(); - ComponentModel policyModel = realm.getComponent(parentId); - ComponentModel actionModel = new ComponentModel(); - - actionModel.setId(action.getId());//need to keep stable UUIDs not to break a link in state table - actionModel.setParentId(policyModel.getId()); - actionModel.setProviderId(action.getProviderId()); - actionModel.setProviderType(ResourceActionProvider.class.getName()); - actionModel.setConfig(action.getConfig()); - - return new ResourceAction(realm.addComponentModel(actionModel)); - } - - public List getPolicies() { - RealmModel realm = getRealm(); - return realm.getComponentsStream(realm.getId(), ResourcePolicyProvider.class.getName()) - .map(ResourcePolicy::new).toList(); - } - - public List getActions(String policyId) { - return getActionsStream(policyId).toList(); - } - - public Stream getActionsStream(String parentId) { - RealmModel realm = session.getContext().getRealm(); - return realm.getComponentsStream(parentId, ResourceActionProvider.class.getName()) - .map(this::toResourceAction).sorted(); - } - - private ResourceAction toResourceAction(ComponentModel model) { - ResourceAction action = new ResourceAction(model); - - action.setActions(getActions(action.getId())); - - return action; - } - - public ResourceAction getActionById(KeycloakSession session, String id) { - RealmModel realm = session.getContext().getRealm(); - ComponentModel component = realm.getComponent(id); - - if (component == null) { - return null; - } - - return toResourceAction(component); - } - - private ResourceAction getFirstAction(ResourcePolicy policy) { - ResourceAction action = getActions(policy.getId()).get(0); - Long notBefore = policy.getNotBefore(); - - if (notBefore != null) { - action.setAfter(notBefore); - } - - return action; - } - - private ResourcePolicyProvider getPolicyProvider(ResourcePolicy policy) { - ComponentFactory factory = (ComponentFactory) session.getKeycloakSessionFactory() - .getProviderFactory(ResourcePolicyProvider.class, policy.getProviderId()); - return (ResourcePolicyProvider) factory.create(session, getRealm().getComponent(policy.getId())); - } - - public ResourceActionProvider getActionProvider(ResourceAction action) { - ComponentFactory actionFactory = (ComponentFactory) session.getKeycloakSessionFactory() - .getProviderFactory(ResourceActionProvider.class, action.getProviderId()); - return (ResourceActionProvider) actionFactory.create(session, getRealm().getComponent(action.getId())); - } - - private RealmModel getRealm() { - return session.getContext().getRealm(); - } - - public void removePolicies() { - RealmModel realm = getRealm(); - realm.getComponentsStream(realm.getId(), ResourcePolicyProvider.class.getName()).forEach(policy -> { - realm.getComponentsStream(policy.getId(), ResourceActionProvider.class.getName()).forEach(realm::removeComponent); - realm.removeComponent(policy); - }); - } - - public void scheduleAllEligibleResources(ResourcePolicy policy) { - if (policy.isEnabled()) { - ResourcePolicyProvider provider = getPolicyProvider(policy); - provider.getEligibleResourcesForInitialAction() - .forEach(resourceId -> processEvent(List.of(policy), new AdhocResourcePolicyEvent(ResourceType.USERS, resourceId))); - } - } - - public void processEvent(ResourcePolicyEvent event) { - processEvent(getPolicies(), event); - } - - public void processEvent(List policies, ResourcePolicyEvent event) { - List currentlyAssignedPolicies = policyStateProvider.getScheduledActionsByResource(event.getResourceId()) - .stream().map(ScheduledAction::policyId).toList(); - - // iterate through the policies, and for those not yet assigned to the user check if they can be assigned - policies.stream() - .filter(policy -> policy.isEnabled() && !getActions(policy.getId()).isEmpty()) - .forEach(policy -> { - ResourcePolicyProvider provider = getPolicyProvider(policy); - try { - if (!currentlyAssignedPolicies.contains(policy.getId())) { - // if policy is not active for the resource, check if the provider allows activating based on the event - if (provider.activateOnEvent(event)) { - if (policy.isScheduled()) { - // policy is scheduled, so we schedule the first action - log.debugf("Scheduling first action of policy %s for resource %s based on event %s", - policy.getId(), event.getResourceId(), event.getOperation()); - policyStateProvider.scheduleAction(policy, getFirstAction(policy), event.getResourceId()); - } else { - // policy is not scheduled, so we run all actions immediately - log.debugf("Running all actions of policy %s for resource %s based on event %s", - policy.getId(), event.getResourceId(), event.getOperation()); - KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), session.getContext(), s -> { - getActions(policy.getId()).forEach(action -> getActionProvider(action).run(List.of(event.getResourceId()))); - }); - } - } - } else { - if (provider.resetOnEvent(event)) { - policyStateProvider.scheduleAction(policy, getFirstAction(policy), event.getResourceId()); - } else if (provider.deactivateOnEvent(event)) { - policyStateProvider.remove(policy.getId(), event.getResourceId()); - } - } - } catch (ResourcePolicyInvalidStateException e) { - policy.getConfig().putSingle("enabled", "false"); - policy.getConfig().putSingle("validation_error", e.getMessage()); - updatePolicy(policy, policy.getConfig()); - log.debugf("Policy %s was disabled due to: %s", policy.getId(), e.getMessage()); - } - }); - } - - public void runScheduledActions() { - this.getPolicies().stream().filter(ResourcePolicy::isEnabled).forEach(policy -> { - - for (ScheduledAction scheduled : policyStateProvider.getDueScheduledActions(policy)) { - List actions = getActions(policy.getId()); - - for (int i = 0; i < actions.size(); i++) { - ResourceAction currentAction = actions.get(i); - - if (currentAction.getId().equals(scheduled.actionId())) { - 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); - policyStateProvider.scheduleAction(policy, nextAction, scheduled.resourceId()); - } else { - // this was the last action, check if the policy is recurring - i.e. if we need to schedule the first action again - if (policy.isRecurring()) { - ResourceAction firstAction = getFirstAction(policy); - policyStateProvider.scheduleAction(policy, firstAction, scheduled.resourceId()); - } else { - // not recurring, remove the state record - policyStateProvider.remove(policy.getId(), scheduled.resourceId()); - } - } - } - } - } - }); - } - - public void removePolicy(String id) { - RealmModel realm = getRealm(); - realm.getComponentsStream(realm.getId(), ResourcePolicyProvider.class.getName()) - .filter(policy -> policy.getId().equals(id)) - .forEach(policy -> { - realm.getComponentsStream(policy.getId(), ResourceActionProvider.class.getName()).forEach(realm::removeComponent); - realm.removeComponent(policy); - }); - policyStateProvider.remove(id); - } - - public ResourcePolicy getPolicy(String id) { - return new ResourcePolicy(getPolicyComponent(id)); - } - - public void updatePolicy(ResourcePolicy policy, MultivaluedHashMap config) { - ComponentModel component = getPolicyComponent(policy.getId()); - component.setConfig(config); - getRealm().updateComponent(component); - } - - private ComponentModel getPolicyComponent(String id) { - ComponentModel component = getRealm().getComponent(id); - - if (component == null || !ResourcePolicyProvider.class.getName().equals(component.getProviderType())) { - throw new BadRequestException("Not a valid resource policy: " + id); - } - - return component; - } - - public ResourcePolicyRepresentation toRepresentation(ResourcePolicy policy) { - ResourcePolicyRepresentation rep = new ResourcePolicyRepresentation(policy.getId(), policy.getProviderId(), policy.getConfig()); - - for (ResourceAction action : getActions(policy.getId())) { - rep.addAction(toRepresentation(action)); - } - - return rep; - } - - private ResourcePolicyActionRepresentation toRepresentation(ResourceAction action) { - List actions = action.getActions().stream().map(this::toRepresentation).toList(); - return new ResourcePolicyActionRepresentation(action.getId(), action.getProviderId(), action.getConfig(), actions); - } - - public ResourcePolicy toModel(ResourcePolicyRepresentation rep) { - MultivaluedHashMap config = ofNullable(rep.getConfig()).orElse(new MultivaluedHashMap<>()); - List conditions = ofNullable(rep.getConditions()).orElse(List.of()); - - for (ResourcePolicyConditionRepresentation condition : conditions) { - String conditionProviderId = condition.getProviderId(); - config.computeIfAbsent("conditions", key -> new ArrayList<>()).add(conditionProviderId); - - for (Entry> configEntry : condition.getConfig().entrySet()) { - config.put(conditionProviderId + "." + configEntry.getKey(), configEntry.getValue()); - } - } - - ResourcePolicy policy = addPolicy(rep.getProviderId(), config); - List actions = new ArrayList<>(); - - for (ResourcePolicyActionRepresentation actionRep : rep.getActions()) { - actions.add(toModel(actionRep)); - } - - createActions(policy, actions); - - return policy; - } - - private ResourceAction toModel(ResourcePolicyActionRepresentation rep) { - List subActions = new ArrayList<>(); - - for (ResourcePolicyActionRepresentation subAction : ofNullable(rep.getActions()).orElse(List.of())) { - subActions.add(toModel(subAction)); - } - - return new ResourceAction(rep.getProviderId(), rep.getConfig(), subActions); - } - - public void bind(ResourcePolicy policy, ResourceType type, String resourceId) { - processEvent(List.of(policy), new AdhocResourcePolicyEvent(type, resourceId)); - } - - public Object resolveResource(ResourceType type, String resourceId) { - Objects.requireNonNull(type, "type"); - Objects.requireNonNull(type, "resourceId"); - return type.resolveResource(session, resourceId); - } -} diff --git a/services/src/main/java/org/keycloak/models/policy/UserActionBuilder.java b/services/src/main/java/org/keycloak/models/policy/UserActionBuilder.java deleted file mode 100644 index 1c69ec734bb..00000000000 --- a/services/src/main/java/org/keycloak/models/policy/UserActionBuilder.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2025 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.models.policy; - -import java.time.Duration; - -public class UserActionBuilder { - - private final ResourceAction action; - - private UserActionBuilder(ResourceAction action) { - this.action = action; - } - - public static UserActionBuilder builder(String providerId) { - ResourceAction action = new ResourceAction(providerId); - return new UserActionBuilder(action); - } - - public ResourceAction build() { - return action; - } - - public UserActionBuilder after(Duration duration) { - action.setAfter(duration.toMillis()); - return this; - } - - public UserActionBuilder withConfig(String key, String value) { - action.setConfig(key, value); - return this; - } -} diff --git a/services/src/main/java/org/keycloak/models/policy/AddRequiredActionProvider.java b/services/src/main/java/org/keycloak/models/workflow/AddRequiredActionStepProvider.java similarity index 63% rename from services/src/main/java/org/keycloak/models/policy/AddRequiredActionProvider.java rename to services/src/main/java/org/keycloak/models/workflow/AddRequiredActionStepProvider.java index 3c0e3ca5e6e..ab394045a0a 100644 --- a/services/src/main/java/org/keycloak/models/policy/AddRequiredActionProvider.java +++ b/services/src/main/java/org/keycloak/models/workflow/AddRequiredActionStepProvider.java @@ -1,7 +1,4 @@ -package org.keycloak.models.policy; - - -import java.util.List; +package org.keycloak.models.workflow; import org.jboss.logging.Logger; import org.keycloak.component.ComponentModel; @@ -9,19 +6,19 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; -import static org.keycloak.models.policy.ResourceAction.AFTER_KEY; +import java.util.List; -public class AddRequiredActionProvider implements ResourceActionProvider { +public class AddRequiredActionStepProvider implements WorkflowStepProvider { public static String REQUIRED_ACTION_KEY = "action"; private final KeycloakSession session; - private final ComponentModel actionModel; - private final Logger log = Logger.getLogger(AddRequiredActionProvider.class); + private final ComponentModel stepModel; + private final Logger log = Logger.getLogger(AddRequiredActionStepProvider.class); - public AddRequiredActionProvider(KeycloakSession session, ComponentModel model) { + public AddRequiredActionStepProvider(KeycloakSession session, ComponentModel model) { this.session = session; - this.actionModel = model; + this.stepModel = model; } @Override @@ -33,21 +30,16 @@ public class AddRequiredActionProvider implements ResourceActionProvider { if (user != null) { try { - UserModel.RequiredAction action = UserModel.RequiredAction.valueOf(actionModel.getConfig().getFirst(REQUIRED_ACTION_KEY)); + UserModel.RequiredAction action = UserModel.RequiredAction.valueOf(stepModel.getConfig().getFirst(REQUIRED_ACTION_KEY)); log.debugv("Adding required action {0} to user {1})", action, user.getId()); user.addRequiredAction(action); } catch (IllegalArgumentException e) { - log.warnv("Invalid required action {0} configured in AddRequiredActionProvider", actionModel.getConfig().getFirst(REQUIRED_ACTION_KEY)); + log.warnv("Invalid required action {0} configured in AddRequiredActionProvider", stepModel.getConfig().getFirst(REQUIRED_ACTION_KEY)); } } } } - @Override - public boolean isRunnable() { - return actionModel.get(AFTER_KEY) != null; - } - @Override public void close() { } diff --git a/services/src/main/java/org/keycloak/models/policy/AddRequiredActionProviderFactory.java b/services/src/main/java/org/keycloak/models/workflow/AddRequiredActionStepProviderFactory.java similarity index 79% rename from services/src/main/java/org/keycloak/models/policy/AddRequiredActionProviderFactory.java rename to services/src/main/java/org/keycloak/models/workflow/AddRequiredActionStepProviderFactory.java index b00f74dc3fe..dc4b58a2842 100644 --- a/services/src/main/java/org/keycloak/models/policy/AddRequiredActionProviderFactory.java +++ b/services/src/main/java/org/keycloak/models/workflow/AddRequiredActionStepProviderFactory.java @@ -1,4 +1,4 @@ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import java.util.List; @@ -10,13 +10,13 @@ import org.keycloak.provider.ConfiguredProvider; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigurationBuilder; -public class AddRequiredActionProviderFactory implements ResourceActionProviderFactory, ConfiguredProvider { +public class AddRequiredActionStepProviderFactory implements WorkflowStepProviderFactory, ConfiguredProvider { public static final String ID = "set-user-required-action"; @Override - public AddRequiredActionProvider create(KeycloakSession session, ComponentModel model) { - return new AddRequiredActionProvider(session, model); + public AddRequiredActionStepProvider create(KeycloakSession session, ComponentModel model) { + return new AddRequiredActionStepProvider(session, model); } @Override diff --git a/services/src/main/java/org/keycloak/models/workflow/AdhocResourcePolicyEvent.java b/services/src/main/java/org/keycloak/models/workflow/AdhocResourcePolicyEvent.java new file mode 100644 index 00000000000..bbf7604a06e --- /dev/null +++ b/services/src/main/java/org/keycloak/models/workflow/AdhocResourcePolicyEvent.java @@ -0,0 +1,8 @@ +package org.keycloak.models.workflow; + +final class AdhocWorkflowEvent extends WorkflowEvent { + + AdhocWorkflowEvent(ResourceType type, String resourceId) { + super(type, ResourceOperationType.AD_HOC, resourceId, null); + } +} diff --git a/services/src/main/java/org/keycloak/models/workflow/AggregatedStepProvider.java b/services/src/main/java/org/keycloak/models/workflow/AggregatedStepProvider.java new file mode 100644 index 00000000000..b4c58ed62a1 --- /dev/null +++ b/services/src/main/java/org/keycloak/models/workflow/AggregatedStepProvider.java @@ -0,0 +1,42 @@ +package org.keycloak.models.workflow; + +import java.util.List; + +import org.jboss.logging.Logger; +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; + +public class AggregatedStepProvider implements WorkflowStepProvider { + + private final KeycloakSession session; + private final ComponentModel model; + private final Logger log = Logger.getLogger(AggregatedStepProvider.class); + + public AggregatedStepProvider(KeycloakSession session, ComponentModel model) { + this.session = session; + this.model = model; + } + + @Override + public void close() { + } + + @Override + public void run(List userIds) { + WorkflowsManager manager = new WorkflowsManager(session); + List steps = manager.getStepById(session, model.getId()) + .getSteps().stream() + .map(manager::getStepProvider) + .toList(); + + for (String userId : userIds) { + for (WorkflowStepProvider step : steps) { + try { + step.run(List.of(userId)); + } catch (Exception e) { + log.errorf(e, "Failed to execute step %s for user %s", model.getProviderId(), userId); + } + } + } + } +} diff --git a/services/src/main/java/org/keycloak/models/policy/AggregatedActionProviderFactory.java b/services/src/main/java/org/keycloak/models/workflow/AggregatedStepProviderFactory.java similarity index 69% rename from services/src/main/java/org/keycloak/models/policy/AggregatedActionProviderFactory.java rename to services/src/main/java/org/keycloak/models/workflow/AggregatedStepProviderFactory.java index a884c533160..1e11d1ed8d4 100644 --- a/services/src/main/java/org/keycloak/models/policy/AggregatedActionProviderFactory.java +++ b/services/src/main/java/org/keycloak/models/workflow/AggregatedStepProviderFactory.java @@ -1,4 +1,4 @@ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import java.util.List; @@ -8,13 +8,13 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.provider.ProviderConfigProperty; -public class AggregatedActionProviderFactory implements ResourceActionProviderFactory { +public class AggregatedStepProviderFactory implements WorkflowStepProviderFactory { - public static final String ID = "aggregated-action-provider"; + public static final String ID = "aggregated-step-provider"; @Override - public AggregatedActionProvider create(KeycloakSession session, ComponentModel model) { - return new AggregatedActionProvider(session, model); + public AggregatedStepProvider create(KeycloakSession session, ComponentModel model) { + return new AggregatedStepProvider(session, model); } @Override diff --git a/services/src/main/java/org/keycloak/models/policy/DeleteUserActionProvider.java b/services/src/main/java/org/keycloak/models/workflow/DeleteUserStepProvider.java similarity index 73% rename from services/src/main/java/org/keycloak/models/policy/DeleteUserActionProvider.java rename to services/src/main/java/org/keycloak/models/workflow/DeleteUserStepProvider.java index 5528a28a304..3accbc89f32 100644 --- a/services/src/main/java/org/keycloak/models/policy/DeleteUserActionProvider.java +++ b/services/src/main/java/org/keycloak/models/workflow/DeleteUserStepProvider.java @@ -15,11 +15,7 @@ * limitations under the License. */ -package org.keycloak.models.policy; - -import static org.keycloak.models.policy.ResourceAction.AFTER_KEY; - -import java.util.List; +package org.keycloak.models.workflow; import org.jboss.logging.Logger; import org.keycloak.component.ComponentModel; @@ -27,15 +23,15 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; -public class DeleteUserActionProvider implements ResourceActionProvider { +import java.util.List; + +public class DeleteUserStepProvider implements WorkflowStepProvider { private final KeycloakSession session; - private final ComponentModel actionModel; - private final Logger log = Logger.getLogger(DeleteUserActionProvider.class); + private final Logger log = Logger.getLogger(DeleteUserStepProvider.class); - public DeleteUserActionProvider(KeycloakSession session, ComponentModel model) { + public DeleteUserStepProvider(KeycloakSession session, ComponentModel model) { this.session = session; - this.actionModel = model; } @Override @@ -57,9 +53,4 @@ public class DeleteUserActionProvider implements ResourceActionProvider { session.users().removeUser(realm, user); } } - - @Override - public boolean isRunnable() { - return actionModel.get(AFTER_KEY) != null; - } } diff --git a/services/src/main/java/org/keycloak/models/policy/DeleteUserActionProviderFactory.java b/services/src/main/java/org/keycloak/models/workflow/DeleteUserStepProviderFactory.java similarity index 80% rename from services/src/main/java/org/keycloak/models/policy/DeleteUserActionProviderFactory.java rename to services/src/main/java/org/keycloak/models/workflow/DeleteUserStepProviderFactory.java index 6205d053635..e511b6a3121 100644 --- a/services/src/main/java/org/keycloak/models/policy/DeleteUserActionProviderFactory.java +++ b/services/src/main/java/org/keycloak/models/workflow/DeleteUserStepProviderFactory.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import java.util.List; @@ -25,13 +25,13 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.provider.ProviderConfigProperty; -public class DeleteUserActionProviderFactory implements ResourceActionProviderFactory { +public class DeleteUserStepProviderFactory implements WorkflowStepProviderFactory { - public static final String ID = "delete-user-action-provider"; + public static final String ID = "delete-user-step-provider"; @Override - public DeleteUserActionProvider create(KeycloakSession session, ComponentModel model) { - return new DeleteUserActionProvider(session, model); + public DeleteUserStepProvider create(KeycloakSession session, ComponentModel model) { + return new DeleteUserStepProvider(session, model); } @Override diff --git a/services/src/main/java/org/keycloak/models/policy/DisableUserActionProvider.java b/services/src/main/java/org/keycloak/models/workflow/DisableUserStepProvider.java similarity index 73% rename from services/src/main/java/org/keycloak/models/policy/DisableUserActionProvider.java rename to services/src/main/java/org/keycloak/models/workflow/DisableUserStepProvider.java index 5ec15a6fefc..5cab6bc9531 100644 --- a/services/src/main/java/org/keycloak/models/policy/DisableUserActionProvider.java +++ b/services/src/main/java/org/keycloak/models/workflow/DisableUserStepProvider.java @@ -15,26 +15,23 @@ * limitations under the License. */ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; -import static org.keycloak.models.policy.ResourceAction.AFTER_KEY; - -import java.util.List; import org.jboss.logging.Logger; import org.keycloak.component.ComponentModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; -public class DisableUserActionProvider implements ResourceActionProvider { +import java.util.List; + +public class DisableUserStepProvider implements WorkflowStepProvider { private final KeycloakSession session; - private final ComponentModel actionModel; - private final Logger log = Logger.getLogger(DisableUserActionProvider.class); + private final Logger log = Logger.getLogger(DisableUserStepProvider.class); - public DisableUserActionProvider(KeycloakSession session, ComponentModel model) { + public DisableUserStepProvider(KeycloakSession session, ComponentModel model) { this.session = session; - this.actionModel = model; } @Override @@ -54,9 +51,4 @@ public class DisableUserActionProvider implements ResourceActionProvider { } } } - - @Override - public boolean isRunnable() { - return actionModel.get(AFTER_KEY) != null; - } } diff --git a/services/src/main/java/org/keycloak/models/policy/DisableUserActionProviderFactory.java b/services/src/main/java/org/keycloak/models/workflow/DisableUserStepProviderFactory.java similarity index 80% rename from services/src/main/java/org/keycloak/models/policy/DisableUserActionProviderFactory.java rename to services/src/main/java/org/keycloak/models/workflow/DisableUserStepProviderFactory.java index c8f98542d62..c9699957e69 100644 --- a/services/src/main/java/org/keycloak/models/policy/DisableUserActionProviderFactory.java +++ b/services/src/main/java/org/keycloak/models/workflow/DisableUserStepProviderFactory.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import java.util.List; @@ -25,13 +25,13 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.provider.ProviderConfigProperty; -public class DisableUserActionProviderFactory implements ResourceActionProviderFactory { +public class DisableUserStepProviderFactory implements WorkflowStepProviderFactory { - public static final String ID = "disable-user-action-provider"; + public static final String ID = "disable-user-step-provider"; @Override - public DisableUserActionProvider create(KeycloakSession session, ComponentModel model) { - return new DisableUserActionProvider(session, model); + public DisableUserStepProvider create(KeycloakSession session, ComponentModel model) { + return new DisableUserStepProvider(session, model); } @Override diff --git a/services/src/main/java/org/keycloak/models/policy/NotifyUserActionProvider.java b/services/src/main/java/org/keycloak/models/workflow/NotifyUserStepProvider.java similarity index 60% rename from services/src/main/java/org/keycloak/models/policy/NotifyUserActionProvider.java rename to services/src/main/java/org/keycloak/models/workflow/NotifyUserStepProvider.java index 7f2c074b75f..33c1bb9c194 100644 --- a/services/src/main/java/org/keycloak/models/policy/NotifyUserActionProvider.java +++ b/services/src/main/java/org/keycloak/models/workflow/NotifyUserStepProvider.java @@ -15,17 +15,9 @@ * limitations under the License. */ -package org.keycloak.models.policy; - -import static org.keycloak.models.policy.ResourceAction.AFTER_KEY; - -import java.time.Duration; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +package org.keycloak.models.workflow; import org.jboss.logging.Logger; - import org.keycloak.component.ComponentModel; import org.keycloak.email.EmailException; import org.keycloak.email.EmailTemplateProvider; @@ -33,7 +25,14 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; -public class NotifyUserActionProvider implements ResourceActionProvider { +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.keycloak.models.workflow.WorkflowStep.AFTER_KEY; + +public class NotifyUserStepProvider implements WorkflowStepProvider { private static final String ACCOUNT_DISABLE_NOTIFICATION_SUBJECT = "accountDisableNotificationSubject"; private static final String ACCOUNT_DELETE_NOTIFICATION_SUBJECT = "accountDeleteNotificationSubject"; @@ -41,12 +40,12 @@ public class NotifyUserActionProvider implements ResourceActionProvider { private static final String ACCOUNT_DELETE_NOTIFICATION_BODY = "accountDeleteNotificationBody"; private final KeycloakSession session; - private final ComponentModel actionModel; - private final Logger log = Logger.getLogger(NotifyUserActionProvider.class); + private final ComponentModel stepModel; + private final Logger log = Logger.getLogger(NotifyUserStepProvider.class); - public NotifyUserActionProvider(KeycloakSession session, ComponentModel model) { + public NotifyUserStepProvider(KeycloakSession session, ComponentModel model) { this.session = session; - this.actionModel = model; + this.stepModel = model; } @Override @@ -79,70 +78,70 @@ public class NotifyUserActionProvider implements ResourceActionProvider { } private String getSubjectKey() { - String nextActionType = getNextActionType(); - String customSubjectKey = actionModel.getConfig().getFirst("custom_subject_key"); + String nextStepType = getNextStepType(); + String customSubjectKey = stepModel.getConfig().getFirst("custom_subject_key"); if (customSubjectKey != null && !customSubjectKey.trim().isEmpty()) { return customSubjectKey; } - // Return default subject key based on next action type - return getDefaultSubjectKey(nextActionType); + // Return default subject key based on next step type + return getDefaultSubjectKey(nextStepType); } private String getBodyTemplate() { - return "resource-policy-notification.ftl"; + return "workflow-notification.ftl"; } private Map getBodyAttributes() { RealmModel realm = session.getContext().getRealm(); Map attributes = new HashMap<>(); - String nextActionType = getNextActionType(); + String nextStepType = getNextStepType(); - // Custom message override or default based on action type - String customMessage = actionModel.getConfig().getFirst("custom_message"); + // Custom message override or default based on step type + String customMessage = stepModel.getConfig().getFirst("custom_message"); if (customMessage != null && !customMessage.trim().isEmpty()) { attributes.put("messageKey", "customMessage"); attributes.put("customMessage", customMessage); } else { - attributes.put("messageKey", getDefaultMessageKey(nextActionType)); + attributes.put("messageKey", getDefaultMessageKey(nextStepType)); } - // Calculate days remaining until next action - int daysRemaining = calculateDaysUntilNextAction(); + // Calculate days remaining until next step + int daysRemaining = calculateDaysUntilNextStep(); // Message parameters for internationalization attributes.put("daysRemaining", daysRemaining); - attributes.put("reason", actionModel.getConfig().getFirstOrDefault("reason", "inactivity")); + attributes.put("reason", stepModel.getConfig().getFirstOrDefault("reason", "inactivity")); attributes.put("realmName", realm.getDisplayName() != null ? realm.getDisplayName() : realm.getName()); - attributes.put("nextActionType", nextActionType); + attributes.put("nextStepType", nextStepType); attributes.put("subjectKey", getSubjectKey()); return attributes; } - private String getNextActionType() { - Map nextActionMap = getNextNonNotificationAction(); - return nextActionMap.isEmpty() ? "unknown-action" : nextActionMap.keySet().iterator().next().getProviderId(); + private String getNextStepType() { + Map nextStepMap = getNextNonNotificationStep(); + return nextStepMap.isEmpty() ? "unknown-step" : nextStepMap.keySet().iterator().next().getProviderId(); } - private int calculateDaysUntilNextAction() { - Map nextActionMap = getNextNonNotificationAction(); - if (nextActionMap.isEmpty()) { + private int calculateDaysUntilNextStep() { + Map nextStepMap = getNextNonNotificationStep(); + if (nextStepMap.isEmpty()) { return 0; } - Long timeToNextAction = nextActionMap.values().iterator().next(); - return Math.toIntExact(Duration.ofMillis(timeToNextAction).toDays()); + Long timeToNextStep = nextStepMap.values().iterator().next(); + return Math.toIntExact(Duration.ofMillis(timeToNextStep).toDays()); } - private Map getNextNonNotificationAction() { - long timeToNextNonNotificationAction = 0L; + private Map getNextNonNotificationStep() { + long timeToNextNonNotificationStep = 0L; RealmModel realm = session.getContext().getRealm(); - ComponentModel policyModel = realm.getComponent(actionModel.getParentId()); + ComponentModel workflowModel = realm.getComponent(stepModel.getParentId()); - List actions = realm.getComponentsStream(policyModel.getId(), ResourceActionProvider.class.getName()) + List steps = realm.getComponentsStream(workflowModel.getId(), WorkflowStepProvider.class.getName()) .sorted((a, b) -> { int priorityA = Integer.parseInt(a.get("priority", "0")); int priorityB = Integer.parseInt(b.get("priority", "0")); @@ -150,17 +149,17 @@ public class NotifyUserActionProvider implements ResourceActionProvider { }) .toList(); - // Find current action and return next non-notification action + // Find current step and return next non-notification step boolean foundCurrent = false; - for (ComponentModel action : actions) { + for (ComponentModel step : steps) { if (foundCurrent) { - timeToNextNonNotificationAction += action.get(AFTER_KEY, 0L); - if (!action.getProviderId().equals("notify-user-action-provider")) { + timeToNextNonNotificationStep += step.get(AFTER_KEY, 0L); + if (!step.getProviderId().equals("notify-user-step-provider")) { // we found the next non-notification action, accumulate its time and break - return Map.of(action, timeToNextNonNotificationAction); + return Map.of(step, timeToNextNonNotificationStep); } } - if (action.getId().equals(actionModel.getId())) { + if (step.getId().equals(stepModel.getId())) { foundCurrent = true; } } @@ -168,24 +167,19 @@ public class NotifyUserActionProvider implements ResourceActionProvider { return Map.of(); } - private String getDefaultSubjectKey(String actionType) { - return switch (actionType) { - case "disable-user-action-provider" -> ACCOUNT_DISABLE_NOTIFICATION_SUBJECT; - case "delete-user-action-provider" -> ACCOUNT_DELETE_NOTIFICATION_SUBJECT; + private String getDefaultSubjectKey(String stepType) { + return switch (stepType) { + case "disable-user-step-provider" -> ACCOUNT_DISABLE_NOTIFICATION_SUBJECT; + case "delete-user-step-provider" -> ACCOUNT_DELETE_NOTIFICATION_SUBJECT; default -> "accountNotificationSubject"; }; } - private String getDefaultMessageKey(String actionType) { - return switch (actionType) { - case "disable-user-action-provider" -> ACCOUNT_DISABLE_NOTIFICATION_BODY; - case "delete-user-action-provider" -> ACCOUNT_DELETE_NOTIFICATION_BODY; + private String getDefaultMessageKey(String stepType) { + return switch (stepType) { + case "disable-user-step-provider" -> ACCOUNT_DISABLE_NOTIFICATION_BODY; + case "delete-user-step-provider" -> ACCOUNT_DELETE_NOTIFICATION_BODY; default -> "accountNotificationBody"; }; } - - @Override - public boolean isRunnable() { - return actionModel.get(AFTER_KEY) != null; - } } diff --git a/services/src/main/java/org/keycloak/models/policy/NotifyUserActionProviderFactory.java b/services/src/main/java/org/keycloak/models/workflow/NotifyUserStepProviderFactory.java similarity index 82% rename from services/src/main/java/org/keycloak/models/policy/NotifyUserActionProviderFactory.java rename to services/src/main/java/org/keycloak/models/workflow/NotifyUserStepProviderFactory.java index 291864ca2c0..6c57a6f6aba 100644 --- a/services/src/main/java/org/keycloak/models/policy/NotifyUserActionProviderFactory.java +++ b/services/src/main/java/org/keycloak/models/workflow/NotifyUserStepProviderFactory.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import java.util.Arrays; import java.util.List; @@ -26,13 +26,13 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.provider.ProviderConfigProperty; -public class NotifyUserActionProviderFactory implements ResourceActionProviderFactory { +public class NotifyUserStepProviderFactory implements WorkflowStepProviderFactory { - public static final String ID = "notify-user-action-provider"; + public static final String ID = "notify-user-step-provider"; @Override - public NotifyUserActionProvider create(KeycloakSession session, ComponentModel model) { - return new NotifyUserActionProvider(session, model); + public NotifyUserStepProvider create(KeycloakSession session, ComponentModel model) { + return new NotifyUserStepProvider(session, model); } @Override @@ -69,7 +69,7 @@ public class NotifyUserActionProviderFactory implements ResourceActionProviderFa public List getConfigProperties() { return Arrays.asList( new ProviderConfigProperty("reason", "Reason", - "Reason for the action (inactivity, policy violation, compliance requirement)", + "Reason for the action (inactivity, workflow violation, compliance requirement)", ProviderConfigProperty.STRING_TYPE, ""), new ProviderConfigProperty("custom_subject_key", "Custom Subject Message Key", diff --git a/services/src/main/java/org/keycloak/models/policy/SetUserAttributeActionProvider.java b/services/src/main/java/org/keycloak/models/workflow/SetUserAttributeStepProvider.java similarity index 74% rename from services/src/main/java/org/keycloak/models/policy/SetUserAttributeActionProvider.java rename to services/src/main/java/org/keycloak/models/workflow/SetUserAttributeStepProvider.java index eb83a173081..00e5ae8e83e 100644 --- a/services/src/main/java/org/keycloak/models/policy/SetUserAttributeActionProvider.java +++ b/services/src/main/java/org/keycloak/models/workflow/SetUserAttributeStepProvider.java @@ -15,13 +15,7 @@ * limitations under the License. */ -package org.keycloak.models.policy; - -import static org.keycloak.models.policy.ResourceAction.PRIORITY_KEY; -import static org.keycloak.models.policy.ResourceAction.AFTER_KEY; - -import java.util.List; -import java.util.Map.Entry; +package org.keycloak.models.workflow; import org.jboss.logging.Logger; import org.keycloak.component.ComponentModel; @@ -29,15 +23,21 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; -public class SetUserAttributeActionProvider implements ResourceActionProvider { +import java.util.List; +import java.util.Map.Entry; + +import static org.keycloak.models.workflow.WorkflowStep.AFTER_KEY; +import static org.keycloak.models.workflow.WorkflowStep.PRIORITY_KEY; + +public class SetUserAttributeStepProvider implements WorkflowStepProvider { private final KeycloakSession session; - private final ComponentModel actionModel; - private final Logger log = Logger.getLogger(SetUserAttributeActionProvider.class); + private final ComponentModel stepModel; + private final Logger log = Logger.getLogger(SetUserAttributeStepProvider.class); - public SetUserAttributeActionProvider(KeycloakSession session, ComponentModel model) { + public SetUserAttributeStepProvider(KeycloakSession session, ComponentModel model) { this.session = session; - this.actionModel = model; + this.stepModel = model; } @Override @@ -52,7 +52,7 @@ public class SetUserAttributeActionProvider implements ResourceActionProvider { UserModel user = session.users().getUserById(realm, id); if (user != null) { - for (Entry> entry : actionModel.getConfig().entrySet()) { + for (Entry> entry : stepModel.getConfig().entrySet()) { String key = entry.getKey(); if (!key.startsWith(AFTER_KEY) && !key.startsWith(PRIORITY_KEY)) { @@ -63,9 +63,4 @@ public class SetUserAttributeActionProvider implements ResourceActionProvider { } } } - - @Override - public boolean isRunnable() { - return true; - } } diff --git a/services/src/main/java/org/keycloak/models/policy/SetUserAttributeActionProviderFactory.java b/services/src/main/java/org/keycloak/models/workflow/SetUserAttributeStepProviderFactory.java similarity index 76% rename from services/src/main/java/org/keycloak/models/policy/SetUserAttributeActionProviderFactory.java rename to services/src/main/java/org/keycloak/models/workflow/SetUserAttributeStepProviderFactory.java index 17b539c47a5..8fa04077653 100644 --- a/services/src/main/java/org/keycloak/models/policy/SetUserAttributeActionProviderFactory.java +++ b/services/src/main/java/org/keycloak/models/workflow/SetUserAttributeStepProviderFactory.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import java.util.List; @@ -25,13 +25,13 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.provider.ProviderConfigProperty; -public class SetUserAttributeActionProviderFactory implements ResourceActionProviderFactory { +public class SetUserAttributeStepProviderFactory implements WorkflowStepProviderFactory { - public static final String ID = "set-user-attr-action-provider"; + public static final String ID = "set-user-attr-step-provider"; @Override - public SetUserAttributeActionProvider create(KeycloakSession session, ComponentModel model) { - return new SetUserAttributeActionProvider(session, model); + public SetUserAttributeStepProvider create(KeycloakSession session, ComponentModel model) { + return new SetUserAttributeStepProvider(session, model); } @Override @@ -61,7 +61,7 @@ public class SetUserAttributeActionProviderFactory implements ResourceActionProv @Override public String getHelpText() { - return "Sets an attribute on the user. Configure attributes to set as 'user.attribute.' in the action's configuration."; + return "Sets an attribute on the user. Configure attributes to set as 'user.attribute.' in the step's configuration."; } @Override diff --git a/services/src/main/java/org/keycloak/models/policy/ResourcePolicyEventListener.java b/services/src/main/java/org/keycloak/models/workflow/WorkflowEventListener.java similarity index 73% rename from services/src/main/java/org/keycloak/models/policy/ResourcePolicyEventListener.java rename to services/src/main/java/org/keycloak/models/workflow/WorkflowEventListener.java index d05c234b1c9..0c3e24ce3b9 100644 --- a/services/src/main/java/org/keycloak/models/policy/ResourcePolicyEventListener.java +++ b/services/src/main/java/org/keycloak/models/workflow/WorkflowEventListener.java @@ -8,7 +8,7 @@ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import org.keycloak.events.Event; import org.keycloak.events.EventListenerProvider; @@ -18,24 +18,24 @@ import org.keycloak.models.RealmModel; import org.keycloak.provider.ProviderEvent; import org.keycloak.provider.ProviderEventListener; -public class ResourcePolicyEventListener implements EventListenerProvider, ProviderEventListener { +public class WorkflowEventListener implements EventListenerProvider, ProviderEventListener { private final KeycloakSession session; - public ResourcePolicyEventListener(KeycloakSession session) { + public WorkflowEventListener(KeycloakSession session) { this.session = session; } @Override public void onEvent(Event event) { - ResourcePolicyEvent policyEvent = ResourceType.USERS.toEvent(event); - trySchedule(policyEvent); + WorkflowEvent workflowEvent = ResourceType.USERS.toEvent(event); + trySchedule(workflowEvent); } @Override public void onEvent(AdminEvent event, boolean includeRepresentation) { - ResourcePolicyEvent policyEvent = ResourceType.USERS.toEvent(event); - trySchedule(policyEvent); + WorkflowEvent workflowEvent = ResourceType.USERS.toEvent(event); + trySchedule(workflowEvent); } @Override @@ -49,9 +49,9 @@ public class ResourcePolicyEventListener implements EventListenerProvider, Provi trySchedule(ResourceType.USERS.toEvent(event)); } - private void trySchedule(ResourcePolicyEvent event) { + private void trySchedule(WorkflowEvent event) { if (event != null) { - ResourcePolicyManager manager = new ResourcePolicyManager(session); + WorkflowsManager manager = new WorkflowsManager(session); manager.processEvent(event); } } diff --git a/services/src/main/java/org/keycloak/models/policy/ResourceActionRunnerScheduledTask.java b/services/src/main/java/org/keycloak/models/workflow/WorkflowRunnerScheduledTask.java similarity index 58% rename from services/src/main/java/org/keycloak/models/policy/ResourceActionRunnerScheduledTask.java rename to services/src/main/java/org/keycloak/models/workflow/WorkflowRunnerScheduledTask.java index 01eff71b82a..b388eeaf4ad 100644 --- a/services/src/main/java/org/keycloak/models/policy/ResourceActionRunnerScheduledTask.java +++ b/services/src/main/java/org/keycloak/models/workflow/WorkflowRunnerScheduledTask.java @@ -1,4 +1,4 @@ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import org.jboss.logging.Logger; import org.keycloak.models.KeycloakContext; @@ -9,21 +9,21 @@ import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.timer.ScheduledTask; /** - * A {@link ScheduledTask} that runs all the scheduled actions for resources on a per-realm basis. + * A {@link ScheduledTask} that runs all the scheduled steps for resources on a per-realm basis. */ -final class ResourceActionRunnerScheduledTask implements ScheduledTask { +final class WorkflowRunnerScheduledTask implements ScheduledTask { - private final Logger logger = Logger.getLogger(ResourceActionRunnerScheduledTask.class); + private final Logger logger = Logger.getLogger(WorkflowRunnerScheduledTask.class); private final KeycloakSessionFactory sessionFactory; - ResourceActionRunnerScheduledTask(KeycloakSessionFactory sessionFactory) { + WorkflowRunnerScheduledTask(KeycloakSessionFactory sessionFactory) { this.sessionFactory = sessionFactory; } @Override public void run(KeycloakSession session) { - // TODO: Depending on how many realms and the actions in use, this task can consume a lot of gears (e.g.: cpu, memory, and network) + // TODO: Depending on how many realms and the steps in use, this task can consume a lot of gears (e.g.: cpu, memory, and network) // we need a smarter mechanism that process realms in batches with some window interval session.realms().getRealmsStream().map(RealmModel::getId).forEach(this::runScheduledTasksOnRealm); } @@ -35,17 +35,17 @@ final class ResourceActionRunnerScheduledTask implements ScheduledTask { RealmModel realm = session.realms().getRealm(id); context.setRealm(realm); - new ResourcePolicyManager(session).runScheduledActions(); + new WorkflowsManager(session).runScheduledSteps(); - sessionFactory.publish(new ResourcePolicyActionRunnerSuccessEvent(session)); + sessionFactory.publish(new WorkflowStepRunnerSuccessEvent(session)); } catch (Exception e) { - logger.errorf(e, "Failed to run resource policy actions on realm with id '%s'", id); + logger.errorf(e, "Failed to run workflow steps on realm with id '%s'", id); } }); } @Override public String getTaskName() { - return "resource-policy-action-runner"; + return "workflow-runner-task"; } } diff --git a/services/src/main/java/org/keycloak/models/workflow/WorkflowStepRunnerSuccessEvent.java b/services/src/main/java/org/keycloak/models/workflow/WorkflowStepRunnerSuccessEvent.java new file mode 100644 index 00000000000..06219483fa8 --- /dev/null +++ b/services/src/main/java/org/keycloak/models/workflow/WorkflowStepRunnerSuccessEvent.java @@ -0,0 +1,7 @@ +package org.keycloak.models.workflow; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.provider.ProviderEvent; + +public record WorkflowStepRunnerSuccessEvent(KeycloakSession session) implements ProviderEvent { +} diff --git a/services/src/main/java/org/keycloak/models/policy/ResourcePolicyEventListenerFactory.java b/services/src/main/java/org/keycloak/models/workflow/WorkflowsEventListenerFactory.java similarity index 69% rename from services/src/main/java/org/keycloak/models/policy/ResourcePolicyEventListenerFactory.java rename to services/src/main/java/org/keycloak/models/workflow/WorkflowsEventListenerFactory.java index bf8027c306e..4809c85b32a 100644 --- a/services/src/main/java/org/keycloak/models/policy/ResourcePolicyEventListenerFactory.java +++ b/services/src/main/java/org/keycloak/models/workflow/WorkflowsEventListenerFactory.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.keycloak.models.policy; +package org.keycloak.models.workflow; import java.time.Duration; @@ -30,15 +30,15 @@ import org.keycloak.services.scheduled.ClusterAwareScheduledTaskRunner; import org.keycloak.timer.TimerProvider; import org.keycloak.provider.ProviderEvent; -public class ResourcePolicyEventListenerFactory implements EventListenerProviderFactory, EnvironmentDependentProviderFactory { +public class WorkflowsEventListenerFactory implements EventListenerProviderFactory, EnvironmentDependentProviderFactory { - public static final String ID = "resource-policy-event-listener"; - private static final long DEFAULT_ACTION_RUNNER_TASK_INTERVAL = Duration.ofHours(12).toMillis(); - private long actionRunnerTaskInterval; + public static final String ID = "workflow-event-listener"; + private static final long DEFAULT_STEP_RUNNER_TASK_INTERVAL = Duration.ofHours(12).toMillis(); + private long stepRunnerTaskInterval; @Override public EventListenerProvider create(KeycloakSession session) { - return new ResourcePolicyEventListener(session); + return new WorkflowEventListener(session); } @Override @@ -48,7 +48,7 @@ public class ResourcePolicyEventListenerFactory implements EventListenerProvider @Override public void init(Scope config) { - actionRunnerTaskInterval = config.getLong("actionRunnerTaskInterval", DEFAULT_ACTION_RUNNER_TASK_INTERVAL); + stepRunnerTaskInterval = config.getLong("stepRunnerTaskInterval", DEFAULT_STEP_RUNNER_TASK_INTERVAL); } @Override @@ -60,11 +60,11 @@ public class ResourcePolicyEventListenerFactory implements EventListenerProvider onEvent(event, session); } }); - scheduleActionRunnerTask(factory); + scheduleStepRunnerTask(factory); } private void onEvent(ProviderEvent event, KeycloakSession session) { - ResourcePolicyEventListener provider = (ResourcePolicyEventListener) session.getProvider(EventListenerProvider.class, getId()); + WorkflowEventListener provider = (WorkflowEventListener) session.getProvider(EventListenerProvider.class, getId()); provider.onEvent(event); } @@ -79,13 +79,13 @@ public class ResourcePolicyEventListenerFactory implements EventListenerProvider @Override public boolean isSupported(Scope config) { - return Profile.isFeatureEnabled(Profile.Feature.RESOURCE_LIFECYCLE); + return Profile.isFeatureEnabled(Profile.Feature.WORKFLOWS); } - private void scheduleActionRunnerTask(KeycloakSessionFactory factory) { + private void scheduleStepRunnerTask(KeycloakSessionFactory factory) { try (KeycloakSession session = factory.create()) { TimerProvider timer = session.getProvider(TimerProvider.class); - timer.schedule(new ClusterAwareScheduledTaskRunner(factory, new ResourceActionRunnerScheduledTask(factory), actionRunnerTaskInterval), actionRunnerTaskInterval); + timer.schedule(new ClusterAwareScheduledTaskRunner(factory, new WorkflowRunnerScheduledTask(factory), stepRunnerTaskInterval), stepRunnerTaskInterval); } } } diff --git a/services/src/main/java/org/keycloak/models/workflow/WorkflowsManager.java b/services/src/main/java/org/keycloak/models/workflow/WorkflowsManager.java new file mode 100644 index 00000000000..bacd51cc541 --- /dev/null +++ b/services/src/main/java/org/keycloak/models/workflow/WorkflowsManager.java @@ -0,0 +1,365 @@ +/* + * Copyright 2025 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.workflow; + +import static java.util.Optional.ofNullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; + +import jakarta.ws.rs.BadRequestException; +import org.jboss.logging.Logger; +import org.keycloak.common.Profile; +import org.keycloak.common.Profile.Feature; +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.component.ComponentFactory; +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.workflow.WorkflowStateProvider.ScheduledStep; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.representations.workflows.WorkflowStepRepresentation; +import org.keycloak.representations.workflows.WorkflowConditionRepresentation; +import org.keycloak.representations.workflows.WorkflowRepresentation; + +public class WorkflowsManager { + + private static final Logger log = Logger.getLogger(WorkflowsManager.class); + + private final KeycloakSession session; + private final WorkflowStateProvider workflowStateProvider; + + public static boolean isFeatureEnabled() { + return Profile.isFeatureEnabled(Feature.WORKFLOWS); + } + + public WorkflowsManager(KeycloakSession session) { + this.session = session; + this.workflowStateProvider = session.getKeycloakSessionFactory().getProviderFactory(WorkflowStateProvider.class).create(session); + } + + public Workflow addWorkflow(String providerId, Map> config) { + return addWorkflow(new Workflow(providerId, config)); + } + + public Workflow addWorkflow(Workflow workflow) { + RealmModel realm = getRealm(); + ComponentModel model = new ComponentModel(); + + model.setParentId(realm.getId()); + model.setProviderId(workflow.getProviderId()); + model.setProviderType(WorkflowProvider.class.getName()); + + MultivaluedHashMap config = workflow.getConfig(); + + if (config != null) { + model.setConfig(config); + } + + return new Workflow(realm.addComponentModel(model)); + } + + // This method takes an ordered list of steps. First step in the list has the highest priority, last step has the lowest priority + public void createSteps(Workflow workflow, List steps) { + for (int i = 0; i < steps.size(); i++) { + WorkflowStep step = steps.get(i); + + // assign priority based on index. + step.setPriority(i + 1); + + List subSteps = Optional.ofNullable(step.getSteps()).orElse(List.of()); + + // persist the new step component. + step = addStep(workflow.getId(), step); + + for (int j = 0; j < subSteps.size(); j++) { + WorkflowStep subStep = subSteps.get(j); + // assign priority based on index. + subStep.setPriority(j + 1); + addStep(step.getId(), subStep); + } + } + } + + private WorkflowStep addStep(String parentId, WorkflowStep step) { + RealmModel realm = getRealm(); + ComponentModel workflowModel = realm.getComponent(parentId); + ComponentModel stepModel = new ComponentModel(); + + stepModel.setId(step.getId());//need to keep stable UUIDs not to break a link in state table + stepModel.setParentId(workflowModel.getId()); + stepModel.setProviderId(step.getProviderId()); + stepModel.setProviderType(WorkflowStepProvider.class.getName()); + stepModel.setConfig(step.getConfig()); + + return new WorkflowStep(realm.addComponentModel(stepModel)); + } + + public List getWorkflows() { + RealmModel realm = getRealm(); + return realm.getComponentsStream(realm.getId(), WorkflowProvider.class.getName()) + .map(Workflow::new).toList(); + } + + public List getSteps(String workflowId) { + return getStepsStream(workflowId).toList(); + } + + public Stream getStepsStream(String parentId) { + RealmModel realm = session.getContext().getRealm(); + return realm.getComponentsStream(parentId, WorkflowStepProvider.class.getName()) + .map(this::toStep).sorted(); + } + + private WorkflowStep toStep(ComponentModel model) { + WorkflowStep step = new WorkflowStep(model); + + step.setSteps(getSteps(step.getId())); + + return step; + } + + public WorkflowStep getStepById(KeycloakSession session, String id) { + RealmModel realm = session.getContext().getRealm(); + ComponentModel component = realm.getComponent(id); + + if (component == null) { + return null; + } + + return toStep(component); + } + + private WorkflowStep getFirstStep(Workflow workflow) { + WorkflowStep step = getSteps(workflow.getId()).get(0); + Long notBefore = workflow.getNotBefore(); + + if (notBefore != null) { + step.setAfter(notBefore); + } + + return step; + } + + private WorkflowProvider getWorkflowProvider(Workflow workflow) { + ComponentFactory factory = (ComponentFactory) session.getKeycloakSessionFactory() + .getProviderFactory(WorkflowProvider.class, workflow.getProviderId()); + return (WorkflowProvider) factory.create(session, getRealm().getComponent(workflow.getId())); + } + + public WorkflowStepProvider getStepProvider(WorkflowStep step) { + ComponentFactory stepFactory = (ComponentFactory) session.getKeycloakSessionFactory() + .getProviderFactory(WorkflowStepProvider.class, step.getProviderId()); + return (WorkflowStepProvider) stepFactory.create(session, getRealm().getComponent(step.getId())); + } + + private RealmModel getRealm() { + return session.getContext().getRealm(); + } + + public void removeWorkflows() { + RealmModel realm = getRealm(); + realm.getComponentsStream(realm.getId(), WorkflowProvider.class.getName()).forEach(workflow -> { + realm.getComponentsStream(workflow.getId(), WorkflowStepProvider.class.getName()).forEach(realm::removeComponent); + realm.removeComponent(workflow); + }); + } + + public void scheduleAllEligibleResources(Workflow workflow) { + if (workflow.isEnabled()) { + WorkflowProvider provider = getWorkflowProvider(workflow); + provider.getEligibleResourcesForInitialStep() + .forEach(resourceId -> processEvent(List.of(workflow), new AdhocWorkflowEvent(ResourceType.USERS, resourceId))); + } + } + + public void processEvent(WorkflowEvent event) { + processEvent(getWorkflows(), event); + } + + public void processEvent(List workflows, WorkflowEvent event) { + List currentlyAssignedWorkflows = workflowStateProvider.getScheduledStepsByResource(event.getResourceId()) + .stream().map(ScheduledStep::workflowId).toList(); + + // iterate through the workflows, and for those not yet assigned to the user check if they can be assigned + workflows.stream() + .filter(workflow -> workflow.isEnabled() && !getSteps(workflow.getId()).isEmpty()) + .forEach(workflow -> { + WorkflowProvider provider = getWorkflowProvider(workflow); + try { + if (!currentlyAssignedWorkflows.contains(workflow.getId())) { + // if workflow is not active for the resource, check if the provider allows activating based on the event + if (provider.activateOnEvent(event)) { + if (workflow.isScheduled()) { + // workflow is scheduled, so we schedule the first step + log.debugf("Scheduling first step of workflow %s for resource %s based on event %s", + workflow.getId(), event.getResourceId(), event.getOperation()); + workflowStateProvider.scheduleStep(workflow, getFirstStep(workflow), event.getResourceId()); + } else { + // workflow is not scheduled, so we run all steps immediately + log.debugf("Running all steps of workflow %s for resource %s based on event %s", + workflow.getId(), event.getResourceId(), event.getOperation()); + KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), session.getContext(), s -> + getSteps(workflow.getId()).forEach(step -> getStepProvider(step).run(List.of(event.getResourceId()))) + ); + } + } + } else { + if (provider.resetOnEvent(event)) { + workflowStateProvider.scheduleStep(workflow, getFirstStep(workflow), event.getResourceId()); + } else if (provider.deactivateOnEvent(event)) { + workflowStateProvider.remove(workflow.getId(), event.getResourceId()); + } + } + } catch (WorkflowInvalidStateException e) { + workflow.getConfig().putSingle("enabled", "false"); + workflow.getConfig().putSingle("validation_error", e.getMessage()); + updateWorkflow(workflow, workflow.getConfig()); + log.debugf("Workflow %s was disabled due to: %s", workflow.getId(), e.getMessage()); + } + }); + } + + public void runScheduledSteps() { + this.getWorkflows().stream().filter(Workflow::isEnabled).forEach(workflow -> { + + for (ScheduledStep scheduled : workflowStateProvider.getDueScheduledSteps(workflow)) { + List steps = getSteps(workflow.getId()); + + for (int i = 0; i < steps.size(); i++) { + WorkflowStep currentStep = steps.get(i); + + if (currentStep.getId().equals(scheduled.stepId())) { + getStepProvider(currentStep).run(List.of(scheduled.resourceId())); + + if (steps.size() > i + 1) { + // schedule the next step using the time offset difference between the steps. + WorkflowStep nextStep = steps.get(i + 1); + workflowStateProvider.scheduleStep(workflow, nextStep, scheduled.resourceId()); + } else { + // this was the last step, check if the workflow is recurring - i.e. if we need to schedule the first step again + if (workflow.isRecurring()) { + WorkflowStep firstStep = getFirstStep(workflow); + workflowStateProvider.scheduleStep(workflow, firstStep, scheduled.resourceId()); + } else { + // not recurring, remove the state record + workflowStateProvider.remove(workflow.getId(), scheduled.resourceId()); + } + } + } + } + } + }); + } + + public void removeWorkflow(String id) { + RealmModel realm = getRealm(); + realm.getComponentsStream(realm.getId(), WorkflowProvider.class.getName()) + .filter(workflow -> workflow.getId().equals(id)) + .forEach(workflow -> { + realm.getComponentsStream(workflow.getId(), WorkflowStepProvider.class.getName()).forEach(realm::removeComponent); + realm.removeComponent(workflow); + }); + workflowStateProvider.remove(id); + } + + public Workflow getWorkflow(String id) { + return new Workflow(getWorkflowComponent(id)); + } + + public void updateWorkflow(Workflow workflow, MultivaluedHashMap config) { + ComponentModel component = getWorkflowComponent(workflow.getId()); + component.setConfig(config); + getRealm().updateComponent(component); + } + + private ComponentModel getWorkflowComponent(String id) { + ComponentModel component = getRealm().getComponent(id); + + if (component == null || !WorkflowProvider.class.getName().equals(component.getProviderType())) { + throw new BadRequestException("Not a valid resource workflow: " + id); + } + + return component; + } + + public WorkflowRepresentation toRepresentation(Workflow workflow) { + WorkflowRepresentation rep = new WorkflowRepresentation(workflow.getId(), workflow.getProviderId(), workflow.getConfig()); + + for (WorkflowStep step : getSteps(workflow.getId())) { + rep.addStep(toRepresentation(step)); + } + + return rep; + } + + private WorkflowStepRepresentation toRepresentation(WorkflowStep step) { + List steps = step.getSteps().stream().map(this::toRepresentation).toList(); + return new WorkflowStepRepresentation(step.getId(), step.getProviderId(), step.getConfig(), steps); + } + + public Workflow toModel(WorkflowRepresentation rep) { + MultivaluedHashMap config = ofNullable(rep.getConfig()).orElse(new MultivaluedHashMap<>()); + List conditions = ofNullable(rep.getConditions()).orElse(List.of()); + + for (WorkflowConditionRepresentation condition : conditions) { + String conditionProviderId = condition.getProviderId(); + config.computeIfAbsent("conditions", key -> new ArrayList<>()).add(conditionProviderId); + + for (Entry> configEntry : condition.getConfig().entrySet()) { + config.put(conditionProviderId + "." + configEntry.getKey(), configEntry.getValue()); + } + } + + Workflow workflow = addWorkflow(rep.getProviderId(), config); + List steps = new ArrayList<>(); + + for (WorkflowStepRepresentation stepRep : rep.getSteps()) { + steps.add(toModel(stepRep)); + } + + createSteps(workflow, steps); + + return workflow; + } + + private WorkflowStep toModel(WorkflowStepRepresentation rep) { + List subSteps = new ArrayList<>(); + + for (WorkflowStepRepresentation subStep : ofNullable(rep.getSteps()).orElse(List.of())) { + subSteps.add(toModel(subStep)); + } + + return new WorkflowStep(rep.getProviderId(), rep.getConfig(), subSteps); + } + + public void bind(Workflow workflow, ResourceType type, String resourceId) { + processEvent(List.of(workflow), new AdhocWorkflowEvent(type, resourceId)); + } + + public Object resolveResource(ResourceType type, String resourceId) { + Objects.requireNonNull(type, "type"); + Objects.requireNonNull(type, "resourceId"); + return type.resolveResource(session, resourceId); + } +} diff --git a/services/src/main/java/org/keycloak/realm/resources/policies/admin/resource/RealmResourcePoliciesResource.java b/services/src/main/java/org/keycloak/realm/resources/policies/admin/resource/RealmResourcePoliciesResource.java deleted file mode 100644 index 669bbb1f299..00000000000 --- a/services/src/main/java/org/keycloak/realm/resources/policies/admin/resource/RealmResourcePoliciesResource.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.keycloak.realm.resources.policies.admin.resource; - -import java.util.List; - -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.NotFoundException; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.policy.ResourcePolicy; -import org.keycloak.models.policy.ResourcePolicyManager; -import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation; - -class RealmResourcePoliciesResource { - - private final KeycloakSession session; - private final ResourcePolicyManager manager; - - public RealmResourcePoliciesResource(KeycloakSession session) { - this.session = session; - manager = new ResourcePolicyManager(session); - } - - @POST - @Consumes(MediaType.APPLICATION_JSON) - public Response create(ResourcePolicyRepresentation rep) { - ResourcePolicy policy = manager.toModel(rep); - return Response.created(session.getContext().getUri().getRequestUriBuilder().path(policy.getId()).build()).build(); - } - - @POST - @Consumes(MediaType.APPLICATION_JSON) - public Response createAll(List reps) { - for (ResourcePolicyRepresentation policy : reps) { - manager.toModel(policy); - } - return Response.created(session.getContext().getUri().getRequestUri()).build(); - } - - @Path("{id}") - public RealmResourcePolicyResource get(@PathParam("id") String id) { - ResourcePolicy policy = manager.getPolicy(id); - - if (policy == null) { - throw new NotFoundException("Resource policy with id " + id + " not found"); - } - - return new RealmResourcePolicyResource(manager, policy); - } - - @GET - @Produces(MediaType.APPLICATION_JSON) - public List list() { - return manager.getPolicies().stream().map(manager::toRepresentation).toList(); - } -} diff --git a/services/src/main/java/org/keycloak/realm/resources/policies/admin/resource/RealmResourcesResource.java b/services/src/main/java/org/keycloak/realm/resources/policies/admin/resource/RealmResourcesResource.java deleted file mode 100644 index 240e4b0a171..00000000000 --- a/services/src/main/java/org/keycloak/realm/resources/policies/admin/resource/RealmResourcesResource.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.keycloak.realm.resources.policies.admin.resource; - -import jakarta.ws.rs.NotFoundException; -import jakarta.ws.rs.Path; -import org.keycloak.common.Profile; -import org.keycloak.common.Profile.Feature; -import org.keycloak.models.KeycloakSession; - -public class RealmResourcesResource { - - private final KeycloakSession session; - - public RealmResourcesResource(KeycloakSession session) { - if (!Profile.isFeatureEnabled(Feature.RESOURCE_LIFECYCLE)) { - throw new NotFoundException(); - } - this.session = session; - } - - @Path("policies") - public RealmResourcePoliciesResource policies() { - return new RealmResourcePoliciesResource(session); - } -} diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index 211390ed695..eed5b42a529 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -86,7 +86,7 @@ import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.organization.admin.resource.OrganizationsResource; import org.keycloak.partialimport.PartialImportResult; import org.keycloak.partialimport.PartialImportResults; -import org.keycloak.realm.resources.policies.admin.resource.RealmResourcesResource; +import org.keycloak.workflow.admin.resource.WorkflowsResource; import org.keycloak.representations.adapters.action.GlobalRequestResult; import org.keycloak.representations.idm.AdminEventRepresentation; import org.keycloak.representations.idm.ClientRepresentation; @@ -637,9 +637,9 @@ public class RealmAdminResource { return new OrganizationsResource(session, auth, adminEvent); } - @Path("resources") - public RealmResourcesResource resources() { - return new RealmResourcesResource(session); + @Path("workflows") + public WorkflowsResource workflows() { + return new WorkflowsResource(session); } @Path("{extension}") diff --git a/services/src/main/java/org/keycloak/realm/resources/policies/admin/resource/RealmResourcePolicyResource.java b/services/src/main/java/org/keycloak/workflow/admin/resource/WorkflowResource.java similarity index 50% rename from services/src/main/java/org/keycloak/realm/resources/policies/admin/resource/RealmResourcePolicyResource.java rename to services/src/main/java/org/keycloak/workflow/admin/resource/WorkflowResource.java index a772b7805c3..f34f32baa0a 100644 --- a/services/src/main/java/org/keycloak/realm/resources/policies/admin/resource/RealmResourcePolicyResource.java +++ b/services/src/main/java/org/keycloak/workflow/admin/resource/WorkflowResource.java @@ -1,4 +1,4 @@ -package org.keycloak.realm.resources.policies.admin.resource; +package org.keycloak.workflow.admin.resource; import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; @@ -12,35 +12,35 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; -import org.keycloak.models.policy.ResourcePolicy; -import org.keycloak.models.policy.ResourcePolicyManager; -import org.keycloak.models.policy.ResourceType; -import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation; +import org.keycloak.models.workflow.ResourceType; +import org.keycloak.models.workflow.Workflow; +import org.keycloak.models.workflow.WorkflowsManager; +import org.keycloak.representations.workflows.WorkflowRepresentation; -class RealmResourcePolicyResource { +public class WorkflowResource { - private final ResourcePolicyManager manager; - private final ResourcePolicy policy; + private final WorkflowsManager manager; + private final Workflow workflow; - public RealmResourcePolicyResource(ResourcePolicyManager manager, ResourcePolicy policy) { + public WorkflowResource(WorkflowsManager manager, Workflow workflow) { this.manager = manager; - this.policy = policy; + this.workflow = workflow; } @DELETE - public void delete(String id) { - manager.removePolicy(policy.getId()); + public void delete() { + manager.removeWorkflow(workflow.getId()); } @PUT - public void update(ResourcePolicyRepresentation rep) { - manager.updatePolicy(policy, rep.getConfig()); + public void update(WorkflowRepresentation rep) { + manager.updateWorkflow(workflow, rep.getConfig()); } @GET @Produces(APPLICATION_JSON) - public ResourcePolicyRepresentation toRepresentation() { - return manager.toRepresentation(policy); + public WorkflowRepresentation toRepresentation() { + return manager.toRepresentation(workflow); } @POST @@ -54,9 +54,9 @@ class RealmResourcePolicyResource { } if (notBefore != null) { - policy.setNotBefore(notBefore); + workflow.setNotBefore(notBefore); } - manager.bind(policy, type, resourceId); + manager.bind(workflow, type, resourceId); } } diff --git a/services/src/main/java/org/keycloak/workflow/admin/resource/WorkflowsResource.java b/services/src/main/java/org/keycloak/workflow/admin/resource/WorkflowsResource.java new file mode 100644 index 00000000000..da86efe2eed --- /dev/null +++ b/services/src/main/java/org/keycloak/workflow/admin/resource/WorkflowsResource.java @@ -0,0 +1,66 @@ +package org.keycloak.workflow.admin.resource; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.keycloak.common.Profile; +import org.keycloak.common.Profile.Feature; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.workflow.Workflow; +import org.keycloak.models.workflow.WorkflowsManager; +import org.keycloak.representations.workflows.WorkflowRepresentation; + +import java.util.List; + +public class WorkflowsResource { + + private final KeycloakSession session; + private final WorkflowsManager manager; + + public WorkflowsResource(KeycloakSession session) { + if (!Profile.isFeatureEnabled(Feature.WORKFLOWS)) { + throw new NotFoundException(); + } + this.session = session; + this.manager = new WorkflowsManager(session); + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + public Response create(WorkflowRepresentation rep) { + Workflow workflow = manager.toModel(rep); + return Response.created(session.getContext().getUri().getRequestUriBuilder().path(workflow.getId()).build()).build(); + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + public Response createAll(List reps) { + for (WorkflowRepresentation workflow : reps) { + manager.toModel(workflow); + } + return Response.created(session.getContext().getUri().getRequestUri()).build(); + } + + @Path("{id}") + public WorkflowResource get(@PathParam("id") String id) { + Workflow workflow = manager.getWorkflow(id); + + if (workflow == null) { + throw new NotFoundException("Workflow with id " + id + " not found"); + } + + return new WorkflowResource(manager, workflow); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + public List list() { + return manager.getWorkflows().stream().map(manager::toRepresentation).toList(); + } +} diff --git a/services/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory index 129f1051acb..99b592490c1 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory @@ -17,4 +17,4 @@ org.keycloak.events.email.EmailEventListenerProviderFactory org.keycloak.events.log.JBossLoggingEventListenerProviderFactory -org.keycloak.models.policy.ResourcePolicyEventListenerFactory \ No newline at end of file +org.keycloak.models.workflow.WorkflowsEventListenerFactory \ No newline at end of file diff --git a/services/src/main/resources/META-INF/services/org.keycloak.models.policy.ResourceActionProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.models.workflow.WorkflowStepProviderFactory similarity index 64% rename from services/src/main/resources/META-INF/services/org.keycloak.models.policy.ResourceActionProviderFactory rename to services/src/main/resources/META-INF/services/org.keycloak.models.workflow.WorkflowStepProviderFactory index dd7f0abb211..a309dc3bce1 100644 --- a/services/src/main/resources/META-INF/services/org.keycloak.models.policy.ResourceActionProviderFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.models.workflow.WorkflowStepProviderFactory @@ -15,10 +15,9 @@ # limitations under the License. # -org.keycloak.models.policy.DisableUserActionProviderFactory -org.keycloak.models.policy.NotifyUserActionProviderFactory -org.keycloak.models.policy.DeleteUserActionProviderFactory -org.keycloak.models.policy.SetUserAttributeActionProviderFactory -org.keycloak.models.policy.AggregatedActionProviderFactory -org.keycloak.models.policy.AddRequiredActionProviderFactory - +org.keycloak.models.workflow.DisableUserStepProviderFactory +org.keycloak.models.workflow.NotifyUserStepProviderFactory +org.keycloak.models.workflow.DeleteUserStepProviderFactory +org.keycloak.models.workflow.SetUserAttributeStepProviderFactory +org.keycloak.models.workflow.AggregatedStepProviderFactory +org.keycloak.models.workflow.AddRequiredActionStepProviderFactory diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/AddRequiredActionTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/AddRequiredActionTest.java deleted file mode 100644 index 09a49e4042b..00000000000 --- a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/AddRequiredActionTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.keycloak.tests.admin.model.policy; - -import org.junit.jupiter.api.Test; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.policy.AddRequiredActionProviderFactory; -import org.keycloak.models.policy.UserCreationTimeResourcePolicyProviderFactory; -import org.keycloak.representations.idm.UserRepresentation; -import org.keycloak.representations.resources.policies.ResourcePolicyActionRepresentation; -import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation; -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.UserConfigBuilder; -import org.keycloak.testframework.remote.runonserver.InjectRunOnServer; -import org.keycloak.testframework.remote.runonserver.RunOnServerClient; - -import java.util.List; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.is; - -@KeycloakIntegrationTest(config = RLMServerConfig.class) -public class AddRequiredActionTest { - - private static final String REALM_NAME = "default"; - - @InjectRunOnServer(permittedPackages = "org.keycloak.tests") - RunOnServerClient runOnServer; - - @InjectRealm(lifecycle = LifeCycle.METHOD) - ManagedRealm managedRealm; - - @Test - public void testActionRun() { - managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() - .of(UserCreationTimeResourcePolicyProviderFactory.ID) - .immediate() - .withActions( - ResourcePolicyActionRepresentation.create() - .of(AddRequiredActionProviderFactory.ID) - .withConfig("action", "UPDATE_PASSWORD") - .build() - ).build()).close(); - - managedRealm.admin().users().create(UserConfigBuilder.create().username("test").build()).close(); - - List< UserRepresentation> users = managedRealm.admin().users().search("test"); - assertThat(users, hasSize(1)); - UserRepresentation userRepresentation = users.get(0); - assertThat(userRepresentation.getRequiredActions(), hasSize(1)); - assertThat(userRepresentation.getRequiredActions().get(0), is(UserModel.RequiredAction.UPDATE_PASSWORD.name())); - } - - private static RealmModel configureSessionContext(KeycloakSession session) { - RealmModel realm = session.realms().getRealmByName(REALM_NAME); - session.getContext().setRealm(realm); - return realm; - } - -} diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/RLMScheduledTaskServerConfig.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/RLMScheduledTaskServerConfig.java deleted file mode 100644 index 10af82082d8..00000000000 --- a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/RLMScheduledTaskServerConfig.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.keycloak.tests.admin.model.policy; - -import org.keycloak.models.policy.ResourcePolicyEventListenerFactory; -import org.keycloak.testframework.server.KeycloakServerConfigBuilder; - -public class RLMScheduledTaskServerConfig extends RLMServerConfig { - - @Override - public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) { - return super.configure(config) - .option("spi-events-listener--" + ResourcePolicyEventListenerFactory.ID + "--action-runner-task-interval", "1000"); - } -} diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/ResourcePolicyManagementTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/ResourcePolicyManagementTest.java deleted file mode 100644 index 3c8a8437c8c..00000000000 --- a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/ResourcePolicyManagementTest.java +++ /dev/null @@ -1,871 +0,0 @@ -/* - * Copyright 2025 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -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.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 java.time.Duration; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; - -import jakarta.ws.rs.core.Response; -import org.hamcrest.Matchers; -import jakarta.mail.MessagingException; -import jakarta.mail.internet.MimeMessage; -import java.io.IOException; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.keycloak.admin.client.resource.RealmResourcePolicies; -import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory; -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.DeleteUserActionProviderFactory; -import org.keycloak.models.policy.DisableUserActionProviderFactory; -import org.keycloak.models.policy.EventBasedResourcePolicyProviderFactory; -import org.keycloak.models.policy.NotifyUserActionProviderFactory; -import org.keycloak.models.policy.ResourceAction; -import org.keycloak.models.policy.ResourceOperationType; -import org.keycloak.models.policy.ResourcePolicy; -import org.keycloak.models.policy.ResourcePolicyManager; -import org.keycloak.models.policy.ResourcePolicyStateProvider; -import org.keycloak.models.policy.ResourcePolicyStateProvider.ScheduledAction; -import org.keycloak.models.policy.SetUserAttributeActionProviderFactory; -import org.keycloak.models.policy.UserCreationTimeResourcePolicyProviderFactory; -import org.keycloak.models.policy.conditions.IdentityProviderPolicyConditionFactory; -import org.keycloak.representations.idm.IdentityProviderRepresentation; -import org.keycloak.representations.resources.policies.ResourcePolicyActionRepresentation; -import org.keycloak.representations.resources.policies.ResourcePolicyConditionRepresentation; -import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation; -import org.keycloak.testframework.annotations.InjectRealm; -import org.keycloak.testframework.annotations.InjectUser; -import org.keycloak.testframework.mail.MailServer; -import org.keycloak.testframework.mail.annotations.InjectMailServer; -import org.keycloak.testframework.annotations.KeycloakIntegrationTest; -import org.keycloak.testframework.injection.LifeCycle; -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; -import org.keycloak.tests.utils.MailUtils; - -@KeycloakIntegrationTest(config = RLMServerConfig.class) -public class ResourcePolicyManagementTest { - - private static final String REALM_NAME = "default"; - - @InjectRunOnServer(permittedPackages = "org.keycloak.tests") - RunOnServerClient runOnServer; - - @InjectRealm(lifecycle = LifeCycle.METHOD) - ManagedRealm managedRealm; - - @InjectUser(ref = "alice", config = DefaultUserConfig.class, lifecycle = LifeCycle.METHOD) - private ManagedUser userAlice; - - @InjectMailServer - private MailServer mailServer; - - @Test - public void testCreate() { - List expectedPolicies = ResourcePolicyRepresentation.create() - .of(UserCreationTimeResourcePolicyProviderFactory.ID) - .withActions( - ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID) - .after(Duration.ofDays(5)) - .build(), - ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID) - .after(Duration.ofDays(5)) - .build() - ).build(); - - RealmResourcePolicies policies = managedRealm.admin().resources().policies(); - - try (Response response = policies.create(expectedPolicies)) { - assertThat(response.getStatus(), is(Response.Status.CREATED.getStatusCode())); - } - - List actualPolicies = policies.list(); - assertThat(actualPolicies, Matchers.hasSize(1)); - - assertThat(actualPolicies.get(0).getProviderId(), is(UserCreationTimeResourcePolicyProviderFactory.ID)); - assertThat(actualPolicies.get(0).getActions(), Matchers.hasSize(2)); - assertThat(actualPolicies.get(0).getActions().get(0).getProviderId(), is(NotifyUserActionProviderFactory.ID)); - assertThat(actualPolicies.get(0).getActions().get(1).getProviderId(), is(DisableUserActionProviderFactory.ID)); - } - - @Test - public void testCreateWithNoConditions() { - List expectedPolicies = ResourcePolicyRepresentation.create() - .of(EventBasedResourcePolicyProviderFactory.ID) - .withActions( - ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID) - .after(Duration.ofDays(5)) - .build(), - ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID) - .after(Duration.ofDays(5)) - .build() - ).build(); - - expectedPolicies.get(0).setConditions(null); - - RealmResourcePolicies policies = managedRealm.admin().resources().policies(); - - try (Response response = policies.create(expectedPolicies)) { - assertThat(response.getStatus(), is(Response.Status.CREATED.getStatusCode())); - } - } - - @Test - public void testDelete() { - RealmResourcePolicies policies = managedRealm.admin().resources().policies(); - - policies.create(ResourcePolicyRepresentation.create() - .of(UserCreationTimeResourcePolicyProviderFactory.ID) - .onEvent(ResourceOperationType.CREATE.toString()) - .recurring() - .withActions( - ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID) - .after(Duration.ofDays(5)) - .build() - ).of(EventBasedResourcePolicyProviderFactory.ID) - .onEvent(ResourceOperationType.LOGIN.toString()) - .recurring() - .withActions( - ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID) - .after(Duration.ofDays(5)) - .build() - ).build()).close(); - - // create a new user - should bind the user to the policy and setup the only action in the policy - managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").email("testuser@example.com").build()).close(); - - List actualPolicies = policies.list(); - assertThat(actualPolicies, Matchers.hasSize(2)); - - ResourcePolicyRepresentation policy = actualPolicies.stream().filter(p -> UserCreationTimeResourcePolicyProviderFactory.ID.equals(p.getProviderId())).findAny().orElse(null); - String id = policy.getId(); - policies.policy(id).delete().close(); - actualPolicies = policies.list(); - assertThat(actualPolicies, Matchers.hasSize(1)); - - runOnServer.run((RunOnServer) session -> { - configureSessionContext(session); - ResourcePolicyManager manager = new ResourcePolicyManager(session); - - List registeredPolicies = manager.getPolicies(); - assertEquals(1, registeredPolicies.size()); - ResourcePolicyStateProvider stateProvider = session.getKeycloakSessionFactory().getProviderFactory(ResourcePolicyStateProvider.class).create(session); - List actions = stateProvider.getScheduledActionsByPolicy(id); - assertTrue(actions.isEmpty()); - }); - } - - @Test - public void testUpdate() { - List expectedPolicies = ResourcePolicyRepresentation.create() - .of(UserCreationTimeResourcePolicyProviderFactory.ID) - .name("test-policy") - .withActions( - ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID) - .after(Duration.ofDays(5)) - .build(), - ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID) - .after(Duration.ofDays(5)) - .build() - ).build(); - - RealmResourcePolicies policies = managedRealm.admin().resources().policies(); - - try (Response response = policies.create(expectedPolicies)) { - assertThat(response.getStatus(), is(Response.Status.CREATED.getStatusCode())); - } - - List actualPolicies = policies.list(); - assertThat(actualPolicies, Matchers.hasSize(1)); - ResourcePolicyRepresentation policy = actualPolicies.get(0); - assertThat(policy.getName(), is("test-policy")); - - policy.setName("changed"); - managedRealm.admin().resources().policies().policy(policy.getId()).update(policy).close(); - actualPolicies = policies.list(); - policy = actualPolicies.get(0); - assertThat(policy.getName(), is("changed")); - } - - @Test - 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)) - .build(), - ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID) - .after(Duration.ofDays(5)) - .build() - ).build()).close(); - - // create a new user - should bind the user to the policy and setup the first action - managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").email("testuser@example.com").build()).close(); - - runOnServer.run((RunOnServer) session -> { - RealmModel realm = configureSessionContext(session); - ResourcePolicyManager manager = new ResourcePolicyManager(session); - UserModel user = session.users().getUserByUsername(realm,"testuser"); - - List registeredPolicies = manager.getPolicies(); - assertEquals(1, registeredPolicies.size()); - - ResourcePolicy policy = registeredPolicies.get(0); - assertEquals(2, manager.getActions(policy.getId()).size()); - ResourceAction notifyAction = manager.getActions(policy.getId()).get(0); - - ResourcePolicyStateProvider stateProvider = session.getProvider(ResourcePolicyStateProvider.class); - 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.runScheduledActions(); - - user = session.users().getUserById(realm, user.getId()); - - // Verify that the next action was scheduled for the user - ResourceAction disableAction = manager.getActions(policy.getId()).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); - } - }); - - // Verify that the first action (notify) was executed by checking email was sent - MimeMessage testUserMessage = findEmailByRecipient(mailServer, "testuser@example.com"); - assertNotNull(testUserMessage, "The first action (notify) should have sent an email."); - - mailServer.runCleanup(); - } - - @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()).close(); - } - - // 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()).close(); - } - - IdentityProviderRepresentation idp = new IdentityProviderRepresentation(); - idp.setAlias("someidp"); - idp.setProviderId(KeycloakOIDCIdentityProviderFactory.PROVIDER_ID); - idp.setEnabled(true); - managedRealm.admin().identityProviders().create(idp).close(); - - managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() - .of(UserCreationTimeResourcePolicyProviderFactory.ID) - .onEvent(ResourceOperationType.ADD_FEDERATED_IDENTITY.name()) - .onConditions(ResourcePolicyConditionRepresentation.create() - .of(IdentityProviderPolicyConditionFactory.ID) - .withConfig(IdentityProviderPolicyConditionFactory.EXPECTED_ALIASES, "someidp") - .build()) - .withActions( - ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID) - .after(Duration.ofDays(5)) - .build(), - ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID) - .after(Duration.ofDays(5)) - .build() - ).build()).close(); - - // 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()).close(); - } - - // 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()).close(); - } - - runOnServer.run((RunOnServer) session -> { - RealmModel realm = configureSessionContext(session); - ResourcePolicyManager policyManager = new ResourcePolicyManager(session); - List registeredPolicies = policyManager.getPolicies(); - assertEquals(1, registeredPolicies.size()); - ResourcePolicy policy = registeredPolicies.get(0); - - assertEquals(2, policyManager.getActions(policy.getId()).size()); - ResourceAction notifyAction = policyManager.getActions(policy.getId()).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 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.runScheduledActions(); - - // check the same users are now scheduled to run the second action. - ResourceAction disableAction = policyManager.getActions(policy.getId()).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 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 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); - } - }); - } - - @Test - public void testDisableResourcePolicy() { - // create a test policy - managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() - .of(UserCreationTimeResourcePolicyProviderFactory.ID) - .onEvent(ResourceOperationType.CREATE.toString()) - .name("test-policy") - .withActions( - ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID) - .after(Duration.ofDays(5)) - .build(), - ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID) - .after(Duration.ofDays(5)) - .build() - ).build()).close(); - - RealmResourcePolicies policies = managedRealm.admin().resources().policies(); - List actualPolicies = policies.list(); - assertThat(actualPolicies, Matchers.hasSize(1)); - ResourcePolicyRepresentation policy = actualPolicies.get(0); - assertThat(policy.getName(), is("test-policy")); - - // create a new user - should bind the user to the policy and setup the first action - managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").email("testuser@example.com").build()).close(); - - runOnServer.run((RunOnServer) session -> { - RealmModel realm = configureSessionContext(session); - ResourcePolicyManager manager = new ResourcePolicyManager(session); - - try { - // Advance time so the user is eligible for the first action, then run the scheduled actions so they transition to the next one. - Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds())); - manager.runScheduledActions(); - - UserModel user = session.users().getUserByUsername(realm, "testuser"); - assertTrue(user.isEnabled(), "The second action (disable) should NOT have run."); - } finally { - Time.setOffset(0); - } - }); - - // Verify that the first action (notify) was executed by checking email was sent - MimeMessage testUserMessage = findEmailByRecipient(mailServer, "testuser@example.com"); - assertNotNull(testUserMessage, "The first action (notify) should have sent an email."); - - mailServer.runCleanup(); - - // disable the policy - scheduled actions should be paused and policy should not activate for new users - policy.getConfig().putSingle("enabled", "false"); - managedRealm.admin().resources().policies().policy(policy.getId()).update(policy).close(); - - // create another user - should NOT bind the user to the policy as it is disabled - managedRealm.admin().users().create(UserConfigBuilder.create().username("anotheruser").build()).close(); - - runOnServer.run((RunOnServer) session -> { - RealmModel realm = configureSessionContext(session); - ResourcePolicyManager manager = new ResourcePolicyManager(session); - - List registeredPolicies = manager.getPolicies(); - assertEquals(1, registeredPolicies.size()); - ResourcePolicyStateProvider stateProvider = session.getKeycloakSessionFactory().getProviderFactory(ResourcePolicyStateProvider.class).create(session); - List scheduledActions = stateProvider.getScheduledActionsByPolicy(registeredPolicies.get(0)); - - // verify that there's only one scheduled action, for the first user - assertEquals(1, scheduledActions.size()); - UserModel scheduledActionUser = session.users().getUserById(realm, scheduledActions.get(0).resourceId()); - assertNotNull(scheduledActionUser); - assertTrue(scheduledActionUser.getUsername().startsWith("testuser")); - - try { - // Advance time so the first user would be eligible for the second action, then run the scheduled actions. - Time.setOffset(Math.toIntExact(Duration.ofDays(12).toSeconds())); - manager.runScheduledActions(); - - UserModel user = session.users().getUserByUsername(realm, "testuser"); - // Verify that the action was NOT executed as the policy is disabled. - assertTrue(user.isEnabled(), "The second action (disable) should NOT have run as the policy is disabled."); - } finally { - Time.setOffset(0); - } - }); - - // re-enable the policy - scheduled actions should resume and new users should be bound to the policy - policy.getConfig().putSingle("enabled", "true"); - managedRealm.admin().resources().policies().policy(policy.getId()).update(policy).close(); - - // create a third user - should bind the user to the policy as it is enabled again - managedRealm.admin().users().create(UserConfigBuilder.create().username("thirduser").email("thirduser@example.com").build()).close(); - - runOnServer.run((RunOnServer) session -> { - RealmModel realm = configureSessionContext(session); - ResourcePolicyManager manager = new ResourcePolicyManager(session); - - try { - // Advance time so the first user would be eligible for the second action, and third user would be eligible for the first action, then run the scheduled actions. - Time.setOffset(Math.toIntExact(Duration.ofDays(12).toSeconds())); - manager.runScheduledActions(); - - UserModel user = session.users().getUserByUsername(realm, "testuser"); - // Verify that the action was executed as the policy was re-enabled. - assertFalse(user.isEnabled(), "The second action (disable) should have run as the policy was re-enabled."); - - // Verify that the third user was bound to the policy - user = session.users().getUserByUsername(realm, "thirduser"); - assertTrue(user.isEnabled(), "The second action (disable) should NOT have run"); - } finally { - Time.setOffset(0); - } - }); - - // Verify that the first action (notify) was executed by checking email was sent - testUserMessage = findEmailByRecipient(mailServer, "thirduser@example.com"); - assertNotNull(testUserMessage, "The first action (notify) should have sent an email."); - - mailServer.runCleanup(); - } - - @Test - public void testRecurringPolicy() { - managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() - .of(UserCreationTimeResourcePolicyProviderFactory.ID) - .onEvent(ResourceOperationType.CREATE.toString()) - .recurring() - .withActions( - ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID) - .after(Duration.ofDays(5)) - .build() - ).build()).close(); - - // create a new user - should bind the user to the policy and setup the only action in the policy - managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").email("testuser@example.com").build()).close(); - - runOnServer.run((RunOnServer) session -> { - RealmModel realm = configureSessionContext(session); - ResourcePolicyManager manager = new ResourcePolicyManager(session); - - try { - Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds())); - manager.runScheduledActions(); - - UserModel user = session.users().getUserByUsername(realm, "testuser"); - ResourcePolicy policy = manager.getPolicies().get(0); - ResourceAction action = manager.getActions(policy.getId()).get(0); - - // Verify that the action was scheduled again for the user - ResourcePolicyStateProvider stateProvider = session.getProvider(ResourcePolicyStateProvider.class); - ResourcePolicyStateProvider.ScheduledAction scheduledAction = stateProvider.getScheduledAction(policy.getId(), user.getId()); - assertNotNull(scheduledAction, "An action should have been scheduled for the user " + user.getUsername()); - assertEquals(action.getId(), scheduledAction.actionId(), "The action should have been scheduled again"); - - Time.setOffset(Math.toIntExact(Duration.ofDays(12).toSeconds())); - manager.runScheduledActions(); - } finally { - Time.setOffset(0); - } - }); - - // Verify that there should be two emails sent - assertEquals(2, findEmailsByRecipient(mailServer, "testuser@example.com").size()); - mailServer.runCleanup(); - } - - @Test - public void testRunImmediatePolicy() { - // create a test policy with no time conditions - should run immediately when scheduled - managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() - .of(UserCreationTimeResourcePolicyProviderFactory.ID) - .immediate() - .withActions( - ResourcePolicyActionRepresentation.create().of(SetUserAttributeActionProviderFactory.ID) - .after(Duration.ofDays(1)) - .withConfig("message", "message") - .build(), - ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID) - .after(Duration.ofDays(1)) - .build() - ).build()).close(); - - // create a new user - should be bound to the new policy and all actions should run right away - managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").build()).close(); - - // check the user has the attribute set and is disabled - runOnServer.run(session -> { - configureSessionContext(session); - UserModel user = session.users().getUserByUsername(session.getContext().getRealm(), "testuser"); - assertEquals("message", user.getAttributes().get("message").get(0)); - assertFalse(user.isEnabled()); - }); - } - - @Test - public void testNotifyUserActionSendsEmailWithDefaultDisableMessage() { - // Create policy: disable at 10 days, notify 3 days before (at day 7) - managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() - .of(UserCreationTimeResourcePolicyProviderFactory.ID) - .withActions( - ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID) - .after(Duration.ofDays(7)) - .withConfig("reason", "inactivity") - .build(), - ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID) - .after(Duration.ofDays(3)) - .build() - ).build()).close(); - - managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").email("test@example.com").name("John", "").build()).close(); - - runOnServer.run(session -> { - ResourcePolicyManager manager = new ResourcePolicyManager(session); - - try { - // Simulate user being 7 days old (eligible for notify action) - Time.setOffset(Math.toIntExact(Duration.ofDays(7).toSeconds())); - - manager.runScheduledActions(); - } finally { - Time.setOffset(0); - } - }); - - // Verify email was sent to our test user - MimeMessage testUserMessage = findEmailByRecipient(mailServer, "test@example.com"); - assertNotNull(testUserMessage, "No email found for test@example.com"); - verifyEmailContent(testUserMessage, "test@example.com", "Disable", "John", "3", "inactivity"); - - mailServer.runCleanup(); - } - - @Test - public void testNotifyUserActionSendsEmailWithDefaultDeleteMessage() { - // Create policy: delete at 30 days, notify 15 days before (at day 15) - managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() - .of(UserCreationTimeResourcePolicyProviderFactory.ID) - .withActions( - ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID) - .after(Duration.ofDays(15)) - .withConfig("reason", "inactivity") - .build(), - ResourcePolicyActionRepresentation.create().of(DeleteUserActionProviderFactory.ID) - .after(Duration.ofDays(15)) - .build() - ).build()).close(); - - managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser2").email("test2@example.com").name("Jane", "").build()).close(); - - runOnServer.run(session -> { - - ResourcePolicyManager manager = new ResourcePolicyManager(session); - - try { - // Simulate user being 15 days old - Time.setOffset(Math.toIntExact(Duration.ofDays(15).toSeconds())); - manager.runScheduledActions(); - } finally { - Time.setOffset(0); - } - }); - - // Verify email was sent to our test user - MimeMessage testUserMessage = findEmailByRecipient(mailServer, "test2@example.com"); - assertNotNull(testUserMessage, "No email found for test2@example.com"); - verifyEmailContent(testUserMessage, "test2@example.com", "Deletion", "Jane", "15", "inactivity", "permanently deleted"); - - mailServer.runCleanup(); - } - - @Test - public void testNotifyUserActionWithCustomMessageOverride() { - // Create policy: disable at 7 days, notify 2 days before (at day 5) with custom message - managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() - .of(UserCreationTimeResourcePolicyProviderFactory.ID) - .withActions( - ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID) - .after(Duration.ofDays(5)) - .withConfig("reason", "compliance requirement") - .withConfig("custom_message", "Your account requires immediate attention due to new compliance policies.") - .withConfig("custom_subject_key", "customComplianceSubject") - .build(), - ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID) - .after(Duration.ofDays(2)) - .build() - ).build()).close(); - - managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser3").email("test3@example.com").name("Bob", "").build()).close(); - - runOnServer.run(session -> { - ResourcePolicyManager manager = new ResourcePolicyManager(session); - - try { - // Simulate user being 5 days old - Time.setOffset(Math.toIntExact(Duration.ofDays(5).toSeconds())); - manager.runScheduledActions(); - } finally { - Time.setOffset(0); - } - }); - - // Verify email was sent to our test user - MimeMessage testUserMessage = findEmailByRecipient(mailServer, "test3@example.com"); - assertNotNull(testUserMessage, "No email found for test3@example.com"); - verifyEmailContent(testUserMessage, "test3@example.com", "", "Bob", "2", "immediate attention due to new compliance policies"); - - mailServer.runCleanup(); - } - - @Test - public void testNotifyUserActionSkipsUsersWithoutEmailButLogsWarning() { - managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() - .of(UserCreationTimeResourcePolicyProviderFactory.ID) - .withActions( - ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID) - .after(Duration.ofDays(5)) - .build(), - ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID) - .after(Duration.ofDays(5)) - .build() - ).build()).close(); - - managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser4").name("NoEmail", "").build()).close(); - - runOnServer.run(session -> { - RealmModel realm = configureSessionContext(session); - ResourcePolicyManager manager = new ResourcePolicyManager(session); - - try { - Time.setOffset(Math.toIntExact(Duration.ofDays(5).toSeconds())); - manager.runScheduledActions(); - - // But should still create state record for the policy flow - UserModel user = session.users().getUserByUsername(realm, "testuser4"); - ResourcePolicyStateProvider stateProvider = session.getProvider(ResourcePolicyStateProvider.class); - var scheduledActions = stateProvider.getScheduledActionsByResource(user.getId()); - assertEquals(1, scheduledActions.size()); - } finally { - Time.setOffset(0); - } - }); - - // Should NOT send email to user without email address - MimeMessage testUserMessage = findEmailByRecipientContaining("testuser4"); - assertNull(testUserMessage, "No email should be sent to user without email address"); - } - - @Test - public void testCompleteUserLifecycleWithMultipleNotifications() { - // Create policy: just disable at 30 days with one notification before - managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() - .of(UserCreationTimeResourcePolicyProviderFactory.ID) - .withActions( - ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID) - .after(Duration.ofDays(15)) - .withConfig("reason", "inactivity") - .build(), - ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID) - .after(Duration.ofDays(15)) - .build() - ).build()).close(); - - managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser5").email("testuser5@example.com").name("TestUser5", "").build()).close(); - - runOnServer.run(session -> { - RealmModel realm = configureSessionContext(session); - ResourcePolicyManager manager = new ResourcePolicyManager(session); - UserModel user = session.users().getUserByUsername(realm, "testuser5"); - - try { - // Day 15: First notification - this should run the notify action and schedule the disable action - Time.setOffset(Math.toIntExact(Duration.ofDays(15).toSeconds())); - manager.runScheduledActions(); - - // Check that user is still enabled after notification - user = session.users().getUserById(realm, user.getId()); - assertTrue(user.isEnabled(), "User should still be enabled after notification"); - - // Day 30 + 15 minutes: Disable user - run 15 minutes after the scheduled time to ensure it's due - Time.setOffset(Math.toIntExact(Duration.ofDays(30).toSeconds()) + Math.toIntExact(Duration.ofMinutes(15).toSeconds())); - manager.runScheduledActions(); - - // Verify user is disabled - user = session.users().getUserById(realm, user.getId()); - assertNotNull(user, "User should still exist after disable"); - assertFalse(user.isEnabled(), "User should be disabled"); - - } finally { - Time.setOffset(0); - } - }); - - // Verify notification was sent - MimeMessage testUserMessage = findEmailByRecipient(mailServer, "testuser5@example.com"); - assertNotNull(testUserMessage, "No email found for testuser5@example.com"); - verifyEmailContent(testUserMessage, "testuser5@example.com", "Disable", "TestUser5", "15", "inactivity"); - - mailServer.runCleanup(); - } - - public static List findEmailsByRecipient(MailServer mailServer, String expectedRecipient) { - return Arrays.stream(mailServer.getReceivedMessages()) - .filter(msg -> { - try { - return MailUtils.getRecipient(msg).equals(expectedRecipient); - } catch (Exception e) { - return false; - } - }) - .toList(); - } - - public static MimeMessage findEmailByRecipient(MailServer mailServer, String expectedRecipient) { - return Arrays.stream(mailServer.getReceivedMessages()) - .filter(msg -> { - try { - return MailUtils.getRecipient(msg).equals(expectedRecipient); - } catch (Exception e) { - return false; - } - }) - .findFirst() - .orElse(null); - } - - private MimeMessage findEmailByRecipientContaining(String recipientPart) { - return Arrays.stream(mailServer.getReceivedMessages()) - .filter(msg -> { - try { - return MailUtils.getRecipient(msg).contains(recipientPart); - } catch (Exception e) { - return false; - } - }) - .findFirst() - .orElse(null); - } - - private static RealmModel configureSessionContext(KeycloakSession session) { - RealmModel realm = session.realms().getRealmByName(REALM_NAME); - session.getContext().setRealm(realm); - return realm; - } - - public static void verifyEmailContent(MimeMessage message, String expectedRecipient, String subjectContains, - String... contentContains) { - try { - assertEquals(expectedRecipient, MailUtils.getRecipient(message)); - assertTrue(message.getSubject().contains(subjectContains), - "Subject should contain '" + subjectContains + "'"); - - MailUtils.EmailBody body = MailUtils.getBody(message); - String textContent = body.getText(); - String htmlContent = body.getHtml(); - - for (String expectedContent : contentContains) { - boolean foundInText = textContent.contains(expectedContent); - boolean foundInHtml = htmlContent.contains(expectedContent); - assertTrue(foundInText || foundInHtml, - "Email content should contain: " + expectedContent + - "\nText: " + textContent + - "\nHTML: " + htmlContent); - } - } catch (MessagingException | IOException e) { - Assertions.fail("Failed to read email message: " + e.getMessage()); - } - } - - private static class DefaultUserConfig implements UserConfig { - - @Override - public UserConfigBuilder configure(UserConfigBuilder user) { - user.username("alice"); - user.password("alice"); - user.name("alice", "alice"); - user.email("master-admin@email.org"); - return user; - } - } -} diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/AddRequiredActionTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/AddRequiredActionTest.java new file mode 100644 index 00000000000..50a0f3b2cc3 --- /dev/null +++ b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/AddRequiredActionTest.java @@ -0,0 +1,52 @@ +package org.keycloak.tests.admin.model.workflow; + +import org.junit.jupiter.api.Test; +import org.keycloak.models.UserModel; +import org.keycloak.models.workflow.AddRequiredActionStepProvider; +import org.keycloak.models.workflow.AddRequiredActionStepProviderFactory; +import org.keycloak.models.workflow.UserCreationTimeWorkflowProviderFactory; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.representations.workflows.WorkflowStepRepresentation; +import org.keycloak.representations.workflows.WorkflowRepresentation; +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.UserConfigBuilder; + +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; + +@KeycloakIntegrationTest(config = WorkflowsServerConfig.class) +public class AddRequiredActionTest { + + private static final String REALM_NAME = "default"; + + @InjectRealm(lifecycle = LifeCycle.METHOD) + ManagedRealm managedRealm; + + @Test + public void testStepRun() { + managedRealm.admin().workflows().create(WorkflowRepresentation.create() + .of(UserCreationTimeWorkflowProviderFactory.ID) + .immediate() + .withSteps( + WorkflowStepRepresentation.create() + .of(AddRequiredActionStepProviderFactory.ID) + .withConfig(AddRequiredActionStepProvider.REQUIRED_ACTION_KEY, "UPDATE_PASSWORD") + .build() + ).build()).close(); + + managedRealm.admin().users().create(UserConfigBuilder.create().username("test").build()).close(); + + List< UserRepresentation> users = managedRealm.admin().users().search("test"); + assertThat(users, hasSize(1)); + UserRepresentation userRepresentation = users.get(0); + assertThat(userRepresentation.getRequiredActions(), hasSize(1)); + assertThat(userRepresentation.getRequiredActions().get(0), is(UserModel.RequiredAction.UPDATE_PASSWORD.name())); + } + +} diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/AdhocPolicyTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/AdhocWorkflowTest.java similarity index 58% rename from tests/base/src/test/java/org/keycloak/tests/admin/model/policy/AdhocPolicyTest.java rename to tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/AdhocWorkflowTest.java index 40874e5ac5f..6b9a77a6b1d 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/AdhocPolicyTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/AdhocWorkflowTest.java @@ -1,4 +1,4 @@ -package org.keycloak.tests.admin.model.policy; +package org.keycloak.tests.admin.model.workflow; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasSize; @@ -15,14 +15,14 @@ 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.ResourcePolicyManager; -import org.keycloak.models.policy.ResourceType; -import org.keycloak.models.policy.SetUserAttributeActionProviderFactory; +import org.keycloak.models.workflow.EventBasedWorkflowProviderFactory; +import org.keycloak.models.workflow.WorkflowsManager; +import org.keycloak.models.workflow.ResourceType; +import org.keycloak.models.workflow.SetUserAttributeStepProviderFactory; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.UserRepresentation; -import org.keycloak.representations.resources.policies.ResourcePolicyActionRepresentation; -import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation; +import org.keycloak.representations.workflows.WorkflowStepRepresentation; +import org.keycloak.representations.workflows.WorkflowRepresentation; import org.keycloak.testframework.annotations.InjectRealm; import org.keycloak.testframework.annotations.KeycloakIntegrationTest; import org.keycloak.testframework.injection.LifeCycle; @@ -31,8 +31,8 @@ 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 AdhocPolicyTest { +@KeycloakIntegrationTest(config = WorkflowsServerConfig.class) +public class AdhocWorkflowTest { private static final String REALM_NAME = "default"; @@ -44,53 +44,53 @@ public class AdhocPolicyTest { @Test public void testCreate() { - managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() - .of(EventBasedResourcePolicyProviderFactory.ID) - .withActions(ResourcePolicyActionRepresentation.create() - .of(SetUserAttributeActionProviderFactory.ID) + managedRealm.admin().workflows().create(WorkflowRepresentation.create() + .of(EventBasedWorkflowProviderFactory.ID) + .withSteps(WorkflowStepRepresentation.create() + .of(SetUserAttributeStepProviderFactory.ID) .withConfig("message", "message") .build()) .build()).close(); - List policies = managedRealm.admin().resources().policies().list(); - assertThat(policies, hasSize(1)); - ResourcePolicyRepresentation policy = policies.get(0); - assertThat(policy.getActions(), hasSize(1)); - ResourcePolicyActionRepresentation aggregatedAction = policy.getActions().get(0); - assertThat(aggregatedAction.getProviderId(), is(SetUserAttributeActionProviderFactory.ID)); + List workflows = managedRealm.admin().workflows().list(); + assertThat(workflows, hasSize(1)); + WorkflowRepresentation workflow = workflows.get(0); + assertThat(workflow.getSteps(), hasSize(1)); + WorkflowStepRepresentation aggregatedStep = workflow.getSteps().get(0); + assertThat(aggregatedStep.getProviderId(), is(SetUserAttributeStepProviderFactory.ID)); } @Test - public void testRunAdHocScheduledPolicy() { - managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() - .of(EventBasedResourcePolicyProviderFactory.ID) - .withActions(ResourcePolicyActionRepresentation.create() - .of(SetUserAttributeActionProviderFactory.ID) + public void testRunAdHocScheduledWorkflow() { + managedRealm.admin().workflows().create(WorkflowRepresentation.create() + .of(EventBasedWorkflowProviderFactory.ID) + .withSteps(WorkflowStepRepresentation.create() + .of(SetUserAttributeStepProviderFactory.ID) .after(Duration.ofDays(5)) .withConfig("message", "message") .build()) .build()).close(); - List policies = managedRealm.admin().resources().policies().list(); - assertThat(policies, hasSize(1)); - ResourcePolicyRepresentation policy = policies.get(0); + List workflows = managedRealm.admin().workflows().list(); + assertThat(workflows, hasSize(1)); + WorkflowRepresentation workflow = workflows.get(0); try (Response response = managedRealm.admin().users().create(getUserRepresentation("alice", "Alice", "Wonderland", "alice@wornderland.org"))) { String id = ApiUtil.getCreatedId(response); - managedRealm.admin().resources().policies().policy(policy.getId()).bind(ResourceType.USERS.name(), id); + managedRealm.admin().workflows().workflow(workflow.getId()).bind(ResourceType.USERS.name(), id); } runOnServer.run((session -> { RealmModel realm = configureSessionContext(session); - ResourcePolicyManager manager = new ResourcePolicyManager(session); + WorkflowsManager manager = new WorkflowsManager(session); UserModel user = session.users().getUserByUsername(realm, "alice"); - manager.runScheduledActions(); + manager.runScheduledSteps(); assertNull(user.getAttributes().get("message")); try { Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds())); - manager.runScheduledActions(); + manager.runScheduledSteps(); user = session.users().getUserByUsername(realm, "alice"); assertNotNull(user.getAttributes().get("message")); } finally { @@ -100,65 +100,65 @@ public class AdhocPolicyTest { } @Test - public void testRunAdHocNonScheduledPolicy() { - managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() - .of(EventBasedResourcePolicyProviderFactory.ID) - .withActions(ResourcePolicyActionRepresentation.create() - .of(SetUserAttributeActionProviderFactory.ID) + public void testRunAdHocNonScheduledWorkflow() { + managedRealm.admin().workflows().create(WorkflowRepresentation.create() + .of(EventBasedWorkflowProviderFactory.ID) + .withSteps(WorkflowStepRepresentation.create() + .of(SetUserAttributeStepProviderFactory.ID) .withConfig("message", "message") .build()) .build()).close(); - List policies = managedRealm.admin().resources().policies().list(); - assertThat(policies, hasSize(1)); - ResourcePolicyRepresentation policy = policies.get(0); + List workflows = managedRealm.admin().workflows().list(); + assertThat(workflows, hasSize(1)); + WorkflowRepresentation workflow = workflows.get(0); try (Response response = managedRealm.admin().users().create(getUserRepresentation("alice", "Alice", "Wonderland", "alice@wornderland.org"))) { String id = ApiUtil.getCreatedId(response); - managedRealm.admin().resources().policies().policy(policy.getId()).bind(ResourceType.USERS.name(), id); + managedRealm.admin().workflows().workflow(workflow.getId()).bind(ResourceType.USERS.name(), id); } runOnServer.run((session -> { RealmModel realm = configureSessionContext(session); - ResourcePolicyManager manager = new ResourcePolicyManager(session); + WorkflowsManager manager = new WorkflowsManager(session); UserModel user = session.users().getUserByUsername(realm, "alice"); - manager.runScheduledActions(); + manager.runScheduledSteps(); assertNotNull(user.getAttributes().get("message")); })); } @Test - public void testRunAdHocTimedScheduledPolicy() { - managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() - .of(EventBasedResourcePolicyProviderFactory.ID) - .withActions(ResourcePolicyActionRepresentation.create() - .of(SetUserAttributeActionProviderFactory.ID) + public void testRunAdHocTimedScheduledWorkflow() { + managedRealm.admin().workflows().create(WorkflowRepresentation.create() + .of(EventBasedWorkflowProviderFactory.ID) + .withSteps(WorkflowStepRepresentation.create() + .of(SetUserAttributeStepProviderFactory.ID) .withConfig("message", "message") .build()) .build()).close(); - List policies = managedRealm.admin().resources().policies().list(); - assertThat(policies, hasSize(1)); - ResourcePolicyRepresentation policy = policies.get(0); + List workflows = managedRealm.admin().workflows().list(); + assertThat(workflows, hasSize(1)); + WorkflowRepresentation workflow = workflows.get(0); String id; try (Response response = managedRealm.admin().users().create(getUserRepresentation("alice", "Alice", "Wonderland", "alice@wornderland.org"))) { id = ApiUtil.getCreatedId(response); - managedRealm.admin().resources().policies().policy(policy.getId()).bind(ResourceType.USERS.name(), id, Duration.ofDays(5).toMillis()); + managedRealm.admin().workflows().workflow(workflow.getId()).bind(ResourceType.USERS.name(), id, Duration.ofDays(5).toMillis()); } runOnServer.run((session -> { RealmModel realm = configureSessionContext(session); - ResourcePolicyManager manager = new ResourcePolicyManager(session); + WorkflowsManager manager = new WorkflowsManager(session); UserModel user = session.users().getUserByUsername(realm, "alice"); - manager.runScheduledActions(); + manager.runScheduledSteps(); assertNull(user.getAttributes().get("message")); try { Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds())); - manager.runScheduledActions(); + manager.runScheduledSteps(); user = session.users().getUserByUsername(realm, "alice"); assertNotNull(user.getAttributes().get("message")); } finally { @@ -167,19 +167,19 @@ public class AdhocPolicyTest { } })); - managedRealm.admin().resources().policies().policy(policy.getId()).bind(ResourceType.USERS.name(), id, Duration.ofDays(10).toMillis()); + managedRealm.admin().workflows().workflow(workflow.getId()).bind(ResourceType.USERS.name(), id, Duration.ofDays(10).toMillis()); runOnServer.run((session -> { RealmModel realm = configureSessionContext(session); - ResourcePolicyManager manager = new ResourcePolicyManager(session); + WorkflowsManager manager = new WorkflowsManager(session); UserModel user = session.users().getUserByUsername(realm, "alice"); - manager.runScheduledActions(); + manager.runScheduledSteps(); assertNull(user.getAttributes().get("message")); try { Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds())); - manager.runScheduledActions(); + manager.runScheduledSteps(); user = session.users().getUserByUsername(realm, "alice"); assertNull(user.getAttributes().get("message")); } finally { @@ -188,7 +188,7 @@ public class AdhocPolicyTest { try { Time.setOffset(Math.toIntExact(Duration.ofDays(11).toSeconds())); - manager.runScheduledActions(); + manager.runScheduledSteps(); user = session.users().getUserByUsername(realm, "alice"); assertNotNull(user.getAttributes().get("message")); } finally { diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/AggregatedActionTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/AggregatedStepTest.java similarity index 60% rename from tests/base/src/test/java/org/keycloak/tests/admin/model/policy/AggregatedActionTest.java rename to tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/AggregatedStepTest.java index 27ca38b5c4e..d9a71b80576 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/AggregatedActionTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/AggregatedStepTest.java @@ -1,4 +1,4 @@ -package org.keycloak.tests.admin.model.policy; +package org.keycloak.tests.admin.model.workflow; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasEntry; @@ -17,15 +17,15 @@ 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.AggregatedActionProviderFactory; -import org.keycloak.models.policy.DisableUserActionProviderFactory; -import org.keycloak.models.policy.ResourcePolicyManager; -import org.keycloak.models.policy.SetUserAttributeActionProviderFactory; -import org.keycloak.models.policy.UserCreationTimeResourcePolicyProviderFactory; +import org.keycloak.models.workflow.AggregatedStepProviderFactory; +import org.keycloak.models.workflow.DisableUserStepProviderFactory; +import org.keycloak.models.workflow.WorkflowsManager; +import org.keycloak.models.workflow.SetUserAttributeStepProviderFactory; +import org.keycloak.models.workflow.UserCreationTimeWorkflowProviderFactory; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.UserRepresentation; -import org.keycloak.representations.resources.policies.ResourcePolicyActionRepresentation; -import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation; +import org.keycloak.representations.workflows.WorkflowStepRepresentation; +import org.keycloak.representations.workflows.WorkflowRepresentation; import org.keycloak.testframework.annotations.InjectRealm; import org.keycloak.testframework.annotations.KeycloakIntegrationTest; import org.keycloak.testframework.injection.LifeCycle; @@ -33,8 +33,8 @@ import org.keycloak.testframework.realm.ManagedRealm; import org.keycloak.testframework.remote.runonserver.InjectRunOnServer; import org.keycloak.testframework.remote.runonserver.RunOnServerClient; -@KeycloakIntegrationTest(config = RLMServerConfig.class) -public class AggregatedActionTest { +@KeycloakIntegrationTest(config = WorkflowsServerConfig.class) +public class AggregatedStepTest { private static final String REALM_NAME = "default"; @@ -46,36 +46,36 @@ public class AggregatedActionTest { @Test public void testCreate() { - managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() - .of(UserCreationTimeResourcePolicyProviderFactory.ID) - .withActions( - ResourcePolicyActionRepresentation.create().of(AggregatedActionProviderFactory.ID) + managedRealm.admin().workflows().create(WorkflowRepresentation.create() + .of(UserCreationTimeWorkflowProviderFactory.ID) + .withSteps( + WorkflowStepRepresentation.create().of(AggregatedStepProviderFactory.ID) .after(Duration.ofDays(5)) - .withActions(ResourcePolicyActionRepresentation.create() - .of(SetUserAttributeActionProviderFactory.ID) + .withSteps(WorkflowStepRepresentation.create() + .of(SetUserAttributeStepProviderFactory.ID) .withConfig("message", "message") .build(), - ResourcePolicyActionRepresentation.create() - .of(DisableUserActionProviderFactory.ID) + WorkflowStepRepresentation.create() + .of(DisableUserStepProviderFactory.ID) .build() ).build()) .build()).close(); - List policies = managedRealm.admin().resources().policies().list(); - assertThat(policies, hasSize(1)); - ResourcePolicyRepresentation policy = policies.get(0); - assertThat(policy.getActions(), hasSize(1)); - ResourcePolicyActionRepresentation aggregatedAction = policy.getActions().get(0); - assertThat(aggregatedAction.getProviderId(), is(AggregatedActionProviderFactory.ID)); - List actions = aggregatedAction.getActions(); - assertThat(actions, hasSize(2)); - assertAction(actions, SetUserAttributeActionProviderFactory.ID, a -> { + List workflows = managedRealm.admin().workflows().list(); + assertThat(workflows, hasSize(1)); + WorkflowRepresentation workflow = workflows.get(0); + assertThat(workflow.getSteps(), hasSize(1)); + WorkflowStepRepresentation aggregatedStep = workflow.getSteps().get(0); + assertThat(aggregatedStep.getProviderId(), is(AggregatedStepProviderFactory.ID)); + List steps = aggregatedStep.getSteps(); + assertThat(steps, hasSize(2)); + assertStep(steps, SetUserAttributeStepProviderFactory.ID, a -> { assertNotNull(a.getConfig()); assertThat(a.getConfig().isEmpty(), is(false)); assertThat(a.getConfig(), hasEntry("priority", List.of("1"))); assertThat(a.getConfig(), hasEntry("message", List.of("message"))); }); - assertAction(actions, DisableUserActionProviderFactory.ID, a -> { + assertStep(steps, DisableUserStepProviderFactory.ID, a -> { assertNotNull(a.getConfig()); assertThat(a.getConfig().isEmpty(), is(false)); assertThat(a.getConfig(), hasEntry("priority", List.of("2"))); @@ -83,18 +83,18 @@ public class AggregatedActionTest { } @Test - public void testActionRun() { - managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() - .of(UserCreationTimeResourcePolicyProviderFactory.ID) - .withActions( - ResourcePolicyActionRepresentation.create().of(AggregatedActionProviderFactory.ID) + public void testStepRun() { + managedRealm.admin().workflows().create(WorkflowRepresentation.create() + .of(UserCreationTimeWorkflowProviderFactory.ID) + .withSteps( + WorkflowStepRepresentation.create().of(AggregatedStepProviderFactory.ID) .after(Duration.ofDays(5)) - .withActions(ResourcePolicyActionRepresentation.create() - .of(SetUserAttributeActionProviderFactory.ID) + .withSteps(WorkflowStepRepresentation.create() + .of(SetUserAttributeStepProviderFactory.ID) .withConfig("message", "message") .build(), - ResourcePolicyActionRepresentation.create() - .of(DisableUserActionProviderFactory.ID) + WorkflowStepRepresentation.create() + .of(DisableUserStepProviderFactory.ID) .build() ).build()) .build()).close(); @@ -103,11 +103,11 @@ public class AggregatedActionTest { runOnServer.run((session -> { RealmModel realm = configureSessionContext(session); - ResourcePolicyManager manager = new ResourcePolicyManager(session); + WorkflowsManager manager = new WorkflowsManager(session); try { Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds())); - manager.runScheduledActions(); + manager.runScheduledSteps(); UserModel user = session.users().getUserByUsername(realm, "alice"); assertNotNull(user.getAttributes().get("message")); assertFalse(user.isEnabled()); @@ -137,8 +137,8 @@ public class AggregatedActionTest { return representation; } - private void assertAction(List actions, String expectedProviderId, Consumer assertions) { - assertTrue(actions.stream() + private void assertStep(List steps, String expectedProviderId, Consumer assertions) { + assertTrue(steps.stream() .anyMatch(a -> { if (a.getProviderId().equals(expectedProviderId)) { assertions.accept(a); diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/BrokeredUserSessionRefreshTimePolicyTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/BrokeredUserSessionRefreshTimeWorkflowTest.java similarity index 73% rename from tests/base/src/test/java/org/keycloak/tests/admin/model/policy/BrokeredUserSessionRefreshTimePolicyTest.java rename to tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/BrokeredUserSessionRefreshTimeWorkflowTest.java index 051b7723de5..a89926888c0 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/BrokeredUserSessionRefreshTimePolicyTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/BrokeredUserSessionRefreshTimeWorkflowTest.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.keycloak.tests.admin.model.policy; +package org.keycloak.tests.admin.model.workflow; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.allOf; @@ -40,19 +40,19 @@ 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.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; -import org.keycloak.models.policy.UserSessionRefreshTimeResourcePolicyProviderFactory; -import org.keycloak.models.policy.conditions.IdentityProviderPolicyConditionFactory; +import org.keycloak.models.workflow.DeleteUserStepProviderFactory; +import org.keycloak.models.workflow.ResourceOperationType; +import org.keycloak.models.workflow.Workflow; +import org.keycloak.models.workflow.WorkflowsManager; +import org.keycloak.models.workflow.WorkflowStateProvider; +import org.keycloak.models.workflow.UserSessionRefreshTimeWorkflowProviderFactory; +import org.keycloak.models.workflow.conditions.IdentityProviderWorkflowConditionFactory; import org.keycloak.representations.idm.FederatedIdentityRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.UserRepresentation; -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.workflows.WorkflowStepRepresentation; +import org.keycloak.representations.workflows.WorkflowConditionRepresentation; +import org.keycloak.representations.workflows.WorkflowRepresentation; import org.keycloak.testframework.annotations.InjectClient; import org.keycloak.testframework.annotations.InjectRealm; import org.keycloak.testframework.annotations.InjectUser; @@ -79,8 +79,8 @@ import org.openqa.selenium.WebDriver; /** */ -@KeycloakIntegrationTest(config = RLMServerConfig.class) -public class BrokeredUserSessionRefreshTimePolicyTest { +@KeycloakIntegrationTest(config = WorkflowsServerConfig.class) +public class BrokeredUserSessionRefreshTimeWorkflowTest { private static final String REALM_NAME = "consumer"; @@ -124,52 +124,52 @@ public class BrokeredUserSessionRefreshTimePolicyTest { private static final String CLIENT_SECRET = "secret"; @Test - public void testInvalidatePolicyOnIdentityProviderRemoval() { - consumerRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() - .of(UserSessionRefreshTimeResourcePolicyProviderFactory.ID) + public void testInvalidateWorkflowOnIdentityProviderRemoval() { + consumerRealm.admin().workflows().create(WorkflowRepresentation.create() + .of(UserSessionRefreshTimeWorkflowProviderFactory.ID) .onEvent(ResourceOperationType.LOGIN.toString()) - .onConditions(ResourcePolicyConditionRepresentation.create() - .of(IdentityProviderPolicyConditionFactory.ID) - .withConfig(IdentityProviderPolicyConditionFactory.EXPECTED_ALIASES, IDP_OIDC_ALIAS) + .onConditions(WorkflowConditionRepresentation.create() + .of(IdentityProviderWorkflowConditionFactory.ID) + .withConfig(IdentityProviderWorkflowConditionFactory.EXPECTED_ALIASES, IDP_OIDC_ALIAS) .build()) - .withActions( - ResourcePolicyActionRepresentation.create().of(DeleteUserActionProviderFactory.ID) + .withSteps( + WorkflowStepRepresentation.create().of(DeleteUserStepProviderFactory.ID) .after(Duration.ofDays(1)) .build() ).build()).close(); - List policies = consumerRealm.admin().resources().policies().list(); - assertThat(policies, hasSize(1)); + List workflows = consumerRealm.admin().workflows().list(); + assertThat(workflows, hasSize(1)); - ResourcePolicyRepresentation policyRep = consumerRealm.admin().resources().policies().policy(policies.get(0).getId()).toRepresentation(); - assertThat(policyRep.getConfig().getFirst("enabled"), nullValue()); + WorkflowRepresentation workflowRep = consumerRealm.admin().workflows().workflow(workflows.get(0).getId()).toRepresentation(); + assertThat(workflowRep.getConfig().getFirst("enabled"), nullValue()); // remove IDP consumerRealm.admin().identityProviders().get(IDP_OIDC_ALIAS).remove(); - // create new user - it will trigger an activation event and therefore should disable the policy + // create new user - it will trigger an activation event and therefore should disable the workflow consumerRealm.admin().users().create(UserConfigBuilder.create().username("test").build()).close(); - // check the policy is disabled - policyRep = consumerRealm.admin().resources().policies().policy(policies.get(0).getId()).toRepresentation(); - assertThat(policyRep.getConfig().getFirst("enabled"), allOf(notNullValue(), is("false"))); - List validationErrors = policyRep.getConfig().get("validation_error"); + // check the workflow is disabled + workflowRep = consumerRealm.admin().workflows().workflow(workflows.get(0).getId()).toRepresentation(); + assertThat(workflowRep.getConfig().getFirst("enabled"), allOf(notNullValue(), is("false"))); + List validationErrors = workflowRep.getConfig().get("validation_error"); assertThat(validationErrors, notNullValue()); assertThat(validationErrors, hasSize(1)); assertThat(validationErrors.get(0), containsString("Identity provider %s does not exist.".formatted(IDP_OIDC_ALIAS))); } @Test - public void tesRunActionOnFederatedUser() { - consumerRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() - .of(UserSessionRefreshTimeResourcePolicyProviderFactory.ID) + public void tesRunStepOnFederatedUser() { + consumerRealm.admin().workflows().create(WorkflowRepresentation.create() + .of(UserSessionRefreshTimeWorkflowProviderFactory.ID) .onEvent(ResourceOperationType.LOGIN.toString()) - .onConditions(ResourcePolicyConditionRepresentation.create() - .of(IdentityProviderPolicyConditionFactory.ID) - .withConfig(IdentityProviderPolicyConditionFactory.EXPECTED_ALIASES, IDP_OIDC_ALIAS) + .onConditions(WorkflowConditionRepresentation.create() + .of(IdentityProviderWorkflowConditionFactory.ID) + .withConfig(IdentityProviderWorkflowConditionFactory.EXPECTED_ALIASES, IDP_OIDC_ALIAS) .build()) - .withActions( - ResourcePolicyActionRepresentation.create().of(DeleteUserActionProviderFactory.ID) + .withSteps( + WorkflowStepRepresentation.create().of(DeleteUserStepProviderFactory.ID) .after(Duration.ofDays(1)) .build() ).build()).close(); @@ -184,16 +184,16 @@ public class BrokeredUserSessionRefreshTimePolicyTest { runOnServer.run((session -> { RealmModel realm = configureSessionContext(session); - ResourcePolicyManager manager = new ResourcePolicyManager(session); + WorkflowsManager manager = new WorkflowsManager(session); - manager.runScheduledActions(); + manager.runScheduledSteps(); UserModel user = session.users().getUserByUsername(realm, username); assertNotNull(user); assertTrue(user.isEnabled()); try { Time.setOffset(Math.toIntExact(Duration.ofDays(2).toSeconds())); - manager.runScheduledActions(); + manager.runScheduledSteps(); user = session.users().getUserByUsername(realm, username); assertNull(user); } finally { @@ -202,7 +202,7 @@ public class BrokeredUserSessionRefreshTimePolicyTest { })); // now authenticate with bob directly in the consumer realm - he is not associated with the IDP and thus not influenced - // by the idp-exclusive lifecycle policy. + // by the idp-exclusive lifecycle workflow. consumerRealmOAuth.openLoginForm(); loginPage.fillLogin(bobFromConsumerRealm.getUsername(), bobFromConsumerRealm.getPassword()); loginPage.submit(); @@ -210,10 +210,10 @@ public class BrokeredUserSessionRefreshTimePolicyTest { runOnServer.run(session -> { RealmModel realm = configureSessionContext(session); - ResourcePolicyManager manager = new ResourcePolicyManager(session); + WorkflowsManager manager = new WorkflowsManager(session); // run the scheduled tasks - bob should not be affected. - manager.runScheduledActions(); + manager.runScheduledSteps(); UserModel user = session.users().getUserByUsername(realm, "bob"); assertNotNull(user); assertTrue(user.isEnabled()); @@ -221,7 +221,7 @@ public class BrokeredUserSessionRefreshTimePolicyTest { try { // run with a time offset - bob should still not be affected. Time.setOffset(Math.toIntExact(Duration.ofDays(2).toSeconds())); - manager.runScheduledActions(); + manager.runScheduledSteps(); user = session.users().getUserByUsername(realm, "bob"); assertNotNull(user); } finally { @@ -231,16 +231,16 @@ public class BrokeredUserSessionRefreshTimePolicyTest { } @Test - public void testAddRemoveFedIdentityAffectsPolicyAssociation() { - consumerRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() - .of(UserSessionRefreshTimeResourcePolicyProviderFactory.ID) + public void testAddRemoveFedIdentityAffectsWorkflowAssociation() { + consumerRealm.admin().workflows().create(WorkflowRepresentation.create() + .of(UserSessionRefreshTimeWorkflowProviderFactory.ID) .onEvent(ResourceOperationType.ADD_FEDERATED_IDENTITY.toString()) - .onConditions(ResourcePolicyConditionRepresentation.create() - .of(IdentityProviderPolicyConditionFactory.ID) - .withConfig(IdentityProviderPolicyConditionFactory.EXPECTED_ALIASES, IDP_OIDC_ALIAS) + .onConditions(WorkflowConditionRepresentation.create() + .of(IdentityProviderWorkflowConditionFactory.ID) + .withConfig(IdentityProviderWorkflowConditionFactory.EXPECTED_ALIASES, IDP_OIDC_ALIAS) .build()) - .withActions( - ResourcePolicyActionRepresentation.create().of(DeleteUserActionProviderFactory.ID) + .withSteps( + WorkflowStepRepresentation.create().of(DeleteUserStepProviderFactory.ID) .after(Duration.ofDays(1)) .build() ).build()).close(); @@ -249,38 +249,38 @@ public class BrokeredUserSessionRefreshTimePolicyTest { runOnServer.run(session -> { RealmModel realm = configureSessionContext(session); - ResourcePolicyManager manager = new ResourcePolicyManager(session); - ResourcePolicy policy = manager.getPolicies().get(0); + WorkflowsManager manager = new WorkflowsManager(session); + Workflow workflow = manager.getWorkflows().get(0); UserModel alice = session.users().getUserByUsername(realm, "alice"); assertNotNull(alice); - // alice should be associated with the policy - ResourcePolicyStateProvider stateProvider = session.getProvider(ResourcePolicyStateProvider.class); - ResourcePolicyStateProvider.ScheduledAction scheduledAction = stateProvider.getScheduledAction(policy.getId(), alice.getId()); - assertNotNull(scheduledAction, "An action should have been scheduled for the user " + alice.getUsername()); + // alice should be associated with the workflow + WorkflowStateProvider stateProvider = session.getProvider(WorkflowStateProvider.class); + WorkflowStateProvider.ScheduledStep scheduledStep = stateProvider.getScheduledStep(workflow.getId(), alice.getId()); + assertNotNull(scheduledStep, "A step should have been scheduled for the user " + alice.getUsername()); }); - // remove the federated identity - alice should be disassociated from the policy and thus not deleted + // remove the federated identity - alice should be disassociated from the workflow and thus not deleted UserRepresentation aliceInConsumerRealm = consumerRealm.admin().users().search(aliceFromProviderRealm.getUsername()).get(0); assertNotNull(aliceInConsumerRealm); consumerRealm.admin().users().get(aliceInConsumerRealm.getId()).removeFederatedIdentity(IDP_OIDC_ALIAS); runOnServer.run(session -> { RealmModel realm = configureSessionContext(session); - ResourcePolicyManager manager = new ResourcePolicyManager(session); + WorkflowsManager manager = new WorkflowsManager(session); try { - // run with a time offset - alice should not be deleted as she is no longer associated with the IDP and thus the policy + // run with a time offset - alice should not be deleted as she is no longer associated with the IDP and thus the workflow Time.setOffset(Math.toIntExact(Duration.ofDays(2).toSeconds())); - manager.runScheduledActions(); + manager.runScheduledSteps(); UserModel user = session.users().getUserByUsername(realm, "alice"); - assertNotNull(user, "User alice should not be deleted as she is no longer associated with the IDP and thus the policy."); + assertNotNull(user, "User alice should not be deleted as she is no longer associated with the IDP and thus the workflow."); } finally { Time.setOffset(0); } }); - // add a federated identity for user bob - bob should now be associated with the policy and thus deleted when the scheduled tasks run + // add a federated identity for user bob - bob should now be associated with the workflow and thus deleted when the scheduled tasks run FederatedIdentityRepresentation federatedIdentityRepresentation = new FederatedIdentityRepresentation(); federatedIdentityRepresentation.setIdentityProvider(IDP_OIDC_ALIAS); federatedIdentityRepresentation.setUserId("bob-federated-id"); @@ -289,12 +289,12 @@ public class BrokeredUserSessionRefreshTimePolicyTest { runOnServer.run(session -> { RealmModel realm = configureSessionContext(session); - ResourcePolicyManager manager = new ResourcePolicyManager(session); + WorkflowsManager manager = new WorkflowsManager(session); try { - // run with a time offset - bob should be deleted as he is now associated with the IDP and thus with the policy + // run with a time offset - bob should be deleted as he is now associated with the IDP and thus with the workflow Time.setOffset(Math.toIntExact(Duration.ofDays(2).toSeconds())); - manager.runScheduledActions(); + manager.runScheduledSteps(); UserModel user = session.users().getUserByUsername(realm, "bob"); assertNull(user); } finally { diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/GroupMembershipJoinPolicyTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/GroupMembershipJoinWorkflowTest.java similarity index 58% rename from tests/base/src/test/java/org/keycloak/tests/admin/model/policy/GroupMembershipJoinPolicyTest.java rename to tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/GroupMembershipJoinWorkflowTest.java index f4b098eeafc..47aefb441a7 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/GroupMembershipJoinPolicyTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/GroupMembershipJoinWorkflowTest.java @@ -1,4 +1,4 @@ -package org.keycloak.tests.admin.model.policy; +package org.keycloak.tests.admin.model.workflow; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.allOf; @@ -15,23 +15,22 @@ import java.util.List; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; import org.junit.jupiter.api.Test; -import org.keycloak.admin.client.resource.RealmResourcePolicies; import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.admin.client.resource.WorkflowsResource; 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.NotifyUserActionProviderFactory; -import org.keycloak.models.policy.UserSessionRefreshTimeResourcePolicyProviderFactory; -import org.keycloak.models.policy.SetUserAttributeActionProviderFactory; -import org.keycloak.models.policy.conditions.GroupMembershipPolicyConditionFactory; -import org.keycloak.models.policy.ResourceOperationType; -import org.keycloak.models.policy.ResourcePolicyManager; +import org.keycloak.models.workflow.EventBasedWorkflowProviderFactory; +import org.keycloak.models.workflow.NotifyUserStepProviderFactory; +import org.keycloak.models.workflow.UserSessionRefreshTimeWorkflowProviderFactory; +import org.keycloak.models.workflow.SetUserAttributeStepProviderFactory; +import org.keycloak.models.workflow.conditions.GroupMembershipWorkflowConditionFactory; +import org.keycloak.models.workflow.ResourceOperationType; +import org.keycloak.models.workflow.WorkflowsManager; import org.keycloak.representations.idm.UserRepresentation; -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.workflows.WorkflowStepRepresentation; +import org.keycloak.representations.workflows.WorkflowConditionRepresentation; +import org.keycloak.representations.workflows.WorkflowRepresentation; import org.keycloak.representations.userprofile.config.UPConfig; import org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy; import org.keycloak.testframework.annotations.InjectRealm; @@ -44,8 +43,8 @@ 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 GroupMembershipJoinPolicyTest { +@KeycloakIntegrationTest(config = WorkflowsServerConfig.class) +public class GroupMembershipJoinWorkflowTest { private static final String REALM_NAME = "default"; @@ -67,24 +66,24 @@ public class GroupMembershipJoinPolicyTest { groupId = ApiUtil.getCreatedId(response); } - List expectedPolicies = ResourcePolicyRepresentation.create() - .of(EventBasedResourcePolicyProviderFactory.ID) + List expectedWorkflows = WorkflowRepresentation.create() + .of(EventBasedWorkflowProviderFactory.ID) .onEvent(ResourceOperationType.GROUP_MEMBERSHIP_JOIN.name()) - .onConditions(ResourcePolicyConditionRepresentation.create() - .of(GroupMembershipPolicyConditionFactory.ID) - .withConfig(GroupMembershipPolicyConditionFactory.EXPECTED_GROUPS, groupId) + .onConditions(WorkflowConditionRepresentation.create() + .of(GroupMembershipWorkflowConditionFactory.ID) + .withConfig(GroupMembershipWorkflowConditionFactory.EXPECTED_GROUPS, groupId) .build()) - .withActions( - ResourcePolicyActionRepresentation.create() - .of(SetUserAttributeActionProviderFactory.ID) + .withSteps( + WorkflowStepRepresentation.create() + .of(SetUserAttributeStepProviderFactory.ID) .withConfig("attribute", "attr1") .after(Duration.ofDays(5)) .build() ).build(); - RealmResourcePolicies policies = managedRealm.admin().resources().policies(); + WorkflowsResource workflows = managedRealm.admin().workflows(); - try (Response response = policies.create(expectedPolicies)) { + try (Response response = workflows.create(expectedWorkflows)) { assertThat(response.getStatus(), is(Status.CREATED.getStatusCode())); } @@ -101,14 +100,12 @@ public class GroupMembershipJoinPolicyTest { runOnServer.run((session -> { RealmModel realm = configureSessionContext(session); - ResourcePolicyManager manager = new ResourcePolicyManager(session); - - UserModel user = session.users().getUserById(realm, userId); + WorkflowsManager manager = new WorkflowsManager(session); try { - // set offset to 7 days - notify action should run now + // set offset to 7 days - notify step should run now Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds())); - manager.runScheduledActions(); + manager.runScheduledSteps(); } finally { Time.setOffset(0); } @@ -127,35 +124,35 @@ public class GroupMembershipJoinPolicyTest { groupId = ApiUtil.getCreatedId(response); } - managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() - .of(UserSessionRefreshTimeResourcePolicyProviderFactory.ID) + managedRealm.admin().workflows().create(WorkflowRepresentation.create() + .of(UserSessionRefreshTimeWorkflowProviderFactory.ID) .onEvent(ResourceOperationType.LOGIN.toString()) - .onConditions(ResourcePolicyConditionRepresentation.create() - .of(GroupMembershipPolicyConditionFactory.ID) - .withConfig(GroupMembershipPolicyConditionFactory.EXPECTED_GROUPS, groupId) + .onConditions(WorkflowConditionRepresentation.create() + .of(GroupMembershipWorkflowConditionFactory.ID) + .withConfig(GroupMembershipWorkflowConditionFactory.EXPECTED_GROUPS, groupId) .build()) - .withActions( - ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID) + .withSteps( + WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) .after(Duration.ofDays(1)) .build() ).build()).close(); - List policies = managedRealm.admin().resources().policies().list(); - assertThat(policies, hasSize(1)); + List workflows = managedRealm.admin().workflows().list(); + assertThat(workflows, hasSize(1)); - ResourcePolicyRepresentation policyRep = managedRealm.admin().resources().policies().policy(policies.get(0).getId()).toRepresentation(); - assertThat(policyRep.getConfig().getFirst("enabled"), nullValue()); + WorkflowRepresentation workflowRep = managedRealm.admin().workflows().workflow(workflows.get(0).getId()).toRepresentation(); + assertThat(workflowRep.getConfig().getFirst("enabled"), nullValue()); // remove group managedRealm.admin().groups().group(groupId).remove(); - // create new user - it will trigger an activation event and therefore should disable the policy + // create new user - it will trigger an activation event and therefore should disable the workflow managedRealm.admin().users().create(UserConfigBuilder.create().username("test").build()).close(); - // check the policy is disabled - policyRep = managedRealm.admin().resources().policies().policy(policies.get(0).getId()).toRepresentation(); - assertThat(policyRep.getConfig().getFirst("enabled"), allOf(notNullValue(), is("false"))); - List validationErrors = policyRep.getConfig().get("validation_error"); + // check the workflow is disabled + workflowRep = managedRealm.admin().workflows().workflow(workflows.get(0).getId()).toRepresentation(); + assertThat(workflowRep.getConfig().getFirst("enabled"), allOf(notNullValue(), is("false"))); + List validationErrors = workflowRep.getConfig().get("validation_error"); assertThat(validationErrors, notNullValue()); assertThat(validationErrors, hasSize(1)); assertThat(validationErrors.get(0), containsString("Group with id %s does not exist.".formatted(groupId))); diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/RolePolicyConditionTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/RoleWorkflowConditionTest.java similarity index 76% rename from tests/base/src/test/java/org/keycloak/tests/admin/model/policy/RolePolicyConditionTest.java rename to tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/RoleWorkflowConditionTest.java index d0588487944..d29d9243e79 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/RolePolicyConditionTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/RoleWorkflowConditionTest.java @@ -1,11 +1,11 @@ -package org.keycloak.tests.admin.model.policy; +package org.keycloak.tests.admin.model.workflow; 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 static org.keycloak.models.workflow.conditions.RoleWorkflowConditionFactory.EXPECTED_ROLES; import java.time.Duration; import java.util.List; @@ -15,22 +15,22 @@ 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.admin.client.resource.WorkflowsResource; 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.models.workflow.EventBasedWorkflowProviderFactory; +import org.keycloak.models.workflow.ResourceOperationType; +import org.keycloak.models.workflow.WorkflowsManager; +import org.keycloak.models.workflow.SetUserAttributeStepProviderFactory; +import org.keycloak.models.workflow.conditions.RoleWorkflowConditionFactory; 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.workflows.WorkflowStepRepresentation; +import org.keycloak.representations.workflows.WorkflowConditionRepresentation; +import org.keycloak.representations.workflows.WorkflowRepresentation; import org.keycloak.representations.userprofile.config.UPConfig; import org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy; import org.keycloak.testframework.annotations.InjectRealm; @@ -43,8 +43,8 @@ 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 { +@KeycloakIntegrationTest(config = WorkflowsServerConfig.class) +public class RoleWorkflowConditionTest { private static final String REALM_NAME = "default"; @@ -64,7 +64,7 @@ public class RolePolicyConditionTest { @Test public void testConditionForSingleRole() { String expected = "realm-role-1"; - createPolicy(expected); + createWorkflow(expected); assertUserRoles("user-1", false); assertUserRoles("user-2", false, "not-valid-role"); assertUserRoles("user-3", true, expected); @@ -73,7 +73,7 @@ public class RolePolicyConditionTest { @Test public void testConditionForMultipleRole() { List expected = List.of("realm-role-1", "realm-role-2", "client-a/client-role-1"); - createPolicy(expected); + createWorkflow(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); @@ -105,9 +105,9 @@ public class RolePolicyConditionTest { RealmModel realm = configureSessionContext(session); try { - // set offset to 7 days - notify action should run now + // set offset to 7 days - notify step should run now Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds())); - new ResourcePolicyManager(session).runScheduledActions(); + new WorkflowsManager(session).runScheduledSteps(); } finally { Time.setOffset(0); } @@ -123,38 +123,38 @@ public class RolePolicyConditionTest { })); } - private void createPolicy(String... expectedValues) { - createPolicy(Map.of(EXPECTED_ROLES, List.of(expectedValues))); + private void createWorkflow(String... expectedValues) { + createWorkflow(Map.of(EXPECTED_ROLES, List.of(expectedValues))); } - private void createPolicy(List expectedValues) { - createPolicy(Map.of(EXPECTED_ROLES, expectedValues)); + private void createWorkflow(List expectedValues) { + createWorkflow(Map.of(EXPECTED_ROLES, expectedValues)); } - private void createPolicy(Map> attributes) { + private void createWorkflow(Map> attributes) { for (String roleName : attributes.getOrDefault(EXPECTED_ROLES, List.of())) { createRoleIfNotExists(roleName); } - List expectedPolicies = ResourcePolicyRepresentation.create() - .of(EventBasedResourcePolicyProviderFactory.ID) + List expectedWorkflows = WorkflowRepresentation.create() + .of(EventBasedWorkflowProviderFactory.ID) .onEvent(ResourceOperationType.ROLE_GRANTED.name()) .recurring() - .onConditions(ResourcePolicyConditionRepresentation.create() - .of(RolePolicyConditionFactory.ID) + .onConditions(WorkflowConditionRepresentation.create() + .of(RoleWorkflowConditionFactory.ID) .withConfig(attributes) .build()) - .withActions( - ResourcePolicyActionRepresentation.create() - .of(SetUserAttributeActionProviderFactory.ID) + .withSteps( + WorkflowStepRepresentation.create() + .of(SetUserAttributeStepProviderFactory.ID) .withConfig("notified", "true") .after(Duration.ofDays(5)) .build() ).build(); - RealmResourcePolicies policies = managedRealm.admin().resources().policies(); + WorkflowsResource workflows = managedRealm.admin().workflows(); - try (Response response = policies.create(expectedPolicies)) { + try (Response response = workflows.create(expectedWorkflows)) { assertThat(response.getStatus(), is(Status.CREATED.getStatusCode())); } } diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/ActionRunnerScheduledTaskTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/StepRunnerScheduledTaskTest.java similarity index 74% rename from tests/base/src/test/java/org/keycloak/tests/admin/model/policy/ActionRunnerScheduledTaskTest.java rename to tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/StepRunnerScheduledTaskTest.java index 82f2c0eeec2..d1b485c9ef9 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/ActionRunnerScheduledTaskTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/StepRunnerScheduledTaskTest.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.keycloak.tests.admin.model.policy; +package org.keycloak.tests.admin.model.workflow; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -32,14 +32,14 @@ import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserProvider; -import org.keycloak.models.policy.DisableUserActionProviderFactory; -import org.keycloak.models.policy.ResourcePolicyActionRunnerSuccessEvent; -import org.keycloak.models.policy.SetUserAttributeActionProviderFactory; -import org.keycloak.models.policy.UserCreationTimeResourcePolicyProviderFactory; +import org.keycloak.models.workflow.DisableUserStepProviderFactory; +import org.keycloak.models.workflow.WorkflowStepRunnerSuccessEvent; +import org.keycloak.models.workflow.SetUserAttributeStepProviderFactory; +import org.keycloak.models.workflow.UserCreationTimeWorkflowProviderFactory; import org.keycloak.provider.ProviderEventListener; import org.keycloak.representations.idm.RealmRepresentation; -import org.keycloak.representations.resources.policies.ResourcePolicyActionRepresentation; -import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation; +import org.keycloak.representations.workflows.WorkflowStepRepresentation; +import org.keycloak.representations.workflows.WorkflowRepresentation; import org.keycloak.storage.UserStoragePrivateUtil; import org.keycloak.testframework.annotations.InjectAdminClient; import org.keycloak.testframework.annotations.KeycloakIntegrationTest; @@ -47,8 +47,8 @@ import org.keycloak.testframework.realm.UserConfigBuilder; import org.keycloak.testframework.remote.runonserver.InjectRunOnServer; import org.keycloak.testframework.remote.runonserver.RunOnServerClient; -@KeycloakIntegrationTest(config = RLMScheduledTaskServerConfig.class) -public class ActionRunnerScheduledTaskTest { +@KeycloakIntegrationTest(config = WorkflowsScheduledTaskServerConfig.class) +public class StepRunnerScheduledTaskTest { private static final String REALM_NAME = "default"; @@ -59,7 +59,7 @@ public class ActionRunnerScheduledTaskTest { Keycloak adminClient; @Test - public void testActionRunnerScheduledTask() { + public void testStepRunnerScheduledTask() { for (int i = 0; i < 2; i++) { RealmRepresentation realm = new RealmRepresentation(); @@ -68,21 +68,21 @@ public class ActionRunnerScheduledTaskTest { adminClient.realms().create(realm); - assertActionRuns(realm.getRealm()); + assertStepRuns(realm.getRealm()); } } - private void assertActionRuns(String realmName) { + private void assertStepRuns(String realmName) { RealmResource realm = adminClient.realm(realmName); - realm.resources().policies().create(ResourcePolicyRepresentation.create() - .of(UserCreationTimeResourcePolicyProviderFactory.ID) - .withActions( - ResourcePolicyActionRepresentation.create().of(SetUserAttributeActionProviderFactory.ID) + realm.workflows().create(WorkflowRepresentation.create() + .of(UserCreationTimeWorkflowProviderFactory.ID) + .withSteps( + WorkflowStepRepresentation.create().of(SetUserAttributeStepProviderFactory.ID) .after(Duration.ofDays(5)) .withConfig("message", "message") .build(), - ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID) + WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID) .after(Duration.ofDays(5)) .build() ).build()).close(); @@ -99,8 +99,8 @@ public class ActionRunnerScheduledTaskTest { CountDownLatch count = new CountDownLatch(2); ProviderEventListener listener = event -> { - if (event instanceof ResourcePolicyActionRunnerSuccessEvent e) { - KeycloakSession s = e.getSession(); + if (event instanceof WorkflowStepRunnerSuccessEvent e) { + KeycloakSession s = e.session(); RealmModel r = s.getContext().getRealm(); if (!realmName.equals(r.getName())) { @@ -112,7 +112,7 @@ public class ActionRunnerScheduledTaskTest { if (user.isEnabled() && user.getAttributes().containsKey("message")) { // notified count.countDown(); - // force execution of next action + // force execution of next step user.removeAttribute("message"); Time.setOffset(Math.toIntExact(Duration.ofDays(20).toSeconds())); } else if (!user.isEnabled()) { @@ -125,9 +125,9 @@ public class ActionRunnerScheduledTaskTest { try { sessionFactory.register(listener); Time.setOffset(Math.toIntExact(Duration.ofDays(12).toSeconds())); - System.out.println("Waiting for actions to be run for realm " + realmName); + System.out.println("Waiting for steps to be run for realm " + realmName); assertTrue(count.await(15, TimeUnit.SECONDS)); - System.out.println("... actions run for realm " + realmName); + System.out.println("... steps run for realm " + realmName); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/UserAttributePolicyConditionTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/UserAttributeWorkflowConditionTest.java similarity index 71% rename from tests/base/src/test/java/org/keycloak/tests/admin/model/policy/UserAttributePolicyConditionTest.java rename to tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/UserAttributeWorkflowConditionTest.java index 79401aaecaa..5cc1119751d 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/UserAttributePolicyConditionTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/UserAttributeWorkflowConditionTest.java @@ -1,4 +1,4 @@ -package org.keycloak.tests.admin.model.policy; +package org.keycloak.tests.admin.model.workflow; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; @@ -15,19 +15,19 @@ 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.WorkflowsResource; 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.UserAttributePolicyConditionFactory; -import org.keycloak.representations.resources.policies.ResourcePolicyActionRepresentation; -import org.keycloak.representations.resources.policies.ResourcePolicyConditionRepresentation; -import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation; +import org.keycloak.models.workflow.EventBasedWorkflowProviderFactory; +import org.keycloak.models.workflow.ResourceOperationType; +import org.keycloak.models.workflow.WorkflowsManager; +import org.keycloak.models.workflow.SetUserAttributeStepProviderFactory; +import org.keycloak.models.workflow.conditions.UserAttributeWorkflowConditionFactory; +import org.keycloak.representations.workflows.WorkflowStepRepresentation; +import org.keycloak.representations.workflows.WorkflowConditionRepresentation; +import org.keycloak.representations.workflows.WorkflowRepresentation; import org.keycloak.representations.userprofile.config.UPConfig; import org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy; import org.keycloak.testframework.annotations.InjectRealm; @@ -38,8 +38,8 @@ import org.keycloak.testframework.realm.UserConfigBuilder; import org.keycloak.testframework.remote.runonserver.InjectRunOnServer; import org.keycloak.testframework.remote.runonserver.RunOnServerClient; -@KeycloakIntegrationTest(config = RLMServerConfig.class) -public class UserAttributePolicyConditionTest { +@KeycloakIntegrationTest(config = WorkflowsServerConfig.class) +public class UserAttributeWorkflowConditionTest { private static final String REALM_NAME = "default"; @@ -59,7 +59,7 @@ public class UserAttributePolicyConditionTest { @Test public void testConditionForSingleValuedAttribute() { String expected = "valid"; - createPolicy(expected); + createWorkflow(expected); assertUserAttribute("user-1", false); assertUserAttribute("user-2", false, "not-valid"); assertUserAttribute("user-3", true, expected); @@ -68,7 +68,7 @@ public class UserAttributePolicyConditionTest { @Test public void testConditionForMultiValuedAttribute() { List expected = List.of("v1", "v2", "v3"); - createPolicy(expected); + createWorkflow(expected); assertUserAttribute("user-1", false, "v1"); assertUserAttribute("user-2", true, expected); assertUserAttribute("user-3", false, "v1", "v2", "v3", "v4"); @@ -77,7 +77,7 @@ public class UserAttributePolicyConditionTest { @Test public void testConditionForMultipleAttributes() { Map> expected = Map.of("a", List.of("a1"), "b", List.of("b1"), "c", List.of("c11", "c2")); - createPolicy(expected); + createWorkflow(expected); assertUserAttribute("user-1", false, Map.of("a", List.of("a3"), "b", List.of("b1"), "c", List.of("c1", "c2"))); assertUserAttribute("user-2", true, expected); assertUserAttribute("user-3", false, Map.of("a", List.of("a1"), "b", List.of("b1"))); @@ -104,9 +104,9 @@ public class UserAttributePolicyConditionTest { RealmModel realm = configureSessionContext(session); try { - // set offset to 7 days - notify action should run now + // set offset to 7 days - notify step should run now Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds())); - new ResourcePolicyManager(session).runScheduledActions(); + new WorkflowsManager(session).runScheduledSteps(); } finally { Time.setOffset(0); } @@ -122,34 +122,34 @@ public class UserAttributePolicyConditionTest { })); } - private void createPolicy(String... expectedValues) { - createPolicy(Map.of("attribute", List.of(expectedValues))); + private void createWorkflow(String... expectedValues) { + createWorkflow(Map.of("attribute", List.of(expectedValues))); } - private void createPolicy(List expectedValues) { - createPolicy(Map.of("attribute", expectedValues)); + private void createWorkflow(List expectedValues) { + createWorkflow(Map.of("attribute", expectedValues)); } - private void createPolicy(Map> attributes) { - List expectedPolicies = ResourcePolicyRepresentation.create() - .of(EventBasedResourcePolicyProviderFactory.ID) + private void createWorkflow(Map> attributes) { + List expectedWorkflows = WorkflowRepresentation.create() + .of(EventBasedWorkflowProviderFactory.ID) .onEvent(ResourceOperationType.CREATE.name()) .recurring() - .onConditions(ResourcePolicyConditionRepresentation.create() - .of(UserAttributePolicyConditionFactory.ID) + .onConditions(WorkflowConditionRepresentation.create() + .of(UserAttributeWorkflowConditionFactory.ID) .withConfig(attributes) .build()) - .withActions( - ResourcePolicyActionRepresentation.create() - .of(SetUserAttributeActionProviderFactory.ID) + .withSteps( + WorkflowStepRepresentation.create() + .of(SetUserAttributeStepProviderFactory.ID) .withConfig("notified", "true") .after(Duration.ofDays(5)) .build() ).build(); - RealmResourcePolicies policies = managedRealm.admin().resources().policies(); + WorkflowsResource workflows = managedRealm.admin().workflows(); - try (Response response = policies.create(expectedPolicies)) { + try (Response response = workflows.create(expectedWorkflows)) { assertThat(response.getStatus(), is(Status.CREATED.getStatusCode())); } } diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/UserCreationTimePolicyTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/UserCreationTimeWorkflowTest.java similarity index 70% rename from tests/base/src/test/java/org/keycloak/tests/admin/model/policy/UserCreationTimePolicyTest.java rename to tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/UserCreationTimeWorkflowTest.java index 60160b0811c..d92c560a011 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/UserCreationTimePolicyTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/UserCreationTimeWorkflowTest.java @@ -1,4 +1,4 @@ -package org.keycloak.tests.admin.model.policy; +package org.keycloak.tests.admin.model.workflow; import jakarta.mail.internet.MimeMessage; import org.junit.jupiter.api.Test; @@ -6,14 +6,14 @@ 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.DisableUserActionProviderFactory; -import org.keycloak.models.policy.NotifyUserActionProviderFactory; -import org.keycloak.models.policy.ResourcePolicyManager; -import org.keycloak.models.policy.UserCreationTimeResourcePolicyProviderFactory; +import org.keycloak.models.workflow.DisableUserStepProviderFactory; +import org.keycloak.models.workflow.NotifyUserStepProviderFactory; +import org.keycloak.models.workflow.WorkflowsManager; +import org.keycloak.models.workflow.UserCreationTimeWorkflowProviderFactory; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.UserRepresentation; -import org.keycloak.representations.resources.policies.ResourcePolicyActionRepresentation; -import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation; +import org.keycloak.representations.workflows.WorkflowStepRepresentation; +import org.keycloak.representations.workflows.WorkflowRepresentation; import org.keycloak.testframework.annotations.InjectRealm; import org.keycloak.testframework.annotations.KeycloakIntegrationTest; import org.keycloak.testframework.mail.MailServer; @@ -35,10 +35,10 @@ 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.keycloak.tests.admin.model.policy.ResourcePolicyManagementTest.findEmailByRecipient; +import static org.keycloak.tests.admin.model.workflow.WorkflowManagementTest.findEmailByRecipient; -@KeycloakIntegrationTest(config = RLMServerConfig.class) -public class UserCreationTimePolicyTest { +@KeycloakIntegrationTest(config = WorkflowsServerConfig.class) +public class UserCreationTimeWorkflowTest { private static final String REALM_NAME = "default"; @@ -62,40 +62,40 @@ public class UserCreationTimePolicyTest { @Test public void testDisableUserBasedOnCreationDate() { - managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() - .of(UserCreationTimeResourcePolicyProviderFactory.ID) - .withActions( - ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID) + managedRealm.admin().workflows().create(WorkflowRepresentation.create() + .of(UserCreationTimeWorkflowProviderFactory.ID) + .withSteps( + WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) .after(Duration.ofDays(5)) .build(), - ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID) + WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID) .after(Duration.ofDays(5)) .build() ).build()).close(); - // create a new user - this will trigger the association with the policy + // create a new user - this will trigger the association with the workflow managedRealm.admin().users().create( - this.getUserRepresentation("alice", "Alice", "Wonderland", "alice@wornderland.org")); + this.getUserRepresentation("alice", "Alice", "Wonderland", "alice@wornderland.org")).close(); - // test running the scheduled actions + // test running the scheduled steps runOnServer.run((session -> { RealmModel realm = configureSessionContext(session); - ResourcePolicyManager manager = new ResourcePolicyManager(session); + WorkflowsManager manager = new WorkflowsManager(session); UserModel user = session.users().getUserByUsername(realm, "alice"); assertTrue(user.isEnabled()); assertNull(user.getAttributes().get("message")); - // running the scheduled tasks now shouldn't pick up any action as none are due to run yet - manager.runScheduledActions(); + // running the scheduled tasks now shouldn't pick up any step as none are due to run yet + manager.runScheduledSteps(); user = session.users().getUserByUsername(realm, "alice"); assertTrue(user.isEnabled()); assertNull(user.getAttributes().get("message")); try { - // set offset to 7 days - notify action should run now + // set offset to 7 days - notify step should run now Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds())); - manager.runScheduledActions(); + manager.runScheduledSteps(); user = session.users().getUserByUsername(realm, "alice"); assertTrue(user.isEnabled()); } finally { @@ -103,27 +103,27 @@ public class UserCreationTimePolicyTest { } })); - // Verify that the notify action was executed by checking email was sent + // Verify that the notify step was executed by checking email was sent MimeMessage testUserMessage = findEmailByRecipient(mailServer, "alice@wornderland.org"); - assertNotNull(testUserMessage, "The first action (notify) should have sent an email."); + assertNotNull(testUserMessage, "The first step (notify) should have sent an email."); mailServer.runCleanup(); - // logging-in with alice should not reset the policy - we should still run the disable action next + // logging-in with alice should not reset the workflow - we should still run the disable step next oauth.openLoginForm(); loginPage.fillLogin("alice", "alice"); loginPage.submit(); assertTrue(driver.getPageSource().contains("Happy days")); - // test running the scheduled actions + // test running the scheduled steps runOnServer.run((session -> { RealmModel realm = configureSessionContext(session); - ResourcePolicyManager manager = new ResourcePolicyManager(session); + WorkflowsManager manager = new WorkflowsManager(session); try { - // set offset to 11 days - disable action should run now + // set offset to 11 days - disable step should run now Time.setOffset(Math.toIntExact(Duration.ofDays(12).toSeconds())); - manager.runScheduledActions(); + manager.runScheduledSteps(); UserModel user = session.users().getUserByUsername(realm, "alice"); assertFalse(user.isEnabled()); } finally { diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/UserSessionRefreshTimePolicyTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/UserSessionRefreshTimeWorkflowTest.java similarity index 69% rename from tests/base/src/test/java/org/keycloak/tests/admin/model/policy/UserSessionRefreshTimePolicyTest.java rename to tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/UserSessionRefreshTimeWorkflowTest.java index ad1f46b57a7..fa63a35f1e3 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/UserSessionRefreshTimePolicyTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/UserSessionRefreshTimeWorkflowTest.java @@ -15,15 +15,15 @@ * limitations under the License. */ -package org.keycloak.tests.admin.model.policy; +package org.keycloak.tests.admin.model.workflow; 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.assertTrue; -import static org.keycloak.tests.admin.model.policy.ResourcePolicyManagementTest.findEmailByRecipient; -import static org.keycloak.tests.admin.model.policy.ResourcePolicyManagementTest.findEmailsByRecipient; -import static org.keycloak.tests.admin.model.policy.ResourcePolicyManagementTest.verifyEmailContent; +import static org.keycloak.tests.admin.model.workflow.WorkflowManagementTest.findEmailByRecipient; +import static org.keycloak.tests.admin.model.workflow.WorkflowManagementTest.findEmailsByRecipient; +import static org.keycloak.tests.admin.model.workflow.WorkflowManagementTest.verifyEmailContent; import java.time.Duration; import java.util.List; @@ -36,13 +36,13 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; 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; -import org.keycloak.representations.resources.policies.ResourcePolicyRepresentation; +import org.keycloak.models.workflow.DisableUserStepProviderFactory; +import org.keycloak.models.workflow.NotifyUserStepProviderFactory; +import org.keycloak.models.workflow.ResourceOperationType; +import org.keycloak.models.workflow.WorkflowsManager; +import org.keycloak.models.workflow.UserSessionRefreshTimeWorkflowProviderFactory; +import org.keycloak.representations.workflows.WorkflowStepRepresentation; +import org.keycloak.representations.workflows.WorkflowRepresentation; import org.keycloak.testframework.annotations.InjectRealm; import org.keycloak.testframework.annotations.InjectUser; import org.keycloak.testframework.annotations.KeycloakIntegrationTest; @@ -62,8 +62,8 @@ import org.keycloak.testframework.ui.annotations.InjectWebDriver; import org.keycloak.testframework.ui.page.LoginPage; import org.openqa.selenium.WebDriver; -@KeycloakIntegrationTest(config = RLMServerConfig.class) -public class UserSessionRefreshTimePolicyTest { +@KeycloakIntegrationTest(config = WorkflowsServerConfig.class) +public class UserSessionRefreshTimeWorkflowTest { private static final String REALM_NAME = "default"; @@ -93,49 +93,49 @@ public class UserSessionRefreshTimePolicyTest { oauth.realm("default"); runOnServer.run(session -> { - ResourcePolicyManager manager = new ResourcePolicyManager(session); - manager.removePolicies(); + WorkflowsManager manager = new WorkflowsManager(session); + manager.removeWorkflows(); }); } @Test public void testDisabledUserAfterInactivityPeriod() { - managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() - .of(UserSessionRefreshTimeResourcePolicyProviderFactory.ID) + managedRealm.admin().workflows().create(WorkflowRepresentation.create() + .of(UserSessionRefreshTimeWorkflowProviderFactory.ID) .onEvent(ResourceOperationType.LOGIN.toString()) - .withActions( - ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID) + .withSteps( + WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) .after(Duration.ofDays(5)) .build(), - ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID) + WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID) .after(Duration.ofDays(5)) .build() ).build()).close(); - // login with alice - this will attach the policy to the user and schedule the first action + // login with alice - this will attach the workflow to the user and schedule the first step oauth.openLoginForm(); String username = userAlice.getUsername(); loginPage.fillLogin(username, userAlice.getPassword()); loginPage.submit(); assertTrue(driver.getPageSource() != null && driver.getPageSource().contains("Happy days")); - // test running the scheduled actions + // test running the scheduled steps runOnServer.run((session -> { RealmModel realm = configureSessionContext(session); - ResourcePolicyManager manager = new ResourcePolicyManager(session); + WorkflowsManager manager = new WorkflowsManager(session); UserModel user = session.users().getUserByUsername(realm, username); assertTrue(user.isEnabled()); - // running the scheduled tasks now shouldn't pick up any action as none are due to run yet - manager.runScheduledActions(); + // running the scheduled tasks now shouldn't pick up any step as none are due to run yet + manager.runScheduledSteps(); user = session.users().getUserByUsername(realm, username); assertTrue(user.isEnabled()); try { - // set offset to 6 days - notify action should run now + // set offset to 6 days - notify step should run now Time.setOffset(Math.toIntExact(Duration.ofDays(5).toSeconds())); - manager.runScheduledActions(); + manager.runScheduledSteps(); user = session.users().getUserByUsername(realm, username); assertTrue(user.isEnabled()); } finally { @@ -143,22 +143,22 @@ public class UserSessionRefreshTimePolicyTest { } })); - // Verify that the notify action was executed by checking email was sent + // Verify that the notify step was executed by checking email was sent MimeMessage testUserMessage = findEmailByRecipient(mailServer, "master-admin@email.org"); - assertNotNull(testUserMessage, "The first action (notify) should have sent an email."); + assertNotNull(testUserMessage, "The first step (notify) should have sent an email."); mailServer.runCleanup(); - // trigger a login event that should reset the flow of the policy + // trigger a login event that should reset the flow of the workflow oauth.openLoginForm(); runOnServer.run((session -> { try { - // setting the offset to 11 days should not run the second action as we re-started the flow on login + // setting the offset to 11 days should not run the second step as we re-started the flow on login RealmModel realm = configureSessionContext(session); Time.setOffset(Math.toIntExact(Duration.ofDays(11).toSeconds())); - ResourcePolicyManager manager = new ResourcePolicyManager(session); - manager.runScheduledActions(); + WorkflowsManager manager = new WorkflowsManager(session); + manager.runScheduledSteps(); UserModel user = session.users().getUserByUsername(realm, username); assertTrue(user.isEnabled()); } finally { @@ -166,13 +166,13 @@ public class UserSessionRefreshTimePolicyTest { } try { - // first action has run and the next action should be triggered after 5 more days (time difference between the actions) + // first step has run and the next step should be triggered after 5 more days (time difference between the steps) RealmModel realm = configureSessionContext(session); Time.setOffset(Math.toIntExact(Duration.ofDays(17).toSeconds())); - ResourcePolicyManager manager = new ResourcePolicyManager(session); - manager.runScheduledActions(); + WorkflowsManager manager = new WorkflowsManager(session); + manager.runScheduledSteps(); UserModel user = session.users().getUserByUsername(realm, username); - // second action should have run and the user should be disabled now + // second step should have run and the user should be disabled now assertFalse(user.isEnabled()); } finally { Time.setOffset(0); @@ -181,27 +181,27 @@ public class UserSessionRefreshTimePolicyTest { } @Test - public void testMultiplePolicies() { - managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() - .of(UserSessionRefreshTimeResourcePolicyProviderFactory.ID) + public void testMultipleWorkflows() { + managedRealm.admin().workflows().create(WorkflowRepresentation.create() + .of(UserSessionRefreshTimeWorkflowProviderFactory.ID) .onEvent(ResourceOperationType.LOGIN.toString()) - .withActions( - ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID) + .withSteps( + WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) .after(Duration.ofDays(5)) .withConfig("custom_subject_key", "notifier1_subject") .withConfig("custom_message", "notifier1_message") .build() - ).of(UserSessionRefreshTimeResourcePolicyProviderFactory.ID) + ).of(UserSessionRefreshTimeWorkflowProviderFactory.ID) .onEvent(ResourceOperationType.LOGIN.toString()) - .withActions( - ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID) + .withSteps( + WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) .after(Duration.ofDays(10)) .withConfig("custom_subject_key", "notifier2_subject") .withConfig("custom_message", "notifier2_message") .build()) .build()).close(); - // perform a login to associate the policies with the new user. + // perform a login to associate the workflows with the new user. oauth.openLoginForm(); String username = userAlice.getUsername(); loginPage.fillLogin(username, userAlice.getPassword()); @@ -210,7 +210,7 @@ public class UserSessionRefreshTimePolicyTest { runOnServer.run(session -> { RealmModel realm = configureSessionContext(session); - ResourcePolicyManager manager = new ResourcePolicyManager(session); + WorkflowsManager manager = new WorkflowsManager(session); UserProvider users = session.users(); UserModel user = users.getUserByUsername(realm, username); @@ -218,7 +218,7 @@ public class UserSessionRefreshTimePolicyTest { try { Time.setOffset(Math.toIntExact(Duration.ofDays(7).toSeconds())); - manager.runScheduledActions(); + manager.runScheduledSteps(); user = users.getUserByUsername(realm, username); assertTrue(user.isEnabled()); } finally { @@ -226,23 +226,23 @@ public class UserSessionRefreshTimePolicyTest { } }); - // Verify that the first notify action was executed by checking email was sent + // Verify that the first notify step was executed by checking email was sent List testUserMessages = findEmailsByRecipient(mailServer, "master-admin@email.org"); // Only one notify message should be sent assertEquals(1, testUserMessages.size()); - assertNotNull(testUserMessages.get(0), "The first action (notify) should have sent an email."); + assertNotNull(testUserMessages.get(0), "The first step (notify) should have sent an email."); verifyEmailContent(testUserMessages.get(0), "master-admin@email.org", "notifier1_subject", "notifier1_message"); mailServer.runCleanup(); runOnServer.run(session -> { RealmModel realm = configureSessionContext(session); - ResourcePolicyManager manager = new ResourcePolicyManager(session); + WorkflowsManager manager = new WorkflowsManager(session); UserModel user = session.users().getUserByUsername(realm, username); try { Time.setOffset(Math.toIntExact(Duration.ofDays(11).toSeconds())); - manager.runScheduledActions(); + manager.runScheduledSteps(); user = session.users().getUserByUsername(realm, username); assertTrue(user.isEnabled()); } finally { @@ -250,11 +250,11 @@ public class UserSessionRefreshTimePolicyTest { } }); - // Verify that the second notify action was executed by checking email was sent + // Verify that the second notify step was executed by checking email was sent testUserMessages = findEmailsByRecipient(mailServer, "master-admin@email.org"); // Only one notify message should be sent assertEquals(1, testUserMessages.size()); - assertNotNull(testUserMessages.get(0), "The second action (notify) should have sent an email."); + assertNotNull(testUserMessages.get(0), "The second step (notify) should have sent an email."); verifyEmailContent(testUserMessages.get(0), "master-admin@email.org", "notifier2_subject", "notifier2_message"); } diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/WorkflowManagementTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/WorkflowManagementTest.java new file mode 100644 index 00000000000..cf3401ba011 --- /dev/null +++ b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/WorkflowManagementTest.java @@ -0,0 +1,852 @@ +/* + * Copyright 2025 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.tests.admin.model.workflow; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +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 java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import jakarta.ws.rs.core.Response; +import org.hamcrest.Matchers; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import java.io.IOException; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.keycloak.admin.client.resource.WorkflowsResource; +import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory; +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.workflow.DeleteUserStepProviderFactory; +import org.keycloak.models.workflow.DisableUserStepProviderFactory; +import org.keycloak.models.workflow.EventBasedWorkflowProviderFactory; +import org.keycloak.models.workflow.NotifyUserStepProviderFactory; +import org.keycloak.models.workflow.WorkflowStep; +import org.keycloak.models.workflow.ResourceOperationType; +import org.keycloak.models.workflow.Workflow; +import org.keycloak.models.workflow.WorkflowsManager; +import org.keycloak.models.workflow.WorkflowStateProvider; +import org.keycloak.models.workflow.WorkflowStateProvider.ScheduledStep; +import org.keycloak.models.workflow.SetUserAttributeStepProviderFactory; +import org.keycloak.models.workflow.UserCreationTimeWorkflowProviderFactory; +import org.keycloak.models.workflow.conditions.IdentityProviderWorkflowConditionFactory; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.workflows.WorkflowStepRepresentation; +import org.keycloak.representations.workflows.WorkflowConditionRepresentation; +import org.keycloak.representations.workflows.WorkflowRepresentation; +import org.keycloak.testframework.annotations.InjectRealm; +import org.keycloak.testframework.mail.MailServer; +import org.keycloak.testframework.mail.annotations.InjectMailServer; +import org.keycloak.testframework.annotations.KeycloakIntegrationTest; +import org.keycloak.testframework.injection.LifeCycle; +import org.keycloak.testframework.realm.ManagedRealm; +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; +import org.keycloak.tests.utils.MailUtils; + +@KeycloakIntegrationTest(config = WorkflowsServerConfig.class) +public class WorkflowManagementTest { + + private static final String REALM_NAME = "default"; + + @InjectRunOnServer(permittedPackages = "org.keycloak.tests") + RunOnServerClient runOnServer; + + @InjectRealm(lifecycle = LifeCycle.METHOD) + ManagedRealm managedRealm; + + @InjectMailServer + private MailServer mailServer; + + @Test + public void testCreate() { + List expectedWorkflows = WorkflowRepresentation.create() + .of(UserCreationTimeWorkflowProviderFactory.ID) + .withSteps( + WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) + .after(Duration.ofDays(5)) + .build(), + WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID) + .after(Duration.ofDays(5)) + .build() + ).build(); + + WorkflowsResource workflows = managedRealm.admin().workflows(); + + try (Response response = workflows.create(expectedWorkflows)) { + assertThat(response.getStatus(), is(Response.Status.CREATED.getStatusCode())); + } + + List actualWorkflows = workflows.list(); + assertThat(actualWorkflows, Matchers.hasSize(1)); + + assertThat(actualWorkflows.get(0).getProviderId(), is(UserCreationTimeWorkflowProviderFactory.ID)); + assertThat(actualWorkflows.get(0).getSteps(), Matchers.hasSize(2)); + assertThat(actualWorkflows.get(0).getSteps().get(0).getProviderId(), is(NotifyUserStepProviderFactory.ID)); + assertThat(actualWorkflows.get(0).getSteps().get(1).getProviderId(), is(DisableUserStepProviderFactory.ID)); + } + + @Test + public void testCreateWithNoConditions() { + List expectedWorkflows = WorkflowRepresentation.create() + .of(EventBasedWorkflowProviderFactory.ID) + .withSteps( + WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) + .after(Duration.ofDays(5)) + .build(), + WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID) + .after(Duration.ofDays(5)) + .build() + ).build(); + + expectedWorkflows.get(0).setConditions(null); + + try (Response response = managedRealm.admin().workflows().create(expectedWorkflows)) { + assertThat(response.getStatus(), is(Response.Status.CREATED.getStatusCode())); + } + } + + @Test + public void testDelete() { + WorkflowsResource workflows = managedRealm.admin().workflows(); + + workflows.create(WorkflowRepresentation.create() + .of(UserCreationTimeWorkflowProviderFactory.ID) + .onEvent(ResourceOperationType.CREATE.toString()) + .recurring() + .withSteps( + WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) + .after(Duration.ofDays(5)) + .build() + ).of(EventBasedWorkflowProviderFactory.ID) + .onEvent(ResourceOperationType.LOGIN.toString()) + .recurring() + .withSteps( + WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) + .after(Duration.ofDays(5)) + .build() + ).build()).close(); + + // create a new user - should bind the user to the workflow and setup the only step in the workflow + managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").email("testuser@example.com").build()).close(); + + List actualWorkflows = workflows.list(); + assertThat(actualWorkflows, Matchers.hasSize(2)); + + WorkflowRepresentation workflow = actualWorkflows.stream().filter(p -> UserCreationTimeWorkflowProviderFactory.ID.equals(p.getProviderId())).findAny().orElse(null); + assertThat(workflow, notNullValue()); + String id = workflow.getId(); + workflows.workflow(id).delete().close(); + actualWorkflows = workflows.list(); + assertThat(actualWorkflows, Matchers.hasSize(1)); + + runOnServer.run((RunOnServer) session -> { + configureSessionContext(session); + WorkflowsManager manager = new WorkflowsManager(session); + + List registeredWorkflows = manager.getWorkflows(); + assertEquals(1, registeredWorkflows.size()); + WorkflowStateProvider stateProvider = session.getKeycloakSessionFactory().getProviderFactory(WorkflowStateProvider.class).create(session); + List steps = stateProvider.getScheduledStepsByWorkflow(id); + assertTrue(steps.isEmpty()); + }); + } + + @Test + public void testUpdate() { + List expectedWorkflows = WorkflowRepresentation.create() + .of(UserCreationTimeWorkflowProviderFactory.ID) + .name("test-workflow") + .withSteps( + WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) + .after(Duration.ofDays(5)) + .build(), + WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID) + .after(Duration.ofDays(10)) + .build() + ).build(); + + WorkflowsResource workflows = managedRealm.admin().workflows(); + + try (Response response = workflows.create(expectedWorkflows)) { + assertThat(response.getStatus(), is(Response.Status.CREATED.getStatusCode())); + } + + List actualWorkflows = workflows.list(); + assertThat(actualWorkflows, Matchers.hasSize(1)); + WorkflowRepresentation workflow = actualWorkflows.get(0); + assertThat(workflow.getName(), is("test-workflow")); + + workflow.setName("changed"); + managedRealm.admin().workflows().workflow(workflow.getId()).update(workflow).close(); + actualWorkflows = workflows.list(); + workflow = actualWorkflows.get(0); + assertThat(workflow.getName(), is("changed")); + } + + @Test + public void testWorkflowDoesNotFallThroughStepsInSingleRun() { + managedRealm.admin().workflows().create(WorkflowRepresentation.create() + .of(UserCreationTimeWorkflowProviderFactory.ID) + .onEvent(ResourceOperationType.CREATE.toString()) + .withSteps( + WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) + .after(Duration.ofDays(5)) + .build(), + WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID) + .after(Duration.ofDays(10)) + .build() + ).build()).close(); + + // create a new user - should bind the user to the workflow and setup the first step + managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").email("testuser@example.com").build()).close(); + + runOnServer.run((RunOnServer) session -> { + RealmModel realm = configureSessionContext(session); + WorkflowsManager manager = new WorkflowsManager(session); + UserModel user = session.users().getUserByUsername(realm,"testuser"); + + List registeredWorkflows = manager.getWorkflows(); + assertEquals(1, registeredWorkflows.size()); + + Workflow workflow = registeredWorkflows.get(0); + assertEquals(2, manager.getSteps(workflow.getId()).size()); + WorkflowStep notifyStep = manager.getSteps(workflow.getId()).get(0); + + WorkflowStateProvider stateProvider = session.getProvider(WorkflowStateProvider.class); + ScheduledStep scheduledStep = stateProvider.getScheduledStep(workflow.getId(), user.getId()); + assertNotNull(scheduledStep, "A step should have been scheduled for the user " + user.getUsername()); + assertEquals(notifyStep.getId(), scheduledStep.stepId()); + + try { + // Simulate the user being 12 days old, making them eligible for both steps' time conditions. + Time.setOffset(Math.toIntExact(Duration.ofDays(12).toSeconds())); + manager.runScheduledSteps(); + + user = session.users().getUserById(realm, user.getId()); + + // Verify that the next step was scheduled for the user + WorkflowStep disableStep = manager.getSteps(workflow.getId()).get(1); + scheduledStep = stateProvider.getScheduledStep(workflow.getId(), user.getId()); + assertNotNull(scheduledStep, "A step should have been scheduled for the user " + user.getUsername()); + assertEquals(disableStep.getId(), scheduledStep.stepId(), "The second step should have been scheduled"); + } finally { + Time.setOffset(0); + } + }); + + // Verify that the first step (notify) was executed by checking email was sent + MimeMessage testUserMessage = findEmailByRecipient(mailServer, "testuser@example.com"); + assertNotNull(testUserMessage, "The first step (notify) should have sent an email."); + + mailServer.runCleanup(); + } + + @Test + public void testAssignWorkflowToExistingResources() { + // create some realm users + for (int i = 0; i < 10; i++) { + managedRealm.admin().users().create(UserConfigBuilder.create().username("user-" + i).build()).close(); + } + + // 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()).close(); + } + + IdentityProviderRepresentation idp = new IdentityProviderRepresentation(); + idp.setAlias("someidp"); + idp.setProviderId(KeycloakOIDCIdentityProviderFactory.PROVIDER_ID); + idp.setEnabled(true); + managedRealm.admin().identityProviders().create(idp).close(); + + managedRealm.admin().workflows().create(WorkflowRepresentation.create() + .of(UserCreationTimeWorkflowProviderFactory.ID) + .onEvent(ResourceOperationType.ADD_FEDERATED_IDENTITY.name()) + .onConditions(WorkflowConditionRepresentation.create() + .of(IdentityProviderWorkflowConditionFactory.ID) + .withConfig(IdentityProviderWorkflowConditionFactory.EXPECTED_ALIASES, "someidp") + .build()) + .withSteps( + WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) + .after(Duration.ofDays(5)) + .build(), + WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID) + .after(Duration.ofDays(10)) + .build() + ).build()).close(); + + // now with the workflow in place, let's create a couple more idp users - these will be attached to the workflow 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()).close(); + } + + // new realm users created after the workflow - these should not be attached to the workflow because they are not idp users. + for (int i = 0; i < 3; i++) { + managedRealm.admin().users().create(UserConfigBuilder.create().username("new-user-" + i).build()).close(); + } + + runOnServer.run((RunOnServer) session -> { + RealmModel realm = configureSessionContext(session); + WorkflowsManager workflowsManager = new WorkflowsManager(session); + List registeredWorkflows = workflowsManager.getWorkflows(); + assertEquals(1, registeredWorkflows.size()); + Workflow workflow = registeredWorkflows.get(0); + + assertEquals(2, workflowsManager.getSteps(workflow.getId()).size()); + WorkflowStep notifyStep = workflowsManager.getSteps(workflow.getId()).get(0); + + // check no workflows are yet attached to the previous users, only to the ones created after the workflow was in place + WorkflowStateProvider stateProvider = session.getKeycloakSessionFactory().getProviderFactory(WorkflowStateProvider.class).create(session); + List scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow); + assertEquals(3, scheduledSteps.size()); + scheduledSteps.forEach(scheduledStep -> { + assertEquals(notifyStep.getId(), scheduledStep.stepId()); + UserModel user = session.users().getUserById(realm, scheduledStep.resourceId()); + assertNotNull(user); + assertTrue(user.getUsername().startsWith("new-idp-user-")); + }); + + try { + // let's run the schedule steps for the new users so they transition to the next one. + Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds())); + workflowsManager.runScheduledSteps(); + + // check the same users are now scheduled to run the second step. + WorkflowStep disableStep = workflowsManager.getSteps(workflow.getId()).get(1); + scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow); + assertEquals(3, scheduledSteps.size()); + scheduledSteps.forEach(scheduledStep -> { + assertEquals(disableStep.getId(), scheduledStep.stepId()); + UserModel user = session.users().getUserById(realm, scheduledStep.resourceId()); + assertNotNull(user); + assertTrue(user.getUsername().startsWith("new-idp-user-")); + }); + + // assign the workflow to the eligible users - i.e. only users from the same idp who are not yet assigned to the workflow. + workflowsManager.scheduleAllEligibleResources(workflow); + + // check workflow was correctly assigned to the old users, not affecting users already associated with the workflow. + scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow); + assertEquals(13, scheduledSteps.size()); + + List scheduledToNotify = scheduledSteps.stream() + .filter(step -> notifyStep.getId().equals(step.stepId())).toList(); + assertEquals(10, scheduledToNotify.size()); + scheduledToNotify.forEach(scheduledStep -> { + UserModel user = session.users().getUserById(realm, scheduledStep.resourceId()); + assertNotNull(user); + assertTrue(user.getUsername().startsWith("idp-user-")); + }); + + List scheduledToDisable = scheduledSteps.stream() + .filter(step -> disableStep.getId().equals(step.stepId())).toList(); + assertEquals(3, scheduledToDisable.size()); + scheduledToDisable.forEach(scheduledStep -> { + UserModel user = session.users().getUserById(realm, scheduledStep.resourceId()); + assertNotNull(user); + assertTrue(user.getUsername().startsWith("new-idp-user-")); + }); + + } finally { + Time.setOffset(0); + } + }); + } + + @Test + public void testDisableWorkflow() { + // create a test workflow + managedRealm.admin().workflows().create(WorkflowRepresentation.create() + .of(UserCreationTimeWorkflowProviderFactory.ID) + .onEvent(ResourceOperationType.CREATE.toString()) + .name("test-workflow") + .withSteps( + WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) + .after(Duration.ofDays(5)) + .build(), + WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID) + .after(Duration.ofDays(5)) + .build() + ).build()).close(); + + WorkflowsResource workflows = managedRealm.admin().workflows(); + List actualWorkflows = workflows.list(); + assertThat(actualWorkflows, Matchers.hasSize(1)); + WorkflowRepresentation workflow = actualWorkflows.get(0); + assertThat(workflow.getName(), is("test-workflow")); + + // create a new user - should bind the user to the workflow and setup the first step + managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").email("testuser@example.com").build()).close(); + + runOnServer.run((RunOnServer) session -> { + RealmModel realm = configureSessionContext(session); + WorkflowsManager manager = new WorkflowsManager(session); + + try { + // Advance time so the user is eligible for the first step, then run the scheduled steps so they transition to the next one. + Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds())); + manager.runScheduledSteps(); + + UserModel user = session.users().getUserByUsername(realm, "testuser"); + assertTrue(user.isEnabled(), "The second step (disable) should NOT have run."); + } finally { + Time.setOffset(0); + } + }); + + // Verify that the first step (notify) was executed by checking email was sent + MimeMessage testUserMessage = findEmailByRecipient(mailServer, "testuser@example.com"); + assertNotNull(testUserMessage, "The first step (notify) should have sent an email."); + + mailServer.runCleanup(); + + // disable the workflow - scheduled steps should be paused and workflow should not activate for new users + workflow.getConfig().putSingle("enabled", "false"); + managedRealm.admin().workflows().workflow(workflow.getId()).update(workflow).close(); + + // create another user - should NOT bind the user to the workflow as it is disabled + managedRealm.admin().users().create(UserConfigBuilder.create().username("anotheruser").build()).close(); + + runOnServer.run((RunOnServer) session -> { + RealmModel realm = configureSessionContext(session); + WorkflowsManager manager = new WorkflowsManager(session); + + List registeredWorkflow = manager.getWorkflows(); + assertEquals(1, registeredWorkflow.size()); + WorkflowStateProvider stateProvider = session.getKeycloakSessionFactory().getProviderFactory(WorkflowStateProvider.class).create(session); + List scheduledSteps = stateProvider.getScheduledStepsByWorkflow(registeredWorkflow.get(0)); + + // verify that there's only one scheduled step, for the first user + assertEquals(1, scheduledSteps.size()); + UserModel scheduledStepUser = session.users().getUserById(realm, scheduledSteps.get(0).resourceId()); + assertNotNull(scheduledStepUser); + assertTrue(scheduledStepUser.getUsername().startsWith("testuser")); + + try { + // Advance time so the first user would be eligible for the second step, then run the scheduled steps. + Time.setOffset(Math.toIntExact(Duration.ofDays(12).toSeconds())); + manager.runScheduledSteps(); + + UserModel user = session.users().getUserByUsername(realm, "testuser"); + // Verify that the step was NOT executed as the workflow is disabled. + assertTrue(user.isEnabled(), "The second step (disable) should NOT have run as the workflow is disabled."); + } finally { + Time.setOffset(0); + } + }); + + // re-enable the workflow - scheduled steps should resume and new users should be bound to the workflow + workflow.getConfig().putSingle("enabled", "true"); + managedRealm.admin().workflows().workflow(workflow.getId()).update(workflow).close(); + + // create a third user - should bind the user to the workflow as it is enabled again + managedRealm.admin().users().create(UserConfigBuilder.create().username("thirduser").email("thirduser@example.com").build()).close(); + + runOnServer.run((RunOnServer) session -> { + RealmModel realm = configureSessionContext(session); + WorkflowsManager manager = new WorkflowsManager(session); + + try { + // Advance time so the first user would be eligible for the second step, and third user would be eligible for the first step, then run the scheduled steps. + Time.setOffset(Math.toIntExact(Duration.ofDays(12).toSeconds())); + manager.runScheduledSteps(); + + UserModel user = session.users().getUserByUsername(realm, "testuser"); + // Verify that the step was executed as the workflow was re-enabled. + assertFalse(user.isEnabled(), "The second step (disable) should have run as the workflow was re-enabled."); + + // Verify that the third user was bound to the workflow + user = session.users().getUserByUsername(realm, "thirduser"); + assertTrue(user.isEnabled(), "The second step (disable) should NOT have run"); + } finally { + Time.setOffset(0); + } + }); + + // Verify that the first step (notify) was executed by checking email was sent + testUserMessage = findEmailByRecipient(mailServer, "thirduser@example.com"); + assertNotNull(testUserMessage, "The first step (notify) should have sent an email."); + + mailServer.runCleanup(); + } + + @Test + public void testRecurringWorkflow() { + managedRealm.admin().workflows().create(WorkflowRepresentation.create() + .of(UserCreationTimeWorkflowProviderFactory.ID) + .onEvent(ResourceOperationType.CREATE.toString()) + .recurring() + .withSteps( + WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) + .after(Duration.ofDays(5)) + .build() + ).build()).close(); + + // create a new user - should bind the user to the workflow and setup the only step in the workflow + managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").email("testuser@example.com").build()).close(); + + runOnServer.run((RunOnServer) session -> { + RealmModel realm = configureSessionContext(session); + WorkflowsManager manager = new WorkflowsManager(session); + + try { + Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds())); + manager.runScheduledSteps(); + + UserModel user = session.users().getUserByUsername(realm, "testuser"); + Workflow workflow = manager.getWorkflows().get(0); + WorkflowStep step = manager.getSteps(workflow.getId()).get(0); + + // Verify that the step was scheduled again for the user + WorkflowStateProvider stateProvider = session.getProvider(WorkflowStateProvider.class); + ScheduledStep scheduledStep = stateProvider.getScheduledStep(workflow.getId(), user.getId()); + assertNotNull(scheduledStep, "A step should have been scheduled for the user " + user.getUsername()); + assertEquals(step.getId(), scheduledStep.stepId(), "The step should have been scheduled again"); + + Time.setOffset(Math.toIntExact(Duration.ofDays(12).toSeconds())); + manager.runScheduledSteps(); + } finally { + Time.setOffset(0); + } + }); + + // Verify that there should be two emails sent + assertEquals(2, findEmailsByRecipient(mailServer, "testuser@example.com").size()); + mailServer.runCleanup(); + } + + @Test + public void testRunImmediateWorkflow() { + // create a test workflow with no time conditions - should run immediately when scheduled + managedRealm.admin().workflows().create(WorkflowRepresentation.create() + .of(UserCreationTimeWorkflowProviderFactory.ID) + .immediate() + .withSteps( + WorkflowStepRepresentation.create().of(SetUserAttributeStepProviderFactory.ID) + .after(Duration.ofDays(1)) + .withConfig("message", "message") + .build(), + WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID) + .after(Duration.ofDays(2)) + .build() + ).build()).close(); + + // create a new user - should be bound to the new workflow and all steps should run right away + managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").build()).close(); + + // check the user has the attribute set and is disabled + runOnServer.run(session -> { + configureSessionContext(session); + UserModel user = session.users().getUserByUsername(session.getContext().getRealm(), "testuser"); + assertEquals("message", user.getAttributes().get("message").get(0)); + assertFalse(user.isEnabled()); + }); + } + + @Test + public void testNotifyUserStepSendsEmailWithDefaultDisableMessage() { + // Create workflow: disable at 10 days, notify 3 days before (at day 7) + managedRealm.admin().workflows().create(WorkflowRepresentation.create() + .of(UserCreationTimeWorkflowProviderFactory.ID) + .withSteps( + WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) + .after(Duration.ofDays(7)) + .withConfig("reason", "inactivity") + .build(), + WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID) + .after(Duration.ofDays(3)) + .build() + ).build()).close(); + + managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser").email("test@example.com").name("John", "").build()).close(); + + runOnServer.run(session -> { + WorkflowsManager manager = new WorkflowsManager(session); + + try { + // Simulate user being 7 days old (eligible for notify step) + Time.setOffset(Math.toIntExact(Duration.ofDays(7).toSeconds())); + + manager.runScheduledSteps(); + } finally { + Time.setOffset(0); + } + }); + + // Verify email was sent to our test user + MimeMessage testUserMessage = findEmailByRecipient(mailServer, "test@example.com"); + assertNotNull(testUserMessage, "No email found for test@example.com"); + verifyEmailContent(testUserMessage, "test@example.com", "Disable", "John", "3", "inactivity"); + + mailServer.runCleanup(); + } + + @Test + public void testNotifyUserStepSendsEmailWithDefaultDeleteMessage() { + // Create workflow: delete at 30 days, notify 15 days before (at day 15) + managedRealm.admin().workflows().create(WorkflowRepresentation.create() + .of(UserCreationTimeWorkflowProviderFactory.ID) + .withSteps( + WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) + .after(Duration.ofDays(15)) + .withConfig("reason", "inactivity") + .build(), + WorkflowStepRepresentation.create().of(DeleteUserStepProviderFactory.ID) + .after(Duration.ofDays(15)) + .build() + ).build()).close(); + + managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser2").email("test2@example.com").name("Jane", "").build()).close(); + + runOnServer.run(session -> { + + WorkflowsManager manager = new WorkflowsManager(session); + + try { + // Simulate user being 15 days old + Time.setOffset(Math.toIntExact(Duration.ofDays(15).toSeconds())); + manager.runScheduledSteps(); + } finally { + Time.setOffset(0); + } + }); + + // Verify email was sent to our test user + MimeMessage testUserMessage = findEmailByRecipient(mailServer, "test2@example.com"); + assertNotNull(testUserMessage, "No email found for test2@example.com"); + verifyEmailContent(testUserMessage, "test2@example.com", "Deletion", "Jane", "15", "inactivity", "permanently deleted"); + + mailServer.runCleanup(); + } + + @Test + public void testNotifyUserStepWithCustomMessageOverride() { + // Create workflow: disable at 7 days, notify 2 days before (at day 5) with custom message + managedRealm.admin().workflows().create(WorkflowRepresentation.create() + .of(UserCreationTimeWorkflowProviderFactory.ID) + .withSteps( + WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) + .after(Duration.ofDays(5)) + .withConfig("reason", "compliance requirement") + .withConfig("custom_message", "Your account requires immediate attention due to new compliance policies.") + .withConfig("custom_subject_key", "customComplianceSubject") + .build(), + WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID) + .after(Duration.ofDays(7)) + .build() + ).build()).close(); + + managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser3").email("test3@example.com").name("Bob", "").build()).close(); + + runOnServer.run(session -> { + WorkflowsManager manager = new WorkflowsManager(session); + + try { + // Simulate user being 5 days old + Time.setOffset(Math.toIntExact(Duration.ofDays(5).toSeconds())); + manager.runScheduledSteps(); + } finally { + Time.setOffset(0); + } + }); + + // Verify email was sent to our test user + MimeMessage testUserMessage = findEmailByRecipient(mailServer, "test3@example.com"); + assertNotNull(testUserMessage, "No email found for test3@example.com"); + verifyEmailContent(testUserMessage, "test3@example.com", "", "Bob", "2", "immediate attention due to new compliance policies"); + + mailServer.runCleanup(); + } + + @Test + public void testNotifyUserStepSkipsUsersWithoutEmailButLogsWarning() { + managedRealm.admin().workflows().create(WorkflowRepresentation.create() + .of(UserCreationTimeWorkflowProviderFactory.ID) + .withSteps( + WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) + .after(Duration.ofDays(5)) + .build(), + WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID) + .after(Duration.ofDays(10)) + .build() + ).build()).close(); + + managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser4").name("NoEmail", "").build()).close(); + + runOnServer.run(session -> { + RealmModel realm = configureSessionContext(session); + WorkflowsManager manager = new WorkflowsManager(session); + + try { + Time.setOffset(Math.toIntExact(Duration.ofDays(5).toSeconds())); + manager.runScheduledSteps(); + + // But should still create state record for the workflow flow + UserModel user = session.users().getUserByUsername(realm, "testuser4"); + WorkflowStateProvider stateProvider = session.getProvider(WorkflowStateProvider.class); + var scheduledSteps = stateProvider.getScheduledStepsByResource(user.getId()); + assertEquals(1, scheduledSteps.size()); + } finally { + Time.setOffset(0); + } + }); + + // Should NOT send email to user without email address + MimeMessage testUserMessage = findEmailByRecipientContaining("testuser4"); + assertNull(testUserMessage, "No email should be sent to user without email address"); + } + + @Test + public void testCompleteUserLifecycleWithMultipleNotifications() { + // Create workflow: just disable at 30 days with one notification before + managedRealm.admin().workflows().create(WorkflowRepresentation.create() + .of(UserCreationTimeWorkflowProviderFactory.ID) + .withSteps( + WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) + .after(Duration.ofDays(15)) + .withConfig("reason", "inactivity") + .build(), + WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID) + .after(Duration.ofDays(15)) + .build() + ).build()).close(); + + managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser5").email("testuser5@example.com").name("TestUser5", "").build()).close(); + + runOnServer.run(session -> { + RealmModel realm = configureSessionContext(session); + WorkflowsManager manager = new WorkflowsManager(session); + UserModel user = session.users().getUserByUsername(realm, "testuser5"); + + try { + // Day 15: First notification - this should run the notify step and schedule the disable step + Time.setOffset(Math.toIntExact(Duration.ofDays(15).toSeconds())); + manager.runScheduledSteps(); + + // Check that user is still enabled after notification + user = session.users().getUserById(realm, user.getId()); + assertTrue(user.isEnabled(), "User should still be enabled after notification"); + + // Day 30 + 15 minutes: Disable user - run 15 minutes after the scheduled time to ensure it's due + Time.setOffset(Math.toIntExact(Duration.ofDays(30).toSeconds()) + Math.toIntExact(Duration.ofMinutes(15).toSeconds())); + manager.runScheduledSteps(); + + // Verify user is disabled + user = session.users().getUserById(realm, user.getId()); + assertNotNull(user, "User should still exist after disable"); + assertFalse(user.isEnabled(), "User should be disabled"); + + } finally { + Time.setOffset(0); + } + }); + + // Verify notification was sent + MimeMessage testUserMessage = findEmailByRecipient(mailServer, "testuser5@example.com"); + assertNotNull(testUserMessage, "No email found for testuser5@example.com"); + verifyEmailContent(testUserMessage, "testuser5@example.com", "Disable", "TestUser5", "15", "inactivity"); + + mailServer.runCleanup(); + } + + public static List findEmailsByRecipient(MailServer mailServer, String expectedRecipient) { + return Arrays.stream(mailServer.getReceivedMessages()) + .filter(msg -> { + try { + return MailUtils.getRecipient(msg).equals(expectedRecipient); + } catch (Exception e) { + return false; + } + }) + .toList(); + } + + public static MimeMessage findEmailByRecipient(MailServer mailServer, String expectedRecipient) { + return Arrays.stream(mailServer.getReceivedMessages()) + .filter(msg -> { + try { + return MailUtils.getRecipient(msg).equals(expectedRecipient); + } catch (Exception e) { + return false; + } + }) + .findFirst() + .orElse(null); + } + + private MimeMessage findEmailByRecipientContaining(String recipientPart) { + return Arrays.stream(mailServer.getReceivedMessages()) + .filter(msg -> { + try { + return MailUtils.getRecipient(msg).contains(recipientPart); + } catch (Exception e) { + return false; + } + }) + .findFirst() + .orElse(null); + } + + private static RealmModel configureSessionContext(KeycloakSession session) { + RealmModel realm = session.realms().getRealmByName(REALM_NAME); + session.getContext().setRealm(realm); + return realm; + } + + public static void verifyEmailContent(MimeMessage message, String expectedRecipient, String subjectContains, + String... contentContains) { + try { + assertEquals(expectedRecipient, MailUtils.getRecipient(message)); + assertThat(message.getSubject(), Matchers.containsString(subjectContains)); + + MailUtils.EmailBody body = MailUtils.getBody(message); + String textContent = body.getText(); + String htmlContent = body.getHtml(); + + for (String expectedContent : contentContains) { + boolean foundInText = textContent.contains(expectedContent); + boolean foundInHtml = htmlContent.contains(expectedContent); + assertTrue(foundInText || foundInHtml, + "Email content should contain: " + expectedContent + + "\nText: " + textContent + + "\nHTML: " + htmlContent); + } + } catch (MessagingException | IOException e) { + Assertions.fail("Failed to read email message: " + e.getMessage()); + } + } +} diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/WorkflowsScheduledTaskServerConfig.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/WorkflowsScheduledTaskServerConfig.java new file mode 100644 index 00000000000..94e342cfd57 --- /dev/null +++ b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/WorkflowsScheduledTaskServerConfig.java @@ -0,0 +1,13 @@ +package org.keycloak.tests.admin.model.workflow; + +import org.keycloak.models.workflow.WorkflowsEventListenerFactory; +import org.keycloak.testframework.server.KeycloakServerConfigBuilder; + +public class WorkflowsScheduledTaskServerConfig extends WorkflowsServerConfig { + + @Override + public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) { + return super.configure(config) + .option("spi-events-listener--" + WorkflowsEventListenerFactory.ID + "--step-runner-task-interval", "1000"); + } +} diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/RLMServerConfig.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/WorkflowsServerConfig.java similarity index 85% rename from tests/base/src/test/java/org/keycloak/tests/admin/model/policy/RLMServerConfig.java rename to tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/WorkflowsServerConfig.java index 0a69feecac8..4480edb83d1 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/RLMServerConfig.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/WorkflowsServerConfig.java @@ -15,16 +15,16 @@ * limitations under the License. */ -package org.keycloak.tests.admin.model.policy; +package org.keycloak.tests.admin.model.workflow; import org.keycloak.common.Profile.Feature; import org.keycloak.testframework.server.KeycloakServerConfig; import org.keycloak.testframework.server.KeycloakServerConfigBuilder; -public class RLMServerConfig implements KeycloakServerConfig { +public class WorkflowsServerConfig implements KeycloakServerConfig { @Override public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) { - return config.features(Feature.RESOURCE_LIFECYCLE); + return config.features(Feature.WORKFLOWS); } } diff --git a/themes/src/main/resources/theme/base/email/html/resource-policy-notification.ftl b/themes/src/main/resources/theme/base/email/html/workflow-notification.ftl similarity index 100% rename from themes/src/main/resources/theme/base/email/html/resource-policy-notification.ftl rename to themes/src/main/resources/theme/base/email/html/workflow-notification.ftl diff --git a/themes/src/main/resources/theme/base/email/text/resource-policy-notification.ftl b/themes/src/main/resources/theme/base/email/text/workflow-notification.ftl similarity index 100% rename from themes/src/main/resources/theme/base/email/text/resource-policy-notification.ftl rename to themes/src/main/resources/theme/base/email/text/workflow-notification.ftl