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

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


The following commit(s) were added to refs/heads/master by this push:
     new a958faa821 [SYNCOPE-1830] added support for search on membership 
attributes on Elasticsearch and Opensearch (#863)
a958faa821 is described below

commit a958faa8217ed458c021f6d8d125de83e6145733
Author: Andrea Patricelli <[email protected]>
AuthorDate: Tue Oct 15 15:40:53 2024 +0200

    [SYNCOPE-1830] added support for search on membership attributes on 
Elasticsearch and Opensearch (#863)
---
 .../src/test/resources/domains/MasterContent.xml   |  3 ++
 .../src/test/resources/domains/MasterContent.xml   |  3 ++
 .../elasticsearch/client/ElasticsearchUtils.java   | 27 +++++++++++
 .../ext/opensearch/client/OpenSearchUtils.java     | 27 +++++++++++
 .../syncope/fit/core/LinkedAccountITCase.java      |  8 ++--
 .../apache/syncope/fit/core/MembershipITCase.java  |  2 +-
 .../org/apache/syncope/fit/core/SearchITCase.java  | 55 ++++++++++++++++++++++
 7 files changed, 120 insertions(+), 5 deletions(-)

diff --git a/core/persistence-jpa/src/test/resources/domains/MasterContent.xml 
b/core/persistence-jpa/src/test/resources/domains/MasterContent.xml
index 4b3244d439..76c056f9b1 100644
--- a/core/persistence-jpa/src/test/resources/domains/MasterContent.xml
+++ b/core/persistence-jpa/src/test/resources/domains/MasterContent.xml
@@ -384,6 +384,9 @@ under the License.
                 realm_id="e4c28e7a-9dbf-4ee7-9441-93812a0d4a28"
                 creator="admin" lastModifier="admin" 
                 creationDate="2010-10-20 11:00:00" lastChangeDate="2010-10-20 
11:00:00"/>
+  <TypeExtension id="88a71478-30aa-4ee0-8b2b-6cb32e7ba264"
+                 group_id="f779c0d4-633b-4be5-8f57-32eb478a3ca5" 
anyType_id="PRINTER"/>
+  <TypeExtension_AnyTypeClass 
typeExtension_id="88a71478-30aa-4ee0-8b2b-6cb32e7ba264" 
anyTypeClass_id="other"/>
   <SyncopeGroup id="0cbcabd2-4410-4b6b-8f05-a052b451d18f" 
name="groupForWorkflowApproval"
                 realm_id="e4c28e7a-9dbf-4ee7-9441-93812a0d4a28"
                 creator="admin" lastModifier="admin" 
diff --git 
a/core/persistence-neo4j/src/test/resources/domains/MasterContent.xml 
b/core/persistence-neo4j/src/test/resources/domains/MasterContent.xml
index 446550335a..233e9f3d25 100644
--- a/core/persistence-neo4j/src/test/resources/domains/MasterContent.xml
+++ b/core/persistence-neo4j/src/test/resources/domains/MasterContent.xml
@@ -384,6 +384,9 @@ under the License.
                 creator="admin" lastModifier="admin" 
                 creationDate="2010-10-20 11:00:00" lastChangeDate="2010-10-20 
11:00:00"/>
   <SyncopeGroup_Realm left="f779c0d4-633b-4be5-8f57-32eb478a3ca5" 
right="e4c28e7a-9dbf-4ee7-9441-93812a0d4a28"/>
+  <TypeExtension id="88a71478-30aa-4ee0-8b2b-6cb32e7ba264"
+                 group_id="f779c0d4-633b-4be5-8f57-32eb478a3ca5" 
anyType_id="PRINTER"/>
+  <TypeExtension_AnyTypeClass 
typeExtension_id="88a71478-30aa-4ee0-8b2b-6cb32e7ba264" 
anyTypeClass_id="other"/>
   <SyncopeGroup id="0cbcabd2-4410-4b6b-8f05-a052b451d18f" 
name="groupForWorkflowApproval"
                 creator="admin" lastModifier="admin" 
                 creationDate="2010-10-20 11:00:00" lastChangeDate="2010-10-20 
11:00:00"/>
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 31ea96b55f..621f0723ac 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
@@ -20,6 +20,7 @@ package org.apache.syncope.ext.elasticsearch.client;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -35,10 +36,14 @@ import org.apache.syncope.core.persistence.api.dao.UserDAO;
 import org.apache.syncope.core.persistence.api.entity.Any;
 import org.apache.syncope.core.persistence.api.entity.AnyTypeClass;
 import org.apache.syncope.core.persistence.api.entity.AuditEvent;
+import org.apache.syncope.core.persistence.api.entity.GroupablePlainAttr;
+import org.apache.syncope.core.persistence.api.entity.GroupableRelatable;
+import org.apache.syncope.core.persistence.api.entity.Membership;
 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.Relationship;
 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;
@@ -98,6 +103,7 @@ public class ElasticsearchUtils {
      * @param any user, group or any object to index
      * @return document specialized with content from the provided any
      */
+    @SuppressWarnings("unchecked")
     @Transactional
     public Map<String, Object> document(final Any<?> any) {
         Map<String, Object> builder = new HashMap<>();
@@ -200,6 +206,27 @@ public class ElasticsearchUtils {
             builder.put(plainAttr.getSchema().getKey(), values.size() == 1 ? 
values.get(0) : values);
         }
 
+        // add also flattened membership attributes
+        if (any instanceof GroupableRelatable) {
+            GroupableRelatable<? extends Any, ? extends Membership, ? extends 
GroupablePlainAttr, ? extends Any, ? 
+                    extends Relationship> entity = 
GroupableRelatable.class.cast(any);
+            entity.getMemberships().forEach(m -> 
entity.getPlainAttrs(m).forEach(mAttr -> {
+                List<Object> values = 
mAttr.getValues().stream().map(PlainAttrValue::getValue)
+                        .collect(Collectors.toList());
+
+                Optional.ofNullable(mAttr.getUniqueValue()).ifPresent(v -> 
values.add(v.getValue()));
+
+                Object attr = 
builder.computeIfAbsent(mAttr.getSchema().getKey(), k -> new HashSet<>());
+                // also support case in which there is also an existing 
attribute set previously
+                if (attr instanceof Collection) {
+                    ((Collection<Object>) attr).addAll(values);
+                } else {
+                    values.add(attr);
+                    builder.put(mAttr.getSchema().getKey(), values.size() == 1 
? values.get(0) : values);
+                }
+            }));
+        }
+        
         return builder;
     }
 
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 1794ada4a6..6ef66d10ae 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
@@ -20,6 +20,7 @@ package org.apache.syncope.ext.opensearch.client;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -35,10 +36,14 @@ import org.apache.syncope.core.persistence.api.dao.UserDAO;
 import org.apache.syncope.core.persistence.api.entity.Any;
 import org.apache.syncope.core.persistence.api.entity.AnyTypeClass;
 import org.apache.syncope.core.persistence.api.entity.AuditEvent;
+import org.apache.syncope.core.persistence.api.entity.GroupablePlainAttr;
+import org.apache.syncope.core.persistence.api.entity.GroupableRelatable;
+import org.apache.syncope.core.persistence.api.entity.Membership;
 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.Relationship;
 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;
@@ -98,6 +103,7 @@ public class OpenSearchUtils {
      * @param any user, group or any object to index
      * @return document specialized with content from the provided any
      */
+    @SuppressWarnings("unchecked")
     @Transactional
     public Map<String, Object> document(final Any<?> any) {
         Map<String, Object> builder = new HashMap<>();
@@ -200,6 +206,27 @@ public class OpenSearchUtils {
             builder.put(plainAttr.getSchema().getKey(), values.size() == 1 ? 
values.get(0) : values);
         }
 
+        // add also flattened membership attributes
+        if (any instanceof GroupableRelatable) {
+            GroupableRelatable<? extends Any, ? extends Membership, ? extends 
GroupablePlainAttr, ? extends Any, ?
+                    extends Relationship> entity = 
GroupableRelatable.class.cast(any);
+            entity.getMemberships().forEach(m -> 
entity.getPlainAttrs(m).forEach(mAttr -> {
+                List<Object> values = 
mAttr.getValues().stream().map(PlainAttrValue::getValue)
+                        .collect(Collectors.toList());
+
+                Optional.ofNullable(mAttr.getUniqueValue()).ifPresent(v -> 
values.add(v.getValue()));
+
+                Object attr = 
builder.computeIfAbsent(mAttr.getSchema().getKey(), k -> new HashSet<>());
+                // also support case in which there is also an existing 
attribute set previously
+                if (attr instanceof Collection) {
+                    ((Collection<Object>) attr).addAll(values);
+                } else {
+                    values.add(attr);
+                    builder.put(mAttr.getSchema().getKey(), values.size() == 1 
? values.get(0) : values);
+                }
+            }));
+        }
+        
         return builder;
     }
 
diff --git 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/LinkedAccountITCase.java
 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/LinkedAccountITCase.java
index 819b74101b..61b38a3ba1 100644
--- 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/LinkedAccountITCase.java
+++ 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/LinkedAccountITCase.java
@@ -18,6 +18,7 @@
  */
 package org.apache.syncope.fit.core;
 
+import static org.awaitility.Awaitility.await;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
@@ -377,10 +378,9 @@ public class LinkedAccountITCase extends AbstractITCase {
             assertEquals(1, task.getExecutions().size());
             assertEquals(ExecStatus.SUCCESS.name(), 
task.getExecutions().get(0).getStatus());
 
-            tasks = TASK_SERVICE.search(
-                    new 
TaskQuery.Builder(TaskType.PROPAGATION).resource(RESOURCE_NAME_REST).
-                            
anyTypeKind(AnyTypeKind.USER).entityKey(user.getKey()).build());
-            assertEquals(3, tasks.getTotalCount());
+            await().until(() -> TASK_SERVICE.search(
+                    new 
TaskQuery.Builder(TaskType.PROPAGATION).resource(RESOURCE_NAME_REST)
+                            
.anyTypeKind(AnyTypeKind.USER).entityKey(user.getKey()).build()).getTotalCount()
 == 3);
 
             // 6. verify that both user and account are now found on resource
             response = webClient.get();
diff --git 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MembershipITCase.java
 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MembershipITCase.java
index 7b52447845..8575e5ebaa 100644
--- 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MembershipITCase.java
+++ 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MembershipITCase.java
@@ -105,7 +105,7 @@ public class MembershipITCase extends AbstractITCase {
             assertEquals(1, 
membership.getPlainAttr("aLong").orElseThrow().getValues().size());
             assertEquals("1977", 
membership.getPlainAttr("aLong").orElseThrow().getValues().get(0));
 
-            // 3. verify that derived attrbutes from 'csv' and 'other' are 
also populated for user's membership
+            // 3. verify that derived attributes from 'csv' and 'other' are 
also populated for user's membership
             
assertFalse(membership.getDerAttr("csvuserid").orElseThrow().getValues().isEmpty());
             
assertFalse(membership.getDerAttr("noschema").orElseThrow().getValues().isEmpty());
 
diff --git 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java
 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java
index 6c7870945d..4609efbf09 100644
--- 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java
+++ 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java
@@ -18,6 +18,7 @@
  */
 package org.apache.syncope.fit.core;
 
+import static org.awaitility.Awaitility.await;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
@@ -42,6 +43,7 @@ import org.apache.syncope.common.lib.request.AnyObjectCR;
 import org.apache.syncope.common.lib.request.AnyObjectUR;
 import org.apache.syncope.common.lib.request.AttrPatch;
 import org.apache.syncope.common.lib.request.GroupCR;
+import org.apache.syncope.common.lib.request.GroupUR;
 import org.apache.syncope.common.lib.request.MembershipUR;
 import org.apache.syncope.common.lib.request.UserCR;
 import org.apache.syncope.common.lib.request.UserUR;
@@ -53,6 +55,7 @@ import org.apache.syncope.common.lib.to.PagedConnObjectResult;
 import org.apache.syncope.common.lib.to.PagedResult;
 import org.apache.syncope.common.lib.to.RealmTO;
 import org.apache.syncope.common.lib.to.RoleTO;
+import org.apache.syncope.common.lib.to.TypeExtensionTO;
 import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
@@ -1058,4 +1061,56 @@ public class SearchITCase extends AbstractITCase {
         }
     }
 
+    @Test
+    void userByMembershipAttribute() {
+        // search user by membership attribute
+        UserTO puccini = USER_SERVICE.read("puccini");
+        GroupTO additional = GROUP_SERVICE.read("additional");
+        GroupTO employee = GROUP_SERVICE.read("employee");
+        TypeExtensionTO typeExtensionTO = new TypeExtensionTO();
+        typeExtensionTO.setAnyType(AnyTypeKind.USER.name());
+        typeExtensionTO.getAuxClasses().add("other");
+        updateGroup(new 
GroupUR.Builder(employee.getKey()).typeExtension(typeExtensionTO).build());
+        // add a membership and its plain attribute
+        updateUser(new UserUR.Builder(puccini.getKey())
+                .plainAttr(attrAddReplacePatch("ctype", "myownctype"))
+                .memberships(
+                new 
MembershipUR.Builder(additional.getKey()).plainAttrs(attr("ctype", 
"additionalctype"))
+                        .build(), new MembershipUR.Builder(employee.getKey())
+                                .plainAttrs(attr("ctype", 
"additionalemployeectype"))
+                                .build()).build());
+        await().until(() -> USER_SERVICE.search(new 
AnyQuery.Builder().page(1).size(10)
+                
.fiql(SyncopeClient.getUserSearchConditionBuilder().is("ctype").equalTo("additionalctype").query())
+                .build()).getTotalCount() == 1);
+        assertTrue(USER_SERVICE.search(new AnyQuery.Builder().page(1).size(10)
+                
.fiql(SyncopeClient.getUserSearchConditionBuilder().is("ctype").equalTo("additionalctype").query())
+                .build()).getResult().stream().anyMatch(u -> 
"puccini".equals(u.getUsername())));
+        assertTrue(USER_SERVICE.search(new AnyQuery.Builder().page(1).size(10)
+                
.fiql(SyncopeClient.getUserSearchConditionBuilder().is("ctype").equalTo("additionalemployeectype")
+                        .query()).build()).getResult().stream().anyMatch(u -> 
"puccini".equals(u.getUsername())));
+        // check also that search on user plain attribute (not in membership) 
works
+        assertTrue(USER_SERVICE.search(new AnyQuery.Builder().page(1).size(10)
+                
.fiql(SyncopeClient.getUserSearchConditionBuilder().is("ctype").equalTo("myownctype").query())
+                .build()).getResult().stream().anyMatch(u -> 
"puccini".equals(u.getUsername())));
+    }
+    
+    @Test
+    void anyObjectByMembershipAttribute() {
+        // search user by membership attribute
+        AnyObjectTO canonMf = 
ANY_OBJECT_SERVICE.read("8559d14d-58c2-46eb-a2d4-a7d35161e8f8");
+        GroupTO otherchild = GROUP_SERVICE.read("otherchild");
+        // add a membership and its plain attribute
+        updateAnyObject(new AnyObjectUR.Builder(canonMf.getKey()).memberships(
+                new 
MembershipUR.Builder(otherchild.getKey()).plainAttrs(attr("ctype", 
"otherchildctype"))
+                        .build()).build());
+        await().until(() -> ANY_OBJECT_SERVICE.search(new 
AnyQuery.Builder().page(1).size(10)
+                
.fiql(SyncopeClient.getAnyObjectSearchConditionBuilder(PRINTER).is("ctype").equalTo("otherchildctype")
+                        .query()).build()).getTotalCount() == 1);
+        assertTrue(ANY_OBJECT_SERVICE.search(new 
AnyQuery.Builder().page(1).size(10)
+                        
.fiql(SyncopeClient.getAnyObjectSearchConditionBuilder(PRINTER).is("ctype").equalTo(
+                                        "otherchildctype")
+                                .query()).build()).getResult().stream()
+                .anyMatch(u -> 
"8559d14d-58c2-46eb-a2d4-a7d35161e8f8".equals(u.getKey())));
+    }
+
 }

Reply via email to