diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/RealmDAO.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/RealmDAO.java index e05eb840b57..a0a573caaa1 100644 --- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/RealmDAO.java +++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/RealmDAO.java @@ -45,6 +45,8 @@ public interface RealmDAO extends DAO { List findDescendants(String base, String keyword, int page, int itemsPerPage); + List findDescendants(String base, String prefix); + List findByPolicy(T policy); List findByLogicActions(Implementation logicActions); @@ -53,6 +55,10 @@ public interface RealmDAO extends DAO { List findChildren(Realm realm); + int count(); + + List findAllKeys(int page, int itemsPerPage); + Realm save(Realm realm); void delete(Realm realm); diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java index b6d4c43a85e..aee5fbfe43e 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java @@ -605,8 +605,8 @@ public PolicyDAO policyDAO( @ConditionalOnMissingBean @Bean - public RealmDAO realmDAO(final @Lazy RoleDAO roleDAO) { - return new JPARealmDAO(roleDAO); + public RealmDAO realmDAO(final @Lazy RoleDAO roleDAO, final ApplicationEventPublisher publisher) { + return new JPARealmDAO(roleDAO, publisher); } @ConditionalOnMissingBean diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAnyObjectDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAnyObjectDAO.java index 4e219060ab0..50d98e608f6 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAnyObjectDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAnyObjectDAO.java @@ -147,7 +147,7 @@ public OffsetDateTime findLastChange(final String key) { @Override public Map countByType() { Query query = entityManager().createQuery( - "SELECT e.type, COUNT(e) AS countByType FROM " + anyUtils().anyClass().getSimpleName() + " e " + "SELECT e.type, COUNT(e) AS countByType FROM " + anyUtils().anyClass().getSimpleName() + " e " + "GROUP BY e.type ORDER BY countByType DESC"); @SuppressWarnings("unchecked") List results = query.getResultList(); @@ -161,7 +161,7 @@ public Map countByType() { @Override public Map countByRealm(final AnyType anyType) { Query query = entityManager().createQuery( - "SELECT e.realm, COUNT(e) FROM " + anyUtils().anyClass().getSimpleName() + " e " + "SELECT e.realm, COUNT(e) FROM " + anyUtils().anyClass().getSimpleName() + " e " + "WHERE e.type=:type GROUP BY e.realm"); query.setParameter("type", anyType); @@ -240,14 +240,14 @@ public List, AnyObject>> findAllRelationships(final AnyObjec @Override public int count() { Query query = entityManager().createQuery( - "SELECT COUNT(e) FROM " + anyUtils().anyClass().getSimpleName() + " e"); + "SELECT COUNT(e) FROM " + anyUtils().anyClass().getSimpleName() + " e"); return ((Number) query.getSingleResult()).intValue(); } @Override public List findAll(final int page, final int itemsPerPage) { TypedQuery query = entityManager().createQuery( - "SELECT e FROM " + anyUtils().anyClass().getSimpleName() + " e ORDER BY e.id", AnyObject.class); + "SELECT e FROM " + anyUtils().anyClass().getSimpleName() + " e ORDER BY e.id", AnyObject.class); query.setFirstResult(itemsPerPage * (page <= 0 ? 0 : page - 1)); query.setMaxResults(itemsPerPage); diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAnySearchDAO.java index 4e6d29aeb2c..b069639bdab 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAnySearchDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAnySearchDAO.java @@ -138,10 +138,7 @@ protected Triple, Set> getAdminRealmsFilter( return noRealm; }); - realmKeys.addAll( - realmDAO.findDescendants(realm.getFullPath(), null, -1, -1).stream(). - filter(r -> r.getFullPath().startsWith(base.getFullPath())). - map(Realm::getKey).collect(Collectors.toSet())); + realmKeys.addAll(realmDAO.findDescendants(realm.getFullPath(), base.getFullPath())); } else { DynRealm dynRealm = dynRealmDAO.find(realmPath); if (dynRealm == null) { diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPADynRealmDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPADynRealmDAO.java index 716c3dc829e..de05bcb6e54 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPADynRealmDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPADynRealmDAO.java @@ -33,7 +33,7 @@ import org.apache.syncope.core.persistence.api.search.SearchCondConverter; import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; import org.apache.syncope.core.persistence.jpa.entity.JPADynRealm; -import org.apache.syncope.core.provisioning.api.event.AnyLifecycleEvent; +import org.apache.syncope.core.provisioning.api.event.EntityLifecycleEvent; import org.apache.syncope.core.spring.security.AuthContextUtils; import org.identityconnectors.framework.common.objects.SyncDeltaType; import org.springframework.context.ApplicationEventPublisher; @@ -122,7 +122,7 @@ protected void notifyDynMembershipRemoval(final List anyKeys) { } if (any != null) { publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, any, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, any, AuthContextUtils.getDomain())); } }); } @@ -144,7 +144,7 @@ public DynRealm saveAndRefreshDynMemberships(final DynRealm dynRealm) { insert.executeUpdate(); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, any, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, any, AuthContextUtils.getDomain())); cleared.remove(any.getKey()); })); diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAGroupDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAGroupDAO.java index bdb3d9da967..27f7b479bad 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAGroupDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAGroupDAO.java @@ -65,7 +65,7 @@ import org.apache.syncope.core.persistence.jpa.entity.group.JPATypeExtension; import org.apache.syncope.core.persistence.jpa.entity.user.JPAUDynGroupMembership; import org.apache.syncope.core.persistence.jpa.entity.user.JPAUMembership; -import org.apache.syncope.core.provisioning.api.event.AnyLifecycleEvent; +import org.apache.syncope.core.provisioning.api.event.EntityLifecycleEvent; import org.apache.syncope.core.provisioning.api.utils.RealmUtils; import org.apache.syncope.core.spring.security.AuthContextUtils; import org.apache.syncope.core.spring.security.DelegatedAdministrationException; @@ -147,14 +147,14 @@ public OffsetDateTime findLastChange(final String key) { @Override public int count() { Query query = entityManager().createQuery( - "SELECT COUNT(e) FROM " + anyUtils().anyClass().getSimpleName() + " e"); + "SELECT COUNT(e) FROM " + anyUtils().anyClass().getSimpleName() + " e"); return ((Number) query.getSingleResult()).intValue(); } @Override public Map countByRealm() { Query query = entityManager().createQuery( - "SELECT e.realm, COUNT(e) FROM " + anyUtils().anyClass().getSimpleName() + " e GROUP BY e.realm"); + "SELECT e.realm, COUNT(e) FROM " + anyUtils().anyClass().getSimpleName() + " e GROUP BY e.realm"); @SuppressWarnings("unchecked") List results = query.getResultList(); @@ -277,7 +277,7 @@ public List findUMemberships(final Group group) { @Override public List findAll(final int page, final int itemsPerPage) { TypedQuery query = entityManager().createQuery( - "SELECT e FROM " + anyUtils().anyClass().getSimpleName() + " e ORDER BY e.id", Group.class); + "SELECT e FROM " + anyUtils().anyClass().getSimpleName() + " e ORDER BY e.id", Group.class); query.setFirstResult(itemsPerPage * (page <= 0 ? 0 : page - 1)); query.setMaxResults(itemsPerPage); @@ -321,7 +321,7 @@ public Group saveAndRefreshDynMemberships(final Group group) { insert.executeUpdate(); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, user, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, user, AuthContextUtils.getDomain())); }); } } @@ -350,7 +350,7 @@ public Group saveAndRefreshDynMemberships(final Group group) { insert.executeUpdate(); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, any, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, any, AuthContextUtils.getDomain())); }); } }); @@ -377,7 +377,7 @@ public void delete(final Group group) { anyObjectDAO.save(leftEnd); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, leftEnd, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, leftEnd, AuthContextUtils.getDomain())); }); findUMemberships(group).forEach(membership -> { @@ -394,7 +394,7 @@ public void delete(final Group group) { userDAO.save(leftEnd); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, leftEnd, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, leftEnd, AuthContextUtils.getDomain())); }); clearUDynMembers(group); @@ -582,8 +582,8 @@ public Pair, Set> refreshDynMemberships(final AnyObject anyO delete.executeUpdate(); } - publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, memb.getGroup(), AuthContextUtils.getDomain())); + publisher.publishEvent(new EntityLifecycleEvent<>( + this, SyncDeltaType.UPDATE, memb.getGroup(), AuthContextUtils.getDomain())); }); return Pair.of(before, after); @@ -601,8 +601,8 @@ public Set removeDynMemberships(final AnyObject anyObject) { dynGroups.forEach(group -> { before.add(group.getKey()); - publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, group, AuthContextUtils.getDomain())); + publisher.publishEvent(new EntityLifecycleEvent<>( + this, SyncDeltaType.UPDATE, group, AuthContextUtils.getDomain())); }); return before; @@ -680,8 +680,8 @@ public Pair, Set> refreshDynMemberships(final User user) { delete.executeUpdate(); } - publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, memb.getGroup(), AuthContextUtils.getDomain())); + publisher.publishEvent(new EntityLifecycleEvent<>( + this, SyncDeltaType.UPDATE, memb.getGroup(), AuthContextUtils.getDomain())); }); return Pair.of(before, after); @@ -699,8 +699,8 @@ public Set removeDynMemberships(final User user) { dynGroups.forEach(group -> { before.add(group.getKey()); - publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, group, AuthContextUtils.getDomain())); + publisher.publishEvent(new EntityLifecycleEvent<>( + this, SyncDeltaType.UPDATE, group, AuthContextUtils.getDomain())); }); return before; diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmDAO.java index 7f37732fc93..8bc604440d6 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmDAO.java @@ -23,6 +23,7 @@ import jakarta.persistence.TypedQuery; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.apache.syncope.common.lib.SyncopeConstants; import org.apache.syncope.core.persistence.api.dao.MalformedPathException; @@ -41,14 +42,21 @@ import org.apache.syncope.core.persistence.api.entity.policy.ProvisioningPolicy; import org.apache.syncope.core.persistence.api.entity.policy.TicketExpirationPolicy; import org.apache.syncope.core.persistence.jpa.entity.JPARealm; +import org.apache.syncope.core.provisioning.api.event.EntityLifecycleEvent; +import org.apache.syncope.core.spring.security.AuthContextUtils; +import org.identityconnectors.framework.common.objects.SyncDeltaType; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.transaction.annotation.Transactional; public class JPARealmDAO extends AbstractDAO implements RealmDAO { protected final RoleDAO roleDAO; - public JPARealmDAO(final RoleDAO roleDAO) { + protected final ApplicationEventPublisher publisher; + + public JPARealmDAO(final RoleDAO roleDAO, final ApplicationEventPublisher publisher) { this.roleDAO = roleDAO; + this.publisher = publisher; } @Override @@ -183,6 +191,27 @@ public List findDescendants( return query.getResultList(); } + @Override + public List findDescendants(final String base, final String prefix) { + List parameters = new ArrayList<>(); + + StringBuilder queryString = buildDescendantQuery(base, null, parameters); + TypedQuery query = entityManager().createQuery(queryString. + append(" AND (e.fullPath=?"). + append(setParameter(parameters, prefix)). + append(" OR e.fullPath LIKE ?"). + append(setParameter(parameters, SyncopeConstants.ROOT_REALM.equals(prefix) ? "/%" : prefix + "/%")). + append(')'). + append(" ORDER BY e.fullPath").toString(), + Realm.class); + + for (int i = 1; i <= parameters.size(); i++) { + query.setParameter(i, parameters.get(i - 1)); + } + + return query.getResultList().stream().map(Realm::getKey).collect(Collectors.toList()); + } + protected List findSamePolicyChildren(final Realm realm, final T policy) { List result = new ArrayList<>(); @@ -284,6 +313,30 @@ protected String buildFullPath(final Realm realm) { : StringUtils.appendIfMissing(realm.getParent().getFullPath(), "/") + realm.getName(); } + @Override + public int count() { + Query query = entityManager().createNativeQuery( + "SELECT COUNT(id) FROM " + JPARealm.TABLE); + return ((Number) query.getSingleResult()).intValue(); + } + + @Override + public List findAllKeys(final int page, final int itemsPerPage) { + Query query = entityManager().createNativeQuery("SELECT id FROM " + JPARealm.TABLE + " ORDER BY fullPath"); + + query.setFirstResult(itemsPerPage * (page <= 0 ? 0 : page - 1)); + + if (itemsPerPage > 0) { + query.setMaxResults(itemsPerPage); + } + + @SuppressWarnings("unchecked") + List raw = query.getResultList(); + return raw.stream().map(key -> key instanceof Object[] + ? (String) ((Object[]) key)[0] + : ((String) key)).collect(Collectors.toList()); + } + @Override public Realm save(final Realm realm) { String fullPathBefore = realm.getFullPath(); @@ -298,6 +351,9 @@ public Realm save(final Realm realm) { findChildren(realm).forEach(this::save); } + publisher.publishEvent( + new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, merged, AuthContextUtils.getDomain())); + return merged; } @@ -313,6 +369,9 @@ public void delete(final Realm realm) { toBeDeleted.setParent(null); entityManager().remove(toBeDeleted); + + publisher.publishEvent( + new EntityLifecycleEvent<>(this, SyncDeltaType.DELETE, toBeDeleted, AuthContextUtils.getDomain())); }); } } diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARoleDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARoleDAO.java index 2064b8f6bba..ee91ccb6f51 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARoleDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARoleDAO.java @@ -35,7 +35,7 @@ import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; import org.apache.syncope.core.persistence.jpa.entity.JPARole; import org.apache.syncope.core.persistence.jpa.entity.user.JPAUser; -import org.apache.syncope.core.provisioning.api.event.AnyLifecycleEvent; +import org.apache.syncope.core.provisioning.api.event.EntityLifecycleEvent; import org.apache.syncope.core.spring.security.AuthContextUtils; import org.identityconnectors.framework.common.objects.SyncDeltaType; import org.springframework.context.ApplicationEventPublisher; @@ -129,7 +129,7 @@ public Role saveAndRefreshDynMemberships(final Role role) { insert.executeUpdate(); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, user, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, user, AuthContextUtils.getDomain())); }); } @@ -145,7 +145,7 @@ public void delete(final Role role) { query.getResultList().forEach(user -> { user.getRoles().remove(role); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, user, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, user, AuthContextUtils.getDomain())); }); clearDynMembers(role); diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAUserDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAUserDAO.java index ed40bb3cf78..61dfa5e631b 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAUserDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAUserDAO.java @@ -147,14 +147,14 @@ public Optional findUsername(final String key) { @Override public int count() { Query query = entityManager().createQuery( - "SELECT COUNT(e) FROM " + anyUtils().anyClass().getSimpleName() + " e"); + "SELECT COUNT(e) FROM " + anyUtils().anyClass().getSimpleName() + " e"); return ((Number) query.getSingleResult()).intValue(); } @Override public Map countByRealm() { Query query = entityManager().createQuery( - "SELECT e.realm, COUNT(e) FROM " + anyUtils().anyClass().getSimpleName() + " e GROUP BY e.realm"); + "SELECT e.realm, COUNT(e) FROM " + anyUtils().anyClass().getSimpleName() + " e GROUP BY e.realm"); @SuppressWarnings("unchecked") List results = query.getResultList(); @@ -166,7 +166,7 @@ public Map countByRealm() { @Override public Map countByStatus() { Query query = entityManager().createQuery( - "SELECT e.status, COUNT(e) FROM " + anyUtils().anyClass().getSimpleName() + " e GROUP BY e.status"); + "SELECT e.status, COUNT(e) FROM " + anyUtils().anyClass().getSimpleName() + " e GROUP BY e.status"); @SuppressWarnings("unchecked") List results = query.getResultList(); @@ -270,7 +270,7 @@ public UMembership findMembership(final String key) { @Override public List findAll(final int page, final int itemsPerPage) { TypedQuery query = entityManager().createQuery( - "SELECT e FROM " + anyUtils().anyClass().getSimpleName() + " e ORDER BY e.id", User.class); + "SELECT e FROM " + anyUtils().anyClass().getSimpleName() + " e ORDER BY e.id", User.class); query.setFirstResult(itemsPerPage * (page <= 0 ? 0 : page - 1)); query.setMaxResults(itemsPerPage); diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/event/AnyLifecycleEvent.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/event/EntityLifecycleEvent.java similarity index 78% rename from core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/event/AnyLifecycleEvent.java rename to core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/event/EntityLifecycleEvent.java index 8fbc75f1c34..faab2acc765 100644 --- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/event/AnyLifecycleEvent.java +++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/event/EntityLifecycleEvent.java @@ -18,25 +18,25 @@ */ package org.apache.syncope.core.provisioning.api.event; -import org.apache.syncope.core.persistence.api.entity.Any; +import org.apache.syncope.core.persistence.api.entity.Entity; import org.identityconnectors.framework.common.objects.SyncDeltaType; import org.springframework.context.ApplicationEvent; -public class AnyLifecycleEvent> extends ApplicationEvent { +public class EntityLifecycleEvent extends ApplicationEvent { private static final long serialVersionUID = -781747175059834365L; private final SyncDeltaType type; - private final A any; + private final E entity; private final String domain; - public AnyLifecycleEvent(final Object source, final SyncDeltaType type, final A any, final String domain) { + public EntityLifecycleEvent(final Object source, final SyncDeltaType type, final E entity, final String domain) { super(source); this.type = type; - this.any = any; + this.entity = entity; this.domain = domain; } @@ -44,8 +44,8 @@ public SyncDeltaType getType() { return type; } - public A getAny() { - return any; + public E getEntity() { + return entity; } public String getDomain() { diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/AbstractPropagationTaskExecutor.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/AbstractPropagationTaskExecutor.java index a080189dd88..a20aa19cc35 100644 --- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/AbstractPropagationTaskExecutor.java +++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/AbstractPropagationTaskExecutor.java @@ -58,7 +58,7 @@ import org.apache.syncope.core.provisioning.api.ConnectorManager; import org.apache.syncope.core.provisioning.api.TimeoutException; import org.apache.syncope.core.provisioning.api.data.TaskDataBinder; -import org.apache.syncope.core.provisioning.api.event.AnyLifecycleEvent; +import org.apache.syncope.core.provisioning.api.event.EntityLifecycleEvent; import org.apache.syncope.core.provisioning.api.notification.NotificationManager; import org.apache.syncope.core.provisioning.api.propagation.PropagationActions; import org.apache.syncope.core.provisioning.api.propagation.PropagationManager; @@ -236,7 +236,7 @@ protected Uid doCreate( taskInfo.getEntityKey(), plainSchemaDAO.find(provision.getUidOnCreate()), result.getUidValue()); - publisher.publishEvent(new AnyLifecycleEvent<>( + publisher.publishEvent(new EntityLifecycleEvent<>( this, SyncDeltaType.UPDATE, anyUtils.dao().find(taskInfo.getEntityKey()), diff --git a/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/AbstractAnyObjectWorkflowAdapter.java b/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/AbstractAnyObjectWorkflowAdapter.java index a5cf36a090e..35158dca12d 100644 --- a/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/AbstractAnyObjectWorkflowAdapter.java +++ b/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/AbstractAnyObjectWorkflowAdapter.java @@ -35,7 +35,7 @@ import org.apache.syncope.core.persistence.api.entity.group.Group; import org.apache.syncope.core.provisioning.api.WorkflowResult; import org.apache.syncope.core.provisioning.api.data.AnyObjectDataBinder; -import org.apache.syncope.core.provisioning.api.event.AnyLifecycleEvent; +import org.apache.syncope.core.provisioning.api.event.EntityLifecycleEvent; import org.apache.syncope.core.spring.security.AuthContextUtils; import org.apache.syncope.core.workflow.api.AnyObjectWorkflowAdapter; import org.identityconnectors.framework.common.objects.SyncDeltaType; @@ -78,7 +78,7 @@ public WorkflowResult create(final AnyObjectCR anyObjectCR, final String // finally publish events for all groups affected by this operation, via membership anyObject.getMemberships().stream().forEach(m -> publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, m.getRightEnd(), AuthContextUtils.getDomain()))); + new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, m.getRightEnd(), AuthContextUtils.getDomain()))); return result; } @@ -109,7 +109,7 @@ public WorkflowResult update( // finally publish events for all groups affected by this operation, via membership result.getResult().getMemberships().stream().map(MembershipUR::getGroup).distinct(). map(groupDAO::find).filter(Objects::nonNull). - forEach(group -> publisher.publishEvent(new AnyLifecycleEvent<>( + forEach(group -> publisher.publishEvent(new EntityLifecycleEvent<>( this, SyncDeltaType.UPDATE, group, AuthContextUtils.getDomain()))); return result; @@ -127,7 +127,7 @@ public void delete(final String anyObjectKey, final String eraser, final String doDelete(anyObject, eraser, context); // finally publish events for all groups affected by this operation, via membership - groups.forEach(group -> publisher.publishEvent(new AnyLifecycleEvent<>( + groups.forEach(group -> publisher.publishEvent(new EntityLifecycleEvent<>( this, SyncDeltaType.UPDATE, group, AuthContextUtils.getDomain()))); } } diff --git a/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/AbstractUserWorkflowAdapter.java b/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/AbstractUserWorkflowAdapter.java index 669b80e9a50..1626dfbabec 100644 --- a/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/AbstractUserWorkflowAdapter.java +++ b/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/AbstractUserWorkflowAdapter.java @@ -46,7 +46,7 @@ import org.apache.syncope.core.provisioning.api.PropagationByResource; import org.apache.syncope.core.provisioning.api.UserWorkflowResult; import org.apache.syncope.core.provisioning.api.data.UserDataBinder; -import org.apache.syncope.core.provisioning.api.event.AnyLifecycleEvent; +import org.apache.syncope.core.provisioning.api.event.EntityLifecycleEvent; import org.apache.syncope.core.provisioning.api.rules.RuleEnforcer; import org.apache.syncope.core.spring.policy.AccountPolicyException; import org.apache.syncope.core.spring.policy.PasswordPolicyException; @@ -250,7 +250,7 @@ public UserWorkflowResult> create( // finally publish events for all groups affected by this operation, via membership user.getMemberships().stream().forEach(m -> publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, m.getRightEnd(), AuthContextUtils.getDomain()))); + new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, m.getRightEnd(), AuthContextUtils.getDomain()))); return result; } @@ -318,7 +318,7 @@ public UserWorkflowResult> update( // finally publish events for all groups affected by this operation, via membership result.getResult().getLeft().getMemberships().stream().map(MembershipUR::getGroup).distinct(). map(groupDAO::find).filter(Objects::nonNull). - forEach(group -> publisher.publishEvent(new AnyLifecycleEvent<>( + forEach(group -> publisher.publishEvent(new EntityLifecycleEvent<>( this, SyncDeltaType.UPDATE, group, AuthContextUtils.getDomain()))); return result; @@ -410,7 +410,7 @@ public void delete(final String userKey, final String eraser, final String conte doDelete(user, eraser, context); // finally publish events for all groups affected by this operation, via membership - groups.forEach(group -> publisher.publishEvent(new AnyLifecycleEvent<>( + groups.forEach(group -> publisher.publishEvent(new EntityLifecycleEvent<>( this, SyncDeltaType.UPDATE, group, AuthContextUtils.getDomain()))); } } diff --git a/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/DefaultAnyObjectWorkflowAdapter.java b/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/DefaultAnyObjectWorkflowAdapter.java index c2bc29dd197..2f787eae1ab 100644 --- a/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/DefaultAnyObjectWorkflowAdapter.java +++ b/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/DefaultAnyObjectWorkflowAdapter.java @@ -28,7 +28,7 @@ import org.apache.syncope.core.provisioning.api.PropagationByResource; import org.apache.syncope.core.provisioning.api.WorkflowResult; import org.apache.syncope.core.provisioning.api.data.AnyObjectDataBinder; -import org.apache.syncope.core.provisioning.api.event.AnyLifecycleEvent; +import org.apache.syncope.core.provisioning.api.event.EntityLifecycleEvent; import org.apache.syncope.core.spring.security.AuthContextUtils; import org.identityconnectors.framework.common.objects.SyncDeltaType; import org.springframework.context.ApplicationEventPublisher; @@ -58,7 +58,7 @@ protected WorkflowResult doCreate( anyObject = anyObjectDAO.save(anyObject); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.CREATE, anyObject, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.CREATE, anyObject, AuthContextUtils.getDomain())); PropagationByResource propByRes = new PropagationByResource<>(); propByRes.set(ResourceOperation.CREATE, anyObjectDAO.findAllResourceKeys(anyObject.getKey())); @@ -74,8 +74,8 @@ protected WorkflowResult doUpdate( metadata(anyObject, updater, context); AnyObject updated = anyObjectDAO.save(anyObject); - publisher.publishEvent(new AnyLifecycleEvent<>( - this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); + publisher.publishEvent( + new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); return new WorkflowResult<>(anyObjectUR, propByRes, "update"); } @@ -85,6 +85,6 @@ protected void doDelete(final AnyObject anyObject, final String eraser, final St anyObjectDAO.delete(anyObject); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.DELETE, anyObject, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.DELETE, anyObject, AuthContextUtils.getDomain())); } } diff --git a/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/DefaultGroupWorkflowAdapter.java b/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/DefaultGroupWorkflowAdapter.java index 3c1175385d3..2cab7cdd42b 100644 --- a/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/DefaultGroupWorkflowAdapter.java +++ b/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/DefaultGroupWorkflowAdapter.java @@ -27,7 +27,7 @@ import org.apache.syncope.core.provisioning.api.PropagationByResource; import org.apache.syncope.core.provisioning.api.WorkflowResult; import org.apache.syncope.core.provisioning.api.data.GroupDataBinder; -import org.apache.syncope.core.provisioning.api.event.AnyLifecycleEvent; +import org.apache.syncope.core.provisioning.api.event.EntityLifecycleEvent; import org.apache.syncope.core.spring.security.AuthContextUtils; import org.identityconnectors.framework.common.objects.SyncDeltaType; import org.springframework.context.ApplicationEventPublisher; @@ -54,7 +54,7 @@ protected WorkflowResult doCreate(final GroupCR groupCR, final String cr group = groupDAO.saveAndRefreshDynMemberships(group); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.CREATE, group, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.CREATE, group, AuthContextUtils.getDomain())); PropagationByResource propByRes = new PropagationByResource<>(); propByRes.set(ResourceOperation.CREATE, groupDAO.findAllResourceKeys(group.getKey())); @@ -70,8 +70,8 @@ protected WorkflowResult doUpdate( metadata(group, updater, context); Group updated = groupDAO.save(group); - publisher.publishEvent(new AnyLifecycleEvent<>( - this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); + publisher.publishEvent( + new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); return new WorkflowResult<>(groupUR, propByRes, "update"); } @@ -81,6 +81,6 @@ protected void doDelete(final Group group, final String eraser, final String con groupDAO.delete(group); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.DELETE, group, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.DELETE, group, AuthContextUtils.getDomain())); } } diff --git a/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/DefaultUserWorkflowAdapter.java b/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/DefaultUserWorkflowAdapter.java index 977f162c6d6..36509fbc05a 100644 --- a/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/DefaultUserWorkflowAdapter.java +++ b/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/DefaultUserWorkflowAdapter.java @@ -32,7 +32,7 @@ import org.apache.syncope.core.provisioning.api.PropagationByResource; import org.apache.syncope.core.provisioning.api.UserWorkflowResult; import org.apache.syncope.core.provisioning.api.data.UserDataBinder; -import org.apache.syncope.core.provisioning.api.event.AnyLifecycleEvent; +import org.apache.syncope.core.provisioning.api.event.EntityLifecycleEvent; import org.apache.syncope.core.provisioning.api.rules.RuleEnforcer; import org.apache.syncope.core.spring.security.AuthContextUtils; import org.apache.syncope.core.spring.security.SecurityProperties; @@ -91,7 +91,7 @@ protected UserWorkflowResult> doCreate( user = userDAO.save(user); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.CREATE, user, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.CREATE, user, AuthContextUtils.getDomain())); PropagationByResource propByRes = new PropagationByResource<>(); propByRes.set(ResourceOperation.CREATE, userDAO.findAllResourceKeys(user.getKey())); @@ -122,7 +122,7 @@ protected UserWorkflowResult doActivate( User updated = userDAO.save(user); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); return new UserWorkflowResult<>(updated.getKey(), null, null, "activate"); } @@ -137,8 +137,8 @@ protected UserWorkflowResult> doUpdate( metadata(user, updater, context); User updated = userDAO.save(user); - publisher.publishEvent(new AnyLifecycleEvent<>( - this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); + publisher.publishEvent( + new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); return new UserWorkflowResult<>( Pair.of(userUR, !user.isSuspended()), @@ -154,7 +154,7 @@ protected UserWorkflowResult doSuspend(final User user, final String upd User updated = userDAO.save(user); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); return new UserWorkflowResult<>(updated.getKey(), null, null, "suspend"); } @@ -166,7 +166,7 @@ protected UserWorkflowResult doReactivate(final User user, final String User updated = userDAO.save(user); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); return new UserWorkflowResult<>(updated.getKey(), null, null, "reactivate"); } @@ -180,7 +180,7 @@ protected void doRequestPasswordReset(final User user, final String updater, fin User updated = userDAO.save(user); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); } @Override @@ -208,6 +208,6 @@ protected void doDelete(final User user, final String eraser, final String conte userDAO.delete(user); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.DELETE, user, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.DELETE, user, AuthContextUtils.getDomain())); } } diff --git a/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchIndexLoader.java b/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchIndexLoader.java index c3fe69d34db..93e55ed7b59 100644 --- a/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchIndexLoader.java +++ b/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchIndexLoader.java @@ -56,6 +56,11 @@ public void load(final String domain, final DataSource datasource) { indexManager.defaultSettings(), indexManager.defaultAnyMapping()); } + if (!indexManager.existsRealmIndex(domain)) { + indexManager.createRealmIndex(domain, + indexManager.defaultSettings(), indexManager.defaultRealmMapping()); + } + if (!indexManager.existsAuditIndex(domain)) { indexManager.createAuditIndex(domain, indexManager.defaultSettings(), indexManager.defaultAuditMapping()); diff --git a/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchIndexManager.java b/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchIndexManager.java index c426431b48e..919ff7312f5 100644 --- a/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchIndexManager.java +++ b/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchIndexManager.java @@ -20,6 +20,7 @@ import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch._types.ElasticsearchException; +import co.elastic.clients.elasticsearch._types.Refresh; import co.elastic.clients.elasticsearch._types.analysis.CustomNormalizer; import co.elastic.clients.elasticsearch._types.analysis.Normalizer; import co.elastic.clients.elasticsearch._types.mapping.DynamicTemplate; @@ -45,7 +46,9 @@ import java.util.Map; import org.apache.syncope.common.lib.types.AnyTypeKind; import org.apache.syncope.core.persistence.api.entity.Any; -import org.apache.syncope.core.provisioning.api.event.AnyLifecycleEvent; +import org.apache.syncope.core.persistence.api.entity.Entity; +import org.apache.syncope.core.persistence.api.entity.Realm; +import org.apache.syncope.core.provisioning.api.event.EntityLifecycleEvent; import org.apache.syncope.core.spring.security.SecureRandomUtils; import org.identityconnectors.framework.common.objects.SyncDeltaType; import org.slf4j.Logger; @@ -85,6 +88,12 @@ public boolean existsAnyIndex(final String domain, final AnyTypeKind kind) throw value(); } + public boolean existsRealmIndex(final String domain) throws IOException { + return client.indices().exists(new ExistsRequest.Builder(). + index(ElasticsearchUtils.getRealmIndex(domain)).build()). + value(); + } + public boolean existsAuditIndex(final String domain) throws IOException { return client.indices().exists(new ExistsRequest.Builder(). index(ElasticsearchUtils.getAuditIndex(domain)).build()). @@ -119,6 +128,19 @@ public TypeMapping defaultAnyMapping() throws IOException { build(); } + public TypeMapping defaultRealmMapping() throws IOException { + return new TypeMapping.Builder(). + dynamicTemplates(List.of(Map.of( + "strings", + new DynamicTemplate.Builder(). + matchMappingType("string"). + mapping(new Property.Builder(). + keyword(new KeywordProperty.Builder().normalizer("string_lowercase").build()). + build()). + build()))). + build(); + } + public TypeMapping defaultAuditMapping() throws IOException { return new TypeMapping.Builder(). dynamicTemplates(List.of(Map.of( @@ -198,6 +220,45 @@ public void removeAnyIndex(final String domain, final AnyTypeKind kind) throws I LOG.debug("Successfully removed {}: {}", ElasticsearchUtils.getAnyIndex(domain, kind), response); } + protected CreateIndexResponse doCreateRealmIndex( + final String domain, + final IndexSettings settings, + final TypeMapping mappings) throws IOException { + + return client.indices().create( + new CreateIndexRequest.Builder(). + index(ElasticsearchUtils.getRealmIndex(domain)). + settings(settings). + mappings(mappings). + build()); + } + + public void createRealmIndex( + final String domain, + final IndexSettings settings, + final TypeMapping mappings) + throws IOException { + + try { + CreateIndexResponse response = doCreateRealmIndex(domain, settings, mappings); + + LOG.debug("Successfully created realm index {}: {}", + ElasticsearchUtils.getRealmIndex(domain), response); + } catch (ElasticsearchException e) { + LOG.debug("Could not create realm index {} because it already exists", + ElasticsearchUtils.getRealmIndex(domain), e); + + removeRealmIndex(domain); + doCreateRealmIndex(domain, settings, mappings); + } + } + + public void removeRealmIndex(final String domain) throws IOException { + DeleteIndexResponse response = client.indices().delete( + new DeleteIndexRequest.Builder().index(ElasticsearchUtils.getRealmIndex(domain)).build()); + LOG.debug("Successfully removed {}: {}", ElasticsearchUtils.getRealmIndex(domain), response); + } + protected CreateIndexResponse doCreateAuditIndex( final String domain, final IndexSettings settings, @@ -238,31 +299,54 @@ public void removeAuditIndex(final String domain) throws IOException { } @TransactionalEventListener - public void any(final AnyLifecycleEvent> event) throws IOException { - LOG.debug("About to {} index for {}", event.getType().name(), event.getAny()); - - if (event.getType() == SyncDeltaType.DELETE) { - DeleteRequest request = new DeleteRequest.Builder().index( - ElasticsearchUtils.getAnyIndex(event.getDomain(), event.getAny().getType().getKind())). - id(event.getAny().getKey()). - build(); - DeleteResponse response = client.delete(request); - LOG.debug("Index successfully deleted for {}[{}]: {}", - event.getAny().getType().getKind(), event.getAny().getKey(), response); - } else { - IndexRequest> request = new IndexRequest.Builder>(). - index(ElasticsearchUtils.getAnyIndex(event.getDomain(), event.getAny().getType().getKind())). - id(event.getAny().getKey()). - document(elasticsearchUtils.document(event.getAny())). - build(); - IndexResponse response = client.index(request); - LOG.debug("Index successfully created or updated for {}: {}", event.getAny(), response); + public void entity(final EntityLifecycleEvent event) throws IOException { + LOG.debug("About to {} index for {}", event.getType().name(), event.getEntity()); + + if (event.getEntity() instanceof Any) { + Any any = (Any) event.getEntity(); + + if (event.getType() == SyncDeltaType.DELETE) { + DeleteRequest request = new DeleteRequest.Builder().index( + ElasticsearchUtils.getAnyIndex(event.getDomain(), any.getType().getKind())). + id(any.getKey()). + build(); + DeleteResponse response = client.delete(request); + LOG.debug("Index successfully deleted for {}[{}]: {}", + any.getType().getKind(), any.getKey(), response); + } else { + IndexRequest> request = new IndexRequest.Builder>(). + index(ElasticsearchUtils.getAnyIndex(event.getDomain(), any.getType().getKind())). + id(any.getKey()). + document(elasticsearchUtils.document(any)). + build(); + IndexResponse response = client.index(request); + LOG.debug("Index successfully created or updated for {}: {}", any, response); + } + } else if (event.getEntity() instanceof Realm) { + Realm realm = (Realm) event.getEntity(); + + if (event.getType() == SyncDeltaType.DELETE) { + DeleteRequest request = new DeleteRequest.Builder(). + index(ElasticsearchUtils.getRealmIndex(event.getDomain())). + id(realm.getKey()). + refresh(Refresh.True). + build(); + DeleteResponse response = client.delete(request); + LOG.debug("Index successfully deleted for {}: {}", realm, response); + } else { + IndexRequest> request = new IndexRequest.Builder>(). + index(ElasticsearchUtils.getRealmIndex(event.getDomain())). + id(realm.getKey()). + document(elasticsearchUtils.document(realm)). + refresh(Refresh.True). + build(); + IndexResponse response = client.index(request); + LOG.debug("Index successfully created or updated for {}: {}", realm, response); + } } } - public void audit(final String domain, final long instant, final JsonNode message) - throws IOException { - + public void audit(final String domain, final long instant, final JsonNode message) throws IOException { LOG.debug("About to audit"); IndexRequest> request = new IndexRequest.Builder>(). diff --git a/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchUtils.java b/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchUtils.java index 1326487fd5f..f4e17e8da83 100644 --- a/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchUtils.java +++ b/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchUtils.java @@ -38,6 +38,7 @@ import org.apache.syncope.core.persistence.api.entity.PlainAttr; import org.apache.syncope.core.persistence.api.entity.PlainAttrValue; import org.apache.syncope.core.persistence.api.entity.Privilege; +import org.apache.syncope.core.persistence.api.entity.Realm; import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject; import org.apache.syncope.core.persistence.api.entity.group.Group; import org.apache.syncope.core.persistence.api.entity.user.User; @@ -52,6 +53,10 @@ public static String getAnyIndex(final String domain, final AnyTypeKind kind) { return domain.toLowerCase() + '_' + kind.name().toLowerCase(); } + public static String getRealmIndex(final String domain) { + return domain.toLowerCase() + "_realm"; + } + public static String getAuditIndex(final String domain) { return domain.toLowerCase() + "_audit"; } @@ -121,7 +126,7 @@ public Map document(final Any any) { builder.put("relationships", relationships); builder.put("relationshipTypes", relationshipTypes); - ElasticsearchUtils.this.customizeDocument(builder, anyObject); + customizeDocument(builder, anyObject); } else if (any instanceof Group) { Group group = ((Group) any); builder.put("name", group.getName()); @@ -137,7 +142,7 @@ public Map document(final Any any) { members.addAll(groupDAO.findADynMembers(group)); builder.put("members", members); - ElasticsearchUtils.this.customizeDocument(builder, group); + customizeDocument(builder, group); } else if (any instanceof User) { User user = ((User) any); builder.put("username", user.getUsername()); @@ -193,6 +198,21 @@ protected void customizeDocument(final Map builder, final Group protected void customizeDocument(final Map builder, final User user) { } + public Map document(final Realm realm) { + Map builder = new HashMap<>(); + builder.put("id", realm.getKey()); + builder.put("name", realm.getName()); + builder.put("parent_id", realm.getParent() == null ? null : realm.getParent().getKey()); + builder.put("fullPath", realm.getFullPath()); + + customizeDocument(builder, realm); + + return builder; + } + + protected void customizeDocument(final Map builder, final Realm realm) { + } + public Map document( final long instant, final JsonNode message, diff --git a/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/ElasticsearchPersistenceContext.java b/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/ElasticsearchPersistenceContext.java index 88e9e1e1609..fb6f7cc495f 100644 --- a/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/ElasticsearchPersistenceContext.java +++ b/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/ElasticsearchPersistenceContext.java @@ -27,13 +27,16 @@ import org.apache.syncope.core.persistence.api.dao.GroupDAO; import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; import org.apache.syncope.core.persistence.api.dao.RealmDAO; +import org.apache.syncope.core.persistence.api.dao.RoleDAO; import org.apache.syncope.core.persistence.api.dao.UserDAO; import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory; import org.apache.syncope.core.persistence.api.entity.EntityFactory; import org.apache.syncope.core.persistence.jpa.dao.ElasticsearchAnySearchDAO; import org.apache.syncope.core.persistence.jpa.dao.ElasticsearchAuditConfDAO; +import org.apache.syncope.core.persistence.jpa.dao.ElasticsearchRealmDAO; import org.apache.syncope.ext.elasticsearch.client.ElasticsearchProperties; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; @@ -70,6 +73,17 @@ public AnySearchDAO anySearchDAO( props.getIndexMaxResultWindow()); } + @ConditionalOnMissingBean(name = "elasticsearchRealmDAO") + @Bean + public RealmDAO realmDAO( + final @Lazy RoleDAO roleDAO, + final ApplicationEventPublisher publisher, + final ElasticsearchProperties props, + final ElasticsearchClient client) { + + return new ElasticsearchRealmDAO(roleDAO, publisher, client, props.getIndexMaxResultWindow()); + } + @ConditionalOnMissingBean(name = "elasticsearchAuditConfDAO") @Bean public AuditConfDAO auditConfDAO( diff --git a/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAnySearchDAO.java b/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAnySearchDAO.java index 31b46156585..1f28a037573 100644 --- a/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAnySearchDAO.java +++ b/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAnySearchDAO.java @@ -157,12 +157,10 @@ protected Triple, Set, Set> getAdminRealmsFilter return noRealm; }); - realmDAO.findDescendants( - realm.getFullPath(), null, -1, -1).stream(). - filter(r -> r.getFullPath().startsWith(base.getFullPath())). + realmDAO.findDescendants(realm.getFullPath(), base.getFullPath()). forEach(descendant -> queries.add( new Query.Builder().term(QueryBuilders.term(). - field("realm").value(descendant.getKey()).build()). + field("realm").value(descendant).build()). build())); } else { DynRealm dynRealm = dynRealmDAO.find(realmPath); diff --git a/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchRealmDAO.java b/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchRealmDAO.java new file mode 100644 index 00000000000..4e618ee430d --- /dev/null +++ b/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchRealmDAO.java @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.syncope.core.persistence.jpa.dao; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch._types.ScriptLanguage; +import co.elastic.clients.elasticsearch._types.ScriptSortType; +import co.elastic.clients.elasticsearch._types.SearchType; +import co.elastic.clients.elasticsearch._types.SortOptions; +import co.elastic.clients.elasticsearch._types.SortOrder; +import co.elastic.clients.elasticsearch._types.query_dsl.Query; +import co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders; +import co.elastic.clients.elasticsearch.core.CountRequest; +import co.elastic.clients.elasticsearch.core.SearchRequest; +import co.elastic.clients.elasticsearch.core.search.Hit; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.apache.syncope.common.lib.SyncopeConstants; +import org.apache.syncope.core.persistence.api.dao.MalformedPathException; +import org.apache.syncope.core.persistence.api.dao.RoleDAO; +import org.apache.syncope.core.persistence.api.entity.Realm; +import org.apache.syncope.core.spring.security.AuthContextUtils; +import org.apache.syncope.ext.elasticsearch.client.ElasticsearchUtils; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.transaction.annotation.Transactional; + +public class ElasticsearchRealmDAO extends JPARealmDAO { + + protected static final List ES_SORT_OPTIONS_REALM = List.of( + new SortOptions.Builder(). + script(s -> s.type(ScriptSortType.Number). + script(t -> t.inline(i -> i.lang(ScriptLanguage.Painless). + source("doc['fullPath'].value.chars().filter(ch -> ch == '/').count()"))). + order(SortOrder.Asc)). + build()); + + protected final ElasticsearchClient client; + + protected final int indexMaxResultWindow; + + public ElasticsearchRealmDAO( + final RoleDAO roleDAO, + final ApplicationEventPublisher publisher, + final ElasticsearchClient client, + final int indexMaxResultWindow) { + + super(roleDAO, publisher); + this.client = client; + this.indexMaxResultWindow = indexMaxResultWindow; + } + + @Transactional(readOnly = true) + @Override + public Realm findByFullPath(final String fullPath) { + if (SyncopeConstants.ROOT_REALM.equals(fullPath)) { + return getRoot(); + } + + if (StringUtils.isBlank(fullPath) || !PATH_PATTERN.matcher(fullPath).matches()) { + throw new MalformedPathException(fullPath); + } + + SearchRequest request = new SearchRequest.Builder(). + index(ElasticsearchUtils.getRealmIndex(AuthContextUtils.getDomain())). + searchType(SearchType.QueryThenFetch). + query(new Query.Builder().term(QueryBuilders.term(). + field("fullPath").value(fullPath).build()).build()). + size(1). + build(); + + try { + String result = client.search(request, Void.class).hits().hits().stream().findFirst(). + map(Hit::id). + orElse(null); + return find(result); + } catch (Exception e) { + LOG.error("While searching ES for one match", e); + } + + return null; + } + + protected List search(final Query query) { + SearchRequest request = new SearchRequest.Builder(). + index(ElasticsearchUtils.getRealmIndex(AuthContextUtils.getDomain())). + searchType(SearchType.QueryThenFetch). + query(query). + sort(ES_SORT_OPTIONS_REALM). + build(); + + try { + return client.search(request, Void.class).hits().hits().stream(). + map(Hit::id). + collect(Collectors.toList()); + } catch (Exception e) { + LOG.error("While searching in Elasticsearch", e); + return List.of(); + } + } + + @Override + public List findByName(final String name) { + List result = search( + new Query.Builder().term(QueryBuilders.term(). + field("name").value(name).build()).build()); + return result.stream().map(this::find).collect(Collectors.toList()); + } + + @Override + public List findChildren(final Realm realm) { + List result = search( + new Query.Builder().term(QueryBuilders.term(). + field("parent_id").value(realm.getKey()).build()).build()); + return result.stream().map(this::find).collect(Collectors.toList()); + } + + protected Query buildDescendantQuery(final String base, final String keyword) { + Query prefix = new Query.Builder().disMax(QueryBuilders.disMax().queries( + new Query.Builder().term(QueryBuilders.term(). + field("fullPath").value(base).build()).build(), + new Query.Builder().regexp(QueryBuilders.regexp(). + field("fullPath").value(SyncopeConstants.ROOT_REALM.equals(base) ? "/.*" : base + "/.*"). + build()).build()).build()).build(); + + if (keyword == null) { + return prefix; + } + + return new Query.Builder().bool(QueryBuilders.bool().must( + prefix, + new Query.Builder().wildcard(QueryBuilders.wildcard(). + field("name").value(keyword.replace("%", "*").toLowerCase()).build()). + build()).build()). + build(); + } + + @Override + public int countDescendants(final String base, final String keyword) { + CountRequest request = new CountRequest.Builder(). + index(ElasticsearchUtils.getRealmIndex(AuthContextUtils.getDomain())). + query(buildDescendantQuery(base, keyword)). + build(); + + try { + return (int) client.count(request).count(); + } catch (Exception e) { + LOG.error("While counting in Elasticsearch", e); + return 0; + } + } + + @Override + public List findDescendants( + final String base, + final String keyword, + final int page, + final int itemsPerPage) { + + SearchRequest request = new SearchRequest.Builder(). + index(ElasticsearchUtils.getRealmIndex(AuthContextUtils.getDomain())). + searchType(SearchType.QueryThenFetch). + query(buildDescendantQuery(base, keyword)). + from(itemsPerPage * (page <= 0 ? 0 : page - 1)). + size(itemsPerPage < 0 ? indexMaxResultWindow : itemsPerPage). + sort(ES_SORT_OPTIONS_REALM). + build(); + + List result = List.of(); + try { + result = client.search(request, Void.class).hits().hits().stream(). + map(Hit::id). + collect(Collectors.toList()); + } catch (Exception e) { + LOG.error("While searching in Elasticsearch", e); + } + + return result.stream().map(this::find).collect(Collectors.toList()); + } + + @Override + public List findDescendants(final String base, final String prefix) { + Query prefixQuery = new Query.Builder().disMax(QueryBuilders.disMax().queries( + new Query.Builder().term(QueryBuilders.term(). + field("fullPath").value(base).build()).build(), + new Query.Builder().prefix(QueryBuilders.prefix(). + field("fullPath").value(SyncopeConstants.ROOT_REALM.equals(prefix) ? "/" : prefix + "/"). + build()).build()).build()).build(); + + Query query = new Query.Builder().bool(QueryBuilders.bool().must( + buildDescendantQuery(base, (String) null), + prefixQuery).build()). + build(); + + SearchRequest request = new SearchRequest.Builder(). + index(ElasticsearchUtils.getRealmIndex(AuthContextUtils.getDomain())). + searchType(SearchType.QueryThenFetch). + query(query). + from(0). + size(indexMaxResultWindow). + sort(ES_SORT_OPTIONS_REALM). + build(); + + List result = List.of(); + try { + result = client.search(request, Void.class).hits().hits().stream(). + map(Hit::id). + collect(Collectors.toList()); + } catch (Exception e) { + LOG.error("While searching in Elasticsearch", e); + } + return result; + } +} diff --git a/ext/elasticsearch/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAnySearchDAOTest.java b/ext/elasticsearch/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAnySearchDAOTest.java index 02619dc8138..03b77935fe2 100644 --- a/ext/elasticsearch/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAnySearchDAOTest.java +++ b/ext/elasticsearch/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchAnySearchDAOTest.java @@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -112,22 +113,19 @@ protected void setupSearchDAO() { public void getAdminRealmsFilter4realm() throws IOException { // 1. mock Realm root = mock(Realm.class); - when(root.getKey()).thenReturn("rootKey"); when(root.getFullPath()).thenReturn(SyncopeConstants.ROOT_REALM); when(realmDAO.findByFullPath(SyncopeConstants.ROOT_REALM)).thenReturn(root); - when(realmDAO.findDescendants(SyncopeConstants.ROOT_REALM, null, -1, -1)).thenReturn(List.of(root)); + when(realmDAO.findDescendants(eq(SyncopeConstants.ROOT_REALM), anyString())).thenReturn(List.of("rootKey")); // 2. test Set adminRealms = Set.of(SyncopeConstants.ROOT_REALM); Triple, Set, Set> filter = searchDAO.getAdminRealmsFilter(root, true, adminRealms, AnyTypeKind.USER); - assertThat( - new Query.Builder().disMax(QueryBuilders.disMax().queries( - new Query.Builder().term(QueryBuilders.term().field("realm").value( - "rootKey").build()).build()).build()). - build()). + assertThat(new Query.Builder().disMax(QueryBuilders.disMax().queries( + new Query.Builder().term(QueryBuilders.term().field("realm").value("rootKey").build()). + build()).build()).build()). usingRecursiveComparison().isEqualTo(filter.getLeft().get()); assertEquals(Set.of(), filter.getMiddle()); assertEquals(Set.of(), filter.getRight()); diff --git a/ext/elasticsearch/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/ElasticsearchReindex.java b/ext/elasticsearch/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/ElasticsearchReindex.java index 1756cbe87fe..c11a9f4c2da 100644 --- a/ext/elasticsearch/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/ElasticsearchReindex.java +++ b/ext/elasticsearch/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/ElasticsearchReindex.java @@ -28,6 +28,7 @@ import org.apache.syncope.core.persistence.api.dao.AnyDAO; import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO; import org.apache.syncope.core.persistence.api.dao.GroupDAO; +import org.apache.syncope.core.persistence.api.dao.RealmDAO; import org.apache.syncope.core.persistence.api.dao.UserDAO; import org.apache.syncope.core.persistence.api.entity.task.SchedTask; import org.apache.syncope.core.persistence.api.entity.task.TaskExec; @@ -61,6 +62,9 @@ public class ElasticsearchReindex extends AbstractSchedTaskJobDelegate op.index(idx -> idx. + index(rindex). + id(realm). + document(utils.document(realmDAO.find(realm))))); + } + + try { + BulkResponse response = client.bulk(bulkRequest.build()); + LOG.debug("Index successfully created for {} [{}/{}]: {}", + rindex, page, AnyDAO.DEFAULT_PAGE_SIZE, response); + } catch (Exception e) { + LOG.error("Could not create index for {} [{}/{}]: {}", + rindex, page, AnyDAO.DEFAULT_PAGE_SIZE, e); + } + } + indexManager.createAuditIndex(AuthContextUtils.getDomain(), auditSettings(), auditMapping()); setStatus("Rebuild indexes for domain " + AuthContextUtils.getDomain() + " successfully completed"); diff --git a/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/api/UserRequestHandler.java b/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/api/UserRequestHandler.java index 94a80a07179..f1004070572 100644 --- a/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/api/UserRequestHandler.java +++ b/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/api/UserRequestHandler.java @@ -25,10 +25,10 @@ import org.apache.syncope.common.lib.to.UserRequestForm; import org.apache.syncope.common.lib.to.WorkflowTaskExecInput; import org.apache.syncope.core.persistence.api.dao.search.OrderByClause; -import org.apache.syncope.core.persistence.api.entity.Any; +import org.apache.syncope.core.persistence.api.entity.Entity; import org.apache.syncope.core.persistence.api.entity.user.User; import org.apache.syncope.core.provisioning.api.UserWorkflowResult; -import org.apache.syncope.core.provisioning.api.event.AnyLifecycleEvent; +import org.apache.syncope.core.provisioning.api.event.EntityLifecycleEvent; import org.flowable.engine.runtime.ProcessInstance; import org.springframework.transaction.event.TransactionalEventListener; @@ -85,7 +85,7 @@ Pair> getUserRequests( * @param event delete event */ @TransactionalEventListener - void cancelByUser(AnyLifecycleEvent> event); + void cancelByUser(EntityLifecycleEvent event); /** * Get the form matching the provided task id. diff --git a/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/impl/FlowableUserRequestHandler.java b/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/impl/FlowableUserRequestHandler.java index 1ab103551e1..eb7498e0437 100644 --- a/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/impl/FlowableUserRequestHandler.java +++ b/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/impl/FlowableUserRequestHandler.java @@ -45,13 +45,13 @@ import org.apache.syncope.core.persistence.api.dao.NotFoundException; import org.apache.syncope.core.persistence.api.dao.UserDAO; import org.apache.syncope.core.persistence.api.dao.search.OrderByClause; -import org.apache.syncope.core.persistence.api.entity.Any; +import org.apache.syncope.core.persistence.api.entity.Entity; import org.apache.syncope.core.persistence.api.entity.EntityFactory; import org.apache.syncope.core.persistence.api.entity.user.User; import org.apache.syncope.core.provisioning.api.PropagationByResource; import org.apache.syncope.core.provisioning.api.UserWorkflowResult; import org.apache.syncope.core.provisioning.api.data.UserDataBinder; -import org.apache.syncope.core.provisioning.api.event.AnyLifecycleEvent; +import org.apache.syncope.core.provisioning.api.event.EntityLifecycleEvent; import org.apache.syncope.core.spring.ApplicationContextProvider; import org.apache.syncope.core.spring.security.AuthContextUtils; import org.apache.syncope.core.workflow.api.WorkflowException; @@ -266,12 +266,12 @@ public void cancelByProcessDefinition(final String processDefinitionId) { } @Override - public void cancelByUser(final AnyLifecycleEvent> event) { + public void cancelByUser(final EntityLifecycleEvent event) { if (AuthContextUtils.getDomain().equals(event.getDomain()) && event.getType() == SyncDeltaType.DELETE - && event.getAny() instanceof User) { + && event.getEntity() instanceof User) { - User user = (User) event.getAny(); + User user = (User) event.getEntity(); engine.getRuntimeService().createNativeProcessInstanceQuery(). sql(createProcessInstanceQuery(user.getKey()).toString()). list().forEach(procInst -> engine.getRuntimeService().deleteProcessInstance( diff --git a/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/impl/FlowableUserWorkflowAdapter.java b/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/impl/FlowableUserWorkflowAdapter.java index 46fa89f022c..8989ce0b3af 100644 --- a/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/impl/FlowableUserWorkflowAdapter.java +++ b/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/impl/FlowableUserWorkflowAdapter.java @@ -43,7 +43,7 @@ import org.apache.syncope.core.provisioning.api.PropagationByResource; import org.apache.syncope.core.provisioning.api.UserWorkflowResult; import org.apache.syncope.core.provisioning.api.data.UserDataBinder; -import org.apache.syncope.core.provisioning.api.event.AnyLifecycleEvent; +import org.apache.syncope.core.provisioning.api.event.EntityLifecycleEvent; import org.apache.syncope.core.provisioning.api.rules.RuleEnforcer; import org.apache.syncope.core.spring.security.AuthContextUtils; import org.apache.syncope.core.spring.security.SecurityProperties; @@ -152,7 +152,7 @@ protected UserWorkflowResult> doCreate( User created = userDAO.save(user); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.CREATE, created, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.CREATE, created, AuthContextUtils.getDomain())); engine.getRuntimeService().updateBusinessKey( procInst.getProcessInstanceId(), FlowableRuntimeUtils.getWFProcBusinessKey(created.getKey())); @@ -246,7 +246,7 @@ protected UserWorkflowResult doActivate( User updated = userDAO.save(user); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); variables.keySet().forEach(key -> engine.getRuntimeService().removeVariable(procInstID, key)); engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.USER); @@ -285,7 +285,7 @@ protected UserWorkflowResult> doUpdate( User updated = userDAO.save(user); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.USER); engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.WF_EXECUTOR); @@ -344,7 +344,7 @@ protected UserWorkflowResult doSuspend(final User user, final String upd User updated = userDAO.save(user); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); @SuppressWarnings("unchecked") PropagationByResource propByRes = engine.getRuntimeService().getVariable( @@ -377,7 +377,7 @@ protected UserWorkflowResult doReactivate(final User user, final String User updated = userDAO.save(user); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); @SuppressWarnings("unchecked") PropagationByResource propByRes = engine.getRuntimeService().getVariable( @@ -413,7 +413,7 @@ protected void doRequestPasswordReset(final User user, final String updater, fin User updated = userDAO.save(user); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); variables.keySet().forEach(key -> engine.getRuntimeService().removeVariable(procInstID, key)); engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.USER); @@ -439,7 +439,7 @@ protected UserWorkflowResult> doConfirmPasswordReset( User updated = userDAO.save(user); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); variables.keySet().forEach(key -> engine.getRuntimeService().removeVariable(procInstID, key)); engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.USER); @@ -483,7 +483,7 @@ protected void doDelete(final User user, final String eraser, final String conte userDAO.delete(user.getKey()); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.DELETE, user, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.DELETE, user, AuthContextUtils.getDomain())); if (!engine.getHistoryService().createHistoricProcessInstanceQuery(). processInstanceId(procInstID).list().isEmpty()) { @@ -505,7 +505,7 @@ protected void doDelete(final User user, final String eraser, final String conte User updated = userDAO.save(user); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain())); engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.TASK); engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.USER); @@ -528,7 +528,7 @@ public UserWorkflowResult executeNextTask(final WorkflowTaskExecInput wo user = userDAO.save(user); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.UPDATE, user, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, user, AuthContextUtils.getDomain())); engine.getRuntimeService().setVariable( procInstID, FlowableRuntimeUtils.USER_TO, dataBinder.getUserTO(user, true)); @@ -539,7 +539,7 @@ public UserWorkflowResult executeNextTask(final WorkflowTaskExecInput wo userDAO.delete(user.getKey()); publisher.publishEvent( - new AnyLifecycleEvent<>(this, SyncDeltaType.DELETE, user, AuthContextUtils.getDomain())); + new EntityLifecycleEvent<>(this, SyncDeltaType.DELETE, user, AuthContextUtils.getDomain())); if (!engine.getHistoryService().createHistoricProcessInstanceQuery(). processInstanceId(procInstID).list().isEmpty()) { diff --git a/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchIndexLoader.java b/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchIndexLoader.java index 49f8b213705..e6cde59f4cf 100644 --- a/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchIndexLoader.java +++ b/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchIndexLoader.java @@ -56,6 +56,11 @@ public void load(final String domain, final DataSource datasource) { indexManager.defaultSettings(), indexManager.defaultAnyMapping()); } + if (!indexManager.existsRealmIndex(domain)) { + indexManager.createRealmIndex(domain, + indexManager.defaultSettings(), indexManager.defaultRealmMapping()); + } + if (!indexManager.existsAuditIndex(domain)) { indexManager.createAuditIndex(domain, indexManager.defaultSettings(), indexManager.defaultAuditMapping()); diff --git a/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchIndexManager.java b/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchIndexManager.java index fb5f57581e1..ebd016ff228 100644 --- a/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchIndexManager.java +++ b/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchIndexManager.java @@ -24,11 +24,14 @@ import java.util.Map; import org.apache.syncope.common.lib.types.AnyTypeKind; import org.apache.syncope.core.persistence.api.entity.Any; -import org.apache.syncope.core.provisioning.api.event.AnyLifecycleEvent; +import org.apache.syncope.core.persistence.api.entity.Entity; +import org.apache.syncope.core.persistence.api.entity.Realm; +import org.apache.syncope.core.provisioning.api.event.EntityLifecycleEvent; import org.apache.syncope.core.spring.security.SecureRandomUtils; import org.identityconnectors.framework.common.objects.SyncDeltaType; import org.opensearch.client.opensearch.OpenSearchClient; import org.opensearch.client.opensearch._types.OpenSearchException; +import org.opensearch.client.opensearch._types.Refresh; import org.opensearch.client.opensearch._types.analysis.CustomNormalizer; import org.opensearch.client.opensearch._types.analysis.Normalizer; import org.opensearch.client.opensearch._types.mapping.DynamicTemplate; @@ -61,7 +64,7 @@ public class OpenSearchIndexManager { protected final OpenSearchClient client; - protected final OpenSearchUtils ppenSearchUtils; + protected final OpenSearchUtils openSearchUtils; protected final String numberOfShards; @@ -74,7 +77,7 @@ public OpenSearchIndexManager( final String numberOfReplicas) { this.client = client; - this.ppenSearchUtils = ppenSearchUtils; + this.openSearchUtils = ppenSearchUtils; this.numberOfShards = numberOfShards; this.numberOfReplicas = numberOfReplicas; } @@ -85,6 +88,12 @@ public boolean existsAnyIndex(final String domain, final AnyTypeKind kind) throw value(); } + public boolean existsRealmIndex(final String domain) throws IOException { + return client.indices().exists(new ExistsRequest.Builder(). + index(OpenSearchUtils.getRealmIndex(domain)).build()). + value(); + } + public boolean existsAuditIndex(final String domain) throws IOException { return client.indices().exists(new ExistsRequest.Builder(). index(OpenSearchUtils.getAuditIndex(domain)).build()). @@ -119,6 +128,19 @@ public TypeMapping defaultAnyMapping() throws IOException { build(); } + public TypeMapping defaultRealmMapping() throws IOException { + return new TypeMapping.Builder(). + dynamicTemplates(List.of(Map.of( + "strings", + new DynamicTemplate.Builder(). + matchMappingType("string"). + mapping(new Property.Builder(). + keyword(new KeywordProperty.Builder().normalizer("string_lowercase").build()). + build()). + build()))). + build(); + } + public TypeMapping defaultAuditMapping() throws IOException { return new TypeMapping.Builder(). dynamicTemplates(List.of(Map.of( @@ -198,6 +220,45 @@ public void removeAnyIndex(final String domain, final AnyTypeKind kind) throws I LOG.debug("Successfully removed {}: {}", OpenSearchUtils.getAnyIndex(domain, kind), response); } + protected CreateIndexResponse doCreateRealmIndex( + final String domain, + final IndexSettings settings, + final TypeMapping mappings) throws IOException { + + return client.indices().create( + new CreateIndexRequest.Builder(). + index(OpenSearchUtils.getRealmIndex(domain)). + settings(settings). + mappings(mappings). + build()); + } + + public void createRealmIndex( + final String domain, + final IndexSettings settings, + final TypeMapping mappings) + throws IOException { + + try { + CreateIndexResponse response = doCreateRealmIndex(domain, settings, mappings); + + LOG.debug("Successfully created realm index {}: {}", + OpenSearchUtils.getRealmIndex(domain), response); + } catch (OpenSearchException e) { + LOG.debug("Could not create realm index {} because it already exists", + OpenSearchUtils.getRealmIndex(domain), e); + + removeRealmIndex(domain); + doCreateRealmIndex(domain, settings, mappings); + } + } + + public void removeRealmIndex(final String domain) throws IOException { + DeleteIndexResponse response = client.indices().delete( + new DeleteIndexRequest.Builder().index(OpenSearchUtils.getRealmIndex(domain)).build()); + LOG.debug("Successfully removed {}: {}", OpenSearchUtils.getRealmIndex(domain), response); + } + protected CreateIndexResponse doCreateAuditIndex( final String domain, final IndexSettings settings, @@ -238,37 +299,60 @@ public void removeAuditIndex(final String domain) throws IOException { } @TransactionalEventListener - public void any(final AnyLifecycleEvent> event) throws IOException { - LOG.debug("About to {} index for {}", event.getType().name(), event.getAny()); - - if (event.getType() == SyncDeltaType.DELETE) { - DeleteRequest request = new DeleteRequest.Builder().index( - OpenSearchUtils.getAnyIndex(event.getDomain(), event.getAny().getType().getKind())). - id(event.getAny().getKey()). - build(); - DeleteResponse response = client.delete(request); - LOG.debug("Index successfully deleted for {}[{}]: {}", - event.getAny().getType().getKind(), event.getAny().getKey(), response); - } else { - IndexRequest> request = new IndexRequest.Builder>(). - index(OpenSearchUtils.getAnyIndex(event.getDomain(), event.getAny().getType().getKind())). - id(event.getAny().getKey()). - document(ppenSearchUtils.document(event.getAny())). - build(); - IndexResponse response = client.index(request); - LOG.debug("Index successfully created or updated for {}: {}", event.getAny(), response); + public void entity(final EntityLifecycleEvent event) throws IOException { + LOG.debug("About to {} index for {}", event.getType().name(), event.getEntity()); + + if (event.getEntity() instanceof Any) { + Any any = (Any) event.getEntity(); + + if (event.getType() == SyncDeltaType.DELETE) { + DeleteRequest request = new DeleteRequest.Builder().index( + OpenSearchUtils.getAnyIndex(event.getDomain(), any.getType().getKind())). + id(any.getKey()). + build(); + DeleteResponse response = client.delete(request); + LOG.debug("Index successfully deleted for {}[{}]: {}", + any.getType().getKind(), any.getKey(), response); + } else { + IndexRequest> request = new IndexRequest.Builder>(). + index(OpenSearchUtils.getAnyIndex(event.getDomain(), any.getType().getKind())). + id(any.getKey()). + document(openSearchUtils.document(any)). + build(); + IndexResponse response = client.index(request); + LOG.debug("Index successfully created or updated for {}: {}", any, response); + } + } else if (event.getEntity() instanceof Realm) { + Realm realm = (Realm) event.getEntity(); + + if (event.getType() == SyncDeltaType.DELETE) { + DeleteRequest request = new DeleteRequest.Builder(). + index(OpenSearchUtils.getRealmIndex(event.getDomain())). + id(realm.getKey()). + refresh(Refresh.True). + build(); + DeleteResponse response = client.delete(request); + LOG.debug("Index successfully deleted for {}: {}", realm, response); + } else { + IndexRequest> request = new IndexRequest.Builder>(). + index(OpenSearchUtils.getRealmIndex(event.getDomain())). + id(realm.getKey()). + document(openSearchUtils.document(realm)). + refresh(Refresh.True). + build(); + IndexResponse response = client.index(request); + LOG.debug("Index successfully created or updated for {}: {}", realm, response); + } } } - public void audit(final String domain, final long instant, final JsonNode message) - throws IOException { - + public void audit(final String domain, final long instant, final JsonNode message) throws IOException { LOG.debug("About to audit"); IndexRequest> request = new IndexRequest.Builder>(). index(OpenSearchUtils.getAuditIndex(domain)). id(SecureRandomUtils.generateRandomUUID().toString()). - document(ppenSearchUtils.document(instant, message, domain)). + document(openSearchUtils.document(instant, message, domain)). build(); IndexResponse response = client.index(request); diff --git a/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchUtils.java b/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchUtils.java index 7329456237a..2c6902c1dd2 100644 --- a/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchUtils.java +++ b/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchUtils.java @@ -38,6 +38,7 @@ import org.apache.syncope.core.persistence.api.entity.PlainAttr; import org.apache.syncope.core.persistence.api.entity.PlainAttrValue; import org.apache.syncope.core.persistence.api.entity.Privilege; +import org.apache.syncope.core.persistence.api.entity.Realm; import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject; import org.apache.syncope.core.persistence.api.entity.group.Group; import org.apache.syncope.core.persistence.api.entity.user.User; @@ -52,6 +53,10 @@ public static String getAnyIndex(final String domain, final AnyTypeKind kind) { return domain.toLowerCase() + '_' + kind.name().toLowerCase(); } + public static String getRealmIndex(final String domain) { + return domain.toLowerCase() + "_realm"; + } + public static String getAuditIndex(final String domain) { return domain.toLowerCase() + "_audit"; } @@ -121,7 +126,7 @@ public Map document(final Any any) { builder.put("relationships", relationships); builder.put("relationshipTypes", relationshipTypes); - OpenSearchUtils.this.customizeDocument(builder, anyObject); + customizeDocument(builder, anyObject); } else if (any instanceof Group) { Group group = ((Group) any); builder.put("name", group.getName()); @@ -137,7 +142,7 @@ public Map document(final Any any) { members.addAll(groupDAO.findADynMembers(group)); builder.put("members", members); - OpenSearchUtils.this.customizeDocument(builder, group); + customizeDocument(builder, group); } else if (any instanceof User) { User user = ((User) any); builder.put("username", user.getUsername()); @@ -193,6 +198,21 @@ protected void customizeDocument(final Map builder, final Group protected void customizeDocument(final Map builder, final User user) { } + public Map document(final Realm realm) { + Map builder = new HashMap<>(); + builder.put("id", realm.getKey()); + builder.put("name", realm.getName()); + builder.put("parent_id", realm.getParent() == null ? null : realm.getParent().getKey()); + builder.put("fullPath", realm.getFullPath()); + + customizeDocument(builder, realm); + + return builder; + } + + protected void customizeDocument(final Map builder, final Realm realm) { + } + public Map document( final long instant, final JsonNode message, diff --git a/ext/opensearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/OpenSearchPersistenceContext.java b/ext/opensearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/OpenSearchPersistenceContext.java index b5fbebdb739..8725d1e2e83 100644 --- a/ext/opensearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/OpenSearchPersistenceContext.java +++ b/ext/opensearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/OpenSearchPersistenceContext.java @@ -26,14 +26,17 @@ import org.apache.syncope.core.persistence.api.dao.GroupDAO; import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; import org.apache.syncope.core.persistence.api.dao.RealmDAO; +import org.apache.syncope.core.persistence.api.dao.RoleDAO; import org.apache.syncope.core.persistence.api.dao.UserDAO; import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory; import org.apache.syncope.core.persistence.api.entity.EntityFactory; import org.apache.syncope.core.persistence.jpa.dao.OpenSearchAnySearchDAO; import org.apache.syncope.core.persistence.jpa.dao.OpenSearchAuditConfDAO; +import org.apache.syncope.core.persistence.jpa.dao.OpenSearchRealmDAO; import org.apache.syncope.ext.opensearch.client.OpenSearchProperties; import org.opensearch.client.opensearch.OpenSearchClient; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; @@ -70,6 +73,17 @@ public AnySearchDAO anySearchDAO( props.getIndexMaxResultWindow()); } + @ConditionalOnMissingBean(name = "openSearchRealmDAO") + @Bean + public RealmDAO realmDAO( + final @Lazy RoleDAO roleDAO, + final ApplicationEventPublisher publisher, + final OpenSearchProperties props, + final OpenSearchClient client) { + + return new OpenSearchRealmDAO(roleDAO, publisher, client, props.getIndexMaxResultWindow()); + } + @ConditionalOnMissingBean(name = "openSearchAuditConfDAO") @Bean public AuditConfDAO auditConfDAO( diff --git a/ext/opensearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OpenSearchAnySearchDAO.java b/ext/opensearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OpenSearchAnySearchDAO.java index 447e168e100..b138a3a977c 100644 --- a/ext/opensearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OpenSearchAnySearchDAO.java +++ b/ext/opensearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OpenSearchAnySearchDAO.java @@ -157,12 +157,10 @@ protected Triple, Set, Set> getAdminRealmsFilter return noRealm; }); - realmDAO.findDescendants( - realm.getFullPath(), null, -1, -1).stream(). - filter(r -> r.getFullPath().startsWith(base.getFullPath())). + realmDAO.findDescendants(realm.getFullPath(), base.getFullPath()). forEach(descendant -> queries.add( new Query.Builder().term(QueryBuilders.term(). - field("realm").value(FieldValue.of(descendant.getKey())).build()). + field("realm").value(FieldValue.of(descendant)).build()). build())); } else { DynRealm dynRealm = dynRealmDAO.find(realmPath); diff --git a/ext/opensearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OpenSearchRealmDAO.java b/ext/opensearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OpenSearchRealmDAO.java new file mode 100644 index 00000000000..2fc2de406f7 --- /dev/null +++ b/ext/opensearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OpenSearchRealmDAO.java @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.syncope.core.persistence.jpa.dao; + +import java.util.List; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.apache.syncope.common.lib.SyncopeConstants; +import org.apache.syncope.core.persistence.api.dao.MalformedPathException; +import org.apache.syncope.core.persistence.api.dao.RoleDAO; +import org.apache.syncope.core.persistence.api.entity.Realm; +import org.apache.syncope.core.spring.security.AuthContextUtils; +import org.apache.syncope.ext.opensearch.client.OpenSearchUtils; +import org.opensearch.client.opensearch.OpenSearchClient; +import org.opensearch.client.opensearch._types.FieldValue; +import org.opensearch.client.opensearch._types.ScriptSortType; +import org.opensearch.client.opensearch._types.SearchType; +import org.opensearch.client.opensearch._types.SortOptions; +import org.opensearch.client.opensearch._types.SortOrder; +import org.opensearch.client.opensearch._types.query_dsl.Query; +import org.opensearch.client.opensearch._types.query_dsl.QueryBuilders; +import org.opensearch.client.opensearch.core.CountRequest; +import org.opensearch.client.opensearch.core.SearchRequest; +import org.opensearch.client.opensearch.core.search.Hit; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.transaction.annotation.Transactional; + +public class OpenSearchRealmDAO extends JPARealmDAO { + + protected static final List ES_SORT_OPTIONS_REALM = List.of( + new SortOptions.Builder(). + script(s -> s.type(ScriptSortType.Number). + script(t -> t.inline(i -> i.lang("painless"). + source("doc['fullPath'].value.chars().filter(ch -> ch == '/').count()"))). + order(SortOrder.Asc)). + build()); + + protected final OpenSearchClient client; + + protected final int indexMaxResultWindow; + + public OpenSearchRealmDAO( + final RoleDAO roleDAO, + final ApplicationEventPublisher publisher, + final OpenSearchClient client, + final int indexMaxResultWindow) { + + super(roleDAO, publisher); + this.client = client; + this.indexMaxResultWindow = indexMaxResultWindow; + } + + @Transactional(readOnly = true) + @Override + public Realm findByFullPath(final String fullPath) { + if (SyncopeConstants.ROOT_REALM.equals(fullPath)) { + return getRoot(); + } + + if (StringUtils.isBlank(fullPath) || !PATH_PATTERN.matcher(fullPath).matches()) { + throw new MalformedPathException(fullPath); + } + + SearchRequest request = new SearchRequest.Builder(). + index(OpenSearchUtils.getRealmIndex(AuthContextUtils.getDomain())). + searchType(SearchType.QueryThenFetch). + query(new Query.Builder().term(QueryBuilders.term(). + field("fullPath").value(FieldValue.of(fullPath)).build()).build()). + size(1). + build(); + + try { + String result = client.search(request, Void.class).hits().hits().stream().findFirst(). + map(Hit::id). + orElse(null); + return find(result); + } catch (Exception e) { + LOG.error("While searching ES for one match", e); + } + + return null; + } + + protected List search(final Query query) { + SearchRequest request = new SearchRequest.Builder(). + index(OpenSearchUtils.getRealmIndex(AuthContextUtils.getDomain())). + searchType(SearchType.QueryThenFetch). + query(query). + sort(ES_SORT_OPTIONS_REALM). + build(); + + try { + return client.search(request, Void.class).hits().hits().stream(). + map(Hit::id). + collect(Collectors.toList()); + } catch (Exception e) { + LOG.error("While searching in OpenSearch", e); + return List.of(); + } + } + + @Override + public List findByName(final String name) { + List result = search( + new Query.Builder().term(QueryBuilders.term(). + field("name").value(FieldValue.of(name)).build()).build()); + return result.stream().map(this::find).collect(Collectors.toList()); + } + + @Override + public List findChildren(final Realm realm) { + List result = search( + new Query.Builder().term(QueryBuilders.term(). + field("parent_id").value(FieldValue.of(realm.getKey())).build()).build()); + return result.stream().map(this::find).collect(Collectors.toList()); + } + + protected Query buildDescendantQuery(final String base, final String keyword) { + Query prefix = new Query.Builder().disMax(QueryBuilders.disMax().queries( + new Query.Builder().term(QueryBuilders.term(). + field("fullPath").value(FieldValue.of(base)).build()).build(), + new Query.Builder().regexp(QueryBuilders.regexp(). + field("fullPath").value(SyncopeConstants.ROOT_REALM.equals(base) ? "/.*" : base + "/.*"). + build()).build()).build()).build(); + + if (keyword == null) { + return prefix; + } + + return new Query.Builder().bool(QueryBuilders.bool().must( + prefix, + new Query.Builder().wildcard(QueryBuilders.wildcard(). + field("name").value(keyword.replace("%", "*").toLowerCase()).build()). + build()).build()). + build(); + } + + @Override + public int countDescendants(final String base, final String keyword) { + CountRequest request = new CountRequest.Builder(). + index(OpenSearchUtils.getRealmIndex(AuthContextUtils.getDomain())). + query(buildDescendantQuery(base, keyword)). + build(); + + try { + return (int) client.count(request).count(); + } catch (Exception e) { + LOG.error("While counting in OpenSearch", e); + return 0; + } + } + + @Override + public List findDescendants( + final String base, + final String keyword, + final int page, + final int itemsPerPage) { + + SearchRequest request = new SearchRequest.Builder(). + index(OpenSearchUtils.getRealmIndex(AuthContextUtils.getDomain())). + searchType(SearchType.QueryThenFetch). + query(buildDescendantQuery(base, keyword)). + from(itemsPerPage * (page <= 0 ? 0 : page - 1)). + size(itemsPerPage < 0 ? indexMaxResultWindow : itemsPerPage). + sort(ES_SORT_OPTIONS_REALM). + build(); + + List result = List.of(); + try { + result = client.search(request, Void.class).hits().hits().stream(). + map(Hit::id). + collect(Collectors.toList()); + } catch (Exception e) { + LOG.error("While searching in OpenSearch", e); + } + + return result.stream().map(this::find).collect(Collectors.toList()); + } + + @Override + public List findDescendants(final String base, final String prefix) { + Query prefixQuery = new Query.Builder().disMax(QueryBuilders.disMax().queries( + new Query.Builder().term(QueryBuilders.term(). + field("fullPath").value(FieldValue.of(base)).build()).build(), + new Query.Builder().prefix(QueryBuilders.prefix(). + field("fullPath").value(SyncopeConstants.ROOT_REALM.equals(prefix) ? "/" : prefix + "/"). + build()).build()).build()).build(); + + Query query = new Query.Builder().bool(QueryBuilders.bool().must( + buildDescendantQuery(base, (String) null), + prefixQuery).build()). + build(); + + SearchRequest request = new SearchRequest.Builder(). + index(OpenSearchUtils.getRealmIndex(AuthContextUtils.getDomain())). + searchType(SearchType.QueryThenFetch). + query(query). + from(0). + size(indexMaxResultWindow). + sort(ES_SORT_OPTIONS_REALM). + build(); + + List result = List.of(); + try { + result = client.search(request, Void.class).hits().hits().stream(). + map(Hit::id). + collect(Collectors.toList()); + } catch (Exception e) { + LOG.error("While searching in OpenSearch", e); + } + return result; + } +} diff --git a/ext/opensearch/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/dao/OpenSearchAnySearchDAOTest.java b/ext/opensearch/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/dao/OpenSearchAnySearchDAOTest.java index ef6deea7ffd..2c5a5a113ef 100644 --- a/ext/opensearch/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/dao/OpenSearchAnySearchDAOTest.java +++ b/ext/opensearch/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/dao/OpenSearchAnySearchDAOTest.java @@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -113,22 +114,19 @@ protected void setupSearchDAO() { public void getAdminRealmsFilter4realm() throws IOException { // 1. mock Realm root = mock(Realm.class); - when(root.getKey()).thenReturn("rootKey"); when(root.getFullPath()).thenReturn(SyncopeConstants.ROOT_REALM); when(realmDAO.findByFullPath(SyncopeConstants.ROOT_REALM)).thenReturn(root); - when(realmDAO.findDescendants(SyncopeConstants.ROOT_REALM, null, -1, -1)).thenReturn(List.of(root)); + when(realmDAO.findDescendants(eq(SyncopeConstants.ROOT_REALM), anyString())).thenReturn(List.of("rootKey")); // 2. test Set adminRealms = Set.of(SyncopeConstants.ROOT_REALM); Triple, Set, Set> filter = searchDAO.getAdminRealmsFilter(root, true, adminRealms, AnyTypeKind.USER); - assertThat( - new Query.Builder().disMax(QueryBuilders.disMax().queries( - new Query.Builder().term(QueryBuilders.term().field("realm").value( - FieldValue.of("rootKey")).build()).build()).build()). - build()). + assertThat(new Query.Builder().disMax(QueryBuilders.disMax().queries( + new Query.Builder().term(QueryBuilders.term().field("realm").value(FieldValue.of("rootKey")).build()). + build()).build()).build()). usingRecursiveComparison().isEqualTo(filter.getLeft().get()); assertEquals(Set.of(), filter.getMiddle()); assertEquals(Set.of(), filter.getRight()); diff --git a/ext/opensearch/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/OpenSearchReindex.java b/ext/opensearch/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/OpenSearchReindex.java index 02273c540a8..782c8de24aa 100644 --- a/ext/opensearch/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/OpenSearchReindex.java +++ b/ext/opensearch/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/OpenSearchReindex.java @@ -23,6 +23,7 @@ import org.apache.syncope.core.persistence.api.dao.AnyDAO; import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO; import org.apache.syncope.core.persistence.api.dao.GroupDAO; +import org.apache.syncope.core.persistence.api.dao.RealmDAO; import org.apache.syncope.core.persistence.api.dao.UserDAO; import org.apache.syncope.core.persistence.api.entity.task.SchedTask; import org.apache.syncope.core.persistence.api.entity.task.TaskExec; @@ -61,6 +62,9 @@ public class OpenSearchReindex extends AbstractSchedTaskJobDelegate { @Autowired protected AnyObjectDAO anyObjectDAO; + @Autowired + protected RealmDAO realmDAO; + protected IndexSettings userSettings() throws IOException { return indexManager.defaultSettings(); } @@ -73,6 +77,10 @@ protected IndexSettings anyObjectSettings() throws IOException { return indexManager.defaultSettings(); } + protected IndexSettings realmSettings() throws IOException { + return indexManager.defaultSettings(); + } + protected IndexSettings auditSettings() throws IOException { return indexManager.defaultSettings(); } @@ -89,6 +97,10 @@ protected TypeMapping anyObjectMapping() throws IOException { return indexManager.defaultAnyMapping(); } + protected TypeMapping realmMapping() throws IOException { + return indexManager.defaultRealmMapping(); + } + protected TypeMapping auditMapping() throws IOException { return indexManager.defaultAuditMapping(); } @@ -179,6 +191,31 @@ protected String doExecute(final boolean dryRun, final String executor, final Jo } } + indexManager.createRealmIndex(AuthContextUtils.getDomain(), realmSettings(), realmMapping()); + + int realms = realmDAO.count(); + String rindex = OpenSearchUtils.getRealmIndex(AuthContextUtils.getDomain()); + setStatus("Indexing " + realms + " realms under " + rindex + "..."); + for (int page = 1; page <= (realms / AnyDAO.DEFAULT_PAGE_SIZE) + 1; page++) { + BulkRequest.Builder bulkRequest = new BulkRequest.Builder(); + + for (String realm : realmDAO.findAllKeys(page, AnyDAO.DEFAULT_PAGE_SIZE)) { + bulkRequest.operations(op -> op.index(idx -> idx. + index(rindex). + id(realm). + document(utils.document(realmDAO.find(realm))))); + } + + try { + BulkResponse response = client.bulk(bulkRequest.build()); + LOG.debug("Index successfully created for {} [{}/{}]: {}", + rindex, page, AnyDAO.DEFAULT_PAGE_SIZE, response); + } catch (Exception e) { + LOG.error("Could not create index for {} [{}/{}]: {}", + rindex, page, AnyDAO.DEFAULT_PAGE_SIZE, e); + } + } + indexManager.createAuditIndex(AuthContextUtils.getDomain(), auditSettings(), auditMapping()); setStatus("Rebuild indexes for domain " + AuthContextUtils.getDomain() + " successfully completed"); diff --git a/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/ITImplementationLookup.java b/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/ITImplementationLookup.java index 6d06f55848e..196db1df6ad 100644 --- a/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/ITImplementationLookup.java +++ b/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/ITImplementationLookup.java @@ -214,8 +214,6 @@ public class ITImplementationLookup implements ImplementationLookup { private final OpenSearchInit openSearchInit; - private boolean loaded; - public ITImplementationLookup( final UserWorkflowAdapter uwf, final AnySearchDAO anySearchDAO, @@ -237,11 +235,6 @@ public int getOrder() { @Override public void load(final String domain, final DataSource datasource) { - if (loaded) { - LOG.debug("Already loaded, nothing to do"); - return; - } - // in case the Flowable extension is enabled, enable modifications for test users if (enableFlowableForTestUsers != null && AopUtils.getTargetClass(uwf).getName().contains("Flowable")) { AuthContextUtils.callAsAdmin(domain, () -> { @@ -265,8 +258,6 @@ public void load(final String domain, final DataSource datasource) { return null; }); } - - loaded = true; } @Override diff --git a/src/main/asciidoc/reference-guide/concepts/extensions.adoc b/src/main/asciidoc/reference-guide/concepts/extensions.adoc index 9464c8f551c..716fab5812e 100644 --- a/src/main/asciidoc/reference-guide/concepts/extensions.adoc +++ b/src/main/asciidoc/reference-guide/concepts/extensions.adoc @@ -101,8 +101,8 @@ This extension adds features to all components and layers that are available, an ==== Elasticsearch -This extension provides an alternate internal search engine for <> and <>, -requiring an external https://www.elastic.co/[Elasticsearch^] cluster. +This extension provides an alternate internal search engine for <>,<> and +<>, requiring an external https://www.elastic.co/[Elasticsearch^] cluster. [WARNING] This extension supports Elasticsearch server versions starting from 8.x. @@ -126,8 +126,8 @@ endif::[] ==== OpenSearch -This extension provides an alternate internal search engine for <> and <>, -requiring an external https://opensearch.org/[OpenSearch^] cluster. +This extension provides an alternate internal search engine for <>,<> and +<>, requiring an external https://opensearch.org/[OpenSearch^] cluster. [TIP] As search operations are central for different aspects of the <>, the global