This is an automated email from the ASF dual-hosted git repository.

ilgrosso pushed a commit to branch 3_0_X
in repository https://gitbox.apache.org/repos/asf/syncope.git


The following commit(s) were added to refs/heads/3_0_X by this push:
     new ce5e191a49 [SYNCOPE-1787] RealmDAO for Elasticsearch and OpenSearch 
(#544)
ce5e191a49 is described below

commit ce5e191a49f368fedd468a07e5f79eefae0a7f02
Author: Francesco Chicchiriccò <ilgro...@users.noreply.github.com>
AuthorDate: Mon Nov 6 15:25:48 2023 +0100

    [SYNCOPE-1787] RealmDAO for Elasticsearch and OpenSearch (#544)
---
 .../syncope/core/persistence/api/dao/RealmDAO.java |   6 +
 .../core/persistence/jpa/PersistenceContext.java   |   4 +-
 .../core/persistence/jpa/dao/JPAAnyObjectDAO.java  |   8 +-
 .../core/persistence/jpa/dao/JPAAnySearchDAO.java  |   5 +-
 .../core/persistence/jpa/dao/JPADynRealmDAO.java   |   6 +-
 .../core/persistence/jpa/dao/JPAGroupDAO.java      |  32 +--
 .../core/persistence/jpa/dao/JPARealmDAO.java      |  61 +++++-
 .../core/persistence/jpa/dao/JPARoleDAO.java       |   6 +-
 .../core/persistence/jpa/dao/JPAUserDAO.java       |   8 +-
 ...fecycleEvent.java => EntityLifecycleEvent.java} |  14 +-
 .../AbstractPropagationTaskExecutor.java           |   4 +-
 .../java/AbstractAnyObjectWorkflowAdapter.java     |   8 +-
 .../workflow/java/AbstractUserWorkflowAdapter.java |   8 +-
 .../java/DefaultAnyObjectWorkflowAdapter.java      |  10 +-
 .../workflow/java/DefaultGroupWorkflowAdapter.java |  10 +-
 .../workflow/java/DefaultUserWorkflowAdapter.java  |  18 +-
 .../client/ElasticsearchIndexLoader.java           |   5 +
 .../client/ElasticsearchIndexManager.java          | 130 +++++++++---
 .../elasticsearch/client/ElasticsearchUtils.java   |  24 ++-
 .../jpa/ElasticsearchPersistenceContext.java       |  14 ++
 .../jpa/dao/ElasticsearchAnySearchDAO.java         |   6 +-
 .../persistence/jpa/dao/ElasticsearchRealmDAO.java | 230 +++++++++++++++++++++
 .../jpa/dao/ElasticsearchAnySearchDAOTest.java     |  12 +-
 .../java/job/ElasticsearchReindex.java             |  37 ++++
 .../core/flowable/api/UserRequestHandler.java      |   6 +-
 .../flowable/impl/FlowableUserRequestHandler.java  |  10 +-
 .../flowable/impl/FlowableUserWorkflowAdapter.java |  24 +--
 .../opensearch/client/OpenSearchIndexLoader.java   |   5 +
 .../opensearch/client/OpenSearchIndexManager.java  | 136 +++++++++---
 .../ext/opensearch/client/OpenSearchUtils.java     |  24 ++-
 .../jpa/OpenSearchPersistenceContext.java          |  14 ++
 .../jpa/dao/OpenSearchAnySearchDAO.java            |   6 +-
 .../persistence/jpa/dao/OpenSearchRealmDAO.java    | 230 +++++++++++++++++++++
 .../jpa/dao/OpenSearchAnySearchDAOTest.java        |  12 +-
 .../provisioning/java/job/OpenSearchReindex.java   |  37 ++++
 .../fit/core/reference/ITImplementationLookup.java |   9 -
 pom.xml                                            |   2 +-
 .../reference-guide/concepts/extensions.adoc       |   8 +-
 38 files changed, 1007 insertions(+), 182 deletions(-)

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 e05eb840b5..a0a573caaa 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<Realm> {
 
     List<Realm> findDescendants(String base, String keyword, int page, int 
itemsPerPage);
 
+    List<String> findDescendants(String base, String prefix);
+    
     <T extends Policy> List<Realm> findByPolicy(T policy);
 
     List<Realm> findByLogicActions(Implementation logicActions);
@@ -53,6 +55,10 @@ public interface RealmDAO extends DAO<Realm> {
 
     List<Realm> findChildren(Realm realm);
 
+    int count();
+
+    List<String> 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 e086d04846..2f3cf23387 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 class PersistenceContext {
 
     @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 1b62ce3ce3..dac06483e8 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 class JPAAnyObjectDAO extends 
AbstractAnyDAO<AnyObject> implements AnyObj
     @Override
     public Map<AnyType, Integer> 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<Object[]> results = query.getResultList();
@@ -161,7 +161,7 @@ public class JPAAnyObjectDAO extends 
AbstractAnyDAO<AnyObject> implements AnyObj
     @Override
     public Map<String, Integer> 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 class JPAAnyObjectDAO extends 
AbstractAnyDAO<AnyObject> implements AnyObj
     @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<AnyObject> findAll(final int page, final int itemsPerPage) {
         TypedQuery<AnyObject> 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 f1529b1d42..8fc9582573 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 @@ public class JPAAnySearchDAO extends AbstractAnySearchDAO {
                                 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 a575ba5358..45af5ff936 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.entity.DynRealm;
 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 @@ public class JPADynRealmDAO extends AbstractDAO<DynRealm> 
implements DynRealmDAO
             }
             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 class JPADynRealmDAO extends AbstractDAO<DynRealm> 
implements DynRealmDAO
             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 31d08bd2af..088d58fc42 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.JPAGroup;
 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 class JPAGroupDAO extends AbstractAnyDAO<Group> 
implements GroupDAO {
     @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<String, Integer> 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<Object[]> results = query.getResultList();
@@ -277,7 +277,7 @@ public class JPAGroupDAO extends AbstractAnyDAO<Group> 
implements GroupDAO {
     @Override
     public List<Group> findAll(final int page, final int itemsPerPage) {
         TypedQuery<Group> 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 class JPAGroupDAO extends AbstractAnyDAO<Group> 
implements GroupDAO {
                     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 class JPAGroupDAO extends AbstractAnyDAO<Group> 
implements GroupDAO {
                     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 class JPAGroupDAO extends AbstractAnyDAO<Group> 
implements GroupDAO {
 
             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 class JPAGroupDAO extends AbstractAnyDAO<Group> 
implements GroupDAO {
 
             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 class JPAGroupDAO extends AbstractAnyDAO<Group> 
implements GroupDAO {
                 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 class JPAGroupDAO extends AbstractAnyDAO<Group> 
implements GroupDAO {
         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 class JPAGroupDAO extends AbstractAnyDAO<Group> 
implements GroupDAO {
                 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 class JPAGroupDAO extends AbstractAnyDAO<Group> 
implements GroupDAO {
         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 db8bb71b0b..a552156d1c 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
@@ -20,6 +20,7 @@ package org.apache.syncope.core.persistence.jpa.dao;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.stream.Collectors;
 import javax.persistence.NoResultException;
 import javax.persistence.Query;
 import javax.persistence.TypedQuery;
@@ -41,14 +42,21 @@ import 
org.apache.syncope.core.persistence.api.entity.policy.PropagationPolicy;
 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<Realm> 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 class JPARealmDAO extends AbstractDAO<Realm> 
implements RealmDAO {
         return query.getResultList();
     }
 
+    @Override
+    public List<String> findDescendants(final String base, final String 
prefix) {
+        List<Object> parameters = new ArrayList<>();
+
+        StringBuilder queryString = buildDescendantQuery(base, null, 
parameters);
+        TypedQuery<Realm> 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 <T extends Policy> List<Realm> findSamePolicyChildren(final 
Realm realm, final T policy) {
         List<Realm> result = new ArrayList<>();
 
@@ -284,6 +313,30 @@ public class JPARealmDAO extends AbstractDAO<Realm> 
implements RealmDAO {
                 : 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<String> 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<Object> 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 class JPARealmDAO extends AbstractDAO<Realm> 
implements RealmDAO {
             findChildren(realm).forEach(this::save);
         }
 
+        publisher.publishEvent(
+                new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, merged, 
AuthContextUtils.getDomain()));
+
         return merged;
     }
 
@@ -313,6 +369,9 @@ public class JPARealmDAO extends AbstractDAO<Realm> 
implements RealmDAO {
             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 03d80be7d7..db34a8ab26 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.SearchCondConverter;
 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 class JPARoleDAO extends AbstractDAO<Role> 
implements RoleDAO {
                 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 class JPARoleDAO extends AbstractDAO<Role> 
implements RoleDAO {
         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 4f6a575040..b78401d0d3 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 class JPAUserDAO extends AbstractAnyDAO<User> 
implements UserDAO {
     @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<String, Integer> 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<Object[]> results = query.getResultList();
@@ -166,7 +166,7 @@ public class JPAUserDAO extends AbstractAnyDAO<User> 
implements UserDAO {
     @Override
     public Map<String, Integer> 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<Object[]> results = query.getResultList();
@@ -270,7 +270,7 @@ public class JPAUserDAO extends AbstractAnyDAO<User> 
implements UserDAO {
     @Override
     public List<User> findAll(final int page, final int itemsPerPage) {
         TypedQuery<User> 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 8fbc75f1c3..faab2acc76 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<A extends Any<?>> extends ApplicationEvent {
+public class EntityLifecycleEvent<E extends Entity> 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 class AnyLifecycleEvent<A extends Any<?>> extends 
ApplicationEvent {
         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 a080189dd8..a20aa19cc3 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.Connector;
 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 @@ public abstract class AbstractPropagationTaskExecutor 
implements PropagationTask
                             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 a5cf36a090..35158dca12 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.anyobject.AnyObject;
 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 abstract class AbstractAnyObjectWorkflowAdapter
 
         // 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 abstract class AbstractAnyObjectWorkflowAdapter
         // 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 abstract class AbstractAnyObjectWorkflowAdapter
         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 669b80e9a5..1626dfbabe 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.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.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 abstract class AbstractUserWorkflowAdapter extends 
AbstractWorkflowAdapte
 
         // 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 abstract class AbstractUserWorkflowAdapter extends 
AbstractWorkflowAdapte
         // 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 abstract class AbstractUserWorkflowAdapter extends 
AbstractWorkflowAdapte
         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 c2bc29dd19..2f787eae1a 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.persistence.api.entity.anyobject.AnyObject;
 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 @@ public class DefaultAnyObjectWorkflowAdapter extends 
AbstractAnyObjectWorkflowAd
         anyObject = anyObjectDAO.save(anyObject);
 
         publisher.publishEvent(
-                new AnyLifecycleEvent<>(this, SyncDeltaType.CREATE, anyObject, 
AuthContextUtils.getDomain()));
+                new EntityLifecycleEvent<>(this, SyncDeltaType.CREATE, 
anyObject, AuthContextUtils.getDomain()));
 
         PropagationByResource<String> propByRes = new 
PropagationByResource<>();
         propByRes.set(ResourceOperation.CREATE, 
anyObjectDAO.findAllResourceKeys(anyObject.getKey()));
@@ -74,8 +74,8 @@ public class DefaultAnyObjectWorkflowAdapter extends 
AbstractAnyObjectWorkflowAd
         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 @@ public class DefaultAnyObjectWorkflowAdapter extends 
AbstractAnyObjectWorkflowAd
         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 3c1175385d..2cab7cdd42 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.persistence.api.entity.group.Group;
 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 @@ public class DefaultGroupWorkflowAdapter extends 
AbstractGroupWorkflowAdapter {
         group = groupDAO.saveAndRefreshDynMemberships(group);
 
         publisher.publishEvent(
-                new AnyLifecycleEvent<>(this, SyncDeltaType.CREATE, group, 
AuthContextUtils.getDomain()));
+                new EntityLifecycleEvent<>(this, SyncDeltaType.CREATE, group, 
AuthContextUtils.getDomain()));
 
         PropagationByResource<String> propByRes = new 
PropagationByResource<>();
         propByRes.set(ResourceOperation.CREATE, 
groupDAO.findAllResourceKeys(group.getKey()));
@@ -70,8 +70,8 @@ public class DefaultGroupWorkflowAdapter extends 
AbstractGroupWorkflowAdapter {
         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 @@ public class DefaultGroupWorkflowAdapter extends 
AbstractGroupWorkflowAdapter {
         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 977f162c6d..36509fbc05 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.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.provisioning.api.rules.RuleEnforcer;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
 import org.apache.syncope.core.spring.security.SecurityProperties;
@@ -91,7 +91,7 @@ public class DefaultUserWorkflowAdapter extends 
AbstractUserWorkflowAdapter {
         user = userDAO.save(user);
 
         publisher.publishEvent(
-                new AnyLifecycleEvent<>(this, SyncDeltaType.CREATE, user, 
AuthContextUtils.getDomain()));
+                new EntityLifecycleEvent<>(this, SyncDeltaType.CREATE, user, 
AuthContextUtils.getDomain()));
 
         PropagationByResource<String> propByRes = new 
PropagationByResource<>();
         propByRes.set(ResourceOperation.CREATE, 
userDAO.findAllResourceKeys(user.getKey()));
@@ -122,7 +122,7 @@ public class DefaultUserWorkflowAdapter extends 
AbstractUserWorkflowAdapter {
         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 @@ public class DefaultUserWorkflowAdapter extends 
AbstractUserWorkflowAdapter {
         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 @@ public class DefaultUserWorkflowAdapter extends 
AbstractUserWorkflowAdapter {
         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 @@ public class DefaultUserWorkflowAdapter extends 
AbstractUserWorkflowAdapter {
         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 @@ public class DefaultUserWorkflowAdapter extends 
AbstractUserWorkflowAdapter {
         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 @@ public class DefaultUserWorkflowAdapter extends 
AbstractUserWorkflowAdapter {
         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 c3fe69d34d..93e55ed7b5 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 class ElasticsearchIndexLoader implements 
SyncopeCoreLoader {
                         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 c426431b48..919ff7312f 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 @@ package org.apache.syncope.ext.elasticsearch.client;
 
 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.List;
 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 class ElasticsearchIndexManager {
                 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 class ElasticsearchIndexManager {
                 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 class ElasticsearchIndexManager {
         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 class ElasticsearchIndexManager {
     }
 
     @TransactionalEventListener
-    public void any(final AnyLifecycleEvent<Any<?>> 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<Map<String, Object>> request = new 
IndexRequest.Builder<Map<String, Object>>().
-                    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<Entity> 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<Map<String, Object>> request = new 
IndexRequest.Builder<Map<String, Object>>().
+                        
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<Map<String, Object>> request = new 
IndexRequest.Builder<Map<String, Object>>().
+                        
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<Map<String, Object>> request = new 
IndexRequest.Builder<Map<String, Object>>().
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 1326487fd5..f4e17e8da8 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.AnyTypeClass;
 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 class ElasticsearchUtils {
         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 class ElasticsearchUtils {
             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 class ElasticsearchUtils {
             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 @@ public class ElasticsearchUtils {
     protected void customizeDocument(final Map<String, Object> builder, final 
User user) {
     }
 
+    public Map<String, Object> document(final Realm realm) {
+        Map<String, Object> 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<String, Object> builder, final 
Realm realm) {
+    }
+
     public Map<String, Object> 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 88e9e1e160..fb6f7cc495 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.DynRealmDAO;
 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 class ElasticsearchPersistenceContext {
                 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 31b4615658..1f28a03757 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 @@ public class ElasticsearchAnySearchDAO extends 
AbstractAnySearchDAO {
                         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 0000000000..4e618ee430
--- /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<SortOptions> 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<String> 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<Realm> findByName(final String name) {
+        List<String> 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<Realm> findChildren(final Realm realm) {
+        List<String> 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<Realm> 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<String> 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<String> 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<String> 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 02619dc813..03b77935fe 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.assertEquals;
 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 @@ public class ElasticsearchAnySearchDAOTest {
     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<String> adminRealms = Set.of(SyncopeConstants.ROOT_REALM);
         Triple<Optional<Query>, Set<String>, Set<String>> 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 1756cbe87f..c11a9f4c2d 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.common.lib.types.AnyTypeKind;
 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<SchedTask
     @Autowired
     protected AnyObjectDAO anyObjectDAO;
 
+    @Autowired
+    protected RealmDAO realmDAO;
+
     protected IndexSettings userSettings() throws IOException {
         return indexManager.defaultSettings();
     }
@@ -73,6 +77,10 @@ public class ElasticsearchReindex extends 
AbstractSchedTaskJobDelegate<SchedTask
         return indexManager.defaultSettings();
     }
 
+    protected IndexSettings realmSettings() throws IOException {
+        return indexManager.defaultSettings();
+    }
+
     protected IndexSettings auditSettings() throws IOException {
         return indexManager.defaultSettings();
     }
@@ -89,6 +97,10 @@ public class ElasticsearchReindex extends 
AbstractSchedTaskJobDelegate<SchedTask
         return indexManager.defaultAnyMapping();
     }
 
+    protected TypeMapping realmMapping() throws IOException {
+        return indexManager.defaultRealmMapping();
+    }
+
     protected TypeMapping auditMapping() throws IOException {
         return indexManager.defaultAuditMapping();
     }
@@ -179,6 +191,31 @@ public class ElasticsearchReindex extends 
AbstractSchedTaskJobDelegate<SchedTask
                     }
                 }
 
+                indexManager.createRealmIndex(AuthContextUtils.getDomain(), 
realmSettings(), realmMapping());
+
+                int realms = realmDAO.count();
+                String rindex = 
ElasticsearchUtils.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/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 94a80a0717..f100407057 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.UserRequest;
 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 @@ public interface UserRequestHandler {
      * @param event delete event
      */
     @TransactionalEventListener
-    void cancelByUser(AnyLifecycleEvent<Any<?>> event);
+    void cancelByUser(EntityLifecycleEvent<Entity> 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 1ab103551e..eb7498e043 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.flowable.support.DomainProcessEngine;
 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 class FlowableUserRequestHandler implements 
UserRequestHandler {
     }
 
     @Override
-    public void cancelByUser(final AnyLifecycleEvent<Any<?>> event) {
+    public void cancelByUser(final EntityLifecycleEvent<Entity> 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 46fa89f022..8989ce0b3a 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.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.provisioning.api.rules.RuleEnforcer;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
 import org.apache.syncope.core.spring.security.SecurityProperties;
@@ -152,7 +152,7 @@ public class FlowableUserWorkflowAdapter extends 
AbstractUserWorkflowAdapter imp
         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 @@ public class FlowableUserWorkflowAdapter extends 
AbstractUserWorkflowAdapter imp
         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 @@ public class FlowableUserWorkflowAdapter extends 
AbstractUserWorkflowAdapter imp
         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 @@ public class FlowableUserWorkflowAdapter extends 
AbstractUserWorkflowAdapter imp
         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<String> propByRes = 
engine.getRuntimeService().getVariable(
@@ -377,7 +377,7 @@ public class FlowableUserWorkflowAdapter extends 
AbstractUserWorkflowAdapter imp
         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<String> propByRes = 
engine.getRuntimeService().getVariable(
@@ -413,7 +413,7 @@ public class FlowableUserWorkflowAdapter extends 
AbstractUserWorkflowAdapter imp
         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 @@ public class FlowableUserWorkflowAdapter extends 
AbstractUserWorkflowAdapter imp
         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 @@ public class FlowableUserWorkflowAdapter extends 
AbstractUserWorkflowAdapter imp
             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 @@ public class FlowableUserWorkflowAdapter extends 
AbstractUserWorkflowAdapter imp
             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 class FlowableUserWorkflowAdapter extends 
AbstractUserWorkflowAdapter imp
         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 class FlowableUserWorkflowAdapter extends 
AbstractUserWorkflowAdapter imp
             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 49f8b21370..e6cde59f4c 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 class OpenSearchIndexLoader implements 
SyncopeCoreLoader {
                         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 fb5f57581e..ebd016ff22 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.List;
 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 class OpenSearchIndexManager {
             final String numberOfReplicas) {
 
         this.client = client;
-        this.ppenSearchUtils = ppenSearchUtils;
+        this.openSearchUtils = ppenSearchUtils;
         this.numberOfShards = numberOfShards;
         this.numberOfReplicas = numberOfReplicas;
     }
@@ -85,6 +88,12 @@ public class OpenSearchIndexManager {
                 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 class OpenSearchIndexManager {
                 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 class OpenSearchIndexManager {
         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 class OpenSearchIndexManager {
     }
 
     @TransactionalEventListener
-    public void any(final AnyLifecycleEvent<Any<?>> 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<Map<String, Object>> request = new 
IndexRequest.Builder<Map<String, Object>>().
-                    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<Entity> 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<Map<String, Object>> request = new 
IndexRequest.Builder<Map<String, Object>>().
+                        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<Map<String, Object>> request = new 
IndexRequest.Builder<Map<String, Object>>().
+                        
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<Map<String, Object>> request = new 
IndexRequest.Builder<Map<String, Object>>().
                 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 7329456237..2c6902c1dd 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.AnyTypeClass;
 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 class OpenSearchUtils {
         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 class OpenSearchUtils {
             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 class OpenSearchUtils {
             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 @@ public class OpenSearchUtils {
     protected void customizeDocument(final Map<String, Object> builder, final 
User user) {
     }
 
+    public Map<String, Object> document(final Realm realm) {
+        Map<String, Object> 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<String, Object> builder, final 
Realm realm) {
+    }
+
     public Map<String, Object> 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 b5fbebdb73..8725d1e2e8 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.DynRealmDAO;
 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 class OpenSearchPersistenceContext {
                 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 447e168e10..b138a3a977 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 @@ public class OpenSearchAnySearchDAO extends 
AbstractAnySearchDAO {
                         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 0000000000..2fc2de406f
--- /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<SortOptions> 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<String> 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<Realm> findByName(final String name) {
+        List<String> 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<Realm> findChildren(final Realm realm) {
+        List<String> 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<Realm> 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<String> 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<String> 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<String> 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 ef6deea7ff..2c5a5a113e 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.assertEquals;
 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 @@ public class OpenSearchAnySearchDAOTest {
     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<String> adminRealms = Set.of(SyncopeConstants.ROOT_REALM);
         Triple<Optional<Query>, Set<String>, Set<String>> 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 02273c540a..782c8de24a 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.common.lib.types.AnyTypeKind;
 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<SchedTask> {
     @Autowired
     protected AnyObjectDAO anyObjectDAO;
 
+    @Autowired
+    protected RealmDAO realmDAO;
+
     protected IndexSettings userSettings() throws IOException {
         return indexManager.defaultSettings();
     }
@@ -73,6 +77,10 @@ public class OpenSearchReindex extends 
AbstractSchedTaskJobDelegate<SchedTask> {
         return indexManager.defaultSettings();
     }
 
+    protected IndexSettings realmSettings() throws IOException {
+        return indexManager.defaultSettings();
+    }
+
     protected IndexSettings auditSettings() throws IOException {
         return indexManager.defaultSettings();
     }
@@ -89,6 +97,10 @@ public class OpenSearchReindex extends 
AbstractSchedTaskJobDelegate<SchedTask> {
         return indexManager.defaultAnyMapping();
     }
 
+    protected TypeMapping realmMapping() throws IOException {
+        return indexManager.defaultRealmMapping();
+    }
+
     protected TypeMapping auditMapping() throws IOException {
         return indexManager.defaultAuditMapping();
     }
@@ -179,6 +191,31 @@ public class OpenSearchReindex extends 
AbstractSchedTaskJobDelegate<SchedTask> {
                     }
                 }
 
+                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 6d06f55848..196db1df6a 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 class ITImplementationLookup implements 
ImplementationLookup {
 
     @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 class ITImplementationLookup implements 
ImplementationLookup {
                 return null;
             });
         }
-
-        loaded = true;
     }
 
     @Override
diff --git a/pom.xml b/pom.xml
index 2f97bcf3b4..4017c76d6c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -432,7 +432,7 @@ under the License.
     <opensearch.version>2.11.0</opensearch.version>
     <opensearch-java.version>2.8.0</opensearch-java.version>
 
-    <log4j2.version>2.21.0</log4j2.version>
+    <log4j2.version>2.21.1</log4j2.version>
     <disruptor.version>3.4.4</disruptor.version>
 
     <commons-jexl.version>3.3</commons-jexl.version>
diff --git a/src/main/asciidoc/reference-guide/concepts/extensions.adoc 
b/src/main/asciidoc/reference-guide/concepts/extensions.adoc
index f920565bf6..c050ef0111 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 
<<users-groups-and-any-objects>> and <<audit-events>>,
-requiring an external https://www.elastic.co/[Elasticsearch^] cluster.
+This extension provides an alternate internal search engine for 
<<users-groups-and-any-objects>>,<<realms>> and
+<<audit-events>>, 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 
<<users-groups-and-any-objects>> and <<audit-events>>,
-requiring an external https://opensearch.org/[OpenSearch^] cluster.
+This extension provides an alternate internal search engine for 
<<users-groups-and-any-objects>>,<<realms>> and
+<<audit-events>>, requiring an external https://opensearch.org/[OpenSearch^] 
cluster.
 
 [TIP]
 As search operations are central for different aspects of the 
<<provisioning,provisioning process>>, the global

Reply via email to