mirror of
https://github.com/keycloak/keycloak.git
synced 2026-05-28 04:13:22 -04:00
Initial commit for the RLM feature
Closes #40340 Closes #40341 Co-authored-by: Stefan Guilhen <sguilhen@redhat.com> Co-authored-by: Pedro Igor <pigor.craveiro@gmail.com> Signed-off-by: vramik <vramik@redhat.com>
This commit is contained in:
parent
20cb13e8dc
commit
a8225655cf
54 changed files with 3298 additions and 0 deletions
|
|
@ -136,6 +136,8 @@ public class Profile {
|
|||
ROLLING_UPDATES_V1("Rolling Updates", Type.DEFAULT, 1),
|
||||
ROLLING_UPDATES_V2("Rolling Updates for patch releases", Type.PREVIEW, 2),
|
||||
|
||||
RESOURCE_LIFECYCLE("Resource lifecycle management", Type.EXPERIMENTAL),
|
||||
|
||||
LOG_MDC("Mapped Diagnostic Context (MDC) information in logs", Type.PREVIEW),
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import org.keycloak.models.SubjectCredentialManager;
|
|||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.cache.CachedUserModel;
|
||||
import org.keycloak.models.cache.infinispan.entities.CachedUser;
|
||||
import org.keycloak.models.policy.ResourcePolicyManager;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.RoleUtils;
|
||||
|
||||
|
|
@ -471,6 +472,17 @@ 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,6 +252,8 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
|
|||
|
||||
entity.setStarted(currentTime);
|
||||
entity.setLastSessionRefresh(currentTime);
|
||||
|
||||
user.setLastSessionRefreshTime(entity.getLastSessionRefresh());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -264,6 +264,8 @@ public class UserSessionAdapter<T extends SessionRefreshStore & UserSessionProvi
|
|||
}
|
||||
};
|
||||
|
||||
getUser().setLastSessionRefreshTime(lastSessionRefresh);
|
||||
|
||||
update(task);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -272,6 +272,8 @@ public class UserSessionEntity extends SessionEntity {
|
|||
|
||||
entity.setStarted(currentTime);
|
||||
entity.setLastSessionRefresh(currentTime);
|
||||
|
||||
user.setLastSessionRefreshTime(entity.getLastSessionRefresh());
|
||||
}
|
||||
|
||||
public static UserSessionEntity createFromModel(UserSessionModel userSession) {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import org.keycloak.models.jpa.entities.UserEntity;
|
|||
import org.keycloak.models.jpa.entities.UserGroupMembershipEntity;
|
||||
import org.keycloak.models.jpa.entities.UserRequiredActionEntity;
|
||||
import org.keycloak.models.jpa.entities.UserRoleMappingEntity;
|
||||
import org.keycloak.models.policy.ResourcePolicyManager;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.RoleUtils;
|
||||
|
||||
|
|
@ -576,6 +577,12 @@ 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) {
|
||||
|
|
|
|||
|
|
@ -91,6 +91,9 @@ 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.
|
||||
|
|
@ -226,6 +229,14 @@ 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<>();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright 2025 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
import jakarta.persistence.criteria.Subquery;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.connections.jpa.JpaConnectionProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.jpa.entities.UserEntity;
|
||||
|
||||
public abstract class AbstractUserResourcePolicyProvider implements ResourcePolicyProvider {
|
||||
|
||||
private final ComponentModel policyModel;
|
||||
private final EntityManager em;
|
||||
|
||||
public AbstractUserResourcePolicyProvider(KeycloakSession session, ComponentModel model) {
|
||||
this.policyModel = model;
|
||||
this.em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
||||
}
|
||||
|
||||
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 eligable for initial action
|
||||
@Override
|
||||
public List<String> getEligibleResourcesForInitialAction(long time) {
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaQuery<String> query = cb.createQuery(String.class);
|
||||
Root<UserEntity> userRoot = query.from(UserEntity.class);
|
||||
|
||||
// Subquery will find if a state record exists for the user and policy
|
||||
// SELECT 1 FROM ResourcePolicyStateEntity s WHERE s.resourceId = userRoot.id AND s.policyId = :policyId
|
||||
Subquery<Integer> subquery = query.subquery(Integer.class);
|
||||
Root<ResourcePolicyStateEntity> stateRoot = subquery.from(ResourcePolicyStateEntity.class);
|
||||
subquery.select(cb.literal(1)); // Select 1 for existence check
|
||||
subquery.where(
|
||||
cb.and(
|
||||
cb.equal(stateRoot.get("resourceId"), userRoot.get("id")),
|
||||
cb.equal(stateRoot.get("policyId"), policyModel.getId())
|
||||
)
|
||||
);
|
||||
|
||||
// Time-based condition
|
||||
Predicate timePredicate = timePredicate(time, cb, query, userRoot);
|
||||
|
||||
// NOT EXISTS condition
|
||||
Predicate notExistsPredicate = cb.not(cb.exists(subquery));
|
||||
|
||||
query.where(cb.and(timePredicate, notExistsPredicate));
|
||||
query.select(userRoot.get("id"));
|
||||
|
||||
return em.createQuery(query).getResultList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> filterEligibleResources(List<String> candidateResourceIds, long time) {
|
||||
// If there are no candidates, return an empty list
|
||||
if (candidateResourceIds == null || candidateResourceIds.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaQuery<String> query = cb.createQuery(String.class);
|
||||
Root<UserEntity> userRoot = query.from(UserEntity.class);
|
||||
|
||||
// Time-based condition
|
||||
Predicate timePredicate = timePredicate(time, cb, query, userRoot);
|
||||
|
||||
// IN clause with candidateResourceIds
|
||||
Predicate inClausePredicate = userRoot.get("id").in(candidateResourceIds);
|
||||
|
||||
query.where(cb.and(timePredicate, inClausePredicate));
|
||||
query.select(userRoot.get("id"));
|
||||
|
||||
return em.createQuery(query).getResultList();
|
||||
}
|
||||
|
||||
protected EntityManager getEntityManager() {
|
||||
return em;
|
||||
}
|
||||
|
||||
public ComponentModel getModel() {
|
||||
return policyModel;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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.List;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
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";
|
||||
|
||||
@Override
|
||||
public ResourceType getType() {
|
||||
return ResourceType.USERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FederatedIdentityPolicyProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new FederatedIdentityPolicyProvider(session, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* 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.List;
|
||||
import java.util.Set;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaDelete;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.connections.jpa.JpaConnectionProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
public class JpaResourcePolicyStateProvider implements ResourcePolicyStateProvider {
|
||||
|
||||
private final EntityManager em;
|
||||
private static final Logger LOGGER = Logger.getLogger(JpaResourcePolicyStateProvider.class);
|
||||
private final KeycloakSession session;
|
||||
|
||||
public JpaResourcePolicyStateProvider(KeycloakSession session) {
|
||||
this.session = session;
|
||||
this.em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> findResourceIdsByLastCompletedAction(String policyId, String lastCompletedActionId) {
|
||||
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);
|
||||
|
||||
query.select(stateRoot.get("resourceId"));
|
||||
query.where(cb.and(policyPredicate, actionPredicate));
|
||||
|
||||
return em.createQuery(query).getResultList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(String policyId, String policyProviderId, List<String> resourceIds, String newLastCompletedActionId) {
|
||||
for (String resourceId : resourceIds) {
|
||||
ResourcePolicyStateEntity.PrimaryKey pk = new ResourcePolicyStateEntity.PrimaryKey(resourceId, policyId);
|
||||
ResourcePolicyStateEntity entity = em.find(ResourcePolicyStateEntity.class, pk);
|
||||
|
||||
if (entity == null) {
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
LOGGER.tracev("Initial record for policyId ({0}), new_last_compl_actionId ({1}), userId ({2})", policyId, newLastCompletedActionId, resourceId);
|
||||
}
|
||||
entity = new ResourcePolicyStateEntity();
|
||||
entity.setResourceId(resourceId);
|
||||
entity.setPolicyId(policyId);
|
||||
entity.setPolicyProviderId(policyProviderId);
|
||||
em.persist(entity);
|
||||
} 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.setLastCompletedActionId(newLastCompletedActionId);
|
||||
entity.setLastUpdatedTimestamp(Time.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeByCompletedActions(String policyId, Set<String> deletedActionIds) {
|
||||
if (deletedActionIds == null || deletedActionIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaDelete<ResourcePolicyStateEntity> delete = cb.createCriteriaDelete(ResourcePolicyStateEntity.class);
|
||||
Root<ResourcePolicyStateEntity> stateRoot = delete.from(ResourcePolicyStateEntity.class);
|
||||
|
||||
Predicate policyPredicate = cb.equal(stateRoot.get("policyId"), policyId);
|
||||
Predicate inClausePredicate = stateRoot.get("lastCompletedActionId").in(deletedActionIds);
|
||||
|
||||
delete.where(cb.and(policyPredicate, inClausePredicate));
|
||||
|
||||
int deletedCount = em.createQuery(delete).executeUpdate();
|
||||
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
if (deletedCount > 0) {
|
||||
LOGGER.tracev("Deleted {0} orphaned state records for policy {1}", deletedCount, policyId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeByUser(UserModel user) {
|
||||
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()));
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAll() {
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaDelete<ResourcePolicyStateEntity> delete = cb.createCriteriaDelete(ResourcePolicyStateEntity.class);
|
||||
int deletedCount = em.createQuery(delete).executeUpdate();
|
||||
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
if (deletedCount > 0) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
LOGGER.tracev("Deleted {0} state records for realm {1}", deletedCount, realm.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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.Config;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel.RealmRemovedEvent;
|
||||
import org.keycloak.models.UserModel.UserRemovedEvent;
|
||||
|
||||
public class JpaResourcePolicyStateProviderFactory implements ResourcePolicyStateProviderFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "jpa";
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
factory.register(fired -> {
|
||||
if (fired instanceof UserRemovedEvent event) {
|
||||
onUserRemovedEvent(event);
|
||||
} if (fired instanceof RealmRemovedEvent event) {
|
||||
onRealmRemovedEvent(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourcePolicyStateProvider create(KeycloakSession session) {
|
||||
return new JpaResourcePolicyStateProvider(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
private void onRealmRemovedEvent(RealmRemovedEvent event) {
|
||||
KeycloakSession session = event.getKeycloakSession();
|
||||
ResourcePolicyStateProvider provider = session.getProvider(ResourcePolicyStateProvider.class);
|
||||
provider.removeAll();
|
||||
}
|
||||
|
||||
private void onUserRemovedEvent(UserRemovedEvent event) {
|
||||
KeycloakSession session = event.getKeycloakSession();
|
||||
ResourcePolicyStateProvider provider = session.getProvider(ResourcePolicyStateProvider.class);
|
||||
provider.removeByUser(event.getUser());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
* Copyright 2025 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.IdClass;
|
||||
import jakarta.persistence.Table;
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Represents the state of a resource within a time-based policy flow.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "RESOURCE_POLICY_STATE")
|
||||
@IdClass(ResourcePolicyStateEntity.PrimaryKey.class)
|
||||
public class ResourcePolicyStateEntity {
|
||||
|
||||
@Id
|
||||
@Column(name = "RESOURCE_ID")
|
||||
private String resourceId;
|
||||
|
||||
@Id
|
||||
@Column(name = "POLICY_ID")
|
||||
private String policyId;
|
||||
|
||||
@Column(name = "RESOURCE_TYPE")
|
||||
private String resourceType; // do we want/need to store this?
|
||||
|
||||
@Column(name = "POLICY_PROVIDER_ID")
|
||||
private String policyProviderId;
|
||||
|
||||
@Column(name = "LAST_COMPLETED_ACTION_ID")
|
||||
private String lastCompletedActionId;
|
||||
|
||||
@Column(name = "LAST_UPDATED_TIMESTAMP")// might be useful?? - audit?
|
||||
private long lastUpdatedTimestamp;
|
||||
|
||||
public String getResourceId() {
|
||||
return resourceId;
|
||||
}
|
||||
|
||||
public void setResourceId(String resourceId) {
|
||||
this.resourceId = resourceId;
|
||||
}
|
||||
|
||||
public String getPolicyId() {
|
||||
return policyId;
|
||||
}
|
||||
|
||||
public void setPolicyId(String policyId) {
|
||||
this.policyId = policyId;
|
||||
}
|
||||
|
||||
public String getPolicyProviderId() {
|
||||
return policyProviderId;
|
||||
}
|
||||
|
||||
public void setPolicyProviderId(String policyProviderId) {
|
||||
this.policyProviderId = policyProviderId;
|
||||
}
|
||||
|
||||
public String getResourceType() {
|
||||
return resourceType;
|
||||
}
|
||||
|
||||
public void setResourceType(String resourceType) {
|
||||
this.resourceType = resourceType;
|
||||
}
|
||||
|
||||
public String getLastCompletedActionId() {
|
||||
return lastCompletedActionId;
|
||||
}
|
||||
|
||||
public void setLastCompletedActionId(String lastCompletedActionId) {
|
||||
this.lastCompletedActionId = lastCompletedActionId;
|
||||
}
|
||||
|
||||
public long getLastUpdatedTimestamp() {
|
||||
return lastUpdatedTimestamp;
|
||||
}
|
||||
|
||||
public void setLastUpdatedTimestamp(long lastUpdatedTimestamp) {
|
||||
this.lastUpdatedTimestamp = lastUpdatedTimestamp;
|
||||
}
|
||||
|
||||
public static class PrimaryKey implements Serializable {
|
||||
|
||||
private String resourceId;
|
||||
private String policyId;
|
||||
|
||||
public PrimaryKey() {
|
||||
}
|
||||
|
||||
public PrimaryKey(String resourceId, String policyId) {
|
||||
this.resourceId = resourceId;
|
||||
this.policyId = policyId;
|
||||
}
|
||||
|
||||
public String getResourceId() {
|
||||
return resourceId;
|
||||
}
|
||||
|
||||
public void setResourceId(String resourceId) {
|
||||
this.resourceId = resourceId;
|
||||
}
|
||||
|
||||
public String getPolicyId() {
|
||||
return policyId;
|
||||
}
|
||||
|
||||
public void setPolicyId(String policyId) {
|
||||
this.policyId = policyId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
PrimaryKey that = (PrimaryKey) o;
|
||||
return Objects.equals(resourceId, that.resourceId) && Objects.equals(policyId, that.policyId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(resourceId, policyId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ResourcePolicyStateEntity that = (ResourcePolicyStateEntity) o;
|
||||
return Objects.equals(resourceId, that.resourceId) && Objects.equals(policyId, that.policyId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(resourceId, policyId);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright 2025 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.Expression;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.jpa.entities.UserEntity;
|
||||
|
||||
public class UserCreationDateResourcePolicyProvider extends AbstractUserResourcePolicyProvider {
|
||||
|
||||
public UserCreationDateResourcePolicyProvider(KeycloakSession session, ComponentModel model) {
|
||||
super(session, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Predicate timePredicate(long time, CriteriaBuilder cb, CriteriaQuery<String> query, Root<UserEntity> userRoot) {
|
||||
long currentTimeMillis = Time.currentTimeMillis();
|
||||
Expression<Long> timeMoment = cb.sum(userRoot.get("createdTimestamp"), cb.literal(time));
|
||||
return cb.lessThan(timeMoment, cb.literal(currentTimeMillis));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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.List;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
public class UserCreationDateResourcePolicyProviderFactory implements ResourcePolicyProviderFactory<UserCreationDateResourcePolicyProvider> {
|
||||
|
||||
public static final String ID = "user-creation-date-resource-policy";
|
||||
|
||||
@Override
|
||||
public ResourceType getType() {
|
||||
return ResourceType.USERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserCreationDateResourcePolicyProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new UserCreationDateResourcePolicyProvider(session, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright 2025 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.Expression;
|
||||
import jakarta.persistence.criteria.Path;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.jpa.entities.UserEntity;
|
||||
|
||||
public class UserLastSessionRefreshTimeResourcePolicyProvider extends AbstractUserResourcePolicyProvider {
|
||||
|
||||
public UserLastSessionRefreshTimeResourcePolicyProvider(KeycloakSession session, ComponentModel model) {
|
||||
super(session, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Predicate timePredicate(long time, CriteriaBuilder cb, CriteriaQuery<String> query, Root<UserEntity> userRoot) {
|
||||
long currentTimeSeconds = Time.currentTime();
|
||||
Path<Long> lastSessionRefreshTime = userRoot.get("lastSessionRefreshTime");
|
||||
Expression<Long> lastSessionRefreshTimeExpiration = cb.sum(lastSessionRefreshTime, cb.literal(Duration.ofMillis(time).toSeconds()));
|
||||
return cb.and(cb.isNotNull(lastSessionRefreshTime), cb.lessThan(lastSessionRefreshTimeExpiration, cb.literal(currentTimeSeconds)));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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.List;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
public class UserLastSessionRefreshTimeResourcePolicyProviderFactory implements ResourcePolicyProviderFactory<UserLastSessionRefreshTimeResourcePolicyProvider> {
|
||||
|
||||
public static final String ID = "user-last-auth-time-resource-policy";
|
||||
|
||||
@Override
|
||||
public ResourceType getType() {
|
||||
return ResourceType.USERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserLastSessionRefreshTimeResourcePolicyProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new UserLastSessionRefreshTimeResourcePolicyProvider(session, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
|
|
@ -28,4 +28,54 @@
|
|||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="40343-RLM-resource-policy-state-table" author="keycloak">
|
||||
<createTable tableName="RESOURCE_POLICY_STATE">
|
||||
<column name="RESOURCE_ID" type="VARCHAR(255)">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
<column name="POLICY_ID" type="VARCHAR(255)">
|
||||
<constraints nullable="false" />
|
||||
</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" />
|
||||
</createTable>
|
||||
|
||||
<addPrimaryKey
|
||||
constraintName="PK_RESOURCE_POLICY_STATE"
|
||||
tableName="RESOURCE_POLICY_STATE"
|
||||
columnNames="RESOURCE_ID, POLICY_ID" />
|
||||
|
||||
<createIndex indexName="IDX_RES_POLICY_STATE_ACTION"
|
||||
tableName="RESOURCE_POLICY_STATE">
|
||||
<column name="POLICY_ID" />
|
||||
<column name="LAST_COMPLETED_ACTION_ID" />
|
||||
</createIndex>
|
||||
|
||||
<createIndex indexName="IDX_RES_POLICY_STATE_PROVIDER"
|
||||
tableName="RESOURCE_POLICY_STATE">
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
org.keycloak.models.policy.UserCreationDateResourcePolicyProviderFactory
|
||||
org.keycloak.models.policy.UserLastSessionRefreshTimeResourcePolicyProviderFactory
|
||||
org.keycloak.models.policy.FederatedIdentityPolicyProviderFactory
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
org.keycloak.models.policy.JpaResourcePolicyStateProviderFactory
|
||||
|
|
@ -92,6 +92,9 @@
|
|||
<!-- Server Configuration -->
|
||||
<class>org.keycloak.storage.configuration.jpa.entity.ServerConfigEntity</class>
|
||||
|
||||
<!-- Resource Lifecycle Management -->
|
||||
<class>org.keycloak.models.policy.ResourcePolicyStateEntity</class>
|
||||
|
||||
<exclude-unlisted-classes>true</exclude-unlisted-classes>
|
||||
|
||||
<properties>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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.models.UserModel;
|
||||
import org.keycloak.provider.Provider;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Interface serves as state check for policy actions.
|
||||
*/
|
||||
public interface ResourcePolicyStateProvider extends Provider {
|
||||
|
||||
/**
|
||||
* Finds resource IDs that have completed a specific action within a policy.
|
||||
*/
|
||||
List<String> findResourceIdsByLastCompletedAction(String policyId, String lastCompletedActionId);
|
||||
|
||||
/**
|
||||
* Updates the state for a list of resources that have just completed a new action.
|
||||
* This will perform an update for existing states or an insert for new states.
|
||||
*/
|
||||
void update(String policyId, String policyProviderId, List<String> resourceIds, String newLastCompletedActionId);
|
||||
|
||||
/**
|
||||
* Deletes the orphaned state records.
|
||||
*/
|
||||
void removeByCompletedActions(String policyId, Set<String> deletedActionIds);
|
||||
|
||||
/**
|
||||
* Deletes the state records associated with the given {@code user}.
|
||||
*
|
||||
* @param user the user
|
||||
*/
|
||||
void removeByUser(UserModel user);
|
||||
|
||||
/**
|
||||
* Deletes all state records associated with the current realm bound to the session.
|
||||
*/
|
||||
void removeAll();
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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.Config;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
public interface ResourcePolicyStateProviderFactory extends ProviderFactory<ResourcePolicyStateProvider>, EnvironmentDependentProviderFactory {
|
||||
|
||||
@Override
|
||||
default boolean isSupported(Config.Scope config) {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.RESOURCE_LIFECYCLE);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.Spi;
|
||||
|
||||
public class ResourcePolicyStateSpi implements Spi {
|
||||
|
||||
public static final String NAME = "resource-policy-state";
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Provider> getProviderClass() {
|
||||
return ResourcePolicyStateProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||
return ResourcePolicyStateProviderFactory.class;
|
||||
}
|
||||
}
|
||||
|
|
@ -26,3 +26,4 @@ org.keycloak.storage.clientscope.ClientScopeStorageProviderSpi
|
|||
org.keycloak.models.session.UserSessionPersisterSpi
|
||||
org.keycloak.models.session.RevokedTokenPersisterSpi
|
||||
org.keycloak.cluster.ClusterSpi
|
||||
org.keycloak.models.policy.ResourcePolicyStateSpi
|
||||
|
|
|
|||
|
|
@ -400,6 +400,11 @@ 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,6 +404,11 @@ 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;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
|
||||
public class ResourceAction implements Comparable<ResourceAction> {
|
||||
|
||||
private static final String AFTER_KEY = "after";
|
||||
private static final String PRIORITY_KEY = "priority";
|
||||
|
||||
private String id;
|
||||
private String providerId;
|
||||
private MultivaluedHashMap<String, String> config;
|
||||
|
||||
public ResourceAction() {
|
||||
// reflection
|
||||
}
|
||||
|
||||
public ResourceAction(String providerId) {
|
||||
this.providerId = providerId;
|
||||
}
|
||||
|
||||
public ResourceAction(ComponentModel model) {
|
||||
this.id = model.getId();
|
||||
this.providerId = model.getProviderId();
|
||||
this.config = model.getConfig();
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getProviderId() {
|
||||
return providerId;
|
||||
}
|
||||
|
||||
public ResourceAction setConfig(String key, String value) {
|
||||
if (config == null) {
|
||||
config = new MultivaluedHashMap<>();
|
||||
}
|
||||
this.config.putSingle(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MultivaluedHashMap<String, String> getConfig() {
|
||||
if (config == null) {
|
||||
return new MultivaluedHashMap<>();
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
public void setPriority(int priority) {
|
||||
setConfig(PRIORITY_KEY, String.valueOf(priority));
|
||||
}
|
||||
|
||||
// todo, do not expose the priority to user?? in export etc. the order of actions should define the priority
|
||||
public int getPriority() {
|
||||
String value = getConfig().getFirst(PRIORITY_KEY);
|
||||
if (value == null) {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(value);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
public void setAfter(Long ms) {
|
||||
setConfig(AFTER_KEY, String.valueOf(ms));
|
||||
}
|
||||
|
||||
public Long getAfter() {
|
||||
return Long.valueOf(getConfig().getFirstOrDefault(AFTER_KEY, "0"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(ResourceAction other) {
|
||||
return Integer.compare(this.getPriority(), other.getPriority());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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.List;
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
public interface ResourceActionProvider extends Provider {
|
||||
|
||||
void run(List<String> resourceIds);
|
||||
|
||||
boolean isRunnable();
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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.Config;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.component.ComponentFactory;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
|
||||
public interface ResourceActionProviderFactory<P extends ResourceActionProvider> extends ComponentFactory<P, ResourceActionProvider>, EnvironmentDependentProviderFactory {
|
||||
|
||||
ResourceType getType();
|
||||
|
||||
@Override
|
||||
default boolean isSupported(Config.Scope config) {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.RESOURCE_LIFECYCLE);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.Spi;
|
||||
|
||||
public class ResourceActionSpi implements Spi {
|
||||
|
||||
public static String NAME = "rlm-action";
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Provider> getProviderClass() {
|
||||
return ResourceActionProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||
return ResourceActionProviderFactory.class;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
|
||||
public class ResourcePolicy {
|
||||
|
||||
private MultivaluedHashMap<String, String> config;
|
||||
private String providerId;
|
||||
private String id;
|
||||
|
||||
public ResourcePolicy() {
|
||||
// reflection
|
||||
}
|
||||
|
||||
public ResourcePolicy(String providerId) {
|
||||
this.providerId = providerId;
|
||||
this.id = null;
|
||||
this.config = null;
|
||||
}
|
||||
|
||||
public ResourcePolicy(ComponentModel c) {
|
||||
this.id = c.getId();
|
||||
this.providerId = c.getProviderId();
|
||||
this.config = c.getConfig();
|
||||
}
|
||||
|
||||
public ResourcePolicy(String providerId, Map<String, List<String>> config) {
|
||||
this.providerId = providerId;
|
||||
MultivaluedHashMap<String, String> c = new MultivaluedHashMap<>();
|
||||
config.forEach(c::addAll);
|
||||
this.config = c;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getProviderId() {
|
||||
return providerId;
|
||||
}
|
||||
|
||||
public MultivaluedHashMap<String, String> getConfig() {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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.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 {
|
||||
|
||||
/**
|
||||
* Finds all resources that are eligible for the first action of a policy.
|
||||
*
|
||||
* @param time The time delay for the first action.
|
||||
* @return A list of eligible resource IDs.
|
||||
*/
|
||||
List<String> getEligibleResourcesForInitialAction(long time);
|
||||
|
||||
/**
|
||||
* This method checks a list of candidates and returns only those that are eligible based on time.
|
||||
*/
|
||||
List<String> filterEligibleResources(List<String> candidateResourceIds, long time);
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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.Config;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.component.ComponentFactory;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
|
||||
public interface ResourcePolicyProviderFactory<P extends ResourcePolicyProvider> extends ComponentFactory<P, ResourcePolicyProvider>, EnvironmentDependentProviderFactory {
|
||||
|
||||
ResourceType getType();
|
||||
|
||||
@Override
|
||||
default boolean isSupported(Config.Scope config) {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.RESOURCE_LIFECYCLE);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.Spi;
|
||||
|
||||
public class ResourcePolicySpi implements Spi {
|
||||
|
||||
public static final String NAME = "rlm-policy";
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Provider> getProviderClass() {
|
||||
return ResourcePolicyProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||
return ResourcePolicyProviderFactory.class;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
public enum ResourceType {
|
||||
USERS
|
||||
}
|
||||
|
|
@ -292,6 +292,11 @@ 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;
|
||||
|
|
|
|||
|
|
@ -105,3 +105,5 @@ org.keycloak.cookie.CookieSpi
|
|||
org.keycloak.organization.OrganizationSpi
|
||||
org.keycloak.securityprofile.SecurityProfileSpi
|
||||
org.keycloak.logging.MappedDiagnosticContextSpi
|
||||
org.keycloak.models.policy.ResourceActionSpi
|
||||
org.keycloak.models.policy.ResourcePolicySpi
|
||||
|
|
|
|||
|
|
@ -232,6 +232,8 @@ public interface UserModel extends RoleMapperModel {
|
|||
*/
|
||||
SubjectCredentialManager credentialManager();
|
||||
|
||||
void setLastSessionRefreshTime(int lastSessionRefreshTime);
|
||||
|
||||
enum RequiredAction {
|
||||
VERIFY_EMAIL,
|
||||
UPDATE_PROFILE,
|
||||
|
|
|
|||
|
|
@ -251,6 +251,11 @@ 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,63 @@
|
|||
/*
|
||||
* 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.List;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
public class DeleteUserActionProvider implements ResourceActionProvider {
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final ComponentModel actionModel;
|
||||
private final Logger log = Logger.getLogger(DeleteUserActionProvider.class);
|
||||
|
||||
public DeleteUserActionProvider(KeycloakSession session, ComponentModel model) {
|
||||
this.session = session;
|
||||
this.actionModel = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(List<String> ids) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
|
||||
for (String id : ids) {
|
||||
UserModel user = session.users().getUserById(realm, id);
|
||||
|
||||
if (user == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
log.debugv("Deleting user {0} ({1})", user.getUsername(), user.getId());
|
||||
session.users().removeUser(realm, user);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunnable() {
|
||||
return actionModel.get("after") != null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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.List;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
public class DeleteUserActionProviderFactory implements ResourceActionProviderFactory<DeleteUserActionProvider> {
|
||||
|
||||
public static final String ID = "delete-user-action-provider";
|
||||
|
||||
@Override
|
||||
public DeleteUserActionProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new DeleteUserActionProvider(session, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceType getType() {
|
||||
return ResourceType.USERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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.List;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
public class DisableUserActionProvider implements ResourceActionProvider {
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final ComponentModel actionModel;
|
||||
private final Logger log = Logger.getLogger(DisableUserActionProvider.class);
|
||||
|
||||
public DisableUserActionProvider(KeycloakSession session, ComponentModel model) {
|
||||
this.session = session;
|
||||
this.actionModel = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(List<String> userIds) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
|
||||
for (String id : userIds) {
|
||||
UserModel user = session.users().getUserById(realm, id);
|
||||
|
||||
if (user != null && user.isEnabled()) {
|
||||
log.debugv("Disabling user {0} ({1})", user.getUsername(), user.getId());
|
||||
user.setEnabled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunnable() {
|
||||
return actionModel.get("after") != null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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.List;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
public class DisableUserActionProviderFactory implements ResourceActionProviderFactory<DisableUserActionProvider> {
|
||||
|
||||
public static final String ID = "disable-user-action-provider";
|
||||
|
||||
@Override
|
||||
public DisableUserActionProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new DisableUserActionProvider(session, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceType getType() {
|
||||
return ResourceType.USERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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.List;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
public class NotifyUserActionProvider implements ResourceActionProvider {
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final ComponentModel actionModel;
|
||||
private final Logger log = Logger.getLogger(NotifyUserActionProvider.class);
|
||||
|
||||
public NotifyUserActionProvider(KeycloakSession session, ComponentModel model) {
|
||||
this.session = session;
|
||||
this.actionModel = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(List<String> userIds) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
|
||||
for (String id : userIds) {
|
||||
UserModel user = session.users().getUserById(realm, id);
|
||||
|
||||
if (user != null) {
|
||||
log.debugv("Disabling user {0} ({1})", user.getUsername(), user.getId());
|
||||
user.setSingleAttribute(getMessageKey(), getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getMessageKey() {
|
||||
return actionModel.getConfig().getFirstOrDefault("message_key", "message");
|
||||
}
|
||||
|
||||
private String getMessage() {
|
||||
return actionModel.getConfig().getFirstOrDefault(getMessageKey(), "sent");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunnable() {
|
||||
return actionModel.get("after") != null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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.List;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
public class NotifyUserActionProviderFactory implements ResourceActionProviderFactory<NotifyUserActionProvider> {
|
||||
|
||||
public static final String ID = "notify-user-action-provider";
|
||||
|
||||
@Override
|
||||
public NotifyUserActionProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new NotifyUserActionProvider(session, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceType getType() {
|
||||
return ResourceType.USERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
/*
|
||||
* Copyright 2025 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.Profile.Feature;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.component.ComponentFactory;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
public class ResourcePolicyManager {
|
||||
|
||||
private static final Logger log = Logger.getLogger(ResourcePolicyManager.class);
|
||||
|
||||
public static boolean isFeatureEnabled() {
|
||||
return Profile.isFeatureEnabled(Feature.RESOURCE_LIFECYCLE);
|
||||
}
|
||||
|
||||
private final KeycloakSession session;
|
||||
|
||||
public ResourcePolicyManager(KeycloakSession session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
public ResourcePolicy addPolicy(String providerId) {
|
||||
return addPolicy(new ResourcePolicy(providerId));
|
||||
}
|
||||
|
||||
public ResourcePolicy addPolicy(String providerId, Map<String, List<String>> config) {
|
||||
return addPolicy(new ResourcePolicy(providerId, config));
|
||||
}
|
||||
|
||||
public ResourcePolicy addPolicy(ResourcePolicy policy) {
|
||||
RealmModel realm = getRealm();
|
||||
ComponentModel model = new ComponentModel();
|
||||
|
||||
model.setParentId(realm.getId());
|
||||
model.setProviderId(policy.getProviderId());
|
||||
model.setProviderType(ResourcePolicyProvider.class.getName());
|
||||
|
||||
MultivaluedHashMap<String, String> config = policy.getConfig();
|
||||
|
||||
if (config != null) {
|
||||
model.setConfig(config);
|
||||
}
|
||||
|
||||
return new ResourcePolicy(realm.addComponentModel(model));
|
||||
}
|
||||
|
||||
/*
|
||||
This method takes an ordered list of actions. First action in the list has the highest priority, last action has the lowest priority
|
||||
It is used for both create and update actions
|
||||
---------------------------------------------------------------------------------------
|
||||
using delete-and-recreate approach for now as it seems more simple and robust solution
|
||||
todo: consider changing it to "diff-and-update" (more complex) approach where we'd need to
|
||||
* keep existing actions
|
||||
* create newly added actions
|
||||
* delete removed actions
|
||||
* reorder existing action according to new order (we may add gaps between priority so that we won't need to update all existing actions)
|
||||
* with the gap approach, it may eventually happen that there won't be any space between the two action, in that case we'd have to trigger recalculation of priorities
|
||||
*/
|
||||
public void updateActions(ResourcePolicy policy, List<ResourceAction> actions) {
|
||||
|
||||
validateActions(actions);
|
||||
|
||||
// get the stable IDs of the new actions
|
||||
Set<String> newActionIds = actions.stream()
|
||||
.map(ResourceAction::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
// get the stable IDs of the old actions
|
||||
List<ResourceAction> oldActions = getActions(policy);
|
||||
Set<String> oldActionIds = oldActions.stream()
|
||||
.map(ResourceAction::getId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
RealmModel realm = getRealm();
|
||||
// remove all existing actions of the policy
|
||||
realm.removeComponents(policy.getId());
|
||||
|
||||
// add the new actions
|
||||
for (int i = 0; i < actions.size(); i++) {
|
||||
ResourceAction action = actions.get(i);
|
||||
|
||||
// assign priority based on index.
|
||||
action.setPriority(i + 1);
|
||||
|
||||
// persist the new action component.
|
||||
addAction(policy, action);
|
||||
}
|
||||
}
|
||||
|
||||
private ResourceAction addAction(ResourcePolicy policy, ResourceAction action) {
|
||||
RealmModel realm = getRealm();
|
||||
ComponentModel policyModel = realm.getComponent(policy.getId());
|
||||
ComponentModel actionModel = new ComponentModel();
|
||||
|
||||
actionModel.setId(action.getId());//need to keep stable UUIDs not to break a link in state table
|
||||
actionModel.setParentId(policyModel.getId());
|
||||
actionModel.setProviderId(action.getProviderId());
|
||||
actionModel.setProviderType(ResourceActionProvider.class.getName());
|
||||
actionModel.setConfig(action.getConfig());
|
||||
|
||||
return new ResourceAction(realm.addComponentModel(actionModel));
|
||||
}
|
||||
|
||||
public List<ResourcePolicy> getPolicies() {
|
||||
RealmModel realm = getRealm();
|
||||
return realm.getComponentsStream(realm.getId(), ResourcePolicyProvider.class.getName())
|
||||
.map(ResourcePolicy::new).toList();
|
||||
}
|
||||
|
||||
public List<ResourceAction> getActions(ResourcePolicy policy) {
|
||||
RealmModel realm = getRealm();
|
||||
return realm.getComponentsStream(policy.getId(), ResourceActionProvider.class.getName())
|
||||
.map(ResourceAction::new).sorted().toList();
|
||||
}
|
||||
|
||||
public void runPolicies() {
|
||||
List<ResourcePolicy> policies = getPolicies();
|
||||
|
||||
for (ResourcePolicy policy : policies) {
|
||||
runPolicy(policy);
|
||||
}
|
||||
}
|
||||
|
||||
private void runPolicy(ResourcePolicy policy) {
|
||||
log.tracev("Running policy {0}", policy.getProviderId());
|
||||
|
||||
// no actions -> skip
|
||||
List<ResourceAction> actions = getActions(policy);
|
||||
if (actions.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ResourcePolicyProvider policyProvider = getPolicyProvider(policy);
|
||||
ResourcePolicyStateProvider stateProvider = getResourcePolicyStateProvider();
|
||||
|
||||
// fetch all candidate lists for subsequent actions
|
||||
// need to load all candidates before creation a state record for initial action
|
||||
// if we don't do this, we risk executing more actions for single resource (user) in one run (in case the actions were modified by admin)
|
||||
Map<String, List<String>> candidatesForAction = new HashMap<>();
|
||||
for (int i = 1; i < actions.size(); i++) {
|
||||
ResourceAction previousAction = actions.get(i - 1);
|
||||
List<String> candidateIds = stateProvider.findResourceIdsByLastCompletedAction(policy.getId(), previousAction.getId());
|
||||
candidatesForAction.put(actions.get(i).getId(), candidateIds);
|
||||
}
|
||||
|
||||
// Process the Initial action (State Zero) - look for eligable users NOT present in the state table.
|
||||
ResourceAction initialAction = actions.get(0);
|
||||
ResourceActionProvider actionProvider = getActionProvider(initialAction);
|
||||
log.tracev("Initial action {0}", initialAction.getProviderId());
|
||||
|
||||
List<String> newResourceIds = policyProvider.getEligibleResourcesForInitialAction(initialAction.getAfter());
|
||||
log.tracev("Eligable resource IDs for initial action {0}", newResourceIds);
|
||||
// <comment> todo: do we want to wrap it into separate tx? So we have more granular approach for handling errors & possible retries??
|
||||
if (!newResourceIds.isEmpty()) {
|
||||
// run action
|
||||
runAction(actionProvider, newResourceIds);
|
||||
|
||||
// create state record
|
||||
stateProvider.update(policy.getId(), policy.getProviderId(), newResourceIds, initialAction.getId());
|
||||
}
|
||||
// </comment>
|
||||
|
||||
// Process the rest of the actions
|
||||
for (ResourceAction action : actions) {
|
||||
// Find all resources that have completed the PREVIOUS action.
|
||||
List<String> candidateIds = candidatesForAction.getOrDefault(action.getId(), Collections.emptyList());
|
||||
|
||||
if (candidateIds.isEmpty()) {
|
||||
continue; // No users are at this stage yet.
|
||||
}
|
||||
|
||||
// Ask the policyProvider to filter these candidates based on time.
|
||||
List<String> eligibleIds = policyProvider.filterEligibleResources(candidateIds, action.getAfter());
|
||||
|
||||
// <comment> todo: do we want to wrap it into separate tx? So we have more granular approach for handling errors & possible retries??
|
||||
if (!eligibleIds.isEmpty()) {
|
||||
// Get the action provider and run the action on the eligible users.
|
||||
actionProvider = getActionProvider(action);
|
||||
runAction(actionProvider, eligibleIds);
|
||||
|
||||
// Update the state for the users that were processed.
|
||||
stateProvider.update(policy.getId(), policy.getProviderId(), eligibleIds, action.getId());
|
||||
}
|
||||
// </comment>
|
||||
}
|
||||
}
|
||||
|
||||
private void runAction(ResourceActionProvider actionProvider, List<String> newResourceIds) {
|
||||
actionProvider.run(newResourceIds == null ? List.of() : newResourceIds);
|
||||
}
|
||||
|
||||
private ResourcePolicyProvider getPolicyProvider(ResourcePolicy policy) {
|
||||
ComponentFactory<?, ?> factory = (ComponentFactory<?, ?>) session.getKeycloakSessionFactory()
|
||||
.getProviderFactory(ResourcePolicyProvider.class, policy.getProviderId());
|
||||
return (ResourcePolicyProvider) factory.create(session, getRealm().getComponent(policy.getId()));
|
||||
}
|
||||
|
||||
private ResourceActionProvider getActionProvider(ResourceAction action) {
|
||||
ComponentFactory<?, ?> actionFactory = (ComponentFactory<?, ?>) session.getKeycloakSessionFactory()
|
||||
.getProviderFactory(ResourceActionProvider.class, action.getProviderId());
|
||||
return (ResourceActionProvider) actionFactory.create(session, getRealm().getComponent(action.getId()));
|
||||
}
|
||||
|
||||
private ResourcePolicyStateProvider getResourcePolicyStateProvider() {
|
||||
ProviderFactory<ResourcePolicyStateProvider> providerFactory = session.getKeycloakSessionFactory().getProviderFactory(ResourcePolicyStateProvider.class);
|
||||
return providerFactory.create(session);
|
||||
}
|
||||
|
||||
private RealmModel getRealm() {
|
||||
return session.getContext().getRealm();
|
||||
}
|
||||
|
||||
private void validateActions(List<ResourceAction> actions) {
|
||||
// the list should be in the desired priority order
|
||||
for (int i = 0; i < actions.size(); i++) {
|
||||
ResourceAction currentAction = actions.get(i);
|
||||
|
||||
// check that each action's duration is positive.
|
||||
if (currentAction.getAfter() <= 0) {
|
||||
throw new BadRequestException("Validation Error: 'after' duration must be positive.");
|
||||
}
|
||||
|
||||
if (i > 0) {// skip for initial action
|
||||
ResourceAction previousAction = actions.get(i - 1);
|
||||
// compare current with the previous action in the list
|
||||
if (currentAction.getAfter() < previousAction.getAfter()) {
|
||||
throw new BadRequestException(
|
||||
String.format("Validation Error: The 'after' duration for action #%d (%s) cannot be less than the duration of the preceding action #%d (%s).",
|
||||
i + 1, formatDuration(currentAction.getAfter()),
|
||||
i, formatDuration(previousAction.getAfter()))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String formatDuration(long millis) {
|
||||
long days = Duration.ofMillis(millis).toDays();
|
||||
if (days > 0) {
|
||||
return String.format("%d day(s)", days);
|
||||
} else {
|
||||
long hours = Duration.ofMillis(millis).toHours();
|
||||
return String.format("%d hour(s)", hours);
|
||||
}
|
||||
}
|
||||
|
||||
public void removePolicies() {
|
||||
RealmModel realm = getRealm();
|
||||
realm.getComponentsStream(realm.getId(), ResourcePolicyProvider.class.getName()).forEach(policy -> {
|
||||
realm.getComponentsStream(policy.getId(), ResourceActionProvider.class.getName()).forEach(realm::removeComponent);
|
||||
realm.removeComponent(policy);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright 2025 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.policy;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||
|
||||
public class UserActionBuilder {
|
||||
|
||||
private final ResourceAction action;
|
||||
|
||||
private UserActionBuilder(ResourceAction action) {
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
public static UserActionBuilder builder(String providerId) {
|
||||
ResourceAction action = new ResourceAction(providerId);
|
||||
return new UserActionBuilder(action);
|
||||
}
|
||||
|
||||
public ResourceAction build() {
|
||||
return action;
|
||||
}
|
||||
|
||||
public UserActionBuilder after(Duration duration) {
|
||||
action.setAfter(duration.toMillis());
|
||||
return this;
|
||||
}
|
||||
|
||||
public UserActionBuilder withConfig(String key, String value) {
|
||||
action.setConfig(key, value);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
org.keycloak.models.policy.DisableUserActionProviderFactory
|
||||
org.keycloak.models.policy.NotifyUserActionProviderFactory
|
||||
org.keycloak.models.policy.DeleteUserActionProviderFactory
|
||||
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright 2025 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.tests.admin.model.policy;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.policy.ResourceAction;
|
||||
import org.keycloak.models.policy.ResourcePolicy;
|
||||
import org.keycloak.models.policy.ResourcePolicyManager;
|
||||
|
||||
public class PolicyBuilder {
|
||||
|
||||
public static PolicyBuilder create() {
|
||||
return new PolicyBuilder();
|
||||
}
|
||||
|
||||
private String providerId;
|
||||
private Map<String, List<String>> config = new HashMap<>();
|
||||
private final Map<String, List<ResourceAction>> actions = new HashMap<>();
|
||||
|
||||
private PolicyBuilder() {
|
||||
}
|
||||
|
||||
public PolicyBuilder of(String providerId) {
|
||||
this.providerId = providerId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PolicyBuilder withActions(ResourceAction... actions) {
|
||||
this.actions.computeIfAbsent(providerId, (k) -> new ArrayList<>()).addAll(List.of(actions));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PolicyBuilder withConfig(String key, String value) {
|
||||
config.put(key, List.of(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PolicyBuilder withConfig(String key, List<String> value) {
|
||||
config.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResourcePolicyManager build(KeycloakSession session) {
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
|
||||
for (Entry<String, List<ResourceAction>> entry : actions.entrySet()) {
|
||||
ResourcePolicy policy = manager.addPolicy(entry.getKey(), config);
|
||||
manager.updateActions(policy, entry.getValue());
|
||||
}
|
||||
|
||||
return manager;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright 2025 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.tests.admin.model.policy;
|
||||
|
||||
import org.keycloak.common.Profile.Feature;
|
||||
import org.keycloak.testframework.server.KeycloakServerConfig;
|
||||
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
|
||||
|
||||
public class RLMServerConfig implements KeycloakServerConfig {
|
||||
|
||||
@Override
|
||||
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
|
||||
return config.features(Feature.RESOURCE_LIFECYCLE);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,363 @@
|
|||
/*
|
||||
* Copyright 2025 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.tests.admin.model.policy;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.connections.jpa.JpaConnectionProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserProvider;
|
||||
import org.keycloak.models.policy.DisableUserActionProviderFactory;
|
||||
import org.keycloak.models.policy.NotifyUserActionProviderFactory;
|
||||
import org.keycloak.models.policy.ResourceAction;
|
||||
import org.keycloak.models.policy.ResourcePolicy;
|
||||
import org.keycloak.models.policy.ResourcePolicyManager;
|
||||
import org.keycloak.models.policy.ResourcePolicyStateEntity;
|
||||
import org.keycloak.models.policy.ResourcePolicyStateProvider;
|
||||
import org.keycloak.models.policy.UserActionBuilder;
|
||||
import org.keycloak.models.policy.UserCreationDateResourcePolicyProviderFactory;
|
||||
import org.keycloak.models.policy.UserLastSessionRefreshTimeResourcePolicyProviderFactory;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.InjectUser;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.injection.LifeCycle;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.realm.ManagedUser;
|
||||
import org.keycloak.testframework.realm.UserConfig;
|
||||
import org.keycloak.testframework.realm.UserConfigBuilder;
|
||||
import org.keycloak.testframework.remote.runonserver.InjectRunOnServer;
|
||||
import org.keycloak.testframework.remote.runonserver.RunOnServerClient;
|
||||
|
||||
@KeycloakIntegrationTest(config = RLMServerConfig.class)
|
||||
public class ResourcePolicyManagementTest {
|
||||
|
||||
private static final String REALM_NAME = "default";
|
||||
|
||||
@InjectRunOnServer(permittedPackages = "org.keycloak.tests")
|
||||
RunOnServerClient runOnServer;
|
||||
|
||||
@InjectRealm(lifecycle = LifeCycle.METHOD)
|
||||
ManagedRealm managedRealm;
|
||||
|
||||
@InjectUser(ref = "alice", config = DefaultUserConfig.class, lifecycle = LifeCycle.METHOD)
|
||||
private ManagedUser userAlice;
|
||||
|
||||
@Test
|
||||
public void testCreatePolicy() {
|
||||
runOnServer.run(session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
|
||||
ResourcePolicy created = manager.addPolicy(new ResourcePolicy(UserCreationDateResourcePolicyProviderFactory.ID));
|
||||
assertNotNull(created.getId());
|
||||
|
||||
List<ResourcePolicy> policies = manager.getPolicies();
|
||||
|
||||
assertEquals(1, policies.size());
|
||||
|
||||
ResourcePolicy policy = policies.get(0);
|
||||
|
||||
assertNotNull(policy.getId());
|
||||
assertEquals(created.getId(), policy.getId());
|
||||
assertNotNull(realm.getComponent(policy.getId()));
|
||||
assertEquals(UserCreationDateResourcePolicyProviderFactory.ID, policy.getProviderId());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateAction() {
|
||||
runOnServer.run(session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
ResourcePolicy policy = manager.addPolicy(new ResourcePolicy(UserCreationDateResourcePolicyProviderFactory.ID));
|
||||
|
||||
int expectedActionsSize = 5;
|
||||
|
||||
List<ResourceAction> expectedActions = new ArrayList<>();
|
||||
for (int i = 0; i < expectedActionsSize; i++) {
|
||||
expectedActions.add(UserActionBuilder.builder(DisableUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(i + 1))
|
||||
.build());
|
||||
}
|
||||
manager.updateActions(policy, expectedActions);
|
||||
|
||||
List<ResourceAction> actions = manager.getActions(policy);
|
||||
|
||||
assertEquals(expectedActionsSize, actions.size());
|
||||
|
||||
ResourceAction action = actions.get(0);
|
||||
|
||||
assertNotNull(action.getId());
|
||||
assertNotNull(realm.getComponent(action.getId()));
|
||||
assertEquals(DisableUserActionProviderFactory.ID, action.getProviderId());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled("We need to flush component removals to make this test pass. For that, we need to evaluate a flush as per the TODO in the body of this method")
|
||||
public void testDeleteActionResetsOrphanedState() {
|
||||
//TODO: Evaluate and change org.keycloak.models.jpa.RealmAdapter.removeComponents to flush changes in the persistence context:
|
||||
// if (getEntity().getComponents().removeIf(sameParent)) {
|
||||
// em.flush();
|
||||
// }
|
||||
|
||||
runOnServer.run(session -> {
|
||||
EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
||||
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
ResourcePolicyStateProvider stateProvider = session.getProvider(ResourcePolicyStateProvider.class);
|
||||
UserModel user = session.users().addUser(realm, "test");
|
||||
|
||||
// Create a policy with two actions
|
||||
ResourcePolicy policy = manager.addPolicy(new ResourcePolicy(UserCreationDateResourcePolicyProviderFactory.ID));
|
||||
ResourceAction notify = UserActionBuilder.builder(NotifyUserActionProviderFactory.ID).after(Duration.ofDays(5)).build();
|
||||
ResourceAction disable = UserActionBuilder.builder(DisableUserActionProviderFactory.ID).after(Duration.ofDays(10)).build();
|
||||
manager.updateActions(policy, List.of(notify, disable));
|
||||
|
||||
// Get the created actions to access their IDs
|
||||
List<ResourceAction> createdActions = manager.getActions(policy);
|
||||
ResourceAction createdNotifyAction = createdActions.get(0);
|
||||
ResourceAction createdDisableAction = createdActions.get(1);
|
||||
|
||||
// --- SIMULATE USER PROGRESS ---
|
||||
// Manually set the user's state to have completed 'notify'
|
||||
stateProvider.update(policy.getId(), policy.getProviderId(), List.of(user.getId()), createdNotifyAction.getId());
|
||||
ResourcePolicyStateEntity.PrimaryKey pk = new ResourcePolicyStateEntity.PrimaryKey(user.getId(), policy.getId());
|
||||
assertNotNull(em.find(ResourcePolicyStateEntity.class, pk), "State should exist before the update.");
|
||||
|
||||
// Admin deletes 'notify' by updating the policy with only 'disable'
|
||||
manager.updateActions(policy, List.of(createdDisableAction));
|
||||
|
||||
//need to flush and clear the persistance context cache to get correct result in next call
|
||||
em.flush();
|
||||
em.clear();
|
||||
|
||||
// The user's state record should have been deleted because its last_completed_action (notify) no longer exists.
|
||||
assertNull(em.find(ResourcePolicyStateEntity.class, pk), "State record should be deleted when its completed action is removed.");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPolicyDoesNotFallThroughActionsInSingleRun() {
|
||||
runOnServer.run(session -> {
|
||||
EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
||||
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
UserModel user = session.users().addUser(realm, "testuser");
|
||||
user.setEnabled(true);
|
||||
|
||||
// Create a policy with notify (5 days) and disable (10 days) actions
|
||||
ResourcePolicy policy = manager.addPolicy(new ResourcePolicy(UserCreationDateResourcePolicyProviderFactory.ID));
|
||||
ResourceAction notifyAction = UserActionBuilder.builder(NotifyUserActionProviderFactory.ID).after(Duration.ofDays(5)).build();
|
||||
ResourceAction disableAction = UserActionBuilder.builder(DisableUserActionProviderFactory.ID).after(Duration.ofDays(10)).build();
|
||||
manager.updateActions(policy, List.of(notifyAction, disableAction));
|
||||
|
||||
ResourceAction createdNotifyAction = manager.getActions(policy).get(0);
|
||||
|
||||
try {
|
||||
// Simulate the user being 12 days old, making them eligible for both actions' time conditions.
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(12).toSeconds()));
|
||||
manager.runPolicies();
|
||||
|
||||
user = session.users().getUserById(realm, user.getId());
|
||||
|
||||
// Verify that ONLY the first action (notify) was executed.
|
||||
assertNotNull(user.getAttributes().get("message"), "The first action (notify) should have run.");
|
||||
assertTrue(user.isEnabled(), "The second action (disable) should NOT have run.");
|
||||
|
||||
// Verify that the user's state is correctly paused after the first action.
|
||||
ResourcePolicyStateEntity.PrimaryKey pk = new ResourcePolicyStateEntity.PrimaryKey(user.getId(), policy.getId());
|
||||
ResourcePolicyStateEntity state = em.find(ResourcePolicyStateEntity.class, pk);
|
||||
|
||||
assertNotNull(state, "A state record should have been created for the user.");
|
||||
assertEquals(createdNotifyAction.getId(), state.getLastCompletedActionId(), "The user's state should be at the first action.");
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTimeVsPriorityConflictingActions() {
|
||||
runOnServer.run(session -> {
|
||||
configureSessionContext(session);
|
||||
ResourcePolicyManager manager = new ResourcePolicyManager(session);
|
||||
ResourcePolicy policy = manager.addPolicy(UserCreationDateResourcePolicyProviderFactory.ID);
|
||||
|
||||
ResourceAction action1 = UserActionBuilder.builder(DisableUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(10))
|
||||
.build();
|
||||
|
||||
ResourceAction action2 = UserActionBuilder.builder(DisableUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build();
|
||||
|
||||
try {
|
||||
manager.updateActions(policy, List.of(action1, action2));
|
||||
fail("Expected exception was not thrown");
|
||||
} catch (BadRequestException expected) {}
|
||||
});
|
||||
}
|
||||
|
||||
@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);
|
||||
return realm;
|
||||
}
|
||||
|
||||
private static class DefaultUserConfig implements UserConfig {
|
||||
|
||||
@Override
|
||||
public UserConfigBuilder configure(UserConfigBuilder user) {
|
||||
user.username("alice");
|
||||
user.password("alice");
|
||||
user.name("alice", "alice");
|
||||
user.email("master-admin@email.org");
|
||||
return user;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.tests.admin.model.policy;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.admin.client.resource.UsersResource;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.policy.DeleteUserActionProviderFactory;
|
||||
import org.keycloak.models.policy.FederatedIdentityPolicyProviderFactory;
|
||||
import org.keycloak.models.policy.ResourcePolicyManager;
|
||||
import org.keycloak.models.policy.UserActionBuilder;
|
||||
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testframework.annotations.InjectClient;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.InjectUser;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.injection.LifeCycle;
|
||||
import org.keycloak.testframework.oauth.OAuthClient;
|
||||
import org.keycloak.testframework.oauth.annotations.InjectOAuthClient;
|
||||
import org.keycloak.testframework.realm.ClientConfig;
|
||||
import org.keycloak.testframework.realm.ClientConfigBuilder;
|
||||
import org.keycloak.testframework.realm.ManagedClient;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.realm.ManagedUser;
|
||||
import org.keycloak.testframework.realm.RealmConfig;
|
||||
import org.keycloak.testframework.realm.RealmConfigBuilder;
|
||||
import org.keycloak.testframework.realm.UserConfig;
|
||||
import org.keycloak.testframework.realm.UserConfigBuilder;
|
||||
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.ConsentPage;
|
||||
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 {
|
||||
|
||||
private static final String REALM_NAME = "consumer";
|
||||
|
||||
@InjectRunOnServer(permittedPackages = "org.keycloak.tests")
|
||||
RunOnServerClient runOnServer;
|
||||
|
||||
@InjectRealm(ref = "consumer", config = ConsumerRealmConf.class, lifecycle = LifeCycle.METHOD)
|
||||
ManagedRealm consumerRealm;
|
||||
|
||||
@InjectRealm(ref = "provider", lifecycle = LifeCycle.METHOD)
|
||||
ManagedRealm providerRealm;
|
||||
|
||||
@InjectUser(ref = "provider", realmRef = "provider", config = ProviderRealmUserConf.class)
|
||||
ManagedUser userFromProviderRealm;
|
||||
|
||||
@InjectOAuthClient(ref = "consumer", realmRef = "consumer")
|
||||
OAuthClient consumerRealmOAuth;
|
||||
|
||||
@InjectClient(realmRef = "provider", config = ProviderRealmClientConf.class)
|
||||
ManagedClient providerRealmClient;
|
||||
|
||||
@InjectWebDriver
|
||||
WebDriver driver;
|
||||
|
||||
@InjectPage
|
||||
LoginPage loginPage;
|
||||
|
||||
@InjectPage
|
||||
ConsentPage consentPage;
|
||||
|
||||
private static final String REALM_PROV_NAME = "provider";
|
||||
private static final String REALM_CONS_NAME = "consumer";
|
||||
|
||||
private static final String IDP_OIDC_ALIAS = "kc-oidc-idp";
|
||||
private static final String IDP_OIDC_PROVIDER_ID = "keycloak-oidc";
|
||||
|
||||
private static final String CLIENT_ID = "brokerapp";
|
||||
private static final String CLIENT_SECRET = "secret";
|
||||
|
||||
@Test
|
||||
public void tesRunActionOnFederatedUser() {
|
||||
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.submit();
|
||||
consentPage.waitForPage();
|
||||
consentPage.assertCurrent();
|
||||
consentPage.confirm();
|
||||
assertTrue(driver.getPageSource().contains("Happy days"), "Test user should be successfully logged in.");
|
||||
|
||||
UsersResource users = consumerRealm.admin().users();
|
||||
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);
|
||||
|
||||
manager.runPolicies();
|
||||
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();
|
||||
user = session.users().getUserByUsername(realm, username);
|
||||
assertNull(user);
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private static IdentityProviderRepresentation setUpIdentityProvider() {
|
||||
IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID);
|
||||
|
||||
Map<String, String> config = idp.getConfig();
|
||||
|
||||
config.put("clientId", CLIENT_ID);
|
||||
config.put("clientSecret", CLIENT_SECRET);
|
||||
config.put("prompt", "login");
|
||||
config.put("authorizationUrl", "http://localhost:8080/realms/" + REALM_PROV_NAME + "/protocol/openid-connect/auth");
|
||||
config.put("tokenUrl", "http://localhost:8080/realms/" + REALM_PROV_NAME + "/protocol/openid-connect/token");
|
||||
config.put("logoutUrl", "http://localhost:8080/realms/" + REALM_PROV_NAME + "/protocol/openid-connect/logout");
|
||||
config.put("userInfoUrl", "http://localhost:8080/realms/" + REALM_PROV_NAME + "/protocol/openid-connect/userinfo");
|
||||
config.put("defaultScope", "email profile");
|
||||
config.put("backchannelSupported", "true");
|
||||
|
||||
return idp;
|
||||
}
|
||||
|
||||
private static IdentityProviderRepresentation createIdentityProvider(String alias, String providerId) {
|
||||
IdentityProviderRepresentation identityProviderRepresentation = new IdentityProviderRepresentation();
|
||||
|
||||
identityProviderRepresentation.setAlias(alias);
|
||||
identityProviderRepresentation.setDisplayName(providerId);
|
||||
identityProviderRepresentation.setProviderId(providerId);
|
||||
identityProviderRepresentation.setEnabled(true);
|
||||
|
||||
return identityProviderRepresentation;
|
||||
}
|
||||
|
||||
private static class ProviderRealmUserConf implements UserConfig {
|
||||
|
||||
@Override
|
||||
public UserConfigBuilder configure(UserConfigBuilder builder) {
|
||||
builder.username("provider");
|
||||
builder.password("password");
|
||||
builder.email("provider@local");
|
||||
builder.emailVerified(true);
|
||||
builder.name("Provider", "User");
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ProviderRealmClientConf implements ClientConfig {
|
||||
|
||||
@Override
|
||||
public ClientConfigBuilder configure(ClientConfigBuilder builder) {
|
||||
builder.clientId(CLIENT_ID);
|
||||
builder.name(CLIENT_ID);
|
||||
builder.secret(CLIENT_SECRET);
|
||||
builder.consentRequired(true);
|
||||
builder.redirectUris( "http://localhost:8080/realms/" + REALM_CONS_NAME + "/broker/" + IDP_OIDC_ALIAS + "/endpoint/*");
|
||||
builder.adminUrl("http://localhost:8080/realms/" + REALM_CONS_NAME + "/broker/" + IDP_OIDC_ALIAS + "/endpoint");
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ConsumerRealmConf implements RealmConfig {
|
||||
|
||||
@Override
|
||||
public RealmConfigBuilder configure(RealmConfigBuilder builder) {
|
||||
builder.identityProvider(setUpIdentityProvider());
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
private static RealmModel configureSessionContext(KeycloakSession session) {
|
||||
RealmModel realm = session.realms().getRealmByName(REALM_NAME);
|
||||
session.getContext().setRealm(realm);
|
||||
return realm;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
/*
|
||||
* Copyright 2025 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.tests.admin.model.policy;
|
||||
|
||||
import static org.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.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.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.testframework.annotations.InjectUser;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.injection.LifeCycle;
|
||||
import org.keycloak.testframework.oauth.OAuthClient;
|
||||
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)
|
||||
public class UserSessionRefreshTimePolicyTest {
|
||||
|
||||
private static final String REALM_NAME = "default";
|
||||
|
||||
@InjectRunOnServer(permittedPackages = "org.keycloak.tests")
|
||||
RunOnServerClient runOnServer;
|
||||
|
||||
@InjectUser(ref = "alice", config = DefaultUserConfig.class, lifecycle = LifeCycle.METHOD)
|
||||
private ManagedUser userAlice;
|
||||
|
||||
@InjectWebDriver
|
||||
WebDriver driver;
|
||||
|
||||
@InjectPage
|
||||
LoginPage loginPage;
|
||||
|
||||
@InjectOAuthClient
|
||||
OAuthClient oauth;
|
||||
|
||||
@BeforeEach
|
||||
public void onBefore() {
|
||||
oauth.realm("default");
|
||||
}
|
||||
|
||||
@Test
|
||||
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(
|
||||
UserActionBuilder.builder(NotifyUserActionProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.build(),
|
||||
UserActionBuilder.builder(DisableUserActionProviderFactory.ID)
|
||||
.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());
|
||||
assertTrue(user.isEnabled());
|
||||
assertNull(user.getAttributes().get("message"));
|
||||
|
||||
manager.runPolicies();
|
||||
user = session.users().getUserByUsername(realm, "alice");
|
||||
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");
|
||||
assertTrue(user.isEnabled());
|
||||
assertNotNull(user.getAttributes().get("message"));
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
|
||||
try {
|
||||
entity.setLastSessionRefreshTime(Math.toIntExact(Time.currentTime() + Duration.ofDays(11).toSeconds()));
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(11).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(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");
|
||||
assertFalse(user.isEnabled());
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@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());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateUserLastRefreshTimeOnRefreshToken() {
|
||||
AccessTokenResponse tokenResponse = oauth.doPasswordGrantRequest("alice", "alice");
|
||||
assertNotNull(tokenResponse);
|
||||
|
||||
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);
|
||||
|
||||
assertNotNull(tokenResponse.getRefreshToken());
|
||||
|
||||
try {
|
||||
runOnServer.run((session) -> {
|
||||
Time.setOffset(Math.toIntExact(Duration.ofMinutes(10).toSeconds()));
|
||||
});
|
||||
|
||||
oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
|
||||
} 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());
|
||||
});
|
||||
}
|
||||
|
||||
private static RealmModel configureSessionContext(KeycloakSession session) {
|
||||
RealmModel realm = session.realms().getRealmByName(REALM_NAME);
|
||||
session.getContext().setRealm(realm);
|
||||
return realm;
|
||||
}
|
||||
|
||||
private static class DefaultUserConfig implements UserConfig {
|
||||
|
||||
@Override
|
||||
public UserConfigBuilder configure(UserConfigBuilder user) {
|
||||
user.username("alice");
|
||||
user.password("alice");
|
||||
user.name("alice", "alice");
|
||||
user.email("master-admin@email.org");
|
||||
return user;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue