mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 23:12:06 -03:30
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:
parent
03b5753c84
commit
70659ac183
@ -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;
|
||||
|
||||
@ -252,8 +252,6 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
|
||||
|
||||
entity.setStarted(currentTime);
|
||||
entity.setLastSessionRefresh(currentTime);
|
||||
|
||||
user.setLastSessionRefreshTime(entity.getLastSessionRefresh());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -264,8 +264,6 @@ public class UserSessionAdapter<T extends SessionRefreshStore & UserSessionProvi
|
||||
}
|
||||
};
|
||||
|
||||
getUser().setLastSessionRefreshTime(lastSessionRefresh);
|
||||
|
||||
update(task);
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<>();
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
@ -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) {};
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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.
|
||||
*
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
package org.keycloak.models.policy;
|
||||
|
||||
public enum ResourceOperationType {
|
||||
|
||||
CREATE,
|
||||
|
||||
LOGIN
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -232,8 +232,6 @@ public interface UserModel extends RoleMapperModel {
|
||||
*/
|
||||
SubjectCredentialManager credentialManager();
|
||||
|
||||
void setLastSessionRefreshTime(int lastSessionRefreshTime);
|
||||
|
||||
enum RequiredAction {
|
||||
VERIFY_EMAIL,
|
||||
UPDATE_PROFILE,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,4 +16,5 @@
|
||||
#
|
||||
|
||||
org.keycloak.events.email.EmailEventListenerProviderFactory
|
||||
org.keycloak.events.log.JBossLoggingEventListenerProviderFactory
|
||||
org.keycloak.events.log.JBossLoggingEventListenerProviderFactory
|
||||
org.keycloak.models.policy.ResourcePolicyEventListenerFactory
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user