From 0d5dfc3eaea0f0140c0aeb64ffe327fc954d2b3d Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Wed, 10 Sep 2025 12:40:17 -0300 Subject: [PATCH] Add support for ad-hoc policies (#42508) Closes #42126 Signed-off-by: Pedro Igor --- .../client/resource/RealmResourcePolicy.java | 14 ++ .../EventBasedResourcePolicyProvider.java | 7 +- .../JpaResourcePolicyStateProvider.java | 25 +- .../policy/ResourcePolicyStateProvider.java | 16 +- .../models/policy/ResourceOperationType.java | 3 +- .../models/policy/ResourcePolicy.java | 9 + .../keycloak/models/policy/ResourceType.java | 18 +- .../policy/AdhocResourcePolicyEvent.java | 8 + .../models/policy/ResourcePolicyManager.java | 35 ++- .../resource/RealmResourcePolicyResource.java | 24 ++ .../admin/model/policy/AdhocPolicyTest.java | 219 ++++++++++++++++++ .../policy/ResourcePolicyManagementTest.java | 60 +++-- 12 files changed, 407 insertions(+), 31 deletions(-) create mode 100644 services/src/main/java/org/keycloak/models/policy/AdhocResourcePolicyEvent.java create mode 100644 tests/base/src/test/java/org/keycloak/tests/admin/model/policy/AdhocPolicyTest.java 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/RealmResourcePolicy.java index 15c15966a53..7424083405c 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/RealmResourcePolicy.java @@ -5,8 +5,12 @@ import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; import jakarta.ws.rs.PUT; +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.representations.resources.policies.ResourcePolicyRepresentation; @@ -22,4 +26,14 @@ public interface RealmResourcePolicy { @GET @Produces(APPLICATION_JSON) ResourcePolicyRepresentation toRepresentation(); + + @Path("bind/{type}/{resourceId}") + @POST + @Consumes(MediaType.APPLICATION_JSON) + void bind(@PathParam("type") String type, @PathParam("resourceId") String resourceId); + + @Path("bind/{type}/{resourceId}") + @POST + @Consumes(MediaType.APPLICATION_JSON) + void bind(@PathParam("type") String type, @PathParam("resourceId") String resourceId, Long milliseconds); } diff --git a/model/jpa/src/main/java/org/keycloak/models/policy/EventBasedResourcePolicyProvider.java b/model/jpa/src/main/java/org/keycloak/models/policy/EventBasedResourcePolicyProvider.java index 120593af892..22d90c88f89 100644 --- a/model/jpa/src/main/java/org/keycloak/models/policy/EventBasedResourcePolicyProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/policy/EventBasedResourcePolicyProvider.java @@ -43,14 +43,15 @@ public class EventBasedResourcePolicyProvider implements ResourcePolicyProvider } protected boolean isActivationEvent(ResourcePolicyEvent event) { - List events = model.getConfig().getOrDefault("events", List.of()); ResourceOperationType operation = event.getOperation(); - if (events.contains(operation.name())) { + if (ResourceOperationType.AD_HOC.equals(operation)) { return true; } - return false; + List events = model.getConfig().getOrDefault("events", List.of()); + + return events.contains(operation.name()); } @Override 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 index 5d97ac183c6..b553f1cf10c 100644 --- a/model/jpa/src/main/java/org/keycloak/models/policy/JpaResourcePolicyStateProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/policy/JpaResourcePolicyStateProvider.java @@ -28,6 +28,7 @@ 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; @@ -88,12 +89,16 @@ public class JpaResourcePolicyStateProvider implements ResourcePolicyStateProvid } @Override - public List getScheduledActionsByPolicy(ResourcePolicy policy) { + 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"), policy.getId()); + Predicate byPolicy = cb.equal(stateRoot.get("policyId"), id); query.where(byPolicy); return em.createQuery(query).getResultStream() @@ -139,6 +144,22 @@ public class JpaResourcePolicyStateProvider implements ResourcePolicyStateProvid } } + @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(); 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/policy/ResourcePolicyStateProvider.java index 3344efd3f1e..5793297cffa 100644 --- a/model/storage-private/src/main/java/org/keycloak/models/policy/ResourcePolicyStateProvider.java +++ b/model/storage-private/src/main/java/org/keycloak/models/policy/ResourcePolicyStateProvider.java @@ -40,6 +40,12 @@ public interface ResourcePolicyStateProvider extends Provider { */ void remove(String policyId, String resourceId); + /** + * Removes any record identified by the specified {@code policyId}. + * @param policyId the id of the policy. + */ + void remove(String policyId); + /** * Deletes all state records associated with the current realm bound to the session. */ @@ -55,7 +61,15 @@ public interface ResourcePolicyStateProvider extends Provider { List getScheduledActionsByResource(String resourceId); - List getScheduledActionsByPolicy(ResourcePolicy policy); + List getScheduledActionsByPolicy(String policy); + + default List getScheduledActionsByPolicy(ResourcePolicy policy) { + if (policy == null) { + return List.of(); + } + + return getScheduledActionsByPolicy(policy.getId()); + } List getDueScheduledActions(ResourcePolicy policy); 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/policy/ResourceOperationType.java index c3b2ff3a6cc..ff7b1372f5d 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourceOperationType.java +++ b/server-spi-private/src/main/java/org/keycloak/models/policy/ResourceOperationType.java @@ -20,7 +20,8 @@ public enum ResourceOperationType { ADD_FEDERATED_IDENTITY(new Class[] {FederatedIdentityCreatedEvent.class}, new Class[] {FederatedIdentityRemovedEvent.class}), REMOVE_FEDERATED_IDENTITY(FederatedIdentityRemovedEvent.class), GROUP_MEMBERSHIP_JOIN(GroupMemberJoinEvent.class), - ROLE_GRANTED(new Class[] {RoleGrantedEvent.class}, new Class[] {RoleRevokedEvent.class}); + ROLE_GRANTED(new Class[] {RoleGrantedEvent.class}, new Class[] {RoleRevokedEvent.class}), + AD_HOC(new Class[] {}); private final List types; private final List deactivationTypes; 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/policy/ResourcePolicy.java index c0be3782319..670c13ae366 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourcePolicy.java +++ b/server-spi-private/src/main/java/org/keycloak/models/policy/ResourcePolicy.java @@ -28,6 +28,7 @@ public class ResourcePolicy { private MultivaluedHashMap config; private String providerId; private String id; + private Long notBefore; public ResourcePolicy() { // reflection @@ -75,4 +76,12 @@ public class ResourcePolicy { public boolean isScheduled() { return config != null && Boolean.parseBoolean(config.getFirstOrDefault("scheduled", "true")); } + + public Long getNotBefore() { + return notBefore; + } + + public void setNotBefore(Long milliseconds) { + this.notBefore = milliseconds; + } } 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/policy/ResourceType.java index b4d2b120045..8f8ccb0aab6 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/policy/ResourceType.java +++ b/server-spi-private/src/main/java/org/keycloak/models/policy/ResourceType.java @@ -23,25 +23,35 @@ import org.keycloak.events.Event; import org.keycloak.events.EventType; import org.keycloak.events.admin.AdminEvent; import org.keycloak.events.admin.OperationType; +import org.keycloak.models.KeycloakSession; import org.keycloak.provider.ProviderEvent; import java.util.List; import java.util.Objects; +import java.util.function.BiFunction; public enum ResourceType { - USERS(org.keycloak.events.admin.ResourceType.USER, List.of(OperationType.CREATE), List.of(EventType.LOGIN, EventType.REGISTER)); + USERS( + org.keycloak.events.admin.ResourceType.USER, + List.of(OperationType.CREATE), + List.of(EventType.LOGIN, EventType.REGISTER), + (session, id) -> session.users().getUserById(session.getContext().getRealm(), id) + ); private final org.keycloak.events.admin.ResourceType supportedAdminResourceType; private final List supportedAdminOperationTypes; private final List supportedEventTypes; + private final BiFunction resourceResolver; ResourceType(org.keycloak.events.admin.ResourceType supportedAdminResourceType, List supportedAdminOperationTypes, - List supportedEventTypes) { + List supportedEventTypes, + BiFunction resourceResolver) { this.supportedAdminResourceType = supportedAdminResourceType; this.supportedAdminOperationTypes = supportedAdminOperationTypes; this.supportedEventTypes = supportedEventTypes; + this.resourceResolver = resourceResolver; } public ResourcePolicyEvent toEvent(AdminEvent event) { @@ -84,4 +94,8 @@ public enum ResourceType { return new ResourcePolicyEvent(this, resourceOperationType, resourceId, event); } + + public Object resolveResource(KeycloakSession session, String id) { + return resourceResolver.apply(session, id); + } } diff --git a/services/src/main/java/org/keycloak/models/policy/AdhocResourcePolicyEvent.java b/services/src/main/java/org/keycloak/models/policy/AdhocResourcePolicyEvent.java new file mode 100644 index 00000000000..d13b8dc49c0 --- /dev/null +++ b/services/src/main/java/org/keycloak/models/policy/AdhocResourcePolicyEvent.java @@ -0,0 +1,8 @@ +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/ResourcePolicyManager.java b/services/src/main/java/org/keycloak/models/policy/ResourcePolicyManager.java index 05e8a565fd0..3cb8ab65e9b 100644 --- a/services/src/main/java/org/keycloak/models/policy/ResourcePolicyManager.java +++ b/services/src/main/java/org/keycloak/models/policy/ResourcePolicyManager.java @@ -23,6 +23,7 @@ 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; @@ -154,7 +155,14 @@ public class ResourcePolicyManager { } private ResourceAction getFirstAction(ResourcePolicy policy) { - return getActions(policy.getId()).get(0); + ResourceAction action = getActions(policy.getId()).get(0); + Long notBefore = policy.getNotBefore(); + + if (notBefore != null) { + action.setAfter(notBefore); + } + + return action; } private ResourcePolicyProvider getPolicyProvider(ResourcePolicy policy) { @@ -184,19 +192,18 @@ public class ResourcePolicyManager { public void scheduleAllEligibleResources(ResourcePolicy policy) { if (policy.isEnabled()) { ResourcePolicyProvider provider = getPolicyProvider(policy); - ResourceAction firstAction = getFirstAction(policy); - provider.getEligibleResourcesForInitialAction().forEach(resourceId -> { - // TODO run each scheduling task in a separate tx as other txs might schedule an action while this is running. - this.policyStateProvider.scheduleAction(policy, firstAction, resourceId); - }); + 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(); - List policies = this.getPolicies(); // iterate through the policies, and for those not yet assigned to the user check if they can be assigned policies.stream() @@ -277,6 +284,7 @@ public class ResourcePolicyManager { realm.getComponentsStream(policy.getId(), ResourceActionProvider.class.getName()).forEach(realm::removeComponent); realm.removeComponent(policy); }); + policyStateProvider.remove(id); } public ResourcePolicy getPolicy(String id) { @@ -316,8 +324,9 @@ public class ResourcePolicyManager { public ResourcePolicy toModel(ResourcePolicyRepresentation rep) { MultivaluedHashMap config = ofNullable(rep.getConfig()).orElse(new MultivaluedHashMap<>()); + List conditions = ofNullable(rep.getConditions()).orElse(List.of()); - for (ResourcePolicyConditionRepresentation condition : rep.getConditions()) { + for (ResourcePolicyConditionRepresentation condition : conditions) { String conditionProviderId = condition.getProviderId(); config.computeIfAbsent("conditions", key -> new ArrayList<>()).add(conditionProviderId); @@ -347,4 +356,14 @@ public class ResourcePolicyManager { 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/realm/resources/policies/admin/resource/RealmResourcePolicyResource.java b/services/src/main/java/org/keycloak/realm/resources/policies/admin/resource/RealmResourcePolicyResource.java index 876acabae76..a772b7805c3 100644 --- a/services/src/main/java/org/keycloak/realm/resources/policies/admin/resource/RealmResourcePolicyResource.java +++ b/services/src/main/java/org/keycloak/realm/resources/policies/admin/resource/RealmResourcePolicyResource.java @@ -2,12 +2,19 @@ package org.keycloak.realm.resources.policies.admin.resource; import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; import jakarta.ws.rs.PUT; +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; class RealmResourcePolicyResource { @@ -35,4 +42,21 @@ class RealmResourcePolicyResource { public ResourcePolicyRepresentation toRepresentation() { return manager.toRepresentation(policy); } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Path("bind/{type}/{resourceId}") + public void bind(@PathParam("type") ResourceType type, @PathParam("resourceId") String resourceId, Long notBefore) { + Object resource = manager.resolveResource(type, resourceId); + + if (resource == null) { + throw new BadRequestException("Resource with id " + resourceId + " not found"); + } + + if (notBefore != null) { + policy.setNotBefore(notBefore); + } + + manager.bind(policy, type, resourceId); + } } 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/policy/AdhocPolicyTest.java new file mode 100644 index 00000000000..40874e5ac5f --- /dev/null +++ b/tests/base/src/test/java/org/keycloak/tests/admin/model/policy/AdhocPolicyTest.java @@ -0,0 +1,219 @@ +package org.keycloak.tests.admin.model.policy; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.time.Duration; +import java.util.List; + +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.Test; +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.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.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.remote.runonserver.InjectRunOnServer; +import org.keycloak.testframework.remote.runonserver.RunOnServerClient; +import org.keycloak.testframework.util.ApiUtil; + +@KeycloakIntegrationTest(config = RLMServerConfig.class) +public class AdhocPolicyTest { + + private static final String REALM_NAME = "default"; + + @InjectRunOnServer(permittedPackages = "org.keycloak.tests") + RunOnServerClient runOnServer; + + @InjectRealm(lifecycle = LifeCycle.METHOD) + ManagedRealm managedRealm; + + @Test + public void testCreate() { + managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() + .of(EventBasedResourcePolicyProviderFactory.ID) + .withActions(ResourcePolicyActionRepresentation.create() + .of(SetUserAttributeActionProviderFactory.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)); + } + + @Test + public void testRunAdHocScheduledPolicy() { + managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() + .of(EventBasedResourcePolicyProviderFactory.ID) + .withActions(ResourcePolicyActionRepresentation.create() + .of(SetUserAttributeActionProviderFactory.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); + + 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); + } + + runOnServer.run((session -> { + RealmModel realm = configureSessionContext(session); + ResourcePolicyManager manager = new ResourcePolicyManager(session); + UserModel user = session.users().getUserByUsername(realm, "alice"); + + manager.runScheduledActions(); + assertNull(user.getAttributes().get("message")); + + try { + Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds())); + manager.runScheduledActions(); + user = session.users().getUserByUsername(realm, "alice"); + assertNotNull(user.getAttributes().get("message")); + } finally { + Time.setOffset(0); + } + })); + } + + @Test + public void testRunAdHocNonScheduledPolicy() { + managedRealm.admin().resources().policies().create(ResourcePolicyRepresentation.create() + .of(EventBasedResourcePolicyProviderFactory.ID) + .withActions(ResourcePolicyActionRepresentation.create() + .of(SetUserAttributeActionProviderFactory.ID) + .withConfig("message", "message") + .build()) + .build()).close(); + + List policies = managedRealm.admin().resources().policies().list(); + assertThat(policies, hasSize(1)); + ResourcePolicyRepresentation policy = policies.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); + } + + runOnServer.run((session -> { + RealmModel realm = configureSessionContext(session); + ResourcePolicyManager manager = new ResourcePolicyManager(session); + UserModel user = session.users().getUserByUsername(realm, "alice"); + + manager.runScheduledActions(); + 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) + .withConfig("message", "message") + .build()) + .build()).close(); + + List policies = managedRealm.admin().resources().policies().list(); + assertThat(policies, hasSize(1)); + ResourcePolicyRepresentation policy = policies.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()); + } + + runOnServer.run((session -> { + RealmModel realm = configureSessionContext(session); + ResourcePolicyManager manager = new ResourcePolicyManager(session); + UserModel user = session.users().getUserByUsername(realm, "alice"); + + manager.runScheduledActions(); + assertNull(user.getAttributes().get("message")); + + try { + Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds())); + manager.runScheduledActions(); + user = session.users().getUserByUsername(realm, "alice"); + assertNotNull(user.getAttributes().get("message")); + } finally { + user.removeAttribute("message"); + Time.setOffset(0); + } + })); + + managedRealm.admin().resources().policies().policy(policy.getId()).bind(ResourceType.USERS.name(), id, Duration.ofDays(10).toMillis()); + + runOnServer.run((session -> { + RealmModel realm = configureSessionContext(session); + ResourcePolicyManager manager = new ResourcePolicyManager(session); + UserModel user = session.users().getUserByUsername(realm, "alice"); + + manager.runScheduledActions(); + assertNull(user.getAttributes().get("message")); + + try { + Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds())); + manager.runScheduledActions(); + user = session.users().getUserByUsername(realm, "alice"); + assertNull(user.getAttributes().get("message")); + } finally { + Time.setOffset(0); + } + + try { + Time.setOffset(Math.toIntExact(Duration.ofDays(11).toSeconds())); + manager.runScheduledActions(); + user = session.users().getUserByUsername(realm, "alice"); + assertNotNull(user.getAttributes().get("message")); + } finally { + Time.setOffset(0); + } + })); + } + + private static RealmModel configureSessionContext(KeycloakSession session) { + RealmModel realm = session.realms().getRealmByName(REALM_NAME); + session.getContext().setRealm(realm); + return realm; + } + + private UserRepresentation getUserRepresentation(String username, String firstName, String lastName, String email) { + UserRepresentation representation = new UserRepresentation(); + representation.setUsername(username); + representation.setFirstName(firstName); + representation.setLastName(lastName); + representation.setEmail(email); + representation.setEnabled(true); + CredentialRepresentation credential = new CredentialRepresentation(); + credential.setType(CredentialRepresentation.PASSWORD); + credential.setValue(username); + representation.setCredentials(List.of(credential)); + return representation; + } +} 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 index 47f2e5ca784..c91d9d00b48 100644 --- 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 @@ -46,15 +46,16 @@ 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.UserSessionRefreshTimeResourcePolicyProviderFactory; import org.keycloak.models.policy.conditions.IdentityProviderPolicyConditionFactory; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.resources.policies.ResourcePolicyActionRepresentation; @@ -121,9 +122,9 @@ public class ResourcePolicyManagementTest { } @Test - public void testDelete() { + public void testCreateWithNoConditions() { List expectedPolicies = ResourcePolicyRepresentation.create() - .of(UserCreationTimeResourcePolicyProviderFactory.ID) + .of(EventBasedResourcePolicyProviderFactory.ID) .withActions( ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID) .after(Duration.ofDays(5)) @@ -131,29 +132,60 @@ public class ResourcePolicyManagementTest { ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID) .after(Duration.ofDays(10)) .build() - ).of(UserSessionRefreshTimeResourcePolicyProviderFactory.ID) - .withActions( - ResourcePolicyActionRepresentation.create().of(NotifyUserActionProviderFactory.ID) - .after(Duration.ofDays(5)) - .build(), - ResourcePolicyActionRepresentation.create().of(DisableUserActionProviderFactory.ID) - .after(Duration.ofDays(10)) - .build()) - .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.get(0); - managedRealm.admin().resources().policies().policy(policy.getId()).delete().close(); + 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