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

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


The following commit(s) were added to refs/heads/master by this push:
     new 49fd6223cd [SYNCOPE-1822] added support for SCIM user extension (#773)
49fd6223cd is described below

commit 49fd6223cd6502be8f1c18a5ee41674072e4b29b
Author: Samuel Garofalo <[email protected]>
AuthorDate: Wed Jul 3 15:56:43 2024 +0200

    [SYNCOPE-1822] added support for SCIM user extension (#773)
---
 .../org/apache/syncope/common/lib/to/Item.java     |  18 +-
 .../console/panels/SCIMConfExtensionUserPanel.java |  83 ++++++
 .../client/console/panels/SCIMConfPanel.java       |  28 ++-
 .../panels/mapping/SCIMExtentionMappingPanel.java  | 279 +++++++++++++++++++++
 .../console/panels/SCIMConfExtensionUserPanel.html |  27 ++
 .../client/console/panels/SCIMConfPanel.properties |   3 +-
 .../console/panels/SCIMConfPanel_it.properties     |   3 +-
 .../console/panels/SCIMConfPanel_pt_BR.properties  |   3 +-
 .../console/panels/SCIMConfPanel_ru.properties     |   3 +-
 .../panels/mapping/SCIMExtentionMappingPanel.html  | 105 ++++++++
 .../SCIMExtentionMappingPanel.properties}          |  15 +-
 .../SCIMExtentionMappingPanel_fr.properties}       |  15 +-
 .../SCIMExtentionMappingPanel_it.properties}       |  15 +-
 .../SCIMExtentionMappingPanel_ja.properties}       |  15 +-
 .../SCIMExtentionMappingPanel_pt_BR.properties}    |  15 +-
 .../SCIMExtentionMappingPanel_ru.properties}       |  15 +-
 .../apache/syncope/common/lib/scim/SCIMConf.java   |  10 +
 .../common/lib/scim/SCIMExtensionUserConf.java     |  72 ++++++
 .../apache/syncope/common/lib/scim/SCIMItem.java   | 114 +++++++++
 .../syncope/common/lib/scim/SCIMReturned.java      |  37 +++
 .../apache/syncope/core/logic/SCIMDataBinder.java  | 164 ++++++------
 .../org/apache/syncope/core/logic/SCIMLogic.java   |  61 +++--
 .../syncope/core/logic/scim/SCIMConfManager.java   |  41 +++
 .../syncope/core/logic/scim/SearchCondVisitor.java |   9 +
 .../ext/scimv2/api/data/SCIMExtensionInfo.java     |  33 +++
 .../syncope/ext/scimv2/api/data/SCIMUser.java      |  13 +-
 .../syncope/ext/scimv2/api/type/Resource.java      |   1 +
 .../org/apache/syncope/fit/core/SCIMITCase.java    | 103 +++++++-
 28 files changed, 1138 insertions(+), 162 deletions(-)

diff --git 
a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/Item.java 
b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/Item.java
index 1cd85207c9..82f147cd02 100644
--- a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/Item.java
+++ b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/Item.java
@@ -35,44 +35,44 @@ public class Item implements Serializable {
      * Attribute schema to be mapped. Consider other we can associate tha same 
attribute schema more than once, with
      * different aliases, to different resource attributes.
      */
-    private String intAttrName;
+    protected String intAttrName;
 
     /**
      * External resource's field to be mapped.
      */
-    private String extAttrName;
+    protected String extAttrName;
 
     /**
      * Specify if the mapped target resource's field is the key.
      */
-    private boolean connObjectKey;
+    protected boolean connObjectKey;
 
     /**
      * Specify if the mapped target resource's field is the password.
      */
-    private boolean password;
+    protected boolean password;
 
     /**
      * Specify if the mapped target resource's field is nullable.
      */
-    private String mandatoryCondition = "false";
+    protected String mandatoryCondition = "false";
 
     /**
      * Mapping purposes.
      */
-    private MappingPurpose purpose;
+    protected MappingPurpose purpose;
 
     /**
      * (Optional) JEXL expression to apply to values before propagation.
      */
-    private String propagationJEXLTransformer;
+    protected String propagationJEXLTransformer;
 
     /**
      * (Optional) JEXL expression to apply to values before pull.
      */
-    private String pullJEXLTransformer;
+    protected String pullJEXLTransformer;
 
-    private final List<String> transformers = new ArrayList<>();
+    protected final List<String> transformers = new ArrayList<>();
 
     public boolean isConnObjectKey() {
         return connObjectKey;
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
new file mode 100644
index 0000000000..b3870d3a23
--- /dev/null
+++ 
b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/SCIMConfExtensionUserPanel.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.panels;
+
+import 
org.apache.syncope.client.console.panels.mapping.SCIMExtentionMappingPanel;
+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;
+
+public class SCIMConfExtensionUserPanel extends SCIMConfTabPanel {
+
+    private static final long serialVersionUID = 2459231778083046011L;
+
+    private final SCIMExtensionUserConf scimExtensionUserConf;
+
+    public SCIMConfExtensionUserPanel(final String id, final SCIMConf 
scimConf) {
+        super(id);
+
+        if (scimConf.getExtensionUserConf() == null) {
+            scimConf.setExtensionUserConf(new 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);
+
+        SCIMExtentionMappingPanel extentionMappingPanel = new 
SCIMExtentionMappingPanel(
+                "mapping", new 
ListModel<>(scimExtensionUserConf.getAttributes()));
+        Form<SCIMExtensionUserConf> form = new Form<>("form", new 
Model<>(scimExtensionUserConf));
+        form.add(extentionMappingPanel);
+        add(form);
+    }
+}
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 160062b962..4ecf6db0db 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
@@ -23,9 +23,12 @@ import 
de.agilecoders.wicket.core.markup.html.bootstrap.tabs.AjaxBootstrapTabbed
 import java.io.Serializable;
 import java.util.ArrayList;
 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.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.wicket.PageReference;
 import org.apache.wicket.ajax.AjaxRequestTarget;
@@ -72,7 +75,14 @@ public abstract class SCIMConfPanel extends 
WizardMgtPanel<SCIMConf> {
 
             @Override
             public void onClick(final AjaxRequestTarget target) {
-                scimConfRestClient.set(SCIMConfPanel.this.scimConfTO);
+                try {
+                    scimConfRestClient.set(SCIMConfPanel.this.scimConfTO);
+                    
SyncopeConsoleSession.get().success(getString(Constants.OPERATION_SUCCEEDED));
+                } catch (Exception e) {
+                    LOG.error("While setting SCIM configuration", e);
+                    SyncopeConsoleSession.get().onException(e);
+                }
+                ((BasePage) 
pageRef.getPage()).getNotificationPanel().refresh(target);
             }
         };
         addInnerObject(saveButton);
@@ -136,6 +146,22 @@ public abstract class SCIMConfPanel extends 
WizardMgtPanel<SCIMConf> {
         tabs.add(new ITabComponent(
                 new Model<>(getString("tab4")), getString("tab4")) {
 
+            private static final long serialVersionUID = 6645614456650987567L;
+
+            @Override
+            public WebMarkupContainer getPanel(final String panelId) {
+                return new SCIMConfExtensionUserPanel(panelId, scimConfTO);
+            }
+
+            @Override
+            public boolean isVisible() {
+                return true;
+            }
+        });
+
+        tabs.add(new ITabComponent(
+                new Model<>(getString("tab5")), getString("tab5")) {
+
             private static final long serialVersionUID = 1998052474181916792L;
 
             @Override
diff --git 
a/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/mapping/SCIMExtentionMappingPanel.java
 
b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/mapping/SCIMExtentionMappingPanel.java
new file mode 100644
index 0000000000..892af3e81e
--- /dev/null
+++ 
b/ext/scimv2/client-console/src/main/java/org/apache/syncope/client/console/panels/mapping/SCIMExtentionMappingPanel.java
@@ -0,0 +1,279 @@
+/*
+ * 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.mapping;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import org.apache.commons.lang3.StringUtils;
+import 
org.apache.syncope.client.console.init.ClassPathScanImplementationLookup;
+import org.apache.syncope.client.console.rest.AnyTypeClassRestClient;
+import org.apache.syncope.client.console.rest.AnyTypeRestClient;
+import org.apache.syncope.client.console.wicket.markup.html.form.ActionLink;
+import org.apache.syncope.client.console.wicket.markup.html.form.ActionsPanel;
+import org.apache.syncope.client.ui.commons.Constants;
+import 
org.apache.syncope.client.ui.commons.ajax.form.IndicatorAjaxFormComponentUpdatingBehavior;
+import org.apache.syncope.client.ui.commons.markup.html.form.AjaxCheckBoxPanel;
+import 
org.apache.syncope.client.ui.commons.markup.html.form.AjaxDropDownChoicePanel;
+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;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.list.ListItem;
+import org.apache.wicket.markup.html.list.ListView;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.model.PropertyModel;
+import org.apache.wicket.spring.injection.annot.SpringBean;
+
+public class SCIMExtentionMappingPanel extends Panel {
+
+    private static final long serialVersionUID = -5268147603868322754L;
+
+    @SpringBean
+    protected AnyTypeRestClient anyTypeRestClient;
+
+    @SpringBean
+    protected AnyTypeClassRestClient anyTypeClassRestClient;
+
+    protected final Label intAttrNameInfo;
+
+    protected final WebMarkupContainer mandatoryHeader;
+
+    protected final Label caseExactLabel;
+
+    protected final Label mutabilityLabel;
+
+    protected final Label returnedLabel;
+
+    protected final Label uniquenessLabel;
+
+    protected final Label multiValuedLabel;
+
+    protected final AjaxButton addMappingBtn;
+
+    protected final ListView<SCIMItem> mappings;
+
+    protected final WebMarkupContainer mappingContainer;
+
+    public SCIMExtentionMappingPanel(
+            final String id,
+            final IModel<List<SCIMItem>> model) {
+
+        super(id);
+        setOutputMarkupId(true);
+
+        mappingContainer = new WebMarkupContainer("mappingContainer");
+        mappingContainer.setOutputMarkupId(true);
+        add(mappingContainer);
+
+        intAttrNameInfo = new Label("intAttrNameInfo", Model.of());
+        mappingContainer.add(intAttrNameInfo);
+
+        mandatoryHeader = new WebMarkupContainer("mandatoryHeader");
+        mandatoryHeader.setOutputMarkupId(true);
+        mappingContainer.add(mandatoryHeader);
+
+        caseExactLabel = new Label("caseExactLabel", Model.of());
+        mappingContainer.add(caseExactLabel);
+        mutabilityLabel = new Label("mutabilityLabel", Model.of());
+        mappingContainer.add(mutabilityLabel);
+        returnedLabel = new Label("returnedLabel", Model.of());
+        mappingContainer.add(returnedLabel);
+        uniquenessLabel = new Label("uniquenessLabel", Model.of());
+        mappingContainer.add(uniquenessLabel);
+        multiValuedLabel = new Label("multiValuedLabel", Model.of());
+        mappingContainer.add(multiValuedLabel);
+
+        mappings = new ListView<>("mappings", model) {
+
+            private static final long serialVersionUID = -8749412138042656239L;
+
+            @Override
+            protected void populateItem(final ListItem<SCIMItem> item) {
+                final SCIMItem itemTO = item.getModelObject();
+
+                //--------------------------------
+                // Internal attribute
+                // -------------------------------
+                AjaxTextFieldPanel intAttrName = new AjaxTextFieldPanel(
+                        "intAttrName",
+                        "intAttrName",
+                        new PropertyModel<>(itemTO, "intAttrName"),
+                        false);
+                intAttrName.setChoices(List.of());
+                intAttrName.setRequired(true).hideLabel();
+                item.add(intAttrName);
+                // -------------------------------
+
+                //--------------------------------
+                // External attribute
+                // -------------------------------
+                AjaxTextFieldPanel extAttrName = new AjaxTextFieldPanel(
+                        "extAttrName",
+                        "extAttrName",
+                        new PropertyModel<>(itemTO, "extAttrName"));
+                extAttrName.setChoices(getExtAttrNames().getObject());
+
+                extAttrName.setRequired(true).hideLabel();
+                extAttrName.setEnabled(true);
+                item.add(extAttrName);
+                // -------------------------------
+
+                //--------------------------------
+                // mandatoryCondition
+                // -------------------------------
+                AjaxCheckBoxPanel mandatoryCondition = new AjaxCheckBoxPanel(
+                        "mandatoryCondition",
+                        "mandatoryCondition",
+                        new PropertyModel<>(itemTO, "mandatoryCondition"));
+                mandatoryCondition.hideLabel();
+                mandatoryCondition.setEnabled(true);
+                item.add(mandatoryCondition);
+                // -------------------------------
+
+                //--------------------------------
+                // CaseExact
+                // -------------------------------
+                AjaxCheckBoxPanel caseExact = new AjaxCheckBoxPanel(
+                        "caseExact",
+                        "caseExact",
+                        new PropertyModel<>(itemTO, "caseExact"));
+                caseExact.hideLabel();
+                caseExact.setEnabled(true);
+                item.add(caseExact);
+                // -------------------------------
+
+                //--------------------------------
+                // Mutability
+                // -------------------------------
+                AjaxCheckBoxPanel mutability = new AjaxCheckBoxPanel(
+                        "mutability",
+                        "mutability",
+                        new PropertyModel<>(itemTO, "mutability"));
+                mutability.hideLabel();
+                mutability.setEnabled(true);
+                item.add(mutability);
+                // -------------------------------
+
+                //--------------------------------
+                // Returned
+                // -------------------------------
+                AjaxDropDownChoicePanel<SCIMReturned> returned = new 
AjaxDropDownChoicePanel<>(
+                        "returned",
+                        "returned",
+                        new PropertyModel<>(itemTO, "returned"));
+                returned.hideLabel();
+                returned.setChoices(List.of(SCIMReturned.values()));
+                returned.setEnabled(true);
+                item.add(returned);
+                // -------------------------------
+
+                //--------------------------------
+                // Uniqueness
+                // -------------------------------
+                AjaxCheckBoxPanel uniqueness = new AjaxCheckBoxPanel(
+                        "uniqueness",
+                        "uniqueness",
+                        new PropertyModel<>(itemTO, "uniqueness"));
+                uniqueness.hideLabel();
+                uniqueness.setEnabled(true);
+                item.add(uniqueness);
+                // -------------------------------
+
+                //--------------------------------
+                // MultiValued
+                // -------------------------------
+                AjaxCheckBoxPanel multiValued = new AjaxCheckBoxPanel(
+                        "multiValued",
+                        "multiValued",
+                        new PropertyModel<>(itemTO, "multiValued"));
+                multiValued.hideLabel();
+                multiValued.setEnabled(true);
+                item.add(multiValued);
+                // -------------------------------
+
+                //--------------------------------
+                // Remove
+                // -------------------------------
+                ActionsPanel<Serializable> actions = new 
ActionsPanel<>("toRemove", null);
+                actions.add(new ActionLink<>() {
+
+                    private static final long serialVersionUID = 
-4097030429755746419L;
+
+                    @Override
+                    public void onClick(final AjaxRequestTarget target, final 
Serializable ignore) {
+                        model.getObject().remove(item.getIndex());
+                        item.getParent().removeAll();
+                        target.add(SCIMExtentionMappingPanel.this);
+                    }
+                }, ActionLink.ActionType.DELETE, StringUtils.EMPTY, 
true).hideLabel();
+                item.add(actions);
+                // -------------------------------
+
+                intAttrName.getField().add(new 
IndicatorAjaxFormComponentUpdatingBehavior(Constants.ON_CHANGE) {
+
+                    private static final long serialVersionUID = 
6890150953186587184L;
+
+                    @Override
+                    protected void onUpdate(final AjaxRequestTarget target) {
+                    }
+                });
+            }
+        };
+
+        mappings.setReuseItems(true);
+        mappingContainer.add(mappings);
+
+        addMappingBtn = new IndicatingAjaxButton("addMappingBtn") {
+
+            private static final long serialVersionUID = -971427869417596230L;
+
+            @Override
+            protected void onSubmit(final AjaxRequestTarget target) {
+                model.getObject().add(new SCIMItem());
+                target.add(SCIMExtentionMappingPanel.this);
+            }
+        };
+        addMappingBtn.setDefaultFormProcessing(false);
+        addMappingBtn.setEnabled(true);
+        mappingContainer.add(addMappingBtn);
+    }
+
+    protected IModel<List<String>> getExtAttrNames() {
+        List<String> choices = new 
ArrayList<>(ClassPathScanImplementationLookup.USER_FIELD_NAMES);
+
+        
anyTypeClassRestClient.list(anyTypeRestClient.read(AnyTypeKind.USER.name()).getClasses()).
+                forEach(anyTypeClassTO -> {
+                    choices.addAll(anyTypeClassTO.getPlainSchemas());
+                    choices.addAll(anyTypeClassTO.getDerSchemas());
+                    choices.addAll(anyTypeClassTO.getVirSchemas());
+                });
+
+        Collections.sort(choices);
+        return Model.ofList(choices);
+    }
+}
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/SCIMConfExtensionUserPanel.html
new file mode 100644
index 0000000000..a14c9a7cf2
--- /dev/null
+++ 
b/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfExtensionUserPanel.html
@@ -0,0 +1,27 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<html xmlns="http://www.w3.org/1999/xhtml"; 
xmlns:wicket="http://wicket.apache.org";>
+  <wicket:panel>
+    <div wicket:id="name"/>
+    <div wicket:id="description"/>
+    <form data-example-id="simple-input-groups" class="bs-example 
bs-example-form" wicket:id="form">
+      <span wicket:id="mapping">[modal content]</span>
+    </form>
+  </wicket:panel>
+</html>
diff --git 
a/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel.properties
 
b/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel.properties
index 10d4781a8f..fd36da900c 100644
--- 
a/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel.properties
+++ 
b/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel.properties
@@ -19,4 +19,5 @@ tab2=User
 tab3=EnterpriseUser
 
 saveButton=Save
-tab4=Group
+tab4=ExtensionUser
+tab5=Group
diff --git 
a/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_it.properties
 
b/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_it.properties
index 10d4781a8f..fd36da900c 100644
--- 
a/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_it.properties
+++ 
b/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_it.properties
@@ -19,4 +19,5 @@ tab2=User
 tab3=EnterpriseUser
 
 saveButton=Save
-tab4=Group
+tab4=ExtensionUser
+tab5=Group
diff --git 
a/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_pt_BR.properties
 
b/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_pt_BR.properties
index 10d4781a8f..fd36da900c 100644
--- 
a/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_pt_BR.properties
+++ 
b/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_pt_BR.properties
@@ -19,4 +19,5 @@ tab2=User
 tab3=EnterpriseUser
 
 saveButton=Save
-tab4=Group
+tab4=ExtensionUser
+tab5=Group
diff --git 
a/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_ru.properties
 
b/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_ru.properties
index 10d4781a8f..fd36da900c 100644
--- 
a/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_ru.properties
+++ 
b/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_ru.properties
@@ -19,4 +19,5 @@ tab2=User
 tab3=EnterpriseUser
 
 saveButton=Save
-tab4=Group
+tab4=ExtensionUser
+tab5=Group
diff --git 
a/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/mapping/SCIMExtentionMappingPanel.html
 
b/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/mapping/SCIMExtentionMappingPanel.html
new file mode 100644
index 0000000000..1849e7e9ae
--- /dev/null
+++ 
b/ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/mapping/SCIMExtentionMappingPanel.html
@@ -0,0 +1,105 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<html xmlns="http://www.w3.org/1999/xhtml"; 
xmlns:wicket="http://wicket.apache.org";>
+  <wicket:panel>
+    <div class="table-responsive no-padding">
+      <table id="mappings"
+             class="table table-hover"
+             style="font-size: 1em;margin-top:2px;"
+             wicket:id="mappingContainer">
+        <tbody>
+          <tr>
+            <th>
+              <wicket:message key="intAttrName"/>
+              <span id="intAttrNameInfo" wicket:id="intAttrNameInfo"></span>
+            </th>
+            <th>
+              <wicket:message key="extAttrName"/>
+            </th>
+            <th wicket:id="mandatoryHeader">
+              <wicket:message key="mandatoryCondition"/>
+            </th>
+            <th>
+              <wicket:message key="caseExact"/>
+              <span id="caseExactLabel" wicket:id="caseExactLabel"></span>
+            </th>
+            <th>
+              <wicket:message key="mutability"/>
+              <span id="mutabilityLabel" wicket:id="mutabilityLabel"></span>
+            </th>
+            <th>
+              <wicket:message key="returned"/>
+              <span id="returnedLabel" wicket:id="returnedLabel"></span>
+            </th>
+            <th>
+              <wicket:message key="uniqueness"/>
+              <span id="uniquenessLabel" wicket:id="uniquenessLabel"></span>
+            </th>
+            <th>
+              <wicket:message key="multiValued"/>
+              <span id="multiValuedLabel" wicket:id="multiValuedLabel"></span>
+            </th>
+            <th></th>
+          </tr>
+          <tr wicket:id="mappings">
+            <td>
+              <span wicket:id="intAttrName">[intAttrNames]</span>
+            </td>
+            <td>
+              <span wicket:id="extAttrName">[extAttrName]</span>
+            </td>
+            <td>
+              <span wicket:id="mandatoryCondition">[mandatoryCondition]</span>
+            </td>
+            <td>
+              <span wicket:id="caseExact">[caseExact]</span>
+            </td>
+            <td>
+              <span wicket:id="mutability">[mutability]</span>
+            </td>
+            <td>
+              <span wicket:id="returned">[returned]</span>
+            </td>
+            <td>
+              <span wicket:id="uniqueness">[uniqueness]</span>
+            </td>
+            <td>
+              <span wicket:id="multiValued">[multiValued]</span>
+            </td>
+            <td>
+              <div id="inline-actions">
+                <span wicket:id="toRemove"/>
+              </div>
+            </td>
+          </tr>
+        </tbody>
+
+        <tfoot>
+          <tr>
+            <td colspan="10" style="padding: 5px; text-align: right">
+              <button type="submit" class="btn btn-success btn-circle btn-lg" 
wicket:id="addMappingBtn">
+                <i class="fa fa-plus"></i>
+              </button>
+            </td>
+          </tr>
+        </tfoot>
+      </table>
+    </div>
+  </wicket:panel>
+</html>
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/mapping/SCIMExtentionMappingPanel.properties
similarity index 79%
copy from 
ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_pt_BR.properties
copy to 
ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/mapping/SCIMExtentionMappingPanel.properties
index 10d4781a8f..75bd842a2f 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/mapping/SCIMExtentionMappingPanel.properties
@@ -14,9 +14,12 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-tab1=General
-tab2=User
-tab3=EnterpriseUser
-
-saveButton=Save
-tab4=Group
+extAttrName=Syncope attribute
+mandatoryCondition=Required
+delete=Delete
+intAttrName=SCIM attribute
+caseExact=Case Exact
+mutability=Mutability
+returned=Returned
+uniqueness=Uniqueness
+multiValued=MultiValued
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/mapping/SCIMExtentionMappingPanel_fr.properties
similarity index 79%
copy from 
ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_pt_BR.properties
copy to 
ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/mapping/SCIMExtentionMappingPanel_fr.properties
index 10d4781a8f..75bd842a2f 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/mapping/SCIMExtentionMappingPanel_fr.properties
@@ -14,9 +14,12 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-tab1=General
-tab2=User
-tab3=EnterpriseUser
-
-saveButton=Save
-tab4=Group
+extAttrName=Syncope attribute
+mandatoryCondition=Required
+delete=Delete
+intAttrName=SCIM attribute
+caseExact=Case Exact
+mutability=Mutability
+returned=Returned
+uniqueness=Uniqueness
+multiValued=MultiValued
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/mapping/SCIMExtentionMappingPanel_it.properties
similarity index 78%
copy from 
ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_pt_BR.properties
copy to 
ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/mapping/SCIMExtentionMappingPanel_it.properties
index 10d4781a8f..bbbccc6974 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/mapping/SCIMExtentionMappingPanel_it.properties
@@ -14,9 +14,12 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-tab1=General
-tab2=User
-tab3=EnterpriseUser
-
-saveButton=Save
-tab4=Group
+extAttrName=Attributo Syncope
+mandatoryCondition=Obbligatorio
+delete=Rimuovi
+intAttrName=Attributo SCIM
+caseExact=Case Exact
+mutability=Mutability
+returned=Returned
+uniqueness=Unicit\u00E0
+multiValued=Multi Valore
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/mapping/SCIMExtentionMappingPanel_ja.properties
similarity index 79%
copy from 
ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_pt_BR.properties
copy to 
ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/mapping/SCIMExtentionMappingPanel_ja.properties
index 10d4781a8f..75bd842a2f 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/mapping/SCIMExtentionMappingPanel_ja.properties
@@ -14,9 +14,12 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-tab1=General
-tab2=User
-tab3=EnterpriseUser
-
-saveButton=Save
-tab4=Group
+extAttrName=Syncope attribute
+mandatoryCondition=Required
+delete=Delete
+intAttrName=SCIM attribute
+caseExact=Case Exact
+mutability=Mutability
+returned=Returned
+uniqueness=Uniqueness
+multiValued=MultiValued
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/mapping/SCIMExtentionMappingPanel_pt_BR.properties
similarity index 79%
copy from 
ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_pt_BR.properties
copy to 
ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/mapping/SCIMExtentionMappingPanel_pt_BR.properties
index 10d4781a8f..75bd842a2f 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/mapping/SCIMExtentionMappingPanel_pt_BR.properties
@@ -14,9 +14,12 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-tab1=General
-tab2=User
-tab3=EnterpriseUser
-
-saveButton=Save
-tab4=Group
+extAttrName=Syncope attribute
+mandatoryCondition=Required
+delete=Delete
+intAttrName=SCIM attribute
+caseExact=Case Exact
+mutability=Mutability
+returned=Returned
+uniqueness=Uniqueness
+multiValued=MultiValued
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/mapping/SCIMExtentionMappingPanel_ru.properties
similarity index 79%
copy from 
ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/SCIMConfPanel_pt_BR.properties
copy to 
ext/scimv2/client-console/src/main/resources/org/apache/syncope/client/console/panels/mapping/SCIMExtentionMappingPanel_ru.properties
index 10d4781a8f..75bd842a2f 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/mapping/SCIMExtentionMappingPanel_ru.properties
@@ -14,9 +14,12 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-tab1=General
-tab2=User
-tab3=EnterpriseUser
-
-saveButton=Save
-tab4=Group
+extAttrName=Syncope attribute
+mandatoryCondition=Required
+delete=Delete
+intAttrName=SCIM attribute
+caseExact=Case Exact
+mutability=Mutability
+returned=Returned
+uniqueness=Uniqueness
+multiValued=MultiValued
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 98fe8f2b4b..468b5d289e 100644
--- 
a/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMConf.java
+++ 
b/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMConf.java
@@ -32,6 +32,8 @@ public class SCIMConf implements Serializable {
 
     private SCIMEnterpriseUserConf enterpriseUserConf;
 
+    private SCIMExtensionUserConf extensionUserConf;
+
     private SCIMGroupConf groupConf;
 
     public SCIMGeneralConf getGeneralConf() {
@@ -58,6 +60,14 @@ public class SCIMConf implements Serializable {
         this.enterpriseUserConf = enterpriseUserConf;
     }
 
+    public SCIMExtensionUserConf getExtensionUserConf() {
+        return extensionUserConf;
+    }
+
+    public void setExtensionUserConf(final SCIMExtensionUserConf 
extensionUserConf) {
+        this.extensionUserConf = extensionUserConf;
+    }
+
     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/SCIMExtensionUserConf.java
new file mode 100644
index 0000000000..cae6330caf
--- /dev/null
+++ 
b/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMExtensionUserConf.java
@@ -0,0 +1,72 @@
+/*
+ * 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;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+public class SCIMExtensionUserConf implements Serializable {
+
+    private static final long serialVersionUID = -9091596628402547645L;
+
+    private String name;
+
+    private String description;
+
+    private final List<SCIMItem> attributes = new ArrayList<>();
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(final String name) {
+        this.name = name;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(final String description) {
+        this.description = description;
+    }
+
+    public List<SCIMItem> getAttributes() {
+        return attributes;
+    }
+
+    public boolean add(final SCIMItem item) {
+        return Optional.ofNullable(item).
+                filter(itemTO -> attributes.contains(itemTO) || 
attributes.add(itemTO)).
+                isPresent();
+    }
+
+    @JsonIgnore
+    public Map<String, String> asMap() {
+        Map<String, String> map = new HashMap<>();
+        attributes.forEach(item -> map.put(item.getIntAttrName(), 
item.getExtAttrName()));
+        return Collections.unmodifiableMap(map);
+    }
+}
diff --git 
a/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMItem.java
 
b/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMItem.java
new file mode 100644
index 0000000000..3395d1b81b
--- /dev/null
+++ 
b/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMItem.java
@@ -0,0 +1,114 @@
+/*
+ * 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;
+
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.syncope.common.lib.to.Item;
+
+public class SCIMItem extends Item {
+
+    private boolean caseExact = false;
+
+    private boolean mutability = false;
+
+    private SCIMReturned returned = SCIMReturned.DEFAULT;
+
+    private boolean uniqueness = false;
+
+    private boolean multiValued = false;
+
+    public boolean isCaseExact() {
+        return caseExact;
+    }
+
+    public void setCaseExact(final boolean caseExact) {
+        this.caseExact = caseExact;
+    }
+
+    public boolean isMutability() {
+        return mutability;
+    }
+
+    public void setMutability(final boolean mutability) {
+        this.mutability = mutability;
+    }
+
+    public SCIMReturned getReturned() {
+        return returned;
+    }
+
+    public void setReturned(final SCIMReturned returned) {
+        this.returned = returned;
+    }
+
+    public boolean isUniqueness() {
+        return uniqueness;
+    }
+
+    public void setUniqueness(final boolean uniqueness) {
+        this.uniqueness = uniqueness;
+    }
+
+    public boolean isMultiValued() {
+        return multiValued;
+    }
+
+    public void setMultiValued(final boolean multiValued) {
+        this.multiValued = multiValued;
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        SCIMItem other = (SCIMItem) obj;
+        return new EqualsBuilder().
+                append(intAttrName, other.intAttrName).
+                append(extAttrName, other.extAttrName).
+                append(mandatoryCondition, other.mandatoryCondition).
+                append(caseExact, other.caseExact).
+                append(mutability, other.mutability).
+                append(returned, other.returned).
+                append(uniqueness, other.uniqueness).
+                append(multiValued, other.multiValued).
+                build();
+    }
+
+    @Override
+    public int hashCode() {
+        return new HashCodeBuilder().
+                append(intAttrName).
+                append(extAttrName).
+                append(mandatoryCondition).
+                append(caseExact).
+                append(mutability).
+                append(returned).
+                append(uniqueness).
+                append(multiValued).
+                build();
+    }
+}
diff --git 
a/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMReturned.java
 
b/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMReturned.java
new file mode 100644
index 0000000000..d5ba68f2b3
--- /dev/null
+++ 
b/ext/scimv2/common-lib/src/main/java/org/apache/syncope/common/lib/scim/SCIMReturned.java
@@ -0,0 +1,37 @@
+/*
+ * 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 enum SCIMReturned {
+
+    ALWAYS("always"),
+    NEVER("never"),
+    DEFAULT("default"),
+    REQUEST("request");
+
+    private final String returned;
+
+    SCIMReturned(final String returned) {
+        this.returned = returned;
+    }
+
+    public String getReturned() {
+        return returned;
+    }
+}
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 f39f839712..354a7ecbfc 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
@@ -21,6 +21,7 @@ package org.apache.syncope.core.logic;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -63,6 +64,7 @@ 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.SCIMComplexValue;
 import org.apache.syncope.ext.scimv2.api.data.SCIMEnterpriseInfo;
+import org.apache.syncope.ext.scimv2.api.data.SCIMExtensionInfo;
 import org.apache.syncope.ext.scimv2.api.data.SCIMGroup;
 import org.apache.syncope.ext.scimv2.api.data.SCIMPatchOperation;
 import org.apache.syncope.ext.scimv2.api.data.SCIMUser;
@@ -83,11 +85,6 @@ public class SCIMDataBinder {
 
     protected static final Logger LOG = 
LoggerFactory.getLogger(SCIMDataBinder.class);
 
-    protected static final List<String> USER_SCHEMAS = 
List.of(Resource.User.schema());
-
-    protected static final List<String> ENTERPRISE_USER_SCHEMAS =
-            List.of(Resource.User.schema(), Resource.EnterpriseUser.schema());
-
     protected static final List<String> GROUP_SCHEMAS = 
List.of(Resource.Group.schema());
 
     /**
@@ -195,6 +192,9 @@ public class SCIMDataBinder {
         if (conf.getEnterpriseUserConf() != null) {
             schemas.add(Resource.EnterpriseUser.schema());
         }
+        if (conf.getExtensionUserConf() != null) {
+            schemas.add(Resource.ExtensionUser.schema());
+        }
 
         SCIMUser user = new SCIMUser(
                 userTO.getKey(),
@@ -444,6 +444,13 @@ public class SCIMDataBinder {
             }
         }
 
+        if (conf.getExtensionUserConf() != null) {
+            SCIMExtensionInfo extensionInfo = new SCIMExtensionInfo();
+            conf.getExtensionUserConf().asMap().forEach((scimAttr, 
syncopeAttr) -> extensionInfo.getAttributes().put(
+                    scimAttr, attrs.get(syncopeAttr).getValues().get(0)));
+            user.setExtensionInfo(extensionInfo);
+        }
+
         if (output(attributes, excludedAttributes, "groups")) {
             userTO.getMemberships().forEach(membership -> 
user.getGroups().add(new Group(
                     membership.getGroupKey(),
@@ -499,9 +506,19 @@ public class SCIMDataBinder {
     }
 
     public UserTO toUserTO(final SCIMUser user, final boolean checkSchemas) {
+        SCIMConf conf = confManager.get();
+
+        Set<String> expectedSchemas = new HashSet<>();
+        expectedSchemas.add(Resource.User.schema());
+        if (conf.getEnterpriseUserConf() != null) {
+            expectedSchemas.add(Resource.EnterpriseUser.schema());
+        }
+        if (conf.getExtensionUserConf() != null) {
+            expectedSchemas.add(Resource.ExtensionUser.schema());
+        }
         if (checkSchemas
-                && !USER_SCHEMAS.equals(user.getSchemas())
-                && !ENTERPRISE_USER_SCHEMAS.equals(user.getSchemas())) {
+                && (!user.getSchemas().containsAll(expectedSchemas)
+                || !expectedSchemas.containsAll(user.getSchemas()))) {
 
             throw new BadRequestException(ErrorType.invalidValue);
         }
@@ -512,8 +529,6 @@ public class SCIMDataBinder {
         userTO.setPassword(user.getPassword());
         userTO.setUsername(user.getUserName());
 
-        SCIMConf conf = confManager.get();
-
         if (conf.getUserConf() != null) {
             setAttribute(
                     userTO,
@@ -677,6 +692,11 @@ public class SCIMDataBinder {
                             map(SCIMUserManager::getValue).orElse(null));
         }
 
+        if (conf.getExtensionUserConf() != null && user.getExtensionInfo() != 
null) {
+            conf.getExtensionUserConf().asMap().forEach((scimAttr, 
syncopeAttr) -> setAttribute(
+                    userTO, syncopeAttr, 
user.getExtensionInfo().getAttributes().get(scimAttr)));
+        }
+
         userTO.getMemberships().addAll(user.getGroups().stream().
                 map(group -> new 
MembershipTO.Builder(group.getValue()).build()).
                 toList());
@@ -799,20 +819,18 @@ public class SCIMDataBinder {
         }
 
         switch (op.getPath().getAttribute()) {
-            case "externalId" ->
-                setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getExternalId(), op);
+            case "externalId" -> setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getExternalId(), op);
 
             case "userName" -> {
                 if (op.getOp() != PatchOp.remove && 
!CollectionUtils.isEmpty(op.getValue())) {
-                    userUR.setUsername(new StringReplacePatchItem.Builder().
-                            value(op.getValue().get(0).toString()).build());
+                    userUR.setUsername(
+                            new 
StringReplacePatchItem.Builder().value(op.getValue().get(0).toString()).build());
                 }
             }
 
             case "password" -> {
                 if (op.getOp() != PatchOp.remove && 
!CollectionUtils.isEmpty(op.getValue())) {
-                    userUR.setPassword(new PasswordPatch.Builder().
-                            value(op.getValue().get(0).toString()).build());
+                    userUR.setPassword(new 
PasswordPatch.Builder().value(op.getValue().get(0).toString()).build());
                 }
             }
 
@@ -824,11 +842,9 @@ public class SCIMDataBinder {
                         op.setValue(List.of(BooleanUtils.toBoolean(a)));
                     }
 
-                    statusR = new StatusR.Builder(
-                            before.getKey(),
-                            (boolean) op.getValue().get(0) ? 
StatusRType.REACTIVATE : StatusRType.SUSPEND).
-                            resources(resources).
-                            build();
+                    statusR = new StatusR.Builder(before.getKey(),
+                            (boolean) op.getValue().get(0) ? 
StatusRType.REACTIVATE : StatusRType.SUSPEND).resources(
+                            resources).build();
                 }
             }
 
@@ -855,37 +871,27 @@ public class SCIMDataBinder {
                 }
             }
 
-            case "displayName" ->
-                setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getDisplayName(), op);
+            case "displayName" -> setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getDisplayName(), op);
 
-            case "nickName" ->
-                setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getNickName(), op);
+            case "nickName" -> setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getNickName(), op);
 
-            case "profileUrl" ->
-                setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getProfileUrl(), op);
+            case "profileUrl" -> setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getProfileUrl(), op);
 
-            case "title" ->
-                setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getTitle(), op);
+            case "title" -> setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getTitle(), op);
 
-            case "userType" ->
-                setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getUserType(), op);
+            case "userType" -> setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getUserType(), op);
 
             case "preferredLanguage" ->
-                setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getPreferredLanguage(), op);
+                    setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getPreferredLanguage(), op);
 
-            case "locale" ->
-                setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getLocale(), op);
+            case "locale" -> setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getLocale(), op);
 
-            case "timezone" ->
-                setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getTimezone(), op);
+            case "timezone" -> setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getTimezone(), op);
 
             case "emails" -> {
                 if (!CollectionUtils.isEmpty(op.getValue()) && 
op.getValue().get(0) instanceof SCIMUser) {
-                    setAttribute(
-                            userUR.getPlainAttrs(),
-                            conf.getUserConf().getEmails(),
-                            ((SCIMUser) op.getValue().get(0)).getEmails(),
-                            op.getOp());
+                    setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getEmails(),
+                            ((SCIMUser) op.getValue().get(0)).getEmails(), 
op.getOp());
                 } else if (op.getPath().getFilter() != null) {
                     setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getEmails(), op);
                 }
@@ -893,11 +899,8 @@ public class SCIMDataBinder {
 
             case "phoneNumbers" -> {
                 if (!CollectionUtils.isEmpty(op.getValue()) && 
op.getValue().get(0) instanceof SCIMUser) {
-                    setAttribute(
-                            userUR.getPlainAttrs(),
-                            conf.getUserConf().getPhoneNumbers(),
-                            ((SCIMUser) 
op.getValue().get(0)).getPhoneNumbers(),
-                            op.getOp());
+                    setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getPhoneNumbers(),
+                            ((SCIMUser) 
op.getValue().get(0)).getPhoneNumbers(), op.getOp());
                 } else if (op.getPath().getFilter() != null) {
                     setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getPhoneNumbers(), op);
                 }
@@ -905,11 +908,8 @@ public class SCIMDataBinder {
 
             case "ims" -> {
                 if (!CollectionUtils.isEmpty(op.getValue()) && 
op.getValue().get(0) instanceof SCIMUser) {
-                    setAttribute(
-                            userUR.getPlainAttrs(),
-                            conf.getUserConf().getIms(),
-                            ((SCIMUser) op.getValue().get(0)).getIms(),
-                            op.getOp());
+                    setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getIms(),
+                            ((SCIMUser) op.getValue().get(0)).getIms(), 
op.getOp());
                 } else if (op.getPath().getFilter() != null) {
                     setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getIms(), op);
                 }
@@ -917,11 +917,8 @@ public class SCIMDataBinder {
 
             case "photos" -> {
                 if (!CollectionUtils.isEmpty(op.getValue()) && 
op.getValue().get(0) instanceof SCIMUser) {
-                    setAttribute(
-                            userUR.getPlainAttrs(),
-                            conf.getUserConf().getPhotos(),
-                            ((SCIMUser) op.getValue().get(0)).getPhotos(),
-                            op.getOp());
+                    setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getPhotos(),
+                            ((SCIMUser) op.getValue().get(0)).getPhotos(), 
op.getOp());
                 } else if (op.getPath().getFilter() != null) {
                     setAttribute(userUR.getPlainAttrs(), 
conf.getUserConf().getPhotos(), op);
                 }
@@ -930,44 +927,47 @@ public class SCIMDataBinder {
             case "addresses" -> {
                 if (!CollectionUtils.isEmpty(op.getValue()) && 
op.getValue().get(0) instanceof SCIMUser) {
                     SCIMUser after = (SCIMUser) op.getValue().get(0);
-                    after.getAddresses().stream().filter(address -> 
address.getType() != null).
-                            forEach(address -> 
conf.getUserConf().getAddresses().stream().
-                            filter(object -> 
address.getType().equals(object.getType().name())).findFirst().
-                            ifPresent(addressConf -> 
setAttribute(userUR.getPlainAttrs(), addressConf, op)));
+                    after.getAddresses().stream().filter(address -> 
address.getType() != null).forEach(
+                            address -> 
conf.getUserConf().getAddresses().stream()
+                                    .filter(object -> 
address.getType().equals(object.getType().name())).findFirst()
+                                    .ifPresent(addressConf -> 
setAttribute(userUR.getPlainAttrs(), addressConf, op)));
                 } else if (op.getPath().getFilter() != null) {
-                    conf.getUserConf().getAddresses().stream().
-                            filter(addressConf -> 
BooleanUtils.toBoolean(JexlUtils.evaluateExpr(
-                            filter2JexlExpression(op.getPath().getFilter()),
-                            new MapContext(Map.of("type", 
addressConf.getType().name()))).toString())).findFirst().
-                            ifPresent(addressConf -> 
setAttribute(userUR.getPlainAttrs(), addressConf, op));
+                    
conf.getUserConf().getAddresses().stream().filter(addressConf -> 
BooleanUtils.toBoolean(
+                                    
JexlUtils.evaluateExpr(filter2JexlExpression(op.getPath().getFilter()),
+                                            new MapContext(Map.of("type", 
addressConf.getType().name()))).toString()))
+                            .findFirst()
+                            .ifPresent(addressConf -> 
setAttribute(userUR.getPlainAttrs(), addressConf, op));
                 }
             }
 
-            case "employeeNumber" ->
-                setAttribute(userUR.getPlainAttrs(), 
Optional.ofNullable(conf.getEnterpriseUserConf()).
-                        
map(SCIMEnterpriseUserConf::getEmployeeNumber).orElse(null), op);
+            case "employeeNumber" -> setAttribute(userUR.getPlainAttrs(),
+                    
Optional.ofNullable(conf.getEnterpriseUserConf()).map(SCIMEnterpriseUserConf::getEmployeeNumber)
+                            .orElse(null), op);
 
-            case "costCenter" ->
-                setAttribute(userUR.getPlainAttrs(), 
Optional.ofNullable(conf.getEnterpriseUserConf()).
-                        
map(SCIMEnterpriseUserConf::getCostCenter).orElse(null), op);
+            case "costCenter" -> setAttribute(userUR.getPlainAttrs(),
+                    
Optional.ofNullable(conf.getEnterpriseUserConf()).map(SCIMEnterpriseUserConf::getCostCenter)
+                            .orElse(null), op);
 
-            case "organization" ->
-                setAttribute(userUR.getPlainAttrs(), 
Optional.ofNullable(conf.getEnterpriseUserConf()).
-                        
map(SCIMEnterpriseUserConf::getOrganization).orElse(null), op);
+            case "organization" -> setAttribute(userUR.getPlainAttrs(),
+                    
Optional.ofNullable(conf.getEnterpriseUserConf()).map(SCIMEnterpriseUserConf::getOrganization)
+                            .orElse(null), op);
 
-            case "division" ->
-                setAttribute(userUR.getPlainAttrs(), 
Optional.ofNullable(conf.getEnterpriseUserConf()).
-                        map(SCIMEnterpriseUserConf::getDivision).orElse(null), 
op);
+            case "division" -> setAttribute(userUR.getPlainAttrs(),
+                    
Optional.ofNullable(conf.getEnterpriseUserConf()).map(SCIMEnterpriseUserConf::getDivision)
+                            .orElse(null), op);
 
-            case "department" ->
-                setAttribute(userUR.getPlainAttrs(), 
Optional.ofNullable(conf.getEnterpriseUserConf()).
-                        
map(SCIMEnterpriseUserConf::getDepartment).orElse(null), op);
+            case "department" -> setAttribute(userUR.getPlainAttrs(),
+                    
Optional.ofNullable(conf.getEnterpriseUserConf()).map(SCIMEnterpriseUserConf::getDepartment)
+                            .orElse(null), op);
 
-            case "manager" ->
-                setAttribute(userUR.getPlainAttrs(), 
Optional.ofNullable(conf.getEnterpriseUserConf()).
-                        
map(SCIMEnterpriseUserConf::getManager).map(SCIMManagerConf::getKey).orElse(null),
 op);
+            case "manager" -> setAttribute(userUR.getPlainAttrs(),
+                    
Optional.ofNullable(conf.getEnterpriseUserConf()).map(SCIMEnterpriseUserConf::getManager)
+                            .map(SCIMManagerConf::getKey).orElse(null), op);
 
             default -> {
+                Optional.ofNullable(conf.getExtensionUserConf())
+                        .flatMap(schema -> 
Optional.ofNullable(schema.asMap().get(op.getPath().getAttribute())))
+                        .ifPresent(schema -> 
setAttribute(userUR.getPlainAttrs(), schema, op));
             }
         }
 
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 130c9ec123..9c9d8c558a 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
@@ -21,6 +21,7 @@ package org.apache.syncope.core.logic;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.json.JsonMapper;
 import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import jakarta.ws.rs.core.UriBuilder;
 import java.io.IOException;
 import java.lang.reflect.Method;
@@ -49,15 +50,17 @@ public class SCIMLogic extends AbstractLogic<EntityTO> {
 
     protected static final Object MONITOR = new Object();
 
+    protected static final JsonMapper MAPPER = 
JsonMapper.builder().findAndAddModules().build();
+
     protected static ServiceProviderConfig SERVICE_PROVIDER_CONFIG;
 
     protected static ResourceType USER;
 
     protected static ResourceType GROUP;
 
-    protected static String SCHEMAS;
+    protected String schemas;
 
-    protected static final Map<String, String> SCHEMA_MAP = new HashMap<>();
+    protected final Map<String, String> schemaMap = new HashMap<>();
 
     protected final SCIMConfManager confManager;
 
@@ -67,17 +70,41 @@ public class SCIMLogic extends AbstractLogic<EntityTO> {
 
     protected void init() {
         try {
-            JsonMapper mapper = 
JsonMapper.builder().findAndAddModules().build();
-            JsonNode tree = 
mapper.readTree(SCIMLogic.class.getResourceAsStream('/' + SCHEMAS_JSON));
+            JsonNode tree = 
MAPPER.readTree(SCIMLogic.class.getResourceAsStream('/' + SCHEMAS_JSON));
             if (!tree.isArray()) {
                 throw new IOException("JSON node is not a tree");
             }
 
             ArrayNode schemaArray = (ArrayNode) tree;
-            SCHEMAS = 
mapper.writerWithDefaultPrettyPrinter().writeValueAsString(tree);
+            SCIMConf conf = confManager.get();
+            if (conf.getExtensionUserConf() != null) {
+                ObjectNode extensionObject = MAPPER.createObjectNode();
+                extensionObject.put("id", Resource.ExtensionUser.schema());
+                extensionObject.put("name", 
conf.getExtensionUserConf().getName());
+                extensionObject.put("description", 
conf.getExtensionUserConf().getDescription());
+                ArrayNode attributes = MAPPER.createArrayNode();
+                conf.getExtensionUserConf().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:User\"}"));
+                schemaArray.add(extensionObject);
+            }
+            schemas = 
MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(tree);
 
+            schemaMap.clear();
             for (JsonNode schema : schemaArray) {
-                SCHEMA_MAP.put(schema.get("id").asText(), 
mapper.writeValueAsString(schema));
+                schemaMap.put(schema.get("id").asText(), 
MAPPER.writeValueAsString(schema));
             }
         } catch (IOException e) {
             LOG.error("Could not parse the default schema definitions", e);
@@ -86,11 +113,9 @@ public class SCIMLogic extends AbstractLogic<EntityTO> {
 
     @PreAuthorize("isAuthenticated()")
     public ServiceProviderConfig serviceProviderConfig(final UriBuilder 
uriBuilder) {
-        synchronized (MONITOR) {
-            if (SCHEMAS == null) {
-                init();
-            }
+        init();
 
+        synchronized (MONITOR) {
             if (SERVICE_PROVIDER_CONFIG == null) {
                 SCIMConf conf = confManager.get();
 
@@ -164,24 +189,16 @@ public class SCIMLogic extends AbstractLogic<EntityTO> {
 
     @PreAuthorize("isAuthenticated()")
     public String schemas() {
-        synchronized (MONITOR) {
-            if (SCHEMAS == null) {
-                init();
-            }
-        }
+        init();
 
-        return SCHEMAS;
+        return schemas;
     }
 
     @PreAuthorize("isAuthenticated()")
     public String schema(final String schema) {
-        synchronized (MONITOR) {
-            if (SCHEMAS == null) {
-                init();
-            }
-        }
+        init();
 
-        String found = SCHEMA_MAP.get(schema);
+        String found = schemaMap.get(schema);
         if (found == null) {
             throw new NotFoundException("Schema " + schema + " not found");
         }
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 c964c9e80a..8ee044a08b 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
@@ -22,11 +22,13 @@ import jakarta.ws.rs.core.MediaType;
 import java.time.OffsetDateTime;
 import java.util.Base64;
 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.types.SCIMEntitlement;
 import org.apache.syncope.common.lib.to.PlainSchemaTO;
 import org.apache.syncope.common.lib.types.AttrSchemaType;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
 import org.apache.syncope.common.lib.types.SchemaType;
 import org.apache.syncope.core.logic.SchemaLogic;
 import org.apache.syncope.core.persistence.api.dao.NotFoundException;
@@ -82,6 +84,45 @@ public class SCIMConfManager {
         conf.setGeneralConf(new SCIMGeneralConf());
         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;
+                    }
+                }
+            });
+        }
+
         confParamOps.set(AuthContextUtils.getDomain(),
                 SCIMConf.KEY, 
Base64.getEncoder().encodeToString(POJOHelper.serialize(conf).getBytes()));
     }
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 44c3b0a1e1..ad8cc743fc 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
@@ -123,6 +123,15 @@ public class SearchCondVisitor extends 
SCIMFilterBaseVisitor<SearchCond> {
                         
attrCond.setSchema(conf.getEnterpriseUserConf().getManager().getKey());
                     }
                 }
+
+                if (conf.getExtensionUserConf() != null) {
+                    for (Map.Entry<String, String> entry : 
conf.getExtensionUserConf().asMap().entrySet()) {
+                        if (schemaEquals(Resource.ExtensionUser, 
entry.getKey(), schema)) {
+                            attrCond = new AttrCond();
+                            attrCond.setSchema(entry.getValue());
+                        }
+                    }
+                }
                 break;
 
             case Group:
diff --git 
a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMExtensionInfo.java
 
b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMExtensionInfo.java
new file mode 100644
index 0000000000..a23ee4ef58
--- /dev/null
+++ 
b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMExtensionInfo.java
@@ -0,0 +1,33 @@
+/*
+ * 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 java.util.HashMap;
+import java.util.Map;
+
+public class SCIMExtensionInfo extends SCIMBean {
+
+    private static final long serialVersionUID = 1310985252565467391L;
+
+    private final Map<String, String> attributes = new HashMap<>();
+
+    public Map<String, String> getAttributes() {
+        return attributes;
+    }
+}
diff --git 
a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMUser.java
 
b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMUser.java
index 08120534e9..ccd34ed98c 100644
--- 
a/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMUser.java
+++ 
b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/data/SCIMUser.java
@@ -31,7 +31,7 @@ import java.util.Optional;
     "name", "displayName", "nickName", "profileUrl", "title", "userType", 
"preferredLanguage", "locale", "timezone",
     "emails", "phoneNumbers", "ims", "photos", "addresses", "x509Certificates",
     "groups", "entitlements", "roles",
-    "enterpriseInfo",
+    "enterpriseInfo", "extensionInfo",
     "meta" })
 public class SCIMUser extends SCIMResource {
 
@@ -80,6 +80,9 @@ public class SCIMUser extends SCIMResource {
     @JsonProperty("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User")
     private SCIMEnterpriseInfo enterpriseInfo;
 
+    @JsonProperty("urn:ietf:params:scim:schemas:extension:syncope:2.0:User")
+    private SCIMExtensionInfo extensionInfo;
+
     @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
     public SCIMUser(
             @JsonProperty("id") final String id,
@@ -221,4 +224,12 @@ public class SCIMUser extends SCIMResource {
     public void setEnterpriseInfo(final SCIMEnterpriseInfo enterpriseInfo) {
         this.enterpriseInfo = enterpriseInfo;
     }
+
+    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/type/Resource.java
 
b/ext/scimv2/scim-rest-api/src/main/java/org/apache/syncope/ext/scimv2/api/type/Resource.java
index b26382383f..0cbd6d2828 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
@@ -25,6 +25,7 @@ public enum Resource {
     Schema("urn:ietf:params:scim:schemas:core:2.0:Schema"),
     User("urn:ietf:params:scim:schemas:core:2.0:User"),
     
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"),
     SearchRequest("urn:ietf:params:scim:api:messages:2.0:SearchRequest"),
     ListResponse("urn:ietf:params:scim:api:messages:2.0:ListResponse"),
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 ebbb8530cc..5bfc635687 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
@@ -24,6 +24,7 @@ import static 
org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
 import static org.junit.jupiter.api.Assumptions.assumeTrue;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
@@ -46,7 +47,9 @@ import org.apache.commons.lang3.StringUtils;
 import org.apache.cxf.jaxrs.client.WebClient;
 import org.apache.syncope.common.lib.scim.SCIMComplexConf;
 import org.apache.syncope.common.lib.scim.SCIMConf;
+import org.apache.syncope.common.lib.scim.SCIMExtensionUserConf;
 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;
@@ -59,6 +62,7 @@ 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.SCIMComplexValue;
 import org.apache.syncope.ext.scimv2.api.data.SCIMError;
+import org.apache.syncope.ext.scimv2.api.data.SCIMExtensionInfo;
 import org.apache.syncope.ext.scimv2.api.data.SCIMGroup;
 import org.apache.syncope.ext.scimv2.api.data.SCIMSearchRequest;
 import org.apache.syncope.ext.scimv2.api.data.SCIMUser;
@@ -106,8 +110,8 @@ public class SCIMITCase extends AbstractITCase {
         CONF.getUserConf().getEmails().add(email);
     }
 
-    private static SCIMUser getSampleUser(final String username) {
-        SCIMUser user = new SCIMUser(null, List.of(Resource.User.schema()), 
null, username, true);
+    private static SCIMUser getSampleUser(final String username, final 
List<String> schemas) {
+        SCIMUser user = new SCIMUser(null, schemas, null, username, true);
         user.setPassword("password123");
 
         SCIMUserName name = new SCIMUserName();
@@ -195,6 +199,16 @@ public class SCIMITCase extends AbstractITCase {
 
     @Test
     public void schemas() {
+        SCIMExtensionUserConf extensionUserConf = new SCIMExtensionUserConf();
+        extensionUserConf.setName("syncope");
+        extensionUserConf.setDescription("syncope user");
+        SCIMItem scimItem = new SCIMItem();
+        scimItem.setIntAttrName("gender");
+        scimItem.setExtAttrName("gender");
+        extensionUserConf.add(scimItem);
+        CONF.setExtensionUserConf(extensionUserConf);
+        SCIM_CONF_SERVICE.set(CONF);
+
         Response response = webClient().path("Schemas").get();
         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
         assertEquals(
@@ -203,7 +217,7 @@ public class SCIMITCase extends AbstractITCase {
 
         ArrayNode schemas = response.readEntity(ArrayNode.class);
         assertNotNull(schemas);
-        assertEquals(3, schemas.size());
+        assertEquals(4, schemas.size());
 
         response = webClient().path("Schemas").path("none").get();
         assertEquals(Response.Status.NOT_FOUND.getStatusCode(), 
response.getStatus());
@@ -214,6 +228,16 @@ public class SCIMITCase extends AbstractITCase {
         ObjectNode enterpriseUser = response.readEntity(ObjectNode.class);
         assertNotNull(enterpriseUser);
         assertEquals(Resource.EnterpriseUser.schema(), 
enterpriseUser.get("id").textValue());
+
+        response = 
webClient().path("Schemas").path(Resource.ExtensionUser.schema()).get();
+        assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+
+        ObjectNode extensionUser = response.readEntity(ObjectNode.class);
+        assertNotNull(extensionUser);
+        assertEquals(Resource.ExtensionUser.schema(), 
extensionUser.get("id").textValue());
+
+        CONF.setExtensionUserConf(null);
+        SCIM_CONF_SERVICE.set(CONF);
     }
 
     @Test
@@ -270,6 +294,25 @@ public class SCIMITCase extends AbstractITCase {
         assertEquals("Rossini, Gioacchino", user.getDisplayName());
     }
 
+    @Test
+    void invalidConf() {
+        SCIMExtensionUserConf extensionUserConf = new SCIMExtensionUserConf();
+        extensionUserConf.setName("syncope");
+        extensionUserConf.setDescription("syncope user");
+        SCIMItem scimItem = new SCIMItem();
+        scimItem.setIntAttrName("gender");
+        scimItem.setExtAttrName("gender");
+        scimItem.setMultiValued(true);
+        extensionUserConf.add(scimItem);
+        CONF.setExtensionUserConf(extensionUserConf);
+        try {
+            SCIM_CONF_SERVICE.set(CONF);
+            fail();
+        } catch (Exception ignored) {
+            CONF.setExtensionUserConf(null);
+        }
+    }
+
     @Test
     public void list() throws IOException {
         Response response = webClient().path("Groups").query("count", 
1100000).get();
@@ -366,7 +409,7 @@ public class SCIMITCase extends AbstractITCase {
     public void createUser() throws JsonProcessingException {
         SCIM_CONF_SERVICE.set(CONF);
 
-        SCIMUser user = getSampleUser(UUID.randomUUID().toString());
+        SCIMUser user = getSampleUser(UUID.randomUUID().toString(), 
List.of(Resource.User.schema()));
         user.getRoles().add(new Value("User reviewer"));
         user.getGroups().add(new Group("37d15e4c-cdc1-460b-a591-8505c8133806", 
null, null, null));
 
@@ -390,11 +433,57 @@ public class SCIMITCase extends AbstractITCase {
         assertEquals(user.getGroups().get(0).getValue(), 
userTO.getMemberships().get(0).getGroupKey());
     }
 
+    @Test
+    void crudExtensionUser() {
+        SCIMExtensionUserConf extensionUserConf = new SCIMExtensionUserConf();
+        extensionUserConf.setName("syncope");
+        extensionUserConf.setDescription("syncope user");
+        SCIMItem scimItem = new SCIMItem();
+        scimItem.setIntAttrName("gender");
+        scimItem.setExtAttrName("gender");
+        extensionUserConf.add(scimItem);
+        CONF.setExtensionUserConf(extensionUserConf);
+        SCIM_CONF_SERVICE.set(CONF);
+
+        SCIMUser user = getSampleUser(
+                UUID.randomUUID().toString(), List.of(Resource.User.schema(), 
Resource.ExtensionUser.schema()));
+        SCIMExtensionInfo scimExtensionInfo = new SCIMExtensionInfo();
+        scimExtensionInfo.getAttributes().put("gender", "M");
+        user.setExtensionInfo(scimExtensionInfo);
+
+        Response response = webClient().path("Users").post(user);
+        assertEquals(Response.Status.CREATED.getStatusCode(), 
response.getStatus());
+
+        user = response.readEntity(SCIMUser.class);
+        assertNotNull(user.getId());
+        
assertTrue(response.getLocation().toASCIIString().endsWith(user.getId()));
+
+        UserTO userTO = USER_SERVICE.read(user.getId());
+        assertEquals(user.getUserName(), userTO.getUsername());
+        assertTrue(user.isActive());
+        assertEquals(user.getDisplayName(), 
userTO.getDerAttr("cn").get().getValues().get(0));
+        assertEquals(user.getName().getGivenName(), 
userTO.getPlainAttr("firstname").get().getValues().get(0));
+        assertEquals(user.getName().getFamilyName(), 
userTO.getPlainAttr("surname").get().getValues().get(0));
+        assertEquals(user.getName().getFormatted(), 
userTO.getPlainAttr("fullname").get().getValues().get(0));
+        assertEquals(user.getEmails().get(0).getValue(), 
userTO.getPlainAttr("userId").get().getValues().get(0));
+        assertEquals(user.getEmails().get(1).getValue(), 
userTO.getPlainAttr("email").get().getValues().get(0));
+        assertEquals(user.getExtensionInfo().getAttributes().get("gender"),
+                userTO.getPlainAttr("gender").get().getValues().get(0));
+
+        response = webClient().path("Users").path(user.getId()).delete();
+        assertEquals(Response.Status.NO_CONTENT.getStatusCode(), 
response.getStatus());
+
+        response = webClient().path("Users").path(user.getId()).get();
+        assertEquals(Response.Status.NOT_FOUND.getStatusCode(), 
response.getStatus());
+        CONF.setExtensionUserConf(null);
+        SCIM_CONF_SERVICE.set(CONF);
+    }
+
     @Test
     public void updateUser() {
         SCIM_CONF_SERVICE.set(CONF);
 
-        SCIMUser user = getSampleUser(UUID.randomUUID().toString());
+        SCIMUser user = getSampleUser(UUID.randomUUID().toString(), 
List.of(Resource.User.schema()));
 
         Response response = webClient().path("Users").post(user);
         assertEquals(Response.Status.CREATED.getStatusCode(), 
response.getStatus());
@@ -531,7 +620,7 @@ public class SCIMITCase extends AbstractITCase {
     public void replaceUser() {
         SCIM_CONF_SERVICE.set(CONF);
 
-        SCIMUser user = getSampleUser(UUID.randomUUID().toString());
+        SCIMUser user = getSampleUser(UUID.randomUUID().toString(), 
List.of(Resource.User.schema()));
 
         Response response = webClient().path("Users").post(user);
         assertEquals(Response.Status.CREATED.getStatusCode(), 
response.getStatus());
@@ -552,7 +641,7 @@ public class SCIMITCase extends AbstractITCase {
     public void deleteUser() {
         SCIM_CONF_SERVICE.set(CONF);
 
-        SCIMUser user = getSampleUser(UUID.randomUUID().toString());
+        SCIMUser user = getSampleUser(UUID.randomUUID().toString(), 
List.of(Resource.User.schema()));
 
         Response response = webClient().path("Users").post(user);
         assertEquals(Response.Status.CREATED.getStatusCode(), 
response.getStatus());


Reply via email to