Rework RLM core to schedule action based on events @sguilhen (#42010)

* Rework RLM core to schedule action based on events

Closes #41803

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
Signed-off-by: Stefan Guilhen <sguilhen@redhat.com>
Co-authored-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Stefan Guilhen 2025-08-20 14:59:52 -03:00 committed by GitHub
parent 03b5753c84
commit 70659ac183
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 826 additions and 498 deletions

View File

@ -472,17 +472,6 @@ public class UserAdapter implements CachedUserModel {
return cached.getGroups(keycloakSession, modelSupplier).contains(group.getId()) || RoleUtils.isMember(getGroupsStream(), group);
}
@Override
public void setLastSessionRefreshTime(int lastSessionRefreshTime) {
if (ResourcePolicyManager.isFeatureEnabled()) {
UserModel delegate = modelSupplier.get();
if (delegate != null) {
delegate.setLastSessionRefreshTime(lastSessionRefreshTime);
}
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@ -252,8 +252,6 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
entity.setStarted(currentTime);
entity.setLastSessionRefresh(currentTime);
user.setLastSessionRefreshTime(entity.getLastSessionRefresh());
}
@Override

View File

@ -264,8 +264,6 @@ public class UserSessionAdapter<T extends SessionRefreshStore & UserSessionProvi
}
};
getUser().setLastSessionRefreshTime(lastSessionRefresh);
update(task);
}

View File

@ -272,8 +272,6 @@ public class UserSessionEntity extends SessionEntity {
entity.setStarted(currentTime);
entity.setLastSessionRefresh(currentTime);
user.setLastSessionRefreshTime(entity.getLastSessionRefresh());
}
public static UserSessionEntity createFromModel(UserSessionModel userSession) {

View File

@ -577,13 +577,6 @@ public class UserAdapter implements UserModel, JpaModel<UserEntity> {
return new UserCredentialManager(session, realm, this);
}
@Override
public void setLastSessionRefreshTime(int lastSessionRefreshTime) {
if (ResourcePolicyManager.isFeatureEnabled()) {
user.setLastSessionRefreshTime(lastSessionRefreshTime);
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@ -91,9 +91,6 @@ public class UserEntity {
@Column(name = "REALM_ID")
protected String realmId;
@Column(name = "LAST_SESSION_REFRESH_TIME")
private Integer lastSessionRefreshTime;
// Explicitly not using OrphanRemoval as we're handling the removal manually through HQL but at the same time we still
// want to remove elements from the entity's collection in a manual way. Without this, Hibernate would do a duplicit
// delete query.
@ -229,14 +226,6 @@ public class UserEntity {
this.realmId = realmId;
}
public Integer getLastSessionRefreshTime() {
return lastSessionRefreshTime;
}
public void setLastSessionRefreshTime(int lastAuthenticationTime) {
this.lastSessionRefreshTime = lastAuthenticationTime;
}
public Collection<CredentialEntity> getCredentials() {
if (credentials == null) {
credentials = new LinkedList<>();

View File

@ -27,28 +27,29 @@ import java.util.Collections;
import java.util.List;
import org.keycloak.component.ComponentModel;
import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.jpa.entities.UserEntity;
public abstract class AbstractUserResourcePolicyProvider implements ResourcePolicyProvider {
private final ComponentModel policyModel;
private final EntityManager em;
private final KeycloakSession session;
public AbstractUserResourcePolicyProvider(KeycloakSession session, ComponentModel model) {
this.policyModel = model;
this.em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
this.session = session;
}
public abstract Predicate timePredicate(long time, CriteriaBuilder cb, CriteriaQuery<String> query, Root<UserEntity> userRoot);
@Override
public void close() {
// no-op
}
// For each user row, a subquery is executed to check if a corresponding record exists in
// the state table. If no record is found, the condition is met -> user is eligible for initial action
// For each user row, a subquery is executed to check if a corresponding record exists in
// the state table. If no record is found, the condition is met -> user is eligable for initial action
@Override
public List<String> getEligibleResourcesForInitialAction(long time) {
CriteriaBuilder cb = em.getCriteriaBuilder();
@ -78,7 +79,6 @@ public abstract class AbstractUserResourcePolicyProvider implements ResourcePoli
return em.createQuery(query).getResultList();
}
@Override
public List<String> filterEligibleResources(List<String> candidateResourceIds, long time) {
// If there are no candidates, return an empty list
@ -102,11 +102,76 @@ public abstract class AbstractUserResourcePolicyProvider implements ResourcePoli
return em.createQuery(query).getResultList();
}
/**
* Indicates whether the specified resource is in the scope of this policy. For example, a policy associated with a
* broker is applicable only to users with a federated identity associated with the same broker.
*
* @param resourceId the id of the resource being checked.
* @return {@code true} if the resource is in the policy scope; {@code false} otherwise.
*/
protected boolean isResourceInScope(String resourceId) {
UserModel user = this.getSession().users().getUserById(this.getRealm(), resourceId);
if (user != null) {
List<String> brokerAliases = this.getBrokerAliases();
if (!brokerAliases.isEmpty()) {
return session.users().getFederatedIdentitiesStream(this.getRealm(), user)
.map(FederatedIdentityModel::getIdentityProvider)
.anyMatch(brokerAliases::contains);
}
return true;
}
return false;
}
@Override
public boolean supports(ResourceType type) {
return ResourceType.USERS.equals(type);
}
@Override
public boolean scheduleOnEvent(ResourcePolicyEvent event) {
return this.supports(event.getResourceType())
&& this.getSupportedOperationsForScheduling().contains(event.getOperation())
&& this.isResourceInScope(event.getResourceId());
}
@Override
public boolean resetOnEvent(ResourcePolicyEvent event) {
return this.supports(event.getResourceType())
&& this.getSupportedOperationsForResetting().contains(event.getOperation())
&& this.isResourceInScope(event.getResourceId());
}
@Override
public void close() {
// no-op
}
protected List<ResourceOperationType> getSupportedOperationsForScheduling() {
return List.of();
}
protected List<ResourceOperationType> getSupportedOperationsForResetting() {
return List.of();
}
protected EntityManager getEntityManager() {
return em;
}
public ComponentModel getModel() {
protected ComponentModel getModel() {
return policyModel;
}
protected KeycloakSession getSession() {
return session;
}
protected RealmModel getRealm() {
return getSession().getContext().getRealm();
}
protected List<String> getBrokerAliases() {
return getModel().getConfig().getOrDefault("broker-aliases", List.of());
}
}

View File

@ -1,66 +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.util.ArrayList;
import java.util.List;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import jakarta.persistence.criteria.Subquery;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.jpa.entities.FederatedIdentityEntity;
import org.keycloak.models.jpa.entities.UserEntity;
public class FederatedIdentityPolicyProvider extends UserLastSessionRefreshTimeResourcePolicyProvider {
public FederatedIdentityPolicyProvider(KeycloakSession session, ComponentModel model) {
super(session, model);
}
@Override
public Predicate timePredicate(long time, CriteriaBuilder cb, CriteriaQuery<String> query, Root<UserEntity> userRoot) {
Predicate lastSessionRefreshTimePredicate = super.timePredicate(time, cb, query, userRoot);
Predicate federatedIdentityByBrokerPredicate = createFederatedIdentityByBrokerPredicate(cb, query, userRoot);
return cb.and(lastSessionRefreshTimePredicate, federatedIdentityByBrokerPredicate);
}
private Predicate createFederatedIdentityByBrokerPredicate(CriteriaBuilder cb, CriteriaQuery<String> query, Root<UserEntity> userRoot) {
Subquery<Integer> subquery = query.subquery(Integer.class);
Root<?> from = subquery.from(FederatedIdentityEntity.class);
subquery.select(cb.literal(1));
List<Predicate> finalPredicates = new ArrayList<>();
finalPredicates.add(cb.equal(from.get("user").get("id"), userRoot.get("id")));
finalPredicates.add(from.get("identityProvider").in(getBrokerAliases()));
subquery.where(finalPredicates.toArray(Predicate[]::new));
return cb.exists(subquery);
}
private List<String> getBrokerAliases() {
return getModel().getConfig().getOrDefault("broker-aliases", List.of());
}
}

View File

@ -45,13 +45,72 @@ public class JpaResourcePolicyStateProvider implements ResourcePolicyStateProvid
}
@Override
public List<String> findResourceIdsByLastCompletedAction(String policyId, String lastCompletedActionId) {
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, long scheduledTimeOffset, 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() + scheduledTimeOffset);
em.persist(entity);
}
else {
entity.setScheduledActionId(action.getId());
entity.setScheduledActionTimestamp(Time.currentTimeMillis() + scheduledTimeOffset);
}
}
@Override
public List<ScheduledAction> getDueScheduledActions(ResourcePolicy policy) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<ResourcePolicyStateEntity> query = cb.createQuery(ResourcePolicyStateEntity.class);
Root<ResourcePolicyStateEntity> stateRoot = query.from(ResourcePolicyStateEntity.class);
Predicate byPolicy = cb.equal(stateRoot.get("policyId"), policy.getId());
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<ScheduledAction> getScheduledActionsByResource(String resourceId) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<ResourcePolicyStateEntity> query = cb.createQuery(ResourcePolicyStateEntity.class);
Root<ResourcePolicyStateEntity> 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 List<String> findResourceIdsByScheduledAction(String policyId, String scheduledActionId) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<String> query = cb.createQuery(String.class);
Root<ResourcePolicyStateEntity> stateRoot = query.from(ResourcePolicyStateEntity.class);
Predicate policyPredicate = cb.equal(stateRoot.get("policyId"), policyId);
Predicate actionPredicate = cb.equal(stateRoot.get("lastCompletedActionId"), lastCompletedActionId);
Predicate actionPredicate = cb.equal(stateRoot.get("scheduledActionId"), scheduledActionId);
query.select(stateRoot.get("resourceId"));
query.where(cb.and(policyPredicate, actionPredicate));
@ -77,12 +136,12 @@ public class JpaResourcePolicyStateProvider implements ResourcePolicyStateProvid
} else {
if (LOGGER.isTraceEnabled()) {
LOGGER.tracev("Changing record for policyId ({0}), last_compl_actionId ({1}), new_last_compl_actionId ({2}), userId ({3})",
entity.getPolicyId(), entity.getLastCompletedActionId(), newLastCompletedActionId, resourceId);
entity.getPolicyId(), entity.getScheduledActionId(), newLastCompletedActionId, resourceId);
}
}
entity.setLastCompletedActionId(newLastCompletedActionId);
entity.setLastUpdatedTimestamp(Time.currentTimeMillis());
entity.setScheduledActionId(newLastCompletedActionId);
entity.setScheduledActionTimestamp(Time.currentTimeMillis());
}
}
@ -97,7 +156,7 @@ public class JpaResourcePolicyStateProvider implements ResourcePolicyStateProvid
Root<ResourcePolicyStateEntity> stateRoot = delete.from(ResourcePolicyStateEntity.class);
Predicate policyPredicate = cb.equal(stateRoot.get("policyId"), policyId);
Predicate inClausePredicate = stateRoot.get("lastCompletedActionId").in(deletedActionIds);
Predicate inClausePredicate = stateRoot.get("scheduledActionId").in(deletedActionIds);
delete.where(cb.and(policyPredicate, inClausePredicate));
@ -111,20 +170,29 @@ public class JpaResourcePolicyStateProvider implements ResourcePolicyStateProvid
}
@Override
public void removeByUser(UserModel user) {
public void removeByResource(String resourceId) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaDelete<ResourcePolicyStateEntity> delete = cb.createCriteriaDelete(ResourcePolicyStateEntity.class);
Root<ResourcePolicyStateEntity> root = delete.from(ResourcePolicyStateEntity.class);
delete.where(cb.equal(root.get("resourceId"), user.getId()));
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 user {1}", deletedCount, user.getId());
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 removeAll() {
CriteriaBuilder cb = em.getCriteriaBuilder();

View File

@ -65,6 +65,6 @@ public class JpaResourcePolicyStateProviderFactory implements ResourcePolicyStat
private void onUserRemovedEvent(UserRemovedEvent event) {
KeycloakSession session = event.getKeycloakSession();
ResourcePolicyStateProvider provider = session.getProvider(ResourcePolicyStateProvider.class);
provider.removeByUser(event.getUser());
provider.removeByResource(event.getUser().getId());
}
}

View File

@ -47,11 +47,11 @@ public class ResourcePolicyStateEntity {
@Column(name = "POLICY_PROVIDER_ID")
private String policyProviderId;
@Column(name = "LAST_COMPLETED_ACTION_ID")
private String lastCompletedActionId;
@Column(name = "SCHEDULED_ACTION_ID")
private String scheduledActionId;
@Column(name = "LAST_UPDATED_TIMESTAMP")// might be useful?? - audit?
private long lastUpdatedTimestamp;
@Column(name = "SCHEDULED_ACTION_TIMESTAMP")
private long scheduledActionTimestamp;
public String getResourceId() {
return resourceId;
@ -85,20 +85,20 @@ public class ResourcePolicyStateEntity {
this.resourceType = resourceType;
}
public String getLastCompletedActionId() {
return lastCompletedActionId;
public String getScheduledActionId() {
return scheduledActionId;
}
public void setLastCompletedActionId(String lastCompletedActionId) {
this.lastCompletedActionId = lastCompletedActionId;
public void setScheduledActionId(String scheduledActionId) {
this.scheduledActionId = scheduledActionId;
}
public long getLastUpdatedTimestamp() {
return lastUpdatedTimestamp;
public long getScheduledActionTimestamp() {
return scheduledActionTimestamp;
}
public void setLastUpdatedTimestamp(long lastUpdatedTimestamp) {
this.lastUpdatedTimestamp = lastUpdatedTimestamp;
public void setScheduledActionTimestamp(long scheduledActionTimestamp) {
this.scheduledActionTimestamp = scheduledActionTimestamp;
}
public static class PrimaryKey implements Serializable {

View File

@ -27,9 +27,13 @@ import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.jpa.entities.UserEntity;
public class UserCreationDateResourcePolicyProvider extends AbstractUserResourcePolicyProvider {
import java.util.List;
public UserCreationDateResourcePolicyProvider(KeycloakSession session, ComponentModel model) {
import static org.keycloak.models.policy.ResourceOperationType.CREATE;
public class UserCreationTimeResourcePolicyProvider extends AbstractUserResourcePolicyProvider {
public UserCreationTimeResourcePolicyProvider(KeycloakSession session, ComponentModel model) {
super(session, model);
}
@ -39,4 +43,9 @@ public class UserCreationDateResourcePolicyProvider extends AbstractUserResource
Expression<Long> timeMoment = cb.sum(userRoot.get("createdTimestamp"), cb.literal(time));
return cb.lessThan(timeMoment, cb.literal(currentTimeMillis));
}
@Override
protected List<ResourceOperationType> getSupportedOperationsForScheduling() {
return List.of(CREATE);
}
}

View File

@ -25,9 +25,9 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;
public class UserCreationDateResourcePolicyProviderFactory implements ResourcePolicyProviderFactory<UserCreationDateResourcePolicyProvider> {
public class UserCreationTimeResourcePolicyProviderFactory implements ResourcePolicyProviderFactory<UserCreationTimeResourcePolicyProvider> {
public static final String ID = "user-creation-date-resource-policy";
public static final String ID = "user-creation-time-resource-policy";
@Override
public ResourceType getType() {
@ -35,8 +35,8 @@ public class UserCreationDateResourcePolicyProviderFactory implements ResourcePo
}
@Override
public UserCreationDateResourcePolicyProvider create(KeycloakSession session, ComponentModel model) {
return new UserCreationDateResourcePolicyProvider(session, model);
public UserCreationTimeResourcePolicyProvider create(KeycloakSession session, ComponentModel model) {
return new UserCreationTimeResourcePolicyProvider(session, model);
}
@Override

View File

@ -18,6 +18,7 @@
package org.keycloak.models.policy;
import java.time.Duration;
import java.util.List;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
@ -30,9 +31,12 @@ import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.jpa.entities.UserEntity;
public class UserLastSessionRefreshTimeResourcePolicyProvider extends AbstractUserResourcePolicyProvider {
import static org.keycloak.models.policy.ResourceOperationType.CREATE;
import static org.keycloak.models.policy.ResourceOperationType.LOGIN;
public UserLastSessionRefreshTimeResourcePolicyProvider(KeycloakSession session, ComponentModel model) {
public class UserSessionRefreshTimeResourcePolicyProvider extends AbstractUserResourcePolicyProvider {
public UserSessionRefreshTimeResourcePolicyProvider(KeycloakSession session, ComponentModel model) {
super(session, model);
}
@ -43,4 +47,14 @@ public class UserLastSessionRefreshTimeResourcePolicyProvider extends AbstractUs
Expression<Long> lastSessionRefreshTimeExpiration = cb.sum(lastSessionRefreshTime, cb.literal(Duration.ofMillis(time).toSeconds()));
return cb.and(cb.isNotNull(lastSessionRefreshTime), cb.lessThan(lastSessionRefreshTimeExpiration, cb.literal(currentTimeSeconds)));
}
@Override
protected List<ResourceOperationType> getSupportedOperationsForScheduling() {
return List.of(CREATE, LOGIN);
}
@Override
protected List<ResourceOperationType> getSupportedOperationsForResetting() {
return List.of(LOGIN);
}
}

View File

@ -25,9 +25,9 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;
public class UserLastSessionRefreshTimeResourcePolicyProviderFactory implements ResourcePolicyProviderFactory<UserLastSessionRefreshTimeResourcePolicyProvider> {
public class UserSessionRefreshTimeResourcePolicyProviderFactory implements ResourcePolicyProviderFactory<UserSessionRefreshTimeResourcePolicyProvider> {
public static final String ID = "user-last-auth-time-resource-policy";
public static final String ID = "user-refresh-time-resource-policy";
@Override
public ResourceType getType() {
@ -35,8 +35,8 @@ public class UserLastSessionRefreshTimeResourcePolicyProviderFactory implements
}
@Override
public UserLastSessionRefreshTimeResourcePolicyProvider create(KeycloakSession session, ComponentModel model) {
return new UserLastSessionRefreshTimeResourcePolicyProvider(session, model);
public UserSessionRefreshTimeResourcePolicyProvider create(KeycloakSession session, ComponentModel model) {
return new UserSessionRefreshTimeResourcePolicyProvider(session, model);
}
@Override

View File

@ -38,8 +38,8 @@
</column>
<column name="POLICY_PROVIDER_ID" type="VARCHAR(255)" />
<column name="RESOURCE_TYPE" type="VARCHAR(255)" />
<column name="LAST_COMPLETED_ACTION_ID" type="VARCHAR(255)" />
<column name="LAST_UPDATED_TIMESTAMP" type="BIGINT" />
<column name="SCHEDULED_ACTION_ID" type="VARCHAR(255)" />
<column name="SCHEDULED_ACTION_TIMESTAMP" type="BIGINT" />
</createTable>
<addPrimaryKey
@ -50,7 +50,7 @@
<createIndex indexName="IDX_RES_POLICY_STATE_ACTION"
tableName="RESOURCE_POLICY_STATE">
<column name="POLICY_ID" />
<column name="LAST_COMPLETED_ACTION_ID" />
<column name="SCHEDULED_ACTION_ID" />
</createIndex>
<createIndex indexName="IDX_RES_POLICY_STATE_PROVIDER"
@ -58,24 +58,6 @@
<column name="RESOURCE_ID" />
<column name="POLICY_PROVIDER_ID" />
</createIndex>
<!-- constraint to ensure that each resource has only one policy of a specific type -->
<addUniqueConstraint
constraintName="UC_RES_POLICY_PROVIDER_ID"
tableName="RESOURCE_POLICY_STATE"
columnNames="RESOURCE_ID, POLICY_PROVIDER_ID"
/>
</changeSet>
<changeSet id="RLM-add-last-session-refresh-time-to-user-entity" author="keycloak">
<addColumn tableName="USER_ENTITY">
<column name="LAST_SESSION_REFRESH_TIME" type="BIGINT" />
</addColumn>
<createIndex indexName="IDX_USER_LAST_SESSION_REFRESH_TIME"
tableName="USER_ENTITY">
<column name="LAST_SESSION_REFRESH_TIME" />
</createIndex>
</changeSet>
</databaseChangeLog>

View File

@ -15,6 +15,5 @@
# limitations under the License.
#
org.keycloak.models.policy.UserCreationDateResourcePolicyProviderFactory
org.keycloak.models.policy.UserLastSessionRefreshTimeResourcePolicyProviderFactory
org.keycloak.models.policy.FederatedIdentityPolicyProviderFactory
org.keycloak.models.policy.UserCreationTimeResourcePolicyProviderFactory
org.keycloak.models.policy.UserSessionRefreshTimeResourcePolicyProviderFactory

View File

@ -17,7 +17,6 @@
package org.keycloak.models.policy;
import org.keycloak.models.UserModel;
import org.keycloak.provider.Provider;
import java.util.List;
import java.util.Set;
@ -28,9 +27,9 @@ import java.util.Set;
public interface ResourcePolicyStateProvider extends Provider {
/**
* Finds resource IDs that have completed a specific action within a policy.
* Finds resource IDs scheduled to run the specified action within a policy.
*/
List<String> findResourceIdsByLastCompletedAction(String policyId, String lastCompletedActionId);
List<String> findResourceIdsByScheduledAction(String policyId, String scheduledActionId);
/**
* Updates the state for a list of resources that have just completed a new action.
@ -44,14 +43,35 @@ public interface ResourcePolicyStateProvider extends Provider {
void removeByCompletedActions(String policyId, Set<String> deletedActionIds);
/**
* Deletes the state records associated with the given {@code user}.
* Deletes the state records associated with the given {@code resourceId}.
*
* @param user the user
* @param resourceId the id of the resource.
*/
void removeByUser(UserModel user);
void removeByResource(String resourceId);
/**
* Removes the record identified by the specified {@code policyId} and {@code resourceId}.
* @param policyId the id of the policy.
* @param resourceId the id of the resource.
*/
void remove(String policyId, String resourceId);
/**
* Deletes all state records associated with the current realm bound to the session.
*/
void removeAll();
default void scheduleAction(ResourcePolicy policy, ResourceAction action, String resourceId) {
this.scheduleAction(policy, action, action.getAfter(), resourceId);
}
void scheduleAction(ResourcePolicy policy, ResourceAction action, long scheduledTimeOffset, String resourceId);
ScheduledAction getScheduledAction(String policyId, String resourceId);
List<ScheduledAction> getScheduledActionsByResource(String resourceId);
List<ScheduledAction> getDueScheduledActions(ResourcePolicy policy);
record ScheduledAction (String policyId, String actionId, String resourceId) {};
}

View File

@ -400,11 +400,6 @@ public abstract class AbstractUserAdapter extends UserModelDefaultMethods {
}
@Override
public void setLastSessionRefreshTime(int lastSessionRefreshTime) {
// no-op
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@ -404,11 +404,6 @@ public abstract class AbstractUserAdapterFederatedStorage extends UserModelDefau
return new UserCredentialManager(session, realm, this);
}
@Override
public void setLastSessionRefreshTime(int lastSessionRefreshTime) {
// no-op
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@ -65,6 +65,17 @@ public class AdminEvent {
this.details = toCopy.getDetails() == null ? null : new HashMap<>(toCopy.getDetails());
}
public String getResourceId() {
if (this.resourcePath != null) {
int slashIndex = this.resourcePath.lastIndexOf("/");
if (slashIndex < this.resourcePath.length() - 1) {
// return the id that is found after the last slash
return this.resourcePath.substring(slashIndex + 1);
}
}
return null;
}
/**
* Returns the UUID of the event.
*

View File

@ -0,0 +1,8 @@
package org.keycloak.models.policy;
public enum ResourceOperationType {
CREATE,
LOGIN
}

View File

@ -0,0 +1,26 @@
package org.keycloak.models.policy;
public class ResourcePolicyEvent {
private final ResourceType type;
private final ResourceOperationType operation;
private final String resourceId;
public ResourcePolicyEvent(ResourceType type, ResourceOperationType operation, String resourceId) {
this.type = type;
this.operation = operation;
this.resourceId = resourceId;
}
public ResourceType getResourceType() {
return type;
}
public ResourceOperationType getOperation() {
return operation;
}
public String getResourceId() {
return resourceId;
}
}

View File

@ -20,14 +20,6 @@ package org.keycloak.models.policy;
import java.util.List;
import org.keycloak.provider.Provider;
/**
* TODO: Maybe we want to split the provider into two???
* * Time based
* ** UserCreationDatePolicyProvider, LastAuthenticationTimePolicyProvider ...
* * Origin based
* ** IdpResourceFilterProvider, LdapResourceFilterProvider, AllResourceFilterProvider
*
*/
public interface ResourcePolicyProvider extends Provider {
/**
@ -42,4 +34,41 @@ public interface ResourcePolicyProvider extends Provider {
* This method checks a list of candidates and returns only those that are eligible based on time.
*/
List<String> filterEligibleResources(List<String> candidateResourceIds, long time);
/**
* Checks if the provider supports resources of the specified type.
*
* @param type the resource type.
* @return {@code true} if the provider supports the specified type; {@code false} otherwise.
*/
boolean supports(ResourceType type);
/**
* Indicates whether the policy supports being assigned to a resource based on the event or not. If {@code true}, the
* policy's first action will be scheduled for the resource.
*
* At the very least, implementations should validate the event's resource type and operation to ensure the policy will
* only be assigned 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
* (CREATE, LOGIN, etc.), the resource type, and the resource id.
* @return {@code true} if the policy allows for the setup of the first action based on the received event; {@code false}
* otherwise.
*/
boolean scheduleOnEvent(ResourcePolicyEvent 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
* 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
* 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
* (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.
*/
boolean resetOnEvent(ResourcePolicyEvent event);
}

View File

@ -17,6 +17,68 @@
package org.keycloak.models.policy;
import org.keycloak.events.Event;
import org.keycloak.events.EventType;
import org.keycloak.events.admin.AdminEvent;
import org.keycloak.events.admin.OperationType;
import java.util.List;
import java.util.Objects;
public enum ResourceType {
USERS
USERS(org.keycloak.events.admin.ResourceType.USER, List.of(OperationType.CREATE), List.of(EventType.LOGIN, EventType.REGISTER));
private final org.keycloak.events.admin.ResourceType supportedAdminResourceType;
private final List<OperationType> supportedAdminOperationTypes;
private final List<EventType> supportedEventTypes;
ResourceType(org.keycloak.events.admin.ResourceType supportedAdminResourceType,
List<OperationType> supportedAdminOperationTypes,
List<EventType> supportedEventTypes) {
this.supportedAdminResourceType = supportedAdminResourceType;
this.supportedAdminOperationTypes = supportedAdminOperationTypes;
this.supportedEventTypes = supportedEventTypes;
}
public ResourcePolicyEvent 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());
}
}
return null;
}
public ResourcePolicyEvent 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());
}
}
return null;
}
private ResourceOperationType toOperationType(OperationType operation) {
return switch (operation) {
case CREATE -> ResourceOperationType.CREATE;
default -> null;
};
}
private ResourceOperationType toOperationType(EventType type) {
return switch (type) {
case REGISTER -> ResourceOperationType.CREATE;
case LOGIN -> ResourceOperationType.LOGIN;
default -> null;
};
}
}

View File

@ -292,11 +292,6 @@ public abstract class AbstractInMemoryUserAdapter extends UserModelDefaultMethod
}
@Override
public void setLastSessionRefreshTime(int lastSessionRefreshTime) {
// no-op
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@ -232,8 +232,6 @@ public interface UserModel extends RoleMapperModel {
*/
SubjectCredentialManager credentialManager();
void setLastSessionRefreshTime(int lastSessionRefreshTime);
enum RequiredAction {
VERIFY_EMAIL,
UPDATE_PROFILE,

View File

@ -251,11 +251,6 @@ public class UserModelDelegate implements UserModel {
return delegate.isMemberOf(group);
}
@Override
public void setLastSessionRefreshTime(int lastSessionRefreshTime) {
delegate.setLastSessionRefreshTime(lastSessionRefreshTime);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@ -0,0 +1,49 @@
/*
* 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 org.keycloak.events.Event;
import org.keycloak.events.EventListenerProvider;
import org.keycloak.events.admin.AdminEvent;
import org.keycloak.models.KeycloakSession;
public class ResourcePolicyEventListener implements EventListenerProvider {
private final KeycloakSession session;
public ResourcePolicyEventListener(KeycloakSession session) {
this.session = session;
}
@Override
public void onEvent(Event event) {
ResourcePolicyEvent policyEvent = ResourceType.USERS.toEvent(event);
trySchedule(policyEvent);
}
@Override
public void onEvent(AdminEvent event, boolean includeRepresentation) {
ResourcePolicyEvent policyEvent = ResourceType.USERS.toEvent(event);
trySchedule(policyEvent);
}
private void trySchedule(ResourcePolicyEvent event) {
if (event != null) {
ResourcePolicyManager manager = new ResourcePolicyManager(session);
manager.processEvent(event);
}
}
@Override
public void close() {
}
}

View File

@ -17,54 +17,41 @@
package org.keycloak.models.policy;
import java.util.List;
import org.keycloak.Config;
import org.keycloak.component.ComponentModel;
import org.keycloak.Config.Scope;
import org.keycloak.events.EventListenerProvider;
import org.keycloak.events.EventListenerProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;
public class FederatedIdentityPolicyProviderFactory implements ResourcePolicyProviderFactory<FederatedIdentityPolicyProvider> {
public static final String ID = "federated-identity-policy";
public class ResourcePolicyEventListenerFactory implements EventListenerProviderFactory {
@Override
public ResourceType getType() {
return ResourceType.USERS;
public EventListenerProvider create(KeycloakSession session) {
return new ResourcePolicyEventListener(session);
}
@Override
public FederatedIdentityPolicyProvider create(KeycloakSession session, ComponentModel model) {
return new FederatedIdentityPolicyProvider(session, model);
public boolean isGlobal() {
return true;
}
@Override
public void init(Config.Scope config) {
// no-op
public void init(Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
// no-op
}
@Override
public String getId() {
return ID;
}
@Override
public String getHelpText() {
return "";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return List.of();
return "resource-policy-event-listener";
}
}

View File

@ -35,6 +35,7 @@ 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.provider.ProviderFactory;
public class ResourcePolicyManager {
@ -106,13 +107,12 @@ public class ResourcePolicyManager {
// find which action IDs were deleted
oldActionIds.removeAll(newActionIds); // The remaining IDs are the deleted ones
Set<String> deletedActionIds = oldActionIds;
ResourcePolicyStateProvider stateProvider = getResourcePolicyStateProvider();
// delete orphaned state records - this means that we actually reset the flow for users which completed the action which is being removed
// it seems like the best way to handle this
if (!deletedActionIds.isEmpty()) {
stateProvider.removeByCompletedActions(policy.getId(), deletedActionIds);
if (!oldActionIds.isEmpty()) {
stateProvider.removeByCompletedActions(policy.getId(), oldActionIds);
}
RealmModel realm = getRealm();
@ -183,7 +183,7 @@ public class ResourcePolicyManager {
Map<String, List<String>> candidatesForAction = new HashMap<>();
for (int i = 1; i < actions.size(); i++) {
ResourceAction previousAction = actions.get(i - 1);
List<String> candidateIds = stateProvider.findResourceIdsByLastCompletedAction(policy.getId(), previousAction.getId());
List<String> candidateIds = stateProvider.findResourceIdsByScheduledAction(policy.getId(), previousAction.getId());
candidatesForAction.put(actions.get(i).getId(), candidateIds);
}
@ -295,4 +295,60 @@ public class ResourcePolicyManager {
realm.removeComponent(policy);
});
}
public void processEvent(ResourcePolicyEvent event) {
ResourcePolicyStateProvider state = getResourcePolicyStateProvider();
List<String> currentlyAssignedPolicies = state.getScheduledActionsByResource(event.getResourceId())
.stream().map(ScheduledAction::policyId).toList();
List<ResourcePolicy> policies = this.getPolicies();
// iterate through the policies, and for those not yet assigned to the user check if they can be assigned
policies.stream()
.filter(policy -> !getActions(policy).isEmpty())
.forEach(policy -> {
ResourcePolicyProvider provider = getPolicyProvider(policy);
if (!currentlyAssignedPolicies.contains(policy.getId())) {
// if policy is not assigned, check if the provider allows assigning based on the event
if (provider.scheduleOnEvent(event)) {
state.scheduleAction(policy, getFirstAction(policy), event.getResourceId());
}
} else {
if (provider.resetOnEvent(event)) {
state.scheduleAction(policy, getFirstAction(policy), event.getResourceId());
}
// TODO add a removeOnEvent to allow policies to detach from resources on specific events (e.g. unlinking an identity)
}
});
}
private ResourceAction getFirstAction(ResourcePolicy policy) {
return getActions(policy).get(0);
}
public void runScheduledTasks() {
for (ResourcePolicy policy : getPolicies()) {
ResourcePolicyStateProvider state = getResourcePolicyStateProvider();
for (ScheduledAction scheduled : state.getDueScheduledActions(policy)) {
List<ResourceAction> actions = getActions(policy);
for (int i = 0; i < actions.size(); i++) {
ResourceAction currentAction = actions.get(i);
if (currentAction.getId().equals(scheduled.actionId())) {
runAction(getActionProvider(currentAction), List.of(scheduled.resourceId()));
if (actions.size() > i + 1) {
// schedule the next action using the time offset difference between the actions.
ResourceAction nextAction = actions.get(i + 1);
state.scheduleAction(policy, nextAction,nextAction.getAfter() - currentAction.getAfter(), scheduled.resourceId());
} else {
state.remove(policy.getId(), scheduled.resourceId());
}
}
}
}
}
}
}

View File

@ -16,4 +16,5 @@
#
org.keycloak.events.email.EmailEventListenerProviderFactory
org.keycloak.events.log.JBossLoggingEventListenerProviderFactory
org.keycloak.events.log.JBossLoggingEventListenerProviderFactory
org.keycloak.models.policy.ResourcePolicyEventListenerFactory

View File

@ -1,5 +1,5 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* 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");
@ -34,9 +34,9 @@ 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.FederatedIdentityPolicyProviderFactory;
import org.keycloak.models.policy.ResourcePolicyManager;
import org.keycloak.models.policy.UserActionBuilder;
import org.keycloak.models.policy.UserSessionRefreshTimeResourcePolicyProviderFactory;
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
@ -65,10 +65,9 @@ import org.keycloak.testframework.ui.page.LoginPage;
import org.openqa.selenium.WebDriver;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@KeycloakIntegrationTest(config = RLMServerConfig.class)
public class TransientUserTest {
public class BrokeredUserSessionRefreshTimePolicyTest {
private static final String REALM_NAME = "consumer";
@ -81,8 +80,11 @@ public class TransientUserTest {
@InjectRealm(ref = "provider", lifecycle = LifeCycle.METHOD)
ManagedRealm providerRealm;
@InjectUser(ref = "provider", realmRef = "provider", config = ProviderRealmUserConf.class)
ManagedUser userFromProviderRealm;
@InjectUser(ref = "alice", realmRef = "provider", config = ProviderRealmUserConf.class)
ManagedUser aliceFromProviderRealm;
@InjectUser(ref = "bob", realmRef = "consumer", config = ConsumerRealmUserConf.class)
ManagedUser bobFromConsumerRealm;
@InjectOAuthClient(ref = "consumer", realmRef = "consumer")
OAuthClient consumerRealmOAuth;
@ -110,12 +112,23 @@ public class TransientUserTest {
@Test
public void tesRunActionOnFederatedUser() {
runOnServer.run((session -> {
configureSessionContext(session);
PolicyBuilder.create()
.of(UserSessionRefreshTimeResourcePolicyProviderFactory.ID)
.withConfig("broker-aliases", IDP_OIDC_ALIAS)
.withActions(
UserActionBuilder.builder(DeleteUserActionProviderFactory.ID)
.after(Duration.ofDays(1))
.build()
).build(session);
}));
consumerRealmOAuth.openLoginForm();
loginPage.clickSocial(IDP_OIDC_ALIAS);
Assertions.assertTrue(driver.getCurrentUrl().contains("/realms/" + providerRealm.getName() + "/"), "Driver should be on the provider realm page right now");
String username = userFromProviderRealm.getUsername();
loginPage.fillLogin(username, userFromProviderRealm.getPassword());
loginPage.fillLogin(aliceFromProviderRealm.getUsername(), aliceFromProviderRealm.getPassword());
loginPage.submit();
consentPage.waitForPage();
consentPage.assertCurrent();
@ -123,38 +136,57 @@ public class TransientUserTest {
assertTrue(driver.getPageSource().contains("Happy days"), "Test user should be successfully logged in.");
UsersResource users = consumerRealm.admin().users();
String username = aliceFromProviderRealm.getUsername();
UserRepresentation federatedUser = users.search(username).get(0);
List<FederatedIdentityRepresentation> federatedIdentities = users.get(federatedUser.getId()).getFederatedIdentity();
assertFalse(federatedIdentities.isEmpty());
runOnServer.run((session -> {
RealmModel realm = configureSessionContext(session);
ResourcePolicyManager manager = PolicyBuilder.create()
.of(FederatedIdentityPolicyProviderFactory.ID)
.withConfig("source", "broker")
.withConfig("source-id", List.of("kc-oidc-alias"))
.withConfig("broker-aliases", IDP_OIDC_ALIAS)
.withActions(
UserActionBuilder.builder(DeleteUserActionProviderFactory.ID)
.after(Duration.ofDays(1))
.build()
).build(session);
ResourcePolicyManager manager = new ResourcePolicyManager(session);
manager.runPolicies();
manager.runScheduledTasks();
UserModel user = session.users().getUserByUsername(realm, username);
assertNotNull(user);
assertTrue(user.isEnabled());
try {
manager = new ResourcePolicyManager(session);
Time.setOffset(Math.toIntExact(Duration.ofDays(2).toSeconds()));
manager.runPolicies();
manager.runScheduledTasks();
user = session.users().getUserByUsername(realm, username);
assertNull(user);
} finally {
Time.setOffset(0);
}
}));
// 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.
consumerRealmOAuth.openLoginForm();
loginPage.fillLogin(bobFromConsumerRealm.getUsername(), bobFromConsumerRealm.getPassword());
loginPage.submit();
assertTrue(driver.getPageSource().contains("Happy days"), "Test user should be successfully logged in.");
runOnServer.run((session -> {
RealmModel realm = configureSessionContext(session);
ResourcePolicyManager manager = new ResourcePolicyManager(session);
// run the scheduled tasks - bob should not be affected.
manager.runScheduledTasks();
UserModel user = session.users().getUserByUsername(realm, "bob");
assertNotNull(user);
assertTrue(user.isEnabled());
try {
// run with a time offset - bob should still not be affected.
Time.setOffset(Math.toIntExact(Duration.ofDays(2).toSeconds()));
manager.runScheduledTasks();
user = session.users().getUserByUsername(realm, "bob");
assertNotNull(user);
} finally {
Time.setOffset(0);
}
}));
}
private static IdentityProviderRepresentation setUpIdentityProvider() {
@ -190,12 +222,24 @@ public class TransientUserTest {
@Override
public UserConfigBuilder configure(UserConfigBuilder builder) {
builder.username("provider");
builder.username("alice");
builder.password("password");
builder.email("provider@local");
builder.email("alice@wonderland.org");
builder.emailVerified(true);
builder.name("Provider", "User");
builder.name("Alice", "Wonderland");
return builder;
}
}
private static class ConsumerRealmUserConf implements UserConfig {
@Override
public UserConfigBuilder configure(UserConfigBuilder builder) {
builder.username("bob");
builder.password("password");
builder.email("bob@wonderland.org");
builder.emailVerified(true);
builder.name("Bob", "Madhatter");
return builder;
}
}
@ -220,7 +264,6 @@ public class TransientUserTest {
@Override
public RealmConfigBuilder configure(RealmConfigBuilder builder) {
builder.identityProvider(setUpIdentityProvider());
return builder;
}
}

View File

@ -37,7 +37,6 @@ import org.keycloak.connections.jpa.JpaConnectionProvider;
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.ResourceAction;
@ -46,8 +45,7 @@ import org.keycloak.models.policy.ResourcePolicyManager;
import org.keycloak.models.policy.ResourcePolicyStateEntity;
import org.keycloak.models.policy.ResourcePolicyStateProvider;
import org.keycloak.models.policy.UserActionBuilder;
import org.keycloak.models.policy.UserCreationDateResourcePolicyProviderFactory;
import org.keycloak.models.policy.UserLastSessionRefreshTimeResourcePolicyProviderFactory;
import org.keycloak.models.policy.UserCreationTimeResourcePolicyProviderFactory;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.InjectUser;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
@ -79,7 +77,7 @@ public class ResourcePolicyManagementTest {
RealmModel realm = configureSessionContext(session);
ResourcePolicyManager manager = new ResourcePolicyManager(session);
ResourcePolicy created = manager.addPolicy(new ResourcePolicy(UserCreationDateResourcePolicyProviderFactory.ID));
ResourcePolicy created = manager.addPolicy(new ResourcePolicy(UserCreationTimeResourcePolicyProviderFactory.ID));
assertNotNull(created.getId());
List<ResourcePolicy> policies = manager.getPolicies();
@ -91,7 +89,7 @@ public class ResourcePolicyManagementTest {
assertNotNull(policy.getId());
assertEquals(created.getId(), policy.getId());
assertNotNull(realm.getComponent(policy.getId()));
assertEquals(UserCreationDateResourcePolicyProviderFactory.ID, policy.getProviderId());
assertEquals(UserCreationTimeResourcePolicyProviderFactory.ID, policy.getProviderId());
});
}
@ -100,7 +98,7 @@ public class ResourcePolicyManagementTest {
runOnServer.run(session -> {
RealmModel realm = configureSessionContext(session);
ResourcePolicyManager manager = new ResourcePolicyManager(session);
ResourcePolicy policy = manager.addPolicy(new ResourcePolicy(UserCreationDateResourcePolicyProviderFactory.ID));
ResourcePolicy policy = manager.addPolicy(new ResourcePolicy(UserCreationTimeResourcePolicyProviderFactory.ID));
int expectedActionsSize = 5;
@ -141,7 +139,7 @@ public class ResourcePolicyManagementTest {
UserModel user = session.users().addUser(realm, "test");
// Create a policy with two actions
ResourcePolicy policy = manager.addPolicy(new ResourcePolicy(UserCreationDateResourcePolicyProviderFactory.ID));
ResourcePolicy policy = manager.addPolicy(new ResourcePolicy(UserCreationTimeResourcePolicyProviderFactory.ID));
ResourceAction notify = UserActionBuilder.builder(NotifyUserActionProviderFactory.ID).after(Duration.ofDays(5)).build();
ResourceAction disable = UserActionBuilder.builder(DisableUserActionProviderFactory.ID).after(Duration.ofDays(10)).build();
manager.updateActions(policy, List.of(notify, disable));
@ -180,7 +178,7 @@ public class ResourcePolicyManagementTest {
user.setEnabled(true);
// Create a policy with notify (5 days) and disable (10 days) actions
ResourcePolicy policy = manager.addPolicy(new ResourcePolicy(UserCreationDateResourcePolicyProviderFactory.ID));
ResourcePolicy policy = manager.addPolicy(new ResourcePolicy(UserCreationTimeResourcePolicyProviderFactory.ID));
ResourceAction notifyAction = UserActionBuilder.builder(NotifyUserActionProviderFactory.ID).after(Duration.ofDays(5)).build();
ResourceAction disableAction = UserActionBuilder.builder(DisableUserActionProviderFactory.ID).after(Duration.ofDays(10)).build();
manager.updateActions(policy, List.of(notifyAction, disableAction));
@ -203,7 +201,7 @@ public class ResourcePolicyManagementTest {
ResourcePolicyStateEntity state = em.find(ResourcePolicyStateEntity.class, pk);
assertNotNull(state, "A state record should have been created for the user.");
assertEquals(createdNotifyAction.getId(), state.getLastCompletedActionId(), "The user's state should be at the first action.");
assertEquals(createdNotifyAction.getId(), state.getScheduledActionId(), "The user's state should be at the first action.");
} finally {
Time.setOffset(0);
}
@ -215,7 +213,7 @@ public class ResourcePolicyManagementTest {
runOnServer.run(session -> {
configureSessionContext(session);
ResourcePolicyManager manager = new ResourcePolicyManager(session);
ResourcePolicy policy = manager.addPolicy(UserCreationDateResourcePolicyProviderFactory.ID);
ResourcePolicy policy = manager.addPolicy(UserCreationTimeResourcePolicyProviderFactory.ID);
ResourceAction action1 = UserActionBuilder.builder(DisableUserActionProviderFactory.ID)
.after(Duration.ofDays(10))
@ -232,117 +230,6 @@ public class ResourcePolicyManagementTest {
});
}
@Test
public void testRunSinglePolicy() {
runOnServer.run(session -> {
RealmModel realm = configureSessionContext(session);
ResourcePolicyManager manager = PolicyBuilder.create()
.of(UserLastSessionRefreshTimeResourcePolicyProviderFactory.ID)
.withActions(
UserActionBuilder.builder(NotifyUserActionProviderFactory.ID)
.after(Duration.ofDays(5))
.build(),
UserActionBuilder.builder(DisableUserActionProviderFactory.ID)
.after(Duration.ofDays(10))
.build()
).build(session);
UserProvider users = session.users();
UserModel user = users.getUserByUsername(realm, "alice");
assertTrue(user.isEnabled());
assertNull(user.getAttributes().get("message"));
user.setLastSessionRefreshTime(Time.currentTime());
try {
Time.setOffset(Math.toIntExact(Duration.ofDays(7).toSeconds()));
manager.runPolicies();
user = users.getUserByUsername(realm, "alice");
assertTrue(user.isEnabled());
assertNotNull(user.getAttributes().get("message"));
Time.setOffset(Math.toIntExact(Duration.ofDays(12).toSeconds()));
manager.runPolicies();
user = users.getUserByUsername(realm, "alice");
assertFalse(user.isEnabled());
assertNotNull(user.getAttributes().get("message"));
} finally {
Time.setOffset(0);
}
});
}
@Test
public void testMultiplePolicies() {
runOnServer.run(session -> {
RealmModel realm = configureSessionContext(session);
ResourcePolicyManager manager = PolicyBuilder.create()
.of(UserLastSessionRefreshTimeResourcePolicyProviderFactory.ID)
.withActions(
UserActionBuilder.builder(NotifyUserActionProviderFactory.ID)
.after(Duration.ofDays(5))
.withConfig("message_key", "notifier1")
.build()
).of(UserLastSessionRefreshTimeResourcePolicyProviderFactory.ID)
.withActions(
UserActionBuilder.builder(NotifyUserActionProviderFactory.ID)
.after(Duration.ofDays(10))
.withConfig("message_key", "notifier2")
.build())
.build(session);
UserProvider users = session.users();
UserModel user = users.getUserByUsername(realm, "alice");
assertTrue(user.isEnabled());
assertNull(user.getFirstAttribute("notifier1"));
assertNull(user.getFirstAttribute("notifier2"));
user.setLastSessionRefreshTime(Time.currentTime());
try {
Time.setOffset(Math.toIntExact(Duration.ofDays(7).toSeconds()));
manager.runPolicies();
user = users.getUserByUsername(realm, "alice");
assertTrue(user.isEnabled());
assertNotNull(user.getFirstAttribute("notifier1"));
assertNull(user.getFirstAttribute("notifier2"));
user.removeAttribute("notifier1");
} finally {
Time.setOffset(0);
}
try {
Time.setOffset(Math.toIntExact(Duration.ofDays(11).toSeconds()));
manager.runPolicies();
user = users.getUserByUsername(realm, "alice");
assertTrue(user.isEnabled());
assertNotNull(user.getFirstAttribute("notifier2"));
assertNull(user.getFirstAttribute("notifier1"));
user.removeAttribute("notifier2");
} finally {
Time.setOffset(0);
}
try {
manager.runPolicies();
assertNull(user.getFirstAttribute("notifier1"));
assertNull(user.getFirstAttribute("notifier2"));
} finally {
Time.setOffset(0);
}
// try {
//TODO: test re-run policies based on the last time the action was executed?
// Time.setOffset(Math.toIntExact(Duration.ofDays(40).toSeconds()));
// manager.runPolicies();
// assertNotNull(user.getFirstAttribute("notifier1"));
// assertNotNull(user.getFirstAttribute("notifier2"));
// } finally {
// Time.setOffset(0);
// }
});
}
private static RealmModel configureSessionContext(KeycloakSession session) {
RealmModel realm = session.realms().getRealmByName(REALM_NAME);
session.getContext().setRealm(realm);

View File

@ -0,0 +1,146 @@
package org.keycloak.tests.admin.model.policy;
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.DisableUserActionProviderFactory;
import org.keycloak.models.policy.NotifyUserActionProviderFactory;
import org.keycloak.models.policy.ResourcePolicyManager;
import org.keycloak.models.policy.UserActionBuilder;
import org.keycloak.models.policy.UserCreationTimeResourcePolicyProviderFactory;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.oauth.OAuthClient;
import org.keycloak.testframework.oauth.annotations.InjectOAuthClient;
import org.keycloak.testframework.realm.ManagedRealm;
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.testframework.ui.annotations.InjectPage;
import org.keycloak.testframework.ui.annotations.InjectWebDriver;
import org.keycloak.testframework.ui.page.LoginPage;
import org.openqa.selenium.WebDriver;
import java.time.Duration;
import java.util.List;
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;
@KeycloakIntegrationTest(config = RLMServerConfig.class)
public class UserCreationTimePolicyTest {
private static final String REALM_NAME = "default";
@InjectRunOnServer(permittedPackages = "org.keycloak.tests")
RunOnServerClient runOnServer;
@InjectRealm
ManagedRealm managedRealm;
@InjectWebDriver
WebDriver driver;
@InjectPage
LoginPage loginPage;
@InjectOAuthClient
OAuthClient oauth;
@Test
public void testDisableUserBasedOnCreationDate() {
runOnServer.run((RunOnServer) session -> {
configureSessionContext(session);
PolicyBuilder.create()
.of(UserCreationTimeResourcePolicyProviderFactory.ID)
.withActions(
UserActionBuilder.builder(NotifyUserActionProviderFactory.ID)
.after(Duration.ofDays(5))
.build(),
UserActionBuilder.builder(DisableUserActionProviderFactory.ID)
.after(Duration.ofDays(10))
.build()
).build(session);
});
// create a new user - this will trigger the association with the policy
managedRealm.admin().users().create(
this.getUserRepresentation("alice", "Alice", "Wonderland", "alice@wornderland.org"));
// test running the scheduled actions
runOnServer.run((session -> {
RealmModel realm = configureSessionContext(session);
ResourcePolicyManager manager = new ResourcePolicyManager(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.runScheduledTasks();
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
Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds()));
manager.runScheduledTasks();
user = session.users().getUserByUsername(realm, "alice");
assertTrue(user.isEnabled());
assertNotNull(user.getAttributes().get("message"));
} finally {
Time.setOffset(0);
}
}));
// logging-in with alice should not reset the policy - we should still run the disable action next
oauth.openLoginForm();
loginPage.fillLogin("alice", "alice");
loginPage.submit();
assertTrue(driver.getPageSource().contains("Happy days"));
// test running the scheduled actions
runOnServer.run((session -> {
RealmModel realm = configureSessionContext(session);
ResourcePolicyManager manager = new ResourcePolicyManager(session);
try {
// set offset to 11 days - disable action should run now
Time.setOffset(Math.toIntExact(Duration.ofDays(12).toSeconds()));
manager.runScheduledTasks();
UserModel user = session.users().getUserByUsername(realm, "alice");
assertFalse(user.isEnabled());
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;
}
}

View File

@ -18,28 +18,24 @@
package org.keycloak.tests.admin.model.policy;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
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 jakarta.persistence.EntityManager;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.keycloak.common.util.Time;
import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.jpa.entities.UserEntity;
import org.keycloak.models.UserProvider;
import org.keycloak.models.policy.DisableUserActionProviderFactory;
import org.keycloak.models.policy.NotifyUserActionProviderFactory;
import org.keycloak.models.policy.ResourcePolicyManager;
import org.keycloak.models.policy.UserActionBuilder;
import org.keycloak.models.policy.UserLastSessionRefreshTimeResourcePolicyProviderFactory;
import org.keycloak.models.policy.UserSessionRefreshTimeResourcePolicyProviderFactory;
import org.keycloak.testframework.annotations.InjectUser;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.injection.LifeCycle;
@ -48,14 +44,12 @@ import org.keycloak.testframework.oauth.annotations.InjectOAuthClient;
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.FetchOnServer;
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.testframework.ui.annotations.InjectPage;
import org.keycloak.testframework.ui.annotations.InjectWebDriver;
import org.keycloak.testframework.ui.page.LoginPage;
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
import org.openqa.selenium.WebDriver;
@KeycloakIntegrationTest(config = RLMServerConfig.class)
@ -84,27 +78,12 @@ public class UserSessionRefreshTimePolicyTest {
}
@Test
@Disabled
public void testDisabledUserAfterInactivityPeriod() {
runOnServer.run((RunOnServer) session -> {
RealmModel realm = configureSessionContext(session);
UserModel user = session.users().getUserByUsername(realm, "alice");
EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
UserEntity entity = em.find(UserEntity.class, user.getId());
assertNull(entity.getLastSessionRefreshTime());
});
oauth.openLoginForm();
loginPage.fillLogin("alice", "alice");
loginPage.submit();
assertTrue(driver.getPageSource().contains("Happy days"));
// test run policy
runOnServer.run((session -> {
RealmModel realm = configureSessionContext(session);
ResourcePolicyManager manager = PolicyBuilder.create()
.of(UserLastSessionRefreshTimeResourcePolicyProviderFactory.ID)
.withActions(
configureSessionContext(session);
PolicyBuilder.create()
.of(UserSessionRefreshTimeResourcePolicyProviderFactory.ID)
.withActions(
UserActionBuilder.builder(NotifyUserActionProviderFactory.ID)
.after(Duration.ofDays(5))
.build(),
@ -112,55 +91,66 @@ public class UserSessionRefreshTimePolicyTest {
.after(Duration.ofDays(10))
.build()
).build(session);
});
UserModel user = session.users().getUserByUsername(realm, "alice");
EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
UserEntity entity = em.find(UserEntity.class, user.getId());
assertNotNull(entity.getLastSessionRefreshTime());
// login with alice - this will attach the policy to the user and schedule the first action
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
runOnServer.run((session -> {
RealmModel realm = configureSessionContext(session);
ResourcePolicyManager manager = new ResourcePolicyManager(session);
UserModel user = session.users().getUserByUsername(realm, username);
assertTrue(user.isEnabled());
assertNull(user.getAttributes().get("message"));
manager.runPolicies();
user = session.users().getUserByUsername(realm, "alice");
// running the scheduled tasks now shouldn't pick up any action as none are due to run yet
manager.runScheduledTasks();
user = session.users().getUserByUsername(realm, username);
assertTrue(user.isEnabled());
assertNull(user.getAttributes().get("message"));
try {
manager = new ResourcePolicyManager(session);
Time.setOffset(Math.toIntExact(Duration.ofDays(7).toSeconds()));
manager.runPolicies();
user = session.users().getUserByUsername(realm, "alice");
// set offset to 6 days - notify action should run now
Time.setOffset(Math.toIntExact(Duration.ofDays(5).toSeconds()));
manager.runScheduledTasks();
user = session.users().getUserByUsername(realm, username);
assertTrue(user.isEnabled());
assertNotNull(user.getAttributes().get("message"));
} finally {
Time.setOffset(0);
}
}));
// trigger a login event that should reset the flow of the policy
oauth.openLoginForm();
runOnServer.run((session -> {
try {
entity.setLastSessionRefreshTime(Math.toIntExact(Time.currentTime() + Duration.ofDays(11).toSeconds()));
// setting the offset to 11 days should not run the second action as we re-started the flow on login
RealmModel realm = configureSessionContext(session);
Time.setOffset(Math.toIntExact(Duration.ofDays(11).toSeconds()));
manager.runPolicies();
user = session.users().getUserByUsername(realm, "alice");
ResourcePolicyManager manager = new ResourcePolicyManager(session);
manager.runScheduledTasks();
UserModel user = session.users().getUserByUsername(realm, username);
assertTrue(user.isEnabled());
} finally {
Time.setOffset(0);
}
try {
entity = em.find(UserEntity.class, user.getId());
entity.setLastSessionRefreshTime(Math.toIntExact(Time.currentTime() - Duration.ofDays(10).toSeconds()));
manager.runPolicies();
user = session.users().getUserByUsername(realm, "alice");
assertTrue(user.isEnabled());
} finally {
Time.setOffset(0);
}
try {
entity = em.find(UserEntity.class, user.getId());
entity.setLastSessionRefreshTime(Math.toIntExact(Time.currentTime() - Duration.ofDays(11).toSeconds()));
manager.runPolicies();
user = session.users().getUserByUsername(realm, "alice");
// first action has run and the next action should be triggered after 5 more days (time difference between the actions)
RealmModel realm = configureSessionContext(session);
Time.setOffset(Math.toIntExact(Duration.ofDays(17).toSeconds()));
ResourcePolicyManager manager = new ResourcePolicyManager(session);
manager.runScheduledTasks();
UserModel user = session.users().getUserByUsername(realm, username);
// second action should have run and the user should be disabled now
assertFalse(user.isEnabled());
} finally {
Time.setOffset(0);
@ -169,77 +159,76 @@ public class UserSessionRefreshTimePolicyTest {
}
@Test
public void testUpdateUserLastRefreshTimeOnReAuthentication() {
oauth.openLoginForm();
loginPage.fillLogin("alice", "alice");
loginPage.submit();
assertTrue(driver.getPageSource().contains("Happy days"));
Integer lastSessionRefreshTime = runOnServer.fetch((FetchOnServer) session -> {
RealmModel realm = configureSessionContext(session);
UserModel user = session.users().getUserByUsername(realm, "alice");
EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
UserEntity entity = em.find(UserEntity.class, user.getId());
assertNotNull(entity.getLastSessionRefreshTime());
return entity.getLastSessionRefreshTime();
}, Integer.class);
try {
runOnServer.run((session) -> {
Time.setOffset(Math.toIntExact(Duration.ofMinutes(10).toSeconds()));
});
oauth.openLoginForm();
assertTrue(driver.getPageSource().contains("Happy days"));
} finally {
runOnServer.run((session) -> {
Time.setOffset(0);
});
}
runOnServer.run((session) -> {
RealmModel realm = configureSessionContext(session);
UserModel user = session.users().getUserByUsername(realm, "alice");
EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
UserEntity entity = em.find(UserEntity.class, user.getId());
assertNotEquals(lastSessionRefreshTime, entity.getLastSessionRefreshTime());
public void testMultiplePolicies() {
runOnServer.run(session -> {
PolicyBuilder.create()
.of(UserSessionRefreshTimeResourcePolicyProviderFactory.ID)
.withActions(
UserActionBuilder.builder(NotifyUserActionProviderFactory.ID)
.after(Duration.ofDays(5))
.withConfig("message_key", "notifier1")
.build()
)
.build(session);
PolicyBuilder.create()
.of(UserSessionRefreshTimeResourcePolicyProviderFactory.ID)
.withActions(
UserActionBuilder.builder(NotifyUserActionProviderFactory.ID)
.after(Duration.ofDays(10))
.withConfig("message_key", "notifier2")
.build())
.build(session);
});
}
@Test
public void testUpdateUserLastRefreshTimeOnRefreshToken() {
AccessTokenResponse tokenResponse = oauth.doPasswordGrantRequest("alice", "alice");
assertNotNull(tokenResponse);
// perform a login to associate the policies with the new user.
oauth.openLoginForm();
String username = userAlice.getUsername();
loginPage.fillLogin(username, userAlice.getPassword());
loginPage.submit();
assertTrue(driver.getPageSource() != null && driver.getPageSource().contains("Happy days"));
Integer lastSessionRefreshTime = runOnServer.fetch((FetchOnServer) session -> {
runOnServer.run(session -> {
RealmModel realm = configureSessionContext(session);
UserModel user = session.users().getUserByUsername(realm, "alice");
EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
UserEntity entity = em.find(UserEntity.class, user.getId());
assertNotNull(entity.getLastSessionRefreshTime());
return entity.getLastSessionRefreshTime();
}, Integer.class);
ResourcePolicyManager manager = new ResourcePolicyManager(session);
assertNotNull(tokenResponse.getRefreshToken());
UserProvider users = session.users();
UserModel user = users.getUserByUsername(realm, username);
assertTrue(user.isEnabled());
assertNull(user.getFirstAttribute("notifier1"));
assertNull(user.getFirstAttribute("notifier2"));
try {
runOnServer.run((session) -> {
Time.setOffset(Math.toIntExact(Duration.ofMinutes(10).toSeconds()));
});
oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
} finally {
runOnServer.run((session) -> {
try {
Time.setOffset(Math.toIntExact(Duration.ofDays(7).toSeconds()));
manager.runScheduledTasks();
user = users.getUserByUsername(realm, username);
assertTrue(user.isEnabled());
assertNotNull(user.getFirstAttribute("notifier1"));
assertNull(user.getFirstAttribute("notifier2"));
user.removeAttribute("notifier1");
} finally {
Time.setOffset(0);
});
}
}
try {
Time.setOffset(Math.toIntExact(Duration.ofDays(11).toSeconds()));
manager.runScheduledTasks();
user = users.getUserByUsername(realm, username);
assertTrue(user.isEnabled());
assertNotNull(user.getFirstAttribute("notifier2"));
assertNull(user.getFirstAttribute("notifier1"));
user.removeAttribute("notifier2");
} finally {
Time.setOffset(0);
}
try {
manager.runScheduledTasks();
assertNull(user.getFirstAttribute("notifier1"));
assertNull(user.getFirstAttribute("notifier2"));
} finally {
Time.setOffset(0);
}
runOnServer.run((session) -> {
RealmModel realm = configureSessionContext(session);
UserModel user = session.users().getUserByUsername(realm, "alice");
EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
UserEntity entity = em.find(UserEntity.class, user.getId());
assertNotEquals(lastSessionRefreshTime, entity.getLastSessionRefreshTime());
});
}