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

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


The following commit(s) were added to refs/heads/4_0_X by this push:
     new bf7138a048 [SYNCOPE-1926] optimize user updates on SCIM PATCH (#1226)
bf7138a048 is described below

commit bf7138a048bc6b45b8b7719b4220ef2d9ad18f25
Author: Samuel Garofalo <[email protected]>
AuthorDate: Tue Nov 4 13:45:35 2025 +0100

    [SYNCOPE-1926] optimize user updates on SCIM PATCH (#1226)
---
 .../apache/syncope/core/logic/SCIMDataBinder.java  | 709 ++++++++++++++++-----
 .../syncope/core/logic/SCIMDataBinderTest.java     | 276 +++++++-
 .../scimv2/cxf/service/SCIMUserServiceImpl.java    |  13 +-
 3 files changed, 817 insertions(+), 181 deletions(-)

diff --git 
a/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/SCIMDataBinder.java
 
b/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/SCIMDataBinder.java
index 8f414c475a..22eb5dda26 100644
--- 
a/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/SCIMDataBinder.java
+++ 
b/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/SCIMDataBinder.java
@@ -29,8 +29,10 @@ import java.util.Set;
 import org.apache.commons.jexl3.MapContext;
 import org.apache.commons.lang3.BooleanUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.mutable.Mutable;
+import org.apache.commons.lang3.mutable.MutableInt;
+import org.apache.commons.lang3.mutable.MutableObject;
 import org.apache.commons.lang3.tuple.Pair;
-import org.apache.syncope.common.lib.AnyOperations;
 import org.apache.syncope.common.lib.Attr;
 import org.apache.syncope.common.lib.EntityTOUtils;
 import org.apache.syncope.common.lib.SyncopeConstants;
@@ -39,8 +41,10 @@ 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.PasswordPatch;
 import org.apache.syncope.common.lib.request.StatusR;
+import org.apache.syncope.common.lib.request.StringPatchItem;
 import org.apache.syncope.common.lib.request.StringReplacePatchItem;
 import org.apache.syncope.common.lib.request.UserCR;
 import org.apache.syncope.common.lib.request.UserUR;
@@ -54,6 +58,7 @@ import org.apache.syncope.common.lib.to.AnyObjectTO;
 import org.apache.syncope.common.lib.to.GroupTO;
 import org.apache.syncope.common.lib.to.MembershipTO;
 import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.PatchOperation;
 import org.apache.syncope.common.lib.types.StatusRType;
 import org.apache.syncope.core.logic.scim.SCIMConfManager;
@@ -62,6 +67,7 @@ import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.NotFoundException;
 import org.apache.syncope.core.persistence.api.dao.search.MembershipCond;
 import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
+import org.apache.syncope.core.persistence.api.entity.ExternalResource;
 import org.apache.syncope.core.persistence.api.entity.user.UMembership;
 import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
 import org.apache.syncope.core.spring.security.AuthDataAccessor;
@@ -74,6 +80,7 @@ import 
org.apache.syncope.ext.scimv2.api.data.SCIMComplexValue;
 import org.apache.syncope.ext.scimv2.api.data.SCIMEnterpriseInfo;
 import org.apache.syncope.ext.scimv2.api.data.SCIMExtensionInfo;
 import org.apache.syncope.ext.scimv2.api.data.SCIMGroup;
+import org.apache.syncope.ext.scimv2.api.data.SCIMPatchOp;
 import org.apache.syncope.ext.scimv2.api.data.SCIMPatchOperation;
 import org.apache.syncope.ext.scimv2.api.data.SCIMUser;
 import org.apache.syncope.ext.scimv2.api.data.SCIMUserAddress;
@@ -553,6 +560,292 @@ public class SCIMDataBinder {
                 new 
Attr.Builder(conf.getValue()).value(value.getValue()).build())));
     }
 
+    protected <E extends Enum<?>> void setAttribute(
+            final Set<Attr> attrs,
+            final Set<AttrPatch> attrPatches,
+            final List<SCIMComplexConf<E>> confs,
+            final List<SCIMComplexValue> values) {
+
+        values.stream().filter(value -> value.getType() != null).forEach(value 
-> confs.stream().
+                filter(object -> 
value.getType().equals(object.getType().name())
+                && attrPatches.stream().noneMatch(attrPatch ->
+                
attrPatch.getAttr().getSchema().equals(object.getValue()))).findFirst().
+                ifPresent(conf -> attrs.add(
+                new 
Attr.Builder(conf.getValue()).value(value.getValue()).build())));
+    }
+    
+    public void populateUserUR(
+            final UserUR userUR,
+            final UserTO before,
+            final SCIMUser user,
+            final Collection<String> resources,
+            final SCIMPatchOperation op) {
+        SCIMConf conf = confManager.get();
+
+        if (!SyncopeConstants.ROOT_REALM.equals(before.getRealm())) {
+            userUR.setRealm(new StringReplacePatchItem.Builder()
+                    
.value(SyncopeConstants.ROOT_REALM).operation(PatchOperation.ADD_REPLACE).build());
+        }
+
+        if (StringUtils.isNotBlank(user.getPassword())) {
+            userUR.setPassword(new PasswordPatch.Builder()
+                    
.value(user.getPassword()).resources(resources).operation(PatchOperation.ADD_REPLACE).build());
+        }
+
+        if (StringUtils.isNotBlank(user.getUserName()) && 
!user.getUserName().equals(before.getUsername())) {
+            userUR.setUsername(new StringReplacePatchItem.Builder()
+                    
.value(user.getUserName()).operation(PatchOperation.ADD_REPLACE).build());
+        }
+
+        if (conf.getUserConf() != null) {
+            setAttribute(
+                    before,
+                    userUR,
+                    conf.getUserConf().getExternalId(),
+                    user.getExternalId(),
+                    op);
+
+            if (conf.getUserConf().getName() != null && user.getName() != 
null) {
+                setAttribute(
+                        before,
+                        userUR,
+                        conf.getUserConf().getName().getFamilyName(),
+                        user.getName().getFamilyName(),
+                        op);
+
+                setAttribute(
+                        before,
+                        userUR,
+                        conf.getUserConf().getName().getFormatted(),
+                        user.getName().getFormatted(),
+                        op);
+
+                setAttribute(
+                        before,
+                        userUR,
+                        conf.getUserConf().getName().getGivenName(),
+                        user.getName().getGivenName(),
+                        op);
+
+                setAttribute(
+                        before,
+                        userUR,
+                        conf.getUserConf().getName().getHonorificPrefix(),
+                        user.getName().getHonorificPrefix(),
+                        op);
+
+                setAttribute(
+                        before,
+                        userUR,
+                        conf.getUserConf().getName().getHonorificSuffix(),
+                        user.getName().getHonorificSuffix(),
+                        op);
+
+                setAttribute(
+                        before,
+                        userUR,
+                        conf.getUserConf().getName().getMiddleName(),
+                        user.getName().getMiddleName(),
+                        op);
+            }
+
+            setAttribute(
+                    before,
+                    userUR,
+                    conf.getUserConf().getDisplayName(),
+                    user.getDisplayName(),
+                    op);
+
+            setAttribute(
+                    before,
+                    userUR,
+                    conf.getUserConf().getNickName(),
+                    user.getNickName(),
+                    op);
+
+            setAttribute(
+                    before,
+                    userUR,
+                    conf.getUserConf().getProfileUrl(),
+                    user.getProfileUrl(),
+                    op);
+
+            setAttribute(
+                    before,
+                    userUR,
+                    conf.getUserConf().getTitle(),
+                    user.getTitle(),
+                    op);
+
+            setAttribute(
+                    before,
+                    userUR,
+                    conf.getUserConf().getUserType(),
+                    user.getUserType(),
+                    op);
+
+            setAttribute(
+                    before,
+                    userUR,
+                    conf.getUserConf().getPreferredLanguage(),
+                    user.getPreferredLanguage(),
+                    op);
+
+            setAttribute(
+                    before,
+                    userUR,
+                    conf.getUserConf().getLocale(),
+                    user.getLocale(),
+                    op);
+
+            setAttribute(
+                    before,
+                    userUR,
+                    conf.getUserConf().getTimezone(),
+                    user.getTimezone(),
+                    op);
+
+            setAttribute(
+                    before.getPlainAttrs(), userUR.getPlainAttrs(), 
conf.getUserConf().getEmails(), user.getEmails());
+            setAttribute(
+                    before.getPlainAttrs(),
+                    userUR.getPlainAttrs(),
+                    conf.getUserConf().getPhoneNumbers(),
+                    user.getPhoneNumbers());
+            setAttribute(before.getPlainAttrs(), userUR.getPlainAttrs(), 
conf.getUserConf().getIms(), user.getIms());
+            setAttribute(
+                    before.getPlainAttrs(), userUR.getPlainAttrs(), 
conf.getUserConf().getPhotos(), user.getPhotos());
+
+            user.getAddresses().stream().filter(address -> address.getType() 
!= null).
+                    forEach(address -> 
conf.getUserConf().getAddresses().stream().
+                    filter(object -> 
address.getType().equals(object.getType().name())).findFirst().
+                    ifPresent(addressConf -> {
+                setAttribute(
+                        before,
+                        userUR,
+                        addressConf.getFormatted(),
+                        address.getFormatted(),
+                        op);
+
+                setAttribute(
+                        before,
+                        userUR,
+                        addressConf.getStreetAddress(),
+                        address.getStreetAddress(),
+                        op);
+
+                setAttribute(
+                        before,
+                        userUR,
+                        addressConf.getLocality(),
+                        address.getLocality(),
+                        op);
+
+                setAttribute(
+                        before,
+                        userUR,
+                        addressConf.getRegion(),
+                        address.getRegion(),
+                        op);
+
+                setAttribute(
+                        before,
+                        userUR,
+                        addressConf.getPostalCode(),
+                        address.getPostalCode(),
+                        op);
+
+                setAttribute(
+                        before,
+                        userUR,
+                        addressConf.getCountry(),
+                        address.getCountry(),
+                        op);
+            }));
+
+            for (int i = 0; i < user.getX509Certificates().size(); i++) {
+                Value certificate = user.getX509Certificates().get(i);
+                if (conf.getUserConf().getX509Certificates().size() > i) {
+                    setAttribute(
+                            before,
+                            userUR,
+                            conf.getUserConf().getX509Certificates().get(i),
+                            certificate.getValue(),
+                            op);
+                }
+            }
+        }
+
+        if (conf.getEnterpriseUserConf() != null && user.getEnterpriseInfo() 
!= null) {
+            setAttribute(
+                    before,
+                    userUR,
+                    conf.getEnterpriseUserConf().getEmployeeNumber(),
+                    user.getEnterpriseInfo().getEmployeeNumber(),
+                    op);
+
+            setAttribute(
+                    before,
+                    userUR,
+                    conf.getEnterpriseUserConf().getCostCenter(),
+                    user.getEnterpriseInfo().getCostCenter(),
+                    op);
+
+            setAttribute(
+                    before,
+                    userUR,
+                    conf.getEnterpriseUserConf().getOrganization(),
+                    user.getEnterpriseInfo().getOrganization(),
+                    op);
+
+            setAttribute(
+                    before,
+                    userUR,
+                    conf.getEnterpriseUserConf().getDivision(),
+                    user.getEnterpriseInfo().getDivision(),
+                    op);
+
+            setAttribute(
+                    before,
+                    userUR,
+                    conf.getEnterpriseUserConf().getDepartment(),
+                    user.getEnterpriseInfo().getDepartment(),
+                    op);
+
+            setAttribute(
+                    before,
+                    userUR,
+                    
Optional.ofNullable(conf.getEnterpriseUserConf().getManager()).
+                            map(SCIMManagerConf::getKey).orElse(null),
+                    Optional.ofNullable(user.getEnterpriseInfo().getManager()).
+                            map(SCIMUserManager::getValue).orElse(null),
+                    op);
+        }
+
+        if (conf.getExtensionUserConf() != null && user.getExtensionInfo() != 
null) {
+            conf.getExtensionUserConf().asMap().forEach((scimAttr, 
syncopeAttr) -> setAttribute(
+                    before, userUR, syncopeAttr, 
user.getExtensionInfo().getAttributes().get(scimAttr), op));
+        }
+
+        user.getGroups().forEach(group -> {
+            if (before.getMembership(group.getValue()).isEmpty()
+                    && userUR.getMemberships().stream().noneMatch(membershipUR 
->
+                    membershipUR.getGroup().equals(group.getValue()))) {
+                userUR.getMemberships().add(new 
MembershipUR.Builder(group.getValue())
+                        .operation(PatchOperation.ADD_REPLACE).build());
+            }
+        });
+
+        user.getRoles().forEach(role -> {
+            if (!before.getRoles().contains(role.getValue())
+                    && userUR.getRoles().stream().noneMatch(roleUR ->
+                    roleUR.getValue().equals(role.getValue()))) {
+                userUR.getRoles().add(new StringPatchItem.Builder()
+                        
.value(role.getValue()).operation(PatchOperation.ADD_REPLACE).build());
+            }
+        });
+    }
+
     public UserTO toUserTO(final SCIMUser user, final boolean checkSchemas) {
         SCIMConf conf = confManager.get();
 
@@ -763,6 +1056,45 @@ public class SCIMDataBinder {
         return userCR;
     }
 
+    protected void setAttribute(
+            final UserTO before,
+            final UserUR userUR,
+            final String schema,
+            final String value,
+            final SCIMPatchOperation op) {
+        if (schema == null || value == null) {
+            return;
+        }
+        switch (schema) {
+            case "username" -> {
+                if (!value.equals(before.getUsername()) && 
userUR.getUsername() == null) {
+                    userUR.setUsername(
+                            new 
StringReplacePatchItem.Builder().value(value).operation(PatchOperation.ADD_REPLACE)
+                                    .build());
+                }
+            }
+
+            default -> {
+                if ((before.getPlainAttr(schema).isEmpty()
+                        || 
!value.equals(before.getPlainAttr(schema).get().getValues().getFirst()))
+                        && userUR.getPlainAttrs().stream().noneMatch(attrPatch 
->
+                        attrPatch.getAttr().getSchema().equals(schema))
+                        && op.getOp() != PatchOp.remove) {
+                    userUR.getPlainAttrs()
+                            .add(new AttrPatch.Builder(new 
Attr.Builder(schema).value(value).build()).operation(
+                                    PatchOperation.ADD_REPLACE).build());
+                }
+                if (before.getPlainAttr(schema).isPresent()
+                        && userUR.getPlainAttrs().stream().noneMatch(attrPatch 
->
+                        attrPatch.getAttr().getSchema().equals(schema))
+                        && op.getOp() == PatchOp.remove) {
+                    userUR.getPlainAttrs().add(new AttrPatch.Builder(new 
Attr.Builder(schema).build()).operation(
+                            PatchOperation.DELETE).build());
+                }
+            }
+        }
+    }
+
     protected void setAttribute(final Set<AttrPatch> attrs, final String 
schema, final SCIMPatchOperation op) {
         Optional.ofNullable(schema).ifPresent(a -> {
             Attr.Builder attr = new Attr.Builder(a);
@@ -835,206 +1167,253 @@ public class SCIMDataBinder {
         }
     }
 
-    public Pair<UserUR, StatusR> toUserUpdate(
+    public Pair<List<UserUR>, StatusR> toUserUpdate(
             final UserTO before,
-            final Collection<String> resources,
-            final SCIMPatchOperation op) {
-        StatusR statusR = null;
-
-        if (op.getPath() == null && op.getOp() != PatchOp.remove
-                && !CollectionUtils.isEmpty(op.getValue())
-                && op.getValue().getFirst() instanceof final SCIMUser after) {
-
-            if (after.getActive() != null && before.isSuspended() == 
after.isActive()) {
-                statusR = new StatusR.Builder(
-                        before.getKey(),
-                        after.isActive() ? StatusRType.REACTIVATE : 
StatusRType.SUSPEND).
-                        resources(resources).
-                        build();
-            }
-
-            UserTO updated = toUserTO(after, false);
-            updated.setKey(before.getKey());
-            return Pair.of(AnyOperations.diff(updated, before, true), statusR);
-        }
-
+            final SCIMPatchOp patch) {
+        Mutable<StatusR> statusR = new MutableObject<>();
+        List<UserUR> userURs = new ArrayList<>();
         UserUR userUR = new UserUR.Builder(before.getKey()).build();
-
-        SCIMConf conf = confManager.get();
-        if (conf == null) {
-            return Pair.of(userUR, statusR);
-        }
-
-        switch (op.getPath().getAttribute()) {
-            case "externalId" ->
-                setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getExternalId(), op);
-
-            case "userName" -> {
-                if (op.getOp() != PatchOp.remove && 
!CollectionUtils.isEmpty(op.getValue())) {
-                    userUR.setUsername(
-                            new 
StringReplacePatchItem.Builder().value(op.getValue().getFirst().toString()).build());
+        userURs.add(userUR);
+        List<String> resources = new ArrayList<>(before.getResources());
+        MutableInt numberUR = new MutableInt(0);
+
+        patch.getOperations().forEach(op -> {
+            if (op.getPath() == null && op.getOp() != PatchOp.remove && 
!CollectionUtils.isEmpty(op.getValue())
+                    && op.getValue().getFirst() instanceof final SCIMUser 
after) {
+
+                if (after.getActive() != null && before.isSuspended() == 
after.isActive()) {
+                    statusR.setValue(new StatusR.Builder(before.getKey(),
+                            after.isActive() ? StatusRType.REACTIVATE : 
StatusRType.SUSPEND).resources(resources)
+                            .build());
                 }
-            }
 
-            case "password" -> {
-                if (op.getOp() != PatchOp.remove && 
!CollectionUtils.isEmpty(op.getValue())) {
-                    userUR.setPassword(new 
PasswordPatch.Builder().value(op.getValue().getFirst().toString()).resources(
-                            resources).build());
+                if (!after.getGroups().isEmpty()) {
+                    String groupKey = after.getGroups().getFirst().getValue();
+                    org.apache.syncope.core.persistence.api.entity.group.Group 
group =
+                            groupDAO.findById(groupKey).orElse(null);
+                    if (group != null && 
before.getMembership(groupKey).isEmpty()) {
+                        List<? extends ExternalResource> filteredResources = 
group.getResources().stream()
+                                .filter(resource -> 
resource.getProvisions().stream()
+                                        .anyMatch(provision -> 
AnyTypeKind.USER.name().equals(provision.getAnyType())))
+                                .toList();
+                        filteredResources.forEach(resource -> 
resources.add(resource.getKey()));
+                        if (!filteredResources.isEmpty()) {
+                            UserUR newUserUR = new 
UserUR.Builder(before.getKey()).build();
+                            userURs.add(newUserUR);
+                            numberUR.increment();
+                        }
+                    }
                 }
+                populateUserUR(userURs.get(numberUR.get().intValue()), before, 
after, resources, op);
+                return;
             }
 
-            case "active" -> {
-                if (!CollectionUtils.isEmpty(op.getValue())) {
+            SCIMConf conf = confManager.get();
+            if (conf == null) {
+                return;
+            }
 
-                    // Workaround for Microsoft Entra being not SCIM compliant 
on PATCH requests
-                    if (op.getValue().getFirst() instanceof String a) {
-                        op.setValue(List.of(BooleanUtils.toBoolean(a)));
+            switch (op.getPath().getAttribute()) {
+                case "externalId" ->
+                        
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                                conf.getUserConf().getExternalId(),
+                                op);
+
+                case "userName" -> {
+                    if (op.getOp() != PatchOp.remove && 
!CollectionUtils.isEmpty(op.getValue())) {
+                        userURs.get(numberUR.get().intValue()).setUsername(
+                                new 
StringReplacePatchItem.Builder().value(op.getValue().getFirst().toString())
+                                        .build());
                     }
-
-                    statusR = new StatusR.Builder(before.getKey(),
-                            (boolean) op.getValue().getFirst()
-                            ? StatusRType.REACTIVATE
-                            : 
StatusRType.SUSPEND).resources(resources).build();
                 }
-            }
 
-            case "name" -> {
-                if (conf.getUserConf().getName() != null) {
-                    if (op.getPath().getSub() == null || 
"familyName".equals(op.getPath().getSub())) {
-                        setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getName().getFamilyName(), op);
-                    }
-                    if (op.getPath().getSub() == null || 
"formatted".equals(op.getPath().getSub())) {
-                        setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getName().getFormatted(), op);
-                    }
-                    if (op.getPath().getSub() == null || 
"givenName".equals(op.getPath().getSub())) {
-                        setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getName().getGivenName(), op);
-                    }
-                    if (op.getPath().getSub() == null || 
"honorificPrefix".equals(op.getPath().getSub())) {
-                        setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getName().getHonorificPrefix(), op);
-                    }
-                    if (op.getPath().getSub() == null || 
"honorificSuffix".equals(op.getPath().getSub())) {
-                        setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getName().getHonorificSuffix(), op);
-                    }
-                    if (op.getPath().getSub() == null || 
"middleName".equals(op.getPath().getSub())) {
-                        setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getName().getMiddleName(), op);
+                case "password" -> {
+                    if (op.getOp() != PatchOp.remove && 
!CollectionUtils.isEmpty(op.getValue())) {
+                        userURs.get(numberUR.get().intValue()).setPassword(
+                                new 
PasswordPatch.Builder().value(op.getValue().getFirst().toString())
+                                        .resources(resources).build());
                     }
                 }
-            }
-
-            case "displayName" ->
-                setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getDisplayName(), op);
-
-            case "nickName" ->
-                setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getNickName(), op);
 
-            case "profileUrl" ->
-                setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getProfileUrl(), op);
+                case "active" -> {
+                    if (!CollectionUtils.isEmpty(op.getValue())) {
 
-            case "title" ->
-                setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getTitle(), op);
-
-            case "userType" ->
-                setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getUserType(), op);
-
-            case "preferredLanguage" ->
-                setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getPreferredLanguage(), op);
+                        // Workaround for Microsoft Entra being not SCIM 
compliant on PATCH requests
+                        if (op.getValue().getFirst() instanceof String a) {
+                            op.setValue(List.of(BooleanUtils.toBoolean(a)));
+                        }
 
-            case "locale" ->
-                setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getLocale(), op);
+                        statusR.setValue(new StatusR.Builder(before.getKey(), 
(boolean) op.getValue().getFirst()
+                                ? StatusRType.REACTIVATE
+                                : 
StatusRType.SUSPEND).resources(resources).build());
+                    }
+                }
 
-            case "timezone" ->
-                setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getTimezone(), op);
+                case "name" -> {
+                    if (conf.getUserConf().getName() != null) {
+                        if (op.getPath().getSub() == null || 
"familyName".equals(op.getPath().getSub())) {
+                            
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                                    
conf.getUserConf().getName().getFamilyName(), op);
+                        }
+                        if (op.getPath().getSub() == null || 
"formatted".equals(op.getPath().getSub())) {
+                            
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                                    
conf.getUserConf().getName().getFormatted(), op);
+                        }
+                        if (op.getPath().getSub() == null || 
"givenName".equals(op.getPath().getSub())) {
+                            
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                                    
conf.getUserConf().getName().getGivenName(), op);
+                        }
+                        if (op.getPath().getSub() == null || 
"honorificPrefix".equals(op.getPath().getSub())) {
+                            
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                                    
conf.getUserConf().getName().getHonorificPrefix(), op);
+                        }
+                        if (op.getPath().getSub() == null || 
"honorificSuffix".equals(op.getPath().getSub())) {
+                            
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                                    
conf.getUserConf().getName().getHonorificSuffix(), op);
+                        }
+                        if (op.getPath().getSub() == null || 
"middleName".equals(op.getPath().getSub())) {
+                            
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                                    
conf.getUserConf().getName().getMiddleName(), op);
+                        }
+                    }
+                }
 
-            case "emails" -> {
-                if (!CollectionUtils.isEmpty(op.getValue()) && 
op.getValue().getFirst() instanceof SCIMUser) {
-                    setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getEmails(),
-                            ((SCIMUser) op.getValue().getFirst()).getEmails(), 
op.getOp());
-                } else if (op.getPath().getFilter() != null) {
-                    setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getEmails(), op);
+                case "displayName" ->
+                        
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                                conf.getUserConf().getDisplayName(),
+                                op);
+
+                case "nickName" ->
+                        
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                                conf.getUserConf().getNickName(), op);
+
+                case "profileUrl" ->
+                        
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                                conf.getUserConf().getProfileUrl(),
+                                op);
+
+                case "title" ->
+                        
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                                conf.getUserConf().getTitle(), op);
+
+                case "userType" ->
+                        
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                                conf.getUserConf().getUserType(), op);
+
+                case "preferredLanguage" -> 
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                        conf.getUserConf().getPreferredLanguage(), op);
+
+                case "locale" ->
+                        
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                                conf.getUserConf().getLocale(), op);
+
+                case "timezone" ->
+                        
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                                conf.getUserConf().getTimezone(), op);
+
+                case "emails" -> {
+                    if (!CollectionUtils.isEmpty(op.getValue())
+                            && op.getValue().getFirst() instanceof SCIMUser 
scimUser) {
+                        
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                                conf.getUserConf().getEmails(),
+                                scimUser.getEmails(), op.getOp());
+                    } else if (op.getPath().getFilter() != null) {
+                        
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                                conf.getUserConf().getEmails(), op);
+                    }
                 }
-            }
 
-            case "phoneNumbers" -> {
-                if (!CollectionUtils.isEmpty(op.getValue()) && 
op.getValue().getFirst() instanceof SCIMUser) {
-                    setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getPhoneNumbers(),
-                            ((SCIMUser) 
op.getValue().getFirst()).getPhoneNumbers(), op.getOp());
-                } else if (op.getPath().getFilter() != null) {
-                    setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getPhoneNumbers(), op);
+                case "phoneNumbers" -> {
+                    if (!CollectionUtils.isEmpty(op.getValue())
+                            && op.getValue().getFirst() instanceof SCIMUser 
scimUser) {
+                        
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                                conf.getUserConf().getPhoneNumbers(),
+                                scimUser.getPhoneNumbers(), op.getOp());
+                    } else if (op.getPath().getFilter() != null) {
+                        
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                                conf.getUserConf().getPhoneNumbers(),
+                                op);
+                    }
                 }
-            }
 
-            case "ims" -> {
-                if (!CollectionUtils.isEmpty(op.getValue()) && 
op.getValue().getFirst() instanceof SCIMUser) {
-                    setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getIms(),
-                            ((SCIMUser) op.getValue().getFirst()).getIms(), 
op.getOp());
-                } else if (op.getPath().getFilter() != null) {
-                    setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getIms(), op);
+                case "ims" -> {
+                    if (!CollectionUtils.isEmpty(op.getValue())
+                            && op.getValue().getFirst() instanceof SCIMUser 
scimUser) {
+                        
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                                conf.getUserConf().getIms(),
+                                scimUser.getIms(), op.getOp());
+                    } else if (op.getPath().getFilter() != null) {
+                        
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                                conf.getUserConf().getIms(), op);
+                    }
                 }
-            }
 
-            case "photos" -> {
-                if (!CollectionUtils.isEmpty(op.getValue()) && 
op.getValue().getFirst() instanceof SCIMUser) {
-                    setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getPhotos(),
-                            ((SCIMUser) op.getValue().getFirst()).getPhotos(), 
op.getOp());
-                } else if (op.getPath().getFilter() != null) {
-                    setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getPhotos(), op);
+                case "photos" -> {
+                    if (!CollectionUtils.isEmpty(op.getValue())
+                            && op.getValue().getFirst() instanceof SCIMUser 
scimUser) {
+                        
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                                conf.getUserConf().getPhotos(),
+                                scimUser.getPhotos(), op.getOp());
+                    } else if (op.getPath().getFilter() != null) {
+                        
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                                conf.getUserConf().getPhotos(), op);
+                    }
                 }
-            }
 
-            case "addresses" -> {
-                if (!CollectionUtils.isEmpty(op.getValue())
-                        && op.getValue().getFirst() instanceof final SCIMUser 
after) {
-                    after.getAddresses().stream().filter(address -> 
address.getType() != null).forEach(
-                            address -> 
conf.getUserConf().getAddresses().stream()
-                                    .filter(object -> 
address.getType().equals(object.getType().name())).findFirst()
-                                    .ifPresent(addressConf -> 
setAttribute(userUR.getPlainAttrs(), addressConf, op)));
-                } else if (op.getPath().getFilter() != null) {
-                    
conf.getUserConf().getAddresses().stream().filter(addressConf -> 
BooleanUtils.toBoolean(
-                            
JexlUtils.evaluateExpr(filter2JexlExpression(op.getPath().getFilter()),
-                                    new MapContext(Map.of("type", 
addressConf.getType().name()))).toString()))
-                            .findFirst()
-                            .ifPresent(addressConf -> 
setAttribute(userUR.getPlainAttrs(), addressConf, op));
+                case "addresses" -> {
+                    if (!CollectionUtils.isEmpty(op.getValue()) && 
op.getValue()
+                            .getFirst() instanceof final SCIMUser after) {
+                        after.getAddresses().stream().filter(address -> 
address.getType() != null).forEach(
+                                address -> 
conf.getUserConf().getAddresses().stream()
+                                        .filter(object -> 
address.getType().equals(object.getType().name())).findFirst()
+                                        .ifPresent(addressConf ->
+                                                
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                                                        addressConf, op)));
+                    } else if (op.getPath().getFilter() != null) {
+                        
conf.getUserConf().getAddresses().stream().filter(addressConf -> 
BooleanUtils.toBoolean(
+                                        
JexlUtils.evaluateExpr(filter2JexlExpression(op.getPath().getFilter()),
+                                                new MapContext(Map.of("type", 
addressConf.getType().name()))).
+                                                toString()))
+                                .findFirst().ifPresent(addressConf ->
+                                        
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                                                addressConf, op));
+                    }
                 }
-            }
 
-            case "employeeNumber" ->
-                setAttribute(userUR.getPlainAttrs(),
-                        Optional.ofNullable(conf.getEnterpriseUserConf()).
-                                
map(SCIMEnterpriseUserConf::getEmployeeNumber).orElse(null), op);
+                case "employeeNumber" -> 
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                        
Optional.ofNullable(conf.getEnterpriseUserConf()).map(SCIMEnterpriseUserConf::getEmployeeNumber)
+                                .orElse(null), op);
 
-            case "costCenter" ->
-                setAttribute(userUR.getPlainAttrs(),
-                        Optional.ofNullable(conf.getEnterpriseUserConf()).
-                                
map(SCIMEnterpriseUserConf::getCostCenter).orElse(null), op);
+                case "costCenter" -> 
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                        
Optional.ofNullable(conf.getEnterpriseUserConf()).map(SCIMEnterpriseUserConf::getCostCenter)
+                                .orElse(null), op);
 
-            case "organization" ->
-                setAttribute(userUR.getPlainAttrs(),
-                        Optional.ofNullable(conf.getEnterpriseUserConf()).
-                                
map(SCIMEnterpriseUserConf::getOrganization).orElse(null), op);
+                case "organization" -> 
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                        
Optional.ofNullable(conf.getEnterpriseUserConf()).map(SCIMEnterpriseUserConf::getOrganization)
+                                .orElse(null), op);
 
-            case "division" ->
-                setAttribute(userUR.getPlainAttrs(),
-                        Optional.ofNullable(conf.getEnterpriseUserConf()).
-                                
map(SCIMEnterpriseUserConf::getDivision).orElse(null), op);
+                case "division" -> 
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                        
Optional.ofNullable(conf.getEnterpriseUserConf()).map(SCIMEnterpriseUserConf::getDivision)
+                                .orElse(null), op);
 
-            case "department" ->
-                setAttribute(userUR.getPlainAttrs(),
-                        Optional.ofNullable(conf.getEnterpriseUserConf()).
-                                
map(SCIMEnterpriseUserConf::getDepartment).orElse(null), op);
+                case "department" -> 
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                        
Optional.ofNullable(conf.getEnterpriseUserConf()).map(SCIMEnterpriseUserConf::getDepartment)
+                                .orElse(null), op);
 
-            case "manager" ->
-                setAttribute(userUR.getPlainAttrs(),
-                        Optional.ofNullable(conf.getEnterpriseUserConf()).
-                                
map(SCIMEnterpriseUserConf::getManager).map(SCIMManagerConf::getKey).orElse(null),
 op);
+                case "manager" -> 
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                        
Optional.ofNullable(conf.getEnterpriseUserConf()).map(SCIMEnterpriseUserConf::getManager)
+                                .map(SCIMManagerConf::getKey).orElse(null), 
op);
 
-            default -> {
-                Optional.ofNullable(conf.getExtensionUserConf()).
-                        flatMap(schema -> 
Optional.ofNullable(schema.asMap().get(op.getPath().getAttribute()))).
-                        ifPresent(schema -> 
setAttribute(userUR.getPlainAttrs(), schema, op));
+                default -> {
+                    Optional.ofNullable(conf.getExtensionUserConf())
+                            .flatMap(schema -> 
Optional.ofNullable(schema.asMap().get(op.getPath().getAttribute())))
+                            .ifPresent(schema -> 
setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                                    schema, op));
+                }
             }
-        }
+        });
 
-        return Pair.of(userUR, statusR);
+        return Pair.of(userURs, statusR.get());
     }
 
     @Transactional(readOnly = true)
diff --git 
a/ext/scimv2/logic/src/test/java/org/apache/syncope/core/logic/SCIMDataBinderTest.java
 
b/ext/scimv2/logic/src/test/java/org/apache/syncope/core/logic/SCIMDataBinderTest.java
index d65e4b9d63..0a88e094b3 100644
--- 
a/ext/scimv2/logic/src/test/java/org/apache/syncope/core/logic/SCIMDataBinderTest.java
+++ 
b/ext/scimv2/logic/src/test/java/org/apache/syncope/core/logic/SCIMDataBinderTest.java
@@ -18,20 +18,46 @@
  */
 package org.apache.syncope.core.logic;
 
-import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
 import java.util.stream.Stream;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.syncope.common.lib.Attr;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.request.StatusR;
+import org.apache.syncope.common.lib.request.UserUR;
 import org.apache.syncope.common.lib.scim.SCIMConf;
+import org.apache.syncope.common.lib.scim.SCIMUserConf;
+import org.apache.syncope.common.lib.scim.SCIMUserNameConf;
+import org.apache.syncope.common.lib.to.Provision;
 import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.common.lib.types.PatchOperation;
+import org.apache.syncope.common.lib.types.StatusRType;
 import org.apache.syncope.core.logic.scim.SCIMConfManager;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
+import org.apache.syncope.core.persistence.api.entity.ExternalResource;
 import org.apache.syncope.core.spring.security.AuthDataAccessor;
+import org.apache.syncope.ext.scimv2.api.data.Group;
+import org.apache.syncope.ext.scimv2.api.data.SCIMPatchOp;
 import org.apache.syncope.ext.scimv2.api.data.SCIMPatchOperation;
 import org.apache.syncope.ext.scimv2.api.data.SCIMPatchPath;
+import org.apache.syncope.ext.scimv2.api.data.SCIMUser;
+import org.apache.syncope.ext.scimv2.api.data.SCIMUserName;
+import org.apache.syncope.ext.scimv2.api.data.Value;
+import org.apache.syncope.ext.scimv2.api.type.PatchOp;
+import org.apache.syncope.ext.scimv2.api.type.Resource;
 import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInstance;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
@@ -41,6 +67,8 @@ class SCIMDataBinderTest {
 
     private SCIMDataBinder dataBinder;
 
+    private GroupDAO groupDAO;
+
     private static Stream<String> getValue() {
         return Stream.of("True", "False");
     }
@@ -48,21 +76,253 @@ class SCIMDataBinderTest {
     @BeforeAll
     void setup() {
         SCIMConfManager scimConfManager = mock(SCIMConfManager.class);
-        when(scimConfManager.get()).thenReturn(new SCIMConf());
+        SCIMConf conf = new SCIMConf();
+        conf.setUserConf(new SCIMUserConf());
+        conf.getUserConf().setName(new SCIMUserNameConf());
+        conf.getUserConf().getName().setGivenName("firstname");
+        conf.getUserConf().getName().setFamilyName("surname");
+        when(scimConfManager.get()).thenReturn(conf);
         UserLogic userLogic = mock(UserLogic.class);
         AuthDataAccessor authDataAccessor = mock(AuthDataAccessor.class);
-        GroupDAO  groupDAO = mock(GroupDAO.class);
-        dataBinder = new SCIMDataBinder(scimConfManager, userLogic, 
authDataAccessor,  groupDAO);
+        groupDAO = mock(GroupDAO.class);
+        dataBinder = new SCIMDataBinder(scimConfManager, userLogic, 
authDataAccessor, groupDAO);
     }
 
     @ParameterizedTest
     @MethodSource("getValue")
-    void toUserUpdate(final String value) {
+    void toUserUpdateActive(final String value) {
+        SCIMPatchOp scimPatchOp = new SCIMPatchOp();
+        scimPatchOp.setOperations(List.of(getOperation("active", null, 
PatchOp.add, value)));
+        Pair<List<UserUR>, StatusR> result = dataBinder.toUserUpdate(new 
UserTO(), scimPatchOp);
+        assertNotNull(result);
+        assertEquals(1, result.getLeft().size());
+        assertTrue(result.getLeft().getFirst().isEmpty());
+        assertNotNull(result.getRight());
+        assertTrue(result.getRight().isOnSyncope());
+        assertEquals(
+                Boolean.parseBoolean(value) ? StatusRType.REACTIVATE : 
StatusRType.SUSPEND,
+                result.getRight().getType());
+    }
+
+    @Test
+    void toUserUpdate() {
+        SCIMPatchOp scimPatchOp = new SCIMPatchOp();
+        List<SCIMPatchOperation> operations = new ArrayList<>();
+        operations.add(getOperation("name", "familyName", PatchOp.add, 
"Rossini"));
+        scimPatchOp.setOperations(operations);
+
+        Pair<List<UserUR>, StatusR> result = dataBinder.toUserUpdate(new 
UserTO(), scimPatchOp);
+        assertNotNull(result);
+        assertNull(result.getRight());
+        assertEquals(1, result.getLeft().size());
+        assertEquals(1, result.getLeft().getFirst().getPlainAttrs().size());
+        
assertTrue(result.getLeft().getFirst().getPlainAttrs().stream().anyMatch(attrPatch
 ->
+                PatchOperation.ADD_REPLACE.equals(attrPatch.getOperation())
+                        && attrPatch.getAttr().getSchema().equals("surname")
+                        && 
attrPatch.getAttr().getValues().contains("Rossini")));
+
+        operations.clear();
+        operations.add(getOperation("name", "givenName", PatchOp.add, 
"Gioacchino"));
+        operations.add(getOperation("name", "familyName", PatchOp.remove, 
null));
+        scimPatchOp.setOperations(operations);
+        result = dataBinder.toUserUpdate(new UserTO(), scimPatchOp);
+        assertNotNull(result);
+        assertNull(result.getRight());
+        assertEquals(1, result.getLeft().size());
+        assertEquals(2, result.getLeft().getFirst().getPlainAttrs().size());
+        
assertTrue(result.getLeft().getFirst().getPlainAttrs().stream().anyMatch(attrPatch
 ->
+                PatchOperation.ADD_REPLACE.equals(attrPatch.getOperation())
+                        && attrPatch.getAttr().getSchema().equals("firstname")
+                        && 
attrPatch.getAttr().getValues().contains("Gioacchino")));
+        
assertTrue(result.getLeft().getFirst().getPlainAttrs().stream().anyMatch(attrPatch
 ->
+                PatchOperation.DELETE.equals(attrPatch.getOperation())
+                        && attrPatch.getAttr().getSchema().equals("surname")
+                        &&  attrPatch.getAttr().getValues().isEmpty()));
+
+        operations.clear();
+        operations.add(getOperation("name", "familyName", PatchOp.add, 
"Verdi"));
+        operations.add(getOperation("name", "givenName", PatchOp.replace, 
"Giuseppe"));
+        operations.add(getOperation("userName", null, PatchOp.add, "gverdi"));
+        scimPatchOp.setOperations(operations);
+        result = dataBinder.toUserUpdate(new UserTO(), scimPatchOp);
+        assertNotNull(result);
+        assertNull(result.getRight());
+        assertEquals(1, result.getLeft().size());
+        assertEquals(2, result.getLeft().getFirst().getPlainAttrs().size());
+        assertEquals(PatchOperation.ADD_REPLACE, 
result.getLeft().getFirst().getUsername().getOperation());
+        assertEquals("gverdi", 
result.getLeft().getFirst().getUsername().getValue());
+        
assertTrue(result.getLeft().getFirst().getPlainAttrs().stream().anyMatch(attrPatch
 ->
+                PatchOperation.ADD_REPLACE.equals(attrPatch.getOperation())
+                        && attrPatch.getAttr().getSchema().equals("surname")
+                        && attrPatch.getAttr().getValues().contains("Verdi")));
+        
assertTrue(result.getLeft().getFirst().getPlainAttrs().stream().anyMatch(attrPatch
 ->
+                PatchOperation.ADD_REPLACE.equals(attrPatch.getOperation())
+                        && attrPatch.getAttr().getSchema().equals("firstname")
+                        && 
attrPatch.getAttr().getValues().contains("Giuseppe")));
+
+        operations.clear();
+        operations.add(getOperation("name", "familyName", PatchOp.replace, 
"Puccini"));
+        operations.add(getOperation("name", "givenName", PatchOp.remove, 
null));
+        operations.add(getOperation("active", null, PatchOp.add, "True"));
+        scimPatchOp.setOperations(operations);
+        result = dataBinder.toUserUpdate(new UserTO(), scimPatchOp);
+        assertNotNull(result);
+        assertNotNull(result.getRight());
+        assertTrue(result.getRight().isOnSyncope());
+        assertEquals(StatusRType.REACTIVATE, result.getRight().getType());
+        assertEquals(1, result.getLeft().size());
+        assertEquals(2, result.getLeft().getFirst().getPlainAttrs().size());
+        
assertTrue(result.getLeft().getFirst().getPlainAttrs().stream().anyMatch(attrPatch
 ->
+                PatchOperation.ADD_REPLACE.equals(attrPatch.getOperation())
+                        && attrPatch.getAttr().getSchema().equals("surname")
+                        && 
attrPatch.getAttr().getValues().contains("Puccini")));
+        
assertTrue(result.getLeft().getFirst().getPlainAttrs().stream().anyMatch(attrPatch
 ->
+                PatchOperation.DELETE.equals(attrPatch.getOperation())
+                        && attrPatch.getAttr().getSchema().equals("firstname")
+                        &&  attrPatch.getAttr().getValues().isEmpty()));
+
+        UserTO userTO = new UserTO();
+        userTO.setUsername("bellini");
+        userTO.setRealm(SyncopeConstants.ROOT_REALM);
+        userTO.getPlainAttrs().add(new 
Attr.Builder("surname").value("Bellini").build());
+        SCIMUser scimUser = new SCIMUser(
+                UUID.randomUUID().toString(), List.of(Resource.User.schema()), 
null, "bellini", true);
+        scimUser.setName(new SCIMUserName());
+        scimUser.getName().setFamilyName("Bellini");
+        SCIMPatchOperation operation = new SCIMPatchOperation();
+        operation.setOp(PatchOp.add);
+        operation.setValue(List.of(scimUser));
+        operations.clear();
+        operations.add(operation);
+        scimPatchOp.setOperations(operations);
+        result = dataBinder.toUserUpdate(userTO, scimPatchOp);
+        assertNotNull(result);
+        assertNull(result.getRight());
+        assertEquals(1, result.getLeft().size());
+        assertTrue(result.getLeft().getFirst().isEmpty());
+
+        userTO.setUsername("rossini");
+        userTO.getPlainAttrs().clear();
+        userTO.getPlainAttrs().add(new 
Attr.Builder("surname").value("Rossini").build());
+        result = dataBinder.toUserUpdate(userTO, scimPatchOp);
+        assertNotNull(result);
+        assertNull(result.getRight());
+        assertEquals(1, result.getLeft().size());
+        assertEquals(PatchOperation.ADD_REPLACE, 
result.getLeft().getFirst().getUsername().getOperation());
+        assertEquals("bellini", 
result.getLeft().getFirst().getUsername().getValue());
+        assertEquals(1, result.getLeft().getFirst().getPlainAttrs().size());
+        
assertTrue(result.getLeft().getFirst().getPlainAttrs().stream().anyMatch(attrPatch
 ->
+                PatchOperation.ADD_REPLACE.equals(attrPatch.getOperation())
+                        && attrPatch.getAttr().getSchema().equals("surname")
+                        && 
attrPatch.getAttr().getValues().contains("Bellini")));
+
+        userTO.setUsername("bellini");
+        userTO.setSuspended(true);
+        userTO.getPlainAttrs().clear();
+        userTO.getPlainAttrs().add(new 
Attr.Builder("surname").value("Bellini").build());
+        scimUser.getName().setGivenName("Gioacchino");
+        scimUser.getRoles().add(new Value("User reviewer"));
+        result = dataBinder.toUserUpdate(userTO, scimPatchOp);
+        assertNotNull(result);
+        assertNotNull(result.getRight());
+        assertTrue(result.getRight().isOnSyncope());
+        assertEquals(StatusRType.REACTIVATE, result.getRight().getType());
+        assertEquals(1, result.getLeft().size());
+        assertNull(result.getLeft().getFirst().getUsername());
+        assertEquals(1, result.getLeft().getFirst().getPlainAttrs().size());
+        
assertTrue(result.getLeft().getFirst().getPlainAttrs().stream().anyMatch(attrPatch
 ->
+                PatchOperation.ADD_REPLACE.equals(attrPatch.getOperation())
+                        && attrPatch.getAttr().getSchema().equals("firstname")
+                        && 
attrPatch.getAttr().getValues().contains("Gioacchino")));
+        assertEquals(1, result.getLeft().getFirst().getRoles().size());
+        
assertTrue(result.getLeft().getFirst().getRoles().stream().anyMatch(role ->
+                PatchOperation.ADD_REPLACE.equals(role.getOperation())
+                        && role.getValue().equals("User reviewer")));
+
+        userTO = new UserTO();
+        Group group = new Group("37d15e4c-cdc1-460b-a591-8505c8133806", null, 
"root", null);
+        scimUser = new SCIMUser(
+                UUID.randomUUID().toString(), List.of(Resource.User.schema()), 
null, "bellini", true);
+        scimUser.getGroups().add(group);
+        group = new Group("29f96485-729e-4d31-88a1-6fc60e4677f3", null, 
"citizen", null);
+        scimUser.getGroups().add(group);
+        operation.setOp(PatchOp.add);
+        operation.setValue(List.of(scimUser));
+        operations.clear();
+        operations.add(operation);
+        group = new Group("f779c0d4-633b-4be5-8f57-32eb478a3ca5", null, 
"otherchild", null);
+        SCIMUser scimUser2 =
+                new SCIMUser(UUID.randomUUID().toString(), 
List.of(Resource.User.schema()), null, "bellini", true);
+        scimUser2.getGroups().add(group);
+        SCIMPatchOperation operation2 = new SCIMPatchOperation();
+        operation2.setOp(PatchOp.add);
+        operation2.setValue(List.of(scimUser2));
+        operations.add(operation2);
+        scimPatchOp.setOperations(operations);
+        
when(groupDAO.findById("37d15e4c-cdc1-460b-a591-8505c8133806")).thenAnswer(ic 
-> {
+            org.apache.syncope.core.persistence.api.entity.group.Group 
syncopeGroup =
+                    
mock(org.apache.syncope.core.persistence.api.entity.group.Group.class);
+            ExternalResource resource = mock(ExternalResource.class);
+            Provision provision = mock(Provision.class);
+            when(provision.getAnyType()).thenReturn(AnyTypeKind.USER.name());
+            when(resource.getKey()).thenReturn("resource-ldap");
+            when(resource.getProvisions()).thenAnswer(invocation -> 
List.of(provision));
+            when(syncopeGroup.getResources()).thenAnswer(invocation -> 
List.of(resource));
+            return Optional.of(syncopeGroup);
+        });
+        
when(groupDAO.findById("29f96485-729e-4d31-88a1-6fc60e4677f3")).thenAnswer(ic 
-> {
+            org.apache.syncope.core.persistence.api.entity.group.Group 
syncopeGroup =
+                    
mock(org.apache.syncope.core.persistence.api.entity.group.Group.class);
+            ExternalResource resource = mock(ExternalResource.class);
+            Provision provision = mock(Provision.class);
+            when(provision.getAnyType()).thenReturn(AnyTypeKind.USER.name());
+            when(resource.getKey()).thenReturn("resource-testdb");
+            when(resource.getProvisions()).thenAnswer(invocation -> 
List.of(provision));
+            when(syncopeGroup.getResources()).thenAnswer(invocation -> 
List.of(resource));
+            return Optional.of(syncopeGroup);
+        });
+        
when(groupDAO.findById("f779c0d4-633b-4be5-8f57-32eb478a3ca5")).thenAnswer(ic 
-> {
+            org.apache.syncope.core.persistence.api.entity.group.Group 
syncopeGroup =
+                    
mock(org.apache.syncope.core.persistence.api.entity.group.Group.class);
+            ExternalResource resource = mock(ExternalResource.class);
+            Provision provision = mock(Provision.class);
+            when(provision.getAnyType()).thenReturn(AnyTypeKind.USER.name());
+            
when(resource.getKey()).thenReturn("ws-target-resource-list-mappings-1");
+            when(resource.getProvisions()).thenAnswer(invocation -> 
List.of(provision));
+
+            ExternalResource resource2 = mock(ExternalResource.class);
+            
when(resource2.getKey()).thenReturn("ws-target-resource-list-mappings-2");
+            when(resource2.getProvisions()).thenAnswer(invocation -> 
List.of(provision));
+            when(syncopeGroup.getResources()).thenAnswer(invocation -> 
List.of(resource, resource2));
+            return Optional.of(syncopeGroup);
+        });
+        result = dataBinder.toUserUpdate(userTO, scimPatchOp);
+        assertNotNull(result);
+        assertNull(result.getRight());
+        assertEquals(3, result.getLeft().size());
+        assertTrue(result.getLeft().get(0).isEmpty());
+        assertEquals(2, result.getLeft().get(1).getMemberships().size());
+        
assertTrue(result.getLeft().get(1).getMemberships().stream().anyMatch(membershipUR
 ->
+                PatchOperation.ADD_REPLACE.equals(membershipUR.getOperation())
+                        && 
membershipUR.getGroup().equals("37d15e4c-cdc1-460b-a591-8505c8133806")));
+        
assertTrue(result.getLeft().get(1).getMemberships().stream().anyMatch(membershipUR
 ->
+                PatchOperation.ADD_REPLACE.equals(membershipUR.getOperation())
+                        && 
membershipUR.getGroup().equals("29f96485-729e-4d31-88a1-6fc60e4677f3")));
+        assertEquals(1, result.getLeft().get(2).getMemberships().size());
+        
assertTrue(result.getLeft().get(2).getMemberships().stream().anyMatch(membershipUR
 ->
+                PatchOperation.ADD_REPLACE.equals(membershipUR.getOperation())
+                        && 
membershipUR.getGroup().equals("f779c0d4-633b-4be5-8f57-32eb478a3ca5")));
+    }
+
+    private SCIMPatchOperation getOperation(
+            final String attribute, final String sub, final PatchOp op, final 
String value) {
         SCIMPatchOperation operation = new SCIMPatchOperation();
         SCIMPatchPath scimPatchPath = new SCIMPatchPath();
-        scimPatchPath.setAttribute("active");
+        scimPatchPath.setAttribute(attribute);
+        scimPatchPath.setSub(sub);
+        operation.setOp(op);
         operation.setPath(scimPatchPath);
-        operation.setValue(List.of(value));
-        assertDoesNotThrow(() -> dataBinder.toUserUpdate(new UserTO(), 
List.of(), operation));
+        operation.setValue(value == null ? List.of() : List.of(value));
+        return operation;
     }
 }
diff --git 
a/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMUserServiceImpl.java
 
b/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMUserServiceImpl.java
index 5984aa695e..3b176acad2 100644
--- 
a/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMUserServiceImpl.java
+++ 
b/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMUserServiceImpl.java
@@ -96,14 +96,11 @@ public class SCIMUserServiceImpl extends 
AbstractSCIMService<SCIMUser> implement
             return builder.build();
         }
 
-        patch.getOperations().forEach(op -> {
-            Pair<UserUR, StatusR> update = binder.toUserUpdate(
-                    userLogic.read(id),
-                    userDAO.findAllResourceKeys(id),
-                    op);
-            userLogic.update(update.getLeft(), false);
-            Optional.ofNullable(update.getRight()).ifPresent(statusR -> 
userLogic.status(statusR, false));
-        });
+        Pair<List<UserUR>, StatusR> update = binder.toUserUpdate(
+                userLogic.read(id),
+                patch);
+        update.getLeft().forEach(userUR -> userLogic.update(userUR, false));
+        Optional.ofNullable(update.getRight()).ifPresent(statusR -> 
userLogic.status(statusR, false));
 
         return updateResponse(
                 id,

Reply via email to