You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@syncope.apache.org by il...@apache.org on 2023/04/07 13:09:56 UTC

[syncope] branch 3_0_X updated: [SYNCOPE-1748] Implementing RFC 7644 section 3.5.2. Modifying with PATCH (#436)

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 b50f4a25b1 [SYNCOPE-1748] Implementing RFC 7644 section 3.5.2. Modifying with PATCH  (#436)
b50f4a25b1 is described below

commit b50f4a25b1860967fa360717e008defb12429a31
Author: Francesco Chicchiriccò <il...@users.noreply.github.com>
AuthorDate: Fri Apr 7 15:09:50 2023 +0200

    [SYNCOPE-1748] Implementing RFC 7644 section 3.5.2. Modifying with PATCH  (#436)
---
 .../java/data/AbstractAnyDataBinder.java           |   3 +-
 .../panels/SCIMConfEnterpriseUserPanel.java        | 191 ++---
 .../console/panels/SCIMConfGeneralPanel.java       |  77 +-
 .../client/console/panels/SCIMConfGroupPanel.java  |  69 ++
 .../client/console/panels/SCIMConfPanel.java       |  16 +
 .../client/console/panels/SCIMConfTabPanel.java    |  33 +-
 .../client/console/panels/SCIMConfUserPanel.java   | 630 ++++++++-------
 .../client/console/panels/SCIMConfGroupPanel.html  |  23 +
 .../client/console/panels/SCIMConfPanel.properties |   1 +
 .../console/panels/SCIMConfPanel_it.properties     |   1 +
 .../console/panels/SCIMConfPanel_pt_BR.properties  |   1 +
 .../console/panels/SCIMConfPanel_ru.properties     |   1 +
 .../client/console/panels/SCIMConfUserPanel.html   |   1 +
 .../syncope/common/lib/scim/SCIMComplexConf.java   |   1 -
 .../apache/syncope/common/lib/scim/SCIMConf.java   |   9 +
 .../common/lib/scim/SCIMEnterpriseUserConf.java    |   1 -
 .../syncope/common/lib/scim/SCIMGeneralConf.java   |   1 -
 .../syncope/common/lib/scim/SCIMGroupConf.java}    |  45 +-
 .../syncope/common/lib/scim/SCIMUserConf.java      |  15 +-
 .../apache/syncope/core/logic/SCIMDataBinder.java  | 874 +++++++++++++++------
 .../org/apache/syncope/core/logic/SCIMLogic.java   |   5 +-
 .../syncope/core/logic/scim/SearchCondVisitor.java |  65 +-
 ext/scimv2/scim-rest-api/pom.xml                   |  10 +-
 .../ext/scimv2/api/BadRequestException.java        |   1 -
 .../ext/scimv2/api/data/AuthenticationScheme.java  |   1 -
 .../scimv2/api/data/BulkConfigurationOption.java   |   1 -
 .../ext/scimv2/api/data/ConfigurationOption.java   |   1 -
 .../scimv2/api/data/FilterConfigurationOption.java |   1 -
 .../apache/syncope/ext/scimv2/api/data/Group.java  |   1 -
 .../syncope/ext/scimv2/api/data/ListResponse.java  |   1 -
 .../apache/syncope/ext/scimv2/api/data/Member.java |   1 -
 .../syncope/ext/scimv2/api/data/Reference.java     |   1 -
 .../syncope/ext/scimv2/api/data/ResourceType.java  |   1 -
 .../ext/scimv2/api/data/SCIMComplexValue.java      |   2 +-
 .../syncope/ext/scimv2/api/data/SCIMError.java     |   1 -
 .../syncope/ext/scimv2/api/data/SCIMGroup.java     |  13 +-
 .../api/data/{Reference.java => SCIMPatchOp.java}  |  27 +-
 .../ext/scimv2/api/data/SCIMPatchOperation.java}   |  51 +-
 .../api/data/SCIMPatchOperationDeserializer.java   | 227 ++++++
 .../{SchemaExtension.java => SCIMPatchPath.java}   |  47 +-
 .../syncope/ext/scimv2/api/data/SCIMResource.java  |  11 +-
 .../ext/scimv2/api/data/SCIMSearchRequest.java     |   1 -
 .../syncope/ext/scimv2/api/data/SCIMUser.java      |  24 +-
 .../ext/scimv2/api/data/SchemaExtension.java       |   1 -
 .../ext/scimv2/api/data/ServiceProviderConfig.java |   1 -
 .../apache/syncope/ext/scimv2/api/data/Value.java  |   1 -
 .../ext/scimv2/api/service/GroupService.java       |  58 --
 .../Value.java => service/SCIMGroupService.java}   |  23 +-
 .../{ReadService.java => SCIMResourceService.java} |  29 +-
 .../Value.java => service/SCIMUserService.java}    |  23 +-
 .../ext/scimv2/api/service/UserService.java        |  59 --
 .../api/{data/Value.java => type/PatchOp.java}     |  23 +-
 .../syncope/ext/scimv2/api/type/Resource.java      |   1 +
 .../data/SCIMPatchOperationDeserializerTest.java   | 349 ++++++++
 .../ext/scimv2/cxf/SCIMExceptionMapper.java        |  28 +-
 .../ext/scimv2/cxf/SCIMv2RESTCXFContext.java       |  16 +-
 ...stractService.java => AbstractSCIMService.java} |  10 +-
 ...pServiceImpl.java => SCIMGroupServiceImpl.java} | 144 ++--
 .../ext/scimv2/cxf/service/SCIMServiceImpl.java    |   2 +-
 ...erServiceImpl.java => SCIMUserServiceImpl.java} |  45 +-
 .../org/apache/syncope/fit/core/SCIMITCase.java    | 275 ++++++-
 pom.xml                                            |   4 +-
 62 files changed, 2392 insertions(+), 1187 deletions(-)

diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AbstractAnyDataBinder.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AbstractAnyDataBinder.java
index b237bf43a9..98ac929871 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AbstractAnyDataBinder.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AbstractAnyDataBinder.java
@@ -463,8 +463,7 @@ abstract class AbstractAnyDataBinder {
         SyncopeClientException invalidValues = SyncopeClientException.build(ClientExceptionType.InvalidValues);
 
         // 3. plain attributes
-        anyUR.getPlainAttrs().stream().
-                filter(patch -> patch.getAttr() != null).forEach(patch -> {
+        anyUR.getPlainAttrs().stream().filter(patch -> patch.getAttr() != null).forEach(patch -> {
             PlainSchema schema = getPlainSchema(patch.getAttr().getSchema());
             if (schema == null) {
                 LOG.debug("Invalid " + PlainSchema.class.getSimpleName() + " {}, ignoring...",
diff --git a/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfEnterpriseUserPanel.java b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfEnterpriseUserPanel.java
index 0df39fabf8..2430c8cab5 100644
--- a/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfEnterpriseUserPanel.java
+++ b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfEnterpriseUserPanel.java
@@ -19,16 +19,13 @@
 package org.apache.syncope.client.console.panels;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import org.apache.syncope.client.ui.commons.markup.html.form.AjaxTextFieldPanel;
 import org.apache.syncope.client.ui.commons.wicket.markup.html.bootstrap.tabs.Accordion;
 import org.apache.syncope.common.lib.scim.SCIMConf;
 import org.apache.syncope.common.lib.scim.SCIMEnterpriseUserConf;
 import org.apache.syncope.common.lib.scim.SCIMManagerConf;
-import org.apache.wicket.event.IEvent;
 import org.apache.wicket.extensions.markup.html.tabs.AbstractTab;
-import org.apache.wicket.extensions.markup.html.tabs.ITab;
 import org.apache.wicket.markup.html.WebMarkupContainer;
 import org.apache.wicket.markup.html.basic.Label;
 import org.apache.wicket.model.Model;
@@ -57,98 +54,93 @@ public class SCIMConfEnterpriseUserPanel extends SCIMConfTabPanel {
 
         AjaxTextFieldPanel costCenterPanel =
                 new AjaxTextFieldPanel("costCenter", "costCenter",
-                    new PropertyModel<>("costCenter", "costCenter") {
+                        new PropertyModel<>("costCenter", "costCenter") {
 
-                        private static final long serialVersionUID = -6427731218492117883L;
+                    private static final long serialVersionUID = -6427731218492117883L;
 
-                        @Override
-                        public String getObject() {
-                            return scimEnterpriseUserConf.getCostCenter();
-                        }
-
-                        @Override
-                        public void setObject(final String object) {
-                            scimEnterpriseUserConf.setCostCenter(object);
-                        }
+                    @Override
+                    public String getObject() {
+                        return scimEnterpriseUserConf.getCostCenter();
+                    }
 
-                    });
-        costCenterPanel.setChoices(plainSchemaNames);
+                    @Override
+                    public void setObject(final String object) {
+                        scimEnterpriseUserConf.setCostCenter(object);
+                    }
+                });
+        costCenterPanel.setChoices(userPlainSchemas.getObject());
 
         AjaxTextFieldPanel departmentPanel =
                 new AjaxTextFieldPanel("department", "department",
-                    new PropertyModel<>("department", "department") {
+                        new PropertyModel<>("department", "department") {
 
-                        private static final long serialVersionUID = -6427731218492117883L;
+                    private static final long serialVersionUID = -6427731218492117883L;
 
-                        @Override
-                        public String getObject() {
-                            return scimEnterpriseUserConf.getDepartment();
-                        }
-
-                        @Override
-                        public void setObject(final String object) {
-                            scimEnterpriseUserConf.setDepartment(object);
-                        }
+                    @Override
+                    public String getObject() {
+                        return scimEnterpriseUserConf.getDepartment();
+                    }
 
-                    });
-        departmentPanel.setChoices(plainSchemaNames);
+                    @Override
+                    public void setObject(final String object) {
+                        scimEnterpriseUserConf.setDepartment(object);
+                    }
+                });
+        departmentPanel.setChoices(userPlainSchemas.getObject());
 
         AjaxTextFieldPanel divisionPanel =
                 new AjaxTextFieldPanel("division", "division",
-                    new PropertyModel<>("division", "division") {
+                        new PropertyModel<>("division", "division") {
 
-                        private static final long serialVersionUID = -6427731218492117883L;
+                    private static final long serialVersionUID = -6427731218492117883L;
 
-                        @Override
-                        public String getObject() {
-                            return scimEnterpriseUserConf.getDivision();
-                        }
-
-                        @Override
-                        public void setObject(final String object) {
-                            scimEnterpriseUserConf.setDivision(object);
-                        }
+                    @Override
+                    public String getObject() {
+                        return scimEnterpriseUserConf.getDivision();
+                    }
 
-                    });
-        divisionPanel.setChoices(plainSchemaNames);
+                    @Override
+                    public void setObject(final String object) {
+                        scimEnterpriseUserConf.setDivision(object);
+                    }
+                });
+        divisionPanel.setChoices(userPlainSchemas.getObject());
 
         AjaxTextFieldPanel employeeNumberPanel =
                 new AjaxTextFieldPanel("employeeNumber", "employeeNumber",
-                    new PropertyModel<>("employeeNumber", "employeeNumber") {
+                        new PropertyModel<>("employeeNumber", "employeeNumber") {
 
-                        private static final long serialVersionUID = -6427731218492117883L;
+                    private static final long serialVersionUID = -6427731218492117883L;
 
-                        @Override
-                        public String getObject() {
-                            return scimEnterpriseUserConf.getEmployeeNumber();
-                        }
-
-                        @Override
-                        public void setObject(final String object) {
-                            scimEnterpriseUserConf.setEmployeeNumber(object);
-                        }
+                    @Override
+                    public String getObject() {
+                        return scimEnterpriseUserConf.getEmployeeNumber();
+                    }
 
-                    });
-        employeeNumberPanel.setChoices(plainSchemaNames);
+                    @Override
+                    public void setObject(final String object) {
+                        scimEnterpriseUserConf.setEmployeeNumber(object);
+                    }
+                });
+        employeeNumberPanel.setChoices(userPlainSchemas.getObject());
 
         AjaxTextFieldPanel organizationPanel =
                 new AjaxTextFieldPanel("organization", "organization",
-                    new PropertyModel<>("organization", "organization") {
+                        new PropertyModel<>("organization", "organization") {
 
-                        private static final long serialVersionUID = -6427731218492117883L;
+                    private static final long serialVersionUID = -6427731218492117883L;
 
-                        @Override
-                        public String getObject() {
-                            return scimEnterpriseUserConf.getOrganization();
-                        }
-
-                        @Override
-                        public void setObject(final String object) {
-                            scimEnterpriseUserConf.setOrganization(object);
-                        }
+                    @Override
+                    public String getObject() {
+                        return scimEnterpriseUserConf.getOrganization();
+                    }
 
-                    });
-        organizationPanel.setChoices(plainSchemaNames);
+                    @Override
+                    public void setObject(final String object) {
+                        scimEnterpriseUserConf.setOrganization(object);
+                    }
+                });
+        organizationPanel.setChoices(userPlainSchemas.getObject());
 
         // manager
         buildManagerAccordion();
@@ -160,14 +152,9 @@ public class SCIMConfEnterpriseUserPanel extends SCIMConfTabPanel {
         add(organizationPanel);
     }
 
-    @Override
-    public void onEvent(final IEvent<?> event) {
-        super.onEvent(event);
-    }
-
     private void buildManagerAccordion() {
-        final Accordion accordion = new Accordion("managerAccordion",
-                Collections.<ITab>singletonList(new AbstractTab(Model.of("manager")) {
+        Accordion accordion = new Accordion("managerAccordion",
+                List.of(new AbstractTab(Model.of("manager")) {
 
                     private static final long serialVersionUID = -5861786415855103549L;
 
@@ -178,49 +165,46 @@ public class SCIMConfEnterpriseUserPanel extends SCIMConfTabPanel {
 
                 }), Model.of(-1)); // accordion closed at beginning
         add(accordion.setOutputMarkupId(true));
-
     }
 
     private SCIMConfAccordionContainer buildNameAccordionContent(final String panelId) {
-        final List<AjaxTextFieldPanel> panelList = new ArrayList<>();
+        List<AjaxTextFieldPanel> panelList = new ArrayList<>();
 
         AjaxTextFieldPanel managerKeyPanel =
                 new AjaxTextFieldPanel("accordionContent", "manager.key",
-                    new PropertyModel<>(scimEnterpriseUserConf.getManager(), "accordionContent") {
+                        new PropertyModel<>(scimEnterpriseUserConf.getManager(), "accordionContent") {
 
-                        private static final long serialVersionUID = -6427731218492117883L;
+                    private static final long serialVersionUID = -6427731218492117883L;
 
-                        @Override
-                        public String getObject() {
-                            return scimEnterpriseUserConf.getManager().getKey();
-                        }
-
-                        @Override
-                        public void setObject(final String object) {
-                            scimEnterpriseUserConf.getManager().setKey(object);
-                        }
+                    @Override
+                    public String getObject() {
+                        return scimEnterpriseUserConf.getManager().getKey();
+                    }
 
-                    });
-        managerKeyPanel.setChoices(plainSchemaNames);
+                    @Override
+                    public void setObject(final String object) {
+                        scimEnterpriseUserConf.getManager().setKey(object);
+                    }
+                });
+        managerKeyPanel.setChoices(userPlainSchemas.getObject());
 
         AjaxTextFieldPanel managerDisplaNamePanel =
                 new AjaxTextFieldPanel("accordionContent", "manager.displayName",
-                    new PropertyModel<>(scimEnterpriseUserConf.getManager(), "accordionContent") {
+                        new PropertyModel<>(scimEnterpriseUserConf.getManager(), "accordionContent") {
 
-                        private static final long serialVersionUID = -6427731218492117883L;
+                    private static final long serialVersionUID = -6427731218492117883L;
 
-                        @Override
-                        public String getObject() {
-                            return scimEnterpriseUserConf.getManager().getDisplayName();
-                        }
-
-                        @Override
-                        public void setObject(final String object) {
-                            scimEnterpriseUserConf.getManager().setDisplayName(object);
-                        }
+                    @Override
+                    public String getObject() {
+                        return scimEnterpriseUserConf.getManager().getDisplayName();
+                    }
 
-                    });
-        managerDisplaNamePanel.setChoices(plainSchemaNames);
+                    @Override
+                    public void setObject(final String object) {
+                        scimEnterpriseUserConf.getManager().setDisplayName(object);
+                    }
+                });
+        managerDisplaNamePanel.setChoices(userPlainSchemas.getObject());
 
         panelList.add(managerKeyPanel);
         panelList.add(managerDisplaNamePanel);
@@ -229,5 +213,4 @@ public class SCIMConfEnterpriseUserPanel extends SCIMConfTabPanel {
 
         return new SCIMConfAccordionContainer(panelId, panelList);
     }
-
 }
diff --git a/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfGeneralPanel.java b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfGeneralPanel.java
index 34e9f1bf9c..d368e69d5b 100644
--- a/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfGeneralPanel.java
+++ b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfGeneralPanel.java
@@ -22,20 +22,17 @@ import java.util.Date;
 import org.apache.commons.lang3.time.DateFormatUtils;
 import org.apache.syncope.client.ui.commons.DateOps;
 import org.apache.syncope.client.ui.commons.markup.html.form.AjaxDateTimeFieldPanel;
+import org.apache.syncope.client.ui.commons.markup.html.form.AjaxSpinnerFieldPanel;
 import org.apache.syncope.client.ui.commons.markup.html.form.AjaxTextFieldPanel;
 import org.apache.syncope.common.lib.scim.SCIMConf;
 import org.apache.syncope.common.lib.scim.SCIMGeneralConf;
 import org.apache.wicket.model.Model;
 import org.apache.wicket.model.PropertyModel;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class SCIMConfGeneralPanel extends SCIMConfTabPanel {
 
     private static final long serialVersionUID = 2765863608539154422L;
 
-    private static final Logger LOG = LoggerFactory.getLogger(SCIMConfGeneralPanel.class);
-
     public SCIMConfGeneralPanel(final String id, final SCIMConf scimConf) {
         super(id);
 
@@ -75,71 +72,17 @@ public class SCIMConfGeneralPanel extends SCIMConfTabPanel {
                 }, DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT);
         lastChangeDatePanel.setEnabled(false);
 
-        AjaxTextFieldPanel bulkMaxOperationsPanel =
-                new AjaxTextFieldPanel("bulkMaxOperations", "bulkMaxOperations",
-                        new PropertyModel<>("bulkMaxOperations", "bulkMaxOperations") {
-
-                    private static final long serialVersionUID = -6427731218492117883L;
-
-                    @Override
-                    public String getObject() {
-                        return String.valueOf(scimGeneralConf.getBulkMaxOperations());
-                    }
-
-                    @Override
-                    public void setObject(final String object) {
-                        try {
-                            scimGeneralConf.setBulkMaxOperations(Integer.parseInt(object));
-                        } catch (NumberFormatException e) {
-                            LOG.error("Invalid value provided for 'bulkMaxOperations': {}", object, e);
-                        }
-                    }
-                });
-        bulkMaxOperationsPanel.setChoices(plainSchemaNames);
-
-        AjaxTextFieldPanel bulkMaxMaxPayloadSizePanel =
-                new AjaxTextFieldPanel("bulkMaxMaxPayloadSize", "bulkMaxMaxPayloadSize",
-                        new PropertyModel<>("bulkMaxMaxPayloadSize", "bulkMaxMaxPayloadSize") {
-
-                    private static final long serialVersionUID = -6427731218492117883L;
-
-                    @Override
-                    public String getObject() {
-                        return String.valueOf(scimGeneralConf.getBulkMaxPayloadSize());
-                    }
-
-                    @Override
-                    public void setObject(final String object) {
-                        try {
-                            scimGeneralConf.setBulkMaxPayloadSize(Integer.parseInt(object));
-                        } catch (NumberFormatException e) {
-                            LOG.error("Invalid value provided for 'bulkMaxPayloadSize': {}", object, e);
-                        }
-                    }
-                });
-        bulkMaxMaxPayloadSizePanel.setChoices(plainSchemaNames);
-
-        AjaxTextFieldPanel filterMaxResultsPanel =
-                new AjaxTextFieldPanel("filterMaxResults", "filterMaxResults",
-                        new PropertyModel<>("filterMaxResults", "filterMaxResults") {
+        AjaxSpinnerFieldPanel<Integer> bulkMaxOperationsPanel = new AjaxSpinnerFieldPanel.Builder<Integer>().
+                build("bulkMaxOperations", "bulkMaxOperations", Integer.class,
+                        new PropertyModel<>(scimGeneralConf, "bulkMaxOperations"));
 
-                    private static final long serialVersionUID = -6427731218492117883L;
+        AjaxSpinnerFieldPanel<Integer> bulkMaxMaxPayloadSizePanel = new AjaxSpinnerFieldPanel.Builder<Integer>().
+                build("bulkMaxMaxPayloadSize", "bulkMaxMaxPayloadSize", Integer.class,
+                        new PropertyModel<>(scimGeneralConf, "bulkMaxMaxPayloadSize"));
 
-                    @Override
-                    public String getObject() {
-                        return String.valueOf(scimGeneralConf.getFilterMaxResults());
-                    }
-
-                    @Override
-                    public void setObject(final String object) {
-                        try {
-                            scimGeneralConf.setFilterMaxResults(Integer.parseInt(object));
-                        } catch (NumberFormatException e) {
-                            LOG.error("Invalid value provided for 'filterMaxResults': {}", object, e);
-                        }
-                    }
-                });
-        filterMaxResultsPanel.setChoices(plainSchemaNames);
+        AjaxSpinnerFieldPanel<Integer> filterMaxResultsPanel = new AjaxSpinnerFieldPanel.Builder<Integer>().
+                build("filterMaxResults", "filterMaxResults", Integer.class,
+                        new PropertyModel<>(scimGeneralConf, "filterMaxResults"));
 
         AjaxTextFieldPanel eTagValuePanel = new AjaxTextFieldPanel("eTagValue", "eTagValue",
                 new PropertyModel<>("eTagValue", "eTagValue") {
diff --git a/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfGroupPanel.java b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfGroupPanel.java
new file mode 100644
index 0000000000..95b4d97ee6
--- /dev/null
+++ b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfGroupPanel.java
@@ -0,0 +1,69 @@
+/*
+ * 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.client.console.panels;
+
+import org.apache.syncope.client.ui.commons.markup.html.form.AjaxTextFieldPanel;
+import org.apache.syncope.common.lib.scim.SCIMConf;
+import org.apache.syncope.common.lib.scim.SCIMGroupConf;
+import org.apache.wicket.extensions.ajax.markup.html.autocomplete.AutoCompleteSettings;
+import org.apache.wicket.model.PropertyModel;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class SCIMConfGroupPanel extends SCIMConfTabPanel {
+
+    protected static final Logger LOG = LoggerFactory.getLogger(SCIMConfGroupPanel.class);
+
+    private static final long serialVersionUID = 8747864142447220523L;
+
+    private final SCIMGroupConf scimGroupConf;
+
+    public SCIMConfGroupPanel(final String id, final SCIMConf scimConf) {
+        super(id);
+
+        if (scimConf.getGroupConf() == null) {
+            scimConf.setGroupConf(new SCIMGroupConf());
+        }
+        scimGroupConf = scimConf.getGroupConf();
+
+        final AutoCompleteSettings settings = new AutoCompleteSettings();
+        settings.setShowCompleteListOnFocusGain(true);
+        settings.setShowListOnEmptyInput(true);
+        settings.setCssClassName("custom-autocomplete-box");
+
+        AjaxTextFieldPanel externalIdPanel = new AjaxTextFieldPanel("externalId", "externalId",
+                new PropertyModel<>("externalId", "externalId") {
+
+            private static final long serialVersionUID = -6427731218492117883L;
+
+            @Override
+            public String getObject() {
+                return scimGroupConf.getExternalId();
+            }
+
+            @Override
+            public void setObject(final String object) {
+                scimGroupConf.setExternalId(object);
+            }
+        });
+        externalIdPanel.setChoices(groupPlainSchemas.getObject());
+
+        add(externalIdPanel);
+    }
+}
diff --git a/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfPanel.java b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfPanel.java
index ddc86bb26d..1ca2796d72 100644
--- a/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfPanel.java
+++ b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfPanel.java
@@ -128,6 +128,22 @@ public abstract class SCIMConfPanel extends WizardMgtPanel<SCIMConf> {
             }
         });
 
+        tabs.add(new ITabComponent(
+                new Model<>(getString("tab4")), getString("tab4")) {
+
+            private static final long serialVersionUID = 1998052474181916792L;
+
+            @Override
+            public WebMarkupContainer getPanel(final String panelId) {
+                return new SCIMConfGroupPanel(panelId, scimConfTO);
+            }
+
+            @Override
+            public boolean isVisible() {
+                return true;
+            }
+        });
+
         return tabs;
     }
 
diff --git a/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfTabPanel.java b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfTabPanel.java
index 6ac28e1cff..0c3be86032 100644
--- a/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfTabPanel.java
+++ b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfTabPanel.java
@@ -26,21 +26,38 @@ import org.apache.syncope.common.lib.to.SchemaTO;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.SchemaType;
 import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.model.LoadableDetachableModel;
 
 public class SCIMConfTabPanel extends Panel implements ModalPanel {
 
     private static final long serialVersionUID = -4482885585790492795L;
 
-    protected final List<String> plainSchemaNames = getPlainSchemas();
+    protected final LoadableDetachableModel<List<String>> userPlainSchemas = new LoadableDetachableModel<>() {
+
+        private static final long serialVersionUID = 4659376149825914247L;
+
+        @Override
+        protected List<String> load() {
+            return SchemaRestClient.getSchemas(SchemaType.PLAIN, AnyTypeKind.USER).stream().
+                    map(SchemaTO::getKey).
+                    filter(name -> !"password".equals(name)).
+                    sorted().collect(Collectors.toList());
+        }
+    };
+
+    protected final LoadableDetachableModel<List<String>> groupPlainSchemas = new LoadableDetachableModel<>() {
+
+        private static final long serialVersionUID = 4659376149825914247L;
+
+        @Override
+        protected List<String> load() {
+            return SchemaRestClient.getSchemas(SchemaType.PLAIN, AnyTypeKind.GROUP).stream().
+                    map(SchemaTO::getKey).
+                    sorted().collect(Collectors.toList());
+        }
+    };
 
     public SCIMConfTabPanel(final String id) {
         super(id);
     }
-
-    private static List<String> getPlainSchemas() {
-        return SchemaRestClient.getSchemas(SchemaType.PLAIN, AnyTypeKind.USER).stream().
-                map(SchemaTO::getKey).
-                filter(name -> !"password".equals(name)).
-                sorted().collect(Collectors.toList());
-    }
 }
diff --git a/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfUserPanel.java b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfUserPanel.java
index 44060b8699..938bec2684 100644
--- a/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfUserPanel.java
+++ b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfUserPanel.java
@@ -19,7 +19,6 @@
 package org.apache.syncope.client.console.panels;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import org.apache.syncope.client.console.wicket.markup.html.form.MultiFieldPanel;
 import org.apache.syncope.client.ui.commons.markup.html.form.AjaxTextFieldPanel;
@@ -34,10 +33,8 @@ import org.apache.syncope.common.lib.scim.types.EmailCanonicalType;
 import org.apache.syncope.common.lib.scim.types.IMCanonicalType;
 import org.apache.syncope.common.lib.scim.types.PhoneNumberCanonicalType;
 import org.apache.syncope.common.lib.scim.types.PhotoCanonicalType;
-import org.apache.wicket.event.IEvent;
 import org.apache.wicket.extensions.ajax.markup.html.autocomplete.AutoCompleteSettings;
 import org.apache.wicket.extensions.markup.html.tabs.AbstractTab;
-import org.apache.wicket.extensions.markup.html.tabs.ITab;
 import org.apache.wicket.markup.html.WebMarkupContainer;
 import org.apache.wicket.markup.html.basic.Label;
 import org.apache.wicket.model.Model;
@@ -70,149 +67,158 @@ public class SCIMConfUserPanel extends SCIMConfTabPanel {
         settings.setShowListOnEmptyInput(true);
         settings.setCssClassName("custom-autocomplete-box");
 
-        AjaxTextFieldPanel displayNamePanel = new AjaxTextFieldPanel("displayName", "displayName",
-            new PropertyModel<>("displayName", "displayName") {
+        AjaxTextFieldPanel externalIdPanel = new AjaxTextFieldPanel("externalId", "externalId",
+                new PropertyModel<>("externalId", "externalId") {
 
-                private static final long serialVersionUID = -6427731218492117883L;
+            private static final long serialVersionUID = -6427731218492117883L;
 
-                @Override
-                public String getObject() {
-                    return scimUserConf.getDisplayName();
-                }
+            @Override
+            public String getObject() {
+                return scimUserConf.getExternalId();
+            }
 
-                @Override
-                public void setObject(final String object) {
-                    scimUserConf.setDisplayName(object);
-                }
+            @Override
+            public void setObject(final String object) {
+                scimUserConf.setExternalId(object);
+            }
+        });
+        externalIdPanel.setChoices(userPlainSchemas.getObject());
 
-            });
-        displayNamePanel.setChoices(plainSchemaNames);
+        AjaxTextFieldPanel displayNamePanel = new AjaxTextFieldPanel("displayName", "displayName",
+                new PropertyModel<>("displayName", "displayName") {
 
-        AjaxTextFieldPanel localePanel = new AjaxTextFieldPanel("locale", "locale",
-            new PropertyModel<>("locale", "locale") {
+            private static final long serialVersionUID = -6427731218492117883L;
 
-                private static final long serialVersionUID = -6427731218492117883L;
+            @Override
+            public String getObject() {
+                return scimUserConf.getDisplayName();
+            }
 
-                @Override
-                public String getObject() {
-                    return scimUserConf.getLocale();
-                }
+            @Override
+            public void setObject(final String object) {
+                scimUserConf.setDisplayName(object);
+            }
+        });
+        displayNamePanel.setChoices(userPlainSchemas.getObject());
 
-                @Override
-                public void setObject(final String object) {
-                    scimUserConf.setLocale(object);
-                }
+        AjaxTextFieldPanel localePanel = new AjaxTextFieldPanel("locale", "locale",
+                new PropertyModel<>("locale", "locale") {
 
-            });
-        localePanel.setChoices(plainSchemaNames);
+            private static final long serialVersionUID = -6427731218492117883L;
 
-        AjaxTextFieldPanel nickNamePanel = new AjaxTextFieldPanel("nickName", "nickName",
-            new PropertyModel<>("nickName", "nickName") {
+            @Override
+            public String getObject() {
+                return scimUserConf.getLocale();
+            }
 
-                private static final long serialVersionUID = -6427731218492117883L;
+            @Override
+            public void setObject(final String object) {
+                scimUserConf.setLocale(object);
+            }
+        });
+        localePanel.setChoices(userPlainSchemas.getObject());
 
-                @Override
-                public String getObject() {
-                    return scimUserConf.getNickName();
-                }
+        AjaxTextFieldPanel nickNamePanel = new AjaxTextFieldPanel("nickName", "nickName",
+                new PropertyModel<>("nickName", "nickName") {
 
-                @Override
-                public void setObject(final String object) {
-                    scimUserConf.setNickName(object);
-                }
+            private static final long serialVersionUID = -6427731218492117883L;
 
-            });
-        nickNamePanel.setChoices(plainSchemaNames);
+            @Override
+            public String getObject() {
+                return scimUserConf.getNickName();
+            }
 
-        AjaxTextFieldPanel preferredLanguagePanel = new AjaxTextFieldPanel("preferredLanguage", "preferredLanguage",
-            new PropertyModel<>("preferredLanguage", "preferredLanguage") {
+            @Override
+            public void setObject(final String object) {
+                scimUserConf.setNickName(object);
+            }
+        });
+        nickNamePanel.setChoices(userPlainSchemas.getObject());
 
-                private static final long serialVersionUID = -6427731218492117883L;
+        AjaxTextFieldPanel preferredLanguagePanel = new AjaxTextFieldPanel("preferredLanguage", "preferredLanguage",
+                new PropertyModel<>("preferredLanguage", "preferredLanguage") {
 
-                @Override
-                public String getObject() {
-                    return scimUserConf.getPreferredLanguage();
-                }
+            private static final long serialVersionUID = -6427731218492117883L;
 
-                @Override
-                public void setObject(final String object) {
-                    scimUserConf.setPreferredLanguage(object);
-                }
+            @Override
+            public String getObject() {
+                return scimUserConf.getPreferredLanguage();
+            }
 
-            });
-        preferredLanguagePanel.setChoices(plainSchemaNames);
+            @Override
+            public void setObject(final String object) {
+                scimUserConf.setPreferredLanguage(object);
+            }
+        });
+        preferredLanguagePanel.setChoices(userPlainSchemas.getObject());
 
         AjaxTextFieldPanel profileUrlPanel = new AjaxTextFieldPanel("profileUrl", "profileUrl",
-            new PropertyModel<>("profileUrl", "profileUrl") {
-
-                private static final long serialVersionUID = -6427731218492117883L;
+                new PropertyModel<>("profileUrl", "profileUrl") {
 
-                @Override
-                public String getObject() {
-                    return scimUserConf.getProfileUrl();
-                }
+            private static final long serialVersionUID = -6427731218492117883L;
 
-                @Override
-                public void setObject(final String object) {
-                    scimUserConf.setProfileUrl(object);
-                }
+            @Override
+            public String getObject() {
+                return scimUserConf.getProfileUrl();
+            }
 
-            });
-        profileUrlPanel.setChoices(plainSchemaNames);
+            @Override
+            public void setObject(final String object) {
+                scimUserConf.setProfileUrl(object);
+            }
+        });
+        profileUrlPanel.setChoices(userPlainSchemas.getObject());
 
         AjaxTextFieldPanel timezonePanel = new AjaxTextFieldPanel("timezone", "timezone",
-            new PropertyModel<>("timezone", "timezone") {
+                new PropertyModel<>("timezone", "timezone") {
 
-                private static final long serialVersionUID = -6427731218492117883L;
+            private static final long serialVersionUID = -6427731218492117883L;
 
-                @Override
-                public String getObject() {
-                    return scimUserConf.getTimezone();
-                }
+            @Override
+            public String getObject() {
+                return scimUserConf.getTimezone();
+            }
 
-                @Override
-                public void setObject(final String object) {
-                    scimUserConf.setTimezone(object);
-                }
-
-            });
-        timezonePanel.setChoices(plainSchemaNames);
+            @Override
+            public void setObject(final String object) {
+                scimUserConf.setTimezone(object);
+            }
+        });
+        timezonePanel.setChoices(userPlainSchemas.getObject());
 
         AjaxTextFieldPanel titlePanel = new AjaxTextFieldPanel("title", "title",
-            new PropertyModel<>("title", "title") {
-
-                private static final long serialVersionUID = -6427731218492117883L;
+                new PropertyModel<>("title", "title") {
 
-                @Override
-                public String getObject() {
-                    return scimUserConf.getTitle();
-                }
+            private static final long serialVersionUID = -6427731218492117883L;
 
-                @Override
-                public void setObject(final String object) {
-                    scimUserConf.setTitle(object);
-                }
+            @Override
+            public String getObject() {
+                return scimUserConf.getTitle();
+            }
 
-            });
-        titlePanel.setChoices(plainSchemaNames);
+            @Override
+            public void setObject(final String object) {
+                scimUserConf.setTitle(object);
+            }
+        });
+        titlePanel.setChoices(userPlainSchemas.getObject());
 
         AjaxTextFieldPanel userTypePanel = new AjaxTextFieldPanel("userType", "userType",
-            new PropertyModel<>("userType", "userType") {
+                new PropertyModel<>("userType", "userType") {
 
-                private static final long serialVersionUID = -6427731218492117883L;
+            private static final long serialVersionUID = -6427731218492117883L;
 
-                @Override
-                public String getObject() {
-                    return scimUserConf.getUserType();
-                }
+            @Override
+            public String getObject() {
+                return scimUserConf.getUserType();
+            }
 
-                @Override
-                public void setObject(final String object) {
-                    scimUserConf.setUserType(object);
-                }
-
-            });
-        userTypePanel.setChoices(plainSchemaNames);
+            @Override
+            public void setObject(final String object) {
+                scimUserConf.setUserType(object);
+            }
+        });
+        userTypePanel.setChoices(userPlainSchemas.getObject());
 
         // name
         buildNameAccordion();
@@ -220,7 +226,7 @@ public class SCIMConfUserPanel extends SCIMConfTabPanel {
         // x509certificates
         final AjaxTextFieldPanel x509CertificatesPanel = new AjaxTextFieldPanel("panel", "x509CertificatesPanel",
                 new Model<>(null));
-        x509CertificatesPanel.setChoices(plainSchemaNames);
+        x509CertificatesPanel.setChoices(userPlainSchemas.getObject());
         MultiFieldPanel<String> x509CertificatesMultiPanel = new MultiFieldPanel.Builder<>(
                 new ListModel<>(scimUserConf.getX509Certificates())).build(
                 "x509Certificates",
@@ -250,6 +256,7 @@ public class SCIMConfUserPanel extends SCIMConfTabPanel {
         buildComplexPanels(scimUserConf.getIms(), "imsAccordion", "ims", IMCanonicalType.values());
         buildComplexPanels(scimUserConf.getPhotos(), "photosAccordion", "photos", PhotoCanonicalType.values());
 
+        add(externalIdPanel);
         add(displayNamePanel);
         add(localePanel);
         add(nickNamePanel);
@@ -267,7 +274,6 @@ public class SCIMConfUserPanel extends SCIMConfTabPanel {
         add(new Label("phoneNumbersLabel", Model.of("phoneNumbers")));
         add(new Label("imsLabel", Model.of("ims")));
         add(new Label("photosLabel", Model.of("photos")));
-
     }
 
     private <T extends Enum<?>> void buildComplexPanels(
@@ -293,23 +299,22 @@ public class SCIMConfUserPanel extends SCIMConfTabPanel {
     }
 
     private void buildNameAccordion() {
-        final Accordion accordion = new Accordion("nameAccordion",
-                Collections.<ITab>singletonList(new AbstractTab(Model.of("name")) {
+        Accordion accordion = new Accordion("nameAccordion", List.of(new AbstractTab(Model.of("name")) {
 
-                    private static final long serialVersionUID = -5861786415855103549L;
+            private static final long serialVersionUID = -5861786415855103549L;
 
-                    @Override
-                    public WebMarkupContainer getPanel(final String panelId) {
-                        return buildNameAccordionContent(panelId);
-                    }
+            @Override
+            public WebMarkupContainer getPanel(final String panelId) {
+                return buildNameAccordionContent(panelId);
+            }
 
-                }), Model.of(-1)); // accordion closed at beginning
+        }), Model.of(-1)); // accordion closed at beginning
         add(accordion.setOutputMarkupId(true));
     }
 
     private void buildAddressAccordion(final SCIMUserAddressConf address, final AddressCanonicalType canonicalType) {
         Accordion accordion = new Accordion("addressesAccordion_" + address.getType().name(),
-                Collections.<ITab>singletonList(new AbstractTab(Model.of("address." + address.getType().name())) {
+                List.of(new AbstractTab(Model.of("address." + address.getType().name())) {
 
                     private static final long serialVersionUID = -5861786415855103549L;
 
@@ -317,7 +322,6 @@ public class SCIMConfUserPanel extends SCIMConfTabPanel {
                     public WebMarkupContainer getPanel(final String panelId) {
                         return buildAddressAccordionContent(address, canonicalType, panelId);
                     }
-
                 }), Model.of(-1)); // accordion closed at beginning
         add(accordion.setOutputMarkupId(true));
     }
@@ -329,7 +333,7 @@ public class SCIMConfUserPanel extends SCIMConfTabPanel {
             final T canonicalType) {
 
         Accordion accordion = new Accordion(basePanelId + '_' + complex.getType().name(),
-                Collections.<ITab>singletonList(new AbstractTab(Model.of(baseTabId + '.' + complex.getType().name())) {
+                List.of(new AbstractTab(Model.of(baseTabId + '.' + complex.getType().name())) {
 
                     private static final long serialVersionUID = -5861786415855103549L;
 
@@ -343,115 +347,109 @@ public class SCIMConfUserPanel extends SCIMConfTabPanel {
     }
 
     private SCIMConfAccordionContainer buildNameAccordionContent(final String panelId) {
-        final List<AjaxTextFieldPanel> panelList = new ArrayList<>();
+        List<AjaxTextFieldPanel> panelList = new ArrayList<>();
 
         AjaxTextFieldPanel nameFamilyNamePanel = new AjaxTextFieldPanel("accordionContent", "name.familyName",
-            new PropertyModel<>(scimUserConf.getName(), "accordionContent") {
+                new PropertyModel<>(scimUserConf.getName(), "accordionContent") {
 
-                private static final long serialVersionUID = -6427731218492117883L;
+            private static final long serialVersionUID = -6427731218492117883L;
 
-                @Override
-                public String getObject() {
-                    return scimUserConf.getName().getFamilyName();
-                }
+            @Override
+            public String getObject() {
+                return scimUserConf.getName().getFamilyName();
+            }
 
-                @Override
-                public void setObject(final String object) {
-                    scimUserConf.getName().setFamilyName(object);
-                }
-
-            });
-        nameFamilyNamePanel.setChoices(plainSchemaNames);
+            @Override
+            public void setObject(final String object) {
+                scimUserConf.getName().setFamilyName(object);
+            }
+        });
+        nameFamilyNamePanel.setChoices(userPlainSchemas.getObject());
 
         AjaxTextFieldPanel nameFormattedPanel = new AjaxTextFieldPanel("accordionContent", "name.formatted",
-            new PropertyModel<>(scimUserConf.getName(), "accordionContent") {
-
-                private static final long serialVersionUID = -6427731218492117883L;
+                new PropertyModel<>(scimUserConf.getName(), "accordionContent") {
 
-                @Override
-                public String getObject() {
-                    return scimUserConf.getName().getFormatted();
-                }
+            private static final long serialVersionUID = -6427731218492117883L;
 
-                @Override
-                public void setObject(final String object) {
-                    scimUserConf.getName().setFormatted(object);
-                }
+            @Override
+            public String getObject() {
+                return scimUserConf.getName().getFormatted();
+            }
 
-            });
-        nameFormattedPanel.setChoices(plainSchemaNames);
+            @Override
+            public void setObject(final String object) {
+                scimUserConf.getName().setFormatted(object);
+            }
+        });
+        nameFormattedPanel.setChoices(userPlainSchemas.getObject());
 
         AjaxTextFieldPanel nameGivenNamePanel = new AjaxTextFieldPanel("accordionContent", "name.givenName",
-            new PropertyModel<>(scimUserConf.getName(), "accordionContent") {
+                new PropertyModel<>(scimUserConf.getName(), "accordionContent") {
 
-                private static final long serialVersionUID = -6427731218492117883L;
+            private static final long serialVersionUID = -6427731218492117883L;
 
-                @Override
-                public String getObject() {
-                    return scimUserConf.getName().getGivenName();
-                }
+            @Override
+            public String getObject() {
+                return scimUserConf.getName().getGivenName();
+            }
 
-                @Override
-                public void setObject(final String object) {
-                    scimUserConf.getName().setGivenName(object);
-                }
-
-            });
-        nameGivenNamePanel.setChoices(plainSchemaNames);
+            @Override
+            public void setObject(final String object) {
+                scimUserConf.getName().setGivenName(object);
+            }
+        });
+        nameGivenNamePanel.setChoices(userPlainSchemas.getObject());
 
         AjaxTextFieldPanel nameHonorificPrefixPanel = new AjaxTextFieldPanel("accordionContent", "name.honorificPrefix",
-            new PropertyModel<>(scimUserConf.getName(), "accordionContent") {
-
-                private static final long serialVersionUID = -6427731218492117883L;
+                new PropertyModel<>(scimUserConf.getName(), "accordionContent") {
 
-                @Override
-                public String getObject() {
-                    return scimUserConf.getName().getHonorificPrefix();
-                }
+            private static final long serialVersionUID = -6427731218492117883L;
 
-                @Override
-                public void setObject(final String object) {
-                    scimUserConf.getName().setHonorificPrefix(object);
-                }
+            @Override
+            public String getObject() {
+                return scimUserConf.getName().getHonorificPrefix();
+            }
 
-            });
-        nameHonorificPrefixPanel.setChoices(plainSchemaNames);
+            @Override
+            public void setObject(final String object) {
+                scimUserConf.getName().setHonorificPrefix(object);
+            }
+        });
+        nameHonorificPrefixPanel.setChoices(userPlainSchemas.getObject());
 
         AjaxTextFieldPanel nameHonorificSuffixPanel = new AjaxTextFieldPanel("accordionContent", "name.honorificSuffix",
-            new PropertyModel<>(scimUserConf.getName(), "accordionContent") {
-
-                private static final long serialVersionUID = -6427731218492117883L;
+                new PropertyModel<>(scimUserConf.getName(), "accordionContent") {
 
-                @Override
-                public String getObject() {
-                    return scimUserConf.getName().getHonorificSuffix();
-                }
+            private static final long serialVersionUID = -6427731218492117883L;
 
-                @Override
-                public void setObject(final String object) {
-                    scimUserConf.getName().setHonorificSuffix(object);
-                }
+            @Override
+            public String getObject() {
+                return scimUserConf.getName().getHonorificSuffix();
+            }
 
-            });
-        nameHonorificSuffixPanel.setChoices(plainSchemaNames);
+            @Override
+            public void setObject(final String object) {
+                scimUserConf.getName().setHonorificSuffix(object);
+            }
+        });
+        nameHonorificSuffixPanel.setChoices(userPlainSchemas.getObject());
 
         AjaxTextFieldPanel nameMiddleNamePanel = new AjaxTextFieldPanel("accordionContent", "name.middleName",
-            new PropertyModel<>(scimUserConf.getName(), "accordionContent") {
+                new PropertyModel<>(scimUserConf.getName(), "accordionContent") {
 
-                private static final long serialVersionUID = -6427731218492117883L;
+            private static final long serialVersionUID = -6427731218492117883L;
 
-                @Override
-                public String getObject() {
-                    return scimUserConf.getName().getMiddleName();
-                }
+            @Override
+            public String getObject() {
+                return scimUserConf.getName().getMiddleName();
+            }
 
-                @Override
-                public void setObject(final String object) {
-                    scimUserConf.getName().setMiddleName(object);
-                }
-
-            });
-        nameMiddleNamePanel.setChoices(plainSchemaNames);
+            @Override
+            public void setObject(final String object) {
+                scimUserConf.getName().setMiddleName(object);
+            }
+        });
+        nameMiddleNamePanel.setChoices(userPlainSchemas.getObject());
 
         panelList.add(nameFamilyNamePanel);
         panelList.add(nameFormattedPanel);
@@ -468,61 +466,58 @@ public class SCIMConfUserPanel extends SCIMConfTabPanel {
             final T canonicalType,
             final String panelId) {
 
-        final List<AjaxTextFieldPanel> panelList = new ArrayList<>();
-        final String fieldName = panelId + '.' + canonicalType.name();
+        List<AjaxTextFieldPanel> panelList = new ArrayList<>();
+        String fieldName = panelId + '.' + canonicalType.name();
 
         AjaxTextFieldPanel displayPanel = new AjaxTextFieldPanel("accordionContent", fieldName + ".display",
-            new PropertyModel<>(complex, "accordionContent") {
-
-                private static final long serialVersionUID = -6427731218492117883L;
+                new PropertyModel<>(complex, "accordionContent") {
 
-                @Override
-                public String getObject() {
-                    return complex.getDisplay();
-                }
+            private static final long serialVersionUID = -6427731218492117883L;
 
-                @Override
-                public void setObject(final String object) {
-                    complex.setDisplay(object);
-                }
+            @Override
+            public String getObject() {
+                return complex.getDisplay();
+            }
 
-            });
-        displayPanel.setChoices(plainSchemaNames);
+            @Override
+            public void setObject(final String object) {
+                complex.setDisplay(object);
+            }
+        });
+        displayPanel.setChoices(userPlainSchemas.getObject());
 
         AjaxTextFieldPanel valuePanel = new AjaxTextFieldPanel("accordionContent", fieldName + ".value",
-            new PropertyModel<>(complex, "accordionContent") {
+                new PropertyModel<>(complex, "accordionContent") {
 
-                private static final long serialVersionUID = -6427731218492117883L;
+            private static final long serialVersionUID = -6427731218492117883L;
 
-                @Override
-                public String getObject() {
-                    return complex.getValue();
-                }
+            @Override
+            public String getObject() {
+                return complex.getValue();
+            }
 
-                @Override
-                public void setObject(final String object) {
-                    complex.setValue(object);
-                }
-
-            });
-        valuePanel.setChoices(plainSchemaNames);
+            @Override
+            public void setObject(final String object) {
+                complex.setValue(object);
+            }
+        });
+        valuePanel.setChoices(userPlainSchemas.getObject());
 
         AjaxTextFieldPanel primaryPanel = new AjaxTextFieldPanel("accordionContent", fieldName + ".primary",
-            new PropertyModel<>(complex, "accordionContent") {
-
-                private static final long serialVersionUID = -6427731218492117883L;
+                new PropertyModel<>(complex, "accordionContent") {
 
-                @Override
-                public String getObject() {
-                    return String.valueOf(complex.isPrimary());
-                }
+            private static final long serialVersionUID = -6427731218492117883L;
 
-                @Override
-                public void setObject(final String object) {
-                    complex.setPrimary(Boolean.valueOf(object));
-                }
+            @Override
+            public String getObject() {
+                return String.valueOf(complex.isPrimary());
+            }
 
-            });
+            @Override
+            public void setObject(final String object) {
+                complex.setPrimary(Boolean.parseBoolean(object));
+            }
+        });
         primaryPanel.setChoices(List.of("true", "false"));
 
         panelList.add(displayPanel);
@@ -537,116 +532,110 @@ public class SCIMConfUserPanel extends SCIMConfTabPanel {
             final AddressCanonicalType canonicalType,
             final String panelId) {
 
-        final List<AjaxTextFieldPanel> panelList = new ArrayList<>();
-        final String fieldName = "addresses." + canonicalType.name();
+        List<AjaxTextFieldPanel> panelList = new ArrayList<>();
+        String fieldName = "addresses." + canonicalType.name();
 
         AjaxTextFieldPanel addressCountryPanel = new AjaxTextFieldPanel("accordionContent", fieldName + ".country",
-            new PropertyModel<>(address, "accordionContent") {
-
-                private static final long serialVersionUID = -6427731218492117883L;
+                new PropertyModel<>(address, "accordionContent") {
 
-                @Override
-                public String getObject() {
-                    return address.getCountry();
-                }
+            private static final long serialVersionUID = -6427731218492117883L;
 
-                @Override
-                public void setObject(final String object) {
-                    address.setCountry(object);
-                }
+            @Override
+            public String getObject() {
+                return address.getCountry();
+            }
 
-            });
-        addressCountryPanel.setChoices(plainSchemaNames);
+            @Override
+            public void setObject(final String object) {
+                address.setCountry(object);
+            }
+        });
+        addressCountryPanel.setChoices(userPlainSchemas.getObject());
 
         AjaxTextFieldPanel addressFormattedPanel = new AjaxTextFieldPanel("accordionContent", fieldName + ".formatted",
-            new PropertyModel<>(address, "accordionContent") {
+                new PropertyModel<>(address, "accordionContent") {
 
-                private static final long serialVersionUID = -6427731218492117883L;
+            private static final long serialVersionUID = -6427731218492117883L;
 
-                @Override
-                public String getObject() {
-                    return address.getFormatted();
-                }
+            @Override
+            public String getObject() {
+                return address.getFormatted();
+            }
 
-                @Override
-                public void setObject(final String object) {
-                    address.setFormatted(object);
-                }
-
-            });
-        addressFormattedPanel.setChoices(plainSchemaNames);
+            @Override
+            public void setObject(final String object) {
+                address.setFormatted(object);
+            }
+        });
+        addressFormattedPanel.setChoices(userPlainSchemas.getObject());
 
         AjaxTextFieldPanel addressLocalityPanel = new AjaxTextFieldPanel("accordionContent", fieldName + ".locality",
-            new PropertyModel<>(address, "accordionContent") {
-
-                private static final long serialVersionUID = -6427731218492117883L;
+                new PropertyModel<>(address, "accordionContent") {
 
-                @Override
-                public String getObject() {
-                    return address.getLocality();
-                }
+            private static final long serialVersionUID = -6427731218492117883L;
 
-                @Override
-                public void setObject(final String object) {
-                    address.setLocality(object);
-                }
+            @Override
+            public String getObject() {
+                return address.getLocality();
+            }
 
-            });
-        addressLocalityPanel.setChoices(plainSchemaNames);
+            @Override
+            public void setObject(final String object) {
+                address.setLocality(object);
+            }
+        });
+        addressLocalityPanel.setChoices(userPlainSchemas.getObject());
 
         AjaxTextFieldPanel addressRegionPanel = new AjaxTextFieldPanel("accordionContent", fieldName + ".region",
-            new PropertyModel<>(address, "accordionContent") {
+                new PropertyModel<>(address, "accordionContent") {
 
-                private static final long serialVersionUID = -6427731218492117883L;
+            private static final long serialVersionUID = -6427731218492117883L;
 
-                @Override
-                public String getObject() {
-                    return address.getRegion();
-                }
+            @Override
+            public String getObject() {
+                return address.getRegion();
+            }
 
-                @Override
-                public void setObject(final String object) {
-                    address.setRegion(object);
-                }
-
-            });
-        addressRegionPanel.setChoices(plainSchemaNames);
+            @Override
+            public void setObject(final String object) {
+                address.setRegion(object);
+            }
+        });
+        addressRegionPanel.setChoices(userPlainSchemas.getObject());
 
         AjaxTextFieldPanel addressPostalCodePanel =
                 new AjaxTextFieldPanel("accordionContent", fieldName + ".postalCode",
-                    new PropertyModel<>(address, "accordionContent") {
-
-                        private static final long serialVersionUID = -6427731218492117883L;
+                        new PropertyModel<>(address, "accordionContent") {
 
-                        @Override
-                        public String getObject() {
-                            return address.getPostalCode();
-                        }
+                    private static final long serialVersionUID = -6427731218492117883L;
 
-                        @Override
-                        public void setObject(final String object) {
-                            address.setPostalCode(object);
-                        }
+                    @Override
+                    public String getObject() {
+                        return address.getPostalCode();
+                    }
 
-                    });
-        addressPostalCodePanel.setChoices(plainSchemaNames);
+                    @Override
+                    public void setObject(final String object) {
+                        address.setPostalCode(object);
+                    }
+                });
+        addressPostalCodePanel.setChoices(userPlainSchemas.getObject());
 
         AjaxTextFieldPanel addressPrimaryPanel = new AjaxTextFieldPanel("accordionContent", fieldName + ".primary",
-            new PropertyModel<>(address, "accordionContent") {
-
-                private static final long serialVersionUID = -6427731218492117883L;
+                new PropertyModel<>(address, "accordionContent") {
 
-                @Override
-                public String getObject() {
-                    return String.valueOf(address.isPrimary());
-                }
+            private static final long serialVersionUID = -6427731218492117883L;
 
-                @Override
-                public void setObject(final String object) {
-                    address.setPrimary(Boolean.valueOf(object));
-                }
+            @Override
+            public String getObject() {
+                return String.valueOf(address.isPrimary());
+            }
 
-            });
+            @Override
+            public void setObject(final String object) {
+                address.setPrimary(Boolean.parseBoolean(object));
+            }
+        });
         addressPrimaryPanel.setChoices(List.of("true", "false"));
 
         panelList.add(addressCountryPanel);
@@ -658,9 +647,4 @@ public class SCIMConfUserPanel extends SCIMConfTabPanel {
 
         return new SCIMConfAccordionContainer(panelId, panelList);
     }
-
-    @Override
-    public void onEvent(final IEvent<?> event) {
-        super.onEvent(event);
-    }
 }
diff --git a/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfGroupPanel.html b/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfGroupPanel.html
new file mode 100644
index 0000000000..dd38ad806b
--- /dev/null
+++ b/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfGroupPanel.html
@@ -0,0 +1,23 @@
+<!--
+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.
+-->
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org">
+  <wicket:panel>
+    <div wicket:id="externalId"/>
+  </wicket:panel>
+</html>
diff --git a/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel.properties b/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel.properties
index 40063bde60..10d4781a8f 100644
--- a/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel.properties
+++ b/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel.properties
@@ -19,3 +19,4 @@ tab2=User
 tab3=EnterpriseUser
 
 saveButton=Save
+tab4=Group
diff --git a/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_it.properties b/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_it.properties
index 40063bde60..10d4781a8f 100644
--- a/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_it.properties
+++ b/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_it.properties
@@ -19,3 +19,4 @@ tab2=User
 tab3=EnterpriseUser
 
 saveButton=Save
+tab4=Group
diff --git a/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_pt_BR.properties b/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_pt_BR.properties
index 40063bde60..10d4781a8f 100644
--- a/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_pt_BR.properties
+++ b/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_pt_BR.properties
@@ -19,3 +19,4 @@ tab2=User
 tab3=EnterpriseUser
 
 saveButton=Save
+tab4=Group
diff --git a/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_ru.properties b/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_ru.properties
index 40063bde60..10d4781a8f 100644
--- a/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_ru.properties
+++ b/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_ru.properties
@@ -19,3 +19,4 @@ tab2=User
 tab3=EnterpriseUser
 
 saveButton=Save
+tab4=Group
diff --git a/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfUserPanel.html b/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfUserPanel.html
index 5208aca0fa..9f8d97059f 100644
--- a/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfUserPanel.html
+++ b/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfUserPanel.html
@@ -18,6 +18,7 @@ under the License.
 -->
 <html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org">
   <wicket:panel>
+    <div wicket:id="externalId"/>
     <div wicket:id="displayName"/>
     <div wicket:id="locale"/>
     <div wicket:id="nickName"/>
diff --git a/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMComplexConf.java b/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMComplexConf.java
index 84cb3faaca..bbdf0a4770 100644
--- a/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMComplexConf.java
+++ b/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMComplexConf.java
@@ -63,5 +63,4 @@ public class SCIMComplexConf<E extends Enum<?>> implements Serializable {
     public void setPrimary(final boolean primary) {
         this.primary = primary;
     }
-
 }
diff --git a/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMConf.java b/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMConf.java
index 1875ebaf6e..98fe8f2b4b 100644
--- a/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMConf.java
+++ b/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMConf.java
@@ -32,6 +32,8 @@ public class SCIMConf implements Serializable {
 
     private SCIMEnterpriseUserConf enterpriseUserConf;
 
+    private SCIMGroupConf groupConf;
+
     public SCIMGeneralConf getGeneralConf() {
         return generalConf;
     }
@@ -56,4 +58,11 @@ public class SCIMConf implements Serializable {
         this.enterpriseUserConf = enterpriseUserConf;
     }
 
+    public SCIMGroupConf getGroupConf() {
+        return groupConf;
+    }
+
+    public void setGroupConf(final SCIMGroupConf groupConf) {
+        this.groupConf = groupConf;
+    }
 }
diff --git a/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMEnterpriseUserConf.java b/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMEnterpriseUserConf.java
index e3c1441cff..d3c9affe6c 100644
--- a/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMEnterpriseUserConf.java
+++ b/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMEnterpriseUserConf.java
@@ -110,5 +110,4 @@ public class SCIMEnterpriseUserConf implements Serializable {
     public void setManager(final SCIMManagerConf manager) {
         this.manager = manager;
     }
-
 }
diff --git a/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMGeneralConf.java b/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMGeneralConf.java
index 20da7cacb8..e7352b52ba 100644
--- a/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMGeneralConf.java
+++ b/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMGeneralConf.java
@@ -87,5 +87,4 @@ public class SCIMGeneralConf implements Serializable {
     public void setFilterMaxResults(final int filterMaxResults) {
         this.filterMaxResults = filterMaxResults;
     }
-
 }
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMResource.java b/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMGroupConf.java
similarity index 59%
copy from ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMResource.java
copy to ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMGroupConf.java
index d9bdccc17d..7aa56c86c2 100644
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMResource.java
+++ b/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMGroupConf.java
@@ -16,47 +16,36 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.ext.scimv2.api.data;
+package org.apache.syncope.common.lib.scim;
 
-import java.util.List;
-import java.util.Optional;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 
-public abstract class SCIMResource extends SCIMBean {
+public class SCIMGroupConf implements Serializable {
 
-    private static final long serialVersionUID = -8465880682458920021L;
-
-    private final String id;
+    private static final long serialVersionUID = -2700011089067219156L;
 
     private String externalId;
 
-    private final List<String> schemas;
-
-    private final Meta meta;
-
-    public SCIMResource(final String id, final List<String> schemas, final Meta meta) {
-        this.id = id;
-        this.schemas = Optional.ofNullable(schemas).orElse(List.of());
-        this.meta = meta;
-    }
+    @JsonIgnore
+    public Map<String, String> asMap() {
+        Map<String, String> map = new HashMap<>();
 
-    public String getId() {
-        return id;
-    }
+        if (externalId != null) {
+            map.put("externalId", externalId);
+        }
 
-    public List<String> getSchemas() {
-        return schemas;
+        return Collections.unmodifiableMap(map);
     }
 
-    public Meta getMeta() {
-        return meta;
+    public String getExternalId() {
+        return externalId;
     }
 
     public void setExternalId(final String externalId) {
         this.externalId = externalId;
     }
-
-    public String getExternalId() {
-        return externalId;
-    }
-
 }
diff --git a/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMUserConf.java b/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMUserConf.java
index c2c0d15b92..f89fc8b3da 100644
--- a/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMUserConf.java
+++ b/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMUserConf.java
@@ -34,6 +34,8 @@ public class SCIMUserConf implements Serializable {
 
     private static final long serialVersionUID = -2700011089067219156L;
 
+    private String externalId;
+
     private SCIMUserNameConf name;
 
     private String displayName;
@@ -68,6 +70,10 @@ public class SCIMUserConf implements Serializable {
     public Map<String, String> asMap() {
         Map<String, String> map = new HashMap<>();
 
+        if (externalId != null) {
+            map.put("externalId", externalId);
+        }
+
         if (displayName != null) {
             map.put("displayName", displayName);
         }
@@ -96,6 +102,14 @@ public class SCIMUserConf implements Serializable {
         return Collections.unmodifiableMap(map);
     }
 
+    public String getExternalId() {
+        return externalId;
+    }
+
+    public void setExternalId(final String externalId) {
+        this.externalId = externalId;
+    }
+
     public SCIMUserNameConf getName() {
         return name;
     }
@@ -191,5 +205,4 @@ public class SCIMUserConf implements Serializable {
     public List<String> getX509Certificates() {
         return x509Certificates;
     }
-
 }
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 65943b14c1..f42e820f40 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
@@ -22,23 +22,40 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
+import org.apache.commons.jexl3.MapContext;
+import org.apache.commons.lang3.BooleanUtils;
 import org.apache.commons.lang3.StringUtils;
+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;
+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.PasswordPatch;
+import org.apache.syncope.common.lib.request.StatusR;
+import org.apache.syncope.common.lib.request.StringReplacePatchItem;
 import org.apache.syncope.common.lib.request.UserCR;
+import org.apache.syncope.common.lib.request.UserUR;
 import org.apache.syncope.common.lib.scim.SCIMComplexConf;
 import org.apache.syncope.common.lib.scim.SCIMConf;
+import org.apache.syncope.common.lib.scim.SCIMEnterpriseUserConf;
+import org.apache.syncope.common.lib.scim.SCIMManagerConf;
+import org.apache.syncope.common.lib.scim.SCIMUserAddressConf;
 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.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.AnyDAO;
 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.provisioning.api.jexl.JexlUtils;
 import org.apache.syncope.core.spring.security.AuthDataAccessor;
 import org.apache.syncope.ext.scimv2.api.BadRequestException;
 import org.apache.syncope.ext.scimv2.api.data.Group;
@@ -47,6 +64,7 @@ import org.apache.syncope.ext.scimv2.api.data.Meta;
 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.SCIMGroup;
+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;
 import org.apache.syncope.ext.scimv2.api.data.SCIMUserManager;
@@ -54,9 +72,11 @@ 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.ErrorType;
 import org.apache.syncope.ext.scimv2.api.type.Function;
+import org.apache.syncope.ext.scimv2.api.type.PatchOp;
 import org.apache.syncope.ext.scimv2.api.type.Resource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.util.CollectionUtils;
 
 public class SCIMDataBinder {
 
@@ -69,6 +89,30 @@ public class SCIMDataBinder {
 
     protected static final List<String> GROUP_SCHEMAS = List.of(Resource.Group.schema());
 
+    public static String filter2JexlExpression(final String filter) {
+        String jexlExpression = filter.
+                replace(" co ", " =~ ").
+                replace(" sw ", " =^ ").
+                replace(" ew ", " =$ ");
+
+        boolean endsWithPR = jexlExpression.endsWith(" pr");
+        int pr = endsWithPR ? jexlExpression.indexOf(" pr") : jexlExpression.indexOf(" pr ");
+        while (pr != -1) {
+            String before = jexlExpression.substring(0, pr);
+            int start = before.indexOf(' ') == -1 ? 0 : jexlExpression.substring(0, pr).lastIndexOf(' ', pr) + 1;
+            String literal = jexlExpression.substring(start, pr);
+
+            endsWithPR = jexlExpression.endsWith(" pr");
+            jexlExpression = jexlExpression.replace(
+                    literal + " pr" + (endsWithPR ? "" : " "),
+                    "not(empty(" + literal + "))" + (endsWithPR ? "" : " "));
+
+            pr = endsWithPR ? jexlExpression.indexOf(" pr") : jexlExpression.indexOf(" pr ");
+        }
+
+        return jexlExpression;
+    }
+
     protected final SCIMConfManager confManager;
 
     protected final UserLogic userLogic;
@@ -102,9 +146,8 @@ public class SCIMDataBinder {
             if (conf.getType() != null) {
                 value.setType(conf.getType().name());
             }
-            if (conf.isPrimary()) {
-                value.setPrimary(true);
-            }
+
+            value.setPrimary(conf.isPrimary());
 
             if (!value.isEmpty()) {
                 values.add(value);
@@ -152,8 +195,7 @@ public class SCIMDataBinder {
                 new Meta(
                         Resource.User,
                         userTO.getCreationDate(),
-                        userTO.getLastChangeDate() == null
-                        ? userTO.getCreationDate() : userTO.getLastChangeDate(),
+                        Optional.ofNullable(userTO.getLastChangeDate()).orElse(userTO.getCreationDate()),
                         userTO.getETagValue(),
                         location),
                 output(attributes, excludedAttributes, "userName", userTO.getUsername()),
@@ -166,6 +208,13 @@ public class SCIMDataBinder {
         attrs.put("username", new Attr.Builder("username").value(userTO.getUsername()).build());
 
         if (conf.getUserConf() != null) {
+            if (output(attributes, excludedAttributes, "externalId")
+                    && conf.getUserConf().getExternalId() != null
+                    && attrs.containsKey(conf.getUserConf().getExternalId())) {
+
+                user.setExternalId(attrs.get(conf.getUserConf().getExternalId()).getValues().get(0));
+            }
+
             if (output(attributes, excludedAttributes, "name") && conf.getUserConf().getName() != null) {
                 SCIMUserName name = new SCIMUserName();
 
@@ -300,122 +349,139 @@ public class SCIMDataBinder {
                 });
             }
             if (output(attributes, excludedAttributes, "x509Certificates")) {
-                conf.getUserConf().getX509Certificates().stream()
-                        .filter(attrs::containsKey)
-                        .forEachOrdered(certificate -> user.getX509Certificates().add(
-                        new Value(attrs.get(certificate).getValues().get(0))));
+                conf.getUserConf().getX509Certificates().stream().filter(attrs::containsKey).
+                        forEach(cert -> user.getX509Certificates().add(new Value(attrs.get(cert).getValues().get(0))));
             }
+        }
 
-            if (conf.getEnterpriseUserConf() != null) {
-                SCIMEnterpriseInfo enterpriseInfo = new SCIMEnterpriseInfo();
-
-                if (output(attributes, excludedAttributes, "employeeNumber")
-                        && conf.getEnterpriseUserConf().getEmployeeNumber() != null
-                        && attrs.containsKey(conf.getEnterpriseUserConf().getEmployeeNumber())) {
-
-                    enterpriseInfo.setEmployeeNumber(
-                            attrs.get(conf.getEnterpriseUserConf().getEmployeeNumber()).getValues().get(0));
-                }
-                if (output(attributes, excludedAttributes, "costCenter")
-                        && conf.getEnterpriseUserConf().getCostCenter() != null
-                        && attrs.containsKey(conf.getEnterpriseUserConf().getCostCenter())) {
-
-                    enterpriseInfo.setCostCenter(
-                            attrs.get(conf.getEnterpriseUserConf().getCostCenter()).getValues().get(0));
-                }
-                if (output(attributes, excludedAttributes, "organization")
-                        && conf.getEnterpriseUserConf().getOrganization() != null
-                        && attrs.containsKey(conf.getEnterpriseUserConf().getOrganization())) {
-
-                    enterpriseInfo.setOrganization(
-                            attrs.get(conf.getEnterpriseUserConf().getOrganization()).getValues().get(0));
-                }
-                if (output(attributes, excludedAttributes, "division")
-                        && conf.getEnterpriseUserConf().getDivision() != null
-                        && attrs.containsKey(conf.getEnterpriseUserConf().getDivision())) {
+        if (conf.getEnterpriseUserConf() != null) {
+            SCIMEnterpriseInfo enterpriseInfo = new SCIMEnterpriseInfo();
 
-                    enterpriseInfo.setDivision(
-                            attrs.get(conf.getEnterpriseUserConf().getDivision()).getValues().get(0));
-                }
-                if (output(attributes, excludedAttributes, "department")
-                        && conf.getEnterpriseUserConf().getDepartment() != null
-                        && attrs.containsKey(conf.getEnterpriseUserConf().getDepartment())) {
+            if (output(attributes, excludedAttributes, "employeeNumber")
+                    && conf.getEnterpriseUserConf().getEmployeeNumber() != null
+                    && attrs.containsKey(conf.getEnterpriseUserConf().getEmployeeNumber())) {
 
-                    enterpriseInfo.setDepartment(
-                            attrs.get(conf.getEnterpriseUserConf().getDepartment()).getValues().get(0));
-                }
-                if (output(attributes, excludedAttributes, "manager")
-                        && conf.getEnterpriseUserConf().getManager() != null) {
+                enterpriseInfo.setEmployeeNumber(
+                        attrs.get(conf.getEnterpriseUserConf().getEmployeeNumber()).getValues().get(0));
+            }
+            if (output(attributes, excludedAttributes, "costCenter")
+                    && conf.getEnterpriseUserConf().getCostCenter() != null
+                    && attrs.containsKey(conf.getEnterpriseUserConf().getCostCenter())) {
 
-                    SCIMUserManager manager = new SCIMUserManager();
+                enterpriseInfo.setCostCenter(
+                        attrs.get(conf.getEnterpriseUserConf().getCostCenter()).getValues().get(0));
+            }
+            if (output(attributes, excludedAttributes, "organization")
+                    && conf.getEnterpriseUserConf().getOrganization() != null
+                    && attrs.containsKey(conf.getEnterpriseUserConf().getOrganization())) {
 
-                    if (conf.getEnterpriseUserConf().getManager().getKey() != null
-                            && attrs.containsKey(conf.getEnterpriseUserConf().getManager().getKey())) {
+                enterpriseInfo.setOrganization(
+                        attrs.get(conf.getEnterpriseUserConf().getOrganization()).getValues().get(0));
+            }
+            if (output(attributes, excludedAttributes, "division")
+                    && conf.getEnterpriseUserConf().getDivision() != null
+                    && attrs.containsKey(conf.getEnterpriseUserConf().getDivision())) {
 
-                        try {
-                            UserTO userManager = userLogic.read(attrs.get(
-                                    conf.getEnterpriseUserConf().getManager().getKey()).getValues().get(0));
-                            manager.setValue(userManager.getKey());
-                            manager.setRef(
-                                    StringUtils.substringBefore(location, "/Users") + "/Users/" + userManager.getKey());
+                enterpriseInfo.setDivision(
+                        attrs.get(conf.getEnterpriseUserConf().getDivision()).getValues().get(0));
+            }
+            if (output(attributes, excludedAttributes, "department")
+                    && conf.getEnterpriseUserConf().getDepartment() != null
+                    && attrs.containsKey(conf.getEnterpriseUserConf().getDepartment())) {
 
-                            if (conf.getEnterpriseUserConf().getManager().getDisplayName() != null) {
-                                Attr displayName = userManager.getPlainAttr(
+                enterpriseInfo.setDepartment(
+                        attrs.get(conf.getEnterpriseUserConf().getDepartment()).getValues().get(0));
+            }
+            if (output(attributes, excludedAttributes, "manager")
+                    && conf.getEnterpriseUserConf().getManager() != null) {
+
+                SCIMUserManager manager = new SCIMUserManager();
+
+                if (conf.getEnterpriseUserConf().getManager().getKey() != null
+                        && attrs.containsKey(conf.getEnterpriseUserConf().getManager().getKey())) {
+
+                    try {
+                        UserTO userManager = userLogic.read(attrs.get(
+                                conf.getEnterpriseUserConf().getManager().getKey()).getValues().get(0));
+                        manager.setValue(userManager.getKey());
+                        manager.setRef(
+                                StringUtils.substringBefore(location, "/Users") + "/Users/" + userManager.getKey());
+
+                        if (conf.getEnterpriseUserConf().getManager().getDisplayName() != null) {
+                            Attr displayName = userManager.getPlainAttr(
+                                    conf.getEnterpriseUserConf().getManager().getDisplayName()).orElse(null);
+                            if (displayName == null) {
+                                displayName = userManager.getDerAttr(
                                         conf.getEnterpriseUserConf().getManager().getDisplayName()).orElse(null);
-                                if (displayName == null) {
-                                    displayName = userManager.getDerAttr(
-                                            conf.getEnterpriseUserConf().getManager().getDisplayName()).orElse(null);
-                                }
-                                if (displayName == null) {
-                                    displayName = userManager.getVirAttr(
-                                            conf.getEnterpriseUserConf().getManager().getDisplayName()).orElse(null);
-                                }
-                                if (displayName != null) {
-                                    manager.setDisplayName(displayName.getValues().get(0));
-                                }
                             }
-                        } catch (Exception e) {
-                            LOG.error("Could not read user {}", conf.getEnterpriseUserConf().getManager().getKey(), e);
+                            if (displayName == null) {
+                                displayName = userManager.getVirAttr(
+                                        conf.getEnterpriseUserConf().getManager().getDisplayName()).orElse(null);
+                            }
+                            if (displayName != null) {
+                                manager.setDisplayName(displayName.getValues().get(0));
+                            }
                         }
-                    }
-
-                    if (!manager.isEmpty()) {
-                        enterpriseInfo.setManager(manager);
+                    } catch (Exception e) {
+                        LOG.error("Could not read user {}", conf.getEnterpriseUserConf().getManager().getKey(), e);
                     }
                 }
 
-                if (!enterpriseInfo.isEmpty()) {
-                    user.setEnterpriseInfo(enterpriseInfo);
+                if (!manager.isEmpty()) {
+                    enterpriseInfo.setManager(manager);
                 }
             }
 
-            if (output(attributes, excludedAttributes, "groups")) {
-                userTO.getMemberships().forEach(membership -> user.getGroups().add(new Group(
-                        membership.getGroupKey(),
-                        StringUtils.substringBefore(location, "/Users") + "/Groups/" + membership.getGroupKey(),
-                        membership.getGroupName(),
-                        Function.direct)));
-                userTO.getDynMemberships().forEach(membership -> user.getGroups().add(new Group(
-                        membership.getGroupKey(),
-                        StringUtils.substringBefore(location, "/Users") + "/Groups/" + membership.getGroupKey(),
-                        membership.getGroupName(),
-                        Function.indirect)));
+            if (!enterpriseInfo.isEmpty()) {
+                user.setEnterpriseInfo(enterpriseInfo);
             }
+        }
 
-            if (output(attributes, excludedAttributes, "entitlements")) {
-                authDataAccessor.getAuthorities(userTO.getUsername(), null).forEach(authority -> user.getEntitlements().
-                        add(new Value(authority.getAuthority() + " on Realm(s) " + authority.getRealms())));
-            }
+        if (output(attributes, excludedAttributes, "groups")) {
+            userTO.getMemberships().forEach(membership -> user.getGroups().add(new Group(
+                    membership.getGroupKey(),
+                    StringUtils.substringBefore(location, "/Users") + "/Groups/" + membership.getGroupKey(),
+                    membership.getGroupName(),
+                    Function.direct)));
+            userTO.getDynMemberships().forEach(membership -> user.getGroups().add(new Group(
+                    membership.getGroupKey(),
+                    StringUtils.substringBefore(location, "/Users") + "/Groups/" + membership.getGroupKey(),
+                    membership.getGroupName(),
+                    Function.indirect)));
+        }
 
-            if (output(attributes, excludedAttributes, "roles")) {
-                userTO.getRoles().forEach(role -> user.getRoles().add(new Value(role)));
-            }
+        if (output(attributes, excludedAttributes, "entitlements")) {
+            authDataAccessor.getAuthorities(userTO.getUsername(), null).forEach(authority -> user.getEntitlements().
+                    add(new Value(authority.getAuthority() + " on Realm(s) " + authority.getRealms())));
+        }
+
+        if (output(attributes, excludedAttributes, "roles")) {
+            userTO.getRoles().forEach(role -> user.getRoles().add(new Value(role)));
         }
 
         return user;
     }
 
-    protected <E extends Enum<?>> void fill(
+    protected void setAttribute(
+            final UserTO userTO,
+            final String schema,
+            final String value) {
+
+        if (schema == null || value == null) {
+            return;
+        }
+
+        switch (schema) {
+            case "username":
+                userTO.setUsername(value);
+                break;
+
+            default:
+                userTO.getPlainAttrs().add(new Attr.Builder(schema).value(value).build());
+        }
+    }
+
+    protected <E extends Enum<?>> void setAttribute(
             final Set<Attr> attrs,
             final List<SCIMComplexConf<E>> confs,
             final List<SCIMComplexValue> values) {
@@ -426,8 +492,11 @@ public class SCIMDataBinder {
                 new Attr.Builder(conf.getValue()).value(value.getValue()).build())));
     }
 
-    public UserTO toUserTO(final SCIMUser user) {
-        if (!USER_SCHEMAS.equals(user.getSchemas()) && !ENTERPRISE_USER_SCHEMAS.equals(user.getSchemas())) {
+    public UserTO toUserTO(final SCIMUser user, final boolean checkSchemas) {
+        if (checkSchemas
+                && !USER_SCHEMAS.equals(user.getSchemas())
+                && !ENTERPRISE_USER_SCHEMAS.equals(user.getSchemas())) {
+
             throw new BadRequestException(ErrorType.invalidValue);
         }
 
@@ -440,147 +509,166 @@ public class SCIMDataBinder {
         SCIMConf conf = confManager.get();
 
         if (conf.getUserConf() != null) {
-            if (conf.getUserConf().getName() != null && user.getName() != null) {
-                if (conf.getUserConf().getName().getFamilyName() != null
-                        && user.getName().getFamilyName() != null) {
+            setAttribute(
+                    userTO,
+                    conf.getUserConf().getExternalId(),
+                    user.getExternalId());
 
-                    setAttribute(userTO, conf.getUserConf().getName().getFamilyName(),
-                            user.getName().getFamilyName());
-                }
-                if (conf.getUserConf().getName().getFormatted() != null
-                        && user.getName().getFormatted() != null) {
-
-                    setAttribute(userTO, conf.getUserConf().getName().getFormatted(),
-                            user.getName().getFormatted());
-                }
-                if (conf.getUserConf().getName().getGivenName() != null
-                        && user.getName().getGivenName() != null) {
-
-                    setAttribute(userTO, conf.getUserConf().getName().getGivenName(),
-                            user.getName().getGivenName());
-                }
-                if (conf.getUserConf().getName().getHonorificPrefix() != null
-                        && user.getName().getHonorificPrefix() != null) {
-
-                    setAttribute(userTO, conf.getUserConf().getName().getHonorificPrefix(),
-                            user.getName().getHonorificPrefix());
-                }
-                if (conf.getUserConf().getName().getHonorificSuffix() != null
-                        && user.getName().getHonorificSuffix() != null) {
-
-                    setAttribute(userTO, conf.getUserConf().getName().getHonorificSuffix(),
-                            user.getName().getHonorificSuffix());
-                }
-                if (conf.getUserConf().getName().getMiddleName() != null
-                        && user.getName().getMiddleName() != null) {
-
-                    setAttribute(userTO, conf.getUserConf().getName().getMiddleName(),
-                            user.getName().getMiddleName());
-                }
-            }
-
-            if (conf.getUserConf().getDisplayName() != null && user.getDisplayName() != null) {
-                setAttribute(userTO, conf.getUserConf().getDisplayName(), user.getDisplayName());
-            }
-            if (conf.getUserConf().getNickName() != null && user.getNickName() != null) {
-                setAttribute(userTO, conf.getUserConf().getNickName(), user.getNickName());
-            }
-            if (conf.getUserConf().getProfileUrl() != null && user.getProfileUrl() != null) {
-                setAttribute(userTO, conf.getUserConf().getProfileUrl(), user.getProfileUrl());
-            }
-            if (conf.getUserConf().getTitle() != null && user.getTitle() != null) {
-                setAttribute(userTO, conf.getUserConf().getTitle(),
-                        user.getTitle());
-            }
-            if (conf.getUserConf().getUserType() != null && user.getUserType() != null) {
-                setAttribute(userTO, conf.getUserConf().getUserType(), user.getUserType());
-            }
-            if (conf.getUserConf().getPreferredLanguage() != null && user.getPreferredLanguage() != null) {
-                setAttribute(userTO, conf.getUserConf().getPreferredLanguage(), user.getPreferredLanguage());
-            }
-            if (conf.getUserConf().getLocale() != null && user.getLocale() != null) {
-                setAttribute(userTO, conf.getUserConf().getLocale(), user.getLocale());
-            }
-            if (conf.getUserConf().getTimezone() != null && user.getTimezone() != null) {
-                setAttribute(userTO, conf.getUserConf().getTimezone(), user.getTimezone());
+            if (conf.getUserConf().getName() != null && user.getName() != null) {
+                setAttribute(
+                        userTO,
+                        conf.getUserConf().getName().getFamilyName(),
+                        user.getName().getFamilyName());
+
+                setAttribute(
+                        userTO,
+                        conf.getUserConf().getName().getFormatted(),
+                        user.getName().getFormatted());
+
+                setAttribute(
+                        userTO,
+                        conf.getUserConf().getName().getGivenName(),
+                        user.getName().getGivenName());
+
+                setAttribute(
+                        userTO,
+                        conf.getUserConf().getName().getHonorificPrefix(),
+                        user.getName().getHonorificPrefix());
+
+                setAttribute(
+                        userTO,
+                        conf.getUserConf().getName().getHonorificSuffix(),
+                        user.getName().getHonorificSuffix());
+
+                setAttribute(
+                        userTO,
+                        conf.getUserConf().getName().getMiddleName(),
+                        user.getName().getMiddleName());
             }
 
-            fill(userTO.getPlainAttrs(), conf.getUserConf().getEmails(), user.getEmails());
-            fill(userTO.getPlainAttrs(), conf.getUserConf().getPhoneNumbers(), user.getPhoneNumbers());
-            fill(userTO.getPlainAttrs(), conf.getUserConf().getIms(), user.getIms());
-            fill(userTO.getPlainAttrs(), conf.getUserConf().getPhotos(), user.getPhotos());
+            setAttribute(
+                    userTO,
+                    conf.getUserConf().getDisplayName(),
+                    user.getDisplayName());
+
+            setAttribute(
+                    userTO,
+                    conf.getUserConf().getNickName(),
+                    user.getNickName());
+
+            setAttribute(
+                    userTO,
+                    conf.getUserConf().getProfileUrl(),
+                    user.getProfileUrl());
+
+            setAttribute(
+                    userTO,
+                    conf.getUserConf().getTitle(),
+                    user.getTitle());
+
+            setAttribute(
+                    userTO,
+                    conf.getUserConf().getUserType(),
+                    user.getUserType());
+
+            setAttribute(
+                    userTO,
+                    conf.getUserConf().getPreferredLanguage(),
+                    user.getPreferredLanguage());
+
+            setAttribute(
+                    userTO,
+                    conf.getUserConf().getLocale(),
+                    user.getLocale());
+
+            setAttribute(
+                    userTO,
+                    conf.getUserConf().getTimezone(),
+                    user.getTimezone());
+
+            setAttribute(userTO.getPlainAttrs(), conf.getUserConf().getEmails(), user.getEmails());
+            setAttribute(userTO.getPlainAttrs(), conf.getUserConf().getPhoneNumbers(), user.getPhoneNumbers());
+            setAttribute(userTO.getPlainAttrs(), conf.getUserConf().getIms(), user.getIms());
+            setAttribute(userTO.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 -> {
-                        if (addressConf.getFormatted() != null && address.getFormatted() != null) {
-                            setAttribute(userTO, addressConf.getFormatted(), address.getFormatted());
-                        }
-                        if (addressConf.getStreetAddress() != null && address.getStreetAddress() != null) {
-                            setAttribute(userTO, addressConf.getStreetAddress(), address.getStreetAddress());
-                        }
-                        if (addressConf.getLocality() != null && address.getLocality() != null) {
-                            setAttribute(userTO, addressConf.getLocality(), address.getLocality());
-                        }
-                        if (addressConf.getRegion() != null && address.getRegion() != null) {
-                            setAttribute(userTO, addressConf.getRegion(), address.getRegion());
-                        }
-                        if (addressConf.getPostalCode() != null && address.getPostalCode() != null) {
-                            setAttribute(userTO, addressConf.getPostalCode(), address.getPostalCode());
-                        }
-                        if (addressConf.getCountry() != null && address.getCountry() != null) {
-                            setAttribute(userTO, addressConf.getCountry(), address.getCountry());
-                        }
+                        setAttribute(
+                                userTO,
+                                addressConf.getFormatted(),
+                                address.getFormatted());
+
+                        setAttribute(
+                                userTO,
+                                addressConf.getStreetAddress(),
+                                address.getStreetAddress());
+
+                        setAttribute(
+                                userTO,
+                                addressConf.getLocality(),
+                                address.getLocality());
+
+                        setAttribute(
+                                userTO,
+                                addressConf.getRegion(),
+                                address.getRegion());
+
+                        setAttribute(
+                                userTO,
+                                addressConf.getPostalCode(),
+                                address.getPostalCode());
+
+                        setAttribute(
+                                userTO,
+                                addressConf.getCountry(),
+                                address.getCountry());
                     }));
 
             for (int i = 0; i < user.getX509Certificates().size(); i++) {
                 Value certificate = user.getX509Certificates().get(i);
                 if (conf.getUserConf().getX509Certificates().size() > i) {
-                    setAttribute(userTO, conf.getUserConf().getX509Certificates().get(i), certificate.getValue());
+                    setAttribute(
+                            userTO,
+                            conf.getUserConf().getX509Certificates().get(i),
+                            certificate.getValue());
                 }
             }
         }
 
         if (conf.getEnterpriseUserConf() != null) {
-            if (conf.getEnterpriseUserConf().getEmployeeNumber() != null
-                    && user.getEnterpriseInfo().getEmployeeNumber() != null) {
-
-                setAttribute(userTO, conf.getEnterpriseUserConf().getEmployeeNumber(),
-                        user.getEnterpriseInfo().getEmployeeNumber());
-            }
-            if (conf.getEnterpriseUserConf().getCostCenter() != null
-                    && user.getEnterpriseInfo().getCostCenter() != null) {
-
-                setAttribute(userTO, conf.getEnterpriseUserConf().getCostCenter(),
-                        user.getEnterpriseInfo().getCostCenter());
-            }
-            if (conf.getEnterpriseUserConf().getOrganization() != null
-                    && user.getEnterpriseInfo().getOrganization() != null) {
-
-                setAttribute(userTO, conf.getEnterpriseUserConf().getOrganization(),
-                        user.getEnterpriseInfo().getOrganization());
-            }
-            if (conf.getEnterpriseUserConf().getDivision() != null
-                    && user.getEnterpriseInfo().getDivision() != null) {
-
-                setAttribute(userTO, conf.getEnterpriseUserConf().getDivision(),
-                        user.getEnterpriseInfo().getDivision());
-            }
-            if (conf.getEnterpriseUserConf().getDepartment() != null
-                    && user.getEnterpriseInfo().getDepartment() != null) {
-
-                setAttribute(userTO, conf.getEnterpriseUserConf().getDepartment(),
-                        user.getEnterpriseInfo().getDepartment());
-            }
-            if (conf.getEnterpriseUserConf().getManager() != null
-                    && conf.getEnterpriseUserConf().getManager().getKey() != null
-                    && user.getEnterpriseInfo().getManager() != null
-                    && user.getEnterpriseInfo().getManager().getValue() != null) {
-
-                setAttribute(userTO, conf.getEnterpriseUserConf().getManager().getKey(),
-                        user.getEnterpriseInfo().getManager().getValue());
-            }
+            setAttribute(
+                    userTO,
+                    conf.getEnterpriseUserConf().getEmployeeNumber(),
+                    user.getEnterpriseInfo().getEmployeeNumber());
+
+            setAttribute(
+                    userTO,
+                    conf.getEnterpriseUserConf().getCostCenter(),
+                    user.getEnterpriseInfo().getCostCenter());
+
+            setAttribute(
+                    userTO,
+                    conf.getEnterpriseUserConf().getOrganization(),
+                    user.getEnterpriseInfo().getOrganization());
+
+            setAttribute(
+                    userTO,
+                    conf.getEnterpriseUserConf().getDivision(),
+                    user.getEnterpriseInfo().getDivision());
+
+            setAttribute(
+                    userTO,
+                    conf.getEnterpriseUserConf().getDepartment(),
+                    user.getEnterpriseInfo().getDepartment());
+
+            setAttribute(
+                    userTO,
+                    Optional.ofNullable(conf.getEnterpriseUserConf().getManager()).
+                            map(SCIMManagerConf::getKey).orElse(null),
+                    Optional.ofNullable(user.getEnterpriseInfo().getManager()).
+                            map(SCIMUserManager::getValue).orElse(null));
         }
 
         userTO.getMemberships().addAll(user.getGroups().stream().
@@ -595,21 +683,290 @@ public class SCIMDataBinder {
     }
 
     public UserCR toUserCR(final SCIMUser user) {
-        UserTO userTO = toUserTO(user);
+        UserTO userTO = toUserTO(user, true);
         UserCR userCR = new UserCR();
         EntityTOUtils.toAnyCR(userTO, userCR);
         return userCR;
     }
 
-    protected void setAttribute(final UserTO userTO, final String schema, final String value) {
-        switch (schema) {
-            case "username":
-                userTO.setUsername(value);
+    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);
+            if (!CollectionUtils.isEmpty(op.getValue())) {
+                attr.value(op.getValue().get(0).toString());
+            }
+
+            attrs.add(new AttrPatch.Builder(attr.build()).
+                    operation(op.getOp() == PatchOp.remove ? PatchOperation.DELETE : PatchOperation.ADD_REPLACE).
+                    build());
+        });
+    }
+
+    protected <E extends Enum<?>> void setAttribute(
+            final Set<AttrPatch> attrs,
+            final List<SCIMComplexConf<E>> confs,
+            final SCIMPatchOperation op) {
+
+        confs.stream().
+                filter(conf -> BooleanUtils.toBoolean(JexlUtils.evaluate(
+                filter2JexlExpression(op.getPath().getFilter()),
+                new MapContext(Map.of("type", conf.getType().name()))).toString())).findFirst().
+                ifPresent(conf -> {
+                    if (op.getPath().getSub() == null || "display".equals(op.getPath().getSub())) {
+                        setAttribute(attrs, conf.getDisplay(), op);
+                    }
+                    if (op.getPath().getSub() == null || "value".equals(op.getPath().getSub())) {
+                        setAttribute(attrs, conf.getValue(), op);
+                    }
+                });
+    }
+
+    protected <E extends Enum<?>> void setAttribute(
+            final Set<AttrPatch> attrs,
+            final List<SCIMComplexConf<E>> confs,
+            final List<SCIMComplexValue> values,
+            final PatchOp patchOp) {
+
+        values.stream().
+                filter(value -> value.getType() != null).forEach(value -> confs.stream().
+                filter(conf -> value.getType().equals(conf.getType().name())).findFirst().
+                ifPresent(conf -> attrs.add(new AttrPatch.Builder(
+                new Attr.Builder(conf.getValue()).value(value.getValue()).build()).
+                operation(patchOp == PatchOp.remove ? PatchOperation.DELETE : PatchOperation.ADD_REPLACE).
+                build())));
+    }
+
+    protected void setAttribute(
+            final Set<AttrPatch> attrs,
+            final SCIMUserAddressConf conf,
+            final SCIMPatchOperation op) {
+
+        if (op.getPath().getSub() == null || "formatted".equals(op.getPath().getSub())) {
+            setAttribute(attrs, conf.getFormatted(), op);
+        }
+        if (op.getPath().getSub() == null || "streetAddress".equals(op.getPath().getSub())) {
+            setAttribute(attrs, conf.getStreetAddress(), op);
+        }
+        if (op.getPath().getSub() == null || "locality".equals(op.getPath().getSub())) {
+            setAttribute(attrs, conf.getLocality(), op);
+        }
+        if (op.getPath().getSub() == null || "region".equals(op.getPath().getSub())) {
+            setAttribute(attrs, conf.getRegion(), op);
+        }
+        if (op.getPath().getSub() == null || "postalCode".equals(op.getPath().getSub())) {
+            setAttribute(attrs, conf.getPostalCode(), op);
+        }
+        if (op.getPath().getSub() == null || "country".equals(op.getPath().getSub())) {
+            setAttribute(attrs, conf.getCountry(), op);
+        }
+    }
+
+    public Pair<UserUR, StatusR> toUserUpdate(final UserTO before, final SCIMPatchOperation op) {
+        StatusR statusR = null;
+
+        if (op.getPath() == null && op.getOp() != PatchOp.remove
+                && !CollectionUtils.isEmpty(op.getValue()) && op.getValue().get(0) instanceof SCIMUser) {
+
+            SCIMUser after = (SCIMUser) op.getValue().get(0);
+
+            if (after.getActive() != null && before.isSuspended() == after.isActive()) {
+                statusR = new StatusR.Builder().key(before.getKey()).
+                        type(after.isActive() ? StatusRType.REACTIVATE : StatusRType.SUSPEND).
+                        build();
+            }
+
+            UserTO updated = toUserTO(after, false);
+            updated.setKey(before.getKey());
+            return Pair.of(AnyOperations.diff(updated, before, true), statusR);
+        }
+
+        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);
+                break;
+
+            case "userName":
+                if (op.getOp() != PatchOp.remove && !CollectionUtils.isEmpty(op.getValue())) {
+                    userUR.setUsername(new StringReplacePatchItem.Builder().
+                            value(op.getValue().get(0).toString()).build());
+                }
+                break;
+
+            case "password":
+                if (op.getOp() != PatchOp.remove && !CollectionUtils.isEmpty(op.getValue())) {
+                    userUR.setPassword(new PasswordPatch.Builder().
+                            value(op.getValue().get(0).toString()).build());
+                }
+                break;
+
+            case "active":
+                if (!CollectionUtils.isEmpty(op.getValue())) {
+                    statusR = new StatusR.Builder().key(before.getKey()).
+                            type((boolean) op.getValue().get(0) ? StatusRType.REACTIVATE : StatusRType.SUSPEND).
+                            build();
+                }
+                break;
+
+            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);
+                    }
+                }
+                break;
+
+            case "displayName":
+                setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getDisplayName(), op);
+                break;
+
+            case "nickName":
+                setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getNickName(), op);
+                break;
+
+            case "profileUrl":
+                setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getProfileUrl(), op);
+                break;
+
+            case "title":
+                setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getTitle(), op);
+                break;
+
+            case "userType":
+                setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getUserType(), op);
+                break;
+
+            case "preferredLanguage":
+                setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getPreferredLanguage(), op);
+                break;
+
+            case "locale":
+                setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getLocale(), op);
+                break;
+
+            case "timezone":
+                setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getTimezone(), op);
+                break;
+
+            case "emails":
+                if (!CollectionUtils.isEmpty(op.getValue()) && op.getValue().get(0) instanceof SCIMUser) {
+                    setAttribute(
+                            userUR.getPlainAttrs(),
+                            conf.getUserConf().getEmails(),
+                            ((SCIMUser) op.getValue().get(0)).getEmails(),
+                            op.getOp());
+                } else if (op.getPath().getFilter() != null) {
+                    setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getEmails(), op);
+                }
+                break;
+
+            case "phoneNumbers":
+                if (!CollectionUtils.isEmpty(op.getValue()) && op.getValue().get(0) instanceof SCIMUser) {
+                    setAttribute(
+                            userUR.getPlainAttrs(),
+                            conf.getUserConf().getPhoneNumbers(),
+                            ((SCIMUser) op.getValue().get(0)).getPhoneNumbers(),
+                            op.getOp());
+                } else if (op.getPath().getFilter() != null) {
+                    setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getPhoneNumbers(), op);
+                }
+                break;
+
+            case "ims":
+                if (!CollectionUtils.isEmpty(op.getValue()) && op.getValue().get(0) instanceof SCIMUser) {
+                    setAttribute(
+                            userUR.getPlainAttrs(),
+                            conf.getUserConf().getIms(),
+                            ((SCIMUser) op.getValue().get(0)).getIms(),
+                            op.getOp());
+                } else if (op.getPath().getFilter() != null) {
+                    setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getIms(), op);
+                }
+                break;
+
+            case "photos":
+                if (!CollectionUtils.isEmpty(op.getValue()) && op.getValue().get(0) instanceof SCIMUser) {
+                    setAttribute(
+                            userUR.getPlainAttrs(),
+                            conf.getUserConf().getPhotos(),
+                            ((SCIMUser) op.getValue().get(0)).getPhotos(),
+                            op.getOp());
+                } else if (op.getPath().getFilter() != null) {
+                    setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getPhotos(), op);
+                }
+                break;
+
+            case "addresses":
+                if (!CollectionUtils.isEmpty(op.getValue()) && op.getValue().get(0) instanceof SCIMUser) {
+                    SCIMUser after = (SCIMUser) op.getValue().get(0);
+                    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.evaluate(
+                            filter2JexlExpression(op.getPath().getFilter()),
+                            new MapContext(Map.of("type", addressConf.getType().name()))).toString())).findFirst().
+                            ifPresent(addressConf -> setAttribute(userUR.getPlainAttrs(), addressConf, op));
+                }
+                break;
+
+            case "employeeNumber":
+                setAttribute(userUR.getPlainAttrs(), Optional.ofNullable(conf.getEnterpriseUserConf()).
+                        map(SCIMEnterpriseUserConf::getEmployeeNumber).orElse(null), op);
+                break;
+
+            case "costCenter":
+                setAttribute(userUR.getPlainAttrs(), Optional.ofNullable(conf.getEnterpriseUserConf()).
+                        map(SCIMEnterpriseUserConf::getCostCenter).orElse(null), op);
+                break;
+
+            case "organization":
+                setAttribute(userUR.getPlainAttrs(), Optional.ofNullable(conf.getEnterpriseUserConf()).
+                        map(SCIMEnterpriseUserConf::getOrganization).orElse(null), op);
+                break;
+
+            case "division":
+                setAttribute(userUR.getPlainAttrs(), Optional.ofNullable(conf.getEnterpriseUserConf()).
+                        map(SCIMEnterpriseUserConf::getDivision).orElse(null), op);
+                break;
+
+            case "department":
+                setAttribute(userUR.getPlainAttrs(), Optional.ofNullable(conf.getEnterpriseUserConf()).
+                        map(SCIMEnterpriseUserConf::getDepartment).orElse(null), op);
+                break;
+
+            case "manager":
+                setAttribute(userUR.getPlainAttrs(), Optional.ofNullable(conf.getEnterpriseUserConf()).
+                        map(SCIMEnterpriseUserConf::getManager).map(SCIMManagerConf::getKey).orElse(null), op);
                 break;
 
             default:
-                userTO.getPlainAttrs().add(new Attr.Builder(schema).value(value).build());
         }
+
+        return Pair.of(userUR, statusR);
     }
 
     public SCIMGroup toSCIMGroup(
@@ -623,12 +980,26 @@ public class SCIMDataBinder {
                 new Meta(
                         Resource.Group,
                         groupTO.getCreationDate(),
-                        groupTO.getLastChangeDate() == null
-                        ? groupTO.getCreationDate() : groupTO.getLastChangeDate(),
+                        Optional.ofNullable(groupTO.getLastChangeDate()).orElse(groupTO.getCreationDate()),
                         groupTO.getETagValue(),
                         location),
                 output(attributes, excludedAttributes, "displayName", groupTO.getName()));
 
+        SCIMConf conf = confManager.get();
+
+        Map<String, Attr> attrs = new HashMap<>();
+        attrs.putAll(EntityTOUtils.buildAttrMap(groupTO.getPlainAttrs()));
+        attrs.putAll(EntityTOUtils.buildAttrMap(groupTO.getDerAttrs()));
+        attrs.putAll(EntityTOUtils.buildAttrMap(groupTO.getVirAttrs()));
+
+        if (output(attributes, excludedAttributes, "externalId")
+                && conf.getGroupConf() != null
+                && conf.getGroupConf().getExternalId() != null
+                && attrs.containsKey(conf.getGroupConf().getExternalId())) {
+
+            group.setExternalId(attrs.get(conf.getGroupConf().getExternalId()).getValues().get(0));
+        }
+
         MembershipCond membCond = new MembershipCond();
         membCond.setGroup(groupTO.getKey());
         SearchCond searchCond = SearchCond.getLeaf(membCond);
@@ -657,8 +1028,8 @@ public class SCIMDataBinder {
         return group;
     }
 
-    public static GroupTO toGroupTO(final SCIMGroup group) {
-        if (!GROUP_SCHEMAS.equals(group.getSchemas())) {
+    public GroupTO toGroupTO(final SCIMGroup group, final boolean checkSchemas) {
+        if (checkSchemas && !GROUP_SCHEMAS.equals(group.getSchemas())) {
             throw new BadRequestException(ErrorType.invalidValue);
         }
 
@@ -666,14 +1037,47 @@ public class SCIMDataBinder {
         groupTO.setRealm(SyncopeConstants.ROOT_REALM);
         groupTO.setKey(group.getId());
         groupTO.setName(group.getDisplayName());
+
+        SCIMConf conf = confManager.get();
+        if (conf.getGroupConf() != null
+                && conf.getGroupConf().getExternalId() != null && group.getExternalId() != null) {
+
+            groupTO.getPlainAttrs().add(
+                    new Attr.Builder(conf.getGroupConf().getExternalId()).
+                            value(group.getExternalId()).build());
+        }
+
         return groupTO;
     }
 
-    public static GroupCR toGroupCR(final SCIMGroup group) {
-        if (!GROUP_SCHEMAS.equals(group.getSchemas())) {
-            throw new BadRequestException(ErrorType.invalidValue);
+    public GroupCR toGroupCR(final SCIMGroup group) {
+        GroupTO groupTO = toGroupTO(group, true);
+        GroupCR groupCR = new GroupCR();
+        EntityTOUtils.toAnyCR(groupTO, groupCR);
+        return groupCR;
+    }
+
+    public GroupUR toGroupUR(final GroupTO before, final SCIMPatchOperation op) {
+        if (op.getPath() == null) {
+            throw new UnsupportedOperationException("Empty path not supported for Groups");
+        }
+
+        GroupUR groupUR = new GroupUR.Builder(before.getKey()).build();
+
+        if ("displayName".equals(op.getPath().getAttribute())) {
+            StringReplacePatchItem.Builder name = new StringReplacePatchItem.Builder().
+                    operation(op.getOp() == PatchOp.remove ? PatchOperation.DELETE : PatchOperation.ADD_REPLACE);
+            if (!CollectionUtils.isEmpty(op.getValue())) {
+                name.value(op.getValue().get(0).toString());
+            }
+            groupUR.setName(name.build());
+        } else {
+            SCIMConf conf = confManager.get();
+            if (conf.getGroupConf() != null) {
+                setAttribute(groupUR.getPlainAttrs(), conf.getGroupConf().getExternalId(), op);
+            }
         }
 
-        return new GroupCR.Builder(SyncopeConstants.ROOT_REALM, group.getDisplayName()).build();
+        return groupUR;
     }
 }
diff --git a/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/SCIMLogic.java b/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/SCIMLogic.java
index f300f6f92a..8fe9a9c00b 100644
--- a/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/SCIMLogic.java
+++ b/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/SCIMLogic.java
@@ -101,8 +101,9 @@ public class SCIMLogic extends AbstractLogic<EntityTO> {
                                 conf.getGeneralConf().getLastChangeDate(),
                                 conf.getGeneralConf().getETagValue(),
                                 uriBuilder.build().toASCIIString()),
-                        new ConfigurationOption(false),
-                        new BulkConfigurationOption(false,
+                        new ConfigurationOption(true),
+                        new BulkConfigurationOption(
+                                false,
                                 conf.getGeneralConf().getBulkMaxOperations(),
                                 conf.getGeneralConf().getBulkMaxPayloadSize()),
                         new FilterConfigurationOption(true, conf.getGeneralConf().getFilterMaxResults()),
diff --git a/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/scim/SearchCondVisitor.java b/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/scim/SearchCondVisitor.java
index 890e208381..44c3b0a1e1 100644
--- a/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/scim/SearchCondVisitor.java
+++ b/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/scim/SearchCondVisitor.java
@@ -79,49 +79,64 @@ public class SearchCondVisitor extends SCIMFilterBaseVisitor<SearchCond> {
             attrCond.setSchema("lastChangeDate");
         }
 
-        if (resource == Resource.User) {
-            if (conf.getUserConf() != null) {
-                if (conf.getUserConf().getName() != null) {
-                    for (Map.Entry<String, String> entry : conf.getUserConf().getName().asMap().entrySet()) {
-                        if (schemaEquals(Resource.User, "name." + entry.getKey(), schema)) {
+        switch (resource) {
+            case User:
+                if (conf.getUserConf() != null) {
+                    if (conf.getUserConf().getName() != null) {
+                        for (Map.Entry<String, String> entry : conf.getUserConf().getName().asMap().entrySet()) {
+                            if (schemaEquals(Resource.User, "name." + entry.getKey(), schema)) {
+                                attrCond = new AttrCond();
+                                attrCond.setSchema(entry.getValue());
+                            }
+                        }
+                    }
+
+                    for (Map.Entry<String, String> entry : conf.getUserConf().asMap().entrySet()) {
+                        if (schemaEquals(Resource.User, entry.getKey(), schema)) {
                             attrCond = new AttrCond();
                             attrCond.setSchema(entry.getValue());
                         }
                     }
-                }
 
-                for (Map.Entry<String, String> entry : conf.getUserConf().asMap().entrySet()) {
-                    if (schemaEquals(Resource.User, entry.getKey(), schema)) {
-                        attrCond = new AttrCond();
-                        attrCond.setSchema(entry.getValue());
+                    for (SCIMUserAddressConf address : conf.getUserConf().getAddresses()) {
+                        for (Map.Entry<String, String> entry : address.asMap().entrySet()) {
+                            if (schemaEquals(Resource.User, "addresses." + entry.getKey(), schema)) {
+                                attrCond = new AttrCond();
+                                attrCond.setSchema(entry.getValue());
+                            }
+                        }
                     }
                 }
 
-                for (SCIMUserAddressConf address : conf.getUserConf().getAddresses()) {
-                    for (Map.Entry<String, String> entry : address.asMap().entrySet()) {
-                        if (schemaEquals(Resource.User, "addresses." + entry.getKey(), schema)) {
+                if (conf.getEnterpriseUserConf() != null) {
+                    for (Map.Entry<String, String> entry : conf.getEnterpriseUserConf().asMap().entrySet()) {
+                        if (schemaEquals(Resource.EnterpriseUser, entry.getKey(), schema)) {
                             attrCond = new AttrCond();
                             attrCond.setSchema(entry.getValue());
                         }
                     }
-                }
-            }
 
-            if (conf.getEnterpriseUserConf() != null) {
-                for (Map.Entry<String, String> entry : conf.getEnterpriseUserConf().asMap().entrySet()) {
-                    if (schemaEquals(Resource.EnterpriseUser, entry.getKey(), schema)) {
+                    if (conf.getEnterpriseUserConf().getManager() != null
+                            && conf.getEnterpriseUserConf().getManager().getKey() != null) {
+
                         attrCond = new AttrCond();
-                        attrCond.setSchema(entry.getValue());
+                        attrCond.setSchema(conf.getEnterpriseUserConf().getManager().getKey());
                     }
                 }
+                break;
 
-                if (conf.getEnterpriseUserConf().getManager() != null
-                        && conf.getEnterpriseUserConf().getManager().getKey() != null) {
-
-                    attrCond = new AttrCond();
-                    attrCond.setSchema(conf.getEnterpriseUserConf().getManager().getKey());
+            case Group:
+                if (conf.getGroupConf() != null) {
+                    for (Map.Entry<String, String> entry : conf.getGroupConf().asMap().entrySet()) {
+                        if (schemaEquals(Resource.Group, entry.getKey(), schema)) {
+                            attrCond = new AttrCond();
+                            attrCond.setSchema(entry.getValue());
+                        }
+                    }
                 }
-            }
+                break;
+
+            default:
         }
 
         if (attrCond == null) {
diff --git a/ext/scimv2/scim-rest-api/pom.xml b/ext/scimv2/scim-rest-api/pom.xml
index 76577940a5..bcded1bc96 100644
--- a/ext/scimv2/scim-rest-api/pom.xml
+++ b/ext/scimv2/scim-rest-api/pom.xml
@@ -45,13 +45,19 @@ under the License.
 
     <dependency>
       <groupId>com.fasterxml.jackson.core</groupId>
-      <artifactId>jackson-annotations</artifactId>
+      <artifactId>jackson-databind</artifactId>
     </dependency>
-    
+
     <dependency>
       <groupId>org.apache.cxf</groupId>
       <artifactId>cxf-rt-frontend-jaxrs</artifactId>
     </dependency>
+
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
   
   <build>
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/BadRequestException.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/BadRequestException.java
index 9ec456d56f..ca462dd216 100644
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/BadRequestException.java
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/BadRequestException.java
@@ -39,5 +39,4 @@ public class BadRequestException extends RuntimeException {
     public ErrorType getErrorType() {
         return errorType;
     }
-
 }
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/AuthenticationScheme.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/AuthenticationScheme.java
index 10e3f7722e..9048295b2a 100644
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/AuthenticationScheme.java
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/AuthenticationScheme.java
@@ -78,5 +78,4 @@ public class AuthenticationScheme extends SCIMBean {
     public boolean isPrimary() {
         return primary;
     }
-
 }
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/BulkConfigurationOption.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/BulkConfigurationOption.java
index b81b652e9c..a070491271 100644
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/BulkConfigurationOption.java
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/BulkConfigurationOption.java
@@ -47,5 +47,4 @@ public class BulkConfigurationOption extends ConfigurationOption {
     public int getMaxPayloadSize() {
         return maxPayloadSize;
     }
-
 }
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/ConfigurationOption.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/ConfigurationOption.java
index 821cae451d..5a31ae74f4 100644
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/ConfigurationOption.java
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/ConfigurationOption.java
@@ -35,5 +35,4 @@ public class ConfigurationOption extends SCIMBean {
     public boolean isSupported() {
         return supported;
     }
-
 }
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/FilterConfigurationOption.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/FilterConfigurationOption.java
index 3eda87ed57..578b231697 100644
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/FilterConfigurationOption.java
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/FilterConfigurationOption.java
@@ -39,5 +39,4 @@ public class FilterConfigurationOption extends ConfigurationOption {
     public int getMaxResults() {
         return maxResults;
     }
-
 }
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Group.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Group.java
index 5bddad10fa..304947e49e 100644
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Group.java
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Group.java
@@ -48,5 +48,4 @@ public class Group extends Reference {
     public String getType() {
         return Optional.ofNullable(type).map(Enum::name).orElse(null);
     }
-
 }
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/ListResponse.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/ListResponse.java
index 06c69cb6da..7ccb62c12e 100644
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/ListResponse.java
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/ListResponse.java
@@ -69,5 +69,4 @@ public class ListResponse<R extends SCIMResource> extends SCIMBean {
     public int getItemsPerPage() {
         return itemsPerPage;
     }
-
 }
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Member.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Member.java
index f2513625b4..fd4e6e121d 100644
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Member.java
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Member.java
@@ -33,5 +33,4 @@ public class Member extends Reference {
 
         super(value, display, ref);
     }
-
 }
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Reference.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Reference.java
index a00c79331b..c051794f3a 100644
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Reference.java
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Reference.java
@@ -42,5 +42,4 @@ abstract class Reference extends Value {
     public String getRef() {
         return ref;
     }
-
 }
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/ResourceType.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/ResourceType.java
index feb271419a..ba79d1523b 100644
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/ResourceType.java
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/ResourceType.java
@@ -92,5 +92,4 @@ public class ResourceType extends SCIMBean {
     public Meta getMeta() {
         return meta;
     }
-
 }
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMComplexValue.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMComplexValue.java
index 94c4320368..039e909fbb 100644
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMComplexValue.java
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMComplexValue.java
@@ -66,6 +66,6 @@ public class SCIMComplexValue extends SCIMBean {
 
     @JsonIgnore
     public boolean isEmpty() {
-        return value == null && display == null && type == null;
+        return value == null && display == null;
     }
 }
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMError.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMError.java
index f4f29e078a..112da2b461 100644
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMError.java
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMError.java
@@ -73,5 +73,4 @@ public class SCIMError extends SCIMBean {
     public int getStatus() {
         return status;
     }
-
 }
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMGroup.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMGroup.java
index e2af957d4b..c45d6da49d 100644
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMGroup.java
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMGroup.java
@@ -30,8 +30,6 @@ public class SCIMGroup extends SCIMResource {
 
     private static final long serialVersionUID = -2935466041674390279L;
 
-    private String displayName;
-
     private final List<Member> members = new ArrayList<>();
 
     @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
@@ -41,19 +39,10 @@ public class SCIMGroup extends SCIMResource {
             @JsonProperty("displayName") final String displayName) {
 
         super(id, List.of(Resource.Group.schema()), meta);
-        this.displayName = displayName;
-    }
-
-    public void setDisplayName(final String displayName) {
-        this.displayName = displayName;
-    }
-
-    public String getDisplayName() {
-        return displayName;
+        super.setDisplayName(displayName);
     }
 
     public List<Member> getMembers() {
         return members;
     }
-
 }
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Reference.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMPatchOp.java
similarity index 60%
copy from ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Reference.java
copy to ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMPatchOp.java
index a00c79331b..eb713e95ea 100644
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Reference.java
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMPatchOp.java
@@ -19,28 +19,27 @@
 package org.apache.syncope.ext.scimv2.api.data;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+import org.apache.syncope.ext.scimv2.api.type.Resource;
 
-abstract class Reference extends Value {
+public class SCIMPatchOp extends SCIMBean {
 
-    private static final long serialVersionUID = -6190164044699376089L;
+    private static final long serialVersionUID = 3957352317667344898L;
 
-    private final String display;
+    private final List<String> schemas = List.of(Resource.PatchOp.schema());
 
-    @JsonProperty("$ref")
-    private final String ref;
+    @JsonProperty("Operations")
+    private List<SCIMPatchOperation> operations;
 
-    Reference(final String value, final String display, final String ref) {
-        super(value);
-        this.display = display;
-        this.ref = ref;
+    public List<String> getSchemas() {
+        return schemas;
     }
 
-    public String getDisplay() {
-        return display;
+    public List<SCIMPatchOperation> getOperations() {
+        return operations;
     }
 
-    public String getRef() {
-        return ref;
+    public void setOperations(final List<SCIMPatchOperation> operations) {
+        this.operations = operations;
     }
-
 }
diff --git a/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMComplexConf.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMPatchOperation.java
similarity index 53%
copy from ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMComplexConf.java
copy to ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMPatchOperation.java
index 84cb3faaca..1421baff89 100644
--- a/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMComplexConf.java
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMPatchOperation.java
@@ -16,52 +16,45 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.common.lib.scim;
+package org.apache.syncope.ext.scimv2.api.data;
 
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
 import java.io.Serializable;
+import java.util.List;
+import org.apache.syncope.ext.scimv2.api.type.PatchOp;
 
-public class SCIMComplexConf<E extends Enum<?>> implements Serializable {
+@JsonDeserialize(using = SCIMPatchOperationDeserializer.class)
+public class SCIMPatchOperation extends SCIMBean {
 
-    private static final long serialVersionUID = -6205072829179565927L;
+    private static final long serialVersionUID = 1802654766651071823L;
 
-    private String value;
+    private PatchOp op;
 
-    private String display;
+    private SCIMPatchPath path;
 
-    private E type;
+    private List<Serializable> value;
 
-    private boolean primary;
-
-    public String getValue() {
-        return value;
-    }
-
-    public void setValue(final String value) {
-        this.value = value;
+    public PatchOp getOp() {
+        return op;
     }
 
-    public String getDisplay() {
-        return display;
+    public void setOp(final PatchOp op) {
+        this.op = op;
     }
 
-    public void setDisplay(final String display) {
-        this.display = display;
+    public SCIMPatchPath getPath() {
+        return path;
     }
 
-    public E getType() {
-        return type;
+    public void setPath(final SCIMPatchPath path) {
+        this.path = path;
     }
 
-    public void setType(final E type) {
-        this.type = type;
-    }
-
-    public boolean isPrimary() {
-        return primary;
+    public List<Serializable> getValue() {
+        return value;
     }
 
-    public void setPrimary(final boolean primary) {
-        this.primary = primary;
+    public void setValue(final List<Serializable> value) {
+        this.value = value;
     }
-
 }
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMPatchOperationDeserializer.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMPatchOperationDeserializer.java
new file mode 100644
index 0000000000..5101bbf3a4
--- /dev/null
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMPatchOperationDeserializer.java
@@ -0,0 +1,227 @@
+/*
+ * 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.ext.scimv2.api.data;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.List;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.cxf.common.util.StringUtils;
+import org.apache.syncope.ext.scimv2.api.type.PatchOp;
+
+public class SCIMPatchOperationDeserializer extends StdDeserializer<SCIMPatchOperation> {
+
+    private static final long serialVersionUID = -7401353969242788372L;
+
+    private static final Pattern PATH_PATTERN = Pattern.compile(
+            "^(?<schema>[A-Za-z0-9:.]+:)?(?<attribute>\\w+)(?<filter>\\[.*\\])?(?<sub>\\.\\w+)?");
+
+    private static Serializable scalar(final JsonNode v) {
+        if (v.isNull()) {
+            return null;
+        }
+        if (v.isBoolean()) {
+            return v.booleanValue();
+        }
+        if (v.isFloat()) {
+            return v.floatValue();
+        }
+        if (v.isDouble()) {
+            return v.doubleValue();
+        }
+        if (v.isInt()) {
+            return v.intValue();
+        }
+        if (v.isShort()) {
+            return v.shortValue();
+        }
+        if (v.isLong()) {
+            return v.longValue();
+        }
+
+        return v.asText();
+    }
+
+    public SCIMPatchOperationDeserializer() {
+        this(null);
+    }
+
+    public SCIMPatchOperationDeserializer(final Class<?> vc) {
+        super(vc);
+    }
+
+    @Override
+    public SCIMPatchOperation deserialize(final JsonParser jp, final DeserializationContext ctxt)
+            throws IOException, JsonProcessingException {
+
+        JsonNode node = jp.getCodec().readTree(jp);
+
+        SCIMPatchOperation scimPatchOperation = new SCIMPatchOperation();
+
+        if (node.has("op")) {
+            scimPatchOperation.setOp(PatchOp.valueOf(node.get("op").asText().toLowerCase()));
+        }
+
+        if (node.has("path")) {
+            Matcher matcher = PATH_PATTERN.matcher(node.get("path").asText());
+            if (matcher.matches()) {
+                SCIMPatchPath path = new SCIMPatchPath();
+                scimPatchOperation.setPath(path);
+
+                Optional.ofNullable(matcher.group("schema")).
+                        ifPresent(schema -> path.setSchema(schema.substring(0, schema.length() - 1)));
+
+                path.setAttribute(StringUtils.uncapitalize(matcher.group("attribute")));
+
+                Optional.ofNullable(matcher.group("filter")).
+                        ifPresent(condition -> path.setFilter(condition.substring(1, condition.length() - 1)));
+
+                Optional.ofNullable(matcher.group("sub")).
+                        ifPresent(sub -> path.setSub(StringUtils.uncapitalize(sub.substring(1))));
+            }
+        }
+
+        if (node.has("value")) {
+            JsonNode value = node.get("value");
+
+            if (scimPatchOperation.getPath() == null) {
+                scimPatchOperation.setValue(List.of(jp.getCodec().treeToValue(value, SCIMUser.class)));
+            } else {
+                if ("members".equals(scimPatchOperation.getPath().getAttribute())) {
+                    scimPatchOperation.setValue(List.of(
+                            (Serializable[]) jp.getCodec().treeToValue(value, Member[].class)));
+                } else if (value.isObject()) {
+                    SCIMUser user = new SCIMUser(
+                            null,
+                            List.of(),
+                            null,
+                            "userName".equals(scimPatchOperation.getPath().getAttribute()) ? value.asText() : null,
+                            "active".equals(scimPatchOperation.getPath().getAttribute()) ? value.asBoolean() : null);
+                    user.setEnterpriseInfo(new SCIMEnterpriseInfo());
+
+                    switch (scimPatchOperation.getPath().getAttribute()) {
+                        case "externalId":
+                            user.setExternalId(value.asText());
+                            break;
+
+                        case "name":
+                            user.setName(jp.getCodec().treeToValue(value, SCIMUserName.class));
+                            break;
+
+                        case "displayName":
+                            user.setDisplayName(value.asText());
+                            break;
+
+                        case "nickName":
+                            user.setNickName(value.asText());
+                            break;
+
+                        case "profileUrl":
+                            user.setProfileUrl(value.asText());
+                            break;
+
+                        case "title":
+                            user.setTitle(value.asText());
+                            break;
+
+                        case "userType":
+                            user.setUserType(value.asText());
+                            break;
+
+                        case "preferredLanguage":
+                            user.setPreferredLanguage(value.asText());
+                            break;
+
+                        case "locale":
+                            user.setLocale(value.asText());
+                            break;
+
+                        case "timezone":
+                            user.setTimezone(value.asText());
+                            break;
+
+                        case "emails":
+                            user.getEmails().add(jp.getCodec().treeToValue(value, SCIMComplexValue.class));
+                            break;
+
+                        case "phoneNumbers":
+                            user.getPhoneNumbers().add(jp.getCodec().treeToValue(value, SCIMComplexValue.class));
+                            break;
+
+                        case "ims":
+                            user.getIms().add(jp.getCodec().treeToValue(value, SCIMComplexValue.class));
+                            break;
+
+                        case "photos":
+                            user.getPhotos().add(jp.getCodec().treeToValue(value, SCIMComplexValue.class));
+                            break;
+
+                        case "addresses":
+                            user.getAddresses().add(jp.getCodec().treeToValue(value, SCIMUserAddress.class));
+                            break;
+
+                        case "x509Certificates":
+                            user.getX509Certificates().add(jp.getCodec().treeToValue(value, Value.class));
+                            break;
+
+                        case "employeeNumber":
+                            user.getEnterpriseInfo().setEmployeeNumber(value.asText());
+                            break;
+
+                        case "costCenter":
+                            user.getEnterpriseInfo().setCostCenter(value.asText());
+                            break;
+
+                        case "organization":
+                            user.getEnterpriseInfo().setOrganization(value.asText());
+                            break;
+
+                        case "division":
+                            user.getEnterpriseInfo().setDivision(value.asText());
+                            break;
+
+                        case "department":
+                            user.getEnterpriseInfo().setDepartment(value.asText());
+                            break;
+
+                        case "manager":
+                            user.getEnterpriseInfo().
+                                    setManager(jp.getCodec().treeToValue(value, SCIMUserManager.class));
+                            break;
+
+                        default:
+                    }
+
+                    scimPatchOperation.setValue(List.of(user));
+                } else if (!value.isContainerNode()) {
+                    scimPatchOperation.setValue(List.of(scalar(value)));
+                }
+            }
+        }
+
+        return scimPatchOperation;
+    }
+}
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SchemaExtension.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMPatchPath.java
similarity index 56%
copy from ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SchemaExtension.java
copy to ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMPatchPath.java
index fb2fe2e548..f3873a2676 100644
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SchemaExtension.java
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMPatchPath.java
@@ -18,32 +18,49 @@
  */
 package org.apache.syncope.ext.scimv2.api.data;
 
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
 
-public class SchemaExtension extends SCIMBean {
+public class SCIMPatchPath implements Serializable {
 
-    private static final long serialVersionUID = -2891887818183122384L;
+    private static final long serialVersionUID = -1092849408316419356L;
 
-    private final String schema;
+    private String schema;
 
-    private final boolean required;
+    private String attribute;
 
-    @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
-    public SchemaExtension(
-            @JsonProperty("schema") final String schema,
-            @JsonProperty("required") final boolean required) {
+    private String filter;
 
-        this.schema = schema;
-        this.required = required;
-    }
+    private String sub;
 
     public String getSchema() {
         return schema;
     }
 
-    public boolean isRequired() {
-        return required;
+    public void setSchema(final String schema) {
+        this.schema = schema;
+    }
+
+    public String getAttribute() {
+        return attribute;
+    }
+
+    public void setAttribute(final String attribute) {
+        this.attribute = attribute;
     }
 
+    public String getFilter() {
+        return filter;
+    }
+
+    public void setFilter(final String filter) {
+        this.filter = filter;
+    }
+
+    public String getSub() {
+        return sub;
+    }
+
+    public void setSub(final String sub) {
+        this.sub = sub;
+    }
 }
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMResource.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMResource.java
index d9bdccc17d..566416f5dd 100644
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMResource.java
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMResource.java
@@ -29,11 +29,13 @@ public abstract class SCIMResource extends SCIMBean {
 
     private String externalId;
 
+    private String displayName;
+
     private final List<String> schemas;
 
     private final Meta meta;
 
-    public SCIMResource(final String id, final List<String> schemas, final Meta meta) {
+    protected SCIMResource(final String id, final List<String> schemas, final Meta meta) {
         this.id = id;
         this.schemas = Optional.ofNullable(schemas).orElse(List.of());
         this.meta = meta;
@@ -59,4 +61,11 @@ public abstract class SCIMResource extends SCIMBean {
         return externalId;
     }
 
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    public void setDisplayName(final String displayName) {
+        this.displayName = displayName;
+    }
 }
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMSearchRequest.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMSearchRequest.java
index 2322efa4cd..03bd0d7f17 100644
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMSearchRequest.java
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMSearchRequest.java
@@ -92,5 +92,4 @@ public class SCIMSearchRequest extends SCIMBean {
     public int getCount() {
         return Optional.ofNullable(count).orElse(25);
     }
-
 }
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMUser.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMUser.java
index 775b1ad721..08120534e9 100644
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMUser.java
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMUser.java
@@ -19,10 +19,12 @@
 package org.apache.syncope.ext.scimv2.api.data;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonPropertyOrder;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 
 @JsonPropertyOrder({ "schemas", "id", "externalId",
     "userName", "password", "active",
@@ -39,12 +41,10 @@ public class SCIMUser extends SCIMResource {
 
     private String password;
 
-    private final boolean active;
+    private final Boolean active;
 
     private SCIMUserName name;
 
-    private String displayName;
-
     private String nickName;
 
     private String profileUrl;
@@ -86,7 +86,7 @@ public class SCIMUser extends SCIMResource {
             @JsonProperty("schemas") final List<String> schemas,
             @JsonProperty("meta") final Meta meta,
             @JsonProperty("userName") final String userName,
-            @JsonProperty("active") final boolean active) {
+            @JsonProperty("active") final Boolean active) {
 
         super(id, schemas, meta);
         this.userName = userName;
@@ -105,10 +105,15 @@ public class SCIMUser extends SCIMResource {
         return password;
     }
 
-    public boolean isActive() {
+    @JsonIgnore
+    public Boolean getActive() {
         return active;
     }
 
+    public boolean isActive() {
+        return Optional.ofNullable(active).orElse(true);
+    }
+
     public SCIMUserName getName() {
         return name;
     }
@@ -117,14 +122,6 @@ public class SCIMUser extends SCIMResource {
         this.name = name;
     }
 
-    public String getDisplayName() {
-        return displayName;
-    }
-
-    public void setDisplayName(final String displayName) {
-        this.displayName = displayName;
-    }
-
     public String getNickName() {
         return nickName;
     }
@@ -224,5 +221,4 @@ public class SCIMUser extends SCIMResource {
     public void setEnterpriseInfo(final SCIMEnterpriseInfo enterpriseInfo) {
         this.enterpriseInfo = enterpriseInfo;
     }
-
 }
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SchemaExtension.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SchemaExtension.java
index fb2fe2e548..ae74926f9c 100644
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SchemaExtension.java
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SchemaExtension.java
@@ -45,5 +45,4 @@ public class SchemaExtension extends SCIMBean {
     public boolean isRequired() {
         return required;
     }
-
 }
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/ServiceProviderConfig.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/ServiceProviderConfig.java
index a65775d87a..9e03c96c28 100644
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/ServiceProviderConfig.java
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/ServiceProviderConfig.java
@@ -103,5 +103,4 @@ public class ServiceProviderConfig extends SCIMBean {
     public List<AuthenticationScheme> getAuthenticationSchemes() {
         return authenticationSchemes;
     }
-
 }
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Value.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Value.java
index 543f0003b2..e68a8108fd 100644
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Value.java
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Value.java
@@ -35,5 +35,4 @@ public class Value extends SCIMBean {
     public String getValue() {
         return value;
     }
-
 }
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/service/GroupService.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/service/GroupService.java
deleted file mode 100644
index 8010218a75..0000000000
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/service/GroupService.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * 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.ext.scimv2.api.service;
-
-import javax.validation.constraints.NotNull;
-import javax.ws.rs.Consumes;
-import javax.ws.rs.DELETE;
-import javax.ws.rs.POST;
-import javax.ws.rs.PUT;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.Response;
-import org.apache.cxf.jaxrs.ext.PATCH;
-import org.apache.syncope.ext.scimv2.api.SCIMConstants;
-import org.apache.syncope.ext.scimv2.api.data.SCIMGroup;
-
-@Path("v2/Groups")
-public interface GroupService extends ReadService<SCIMGroup> {
-
-    @POST
-    @Produces({ SCIMConstants.APPLICATION_SCIM_JSON })
-    @Consumes({ SCIMConstants.APPLICATION_SCIM_JSON })
-    Response create(SCIMGroup group);
-
-    @PATCH
-    @Path("{id}")
-    @Produces({ SCIMConstants.APPLICATION_SCIM_JSON })
-    @Consumes({ SCIMConstants.APPLICATION_SCIM_JSON })
-    Response update(@NotNull @PathParam("id") String id);
-
-    @PUT
-    @Path("{id}")
-    @Produces({ SCIMConstants.APPLICATION_SCIM_JSON })
-    @Consumes({ SCIMConstants.APPLICATION_SCIM_JSON })
-    Response replace(@NotNull @PathParam("id") String id, SCIMGroup group);
-
-    @DELETE
-    @Path("{id}")
-    @Produces({ SCIMConstants.APPLICATION_SCIM_JSON })
-    Response delete(@NotNull @PathParam("id") String id);
-}
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Value.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/service/SCIMGroupService.java
similarity index 61%
copy from ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Value.java
copy to ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/service/SCIMGroupService.java
index 543f0003b2..9bb259de22 100644
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Value.java
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/service/SCIMGroupService.java
@@ -16,24 +16,11 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.ext.scimv2.api.data;
+package org.apache.syncope.ext.scimv2.api.service;
 
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-public class Value extends SCIMBean {
-
-    private static final long serialVersionUID = 5337055958765320091L;
-
-    private final String value;
-
-    @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
-    public Value(@JsonProperty("value") final String value) {
-        this.value = value;
-    }
-
-    public String getValue() {
-        return value;
-    }
+import javax.ws.rs.Path;
+import org.apache.syncope.ext.scimv2.api.data.SCIMGroup;
 
+@Path("v2/Groups")
+public interface SCIMGroupService extends SCIMResourceService<SCIMGroup> {
 }
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/service/ReadService.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/service/SCIMResourceService.java
similarity index 70%
rename from ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/service/ReadService.java
rename to ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/service/SCIMResourceService.java
index 4762dd3b8a..38c663ee12 100644
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/service/ReadService.java
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/service/SCIMResourceService.java
@@ -20,19 +20,24 @@ package org.apache.syncope.ext.scimv2.api.service;
 
 import javax.validation.constraints.NotNull;
 import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
 import javax.ws.rs.GET;
 import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Response;
+import org.apache.cxf.jaxrs.ext.PATCH;
 import org.apache.syncope.ext.scimv2.api.SCIMConstants;
 import org.apache.syncope.ext.scimv2.api.data.ListResponse;
+import org.apache.syncope.ext.scimv2.api.data.SCIMPatchOp;
 import org.apache.syncope.ext.scimv2.api.data.SCIMResource;
 import org.apache.syncope.ext.scimv2.api.data.SCIMSearchRequest;
 import org.apache.syncope.ext.scimv2.api.type.SortOrder;
 
-public interface ReadService<R extends SCIMResource> {
+public interface SCIMResourceService<R extends SCIMResource> {
 
     @GET
     @Path("{id}")
@@ -57,4 +62,26 @@ public interface ReadService<R extends SCIMResource> {
     @Produces({ SCIMConstants.APPLICATION_SCIM_JSON })
     @Consumes({ SCIMConstants.APPLICATION_SCIM_JSON })
     ListResponse<R> search(SCIMSearchRequest request);
+
+    @POST
+    @Produces({ SCIMConstants.APPLICATION_SCIM_JSON })
+    @Consumes({ SCIMConstants.APPLICATION_SCIM_JSON })
+    Response create(R resource);
+
+    @PATCH
+    @Path("{id}")
+    @Produces({ SCIMConstants.APPLICATION_SCIM_JSON })
+    @Consumes({ SCIMConstants.APPLICATION_SCIM_JSON })
+    Response update(@NotNull @PathParam("id") String id, SCIMPatchOp patch);
+
+    @PUT
+    @Path("{id}")
+    @Produces({ SCIMConstants.APPLICATION_SCIM_JSON })
+    @Consumes({ SCIMConstants.APPLICATION_SCIM_JSON })
+    Response replace(@NotNull @PathParam("id") String id, R resource);
+
+    @DELETE
+    @Path("{id}")
+    @Produces({ SCIMConstants.APPLICATION_SCIM_JSON })
+    Response delete(@NotNull @PathParam("id") String id);
 }
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Value.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/service/SCIMUserService.java
similarity index 61%
copy from ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Value.java
copy to ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/service/SCIMUserService.java
index 543f0003b2..d71c6bfcb5 100644
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Value.java
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/service/SCIMUserService.java
@@ -16,24 +16,11 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.ext.scimv2.api.data;
+package org.apache.syncope.ext.scimv2.api.service;
 
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-public class Value extends SCIMBean {
-
-    private static final long serialVersionUID = 5337055958765320091L;
-
-    private final String value;
-
-    @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
-    public Value(@JsonProperty("value") final String value) {
-        this.value = value;
-    }
-
-    public String getValue() {
-        return value;
-    }
+import javax.ws.rs.Path;
+import org.apache.syncope.ext.scimv2.api.data.SCIMUser;
 
+@Path("v2/Users")
+public interface SCIMUserService extends SCIMResourceService<SCIMUser> {
 }
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/service/UserService.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/service/UserService.java
deleted file mode 100644
index 62ddc21be1..0000000000
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/service/UserService.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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.ext.scimv2.api.service;
-
-import javax.validation.constraints.NotNull;
-import javax.ws.rs.Consumes;
-import javax.ws.rs.DELETE;
-import javax.ws.rs.POST;
-import javax.ws.rs.PUT;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.Response;
-import org.apache.cxf.jaxrs.ext.PATCH;
-import org.apache.syncope.ext.scimv2.api.SCIMConstants;
-import org.apache.syncope.ext.scimv2.api.data.SCIMUser;
-
-@Path("v2/Users")
-public interface UserService extends ReadService<SCIMUser> {
-
-    @POST
-    @Produces({ SCIMConstants.APPLICATION_SCIM_JSON })
-    @Consumes({ SCIMConstants.APPLICATION_SCIM_JSON })
-    Response create(SCIMUser user);
-
-    @PATCH
-    @Path("{id}")
-    @Produces({ SCIMConstants.APPLICATION_SCIM_JSON })
-    @Consumes({ SCIMConstants.APPLICATION_SCIM_JSON })
-    Response update(@NotNull @PathParam("id") String id);
-
-    @PUT
-    @Path("{id}")
-    @Produces({ SCIMConstants.APPLICATION_SCIM_JSON })
-    @Consumes({ SCIMConstants.APPLICATION_SCIM_JSON })
-    Response replace(@NotNull @PathParam("id") String id, SCIMUser user);
-
-    @DELETE
-    @Path("{id}")
-    @Produces({ SCIMConstants.APPLICATION_SCIM_JSON })
-    Response delete(@NotNull @PathParam("id") String id);
-
-}
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Value.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/type/PatchOp.java
similarity index 61%
copy from ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Value.java
copy to ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/type/PatchOp.java
index 543f0003b2..d28c496475 100644
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Value.java
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/type/PatchOp.java
@@ -16,24 +16,11 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.ext.scimv2.api.data;
+package org.apache.syncope.ext.scimv2.api.type;
 
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-public class Value extends SCIMBean {
-
-    private static final long serialVersionUID = 5337055958765320091L;
-
-    private final String value;
-
-    @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
-    public Value(@JsonProperty("value") final String value) {
-        this.value = value;
-    }
-
-    public String getValue() {
-        return value;
-    }
+public enum PatchOp {
+    add,
+    replace,
+    remove;
 
 }
diff --git a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/type/Resource.java b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/type/Resource.java
index 7800b68d02..b26382383f 100644
--- a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/type/Resource.java
+++ b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/type/Resource.java
@@ -28,6 +28,7 @@ public enum Resource {
     Group("urn:ietf:params:scim:schemas:core:2.0:Group"),
     SearchRequest("urn:ietf:params:scim:api:messages:2.0:SearchRequest"),
     ListResponse("urn:ietf:params:scim:api:messages:2.0:ListResponse"),
+    PatchOp("urn:ietf:params:scim:api:messages:2.0:PatchOp"),
     Error("urn:ietf:params:scim:api:messages:2.0:Error");
 
     private final String schema;
diff --git a/ext/scimv2/scim-rest-api/src/test/java/org/apache/syncope/ext/scimv2/api/data/SCIMPatchOperationDeserializerTest.java b/ext/scimv2/scim-rest-api/src/test/java/org/apache/syncope/ext/scimv2/api/data/SCIMPatchOperationDeserializerTest.java
new file mode 100644
index 0000000000..668c9e5bcf
--- /dev/null
+++ b/ext/scimv2/scim-rest-api/src/test/java/org/apache/syncope/ext/scimv2/api/data/SCIMPatchOperationDeserializerTest.java
@@ -0,0 +1,349 @@
+/*
+ * 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.ext.scimv2.api.data;
+
+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 com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import java.util.List;
+import org.apache.syncope.ext.scimv2.api.type.PatchOp;
+import org.apache.syncope.ext.scimv2.api.type.Resource;
+import org.junit.jupiter.api.Test;
+
+public class SCIMPatchOperationDeserializerTest {
+
+    private static final JsonMapper MAPPER = JsonMapper.builder().
+            findAndAddModules().disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).build();
+
+    @Test
+    public void addMember() throws JsonProcessingException {
+        String input =
+                "{ "
+                + "\"schemas\":[\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"],"
+                + "     \"Operations\":["
+                + "       {"
+                + "        \"op\":\"add\","
+                + "        \"path\":\"members\","
+                + "        \"value\":["
+                + "         {"
+                + "           \"display\": \"Babs Jensen\","
+                + "           \"$ref\": \"https://example.com/v2/Users/2819c223...413861904646\","
+                + "           \"value\": \"2819c223-7f76-453a-919d-413861904646\""
+                + "         }"
+                + "        ]"
+                + "       }"
+                + "     ]"
+                + "   }";
+
+        SCIMPatchOp scimPatchOp = MAPPER.readValue(input, SCIMPatchOp.class);
+        assertNotNull(scimPatchOp);
+        assertEquals(List.of(Resource.PatchOp.schema()), scimPatchOp.getSchemas());
+        assertEquals(1, scimPatchOp.getOperations().size());
+
+        SCIMPatchOperation op = scimPatchOp.getOperations().get(0);
+        assertNotNull(op);
+        assertEquals(PatchOp.add, op.getOp());
+        assertEquals("members", op.getPath().getAttribute());
+
+        assertEquals(1, op.getValue().size());
+
+        Member member = (Member) op.getValue().get(0);
+        assertEquals("Babs Jensen", member.getDisplay());
+        assertEquals("https://example.com/v2/Users/2819c223...413861904646", member.getRef());
+        assertEquals("2819c223-7f76-453a-919d-413861904646", member.getValue());
+    }
+
+    @Test
+    public void removeMembers() throws JsonProcessingException {
+        String input =
+                "{"
+                + "     \"schemas\":[\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"],"
+                + "     \"Operations\":[{"
+                + "       \"op\":\"remove\","
+                + "       \"path\":\"members[value eq \\\"2819c223-7f76-...413861904646\\\"]\""
+                + "     }]"
+                + "   }";
+
+        SCIMPatchOp scimPatchOp = MAPPER.readValue(input, SCIMPatchOp.class);
+        assertNotNull(scimPatchOp);
+        assertEquals(List.of(Resource.PatchOp.schema()), scimPatchOp.getSchemas());
+        assertEquals(1, scimPatchOp.getOperations().size());
+
+        SCIMPatchOperation op = scimPatchOp.getOperations().get(0);
+        assertNotNull(op);
+        assertEquals(PatchOp.remove, op.getOp());
+        assertEquals("members", op.getPath().getAttribute());
+        assertEquals("value eq \"2819c223-7f76-...413861904646\"", op.getPath().getFilter());
+    }
+
+    @Test
+    public void removeAndAddMembers() throws JsonProcessingException {
+        String input =
+                "{ \"schemas\":[\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"],"
+                + "     \"Operations\": ["
+                + "       {"
+                + "         \"op\":\"remove\","
+                + "         \"path\":"
+                + "           \"members[value eq \\\"2819c223...919d-413861904646\\\"]\""
+                + "       },"
+                + "       {"
+                + "         \"op\":\"add\","
+                + "         \"path\":\"members\","
+                + "         \"value\": ["
+                + "           {"
+                + "            \"display\": \"Babs Jensen\","
+                + "            \"$ref\":\"https://example.com/v2/Users/2819c223...413861904646\","
+                + "            \"value\": \"2819c223-7f76-453a-919d-413861904646\""
+                + "           },"
+                + "           {"
+                + "             \"display\": \"James Smith\","
+                + "             \"$ref\":\"https://example.com/v2/Users/08e1d05d...473d93df9210\","
+                + "             \"value\": \"08e1d05d...473d93df9210\""
+                + "           }"
+                + "         ]"
+                + "       }"
+                + "     ]"
+                + "   }";
+
+        SCIMPatchOp scimPatchOp = MAPPER.readValue(input, SCIMPatchOp.class);
+        assertNotNull(scimPatchOp);
+        assertEquals(List.of(Resource.PatchOp.schema()), scimPatchOp.getSchemas());
+        assertEquals(2, scimPatchOp.getOperations().size());
+
+        SCIMPatchOperation op = scimPatchOp.getOperations().get(0);
+        assertNotNull(op);
+        assertEquals(PatchOp.remove, op.getOp());
+        assertEquals("members", op.getPath().getAttribute());
+        assertEquals("value eq \"2819c223...919d-413861904646\"", op.getPath().getFilter());
+
+        op = scimPatchOp.getOperations().get(1);
+        assertNotNull(op);
+        assertEquals(PatchOp.add, op.getOp());
+        assertEquals("members", op.getPath().getAttribute());
+
+        assertEquals(2, op.getValue().size());
+
+        Member member = (Member) op.getValue().get(0);
+        assertEquals("Babs Jensen", member.getDisplay());
+        assertEquals("https://example.com/v2/Users/2819c223...413861904646", member.getRef());
+        assertEquals("2819c223-7f76-453a-919d-413861904646", member.getValue());
+
+        member = (Member) op.getValue().get(1);
+        assertEquals("James Smith", member.getDisplay());
+        assertEquals("https://example.com/v2/Users/08e1d05d...473d93df9210", member.getRef());
+        assertEquals("08e1d05d...473d93df9210", member.getValue());
+    }
+
+    @Test
+    public void addAttributes() throws JsonProcessingException {
+        String input =
+                "{"
+                + "  \"schemas\":[\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"],"
+                + "  \"Operations\": ["
+                + "    {"
+                + "      \"op\": \"add\","
+                + "      \"value\": {"
+                + "        \"emails\": ["
+                + "          {"
+                + "            \"value\": \"babs@jensen.org\","
+                + "            \"type\": \"home\""
+                + "          }"
+                + "        ],"
+                + "        \"nickName\": \"Babs\""
+                + "      }"
+                + "    }"
+                + "  ]"
+                + "}";
+
+        SCIMPatchOp scimPatchOp = MAPPER.readValue(input, SCIMPatchOp.class);
+        assertNotNull(scimPatchOp);
+        assertEquals(List.of(Resource.PatchOp.schema()), scimPatchOp.getSchemas());
+        assertEquals(1, scimPatchOp.getOperations().size());
+
+        SCIMPatchOperation op = scimPatchOp.getOperations().get(0);
+        assertNotNull(op);
+        assertEquals(PatchOp.add, op.getOp());
+        assertNull(op.getPath());
+
+        assertEquals(1, op.getValue().size());
+
+        SCIMUser user = (SCIMUser) op.getValue().get(0);
+        assertEquals("Babs", user.getNickName());
+
+        assertEquals(1, user.getEmails().size());
+
+        SCIMComplexValue email = user.getEmails().get(0);
+        assertEquals("home", email.getType());
+        assertEquals("babs@jensen.org", email.getValue());
+    }
+
+    @Test
+    public void replaceMembers() throws JsonProcessingException {
+        String input =
+                " {"
+                + "     \"schemas\":[\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"],"
+                + "     \"Operations\": [{"
+                + "       \"op\":\"replace\","
+                + "       \"path\":\"members\","
+                + "       \"value\":["
+                + "         {"
+                + "           \"display\": \"Babs Jensen\","
+                + "           \"$ref\": \"https://example.com/v2/Users/2819c223...413861904646\","
+                + "           \"value\": \"2819c223...413861904646\""
+                + "         },"
+                + "         {"
+                + "           \"display\": \"James Smith\","
+                + "           \"$ref\":\"https://example.com/v2/Users/08e1d05d...473d93df9210\","
+                + "           \"value\": \"08e1d05d...473d93df9210\""
+                + "         }"
+                + "       ]"
+                + "     }]"
+                + "   }";
+
+        SCIMPatchOp scimPatchOp = MAPPER.readValue(input, SCIMPatchOp.class);
+        assertNotNull(scimPatchOp);
+        assertEquals(List.of(Resource.PatchOp.schema()), scimPatchOp.getSchemas());
+        assertEquals(1, scimPatchOp.getOperations().size());
+
+        SCIMPatchOperation op = scimPatchOp.getOperations().get(0);
+        assertNotNull(op);
+        assertEquals(PatchOp.replace, op.getOp());
+        assertEquals("members", op.getPath().getAttribute());
+
+        assertEquals(2, op.getValue().size());
+
+        Member member = (Member) op.getValue().get(0);
+        assertEquals("Babs Jensen", member.getDisplay());
+        assertEquals("https://example.com/v2/Users/2819c223...413861904646", member.getRef());
+        assertEquals("2819c223...413861904646", member.getValue());
+
+        member = (Member) op.getValue().get(1);
+        assertEquals("James Smith", member.getDisplay());
+        assertEquals("https://example.com/v2/Users/08e1d05d...473d93df9210", member.getRef());
+        assertEquals("08e1d05d...473d93df9210", member.getValue());
+    }
+
+    @Test
+    public void replaceAttribute() throws JsonProcessingException {
+        String input =
+                "{"
+                + "     \"schemas\": [\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"],"
+                + "     \"Operations\": [{"
+                + "       \"op\":\"replace\","
+                + "       \"path\":\"addresses[type eq \\\"work\\\"]\","
+                + "       \"value\":"
+                + "       {"
+                + "         \"type\": \"work\","
+                + "         \"streetAddress\": \"911 Universal City Plaza\","
+                + "         \"locality\": \"Hollywood\","
+                + "         \"region\": \"CA\","
+                + "         \"postalCode\": \"91608\","
+                + "         \"country\": \"US\","
+                + "         \"formatted\":\"911 Universal City Plaza\\nHollywood, CA 91608 US\","
+                + "         \"primary\": true"
+                + "       }"
+                + "     }]"
+                + "   }";
+
+        SCIMPatchOp scimPatchOp = MAPPER.readValue(input, SCIMPatchOp.class);
+        assertNotNull(scimPatchOp);
+        assertEquals(List.of(Resource.PatchOp.schema()), scimPatchOp.getSchemas());
+        assertEquals(1, scimPatchOp.getOperations().size());
+
+        SCIMPatchOperation op = scimPatchOp.getOperations().get(0);
+        assertNotNull(op);
+        assertEquals(PatchOp.replace, op.getOp());
+        assertEquals("addresses", op.getPath().getAttribute());
+        assertEquals("type eq \"work\"", op.getPath().getFilter());
+
+        assertEquals(1, op.getValue().size());
+
+        SCIMUser user = (SCIMUser) op.getValue().get(0);
+
+        assertEquals(1, user.getAddresses().size());
+
+        SCIMUserAddress address = user.getAddresses().get(0);
+        assertEquals("work", address.getType());
+        assertTrue(address.isPrimary());
+    }
+
+    @Test
+    public void replaceAttributeValue() throws JsonProcessingException {
+        String input =
+                "{"
+                + "     \"schemas\": [\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"],"
+                + "     \"Operations\": [{"
+                + "       \"op\":\"replace\","
+                + "       \"path\":\"addresses[type eq \\\"work\\\"].streetAddress\","
+                + "       \"value\":\"1010 Broadway Ave\""
+                + "     }]"
+                + "   }";
+
+        SCIMPatchOp scimPatchOp = MAPPER.readValue(input, SCIMPatchOp.class);
+        assertNotNull(scimPatchOp);
+        assertEquals(List.of(Resource.PatchOp.schema()), scimPatchOp.getSchemas());
+        assertEquals(1, scimPatchOp.getOperations().size());
+
+        SCIMPatchOperation op = scimPatchOp.getOperations().get(0);
+        assertNotNull(op);
+        assertEquals(PatchOp.replace, op.getOp());
+        assertEquals("addresses", op.getPath().getAttribute());
+        assertEquals("type eq \"work\"", op.getPath().getFilter());
+        assertEquals("streetAddress", op.getPath().getSub());
+
+        assertEquals(1, op.getValue().size());
+
+        String value = (String) op.getValue().get(0);
+        assertEquals("1010 Broadway Ave", value);
+    }
+
+    @Test
+    public void addAttributeValue() throws JsonProcessingException {
+        String input =
+                "{"
+                + "  \"schemas\": [\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"],"
+                + "  \"Operations\": [{"
+                + "      \"op\": \"Add\","
+                + "      \"path\": \"externalId\","
+                + "      \"value\": \"da91cb5d-addb-424f-aa41-799fda8658c3\""
+                + "    }"
+                + "  ]"
+                + "}";
+
+        SCIMPatchOp scimPatchOp = MAPPER.readValue(input, SCIMPatchOp.class);
+        assertNotNull(scimPatchOp);
+        assertEquals(List.of(Resource.PatchOp.schema()), scimPatchOp.getSchemas());
+        assertEquals(1, scimPatchOp.getOperations().size());
+
+        SCIMPatchOperation op = scimPatchOp.getOperations().get(0);
+        assertNotNull(op);
+        assertEquals(PatchOp.add, op.getOp());
+        assertEquals("externalId", op.getPath().getAttribute());
+
+        assertEquals(1, op.getValue().size());
+
+        String value = (String) op.getValue().get(0);
+        assertEquals("da91cb5d-addb-424f-aa41-799fda8658c3", value);
+    }
+}
diff --git a/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/SCIMExceptionMapper.java b/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/SCIMExceptionMapper.java
index 1ab41f09b7..8af56d716c 100644
--- a/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/SCIMExceptionMapper.java
+++ b/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/SCIMExceptionMapper.java
@@ -53,19 +53,19 @@ import org.springframework.transaction.TransactionSystemException;
 @Provider
 public class SCIMExceptionMapper implements ExceptionMapper<Exception> {
 
-    private static final Logger LOG = LoggerFactory.getLogger(SCIMExceptionMapper.class);
+    protected static final Logger LOG = LoggerFactory.getLogger(SCIMExceptionMapper.class);
 
-    private static Class<?> ENTITYEXISTS_EXCLASS = null;
+    protected static Class<?> ENTITYEXISTS_EXCLASS = null;
 
-    private static Class<?> PERSISTENCE_EXCLASS = null;
+    protected static Class<?> PERSISTENCE_EXCLASS = null;
 
-    private static Class<?> ROLLBACK_EXCLASS = null;
+    protected static Class<?> ROLLBACK_EXCLASS = null;
 
-    private static Class<?> JPASYSTEM_EXCLASS = null;
+    protected static Class<?> JPASYSTEM_EXCLASS = null;
 
-    private static Class<?> CONNECTOR_EXCLASS = null;
+    protected static Class<?> CONNECTOR_EXCLASS = null;
 
-    private static Class<?> IBATISPERSISTENCE_EXCLASS = null;
+    protected static Class<?> IBATISPERSISTENCE_EXCLASS = null;
 
     static {
         try {
@@ -122,6 +122,14 @@ public class SCIMExceptionMapper implements ExceptionMapper<Exception> {
             if (builder == null && ex instanceof ValidationException) {
                 builder = builder(ClientExceptionType.RESTValidation, ExceptionUtils.getRootCauseMessage(ex));
             }
+            // process requests for features not yet implemented 
+            if (builder == null && ex instanceof UnsupportedOperationException) {
+                builder = Response.status(Response.Status.INTERNAL_SERVER_ERROR).
+                        entity(new SCIMError(
+                                ErrorType.invalidSyntax,
+                                Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
+                                ExceptionUtils.getRootCauseMessage(ex)));
+            }
             // ...or just report as InternalServerError
             if (builder == null) {
                 builder = Response.status(Response.Status.INTERNAL_SERVER_ERROR).
@@ -132,7 +140,7 @@ public class SCIMExceptionMapper implements ExceptionMapper<Exception> {
         return Optional.ofNullable(builder).map(ResponseBuilder::build).orElse(null);
     }
 
-    private static ResponseBuilder processInvalidEntityExceptions(final Exception ex) {
+    protected ResponseBuilder processInvalidEntityExceptions(final Exception ex) {
         InvalidEntityException iee = null;
 
         if (ex instanceof InvalidEntityException) {
@@ -173,7 +181,7 @@ public class SCIMExceptionMapper implements ExceptionMapper<Exception> {
         return null;
     }
 
-    private static ResponseBuilder processBadRequestExceptions(final Exception ex) {
+    protected ResponseBuilder processBadRequestExceptions(final Exception ex) {
         if (ex instanceof WorkflowException) {
             return builder(ClientExceptionType.Workflow, ExceptionUtils.getRootCauseMessage(ex));
         } else if (PERSISTENCE_EXCLASS.isAssignableFrom(ex.getClass())) {
@@ -195,7 +203,7 @@ public class SCIMExceptionMapper implements ExceptionMapper<Exception> {
         return null;
     }
 
-    private static ResponseBuilder builder(final ClientExceptionType hType, final String msg) {
+    protected ResponseBuilder builder(final ClientExceptionType hType, final String msg) {
         ResponseBuilder builder = Response.status(hType.getResponseStatus());
 
         ErrorType scimType = null;
diff --git a/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/SCIMv2RESTCXFContext.java b/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/SCIMv2RESTCXFContext.java
index f50dce7ae3..1a3e8eeb51 100644
--- a/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/SCIMv2RESTCXFContext.java
+++ b/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/SCIMv2RESTCXFContext.java
@@ -36,12 +36,12 @@ import org.apache.syncope.core.logic.UserLogic;
 import org.apache.syncope.core.logic.scim.SCIMConfManager;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
-import org.apache.syncope.ext.scimv2.api.service.GroupService;
+import org.apache.syncope.ext.scimv2.api.service.SCIMGroupService;
 import org.apache.syncope.ext.scimv2.api.service.SCIMService;
-import org.apache.syncope.ext.scimv2.api.service.UserService;
-import org.apache.syncope.ext.scimv2.cxf.service.GroupServiceImpl;
+import org.apache.syncope.ext.scimv2.api.service.SCIMUserService;
+import org.apache.syncope.ext.scimv2.cxf.service.SCIMGroupServiceImpl;
 import org.apache.syncope.ext.scimv2.cxf.service.SCIMServiceImpl;
-import org.apache.syncope.ext.scimv2.cxf.service.UserServiceImpl;
+import org.apache.syncope.ext.scimv2.cxf.service.SCIMUserServiceImpl;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.annotation.Bean;
@@ -119,7 +119,7 @@ public class SCIMv2RESTCXFContext {
 
     @ConditionalOnMissingBean
     @Bean
-    public GroupService scimv2GroupService(
+    public SCIMGroupService scimv2GroupService(
             final UserDAO userDAO,
             final GroupDAO groupDAO,
             final UserLogic userLogic,
@@ -127,12 +127,12 @@ public class SCIMv2RESTCXFContext {
             final SCIMDataBinder binder,
             final SCIMConfManager confManager) {
 
-        return new GroupServiceImpl(userDAO, groupDAO, userLogic, groupLogic, binder, confManager);
+        return new SCIMGroupServiceImpl(userDAO, groupDAO, userLogic, groupLogic, binder, confManager);
     }
 
     @ConditionalOnMissingBean
     @Bean
-    public UserService scimv2UserService(
+    public SCIMUserService scimv2UserService(
             final UserDAO userDAO,
             final GroupDAO groupDAO,
             final UserLogic userLogic,
@@ -140,6 +140,6 @@ public class SCIMv2RESTCXFContext {
             final SCIMDataBinder binder,
             final SCIMConfManager confManager) {
 
-        return new UserServiceImpl(userDAO, groupDAO, userLogic, groupLogic, binder, confManager);
+        return new SCIMUserServiceImpl(userDAO, groupDAO, userLogic, groupLogic, binder, confManager);
     }
 }
diff --git a/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/AbstractService.java b/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/AbstractSCIMService.java
similarity index 97%
rename from ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/AbstractService.java
rename to ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/AbstractSCIMService.java
index 51e30dfeea..dddb12e687 100644
--- a/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/AbstractService.java
+++ b/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/AbstractSCIMService.java
@@ -54,9 +54,9 @@ import org.apache.syncope.ext.scimv2.api.type.SortOrder;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-abstract class AbstractService<R extends SCIMResource> {
+abstract class AbstractSCIMService<R extends SCIMResource> {
 
-    protected static final Logger LOG = LoggerFactory.getLogger(AbstractService.class);
+    protected static final Logger LOG = LoggerFactory.getLogger(AbstractSCIMService.class);
 
     @Context
     protected UriInfo uriInfo;
@@ -76,7 +76,7 @@ abstract class AbstractService<R extends SCIMResource> {
 
     protected final SCIMConfManager confManager;
 
-    protected AbstractService(
+    protected AbstractSCIMService(
             final UserDAO userDAO,
             final GroupDAO groupDAO,
             final UserLogic userLogic,
@@ -200,7 +200,8 @@ abstract class AbstractService<R extends SCIMResource> {
                         request.getAttributes(),
                         request.getExcludedAttributes());
             } else if (anyTO instanceof GroupTO) {
-                resource = binder.toSCIMGroup((GroupTO) anyTO,
+                resource = binder.toSCIMGroup(
+                        (GroupTO) anyTO,
                         uriInfo.getAbsolutePathBuilder().path(anyTO.getKey()).build().toASCIIString(),
                         request.getAttributes(),
                         request.getExcludedAttributes());
@@ -213,5 +214,4 @@ abstract class AbstractService<R extends SCIMResource> {
 
         return response;
     }
-
 }
diff --git a/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/GroupServiceImpl.java b/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMGroupServiceImpl.java
similarity index 65%
rename from ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/GroupServiceImpl.java
rename to ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMGroupServiceImpl.java
index cd528ab999..9f3c356e77 100644
--- a/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/GroupServiceImpl.java
+++ b/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMGroupServiceImpl.java
@@ -20,11 +20,14 @@ package org.apache.syncope.ext.scimv2.cxf.service;
 
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.ResponseBuilder;
+import org.apache.commons.jexl3.MapContext;
 import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.BooleanUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.common.lib.AnyOperations;
 import org.apache.syncope.common.lib.SyncopeConstants;
@@ -43,18 +46,23 @@ import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
 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.provisioning.api.jexl.JexlUtils;
 import org.apache.syncope.ext.scimv2.api.BadRequestException;
 import org.apache.syncope.ext.scimv2.api.data.ListResponse;
+import org.apache.syncope.ext.scimv2.api.data.Member;
 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.SCIMSearchRequest;
-import org.apache.syncope.ext.scimv2.api.service.GroupService;
+import org.apache.syncope.ext.scimv2.api.service.SCIMGroupService;
 import org.apache.syncope.ext.scimv2.api.type.ErrorType;
+import org.apache.syncope.ext.scimv2.api.type.PatchOp;
 import org.apache.syncope.ext.scimv2.api.type.Resource;
 import org.apache.syncope.ext.scimv2.api.type.SortOrder;
+import org.springframework.util.CollectionUtils;
 
-public class GroupServiceImpl extends AbstractService<SCIMGroup> implements GroupService {
+public class SCIMGroupServiceImpl extends AbstractSCIMService<SCIMGroup> implements SCIMGroupService {
 
-    public GroupServiceImpl(
+    public SCIMGroupServiceImpl(
             final UserDAO userDAO,
             final GroupDAO groupDAO,
             final UserLogic userLogic,
@@ -65,23 +73,26 @@ public class GroupServiceImpl extends AbstractService<SCIMGroup> implements Grou
         super(userDAO, groupDAO, userLogic, groupLogic, binder, confManager);
     }
 
+    private void changeMembership(final String user, final String group, final PatchOp patchOp) {
+        UserUR req = new UserUR.Builder(user).
+                membership(new MembershipUR.Builder(group).operation(patchOp == PatchOp.remove
+                        ? PatchOperation.DELETE : PatchOperation.ADD_REPLACE).build()).
+                build();
+        try {
+            userLogic.update(req, false);
+        } catch (Exception e) {
+            LOG.error("While applying {} on membership of {} to {}", patchOp, group, user, e);
+        }
+    }
+
     @Override
     public Response create(final SCIMGroup group) {
         // first create group, no members assigned
-        ProvisioningResult<GroupTO> result = groupLogic.create(SCIMDataBinder.toGroupCR(group), false);
+        ProvisioningResult<GroupTO> result = groupLogic.create(binder.toGroupCR(group), false);
 
         // then assign members
-        group.getMembers().forEach(member -> {
-            UserUR req = new UserUR.Builder(member.getValue()).
-                    membership(new MembershipUR.Builder(result.getEntity().getKey()).
-                            operation(PatchOperation.ADD_REPLACE).build()).
-                    build();
-            try {
-                userLogic.update(req, false);
-            } catch (Exception e) {
-                LOG.error("While setting membership of {} to {}", result.getEntity().getKey(), member.getValue(), e);
-            }
-        });
+        group.getMembers().forEach(member -> changeMembership(
+                member.getValue(), result.getEntity().getKey(), PatchOp.add));
 
         return createResponse(
                 result.getEntity().getKey(),
@@ -93,7 +104,8 @@ public class GroupServiceImpl extends AbstractService<SCIMGroup> implements Grou
     }
 
     @Override
-    public SCIMGroup get(final String id,
+    public SCIMGroup get(
+            final String id,
             final String attributes,
             final String excludedAttributes) {
 
@@ -104,9 +116,60 @@ public class GroupServiceImpl extends AbstractService<SCIMGroup> implements Grou
                 List.of(ArrayUtils.nullToEmpty(StringUtils.split(excludedAttributes, ','))));
     }
 
+    private Set<String> members(final String group) {
+        Set<String> members = new HashSet<>();
+
+        MembershipCond membCond = new MembershipCond();
+        membCond.setGroup(group);
+        SearchCond searchCond = SearchCond.getLeaf(membCond);
+        int count = userLogic.search(searchCond, 1, 1, List.of(), SyncopeConstants.ROOT_REALM, true, false).getLeft();
+        for (int page = 1; page <= (count / AnyDAO.DEFAULT_PAGE_SIZE) + 1; page++) {
+            members.addAll(userLogic.search(
+                    searchCond,
+                    page,
+                    AnyDAO.DEFAULT_PAGE_SIZE,
+                    List.of(),
+                    SyncopeConstants.ROOT_REALM,
+                    true,
+                    false).
+                    getRight().stream().map(UserTO::getKey).collect(Collectors.toSet()));
+        }
+
+        return members;
+    }
+
     @Override
-    public Response update(final String id) {
-        return Response.status(Response.Status.NOT_IMPLEMENTED).build();
+    public Response update(final String id, final SCIMPatchOp patch) {
+        ResponseBuilder builder = checkETag(Resource.Group, id);
+        if (builder != null) {
+            return builder.build();
+        }
+
+        patch.getOperations().forEach(op -> {
+            if (op.getPath() != null && "members".equals(op.getPath().getAttribute())) {
+                if (CollectionUtils.isEmpty(op.getValue())) {
+                    members(id).stream().filter(member -> op.getPath().getFilter() == null
+                            ? true
+                            : BooleanUtils.toBoolean(JexlUtils.evaluate(
+                                    SCIMDataBinder.filter2JexlExpression(op.getPath().getFilter()),
+                                    new MapContext(Map.of("value", member))).toString())).
+                            forEach(member -> changeMembership(member, id, op.getOp()));
+                } else {
+                    op.getValue().stream().map(Member.class::cast).
+                            forEach(member -> changeMembership(member.getValue(), id, op.getOp()));
+                }
+            } else {
+                groupLogic.update(binder.toGroupUR(groupLogic.read(id), op), false);
+            }
+        });
+
+        return updateResponse(
+                id,
+                binder.toSCIMGroup(
+                        groupLogic.read(id),
+                        uriInfo.getAbsolutePathBuilder().path(id).build().toASCIIString(),
+                        List.of(),
+                        List.of()));
     }
 
     @Override
@@ -121,28 +184,11 @@ public class GroupServiceImpl extends AbstractService<SCIMGroup> implements Grou
         }
 
         // save current group members
-        Set<String> beforeMembers = new HashSet<>();
-
-        MembershipCond membCond = new MembershipCond();
-        membCond.setGroup(id);
-        SearchCond searchCond = SearchCond.getLeaf(membCond);
-        int count = userLogic.search(searchCond,
-                1, 1, List.of(), SyncopeConstants.ROOT_REALM, true, false).getLeft();
-        for (int page = 1; page <= (count / AnyDAO.DEFAULT_PAGE_SIZE) + 1; page++) {
-            beforeMembers.addAll(userLogic.search(
-                    searchCond,
-                    page,
-                    AnyDAO.DEFAULT_PAGE_SIZE,
-                    List.of(),
-                    SyncopeConstants.ROOT_REALM,
-                    true,
-                    false).
-                    getRight().stream().map(UserTO::getKey).collect(Collectors.toSet()));
-        }
+        Set<String> beforeMembers = members(id);
 
         // update group, don't change members
         ProvisioningResult<GroupTO> result = groupLogic.update(
-                AnyOperations.diff(SCIMDataBinder.toGroupTO(group), groupLogic.read(id), false), false);
+                AnyOperations.diff(binder.toGroupTO(group, true), groupLogic.read(id), false), false);
 
         // assign new members
         Set<String> afterMembers = new HashSet<>();
@@ -150,30 +196,12 @@ public class GroupServiceImpl extends AbstractService<SCIMGroup> implements Grou
             afterMembers.add(member.getValue());
 
             if (!beforeMembers.contains(member.getValue())) {
-                UserUR req = new UserUR.Builder(member.getValue()).
-                        membership(new MembershipUR.Builder(result.getEntity().getKey()).
-                                operation(PatchOperation.ADD_REPLACE).build()).
-                        build();
-                try {
-                    userLogic.update(req, false);
-                } catch (Exception e) {
-                    LOG.error("While setting membership of {} to {}",
-                            result.getEntity().getKey(), member.getValue(), e);
-                }
+                changeMembership(member.getValue(), result.getEntity().getKey(), PatchOp.add);
             }
         });
         // remove unconfirmed members
-        beforeMembers.stream().filter(member -> !afterMembers.contains(member)).forEach(user -> {
-            UserUR req = new UserUR.Builder(user).
-                    membership(new MembershipUR.Builder(result.getEntity().getKey()).
-                            operation(PatchOperation.DELETE).build()).
-                    build();
-            try {
-                userLogic.update(req, false);
-            } catch (Exception e) {
-                LOG.error("While removing membership of {} from {}", result.getEntity().getKey(), user, e);
-            }
-        });
+        beforeMembers.stream().filter(member -> !afterMembers.contains(member)).forEach(user -> changeMembership(
+                user, result.getEntity().getKey(), PatchOp.remove));
 
         return updateResponse(
                 result.getEntity().getKey(),
diff --git a/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMServiceImpl.java b/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMServiceImpl.java
index 010afe3728..07f16b000a 100644
--- a/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMServiceImpl.java
+++ b/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMServiceImpl.java
@@ -32,7 +32,7 @@ import org.apache.syncope.ext.scimv2.api.data.SCIMResource;
 import org.apache.syncope.ext.scimv2.api.data.ServiceProviderConfig;
 import org.apache.syncope.ext.scimv2.api.service.SCIMService;
 
-public class SCIMServiceImpl extends AbstractService<SCIMResource> implements SCIMService {
+public class SCIMServiceImpl extends AbstractSCIMService<SCIMResource> implements SCIMService {
 
     protected final SCIMLogic scimLogic;
 
diff --git a/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/UserServiceImpl.java b/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMUserServiceImpl.java
similarity index 75%
rename from ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/UserServiceImpl.java
rename to ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMUserServiceImpl.java
index 23fb340a5e..efb10fcdbd 100644
--- a/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/UserServiceImpl.java
+++ b/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMUserServiceImpl.java
@@ -19,13 +19,18 @@
 package org.apache.syncope.ext.scimv2.cxf.service;
 
 import java.util.List;
+import java.util.Optional;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.ResponseBuilder;
 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.common.lib.AnyOperations;
+import org.apache.syncope.common.lib.request.StatusR;
+import org.apache.syncope.common.lib.request.UserUR;
 import org.apache.syncope.common.lib.to.ProvisioningResult;
 import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.syncope.common.lib.types.StatusRType;
 import org.apache.syncope.core.logic.GroupLogic;
 import org.apache.syncope.core.logic.SCIMDataBinder;
 import org.apache.syncope.core.logic.UserLogic;
@@ -34,16 +39,17 @@ import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
 import org.apache.syncope.ext.scimv2.api.BadRequestException;
 import org.apache.syncope.ext.scimv2.api.data.ListResponse;
+import org.apache.syncope.ext.scimv2.api.data.SCIMPatchOp;
 import org.apache.syncope.ext.scimv2.api.data.SCIMSearchRequest;
 import org.apache.syncope.ext.scimv2.api.data.SCIMUser;
-import org.apache.syncope.ext.scimv2.api.service.UserService;
+import org.apache.syncope.ext.scimv2.api.service.SCIMUserService;
 import org.apache.syncope.ext.scimv2.api.type.ErrorType;
 import org.apache.syncope.ext.scimv2.api.type.Resource;
 import org.apache.syncope.ext.scimv2.api.type.SortOrder;
 
-public class UserServiceImpl extends AbstractService<SCIMUser> implements UserService {
+public class SCIMUserServiceImpl extends AbstractSCIMService<SCIMUser> implements SCIMUserService {
 
-    public UserServiceImpl(
+    public SCIMUserServiceImpl(
             final UserDAO userDAO,
             final GroupDAO groupDAO,
             final UserLogic userLogic,
@@ -79,8 +85,25 @@ public class UserServiceImpl extends AbstractService<SCIMUser> implements UserSe
     }
 
     @Override
-    public Response update(final String id) {
-        return Response.status(Response.Status.NOT_IMPLEMENTED).build();
+    public Response update(final String id, final SCIMPatchOp patch) {
+        ResponseBuilder builder = checkETag(Resource.User, id);
+        if (builder != null) {
+            return builder.build();
+        }
+
+        patch.getOperations().forEach(op -> {
+            Pair<UserUR, StatusR> update = binder.toUserUpdate(userLogic.read(id), op);
+            userLogic.update(update.getLeft(), false);
+            Optional.ofNullable(update.getRight()).ifPresent(statusR -> userLogic.status(statusR, false));
+        });
+
+        return updateResponse(
+                id,
+                binder.toSCIMUser(
+                        userLogic.read(id),
+                        uriInfo.getAbsolutePathBuilder().path(id).build().toASCIIString(),
+                        List.of(),
+                        List.of()));
     }
 
     @Override
@@ -94,8 +117,18 @@ public class UserServiceImpl extends AbstractService<SCIMUser> implements UserSe
             return builder.build();
         }
 
+        UserTO before = userLogic.read(id);
+
         ProvisioningResult<UserTO> result = userLogic.update(
-                AnyOperations.diff(binder.toUserTO(user), userLogic.read(id), false), false);
+                AnyOperations.diff(binder.toUserTO(user, true), before, false), false);
+
+        if (before.isSuspended() == user.isActive()) {
+            StatusR statusR = new StatusR.Builder().key(before.getKey()).
+                    type(user.isActive() ? StatusRType.REACTIVATE : StatusRType.SUSPEND).
+                    build();
+            userLogic.status(statusR, false);
+        }
+
         return updateResponse(
                 result.getEntity().getKey(),
                 binder.toSCIMUser(
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SCIMITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SCIMITCase.java
index 421e046b8f..dec89030d4 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SCIMITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SCIMITCase.java
@@ -20,6 +20,7 @@ package org.apache.syncope.fit.core;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -37,6 +38,7 @@ import java.time.format.DateTimeFormatter;
 import java.time.temporal.ChronoUnit;
 import java.util.List;
 import java.util.UUID;
+import javax.ws.rs.HttpMethod;
 import javax.ws.rs.core.GenericType;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.Response;
@@ -44,6 +46,7 @@ import org.apache.commons.lang3.StringUtils;
 import org.apache.cxf.jaxrs.client.WebClient;
 import org.apache.syncope.common.lib.scim.SCIMComplexConf;
 import org.apache.syncope.common.lib.scim.SCIMConf;
+import org.apache.syncope.common.lib.scim.SCIMGroupConf;
 import org.apache.syncope.common.lib.scim.SCIMUserConf;
 import org.apache.syncope.common.lib.scim.SCIMUserNameConf;
 import org.apache.syncope.common.lib.scim.types.EmailCanonicalType;
@@ -78,8 +81,14 @@ public class SCIMITCase extends AbstractITCase {
 
     static {
         CONF = new SCIMConf();
+
+        CONF.setGroupConf(new SCIMGroupConf());
+
+        CONF.getGroupConf().setExternalId("originalName");
+
         CONF.setUserConf(new SCIMUserConf());
 
+        CONF.getUserConf().setNickName("ctype");
         CONF.getUserConf().setDisplayName("cn");
 
         CONF.getUserConf().setName(new SCIMUserNameConf());
@@ -97,6 +106,29 @@ public class SCIMITCase extends AbstractITCase {
         CONF.getUserConf().getEmails().add(email);
     }
 
+    private static SCIMUser getSampleUser(final String username) {
+        SCIMUser user = new SCIMUser(null, List.of(Resource.User.schema()), null, username, true);
+        user.setPassword("password123");
+
+        SCIMUserName name = new SCIMUserName();
+        name.setGivenName(username);
+        name.setFamilyName("surname");
+        name.setFormatted(username);
+        user.setName(name);
+
+        SCIMComplexValue userId = new SCIMComplexValue();
+        userId.setType(EmailCanonicalType.work.name());
+        userId.setValue(username + "@syncope.apache.org");
+        user.getEmails().add(userId);
+
+        SCIMComplexValue email = new SCIMComplexValue();
+        email.setType(EmailCanonicalType.home.name());
+        email.setValue(username + "@syncope.apache.org");
+        user.getEmails().add(email);
+
+        return user;
+    }
+
     @BeforeAll
     public static void isSCIMAvailable() {
         if (ENABLED == null) {
@@ -132,7 +164,7 @@ public class SCIMITCase extends AbstractITCase {
 
         ServiceProviderConfig serviceProviderConfig = response.readEntity(ServiceProviderConfig.class);
         assertNotNull(serviceProviderConfig);
-        assertFalse(serviceProviderConfig.getPatch().isSupported());
+        assertTrue(serviceProviderConfig.getPatch().isSupported());
         assertFalse(serviceProviderConfig.getBulk().isSupported());
         assertTrue(serviceProviderConfig.getChangePassword().isSupported());
         assertTrue(serviceProviderConfig.getEtag().isSupported());
@@ -330,29 +362,6 @@ public class SCIMITCase extends AbstractITCase {
         assertEquals(newUser.getUsername(), newSCIMUser.getUserName());
     }
 
-    private static SCIMUser getSampleUser(final String username) {
-        SCIMUser user = new SCIMUser(null, List.of(Resource.User.schema()), null, username, true);
-        user.setPassword("password123");
-
-        SCIMUserName name = new SCIMUserName();
-        name.setGivenName(username);
-        name.setFamilyName("surname");
-        name.setFormatted(username);
-        user.setName(name);
-
-        SCIMComplexValue userId = new SCIMComplexValue();
-        userId.setType(EmailCanonicalType.work.name());
-        userId.setValue(username + "@syncope.apache.org");
-        user.getEmails().add(userId);
-
-        SCIMComplexValue email = new SCIMComplexValue();
-        email.setType(EmailCanonicalType.home.name());
-        email.setValue(username + "@syncope.apache.org");
-        user.getEmails().add(email);
-
-        return user;
-    }
-
     @Test
     public void createUser() throws JsonProcessingException {
         SCIM_CONF_SERVICE.set(CONF);
@@ -381,6 +390,143 @@ public class SCIMITCase extends AbstractITCase {
         assertEquals(user.getGroups().get(0).getValue(), userTO.getMemberships().get(0).getGroupKey());
     }
 
+    @Test
+    public void updateUser() {
+        SCIM_CONF_SERVICE.set(CONF);
+
+        SCIMUser user = getSampleUser(UUID.randomUUID().toString());
+
+        Response response = webClient().path("Users").post(user);
+        assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
+
+        user = response.readEntity(SCIMUser.class);
+        assertNotNull(user.getId());
+        assertNull(user.getNickName());
+        assertTrue(user.isActive());
+
+        // 1. update no path, add value and suspend
+        String body =
+                "{"
+                + "  \"schemas\":[\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"],"
+                + "  \"Operations\": ["
+                + "    {"
+                + "      \"op\": \"add\","
+                + "      \"value\": {"
+                + "        \"nickName\": \"" + user.getUserName() + "\","
+                + "        \"active\": false"
+                + "      }"
+                + "    }"
+                + "  ]"
+                + "}";
+        response = webClient().path("Users").path(user.getId()).invoke(HttpMethod.PATCH, body);
+        assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+
+        user = response.readEntity(SCIMUser.class);
+        assertEquals(user.getUserName(), user.getNickName());
+        assertFalse(user.isActive());
+
+        // 2. update with path, reactivate
+        body =
+                "{"
+                + "\"schemas\":[\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"],"
+                + "\"Operations\":[{"
+                + "\"op\":\"Replace\","
+                + "\"path\":\"active\","
+                + "\"value\":true"
+                + "}]"
+                + "}";
+        response = webClient().path("Users").path(user.getId()).invoke(HttpMethod.PATCH, body);
+        assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+
+        user = response.readEntity(SCIMUser.class);
+        assertTrue(user.isActive());
+
+        // 3. update with path, replace simple value
+        assertNotEquals("newSurname", user.getName().getFamilyName());
+        body =
+                "{"
+                + "\"schemas\":[\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"],"
+                + "\"Operations\":["
+                + "{"
+                + "\"op\":\"Replace\","
+                + "\"path\":\"name.familyName\","
+                + "\"value\":\"newSurname\""
+                + "},"
+                + "{"
+                + "\"op\":\"remove\","
+                + "\"path\":\"nickName\""
+                + "}"
+                + "]"
+                + "}";
+        response = webClient().path("Users").path(user.getId()).invoke(HttpMethod.PATCH, body);
+        assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+
+        user = response.readEntity(SCIMUser.class);
+        assertEquals("newSurname", user.getName().getFamilyName());
+        assertNull(user.getNickName());
+
+        // 4. update with path, replace complex value
+        String newMail = UUID.randomUUID().toString() + "@syncope.apache.org";
+        assertNotEquals(
+                newMail,
+                user.getEmails().stream().filter(v -> "work".equals(v.getType())).findFirst().get().getValue());
+        body =
+                "{"
+                + "     \"schemas\": [\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"],"
+                + "     \"Operations\": [{"
+                + "       \"op\":\"replace\","
+                + "       \"path\":\"emails[type eq \\\"work\\\"]\","
+                + "       \"value\":"
+                + "       {"
+                + "         \"type\": \"work\","
+                + "         \"value\": \"" + newMail + "\","
+                + "         \"primary\": true"
+                + "       }"
+                + "     }]"
+                + "   }";
+        response = webClient().path("Users").path(user.getId()).invoke(HttpMethod.PATCH, body);
+        assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+
+        user = response.readEntity(SCIMUser.class);
+        assertEquals(
+                newMail,
+                user.getEmails().stream().filter(v -> "work".equals(v.getType())).findFirst().get().getValue());
+
+        // 5. update with path, filter and sub
+        newMail = "verycomplex" + UUID.randomUUID().toString() + "@syncope.apache.org";
+        body =
+                "{"
+                + "     \"schemas\": [\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"],"
+                + "     \"Operations\": [{"
+                + "       \"op\":\"replace\","
+                + "       \"path\":\"emails[type eq \\\"work\\\"].value\","
+                + "       \"value\":\"" + newMail + "\""
+                + "     }]"
+                + "   }";
+        response = webClient().path("Users").path(user.getId()).invoke(HttpMethod.PATCH, body);
+        assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+
+        user = response.readEntity(SCIMUser.class);
+        assertEquals(
+                newMail,
+                user.getEmails().stream().filter(v -> "work".equals(v.getType())).findFirst().get().getValue());
+
+        // 6. remove with path and filter
+        body =
+                "{"
+                + "     \"schemas\": [\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"],"
+                + "     \"Operations\": [{"
+                + "       \"op\":\"remove\","
+                + "       \"path\":\"emails[type eq \\\"home\\\"]\""
+                + "     }]"
+                + "   }";
+        response = webClient().path("Users").path(user.getId()).invoke(HttpMethod.PATCH, body);
+        assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+
+        user = response.readEntity(SCIMUser.class);
+        assertTrue(user.getEmails().stream().noneMatch(v -> "home".equals(v.getType())));
+    }
+
     @Test
     public void replaceUser() {
         SCIM_CONF_SERVICE.set(CONF);
@@ -456,6 +602,87 @@ public class SCIMITCase extends AbstractITCase {
         assertEquals(ErrorType.uniqueness, error.getScimType());
     }
 
+    @Test
+    public void updateGroup() {
+        SCIM_CONF_SERVICE.set(CONF);
+
+        SCIMGroup group = new SCIMGroup(null, null, UUID.randomUUID().toString());
+        group.getMembers().add(new Member("74cd8ece-715a-44a4-a736-e17b46c4e7e6", null, null));
+        group.getMembers().add(new Member("1417acbe-cbf6-4277-9372-e75e04f97000", null, null));
+        Response response = webClient().path("Groups").post(group);
+        assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
+
+        group = response.readEntity(SCIMGroup.class);
+        assertNotNull(group.getId());
+        assertNull(group.getExternalId());
+        assertEquals(2, group.getMembers().size());
+
+        // 1. update with path, add value
+        String body =
+                "{"
+                + "\"schemas\":[\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"],"
+                + "\"Operations\":[{"
+                + "\"op\":\"Add\","
+                + "\"path\":\"externalId\","
+                + "\"value\":\"" + group.getId() + "\""
+                + "}]"
+                + "}";
+        response = webClient().path("Groups").path(group.getId()).invoke(HttpMethod.PATCH, body);
+        assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+
+        group = response.readEntity(SCIMGroup.class);
+        assertEquals(group.getId(), group.getExternalId());
+
+        // 2. add member, remove member, remove attribute
+        body =
+                "{"
+                + "\"schemas\":[\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"],"
+                + "\"Operations\":["
+                + "{"
+                + "\"op\":\"Add\","
+                + "\"path\":\"members\","
+                + "\"value\":[{"
+                + "\"value\":\"b3cbc78d-32e6-4bd4-92e0-bbe07566a2ee\"}]"
+                + "},"
+                + "{"
+                + "\"op\":\"remove\","
+                + "\"path\":\"members[value eq \\\"74cd8ece-715a-44a4-a736-e17b46c4e7e6\\\"]\""
+                + "},"
+                + "{"
+                + "\"op\":\"remove\","
+                + "\"path\":\"externalId\""
+                + "}"
+                + "]"
+                + "}";
+        response = webClient().path("Groups").path(group.getId()).invoke(HttpMethod.PATCH, body);
+        assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+
+        group = response.readEntity(SCIMGroup.class);
+        assertEquals(2, group.getMembers().size());
+        assertTrue(group.getMembers().stream().
+                anyMatch(m -> "b3cbc78d-32e6-4bd4-92e0-bbe07566a2ee".equals(m.getValue())));
+        assertTrue(group.getMembers().stream().
+                anyMatch(m -> "1417acbe-cbf6-4277-9372-e75e04f97000".equals(m.getValue())));
+        assertNull(group.getExternalId());
+
+        // 3. remove all members
+        body =
+                "{"
+                + "\"schemas\":[\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"],"
+                + "\"Operations\":["
+                + "{"
+                + "\"op\":\"remove\","
+                + "\"path\":\"members\""
+                + "}"
+                + "]"
+                + "}";
+        response = webClient().path("Groups").path(group.getId()).invoke(HttpMethod.PATCH, body);
+        assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+
+        group = response.readEntity(SCIMGroup.class);
+        assertTrue(group.getMembers().isEmpty());
+    }
+
     @Test
     public void replaceGroup() {
         SCIMGroup group = new SCIMGroup(null, null, UUID.randomUUID().toString());
diff --git a/pom.xml b/pom.xml
index b0280b776a..cfd7ceae27 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1735,7 +1735,7 @@ under the License.
         <plugin>
           <groupId>io.fabric8</groupId>
           <artifactId>docker-maven-plugin</artifactId>
-          <version>0.42.0</version>
+          <version>0.42.1</version>
         </plugin>
 
         <plugin>
@@ -1780,7 +1780,7 @@ under the License.
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-enforcer-plugin</artifactId>
-        <version>3.2.1</version>
+        <version>3.3.0</version>
         <executions>
           <execution>
             <id>default-cli</id>