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 38eedf2c4b [SYNCOPE-1893] SCIM Group extension + SCIM AnyObject 
extension (#1145)
38eedf2c4b is described below

commit 38eedf2c4baa1e9bac5fc9fa8ae063bbd4efa361
Author: Samuel Garofalo <72073457+samuelg...@users.noreply.github.com>
AuthorDate: Mon Jul 28 11:25:28 2025 +0200

    [SYNCOPE-1893] SCIM Group extension + SCIM AnyObject extension (#1145)
---
 .../panels/SCIMConfExtensionAnyObjectPanel.java    |  48 ++++
 ...erPanel.java => SCIMConfExtensionAnyPanel.java} |  28 ++-
 .../panels/SCIMConfExtensionGroupPanel.java        |  39 +++
 .../console/panels/SCIMConfExtensionUserPanel.java |  62 +----
 .../client/console/panels/SCIMConfPanel.java       |  32 ++-
 .../panels/mapping/SCIMExtensionMappingPanel.java  |  22 +-
 ...erPanel.html => SCIMConfExtensionAnyPanel.html} |   0
 .../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 +
 .../apache/syncope/common/lib/scim/SCIMConf.java   |  24 +-
 ...sionUserConf.java => SCIMExtensionAnyConf.java} |   2 +-
 .../lib/scim/SCIMExtensionAnyObjectConf.java       |  44 ++++
 .../apache/syncope/core/logic/SCIMDataBinder.java  | 224 ++++++++++++++++-
 .../org/apache/syncope/core/logic/SCIMLogic.java   |  60 ++++-
 .../syncope/core/logic/scim/SCIMConfManager.java   |  81 ++++---
 .../syncope/core/logic/scim/SearchCondVisitor.java |  55 +++--
 .../syncope/core/logic/scim/SCIMFilterTest.java    |   6 +-
 .../apache/syncope/ext/scimv2/api/data/Meta.java   |   7 +-
 .../syncope/ext/scimv2/api/data/SCIMAnyObject.java |  90 +++++++
 .../syncope/ext/scimv2/api/data/SCIMGroup.java     |  17 +-
 .../scimv2/api/service/SCIMAnyObjectService.java   |  26 ++
 .../syncope/ext/scimv2/api/type/Resource.java      |   1 +
 .../ext/scimv2/cxf/SCIMv2RESTCXFContext.java       |  39 ++-
 .../scimv2/cxf/service/AbstractSCIMService.java    |  61 +++--
 ...viceImpl.java => SCIMAnyObjectServiceImpl.java} | 172 +++++++-------
 .../scimv2/cxf/service/SCIMGroupServiceImpl.java   |  18 +-
 .../ext/scimv2/cxf/service/SCIMServiceImpl.java    |   6 +-
 .../scimv2/cxf/service/SCIMUserServiceImpl.java    |  18 +-
 .../org/apache/syncope/fit/core/SCIMITCase.java    | 264 ++++++++++++++++++++-
 31 files changed, 1172 insertions(+), 278 deletions(-)

diff --git 
a/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfExtensionAnyObjectPanel.java
 
b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfExtensionAnyObjectPanel.java
new file mode 100644
index 0000000000..562d8e7ab8
--- /dev/null
+++ 
b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfExtensionAnyObjectPanel.java
@@ -0,0 +1,48 @@
+/*
+ * 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 java.util.Optional;
+import org.apache.syncope.common.lib.scim.SCIMConf;
+import org.apache.syncope.common.lib.scim.SCIMExtensionAnyConf;
+import org.apache.syncope.common.lib.scim.SCIMExtensionAnyObjectConf;
+
+public class SCIMConfExtensionAnyObjectPanel extends SCIMConfExtensionAnyPanel 
{
+
+    private static final long serialVersionUID = -1540432800132655369L;
+
+    public SCIMConfExtensionAnyObjectPanel(final String id, final SCIMConf 
scimConf, final String anyTypeKey) {
+        super(id, scimConf, anyTypeKey);
+    }
+
+    @Override
+    public SCIMExtensionAnyConf getExtensionAnyConf(final SCIMConf scimConf) {
+        Optional<SCIMExtensionAnyObjectConf> scimExtAnyObjectConf =
+                scimConf.getExtensionAnyObjectsConf().stream()
+                        .filter(scimExtensionAnyObjectConf -> 
scimExtensionAnyObjectConf.getType().equals(anyTypeKey))
+                        .findFirst();
+        if (scimExtAnyObjectConf.isPresent()) {
+            return scimExtAnyObjectConf.get();
+        }
+        SCIMExtensionAnyObjectConf scimExtensionAnyObjectConf = new 
SCIMExtensionAnyObjectConf();
+        scimExtensionAnyObjectConf.setType(anyTypeKey);
+        scimConf.getExtensionAnyObjectsConf().add(scimExtensionAnyObjectConf);
+        return scimExtensionAnyObjectConf;
+    }
+}
diff --git 
a/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfExtensionUserPanel.java
 
b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfExtensionAnyPanel.java
similarity index 72%
copy from 
ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfExtensionUserPanel.java
copy to 
ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfExtensionAnyPanel.java
index b8dbb6a377..d328b80cc9 100644
--- 
a/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfExtensionUserPanel.java
+++ 
b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfExtensionAnyPanel.java
@@ -21,23 +21,23 @@ package org.apache.syncope.client.console.panels;
 import 
org.apache.syncope.client.console.panels.mapping.SCIMExtensionMappingPanel;
 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.SCIMExtensionUserConf;
+import org.apache.syncope.common.lib.scim.SCIMExtensionAnyConf;
 import org.apache.wicket.markup.html.form.Form;
 import org.apache.wicket.model.Model;
 import org.apache.wicket.model.PropertyModel;
 import org.apache.wicket.model.util.ListModel;
 
-public class SCIMConfExtensionUserPanel extends SCIMConfTabPanel {
+public abstract class SCIMConfExtensionAnyPanel extends SCIMConfTabPanel {
 
     private static final long serialVersionUID = 2459231778083046011L;
 
-    public SCIMConfExtensionUserPanel(final String id, final SCIMConf 
scimConf) {
+    protected final String anyTypeKey;
+
+    protected SCIMConfExtensionAnyPanel(final String id, final SCIMConf 
scimConf, final String anyTypeKey) {
         super(id);
+        this.anyTypeKey = anyTypeKey;
 
-        if (scimConf.getExtensionUserConf() == null) {
-            scimConf.setExtensionUserConf(new SCIMExtensionUserConf());
-        }
-        SCIMExtensionUserConf scimExtensionUserConf = 
scimConf.getExtensionUserConf();
+        SCIMExtensionAnyConf scimExtensionAnyConf = 
getExtensionAnyConf(scimConf);
 
         AjaxTextFieldPanel namePanel = new AjaxTextFieldPanel("name", "name", 
new PropertyModel<>("name", "name") {
 
@@ -45,12 +45,12 @@ public class SCIMConfExtensionUserPanel extends 
SCIMConfTabPanel {
 
             @Override
             public String getObject() {
-                return scimExtensionUserConf.getName();
+                return scimExtensionAnyConf.getName();
             }
 
             @Override
             public void setObject(final String object) {
-                scimExtensionUserConf.setName(object);
+                scimExtensionAnyConf.setName(object);
             }
         });
         add(namePanel);
@@ -62,20 +62,22 @@ public class SCIMConfExtensionUserPanel extends 
SCIMConfTabPanel {
 
             @Override
             public String getObject() {
-                return scimExtensionUserConf.getDescription();
+                return scimExtensionAnyConf.getDescription();
             }
 
             @Override
             public void setObject(final String object) {
-                scimExtensionUserConf.setDescription(object);
+                scimExtensionAnyConf.setDescription(object);
             }
         });
         add(descriptionPanel);
 
         SCIMExtensionMappingPanel extensionMappingPanel = new 
SCIMExtensionMappingPanel(
-                "mapping", new 
ListModel<>(scimExtensionUserConf.getAttributes()));
-        Form<SCIMExtensionUserConf> form = new Form<>("form", new 
Model<>(scimExtensionUserConf));
+                "mapping", new 
ListModel<>(scimExtensionAnyConf.getAttributes()), anyTypeKey);
+        Form<SCIMExtensionAnyConf> form = new Form<>("form", new 
Model<>(scimExtensionAnyConf));
         form.add(extensionMappingPanel);
         add(form);
     }
+
+    abstract SCIMExtensionAnyConf getExtensionAnyConf(SCIMConf scimConf);
 }
diff --git 
a/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfExtensionGroupPanel.java
 
b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfExtensionGroupPanel.java
new file mode 100644
index 0000000000..3298a2fa8f
--- /dev/null
+++ 
b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfExtensionGroupPanel.java
@@ -0,0 +1,39 @@
+/*
+ * 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.common.lib.scim.SCIMConf;
+import org.apache.syncope.common.lib.scim.SCIMExtensionAnyConf;
+
+public class SCIMConfExtensionGroupPanel extends SCIMConfExtensionAnyPanel {
+
+    private static final long serialVersionUID = -3719006384765921047L;
+
+    public SCIMConfExtensionGroupPanel(final String id, final SCIMConf 
scimConf, final String anyTypeKey) {
+        super(id, scimConf, anyTypeKey);
+    }
+
+    @Override
+    public SCIMExtensionAnyConf getExtensionAnyConf(final SCIMConf scimConf) {
+        if (scimConf.getExtensionGroupConf() == null) {
+            scimConf.setExtensionGroupConf(new SCIMExtensionAnyConf());
+        }
+        return scimConf.getExtensionGroupConf();
+    }
+}
diff --git 
a/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfExtensionUserPanel.java
 
b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfExtensionUserPanel.java
index b8dbb6a377..beb1f0684b 100644
--- 
a/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfExtensionUserPanel.java
+++ 
b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfExtensionUserPanel.java
@@ -18,64 +18,22 @@
  */
 package org.apache.syncope.client.console.panels;
 
-import 
org.apache.syncope.client.console.panels.mapping.SCIMExtensionMappingPanel;
-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.SCIMExtensionUserConf;
-import org.apache.wicket.markup.html.form.Form;
-import org.apache.wicket.model.Model;
-import org.apache.wicket.model.PropertyModel;
-import org.apache.wicket.model.util.ListModel;
+import org.apache.syncope.common.lib.scim.SCIMExtensionAnyConf;
 
-public class SCIMConfExtensionUserPanel extends SCIMConfTabPanel {
+public class SCIMConfExtensionUserPanel extends SCIMConfExtensionAnyPanel {
 
-    private static final long serialVersionUID = 2459231778083046011L;
+    private static final long serialVersionUID = -94504185795020353L;
 
-    public SCIMConfExtensionUserPanel(final String id, final SCIMConf 
scimConf) {
-        super(id);
+    public SCIMConfExtensionUserPanel(final String id, final SCIMConf 
scimConf, final String anyTypeKey) {
+        super(id, scimConf, anyTypeKey);
+    }
 
+    @Override
+    public SCIMExtensionAnyConf getExtensionAnyConf(final SCIMConf scimConf) {
         if (scimConf.getExtensionUserConf() == null) {
-            scimConf.setExtensionUserConf(new SCIMExtensionUserConf());
+            scimConf.setExtensionUserConf(new SCIMExtensionAnyConf());
         }
-        SCIMExtensionUserConf scimExtensionUserConf = 
scimConf.getExtensionUserConf();
-
-        AjaxTextFieldPanel namePanel = new AjaxTextFieldPanel("name", "name", 
new PropertyModel<>("name", "name") {
-
-            private static final long serialVersionUID = 7389942851813193481L;
-
-            @Override
-            public String getObject() {
-                return scimExtensionUserConf.getName();
-            }
-
-            @Override
-            public void setObject(final String object) {
-                scimExtensionUserConf.setName(object);
-            }
-        });
-        add(namePanel);
-
-        AjaxTextFieldPanel descriptionPanel = new AjaxTextFieldPanel(
-                "description", "description", new 
PropertyModel<>("description", "description") {
-
-            private static final long serialVersionUID = -5911179251497048661L;
-
-            @Override
-            public String getObject() {
-                return scimExtensionUserConf.getDescription();
-            }
-
-            @Override
-            public void setObject(final String object) {
-                scimExtensionUserConf.setDescription(object);
-            }
-        });
-        add(descriptionPanel);
-
-        SCIMExtensionMappingPanel extensionMappingPanel = new 
SCIMExtensionMappingPanel(
-                "mapping", new 
ListModel<>(scimExtensionUserConf.getAttributes()));
-        Form<SCIMExtensionUserConf> form = new Form<>("form", new 
Model<>(scimExtensionUserConf));
-        form.add(extensionMappingPanel);
-        add(form);
+        return scimConf.getExtensionUserConf();
     }
 }
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 a36c2f8d15..df886c401a 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
@@ -24,10 +24,12 @@ import java.util.List;
 import org.apache.syncope.client.console.SyncopeConsoleSession;
 import org.apache.syncope.client.console.commons.ITabComponent;
 import org.apache.syncope.client.console.pages.BasePage;
+import org.apache.syncope.client.console.rest.AnyTypeRestClient;
 import org.apache.syncope.client.console.rest.SCIMConfRestClient;
 import org.apache.syncope.client.console.wizards.WizardMgtPanel;
 import org.apache.syncope.client.ui.commons.Constants;
 import org.apache.syncope.common.lib.scim.SCIMConf;
+import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.wicket.PageReference;
 import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.ajax.markup.html.AjaxLink;
@@ -35,6 +37,7 @@ import org.apache.wicket.extensions.markup.html.tabs.ITab;
 import org.apache.wicket.markup.html.WebMarkupContainer;
 import org.apache.wicket.markup.html.panel.Panel;
 import org.apache.wicket.model.Model;
+import org.apache.wicket.model.ResourceModel;
 import org.apache.wicket.spring.injection.annot.SpringBean;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -48,6 +51,9 @@ public class SCIMConfPanel extends WizardMgtPanel<SCIMConf> {
     @SpringBean
     protected SCIMConfRestClient scimConfRestClient;
 
+    @SpringBean
+    protected AnyTypeRestClient anyTypeRestClient;
+
     protected final SCIMConf scimConf;
 
     public SCIMConfPanel(
@@ -124,7 +130,7 @@ public class SCIMConfPanel extends WizardMgtPanel<SCIMConf> 
{
 
             @Override
             public WebMarkupContainer getPanel(final String panelId) {
-                return new SCIMConfExtensionUserPanel(panelId, scimConf);
+                return new SCIMConfExtensionUserPanel(panelId, scimConf, 
AnyTypeKind.USER.name());
             }
         });
 
@@ -138,6 +144,30 @@ public class SCIMConfPanel extends 
WizardMgtPanel<SCIMConf> {
             }
         });
 
+        tabs.add(new ITabComponent(new Model<>(getString("tab6")), 
getString("tab6")) {
+
+            private static final long serialVersionUID = -7858187494595192532L;
+
+            @Override
+            public WebMarkupContainer getPanel(final String panelId) {
+                return new SCIMConfExtensionGroupPanel(panelId, scimConf, 
AnyTypeKind.GROUP.name());
+            }
+        });
+
+        anyTypeRestClient.listAnyTypes().stream()
+                .filter(anyTypeTO -> 
AnyTypeKind.ANY_OBJECT.equals(anyTypeTO.getKind()))
+                .forEach(anyTypeTO ->
+                        tabs.add(new ITabComponent(
+                                new ResourceModel("anyType." + 
anyTypeTO.getKey(), anyTypeTO.getKey())) {
+
+                            private static final long serialVersionUID = 
6429988338658964324L;
+
+                            @Override
+                            public WebMarkupContainer getPanel(final String 
panelId) {
+                                return new 
SCIMConfExtensionAnyObjectPanel(panelId, scimConf, anyTypeTO.getKey());
+                            }
+                        }));
+
         return tabs;
     }
 }
diff --git 
a/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/mapping/SCIMExtensionMappingPanel.java
 
b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/mapping/SCIMExtensionMappingPanel.java
index 683794130c..07b43469f8 100644
--- 
a/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/mapping/SCIMExtensionMappingPanel.java
+++ 
b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/mapping/SCIMExtensionMappingPanel.java
@@ -35,7 +35,6 @@ import 
org.apache.syncope.client.ui.commons.markup.html.form.AjaxDropDownChoiceP
 import 
org.apache.syncope.client.ui.commons.markup.html.form.AjaxTextFieldPanel;
 import org.apache.syncope.common.lib.scim.SCIMItem;
 import org.apache.syncope.common.lib.scim.SCIMReturned;
-import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.ajax.markup.html.form.AjaxButton;
 import org.apache.wicket.extensions.ajax.markup.html.IndicatingAjaxButton;
@@ -79,11 +78,15 @@ public class SCIMExtensionMappingPanel extends Panel {
 
     protected final WebMarkupContainer mappingContainer;
 
+    protected final String anyTypeKey;
+
     public SCIMExtensionMappingPanel(
             final String id,
-            final IModel<List<SCIMItem>> model) {
+            final IModel<List<SCIMItem>> model,
+            final String anyTypeKey) {
 
         super(id);
+        this.anyTypeKey = anyTypeKey;
         setOutputMarkupId(true);
 
         mappingContainer = new WebMarkupContainer("mappingContainer");
@@ -264,9 +267,18 @@ public class SCIMExtensionMappingPanel extends Panel {
     }
 
     protected IModel<List<String>> getExtAttrNames() {
-        List<String> choices = new 
ArrayList<>(ClassPathScanImplementationLookup.USER_FIELD_NAMES);
-
-        
anyTypeClassRestClient.list(anyTypeRestClient.read(AnyTypeKind.USER.name()).getClasses()).
+        List<String> choices = new ArrayList<>();
+        switch (anyTypeKey) {
+            case "USER":
+                
choices.addAll(ClassPathScanImplementationLookup.USER_FIELD_NAMES);
+                break;
+            case "GROUP":
+                
choices.addAll(ClassPathScanImplementationLookup.GROUP_FIELD_NAMES);
+                break;
+            default:
+                
choices.addAll(ClassPathScanImplementationLookup.ANY_OBJECT_FIELD_NAMES);
+        }
+        
anyTypeClassRestClient.list(anyTypeRestClient.read(anyTypeKey).getClasses()).
                 forEach(anyTypeClassTO -> {
                     choices.addAll(anyTypeClassTO.getPlainSchemas());
                     choices.addAll(anyTypeClassTO.getDerSchemas());
diff --git 
a/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfExtensionUserPanel.html
 
b/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfExtensionAnyPanel.html
similarity index 100%
rename from 
ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfExtensionUserPanel.html
rename to 
ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfExtensionAnyPanel.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 fd36da900c..954ee7de18 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
@@ -21,3 +21,4 @@ tab3=EnterpriseUser
 saveButton=Save
 tab4=ExtensionUser
 tab5=Group
+tab6=ExtensionGroup
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 fd36da900c..954ee7de18 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
@@ -21,3 +21,4 @@ tab3=EnterpriseUser
 saveButton=Save
 tab4=ExtensionUser
 tab5=Group
+tab6=ExtensionGroup
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 fd36da900c..954ee7de18 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
@@ -21,3 +21,4 @@ tab3=EnterpriseUser
 saveButton=Save
 tab4=ExtensionUser
 tab5=Group
+tab6=ExtensionGroup
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 fd36da900c..954ee7de18 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
@@ -21,3 +21,4 @@ tab3=EnterpriseUser
 saveButton=Save
 tab4=ExtensionUser
 tab5=Group
+tab6=ExtensionGroup
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 468b5d289e..d90ef49b72 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
@@ -19,6 +19,8 @@
 package org.apache.syncope.common.lib.scim;
 
 import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
 
 public class SCIMConf implements Serializable {
 
@@ -32,10 +34,14 @@ public class SCIMConf implements Serializable {
 
     private SCIMEnterpriseUserConf enterpriseUserConf;
 
-    private SCIMExtensionUserConf extensionUserConf;
+    private SCIMExtensionAnyConf extensionUserConf;
 
     private SCIMGroupConf groupConf;
 
+    private SCIMExtensionAnyConf extensionGroupConf;
+
+    private final List<SCIMExtensionAnyObjectConf> extensionAnyObjectsConf = 
new ArrayList<>();
+
     public SCIMGeneralConf getGeneralConf() {
         return generalConf;
     }
@@ -60,14 +66,26 @@ public class SCIMConf implements Serializable {
         this.enterpriseUserConf = enterpriseUserConf;
     }
 
-    public SCIMExtensionUserConf getExtensionUserConf() {
+    public SCIMExtensionAnyConf getExtensionUserConf() {
         return extensionUserConf;
     }
 
-    public void setExtensionUserConf(final SCIMExtensionUserConf 
extensionUserConf) {
+    public void setExtensionUserConf(final SCIMExtensionAnyConf 
extensionUserConf) {
         this.extensionUserConf = extensionUserConf;
     }
 
+    public SCIMExtensionAnyConf getExtensionGroupConf() {
+        return extensionGroupConf;
+    }
+
+    public void setExtensionGroupConf(final SCIMExtensionAnyConf 
extensionGroupConf) {
+        this.extensionGroupConf = extensionGroupConf;
+    }
+
+    public List<SCIMExtensionAnyObjectConf> getExtensionAnyObjectsConf() {
+        return extensionAnyObjectsConf;
+    }
+
     public SCIMGroupConf getGroupConf() {
         return groupConf;
     }
diff --git 
a/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMExtensionUserConf.java
 
b/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMExtensionAnyConf.java
similarity index 97%
rename from 
ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMExtensionUserConf.java
rename to 
ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMExtensionAnyConf.java
index cae6330caf..74c81e96a5 100644
--- 
a/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMExtensionUserConf.java
+++ 
b/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMExtensionAnyConf.java
@@ -27,7 +27,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 
-public class SCIMExtensionUserConf implements Serializable {
+public class SCIMExtensionAnyConf implements Serializable {
 
     private static final long serialVersionUID = -9091596628402547645L;
 
diff --git 
a/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMExtensionAnyObjectConf.java
 
b/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMExtensionAnyObjectConf.java
new file mode 100644
index 0000000000..7872e44511
--- /dev/null
+++ 
b/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMExtensionAnyObjectConf.java
@@ -0,0 +1,44 @@
+/*
+ * 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.common.lib.scim;
+
+public class SCIMExtensionAnyObjectConf extends SCIMExtensionAnyConf {
+
+    private static final long serialVersionUID = 6600395333691186244L;
+
+    private String type;
+
+    private String externalId;
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(final String type) {
+        this.type = type;
+    }
+
+    public String getExternalId() {
+        return externalId;
+    }
+
+    public void setExternalId(final String externalId) {
+        this.externalId = externalId;
+    }
+}
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 7c37d03a3c..1518675eb0 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
@@ -35,6 +35,8 @@ 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.AnyObjectCR;
+import org.apache.syncope.common.lib.request.AnyObjectUR;
 import org.apache.syncope.common.lib.request.AttrPatch;
 import org.apache.syncope.common.lib.request.GroupCR;
 import org.apache.syncope.common.lib.request.GroupUR;
@@ -46,8 +48,10 @@ 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.SCIMExtensionAnyObjectConf;
 import org.apache.syncope.common.lib.scim.SCIMManagerConf;
 import org.apache.syncope.common.lib.scim.SCIMUserAddressConf;
+import org.apache.syncope.common.lib.to.AnyObjectTO;
 import org.apache.syncope.common.lib.to.GroupTO;
 import org.apache.syncope.common.lib.to.MembershipTO;
 import org.apache.syncope.common.lib.to.UserTO;
@@ -66,6 +70,7 @@ import org.apache.syncope.ext.scimv2.api.BadRequestException;
 import org.apache.syncope.ext.scimv2.api.data.Group;
 import org.apache.syncope.ext.scimv2.api.data.Member;
 import org.apache.syncope.ext.scimv2.api.data.Meta;
+import org.apache.syncope.ext.scimv2.api.data.SCIMAnyObject;
 import org.apache.syncope.ext.scimv2.api.data.SCIMComplexValue;
 import org.apache.syncope.ext.scimv2.api.data.SCIMEnterpriseInfo;
 import org.apache.syncope.ext.scimv2.api.data.SCIMExtensionInfo;
@@ -89,8 +94,6 @@ public class SCIMDataBinder {
 
     protected static final Logger LOG = 
LoggerFactory.getLogger(SCIMDataBinder.class);
 
-    protected static final List<String> GROUP_SCHEMAS = 
List.of(Resource.Group.schema());
-
     /**
      * Translates the given SCIM filter into the equivalent JEXL expression.
      *
@@ -208,7 +211,7 @@ public class SCIMDataBinder {
                 userTO.getKey(),
                 schemas,
                 new Meta(
-                        Resource.User,
+                        Resource.User.name(),
                         userTO.getCreationDate(),
                         
Optional.ofNullable(userTO.getLastChangeDate()).orElse(userTO.getCreationDate()),
                         userTO.getETagValue(),
@@ -509,6 +512,41 @@ public class SCIMDataBinder {
         }
     }
 
+    protected void setAttribute(
+            final GroupTO groupTO,
+            final String schema,
+            final String value) {
+
+        if (schema == null || value == null) {
+            return;
+        }
+
+        switch (schema) {
+            case "name":
+                groupTO.setName(value);
+                break;
+
+            default:
+                groupTO.getPlainAttrs().add(new 
Attr.Builder(schema).value(value).build());
+        }
+    }
+
+    protected void setAttribute(
+            final AnyObjectTO anyObjectTO,
+            final String schema,
+            final String value) {
+
+        if (schema == null || value == null) {
+            return;
+        }
+
+        if ("name".equals(schema)) {
+            anyObjectTO.setName(value);
+        } else {
+            anyObjectTO.getPlainAttrs().add(new 
Attr.Builder(schema).value(value).build());
+        }
+    }
+
     protected <E extends Enum<?>> void setAttribute(
             final Set<Attr> attrs,
             final List<SCIMComplexConf<E>> confs,
@@ -1034,18 +1072,24 @@ public class SCIMDataBinder {
             final List<String> attributes,
             final List<String> excludedAttributes) {
 
+        SCIMConf conf = confManager.get();
+        List<String> schemas = new ArrayList<>();
+        schemas.add(Resource.Group.schema());
+        if (conf.getExtensionGroupConf() != null) {
+            schemas.add(Resource.ExtensionGroup.schema());
+        }
+
         SCIMGroup group = new SCIMGroup(
                 groupTO.getKey(),
+                schemas,
                 new Meta(
-                        Resource.Group,
+                        Resource.Group.name(),
                         groupTO.getCreationDate(),
                         
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()));
@@ -1063,6 +1107,19 @@ public class SCIMDataBinder {
         membCond.setGroup(groupTO.getKey());
         SearchCond searchCond = SearchCond.getLeaf(membCond);
 
+        if (conf.getExtensionGroupConf() != null) {
+            SCIMExtensionInfo extensionInfo = new SCIMExtensionInfo();
+            conf.getExtensionGroupConf().asMap().forEach((scimAttr, 
syncopeAttr) -> {
+                if (output(attributes, excludedAttributes, scimAttr) && 
attrs.containsKey(syncopeAttr)) {
+                    extensionInfo.getAttributes().put(scimAttr, 
attrs.get(syncopeAttr).getValues().get(0));
+                }
+            });
+
+            if (!extensionInfo.isEmpty()) {
+                group.setExtensionInfo(extensionInfo);
+            }
+        }
+
         if (output(attributes, excludedAttributes, "members")) {
             int count = userLogic.search(
                     searchCond, 1, 1, List.of(), SyncopeConstants.ROOT_REALM, 
true, false).getLeft();
@@ -1084,7 +1141,16 @@ public class SCIMDataBinder {
     }
 
     public GroupTO toGroupTO(final SCIMGroup group, final boolean 
checkSchemas) {
-        if (checkSchemas && !GROUP_SCHEMAS.equals(group.getSchemas())) {
+        SCIMConf conf = confManager.get();
+        Set<String> expectedSchemas = new HashSet<>();
+        expectedSchemas.add(Resource.Group.schema());
+        if (conf.getExtensionGroupConf() != null) {
+            expectedSchemas.add(Resource.ExtensionGroup.schema());
+        }
+        if (checkSchemas
+                && (!group.getSchemas().containsAll(expectedSchemas)
+                || !expectedSchemas.containsAll(group.getSchemas()))) {
+
             throw new BadRequestException(ErrorType.invalidValue);
         }
 
@@ -1093,7 +1159,6 @@ public class SCIMDataBinder {
         groupTO.setKey(group.getId());
         groupTO.setName(group.getDisplayName());
 
-        SCIMConf conf = confManager.get();
         if (conf.getGroupConf() != null
                 && conf.getGroupConf().getExternalId() != null && 
group.getExternalId() != null) {
 
@@ -1102,6 +1167,11 @@ public class SCIMDataBinder {
                             value(group.getExternalId()).build());
         }
 
+        if (conf.getExtensionGroupConf() != null && group.getExtensionInfo() 
!= null) {
+            conf.getExtensionGroupConf().asMap().forEach((scimAttr, 
syncopeAttr) -> setAttribute(
+                    groupTO, syncopeAttr, 
group.getExtensionInfo().getAttributes().get(scimAttr)));
+        }
+
         return groupTO;
     }
 
@@ -1128,11 +1198,147 @@ public class SCIMDataBinder {
             groupUR.setName(name.build());
         } else {
             SCIMConf conf = confManager.get();
-            if (conf.getGroupConf() != null) {
+            if (conf.getGroupConf() != null && 
"externalId".equals(op.getPath().getAttribute())) {
                 setAttribute(groupUR.getPlainAttrs(), 
conf.getGroupConf().getExternalId(), op);
             }
+            if (conf.getExtensionGroupConf() != null) {
+                Optional.ofNullable(conf.getExtensionGroupConf()).
+                        flatMap(schema -> 
Optional.ofNullable(schema.asMap().get(op.getPath().getAttribute()))).
+                        ifPresent(schema -> 
setAttribute(groupUR.getPlainAttrs(), schema, op));
+            }
         }
 
         return groupUR;
     }
+
+    public SCIMAnyObject toSCIMAnyObject(
+            final AnyObjectTO anyObjectTO,
+            final String location,
+            final List<String> attributes,
+            final List<String> excludedAttributes) {
+        SCIMConf conf = confManager.get();
+        List<String> schemas = new ArrayList<>();
+        SCIMExtensionAnyObjectConf scimExtensionAnyObjectConf =
+                conf.getExtensionAnyObjectsConf().stream()
+                        .filter(scimExtAnyObjectConf -> 
scimExtAnyObjectConf.getType().equals(anyObjectTO.getType()))
+                        .findFirst()
+                        .orElseThrow(() -> new 
NotFoundException("SCIMExtensionAnyObjectConf not found"));
+        schemas.add("urn:ietf:params:scim:schemas:extension:syncope:2.0:" + 
scimExtensionAnyObjectConf.getType());
+
+        SCIMAnyObject anyObject = new SCIMAnyObject(
+                anyObjectTO.getKey(),
+                schemas,
+                new Meta(
+                        "urn:ietf:params:scim:schemas:extension:syncope:2.0:"
+                                + scimExtensionAnyObjectConf.getType(),
+                        anyObjectTO.getCreationDate(),
+                        
Optional.ofNullable(anyObjectTO.getLastChangeDate()).orElse(anyObjectTO.getCreationDate()),
+                        anyObjectTO.getETagValue(),
+                        location),
+                output(attributes, excludedAttributes, "displayName", 
anyObjectTO.getName()));
+
+        Map<String, Attr> attrs = new HashMap<>();
+        attrs.putAll(EntityTOUtils.buildAttrMap(anyObjectTO.getPlainAttrs()));
+        attrs.putAll(EntityTOUtils.buildAttrMap(anyObjectTO.getDerAttrs()));
+        attrs.putAll(EntityTOUtils.buildAttrMap(anyObjectTO.getVirAttrs()));
+
+        if (output(attributes, excludedAttributes, "externalId")
+                && scimExtensionAnyObjectConf.getExternalId() != null
+                && 
attrs.containsKey(scimExtensionAnyObjectConf.getExternalId())) {
+
+            
anyObject.setExternalId(attrs.get(scimExtensionAnyObjectConf.getExternalId()).getValues().get(0));
+        }
+
+        SCIMExtensionInfo extensionInfo = new SCIMExtensionInfo();
+        scimExtensionAnyObjectConf.asMap().forEach((scimAttr, syncopeAttr) -> {
+            if (output(attributes, excludedAttributes, scimAttr) && 
attrs.containsKey(syncopeAttr)) {
+                extensionInfo.getAttributes().put(scimAttr, 
attrs.get(syncopeAttr).getValues().get(0));
+            }
+        });
+
+        if (!extensionInfo.isEmpty()) {
+            anyObject.setExtensionInfo(extensionInfo);
+        }
+
+        return anyObject;
+    }
+
+    public AnyObjectTO toAnyObjectTO(final SCIMAnyObject anyObject, final 
boolean checkSchemas) {
+        SCIMConf conf = confManager.get();
+        Set<String> expectedSchemas = new HashSet<>();
+        Optional<SCIMExtensionAnyObjectConf> scimExtensionAnyObjectConf =
+                conf.getExtensionAnyObjectsConf().stream()
+                        .filter(scimExtAnyObjectConf ->
+                                
scimExtAnyObjectConf.getType().equals(anyObject.getExtensionUrn()
+                                        
.substring(anyObject.getExtensionUrn().lastIndexOf(':') + 1)))
+                        .findFirst();
+        scimExtensionAnyObjectConf.ifPresent(scimExtAnyObjectConf ->
+                
expectedSchemas.add("urn:ietf:params:scim:schemas:extension:syncope:2.0:"
+                        + scimExtAnyObjectConf.getType()));
+        if (checkSchemas
+                && (!anyObject.getSchemas().containsAll(expectedSchemas)
+                || !expectedSchemas.containsAll(anyObject.getSchemas()))) {
+
+            throw new BadRequestException(ErrorType.invalidValue);
+        }
+
+        AnyObjectTO anyObjectTO = new AnyObjectTO();
+        anyObjectTO.setRealm(SyncopeConstants.ROOT_REALM);
+        anyObjectTO.setKey(anyObject.getId());
+        anyObjectTO.setName(anyObject.getDisplayName());
+        
anyObjectTO.setType(anyObject.getExtensionUrn().substring(anyObject.getExtensionUrn().lastIndexOf(':')
 + 1));
+
+        if (scimExtensionAnyObjectConf.isPresent()
+                && scimExtensionAnyObjectConf.get().getExternalId() != null && 
anyObject.getExternalId() != null) {
+
+            anyObjectTO.getPlainAttrs().add(
+                    new 
Attr.Builder(scimExtensionAnyObjectConf.get().getExternalId()).
+                            value(anyObject.getExternalId()).build());
+        }
+
+        if (scimExtensionAnyObjectConf.isPresent() && 
anyObject.getExtensionInfo() != null) {
+            scimExtensionAnyObjectConf.get().asMap().forEach((scimAttr, 
syncopeAttr) -> setAttribute(
+                    anyObjectTO, syncopeAttr, 
anyObject.getExtensionInfo().getAttributes().get(scimAttr)));
+        }
+
+        return anyObjectTO;
+    }
+
+    public AnyObjectCR toAnyObjectCR(final SCIMAnyObject anyObject) {
+        AnyObjectTO anyObjectTO = toAnyObjectTO(anyObject, true);
+        AnyObjectCR anyObjectCR = new AnyObjectCR();
+        EntityTOUtils.toAnyCR(anyObjectTO, anyObjectCR);
+        return anyObjectCR;
+    }
+
+    public AnyObjectUR toAnyObjectUR(final AnyObjectTO before, final 
SCIMPatchOperation op) {
+        if (op.getPath() == null) {
+            throw new UnsupportedOperationException("Empty path not supported 
for AnyObjects");
+        }
+
+        AnyObjectUR anyObjectUR = new 
AnyObjectUR.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());
+            }
+            anyObjectUR.setName(name.build());
+        } else {
+            SCIMConf conf = confManager.get();
+            Optional<SCIMExtensionAnyObjectConf> scimExtensionAnyObjectConf =
+                    conf.getExtensionAnyObjectsConf().stream()
+                            .filter(scimExtAnyObjectConf -> 
scimExtAnyObjectConf.getType().equals(before.getType()))
+                            .findFirst();
+            if (scimExtensionAnyObjectConf.isPresent() && 
"externalId".equals(op.getPath().getAttribute())) {
+                setAttribute(anyObjectUR.getPlainAttrs(), 
scimExtensionAnyObjectConf.get().getExternalId(), op);
+            }
+            scimExtensionAnyObjectConf.flatMap(extensionAnyObjectConf -> 
Optional.of(extensionAnyObjectConf)
+                            .flatMap(schema -> 
Optional.ofNullable(schema.asMap().get(op.getPath().getAttribute()))))
+                    .ifPresent(schema -> 
setAttribute(anyObjectUR.getPlainAttrs(), schema, op));
+        }
+
+        return anyObjectUR;
+    }
 }
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 610e11795a..4a5bbfc844 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
@@ -100,6 +100,60 @@ public class SCIMLogic extends AbstractLogic<EntityTO> {
                         + "\"location\": 
\"/v2/Schemas/urn:ietf:params:scim:schemas:extension:syncope:2.0:User\"}"));
                 schemaArray.add(extensionObject);
             }
+            if (conf.getExtensionGroupConf() != null) {
+                ObjectNode extensionObject = MAPPER.createObjectNode();
+                extensionObject.put("id", Resource.ExtensionGroup.schema());
+                extensionObject.put("name", 
conf.getExtensionGroupConf().getName());
+                extensionObject.put("description", 
conf.getExtensionGroupConf().getDescription());
+                ArrayNode attributes = MAPPER.createArrayNode();
+                conf.getExtensionGroupConf().getAttributes().forEach(scimItem 
-> {
+                    ObjectNode attribute = MAPPER.createObjectNode();
+                    attribute.put("name", scimItem.getIntAttrName());
+                    attribute.put("type", "string");
+                    attribute.put("multiValued", scimItem.isMultiValued());
+                    attribute.put("required", 
scimItem.getMandatoryCondition());
+                    attribute.put("caseExact", scimItem.isCaseExact());
+                    attribute.put("mutability", scimItem.isMutability());
+                    attribute.put("returned", 
scimItem.getReturned().getReturned());
+                    attribute.put("uniqueness", scimItem.isUniqueness());
+                    attributes.add(attribute);
+                });
+                extensionObject.putIfAbsent("attributes", attributes);
+                extensionObject.putIfAbsent("meta", 
MAPPER.readTree("{\"resourceType\": \"Schema\","
+                        + "\"location\": 
\"/v2/Schemas/urn:ietf:params:scim:schemas:extension:syncope:2.0:Group\"}"));
+                schemaArray.add(extensionObject);
+            }
+            if (!conf.getExtensionAnyObjectsConf().isEmpty()) {
+                conf.getExtensionAnyObjectsConf().forEach(confItem -> {
+                    ObjectNode extensionObject = MAPPER.createObjectNode();
+                    extensionObject.put("id",
+                            
"urn:ietf:params:scim:schemas:extension:syncope:2.0:" + confItem.getType());
+                    extensionObject.put("name", confItem.getName());
+                    extensionObject.put("description", 
confItem.getDescription());
+                    ArrayNode attributes = MAPPER.createArrayNode();
+                    confItem.getAttributes().forEach(scimItem -> {
+                        ObjectNode attribute = MAPPER.createObjectNode();
+                        attribute.put("name", scimItem.getIntAttrName());
+                        attribute.put("type", "string");
+                        attribute.put("multiValued", scimItem.isMultiValued());
+                        attribute.put("required", 
scimItem.getMandatoryCondition());
+                        attribute.put("caseExact", scimItem.isCaseExact());
+                        attribute.put("mutability", scimItem.isMutability());
+                        attribute.put("returned", 
scimItem.getReturned().getReturned());
+                        attribute.put("uniqueness", scimItem.isUniqueness());
+                        attributes.add(attribute);
+                    });
+                    extensionObject.putIfAbsent("attributes", attributes);
+                    try {
+                        extensionObject.putIfAbsent("meta", 
MAPPER.readTree("{\"resourceType\": \"Schema\","
+                                + "\"location\": 
\"/v2/Schemas/urn:ietf:params:scim:schemas:extension:syncope:2.0:"
+                                + confItem.getType() + "}\""));
+                    } catch (IOException e) {
+                        LOG.error("Could not parse the default schema 
definitions", e);
+                    }
+                    schemaArray.add(extensionObject);
+                });
+            }
             schemas = 
MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(tree);
 
             schemaMap.clear();
@@ -121,7 +175,7 @@ public class SCIMLogic extends AbstractLogic<EntityTO> {
 
                 SERVICE_PROVIDER_CONFIG = new ServiceProviderConfig(
                         new Meta(
-                                Resource.ServiceProviderConfig,
+                                Resource.ServiceProviderConfig.name(),
                                 conf.getGeneralConf().getCreationDate(),
                                 conf.getGeneralConf().getLastChangeDate(),
                                 conf.getGeneralConf().getETagValue(),
@@ -162,12 +216,12 @@ public class SCIMLogic extends AbstractLogic<EntityTO> {
             String uri = uriBuilder.build().toASCIIString();
             if (USER == null) {
                 USER = new ResourceType("User", "User", "/Users", "User 
Account", Resource.User.schema(),
-                        new Meta(Resource.ResourceType, null, null, null, uri 
+ "User"));
+                        new Meta(Resource.ResourceType.name(), null, null, 
null, uri + "User"));
                 USER.getSchemaExtensions().add(new 
SchemaExtension(Resource.EnterpriseUser.schema(), true));
             }
             if (GROUP == null) {
                 GROUP = new ResourceType("Group", "Group", "/Groups", "Group", 
Resource.Group.schema(),
-                        new Meta(Resource.ResourceType, null, null, null, uri 
+ "Group"));
+                        new Meta(Resource.ResourceType.name(), null, null, 
null, uri + "Group"));
             }
         }
 
diff --git 
a/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/scim/SCIMConfManager.java
 
b/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/scim/SCIMConfManager.java
index 7fab5c0e1e..7cef51a222 100644
--- 
a/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/scim/SCIMConfManager.java
+++ 
b/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/scim/SCIMConfManager.java
@@ -26,6 +26,7 @@ import 
org.apache.syncope.common.keymaster.client.api.ConfParamOps;
 import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.scim.SCIMConf;
 import org.apache.syncope.common.lib.scim.SCIMGeneralConf;
+import org.apache.syncope.common.lib.scim.SCIMItem;
 import org.apache.syncope.common.lib.scim.types.SCIMEntitlement;
 import org.apache.syncope.common.lib.to.PlainSchemaTO;
 import org.apache.syncope.common.lib.types.AttrSchemaType;
@@ -88,40 +89,15 @@ public class SCIMConfManager {
         conf.getGeneralConf().setLastChangeDate(OffsetDateTime.now());
 
         if (conf.getExtensionUserConf() != null) {
-            conf.getExtensionUserConf().getAttributes().forEach(scimItem -> {
-                try {
-                    PlainSchemaTO schema = schemaLogic.read(SchemaType.PLAIN, 
scimItem.getExtAttrName());
-                    SyncopeClientException invalidMapping =
-                            
SyncopeClientException.build(ClientExceptionType.InvalidMapping);
-                    if 
(!scimItem.getMandatoryCondition().equals(schema.getMandatoryCondition())) {
-                        invalidMapping.getElements().add('\'' + 
scimItem.getIntAttrName()
-                                + "' should " + 
(Boolean.parseBoolean(schema.getMandatoryCondition()) ? "" : "not")
-                                + " be required");
-                    }
-                    if (scimItem.isMultiValued() != schema.isMultivalue()) {
-                        invalidMapping.getElements().add('\'' + 
scimItem.getIntAttrName()
-                                + "' should " + (schema.isMultivalue() ? "" : 
"not") + " be multi-value");
-                    }
-                    if (scimItem.isMutability() != schema.isReadonly()) {
-                        invalidMapping.getElements().add('\'' + 
scimItem.getIntAttrName()
-                                + "' should " + (schema.isReadonly() ? "" : 
"not") + " be readonly");
-                    }
-                    if (scimItem.isUniqueness() != 
schema.isUniqueConstraint()) {
-                        invalidMapping.getElements().add('\'' + 
scimItem.getIntAttrName()
-                                + "' should " + (schema.isUniqueConstraint() ? 
"" : "not") + " be unique");
-                    }
-                    if (!invalidMapping.getElements().isEmpty()) {
-                        throw invalidMapping;
-                    }
-                } catch (NotFoundException e) {
-                    PlainSchemaTO schema = 
schemaLogic.read(SchemaType.VIRTUAL, scimItem.getExtAttrName());
-                    if (scimItem.isMutability() != schema.isReadonly()) {
-                        SyncopeClientException invalidMapping =
-                                
SyncopeClientException.build(ClientExceptionType.InvalidMapping);
-                        invalidMapping.getElements().add('\'' + 
scimItem.getIntAttrName()
-                                + "' should " + (schema.isReadonly() ? "" : 
"not") + " be readonly");
-                        throw invalidMapping;
-                    }
+            
conf.getExtensionUserConf().getAttributes().forEach(this::checkSCIMItem);
+        }
+        if (conf.getExtensionGroupConf() != null) {
+            
conf.getExtensionGroupConf().getAttributes().forEach(this::checkSCIMItem);
+        }
+        if (!conf.getExtensionAnyObjectsConf().isEmpty()) {
+            conf.getExtensionAnyObjectsConf().forEach(confItem -> {
+                if (confItem != null) {
+                    confItem.getAttributes().forEach(this::checkSCIMItem);
                 }
             });
         }
@@ -129,4 +105,41 @@ public class SCIMConfManager {
         confParamOps.set(AuthContextUtils.getDomain(),
                 SCIMConf.KEY, 
Base64.getEncoder().encodeToString(POJOHelper.serialize(conf).getBytes()));
     }
+
+    private void checkSCIMItem(final SCIMItem scimItem) {
+        try {
+            PlainSchemaTO schema = schemaLogic.read(SchemaType.PLAIN, 
scimItem.getExtAttrName());
+            SyncopeClientException invalidMapping =
+                    
SyncopeClientException.build(ClientExceptionType.InvalidMapping);
+            if 
(!scimItem.getMandatoryCondition().equals(schema.getMandatoryCondition())) {
+                invalidMapping.getElements().add('\'' + 
scimItem.getIntAttrName()
+                        + "' should " + 
(Boolean.parseBoolean(schema.getMandatoryCondition()) ? "" : "not")
+                        + " be required");
+            }
+            if (scimItem.isMultiValued() != schema.isMultivalue()) {
+                invalidMapping.getElements().add('\'' + 
scimItem.getIntAttrName()
+                        + "' should " + (schema.isMultivalue() ? "" : "not") + 
" be multi-value");
+            }
+            if (scimItem.isMutability() != schema.isReadonly()) {
+                invalidMapping.getElements().add('\'' + 
scimItem.getIntAttrName()
+                        + "' should " + (schema.isReadonly() ? "" : "not") + " 
be readonly");
+            }
+            if (scimItem.isUniqueness() != schema.isUniqueConstraint()) {
+                invalidMapping.getElements().add('\'' + 
scimItem.getIntAttrName()
+                        + "' should " + (schema.isUniqueConstraint() ? "" : 
"not") + " be unique");
+            }
+            if (!invalidMapping.getElements().isEmpty()) {
+                throw invalidMapping;
+            }
+        } catch (NotFoundException e) {
+            PlainSchemaTO schema = schemaLogic.read(SchemaType.VIRTUAL, 
scimItem.getExtAttrName());
+            if (scimItem.isMutability() != schema.isReadonly()) {
+                SyncopeClientException invalidMapping =
+                        
SyncopeClientException.build(ClientExceptionType.InvalidMapping);
+                invalidMapping.getElements().add('\'' + 
scimItem.getIntAttrName()
+                        + "' should " + (schema.isReadonly() ? "" : "not") + " 
be readonly");
+                throw invalidMapping;
+            }
+        }
+    }
 }
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 fba72c8bef..06647905cc 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
@@ -25,6 +25,7 @@ import java.util.stream.Collectors;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.common.lib.scim.SCIMComplexConf;
 import org.apache.syncope.common.lib.scim.SCIMConf;
+import org.apache.syncope.common.lib.scim.SCIMExtensionAnyObjectConf;
 import org.apache.syncope.common.lib.scim.SCIMUserAddressConf;
 import org.apache.syncope.common.lib.scim.SCIMUserConf;
 import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
@@ -40,12 +41,12 @@ public class SearchCondVisitor extends 
SCIMFilterBaseVisitor<SearchCond> {
     private static final List<String> MULTIVALUE = List.of(
             "emails", "phoneNumbers", "ims", "photos", "addresses");
 
-    private static boolean schemaEquals(final Resource resource, final String 
value, final String schema) {
+    private static boolean schemaEquals(final String resource, final String 
value, final String schema) {
         return resource == null
                 ? value.contains(":")
                 ? StringUtils.substringAfterLast(value, 
":").equalsIgnoreCase(schema)
                 : value.equalsIgnoreCase(schema)
-                : value.equalsIgnoreCase(schema) || (resource.schema() + ":" + 
value).equalsIgnoreCase(schema);
+                : value.equalsIgnoreCase(schema) || (resource + ":" + 
value).equalsIgnoreCase(schema);
     }
 
     private static SearchCond setOperator(final AttrCond attrCond, final 
String operator) {
@@ -154,11 +155,11 @@ public class SearchCondVisitor extends 
SCIMFilterBaseVisitor<SearchCond> {
         return null;
     }
 
-    private final Resource resource;
+    private final String resource;
 
     private final SCIMConf conf;
 
-    public SearchCondVisitor(final Resource resource, final SCIMConf conf) {
+    public SearchCondVisitor(final String resource, final SCIMConf conf) {
         this.resource = resource;
         this.conf = conf;
     }
@@ -171,10 +172,13 @@ public class SearchCondVisitor extends 
SCIMFilterBaseVisitor<SearchCond> {
     public AttrCond createAttrCond(final String schema) {
         AttrCond attrCond = null;
 
-        if (schemaEquals(Resource.User, "userName", schema)) {
+        if (schemaEquals(Resource.User.schema(), "userName", schema)) {
             attrCond = new AnyCond();
             attrCond.setSchema("username");
-        } else if (resource == Resource.Group && schemaEquals(Resource.Group, 
"displayName", schema)) {
+        } else if ((resource.equals(Resource.Group.name())
+                && schemaEquals(Resource.Group.schema(), "displayName", 
schema))
+                || 
(resource.contains("urn:ietf:params:scim:schemas:extension:syncope:2.0:")
+                && schemaEquals(resource, "displayName", schema))) {
             attrCond = new AnyCond();
             attrCond.setSchema("name");
         } else if (schemaEquals(null, "meta.created", schema)) {
@@ -186,11 +190,11 @@ public class SearchCondVisitor extends 
SCIMFilterBaseVisitor<SearchCond> {
         }
 
         switch (resource) {
-            case User:
+            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)) {
+                            if (schemaEquals(Resource.User.schema(), "name." + 
entry.getKey(), schema)) {
                                 attrCond = new AttrCond();
                                 attrCond.setSchema(entry.getValue());
                             }
@@ -198,7 +202,7 @@ public class SearchCondVisitor extends 
SCIMFilterBaseVisitor<SearchCond> {
                     }
 
                     for (Map.Entry<String, String> entry : 
conf.getUserConf().asMap().entrySet()) {
-                        if (schemaEquals(Resource.User, entry.getKey(), 
schema)) {
+                        if (schemaEquals(Resource.User.schema(), 
entry.getKey(), schema)) {
                             attrCond = new AttrCond();
                             attrCond.setSchema(entry.getValue());
                         }
@@ -206,7 +210,7 @@ public class SearchCondVisitor extends 
SCIMFilterBaseVisitor<SearchCond> {
 
                     for (SCIMUserAddressConf address : 
conf.getUserConf().getAddresses()) {
                         for (Map.Entry<String, String> entry : 
address.asMap().entrySet()) {
-                            if (schemaEquals(Resource.User, "addresses." + 
entry.getKey(), schema)) {
+                            if (schemaEquals(Resource.User.schema(), 
"addresses." + entry.getKey(), schema)) {
                                 attrCond = new AttrCond();
                                 attrCond.setSchema(entry.getValue());
                             }
@@ -216,7 +220,7 @@ public class SearchCondVisitor extends 
SCIMFilterBaseVisitor<SearchCond> {
 
                 if (conf.getEnterpriseUserConf() != null) {
                     for (Map.Entry<String, String> entry : 
conf.getEnterpriseUserConf().asMap().entrySet()) {
-                        if (schemaEquals(Resource.EnterpriseUser, 
entry.getKey(), schema)) {
+                        if (schemaEquals(Resource.EnterpriseUser.schema(), 
entry.getKey(), schema)) {
                             attrCond = new AttrCond();
                             attrCond.setSchema(entry.getValue());
                         }
@@ -232,7 +236,7 @@ public class SearchCondVisitor extends 
SCIMFilterBaseVisitor<SearchCond> {
 
                 if (conf.getExtensionUserConf() != null) {
                     for (Map.Entry<String, String> entry : 
conf.getExtensionUserConf().asMap().entrySet()) {
-                        if (schemaEquals(Resource.ExtensionUser, 
entry.getKey(), schema)) {
+                        if (schemaEquals(Resource.ExtensionUser.schema(), 
entry.getKey(), schema)) {
                             attrCond = new AttrCond();
                             attrCond.setSchema(entry.getValue());
                         }
@@ -240,10 +244,19 @@ public class SearchCondVisitor extends 
SCIMFilterBaseVisitor<SearchCond> {
                 }
                 break;
 
-            case Group:
+            case "Group":
                 if (conf.getGroupConf() != null) {
                     for (Map.Entry<String, String> entry : 
conf.getGroupConf().asMap().entrySet()) {
-                        if (schemaEquals(Resource.Group, entry.getKey(), 
schema)) {
+                        if (schemaEquals(Resource.Group.schema(), 
entry.getKey(), schema)) {
+                            attrCond = new AttrCond();
+                            attrCond.setSchema(entry.getValue());
+                        }
+                    }
+                }
+
+                if (conf.getExtensionGroupConf() != null) {
+                    for (Map.Entry<String, String> entry : 
conf.getExtensionGroupConf().asMap().entrySet()) {
+                        if (schemaEquals(Resource.ExtensionGroup.schema(), 
entry.getKey(), schema)) {
                             attrCond = new AttrCond();
                             attrCond.setSchema(entry.getValue());
                         }
@@ -252,6 +265,20 @@ public class SearchCondVisitor extends 
SCIMFilterBaseVisitor<SearchCond> {
                 break;
 
             default:
+                if (!conf.getExtensionAnyObjectsConf().isEmpty()) {
+                    Optional<SCIMExtensionAnyObjectConf> scimExtAnyObject = 
conf.getExtensionAnyObjectsConf().stream()
+                            .filter(scimExtensionAnyConf -> 
scimExtensionAnyConf.getType().equals(resource))
+                            .findFirst();
+                    if (scimExtAnyObject.isPresent()) {
+                        for (Map.Entry<String, String> entry : 
scimExtAnyObject.get().asMap().entrySet()) {
+                            if 
(schemaEquals("urn:ietf:params:scim:schemas:extension:syncope:2.0:" + resource,
+                                    entry.getKey(), schema)) {
+                                attrCond = new AttrCond();
+                                attrCond.setSchema(entry.getValue());
+                            }
+                        }
+                    }
+                }
         }
 
         if (attrCond == null) {
diff --git 
a/ext/scimv2/logic/src/test/java/org/apache/syncope/core/logic/scim/SCIMFilterTest.java
 
b/ext/scimv2/logic/src/test/java/org/apache/syncope/core/logic/scim/SCIMFilterTest.java
index 5e62a41252..bacef28cb9 100644
--- 
a/ext/scimv2/logic/src/test/java/org/apache/syncope/core/logic/scim/SCIMFilterTest.java
+++ 
b/ext/scimv2/logic/src/test/java/org/apache/syncope/core/logic/scim/SCIMFilterTest.java
@@ -25,7 +25,7 @@ import static org.junit.jupiter.api.Assertions.assertNull;
 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.SCIMExtensionUserConf;
+import org.apache.syncope.common.lib.scim.SCIMExtensionAnyConf;
 import org.apache.syncope.common.lib.scim.SCIMItem;
 import org.apache.syncope.common.lib.scim.SCIMUserConf;
 import org.apache.syncope.common.lib.scim.SCIMUserNameConf;
@@ -65,14 +65,14 @@ public class SCIMFilterTest {
         entConf.setOrganization("org");
         conf.setEnterpriseUserConf(entConf);
 
-        SCIMExtensionUserConf extConf = new SCIMExtensionUserConf();
+        SCIMExtensionAnyConf extConf = new SCIMExtensionAnyConf();
         SCIMItem item = new SCIMItem();
         item.setIntAttrName("realm");
         item.setExtAttrName("realm");
         extConf.add(item);
         conf.setExtensionUserConf(extConf);
 
-        VISITOR = new SearchCondVisitor(Resource.User, conf);
+        VISITOR = new SearchCondVisitor(Resource.User.name(), conf);
     }
 
     @Test
diff --git 
a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Meta.java
 
b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Meta.java
index 7c6d7a9cbe..7d17eac50e 100644
--- 
a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Meta.java
+++ 
b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/Meta.java
@@ -24,13 +24,12 @@ import com.fasterxml.jackson.annotation.JsonProperty;
 import java.time.OffsetDateTime;
 import java.util.Optional;
 import javax.ws.rs.core.EntityTag;
-import org.apache.syncope.ext.scimv2.api.type.Resource;
 
 public class Meta extends SCIMBean {
 
     private static final long serialVersionUID = 8976451652101091915L;
 
-    private final Resource resourceType;
+    private final String resourceType;
 
     private final OffsetDateTime created;
 
@@ -43,7 +42,7 @@ public class Meta extends SCIMBean {
 
     @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
     public Meta(
-            @JsonProperty("resourceType") final Resource resourceType,
+            @JsonProperty("resourceType") final String resourceType,
             @JsonProperty("created") final OffsetDateTime created,
             @JsonProperty("lastModified") final OffsetDateTime lastModified,
             @JsonProperty("version") final String version,
@@ -56,7 +55,7 @@ public class Meta extends SCIMBean {
         this.location = location;
     }
 
-    public Resource getResourceType() {
+    public String getResourceType() {
         return resourceType;
     }
 
diff --git 
a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMAnyObject.java
 
b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMAnyObject.java
new file mode 100644
index 0000000000..5316ae1a3c
--- /dev/null
+++ 
b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMAnyObject.java
@@ -0,0 +1,90 @@
+/*
+ * 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.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+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.List;
+import java.util.Map;
+
+@JsonPropertyOrder({ "schemas", "id", "externalId", "displayName", 
"extensionInfo", "meta" })
+public class SCIMAnyObject extends SCIMResource {
+
+    @JsonIgnore
+    private String extensionUrn;
+
+    @JsonIgnore
+    private SCIMExtensionInfo extensionInfo;
+
+    @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+    public SCIMAnyObject(
+            @JsonProperty("id") final String id,
+            @JsonProperty("schemas") final List<String> schemas,
+            @JsonProperty("meta") final Meta meta,
+            @JsonProperty("displayName") final String displayName) {
+
+        super(id, schemas, meta);
+        super.setDisplayName(displayName);
+        this.extensionUrn = schemas.isEmpty() ? null : schemas.get(0);
+    }
+
+    @JsonAnyGetter
+    public Map<String, SCIMExtensionInfo> getExtensionAsMap() {
+        if (extensionUrn != null && extensionInfo != null && 
!extensionInfo.isEmpty()) {
+            return Map.of(extensionUrn, extensionInfo);
+        }
+        return Map.of();
+    }
+
+    @JsonAnySetter
+    public void setDynamicExtension(final String key, final Object value) {
+        if 
(key.startsWith("urn:ietf:params:scim:schemas:extension:syncope:2.0:")) {
+            this.extensionUrn = key;
+
+            if (value instanceof Map) {
+                SCIMExtensionInfo info = new SCIMExtensionInfo();
+                Map<?, ?> rawMap = (Map<?, ?>) value;
+                for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
+                    info.add(entry.getKey().toString(), 
entry.getValue().toString());
+                }
+                this.extensionInfo = info;
+            }
+        }
+    }
+
+    public String getExtensionUrn() {
+        return extensionUrn;
+    }
+
+    public void setExtensionUrn(final String extensionUrn) {
+        this.extensionUrn = extensionUrn;
+    }
+
+    public SCIMExtensionInfo getExtensionInfo() {
+        return extensionInfo;
+    }
+
+    public void setExtensionInfo(final SCIMExtensionInfo extensionInfo) {
+        this.extensionInfo = extensionInfo;
+    }
+}
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 c45d6da49d..248f09ab25 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
@@ -23,26 +23,37 @@ import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonPropertyOrder;
 import java.util.ArrayList;
 import java.util.List;
-import org.apache.syncope.ext.scimv2.api.type.Resource;
 
-@JsonPropertyOrder({ "schemas", "id", "externalId", "displayName", "members", 
"meta" })
+@JsonPropertyOrder({ "schemas", "id", "externalId", "displayName", "members", 
"extensionInfo", "meta" })
 public class SCIMGroup extends SCIMResource {
 
     private static final long serialVersionUID = -2935466041674390279L;
 
     private final List<Member> members = new ArrayList<>();
 
+    @JsonProperty("urn:ietf:params:scim:schemas:extension:syncope:2.0:Group")
+    private SCIMExtensionInfo extensionInfo;
+
     @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
     public SCIMGroup(
             @JsonProperty("id") final String id,
+            @JsonProperty("schemas") final List<String> schemas,
             @JsonProperty("meta") final Meta meta,
             @JsonProperty("displayName") final String displayName) {
 
-        super(id, List.of(Resource.Group.schema()), meta);
+        super(id, schemas, meta);
         super.setDisplayName(displayName);
     }
 
     public List<Member> getMembers() {
         return members;
     }
+
+    public SCIMExtensionInfo getExtensionInfo() {
+        return extensionInfo;
+    }
+
+    public void setExtensionInfo(final SCIMExtensionInfo extensionInfo) {
+        this.extensionInfo = extensionInfo;
+    }
 }
diff --git 
a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/service/SCIMAnyObjectService.java
 
b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/service/SCIMAnyObjectService.java
new file mode 100644
index 0000000000..5572a30ab0
--- /dev/null
+++ 
b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/service/SCIMAnyObjectService.java
@@ -0,0 +1,26 @@
+/*
+ * 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.ws.rs.Path;
+import org.apache.syncope.ext.scimv2.api.data.SCIMAnyObject;
+
+@Path("v2/AnyObjects")
+public interface SCIMAnyObjectService extends 
SCIMResourceService<SCIMAnyObject> {
+}
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 0cbd6d2828..6882adff77 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
@@ -27,6 +27,7 @@ public enum Resource {
     
EnterpriseUser("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"),
     ExtensionUser("urn:ietf:params:scim:schemas:extension:syncope:2.0:User"),
     Group("urn:ietf:params:scim:schemas:core:2.0:Group"),
+    ExtensionGroup("urn:ietf:params:scim:schemas:extension:syncope: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"),
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 d90b301fd9..b51b4858ec 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
@@ -29,16 +29,20 @@ import 
org.apache.cxf.jaxrs.spring.JAXRSServerFactoryBeanDefinitionParser.Spring
 import org.apache.cxf.jaxrs.validation.JAXRSBeanValidationInInterceptor;
 import org.apache.cxf.transport.common.gzip.GZIPInInterceptor;
 import org.apache.cxf.transport.common.gzip.GZIPOutInterceptor;
+import org.apache.syncope.core.logic.AnyObjectLogic;
 import org.apache.syncope.core.logic.GroupLogic;
 import org.apache.syncope.core.logic.SCIMDataBinder;
 import org.apache.syncope.core.logic.SCIMLogic;
 import org.apache.syncope.core.logic.UserLogic;
 import org.apache.syncope.core.logic.scim.SCIMConfManager;
+import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
+import org.apache.syncope.ext.scimv2.api.service.SCIMAnyObjectService;
 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.SCIMUserService;
+import org.apache.syncope.ext.scimv2.cxf.service.SCIMAnyObjectServiceImpl;
 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.SCIMUserServiceImpl;
@@ -75,6 +79,7 @@ public class SCIMv2RESTCXFContext {
             final SCIMService scimService,
             final SCIMGroupService scimv2GroupService,
             final SCIMUserService scimv2UserService,
+            final SCIMAnyObjectService scimv2AnyObjectService,
             final GZIPInInterceptor gzipInInterceptor,
             final GZIPOutInterceptor gzipOutInterceptor,
             final JAXRSBeanValidationInInterceptor validationInInterceptor,
@@ -91,7 +96,8 @@ public class SCIMv2RESTCXFContext {
 
         scimv2Container.setProperties(Map.of("convert.wadl.resources.to.dom", 
"false"));
 
-        scimv2Container.setServiceBeans(List.of(scimService, 
scimv2GroupService, scimv2UserService));
+        scimv2Container.setServiceBeans(
+                List.of(scimService, scimv2GroupService, scimv2UserService, 
scimv2AnyObjectService));
 
         scimv2Container.setInInterceptors(List.of(gzipInInterceptor, 
validationInInterceptor));
 
@@ -108,13 +114,16 @@ public class SCIMv2RESTCXFContext {
     public SCIMService scimService(
             final UserDAO userDAO,
             final GroupDAO groupDAO,
+            final AnyObjectDAO anyObjectDAO,
             final UserLogic userLogic,
             final GroupLogic groupLogic,
+            final AnyObjectLogic anyObjectLogic,
             final SCIMDataBinder binder,
             final SCIMConfManager confManager,
             final SCIMLogic scimLogic) {
 
-        return new SCIMServiceImpl(userDAO, groupDAO, userLogic, groupLogic, 
binder, confManager, scimLogic);
+        return new SCIMServiceImpl(userDAO, groupDAO, anyObjectDAO, userLogic, 
groupLogic, anyObjectLogic, binder,
+                confManager, scimLogic);
     }
 
     @ConditionalOnMissingBean
@@ -122,12 +131,15 @@ public class SCIMv2RESTCXFContext {
     public SCIMGroupService scimv2GroupService(
             final UserDAO userDAO,
             final GroupDAO groupDAO,
+            final AnyObjectDAO anyObjectDAO,
             final UserLogic userLogic,
             final GroupLogic groupLogic,
+            final AnyObjectLogic anyObjectLogic,
             final SCIMDataBinder binder,
             final SCIMConfManager confManager) {
 
-        return new SCIMGroupServiceImpl(userDAO, groupDAO, userLogic, 
groupLogic, binder, confManager);
+        return new SCIMGroupServiceImpl(userDAO, groupDAO, anyObjectDAO, 
userLogic, groupLogic, anyObjectLogic, binder,
+                confManager);
     }
 
     @ConditionalOnMissingBean
@@ -135,11 +147,30 @@ public class SCIMv2RESTCXFContext {
     public SCIMUserService scimv2UserService(
             final UserDAO userDAO,
             final GroupDAO groupDAO,
+            final AnyObjectDAO anyObjectDAO,
             final UserLogic userLogic,
             final GroupLogic groupLogic,
+            final AnyObjectLogic anyObjectLogic,
             final SCIMDataBinder binder,
             final SCIMConfManager confManager) {
 
-        return new SCIMUserServiceImpl(userDAO, groupDAO, userLogic, 
groupLogic, binder, confManager);
+        return new SCIMUserServiceImpl(userDAO, groupDAO, anyObjectDAO, 
userLogic, groupLogic, anyObjectLogic, binder,
+                confManager);
+    }
+
+    @ConditionalOnMissingBean
+    @Bean
+    public SCIMAnyObjectService scimv2AnyObjectService(
+            final UserDAO userDAO,
+            final GroupDAO groupDAO,
+            final AnyObjectDAO anyObjectDAO,
+            final UserLogic userLogic,
+            final GroupLogic groupLogic,
+            final AnyObjectLogic anyObjectLogic,
+            final SCIMDataBinder binder,
+            final SCIMConfManager confManager) {
+
+        return new SCIMAnyObjectServiceImpl(userDAO, groupDAO, anyObjectDAO, 
userLogic, groupLogic, anyObjectLogic,
+                binder, confManager);
     }
 }
diff --git 
a/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/AbstractSCIMService.java
 
b/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/AbstractSCIMService.java
index da3038c1b6..97b561c1d7 100644
--- 
a/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/AbstractSCIMService.java
+++ 
b/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/AbstractSCIMService.java
@@ -29,12 +29,14 @@ import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.cxf.jaxrs.ext.MessageContext;
 import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.to.AnyObjectTO;
 import org.apache.syncope.common.lib.to.AnyTO;
 import org.apache.syncope.common.lib.to.GroupTO;
 import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.syncope.common.rest.api.Preference;
 import org.apache.syncope.common.rest.api.RESTHeaders;
 import org.apache.syncope.core.logic.AbstractAnyLogic;
+import org.apache.syncope.core.logic.AnyObjectLogic;
 import org.apache.syncope.core.logic.GroupLogic;
 import org.apache.syncope.core.logic.SCIMDataBinder;
 import org.apache.syncope.core.logic.UserLogic;
@@ -42,10 +44,13 @@ import org.apache.syncope.core.logic.scim.SCIMConfManager;
 import org.apache.syncope.core.logic.scim.SearchCondConverter;
 import org.apache.syncope.core.logic.scim.SearchCondVisitor;
 import org.apache.syncope.core.persistence.api.dao.AnyDAO;
+import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.NotFoundException;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
+import org.apache.syncope.core.persistence.api.dao.search.AnyTypeCond;
 import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
+import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
 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.SCIMResource;
@@ -56,7 +61,7 @@ import org.apache.syncope.ext.scimv2.api.type.SortOrder;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-abstract class AbstractSCIMService<R extends SCIMResource> {
+public abstract class AbstractSCIMService<R extends SCIMResource> {
 
     protected static final Logger LOG = 
LoggerFactory.getLogger(AbstractSCIMService.class);
 
@@ -70,10 +75,14 @@ abstract class AbstractSCIMService<R extends SCIMResource> {
 
     protected final GroupDAO groupDAO;
 
+    protected final AnyObjectDAO anyObjectDAO;
+
     protected final UserLogic userLogic;
 
     protected final GroupLogic groupLogic;
 
+    protected final AnyObjectLogic anyObjectLogic;
+
     protected final SCIMDataBinder binder;
 
     protected final SCIMConfManager confManager;
@@ -81,42 +90,46 @@ abstract class AbstractSCIMService<R extends SCIMResource> {
     protected AbstractSCIMService(
             final UserDAO userDAO,
             final GroupDAO groupDAO,
+            final AnyObjectDAO anyObjectDAO,
             final UserLogic userLogic,
             final GroupLogic groupLogic,
+            final AnyObjectLogic anyObjectLogic,
             final SCIMDataBinder binder,
             final SCIMConfManager confManager) {
 
         this.userDAO = userDAO;
         this.groupDAO = groupDAO;
+        this.anyObjectDAO = anyObjectDAO;
         this.userLogic = userLogic;
         this.groupLogic = groupLogic;
+        this.anyObjectLogic = anyObjectLogic;
         this.binder = binder;
         this.confManager = confManager;
     }
 
-    protected AnyDAO<?> anyDAO(final Resource type) {
+    protected AnyDAO<?> anyDAO(final String type) {
         switch (type) {
-            case User:
+            case "urn:ietf:params:scim:schemas:core:2.0:User":
                 return userDAO;
 
-            case Group:
+            case "urn:ietf:params:scim:schemas:core:2.0:Group":
                 return groupDAO;
 
             default:
-                throw new UnsupportedOperationException();
+                return anyObjectDAO;
         }
     }
 
-    protected AbstractAnyLogic<?, ?, ?> anyLogic(final Resource type) {
+    protected AbstractAnyLogic<?, ?, ?> anyLogic(final String type) {
         switch (type) {
-            case User:
+            case "User":
                 return userLogic;
 
-            case Group:
+            case "Group":
                 return groupLogic;
 
             default:
-                throw new UnsupportedOperationException();
+                return anyObjectLogic;
         }
     }
 
@@ -153,7 +166,7 @@ abstract class AbstractSCIMService<R extends SCIMResource> {
 
     protected abstract SCIMResource getResource(String key);
 
-    protected ResponseBuilder checkETag(final Resource resource, final String 
key) {
+    protected ResponseBuilder checkETag(final String resource, final String 
key) {
         OffsetDateTime lastChange = anyDAO(resource).findLastChange(key);
         if (lastChange == null) {
             throw new NotFoundException("Resource" + key + " not found");
@@ -165,7 +178,7 @@ abstract class AbstractSCIMService<R extends SCIMResource> {
 
     @SuppressWarnings("unchecked")
     protected ListResponse<R> doSearch(
-            final Resource type,
+            final String type,
             final SCIMSearchRequest request) {
 
         if (type == null) {
@@ -206,10 +219,24 @@ abstract class AbstractSCIMService<R extends 
SCIMResource> {
             sort = List.of(clause);
         }
 
+        SearchCond searchCond = null;
+        String filter = request.getFilter();
+        if (!Resource.Group.name().equals(type) && 
!Resource.User.name().equals(type)) {
+            AnyTypeCond cond = new AnyTypeCond();
+            cond.setAnyTypeKey(type);
+            searchCond = SearchCond.getLeaf(cond);
+            filter = filter.replaceAll(
+                    
"(\\s*(and|or)\\s+)?type\\s+eq\\s+\"[^\"]*\"(\\s*(and|or)\\s+)?", " ")
+                    .trim().replaceAll("\\s{2,}", " ");
+        }
+        if (StringUtils.isNotBlank(filter)) {
+            SearchCond filterCond = SearchCondConverter.convert(visitor, 
filter);
+            searchCond = (searchCond == null)
+                    ? filterCond
+                    : SearchCond.getAnd(filterCond, searchCond);
+        }
         Pair<Integer, ? extends List<? extends AnyTO>> result = 
anyLogic(type).search(
-                StringUtils.isBlank(request.getFilter())
-                ? null
-                : SearchCondConverter.convert(visitor, request.getFilter()),
+                searchCond,
                 page,
                 itemsPerPage,
                 sort,
@@ -237,6 +264,12 @@ abstract class AbstractSCIMService<R extends SCIMResource> 
{
                         
uriInfo.getAbsolutePathBuilder().path(anyTO.getKey()).build().toASCIIString(),
                         request.getAttributes(),
                         request.getExcludedAttributes());
+            } else if (anyTO instanceof AnyObjectTO) {
+                resource = binder.toSCIMAnyObject(
+                        (AnyObjectTO) anyTO,
+                        
uriInfo.getAbsolutePathBuilder().path(anyTO.getKey()).build().toASCIIString(),
+                        request.getAttributes(),
+                        request.getExcludedAttributes());
             }
 
             if (resource != null) {
diff --git 
a/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMUserServiceImpl.java
 
b/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMAnyObjectServiceImpl.java
similarity index 57%
copy from 
ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMUserServiceImpl.java
copy to 
ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMAnyObjectServiceImpl.java
index e5e9ec9d38..e226becc50 100644
--- 
a/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMUserServiceImpl.java
+++ 
b/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMAnyObjectServiceImpl.java
@@ -19,86 +19,114 @@
 package org.apache.syncope.ext.scimv2.cxf.service;
 
 import java.util.List;
-import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 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.request.AnyObjectUR;
+import org.apache.syncope.common.lib.to.AnyObjectTO;
 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.AnyObjectLogic;
 import org.apache.syncope.core.logic.GroupLogic;
 import org.apache.syncope.core.logic.SCIMDataBinder;
 import org.apache.syncope.core.logic.UserLogic;
 import org.apache.syncope.core.logic.scim.SCIMConfManager;
+import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.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.SCIMAnyObject;
 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.data.SCIMUser;
-import org.apache.syncope.ext.scimv2.api.service.SCIMUserService;
+import org.apache.syncope.ext.scimv2.api.service.SCIMAnyObjectService;
 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 SCIMUserServiceImpl extends AbstractSCIMService<SCIMUser> 
implements SCIMUserService {
+public class SCIMAnyObjectServiceImpl extends 
AbstractSCIMService<SCIMAnyObject> implements SCIMAnyObjectService {
 
-    public SCIMUserServiceImpl(
+    public SCIMAnyObjectServiceImpl(
             final UserDAO userDAO,
             final GroupDAO groupDAO,
+            final AnyObjectDAO anyObjectDAO,
             final UserLogic userLogic,
             final GroupLogic groupLogic,
+            final AnyObjectLogic anyObjectLogic,
             final SCIMDataBinder binder,
             final SCIMConfManager confManager) {
 
-        super(userDAO, groupDAO, userLogic, groupLogic, binder, confManager);
+        super(userDAO, groupDAO, anyObjectDAO, userLogic, groupLogic, 
anyObjectLogic, binder, confManager);
     }
 
     @Override
-    public Response create(final SCIMUser user) {
-        ProvisioningResult<UserTO> result = 
userLogic.create(binder.toUserCR(user), false);
+    public SCIMAnyObject get(final String id, final String attributes, final 
String excludedAttributes) {
+        return binder.toSCIMAnyObject(
+                anyObjectLogic.read(id),
+                uriInfo.getAbsolutePathBuilder().build().toASCIIString(),
+                List.of(ArrayUtils.nullToEmpty(StringUtils.split(attributes, 
','))),
+                
List.of(ArrayUtils.nullToEmpty(StringUtils.split(excludedAttributes, ','))));
+    }
+
+    @Override
+    public ListResponse<SCIMAnyObject> search(
+            final String attributes, final String excludedAttributes, final 
String filter, final String sortBy,
+            final SortOrder sortOrder, final Integer startIndex, final Integer 
count) {
+        
+        SCIMSearchRequest request = new SCIMSearchRequest(filter, sortBy, 
sortOrder, startIndex, count);
+        if (attributes != null) {
+            request.getAttributes().addAll(
+                    
List.of(ArrayUtils.nullToEmpty(StringUtils.split(attributes, ','))));
+        }
+        if (excludedAttributes != null) {
+            request.getExcludedAttributes().addAll(
+                    
List.of(ArrayUtils.nullToEmpty(StringUtils.split(excludedAttributes, ','))));
+        }
+
+        Matcher matcher = 
Pattern.compile("type\\s+eq\\s+\"(.*?)\"").matcher(filter);
+        if (matcher.find()) {
+            return doSearch(matcher.group(1), request);
+        } else {
+            throw new UnsupportedOperationException("Need to specify type");
+        }
+    }
+
+    @Override
+    public ListResponse<SCIMAnyObject> search(final SCIMSearchRequest request) 
{
+        Matcher matcher = 
Pattern.compile("type\\s+eq\\s+\"(.*?)\"").matcher(request.getFilter());
+        if (matcher.find()) {
+            return doSearch(matcher.group(1), request);
+        } else {
+            throw new UnsupportedOperationException("Need to specify type");
+        }
+    }
+
+    @Override
+    public Response create(final SCIMAnyObject anyObject) {
+        ProvisioningResult<AnyObjectTO> result = 
anyObjectLogic.create(binder.toAnyObjectCR(anyObject), false);
         return createResponse(
                 result.getEntity().getKey(),
-                binder.toSCIMUser(
+                binder.toSCIMAnyObject(
                         result.getEntity(),
                         
uriInfo.getAbsolutePathBuilder().path(result.getEntity().getKey()).build().toASCIIString(),
                         List.of(),
                         List.of()));
     }
 
-    @Override
-    public SCIMUser get(final String id,
-            final String attributes,
-            final String excludedAttributes) {
-
-        return binder.toSCIMUser(
-                userLogic.read(id),
-                uriInfo.getAbsolutePathBuilder().build().toASCIIString(),
-                List.of(ArrayUtils.nullToEmpty(StringUtils.split(attributes, 
','))),
-                
List.of(ArrayUtils.nullToEmpty(StringUtils.split(excludedAttributes, ','))));
-    }
-
     @Override
     public Response update(final String id, final SCIMPatchOp patch) {
-        ResponseBuilder builder = checkETag(Resource.User, id);
+        SCIMResource resource = getResource(id);
+        Response.ResponseBuilder builder = checkETag(
+                "urn:ietf:params:scim:schemas:extension:syncope:2.0:" + 
resource.getSchemas().get(0), id);
         if (builder != null) {
             return builder.build();
         }
 
         patch.getOperations().forEach(op -> {
-            Pair<UserUR, StatusR> update = binder.toUserUpdate(
-                    userLogic.read(id),
-                    userDAO.findAllResourceKeys(id),
-                    op);
-            userLogic.update(update.getLeft(), false);
-            Optional.ofNullable(update.getRight()).ifPresent(statusR -> 
userLogic.status(statusR, false));
+            AnyObjectUR update = binder.toAnyObjectUR(anyObjectLogic.read(id), 
op);
+            anyObjectLogic.update(update, false);
         });
 
         return updateResponse(
@@ -108,37 +136,29 @@ public class SCIMUserServiceImpl extends 
AbstractSCIMService<SCIMUser> implement
     }
 
     @Override
-    public Response replace(final String id, final SCIMUser user) {
-        if (!id.equals(user.getId())) {
-            throw new BadRequestException(ErrorType.invalidPath, "Expected " + 
id + ", found " + user.getId());
+    public Response replace(final String id, final SCIMAnyObject anyObject) {
+        if (!id.equals(anyObject.getId())) {
+            throw new BadRequestException(ErrorType.invalidPath, "Expected " + 
id + ", found " + anyObject.getId());
         }
 
-        ResponseBuilder builder = checkETag(Resource.User, id);
+        SCIMResource resource = getResource(id);
+        Response.ResponseBuilder builder = checkETag(
+                "urn:ietf:params:scim:schemas:extension:syncope:2.0:" + 
resource.getSchemas().get(0), id);
         if (builder != null) {
             return builder.build();
         }
 
-        UserTO before = userLogic.read(id);
+        AnyObjectTO before = anyObjectLogic.read(id);
 
-        UserUR req = AnyOperations.diff(binder.toUserTO(user, true), before, 
false);
+        AnyObjectUR req = AnyOperations.diff(binder.toAnyObjectTO(anyObject, 
true), before, false);
         req.getResources().clear();
         req.getAuxClasses().clear();
         req.getRelationships().clear();
-        req.getRoles().clear();
-        req.getLinkedAccounts().clear();
-        ProvisioningResult<UserTO> result = userLogic.update(req, false);
-
-        if (before.isSuspended() == user.isActive()) {
-            StatusR statusR = new StatusR.Builder(
-                    before.getKey(),
-                    user.isActive() ? StatusRType.REACTIVATE : 
StatusRType.SUSPEND).
-                    build();
-            userLogic.status(statusR, false);
-        }
+        ProvisioningResult<AnyObjectTO> result = anyObjectLogic.update(req, 
false);
 
         return updateResponse(
                 result.getEntity().getKey(),
-                binder.toSCIMUser(
+                binder.toSCIMAnyObject(
                         result.getEntity(),
                         
uriInfo.getAbsolutePathBuilder().path(result.getEntity().getKey()).build().toASCIIString(),
                         List.of(),
@@ -146,51 +166,25 @@ public class SCIMUserServiceImpl extends 
AbstractSCIMService<SCIMUser> implement
                 false);
     }
 
-    @Override
-    protected SCIMResource getResource(final String key) {
-        return binder.toSCIMUser(
-                userLogic.read(key),
-                
uriInfo.getAbsolutePathBuilder().path(key).build().toASCIIString(),
-                List.of(),
-                List.of());
-    }
-
     @Override
     public Response delete(final String id) {
-        ResponseBuilder builder = checkETag(Resource.User, id);
+        SCIMResource resource = getResource(id);
+        Response.ResponseBuilder builder = checkETag(
+                "urn:ietf:params:scim:schemas:extension:syncope:2.0:" + 
resource.getSchemas().get(0), id);
         if (builder != null) {
             return builder.build();
         }
 
-        anyLogic(Resource.User).delete(id, false);
+        anyObjectLogic.delete(id, false);
         return Response.noContent().build();
     }
 
     @Override
-    public ListResponse<SCIMUser> search(
-            final String attributes,
-            final String excludedAttributes,
-            final String filter,
-            final String sortBy,
-            final SortOrder sortOrder,
-            final Integer startIndex,
-            final Integer count) {
-
-        SCIMSearchRequest request = new SCIMSearchRequest(filter, sortBy, 
sortOrder, startIndex, count);
-        if (attributes != null) {
-            request.getAttributes().addAll(
-                    
List.of(ArrayUtils.nullToEmpty(StringUtils.split(attributes, ','))));
-        }
-        if (excludedAttributes != null) {
-            request.getExcludedAttributes().addAll(
-                    
List.of(ArrayUtils.nullToEmpty(StringUtils.split(excludedAttributes, ','))));
-        }
-
-        return doSearch(Resource.User, request);
-    }
-
-    @Override
-    public ListResponse<SCIMUser> search(final SCIMSearchRequest request) {
-        return doSearch(Resource.User, request);
+    protected SCIMResource getResource(final String key) {
+        return binder.toSCIMAnyObject(
+                anyObjectLogic.read(key),
+                
uriInfo.getAbsolutePathBuilder().path(key).build().toASCIIString(),
+                List.of(),
+                List.of());
     }
 }
diff --git 
a/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMGroupServiceImpl.java
 
b/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMGroupServiceImpl.java
index 04e1777742..f4cd1968c2 100644
--- 
a/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMGroupServiceImpl.java
+++ 
b/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMGroupServiceImpl.java
@@ -38,11 +38,13 @@ import org.apache.syncope.common.lib.to.GroupTO;
 import org.apache.syncope.common.lib.to.ProvisioningResult;
 import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.syncope.common.lib.types.PatchOperation;
+import org.apache.syncope.core.logic.AnyObjectLogic;
 import org.apache.syncope.core.logic.GroupLogic;
 import org.apache.syncope.core.logic.SCIMDataBinder;
 import org.apache.syncope.core.logic.UserLogic;
 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.AnyObjectDAO;
 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;
@@ -67,12 +69,14 @@ public class SCIMGroupServiceImpl extends 
AbstractSCIMService<SCIMGroup> impleme
     public SCIMGroupServiceImpl(
             final UserDAO userDAO,
             final GroupDAO groupDAO,
+            final AnyObjectDAO anyObjectDAO,
             final UserLogic userLogic,
             final GroupLogic groupLogic,
+            final AnyObjectLogic anyObjectLogic,
             final SCIMDataBinder binder,
             final SCIMConfManager confManager) {
 
-        super(userDAO, groupDAO, userLogic, groupLogic, binder, confManager);
+        super(userDAO, groupDAO, anyObjectDAO, userLogic, groupLogic, 
anyObjectLogic, binder, confManager);
     }
 
     private void changeMembership(final String user, final String group, final 
PatchOp patchOp) {
@@ -142,7 +146,7 @@ public class SCIMGroupServiceImpl extends 
AbstractSCIMService<SCIMGroup> impleme
 
     @Override
     public Response update(final String id, final SCIMPatchOp patch) {
-        ResponseBuilder builder = checkETag(Resource.Group, id);
+        ResponseBuilder builder = checkETag(Resource.Group.schema(), id);
         if (builder != null) {
             return builder.build();
         }
@@ -177,7 +181,7 @@ public class SCIMGroupServiceImpl extends 
AbstractSCIMService<SCIMGroup> impleme
             throw new BadRequestException(ErrorType.invalidPath, "Expected " + 
id + ", found " + group.getId());
         }
 
-        ResponseBuilder builder = checkETag(Resource.Group, id);
+        ResponseBuilder builder = checkETag(Resource.Group.schema(), id);
         if (builder != null) {
             return builder.build();
         }
@@ -225,12 +229,12 @@ public class SCIMGroupServiceImpl extends 
AbstractSCIMService<SCIMGroup> impleme
 
     @Override
     public Response delete(final String id) {
-        ResponseBuilder builder = checkETag(Resource.Group, id);
+        ResponseBuilder builder = checkETag(Resource.Group.schema(), id);
         if (builder != null) {
             return builder.build();
         }
 
-        anyLogic(Resource.Group).delete(id, false);
+        anyLogic(Resource.Group.name()).delete(id, false);
         return Response.noContent().build();
     }
 
@@ -254,11 +258,11 @@ public class SCIMGroupServiceImpl extends 
AbstractSCIMService<SCIMGroup> impleme
                     
List.of(ArrayUtils.nullToEmpty(StringUtils.split(excludedAttributes, ','))));
         }
 
-        return doSearch(Resource.Group, request);
+        return doSearch(Resource.Group.name(), request);
     }
 
     @Override
     public ListResponse<SCIMGroup> search(final SCIMSearchRequest request) {
-        return doSearch(Resource.Group, request);
+        return doSearch(Resource.Group.name(), request);
     }
 }
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 2d124aaae5..0b1a868f8e 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
@@ -20,11 +20,13 @@ package org.apache.syncope.ext.scimv2.cxf.service;
 
 import java.util.List;
 import javax.ws.rs.core.Response;
+import org.apache.syncope.core.logic.AnyObjectLogic;
 import org.apache.syncope.core.logic.GroupLogic;
 import org.apache.syncope.core.logic.SCIMDataBinder;
 import org.apache.syncope.core.logic.SCIMLogic;
 import org.apache.syncope.core.logic.UserLogic;
 import org.apache.syncope.core.logic.scim.SCIMConfManager;
+import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
 import org.apache.syncope.ext.scimv2.api.data.ResourceType;
@@ -39,13 +41,15 @@ public class SCIMServiceImpl extends 
AbstractSCIMService<SCIMResource> implement
     public SCIMServiceImpl(
             final UserDAO userDAO,
             final GroupDAO groupDAO,
+            final AnyObjectDAO anyObjectDAO,
             final UserLogic userLogic,
             final GroupLogic groupLogic,
+            final AnyObjectLogic anyObjectLogic,
             final SCIMDataBinder binder,
             final SCIMConfManager confManager,
             final SCIMLogic scimLogic) {
 
-        super(userDAO, groupDAO, userLogic, groupLogic, binder, confManager);
+        super(userDAO, groupDAO, anyObjectDAO, userLogic, groupLogic, 
anyObjectLogic, binder, confManager);
         this.scimLogic = scimLogic;
     }
 
diff --git 
a/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMUserServiceImpl.java
 
b/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMUserServiceImpl.java
index e5e9ec9d38..52810c2caa 100644
--- 
a/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMUserServiceImpl.java
+++ 
b/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMUserServiceImpl.java
@@ -31,10 +31,12 @@ 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.AnyObjectLogic;
 import org.apache.syncope.core.logic.GroupLogic;
 import org.apache.syncope.core.logic.SCIMDataBinder;
 import org.apache.syncope.core.logic.UserLogic;
 import org.apache.syncope.core.logic.scim.SCIMConfManager;
+import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
 import org.apache.syncope.ext.scimv2.api.BadRequestException;
@@ -53,12 +55,14 @@ public class SCIMUserServiceImpl extends 
AbstractSCIMService<SCIMUser> implement
     public SCIMUserServiceImpl(
             final UserDAO userDAO,
             final GroupDAO groupDAO,
+            final AnyObjectDAO anyObjectDAO,
             final UserLogic userLogic,
             final GroupLogic groupLogic,
+            final AnyObjectLogic anyObjectLogic,
             final SCIMDataBinder binder,
             final SCIMConfManager confManager) {
 
-        super(userDAO, groupDAO, userLogic, groupLogic, binder, confManager);
+        super(userDAO, groupDAO, anyObjectDAO, userLogic, groupLogic, 
anyObjectLogic, binder, confManager);
     }
 
     @Override
@@ -87,7 +91,7 @@ public class SCIMUserServiceImpl extends 
AbstractSCIMService<SCIMUser> implement
 
     @Override
     public Response update(final String id, final SCIMPatchOp patch) {
-        ResponseBuilder builder = checkETag(Resource.User, id);
+        ResponseBuilder builder = checkETag(Resource.User.schema(), id);
         if (builder != null) {
             return builder.build();
         }
@@ -113,7 +117,7 @@ public class SCIMUserServiceImpl extends 
AbstractSCIMService<SCIMUser> implement
             throw new BadRequestException(ErrorType.invalidPath, "Expected " + 
id + ", found " + user.getId());
         }
 
-        ResponseBuilder builder = checkETag(Resource.User, id);
+        ResponseBuilder builder = checkETag(Resource.User.schema(), id);
         if (builder != null) {
             return builder.build();
         }
@@ -157,12 +161,12 @@ public class SCIMUserServiceImpl extends 
AbstractSCIMService<SCIMUser> implement
 
     @Override
     public Response delete(final String id) {
-        ResponseBuilder builder = checkETag(Resource.User, id);
+        ResponseBuilder builder = checkETag(Resource.User.schema(), id);
         if (builder != null) {
             return builder.build();
         }
 
-        anyLogic(Resource.User).delete(id, false);
+        anyLogic(Resource.User.name()).delete(id, false);
         return Response.noContent().build();
     }
 
@@ -186,11 +190,11 @@ public class SCIMUserServiceImpl extends 
AbstractSCIMService<SCIMUser> implement
                     
List.of(ArrayUtils.nullToEmpty(StringUtils.split(excludedAttributes, ','))));
         }
 
-        return doSearch(Resource.User, request);
+        return doSearch(Resource.User.name(), request);
     }
 
     @Override
     public ListResponse<SCIMUser> search(final SCIMSearchRequest request) {
-        return doSearch(Resource.User, request);
+        return doSearch(Resource.User.name(), request);
     }
 }
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 c9c705db93..2b7b9a2d9c 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
@@ -46,18 +46,21 @@ import javax.ws.rs.core.Response;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.cxf.jaxrs.client.WebClient;
 import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.request.AnyObjectUR;
 import org.apache.syncope.common.lib.request.GroupUR;
 import org.apache.syncope.common.lib.request.StringPatchItem;
 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.SCIMExtensionUserConf;
+import org.apache.syncope.common.lib.scim.SCIMExtensionAnyConf;
+import org.apache.syncope.common.lib.scim.SCIMExtensionAnyObjectConf;
 import org.apache.syncope.common.lib.scim.SCIMGroupConf;
 import org.apache.syncope.common.lib.scim.SCIMItem;
 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;
+import org.apache.syncope.common.lib.to.AnyObjectTO;
 import org.apache.syncope.common.lib.to.GroupTO;
 import org.apache.syncope.common.lib.to.ProvisioningResult;
 import org.apache.syncope.common.lib.to.UserTO;
@@ -70,6 +73,7 @@ import org.apache.syncope.ext.scimv2.api.data.Group;
 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.ResourceType;
+import org.apache.syncope.ext.scimv2.api.data.SCIMAnyObject;
 import org.apache.syncope.ext.scimv2.api.data.SCIMComplexValue;
 import org.apache.syncope.ext.scimv2.api.data.SCIMError;
 import org.apache.syncope.ext.scimv2.api.data.SCIMExtensionInfo;
@@ -118,6 +122,20 @@ public class SCIMITCase extends AbstractITCase {
         email.setValue("email");
         email.setType(EmailCanonicalType.home);
         CONF.getUserConf().getEmails().add(email);
+
+        SCIMExtensionAnyObjectConf printerConf = new 
SCIMExtensionAnyObjectConf();
+        printerConf.setType(PRINTER);
+        printerConf.setName("syncope");
+        printerConf.setDescription("syncope printer");
+        SCIMItem model = new SCIMItem();
+        model.setIntAttrName("model");
+        model.setExtAttrName("model");
+        SCIMItem location = new SCIMItem();
+        location.setIntAttrName("location");
+        location.setExtAttrName("location");
+        printerConf.add(model);
+        printerConf.add(location);
+        CONF.getExtensionAnyObjectsConf().add(printerConf);
     }
 
     private static SCIMUser getSampleUser(final String username, final 
List<String> schemas) {
@@ -143,6 +161,13 @@ public class SCIMITCase extends AbstractITCase {
         return user;
     }
 
+    private static SCIMGroup getSampleGroup(final String name, final 
List<String> schemas) {
+        SCIMGroup group = new SCIMGroup(null, schemas, null, name);
+        group.setDisplayName(name);
+
+        return group;
+    }
+
     @BeforeAll
     public static void isSCIMAvailable() {
         if (ENABLED == null) {
@@ -209,7 +234,7 @@ public class SCIMITCase extends AbstractITCase {
 
     @Test
     public void schemas() {
-        SCIMExtensionUserConf extensionUserConf = new SCIMExtensionUserConf();
+        SCIMExtensionAnyConf extensionUserConf = new SCIMExtensionAnyConf();
         extensionUserConf.setName("syncope");
         extensionUserConf.setDescription("syncope user");
         SCIMItem scimItem = new SCIMItem();
@@ -217,6 +242,16 @@ public class SCIMITCase extends AbstractITCase {
         scimItem.setExtAttrName("gender");
         extensionUserConf.add(scimItem);
         CONF.setExtensionUserConf(extensionUserConf);
+
+        SCIMExtensionAnyConf extensionGroupConf = new SCIMExtensionAnyConf();
+        extensionGroupConf.setName("syncope");
+        extensionGroupConf.setDescription("syncope group");
+        SCIMItem scimItemGroup = new SCIMItem();
+        scimItemGroup.setIntAttrName("originalName");
+        scimItemGroup.setExtAttrName("originalName");
+        scimItemGroup.setUniqueness(true);
+        extensionGroupConf.add(scimItemGroup);
+        CONF.setExtensionGroupConf(extensionGroupConf);
         SCIM_CONF_SERVICE.set(CONF);
 
         Response response = webClient().path("Schemas").get();
@@ -227,7 +262,7 @@ public class SCIMITCase extends AbstractITCase {
 
         ArrayNode schemas = response.readEntity(ArrayNode.class);
         assertNotNull(schemas);
-        assertEquals(4, schemas.size());
+        assertEquals(6, schemas.size());
 
         response = webClient().path("Schemas").path("none").get();
         assertEquals(Response.Status.NOT_FOUND.getStatusCode(), 
response.getStatus());
@@ -246,7 +281,23 @@ public class SCIMITCase extends AbstractITCase {
         assertNotNull(extensionUser);
         assertEquals(Resource.ExtensionUser.schema(), 
extensionUser.get("id").textValue());
 
+        response = 
webClient().path("Schemas").path(Resource.ExtensionGroup.schema()).get();
+        assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+
+        ObjectNode extensionGroup = response.readEntity(ObjectNode.class);
+        assertNotNull(extensionGroup);
+        assertEquals(Resource.ExtensionGroup.schema(), 
extensionGroup.get("id").textValue());
+
+        response = 
webClient().path("Schemas").path("urn:ietf:params:scim:schemas:extension:syncope:2.0:PRINTER").get();
+        assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+
+        ObjectNode extensionPrinter = response.readEntity(ObjectNode.class);
+        assertNotNull(extensionPrinter);
+        
assertEquals("urn:ietf:params:scim:schemas:extension:syncope:2.0:PRINTER",
+                extensionPrinter.get("id").textValue());
+
         CONF.setExtensionUserConf(null);
+        CONF.setExtensionGroupConf(null);
         SCIM_CONF_SERVICE.set(CONF);
     }
 
@@ -306,7 +357,7 @@ public class SCIMITCase extends AbstractITCase {
 
     @Test
     void invalidConf() {
-        SCIMExtensionUserConf extensionUserConf = new SCIMExtensionUserConf();
+        SCIMExtensionAnyConf extensionUserConf = new SCIMExtensionAnyConf();
         extensionUserConf.setName("syncope");
         extensionUserConf.setDescription("syncope user");
         SCIMItem scimItem = new SCIMItem();
@@ -456,13 +507,13 @@ public class SCIMITCase extends AbstractITCase {
         assertEquals(newUser.getUsername(), newSCIMUser.getUserName());
 
         SCIMEnterpriseUserConf beforeEntConf = CONF.getEnterpriseUserConf();
-        SCIMExtensionUserConf beforeExtConf = CONF.getExtensionUserConf();
+        SCIMExtensionAnyConf beforeExtConf = CONF.getExtensionUserConf();
         try {
             SCIMEnterpriseUserConf entConf = new SCIMEnterpriseUserConf();
             entConf.setOrganization("userId");
             CONF.setEnterpriseUserConf(entConf);
 
-            SCIMExtensionUserConf extConf = new SCIMExtensionUserConf();
+            SCIMExtensionAnyConf extConf = new SCIMExtensionAnyConf();
             SCIMItem item = new SCIMItem();
             item.setIntAttrName("email");
             item.setExtAttrName("email");
@@ -501,6 +552,28 @@ public class SCIMITCase extends AbstractITCase {
             });
             assertNotNull(users);
             assertEquals(1, users.getTotalResults());
+
+            // PRINTER
+            response = webClient().path("AnyObjects").query(
+                    "filter",
+                    
"urn:ietf:params:scim:schemas:extension:syncope:2.0:PRINTER:model eq \"Canon 
MFC8030\"").
+                    get();
+            
assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
response.getStatus());
+
+            response = webClient().path("AnyObjects").query(
+                    "filter",
+                    
"urn:ietf:params:scim:schemas:extension:syncope:2.0:PRINTER:model eq \"Canon 
MFC8030\" "
+                            + "and type eq \"PRINTER\"").
+                    get();
+            assertEquals(Response.Status.OK.getStatusCode(), 
response.getStatus());
+            assertEquals(
+                    SCIMConstants.APPLICATION_SCIM_JSON,
+                    
StringUtils.substringBefore(response.getHeaderString(HttpHeaders.CONTENT_TYPE), 
";"));
+
+            ListResponse<SCIMAnyObject> printers = response.readEntity(new 
GenericType<>() {
+            });
+            assertNotNull(printers);
+            assertEquals(1, printers.getTotalResults());
         } finally {
             CONF.setEnterpriseUserConf(beforeEntConf);
             CONF.setExtensionUserConf(beforeExtConf);
@@ -538,7 +611,7 @@ public class SCIMITCase extends AbstractITCase {
 
     @Test
     void crudExtensionUser() {
-        SCIMExtensionUserConf extensionUserConf = new SCIMExtensionUserConf();
+        SCIMExtensionAnyConf extensionUserConf = new SCIMExtensionAnyConf();
         extensionUserConf.setName("syncope");
         extensionUserConf.setDescription("syncope user");
         SCIMItem scimItem = new SCIMItem();
@@ -792,7 +865,7 @@ public class SCIMITCase extends AbstractITCase {
     public void createGroup() {
         String displayName = UUID.randomUUID().toString();
 
-        SCIMGroup group = new SCIMGroup(null, null, displayName);
+        SCIMGroup group = new SCIMGroup(null, 
List.of(Resource.Group.schema()), null, displayName);
         group.getMembers().add(new 
Member("1417acbe-cbf6-4277-9372-e75e04f97000", null, null));
         assertNull(group.getId());
         assertEquals(displayName, group.getDisplayName());
@@ -820,11 +893,51 @@ public class SCIMITCase extends AbstractITCase {
         assertEquals(ErrorType.uniqueness, error.getScimType());
     }
 
+    @Test
+    void crudExtensionGroup() {
+        SCIMExtensionAnyConf extensionGroupConf = new SCIMExtensionAnyConf();
+        extensionGroupConf.setName("syncope");
+        extensionGroupConf.setDescription("syncope group");
+        SCIMItem scimItemGroup = new SCIMItem();
+        scimItemGroup.setIntAttrName("originalName");
+        scimItemGroup.setExtAttrName("originalName");
+        scimItemGroup.setUniqueness(true);
+        extensionGroupConf.add(scimItemGroup);
+        CONF.setExtensionGroupConf(extensionGroupConf);
+        SCIM_CONF_SERVICE.set(CONF);
+
+        SCIMGroup group = getSampleGroup(
+                UUID.randomUUID().toString(), List.of(Resource.Group.schema(), 
Resource.ExtensionGroup.schema()));
+        SCIMExtensionInfo scimExtensionInfo = new SCIMExtensionInfo();
+        scimExtensionInfo.getAttributes().put("originalName", "originalName");
+        group.setExtensionInfo(scimExtensionInfo);
+
+        Response response = webClient().path("Groups").post(group);
+        assertEquals(Response.Status.CREATED.getStatusCode(), 
response.getStatus());
+
+        group = response.readEntity(SCIMGroup.class);
+        assertNotNull(group.getId());
+        
assertTrue(response.getLocation().toASCIIString().endsWith(group.getId()));
+
+        GroupTO groupTO = GROUP_SERVICE.read(group.getId());
+        assertEquals(group.getDisplayName(), groupTO.getName());
+        
assertEquals(group.getExtensionInfo().getAttributes().get("originalName"),
+                groupTO.getPlainAttr("originalName").get().getValues().get(0));
+
+        response = webClient().path("Groups").path(group.getId()).delete();
+        assertEquals(Response.Status.NO_CONTENT.getStatusCode(), 
response.getStatus());
+
+        response = webClient().path("Groups").path(group.getId()).get();
+        assertEquals(Response.Status.NOT_FOUND.getStatusCode(), 
response.getStatus());
+        CONF.setExtensionGroupConf(null);
+        SCIM_CONF_SERVICE.set(CONF);
+    }
+
     @Test
     public void updateGroup() {
         SCIM_CONF_SERVICE.set(CONF);
 
-        SCIMGroup group = new SCIMGroup(null, null, 
UUID.randomUUID().toString());
+        SCIMGroup group = new SCIMGroup(null, 
List.of(Resource.Group.schema()), 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);
@@ -922,7 +1035,7 @@ public class SCIMITCase extends AbstractITCase {
 
     @Test
     public void replaceGroup() {
-        SCIMGroup group = new SCIMGroup(null, null, 
UUID.randomUUID().toString());
+        SCIMGroup group = new SCIMGroup(null, 
List.of(Resource.Group.schema()), null, UUID.randomUUID().toString());
         group.getMembers().add(new 
Member("b3cbc78d-32e6-4bd4-92e0-bbe07566a2ee", null, null));
         Response response = webClient().path("Groups").post(group);
         assertEquals(Response.Status.CREATED.getStatusCode(), 
response.getStatus());
@@ -968,7 +1081,7 @@ public class SCIMITCase extends AbstractITCase {
 
     @Test
     public void deleteGroup() {
-        SCIMGroup group = new SCIMGroup(null, null, 
UUID.randomUUID().toString());
+        SCIMGroup group = new SCIMGroup(null, 
List.of(Resource.Group.schema()), null, UUID.randomUUID().toString());
         Response response = webClient().path("Groups").post(group);
         assertEquals(Response.Status.CREATED.getStatusCode(), 
response.getStatus());
 
@@ -984,4 +1097,133 @@ public class SCIMITCase extends AbstractITCase {
         response = webClient().path("Groups").path(group.getId()).get();
         assertEquals(Response.Status.NOT_FOUND.getStatusCode(), 
response.getStatus());
     }
+
+    @Test
+    public void createPrinter() {
+        SCIM_CONF_SERVICE.set(CONF);
+
+        String displayName = UUID.randomUUID().toString();
+
+        SCIMAnyObject printer = new SCIMAnyObject(
+                null, 
List.of("urn:ietf:params:scim:schemas:extension:syncope:2.0:PRINTER"), null, 
displayName);
+        SCIMExtensionInfo scimExtensionInfo = new SCIMExtensionInfo();
+        scimExtensionInfo.getAttributes().put("model", "HP Inspire 7224e");
+        printer.setExtensionInfo(scimExtensionInfo);
+        assertNull(printer.getId());
+        assertEquals(displayName, printer.getDisplayName());
+
+        Response response = webClient().path("AnyObjects").post(printer);
+        assertEquals(Response.Status.CREATED.getStatusCode(), 
response.getStatus());
+
+        printer = response.readEntity(SCIMAnyObject.class);
+        assertNotNull(printer.getId());
+        
assertTrue(response.getLocation().toASCIIString().endsWith(printer.getId()));
+        assertEquals(displayName, printer.getDisplayName());
+        
assertTrue(printer.getExtensionInfo().getAttributes().containsKey("model"));
+        
assertTrue(printer.getExtensionInfo().getAttributes().containsValue("HP Inspire 
7224e"));
+
+        response = webClient().path("AnyObjects").post(printer);
+        assertEquals(Response.Status.CONFLICT.getStatusCode(), 
response.getStatus());
+
+        SCIMError error = response.readEntity(SCIMError.class);
+        assertEquals(Response.Status.CONFLICT.getStatusCode(), 
error.getStatus());
+        assertEquals(ErrorType.uniqueness, error.getScimType());
+    }
+
+    @Test
+    public void updatePrinter() {
+        SCIM_CONF_SERVICE.set(CONF);
+
+        SCIMAnyObject printer = new SCIMAnyObject(
+                null,
+                
List.of("urn:ietf:params:scim:schemas:extension:syncope:2.0:PRINTER"),
+                null,
+                UUID.randomUUID().toString());
+        Response response = webClient().path("AnyObjects").post(printer);
+        assertEquals(Response.Status.CREATED.getStatusCode(), 
response.getStatus());
+
+        printer = response.readEntity(SCIMAnyObject.class);
+        assertNotNull(printer.getId());
+        assertNotNull(printer.getDisplayName());
+
+        // update with path, add value
+        String body =
+                "{"
+                        + 
"\"schemas\":[\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"],"
+                        + "\"Operations\":[{"
+                        + "\"op\":\"Add\","
+                        + "\"path\":\"displayName\","
+                        + "\"value\":\"" + printer.getId() + "\""
+                        + "}]"
+                        + "}";
+        response = 
webClient().path("AnyObjects").path(printer.getId()).invoke(HttpMethod.PATCH, 
body);
+        assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+
+        printer = response.readEntity(SCIMAnyObject.class);
+        assertEquals(printer.getId(), printer.getDisplayName());
+    }
+
+    @Test
+    public void replacePrinter() {
+        SCIM_CONF_SERVICE.set(CONF);
+
+        SCIMAnyObject printer = new SCIMAnyObject(
+                null,
+                
List.of("urn:ietf:params:scim:schemas:extension:syncope:2.0:PRINTER"),
+                null,
+                UUID.randomUUID().toString());
+        SCIMExtensionInfo scimExtensionInfo = new SCIMExtensionInfo();
+        scimExtensionInfo.getAttributes().put("location", "1st floor");
+        printer.setExtensionInfo(scimExtensionInfo);
+        Response response = webClient().path("AnyObjects").post(printer);
+        assertEquals(Response.Status.CREATED.getStatusCode(), 
response.getStatus());
+
+        printer = response.readEntity(SCIMAnyObject.class);
+        assertNotNull(printer.getId());
+
+        AnyObjectTO anyObjectTO = ANY_OBJECT_SERVICE.read(printer.getId());
+        assertNotNull(anyObjectTO);
+        ANY_OBJECT_SERVICE.update(new 
AnyObjectUR.Builder(anyObjectTO.getKey()).resource(
+                new StringPatchItem.Builder().value(RESOURCE_NAME_DBSCRIPTED)
+                        .operation(PatchOperation.ADD_REPLACE).build())
+                .build());
+        anyObjectTO = ANY_OBJECT_SERVICE.read(printer.getId());
+        assertNotNull(anyObjectTO);
+        
assertTrue(anyObjectTO.getResources().contains(RESOURCE_NAME_DBSCRIPTED));
+
+        printer.setDisplayName("other" + printer.getId());
+
+        response = 
webClient().path("AnyObjects").path(printer.getId()).put(printer);
+        assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+
+        printer = response.readEntity(SCIMAnyObject.class);
+        assertTrue(printer.getDisplayName().startsWith("other"));
+
+        anyObjectTO = ANY_OBJECT_SERVICE.read(printer.getId());
+        assertNotNull(anyObjectTO);
+        
assertTrue(anyObjectTO.getResources().contains(RESOURCE_NAME_DBSCRIPTED));
+    }
+
+    @Test
+    public void deletePrinter() {
+        SCIMAnyObject printer = new SCIMAnyObject(
+                null,
+                
List.of("urn:ietf:params:scim:schemas:extension:syncope:2.0:PRINTER"),
+                null,
+                UUID.randomUUID().toString());
+        Response response = webClient().path("AnyObjects").post(printer);
+        assertEquals(Response.Status.CREATED.getStatusCode(), 
response.getStatus());
+
+        printer = response.readEntity(SCIMAnyObject.class);
+        assertNotNull(printer.getId());
+
+        response = webClient().path("AnyObjects").path(printer.getId()).get();
+        assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+
+        response = 
webClient().path("AnyObjects").path(printer.getId()).delete();
+        assertEquals(Response.Status.NO_CONTENT.getStatusCode(), 
response.getStatus());
+
+        response = webClient().path("AnyObjects").path(printer.getId()).get();
+        assertEquals(Response.Status.NOT_FOUND.getStatusCode(), 
response.getStatus());
+    }
 }

Reply via email to