You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@syncope.apache.org by an...@apache.org on 2017/04/12 10:15:57 UTC

syncope git commit: [SYNCOPE-1064] improved security to avoid JS hacking and exploitation, added relative tests and moved form customization server side

Repository: syncope
Updated Branches:
  refs/heads/master 7a08dc65a -> f1329b799


[SYNCOPE-1064] improved security to avoid JS hacking and exploitation, added relative tests and moved form customization server side


Project: http://git-wip-us.apache.org/repos/asf/syncope/repo
Commit: http://git-wip-us.apache.org/repos/asf/syncope/commit/f1329b79
Tree: http://git-wip-us.apache.org/repos/asf/syncope/tree/f1329b79
Diff: http://git-wip-us.apache.org/repos/asf/syncope/diff/f1329b79

Branch: refs/heads/master
Commit: f1329b79966d1986bbbfe031e46d61469468abd1
Parents: 7a08dc6
Author: Andrea Patricelli <an...@apache.org>
Authored: Wed Apr 12 11:21:56 2017 +0200
Committer: Andrea Patricelli <an...@apache.org>
Committed: Wed Apr 12 12:14:20 2017 +0200

----------------------------------------------------------------------
 archetype/pom.xml                               |   2 +
 .../archetype-resources/enduser/pom.xml         |   4 +
 client/enduser/pom.xml                          |   8 +
 .../enduser/SyncopeEnduserApplication.java      | 101 ++++++++++
 .../client/enduser/SyncopeEnduserConstants.java |   2 +
 .../client/enduser/SyncopeEnduserSession.java   |  15 ++
 .../enduser/adapters/PlatformInfoAdapter.java   |   6 +-
 .../client/enduser/model/CustomAttribute.java   |  62 ++++++
 .../enduser/model/CustomAttributesInfo.java     |   8 +-
 .../enduser/model/PlatformInfoRequest.java      |  11 +
 .../enduser/resources/BaseUserSelfResource.java |  14 ++
 .../client/enduser/resources/InfoResource.java  |  12 +-
 .../enduser/resources/SchemaResource.java       |  14 +-
 .../resources/UserSelfCreateResource.java       | 200 ++++++++++---------
 .../enduser/resources/UserSelfReadResource.java |  40 +++-
 .../resources/UserSelfUpdateResource.java       | 188 +++++++++--------
 .../enduser/util/UserRequestValidator.java      |  86 ++++++++
 .../resources/app/configuration/customForm.json |   1 -
 .../resources/META-INF/resources/app/js/app.js  |  15 +-
 .../app/js/controllers/UserController.js        |   2 +-
 .../app/js/services/configurationService.js     |  41 ----
 .../resources/app/js/services/schemaService.js  |  12 +-
 .../enduser/src/main/resources/customForm.json  |   1 +
 .../enduser/util/UserRequestValidatorTest.java  |  85 ++++++++
 .../enduser/src/test/resources/customForm.json  |  47 +++++
 deb/enduser/pom.xml                             |   1 +
 deb/enduser/src/deb/control/conffiles           |   1 +
 .../src/main/resources/customForm.json          |   1 +
 .../src/test/resources/customForm.json          |   1 +
 29 files changed, 711 insertions(+), 270 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/archetype/pom.xml
----------------------------------------------------------------------
diff --git a/archetype/pom.xml b/archetype/pom.xml
index f996dcc..1db4417 100644
--- a/archetype/pom.xml
+++ b/archetype/pom.xml
@@ -257,6 +257,7 @@ under the License.
         <targetPath>${project.build.outputDirectory}/archetype-resources/enduser/src/main/resources</targetPath>
         <includes>
           <include>enduser.properties</include>
+          <include>customForm.json</include>
         </includes>
       </resource>
       <resource>
@@ -283,6 +284,7 @@ under the License.
         <includes>
           <include>enduser.properties</include>
           <include>saml2sp-agent.properties</include>
+          <include>customForm.json</include>
         </includes>
       </resource>
       <resource>

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/archetype/src/main/resources/archetype-resources/enduser/pom.xml
----------------------------------------------------------------------
diff --git a/archetype/src/main/resources/archetype-resources/enduser/pom.xml b/archetype/src/main/resources/archetype-resources/enduser/pom.xml
index 36bbf0a..06f77d4 100644
--- a/archetype/src/main/resources/archetype-resources/enduser/pom.xml
+++ b/archetype/src/main/resources/archetype-resources/enduser/pom.xml
@@ -246,6 +246,10 @@ under the License.
                     <copy file="${project.build.directory}/test-classes/enduser.properties" 
                           todir="${project.build.directory}/${project.build.finalName}/WEB-INF/classes" 
                           overwrite="true"/>
+                    
+                    <copy file="${project.build.directory}/test-classes/customForm" 
+                                todir="${project.build.directory}/${project.build.finalName}/WEB-INF/classes" 
+                                overwrite="true"/>
                   </target>
                 </configuration>
                 <goals>

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/client/enduser/pom.xml
----------------------------------------------------------------------
diff --git a/client/enduser/pom.xml b/client/enduser/pom.xml
index 99d4c0b..52fe436 100644
--- a/client/enduser/pom.xml
+++ b/client/enduser/pom.xml
@@ -182,6 +182,14 @@ under the License.
       <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-core</artifactId>
     </dependency>
+    
+    <!-- TEST -->
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    
   </dependencies>
   
   <build>

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserApplication.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserApplication.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserApplication.java
index cfed2c5..fc0d730 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserApplication.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserApplication.java
@@ -18,16 +18,28 @@
  */
 package org.apache.syncope.client.enduser;
 
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
 import java.io.InputStream;
 import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
 import org.apache.syncope.client.enduser.pages.HomePage;
 import java.util.Properties;
 import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.monitor.FileAlterationListener;
+import org.apache.commons.io.monitor.FileAlterationListenerAdaptor;
+import org.apache.commons.io.monitor.FileAlterationMonitor;
+import org.apache.commons.io.monitor.FileAlterationObserver;
 import org.apache.commons.lang3.BooleanUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.client.enduser.annotations.Resource;
 import org.apache.syncope.client.enduser.init.ClassPathScanImplementationLookup;
 import org.apache.syncope.client.enduser.init.EnduserInitializer;
+import org.apache.syncope.client.enduser.model.CustomAttributesInfo;
 import org.apache.syncope.client.enduser.resources.CaptchaResource;
 import org.apache.syncope.client.lib.SyncopeClientFactoryBean;
 import org.apache.syncope.common.lib.SyncopeConstants;
@@ -52,6 +64,8 @@ public class SyncopeEnduserApplication extends WebApplication implements Seriali
 
     private static final String ENDUSER_PROPERTIES = "enduser.properties";
 
+    private static final String CUSTOM_FORM_FILE = "customForm.json";
+
     public static SyncopeEnduserApplication get() {
         return (SyncopeEnduserApplication) WebApplication.get();
     }
@@ -76,6 +90,10 @@ public class SyncopeEnduserApplication extends WebApplication implements Seriali
 
     private SyncopeClientFactoryBean clientFactory;
 
+    private Map<String, CustomAttributesInfo> customForm;
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
     @Override
     protected void init() {
         super.init();
@@ -131,6 +149,80 @@ public class SyncopeEnduserApplication extends WebApplication implements Seriali
                 setContentType(SyncopeClientFactoryBean.ContentType.JSON).
                 setUseCompression(BooleanUtils.toBoolean(useGZIPCompression));
 
+        // read customForm.json
+        try (InputStream is = getClass().getResourceAsStream("/" + CUSTOM_FORM_FILE)) {
+            customForm = MAPPER.readValue(is,
+                    new TypeReference<HashMap<String, CustomAttributesInfo>>() {
+            });
+            File enduserDir = new File(props.getProperty("enduser.directory"));
+            boolean existsEnduserDir = enduserDir.exists() && enduserDir.canRead() && enduserDir.isDirectory();
+            if (existsEnduserDir) {
+                File customFormFile = FileUtils.getFile(enduserDir, CUSTOM_FORM_FILE);
+                if (customFormFile.exists() && customFormFile.canRead() && customFormFile.isFile()) {
+                    customForm = MAPPER.readValue(FileUtils.openInputStream(customFormFile),
+                            new TypeReference<HashMap<String, CustomAttributesInfo>>() {
+                    });
+                }
+            }
+            FileAlterationObserver observer = existsEnduserDir
+                    ? new FileAlterationObserver(enduserDir, new FileFilter() {
+
+                        @Override
+                        public boolean accept(final File pathname) {
+                            return StringUtils.contains(pathname.getPath(), CUSTOM_FORM_FILE);
+                        }
+                    })
+                    : new FileAlterationObserver(getClass().getResource("/" + CUSTOM_FORM_FILE).getFile(),
+                            new FileFilter() {
+
+                        @Override
+                        public boolean accept(final File pathname) {
+                            return StringUtils.contains(pathname.getPath(), CUSTOM_FORM_FILE);
+                        }
+                    });
+
+            FileAlterationMonitor monitor = new FileAlterationMonitor(5000);
+
+            FileAlterationListener listener = new FileAlterationListenerAdaptor() {
+
+                @Override
+                public void onFileChange(final File file) {
+                    try {
+                        LOG.trace("{} has changed. Reloading form customization configuration.", CUSTOM_FORM_FILE);
+                        customForm = MAPPER.readValue(FileUtils.openInputStream(file),
+                                new TypeReference<HashMap<String, CustomAttributesInfo>>() {
+                        });
+                    } catch (IOException e) {
+                        e.printStackTrace(System.err);
+                    }
+                }
+
+                @Override
+                public void onFileCreate(final File file) {
+                    try {
+                        LOG.trace("{} has been created. Loading form customization configuration.", CUSTOM_FORM_FILE);
+                        customForm = MAPPER.readValue(FileUtils.openInputStream(file),
+                                new TypeReference<HashMap<String, CustomAttributesInfo>>() {
+                        });
+                    } catch (IOException e) {
+                        e.printStackTrace(System.err);
+                    }
+                }
+
+                @Override
+                public void onFileDelete(final File file) {
+                    LOG.trace("{} has been deleted. Resetting form customization configuration.", CUSTOM_FORM_FILE);
+                    customForm = null;
+                }
+            };
+
+            observer.addListener(listener);
+            monitor.addObserver(observer);
+            monitor.start();
+        } catch (Exception e) {
+            throw new WicketRuntimeException("Could not read " + CUSTOM_FORM_FILE, e);
+        }
+
         // mount resources
         ClassPathScanImplementationLookup classPathScanImplementationLookup =
                 (ClassPathScanImplementationLookup) getServletContext().
@@ -221,4 +313,13 @@ public class SyncopeEnduserApplication extends WebApplication implements Seriali
         return xsrfEnabled;
     }
 
+    public Map<String, CustomAttributesInfo> getCustomForm() {
+        return customForm;
+    }
+
+    public void setCustomForm(final Map<String, CustomAttributesInfo> customForm) {
+        this.customForm.clear();
+        this.customForm.putAll(customForm);
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserConstants.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserConstants.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserConstants.java
index 4600166..0a683a9 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserConstants.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserConstants.java
@@ -26,6 +26,8 @@ public final class SyncopeEnduserConstants {
 
     public static final String XSRF_HEADER_NAME = "X-XSRF-TOKEN";
 
+    public static final String MEMBERSHIP_ATTR_SEPARATOR = "#";
+
     private SyncopeEnduserConstants() {
         // private constructor for utility class
     }

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserSession.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserSession.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserSession.java
index 168656c..8268ef5 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserSession.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserSession.java
@@ -19,6 +19,7 @@
 package org.apache.syncope.client.enduser;
 
 import java.security.AccessControlException;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -28,6 +29,7 @@ import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.collections4.Predicate;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.cxf.jaxrs.client.WebClient;
+import org.apache.syncope.client.enduser.model.CustomAttributesInfo;
 import org.apache.syncope.client.lib.AnonymousAuthenticationHandler;
 import org.apache.syncope.client.lib.SyncopeClient;
 import org.apache.syncope.common.lib.info.PlatformInfo;
@@ -66,6 +68,8 @@ public class SyncopeEnduserSession extends WebSession {
 
     private final CookieUtils cookieUtils;
 
+    private final Map<String, CustomAttributesInfo> customForm;
+
     private boolean xsrfTokenGenerated = false;
 
     public static SyncopeEnduserSession get() {
@@ -92,6 +96,7 @@ public class SyncopeEnduserSession extends WebSession {
                 return object.getType() == AttrSchemaType.Date;
             }
         });
+        customForm = new HashMap<>();
     }
 
     private void afterAuthentication() {
@@ -197,4 +202,14 @@ public class SyncopeEnduserSession extends WebSession {
     public void setXsrfTokenGenerated(final boolean xsrfTokenGenerated) {
         this.xsrfTokenGenerated = xsrfTokenGenerated;
     }
+
+    public Map<String, CustomAttributesInfo> getCustomForm() {
+        return customForm;
+    }
+
+    public void setCustomForm(final Map<String, CustomAttributesInfo> customForm) {
+        this.customForm.clear();
+        this.customForm.putAll(customForm);
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/client/enduser/src/main/java/org/apache/syncope/client/enduser/adapters/PlatformInfoAdapter.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/adapters/PlatformInfoAdapter.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/adapters/PlatformInfoAdapter.java
index 5ed11be..a961576 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/adapters/PlatformInfoAdapter.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/adapters/PlatformInfoAdapter.java
@@ -18,19 +18,23 @@
  */
 package org.apache.syncope.client.enduser.adapters;
 
+import java.util.Map;
 import org.apache.syncope.client.enduser.SyncopeEnduserApplication;
+import org.apache.syncope.client.enduser.model.CustomAttributesInfo;
 import org.apache.syncope.client.enduser.model.PlatformInfoRequest;
 import org.apache.syncope.common.lib.info.PlatformInfo;
 
 public final class PlatformInfoAdapter {
 
-    public static PlatformInfoRequest toPlatformInfoRequest(final PlatformInfo platformInfo) {
+    public static PlatformInfoRequest toPlatformInfoRequest(final PlatformInfo platformInfo,
+            final Map<String, CustomAttributesInfo> customForm) {
         PlatformInfoRequest request = new PlatformInfoRequest();
         request.setPwdResetAllowed(platformInfo.isPwdResetAllowed());
         request.setSelfRegAllowed(platformInfo.isSelfRegAllowed());
         request.setPwdResetRequiringSecurityQuestions(platformInfo.isPwdResetRequiringSecurityQuestions());
         request.setVersion(platformInfo.getVersion());
         request.setCaptchaEnabled(SyncopeEnduserApplication.get().isCaptchaEnabled());
+        request.setCustomForm(customForm);
 
         return request;
     }

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomAttribute.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomAttribute.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomAttribute.java
new file mode 100644
index 0000000..80d65f6
--- /dev/null
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomAttribute.java
@@ -0,0 +1,62 @@
+/*
+ * 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.enduser.model;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+public class CustomAttribute implements Serializable {
+
+    private static final long serialVersionUID = 4910266842123376686L;
+
+    private Boolean readonly;
+
+    private List<String> defaultValues = new ArrayList<>();
+
+    public CustomAttribute() {
+    }
+
+    public Boolean getReadonly() {
+        return readonly;
+    }
+
+    public void setReadonly(final Boolean readonly) {
+        this.readonly = readonly;
+    }
+
+    public List<String> getDefaultValues() {
+        return defaultValues;
+    }
+
+    public void setDefaultValues(final List<String> defaultValues) {
+        this.defaultValues = defaultValues;
+    }
+
+    public CustomAttribute readonly(final Boolean value) {
+        this.readonly = value;
+        return this;
+    }
+
+    public CustomAttribute defaultValues(final List<String> value) {
+        this.defaultValues = value;
+        return this;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomAttributesInfo.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomAttributesInfo.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomAttributesInfo.java
index 61d08f1..a62a6bb 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomAttributesInfo.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomAttributesInfo.java
@@ -28,7 +28,7 @@ public class CustomAttributesInfo implements Serializable {
 
     private Boolean show = Boolean.TRUE;
 
-    private Map<String, ?> attributes = new LinkedHashMap<>();
+    private Map<String, CustomAttribute> attributes = new LinkedHashMap<>();
 
     public CustomAttributesInfo() {
     }
@@ -41,11 +41,11 @@ public class CustomAttributesInfo implements Serializable {
         this.show = show;
     }
 
-    public Map<String, ?> getAttributes() {
+    public Map<String, CustomAttribute> getAttributes() {
         return attributes;
     }
 
-    public void setAttributes(final Map<String, ?> attributes) {
+    public void setAttributes(final Map<String, CustomAttribute> attributes) {
         this.attributes = attributes;
     }
 
@@ -54,7 +54,7 @@ public class CustomAttributesInfo implements Serializable {
         return this;
     }
 
-    public CustomAttributesInfo attributes(final Map<String, ?> value) {
+    public CustomAttributesInfo attributes(final Map<String, CustomAttribute> value) {
         this.attributes = value;
         return this;
     }

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/PlatformInfoRequest.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/PlatformInfoRequest.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/PlatformInfoRequest.java
index b95706c..a75ae53 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/PlatformInfoRequest.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/PlatformInfoRequest.java
@@ -19,6 +19,7 @@
 package org.apache.syncope.client.enduser.model;
 
 import java.io.Serializable;
+import java.util.Map;
 
 public class PlatformInfoRequest implements Serializable {
 
@@ -34,6 +35,8 @@ public class PlatformInfoRequest implements Serializable {
 
     private boolean captchaEnabled;
 
+    private Map<String, CustomAttributesInfo> customForm;
+
     public PlatformInfoRequest() {
     }
 
@@ -77,4 +80,12 @@ public class PlatformInfoRequest implements Serializable {
         this.captchaEnabled = captchaEnabled;
     }
 
+    public Map<String, CustomAttributesInfo> getCustomForm() {
+        return customForm;
+    }
+
+    public void setCustomForm(final Map<String, CustomAttributesInfo> customForm) {
+        this.customForm = customForm;
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/BaseUserSelfResource.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/BaseUserSelfResource.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/BaseUserSelfResource.java
index 6071642..a3e73e8 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/BaseUserSelfResource.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/BaseUserSelfResource.java
@@ -18,6 +18,8 @@
  */
 package org.apache.syncope.client.enduser.resources;
 
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.List;
@@ -63,5 +65,17 @@ public abstract class BaseUserSelfResource extends BaseResource {
             dateAttr.getValues().addAll(formattedValues);
         }
     }
+    
+    protected void buildResponse(final ResourceResponse response, final int statusCode, final String message) {
+        response.setTextEncoding(StandardCharsets.UTF_8.name());
+        response.setStatusCode(statusCode);
+        response.setWriteCallback(new WriteCallback() {
+
+            @Override
+            public void writeData(final Attributes attributes) throws IOException {
+                attributes.getResponse().write(message);
+            }
+        });
+    }
 
 }

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/InfoResource.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/InfoResource.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/InfoResource.java
index bd4d30f..c96fa2a 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/InfoResource.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/InfoResource.java
@@ -18,15 +18,21 @@
  */
 package org.apache.syncope.client.enduser.resources;
 
+import static org.apache.syncope.client.enduser.resources.BaseResource.MAPPER;
+
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.client.enduser.SyncopeEnduserApplication;
 import org.apache.syncope.client.enduser.SyncopeEnduserConstants;
 import org.apache.syncope.client.enduser.SyncopeEnduserSession;
 import org.apache.syncope.client.enduser.adapters.PlatformInfoAdapter;
 import org.apache.syncope.client.enduser.annotations.Resource;
+import org.apache.syncope.client.enduser.model.CustomAttributesInfo;
 import org.apache.syncope.client.enduser.util.SaltGenerator;
 import org.apache.wicket.request.resource.IResource;
 import org.apache.wicket.util.cookies.CookieUtils;
@@ -57,10 +63,14 @@ public class InfoResource extends BaseResource {
 
                 @Override
                 public void writeData(final IResource.Attributes attributes) throws IOException {
+                    Map<String, CustomAttributesInfo> customForm = SyncopeEnduserApplication.get().getCustomForm();
                     attributes.getResponse().write(
                             MAPPER.writeValueAsString(
                                     PlatformInfoAdapter.toPlatformInfoRequest(
-                                            SyncopeEnduserSession.get().getPlatformInfo())));
+                                            SyncopeEnduserSession.get().getPlatformInfo(),
+                                            customForm == null
+                                                    ? new HashMap<String, CustomAttributesInfo>()
+                                                    : customForm)));
                 }
             });
             response.setStatusCode(Response.Status.OK.getStatusCode());

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/SchemaResource.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/SchemaResource.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/SchemaResource.java
index 169d1b4..5edd4b0 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/SchemaResource.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/SchemaResource.java
@@ -20,13 +20,11 @@ package org.apache.syncope.client.enduser.resources;
 
 import static org.apache.syncope.client.enduser.resources.BaseResource.MAPPER;
 
-import com.fasterxml.jackson.core.type.TypeReference;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import javax.servlet.http.HttpServletRequest;
@@ -36,8 +34,11 @@ import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.collections4.IterableUtils;
 import org.apache.commons.collections4.Predicate;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.client.enduser.SyncopeEnduserApplication;
+import org.apache.syncope.client.enduser.SyncopeEnduserConstants;
 import org.apache.syncope.client.enduser.SyncopeEnduserSession;
 import org.apache.syncope.client.enduser.annotations.Resource;
+import org.apache.syncope.client.enduser.model.CustomAttribute;
 import org.apache.syncope.client.enduser.model.CustomAttributesInfo;
 import org.apache.syncope.client.enduser.model.SchemaResponse;
 import org.apache.syncope.common.lib.to.AbstractSchemaTO;
@@ -103,9 +104,8 @@ public class SchemaResource extends BaseResource {
                 }
             }
 
-            Map<String, CustomAttributesInfo> customForm = MAPPER.readValue(request.getReader().readLine(),
-                    new TypeReference<HashMap<String, CustomAttributesInfo>>() {
-            });
+            // USER from customization, if empty or null ignore it, use it to filter attributes otherwise
+            Map<String, CustomAttributesInfo> customForm = SyncopeEnduserApplication.get().getCustomForm();
 
             SchemaService schemaService = SyncopeEnduserSession.get().getService(SchemaService.class);
             final List<AbstractSchemaTO> plainSchemas = classes.isEmpty()
@@ -176,7 +176,7 @@ public class SchemaResource extends BaseResource {
     }
 
     private List<AbstractSchemaTO> customizeSchemas(final List<AbstractSchemaTO> schemaTOs, final String groupParam,
-            final Map<String, ?> customForm) {
+            final Map<String, CustomAttribute> customForm) {
 
         if (customForm.isEmpty()) {
             return schemaTOs;
@@ -211,7 +211,7 @@ public class SchemaResource extends BaseResource {
     }
 
     private String compositeSchemaKey(final String prefix, final String schemaKey) {
-        return prefix + "#" + schemaKey;
+        return prefix + SyncopeEnduserConstants.MEMBERSHIP_ATTR_SEPARATOR + schemaKey;
     }
 
 }

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfCreateResource.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfCreateResource.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfCreateResource.java
index fbb2e33..b7775b1 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfCreateResource.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfCreateResource.java
@@ -20,8 +20,6 @@ package org.apache.syncope.client.enduser.resources;
 
 import static org.apache.syncope.client.enduser.resources.BaseResource.LOG;
 
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
@@ -34,6 +32,7 @@ import org.apache.commons.lang3.SerializationUtils;
 import org.apache.syncope.client.enduser.SyncopeEnduserConstants;
 import org.apache.syncope.client.enduser.SyncopeEnduserSession;
 import org.apache.syncope.client.enduser.annotations.Resource;
+import org.apache.syncope.client.enduser.util.UserRequestValidator;
 import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.to.AttrTO;
 import org.apache.syncope.common.lib.to.MembershipTO;
@@ -83,113 +82,118 @@ public class UserSelfCreateResource extends BaseUserSelfResource {
             }
 
             if (isSelfRegistrationAllowed() && userTO != null) {
-
-                // 1. membership attributes management
-                Set<AttrTO> membAttrs = new HashSet<>();
-                for (AttrTO attr : userTO.getPlainAttrs()) {
-                    if (attr.getSchema().contains("#")) {
-                        final String[] simpleAttrs = attr.getSchema().split("#");
-                        MembershipTO membership = IterableUtils.find(userTO.getMemberships(),
-                                new Predicate<MembershipTO>() {
-
-                            @Override
-                            public boolean evaluate(final MembershipTO item) {
-                                return simpleAttrs[0].equals(item.getGroupName());
+                LOG.debug("User self registration request for [{}]", userTO.getUsername());
+                LOG.trace("Request is [{}]", userTO);
+
+                // check if request is compliant with customization form rules
+                if (UserRequestValidator.compliant(userTO, SyncopeEnduserSession.get().getCustomForm(), true)) {
+
+                    // 1. membership attributes management
+                    Set<AttrTO> membAttrs = new HashSet<>();
+                    for (AttrTO attr : userTO.getPlainAttrs()) {
+                        if (attr.getSchema().contains(SyncopeEnduserConstants.MEMBERSHIP_ATTR_SEPARATOR)) {
+                            final String[] simpleAttrs = attr.getSchema().split(
+                                    SyncopeEnduserConstants.MEMBERSHIP_ATTR_SEPARATOR);
+                            MembershipTO membership = IterableUtils.find(userTO.getMemberships(),
+                                    new Predicate<MembershipTO>() {
+
+                                @Override
+                                public boolean evaluate(final MembershipTO item) {
+                                    return simpleAttrs[0].equals(item.getGroupName());
+                                }
+                            });
+                            if (membership == null) {
+                                membership = new MembershipTO.Builder().group(null, simpleAttrs[0]).build();
+                                userTO.getMemberships().add(membership);
                             }
-                        });
-                        if (membership == null) {
-                            membership = new MembershipTO.Builder().group(null, simpleAttrs[0]).build();
-                            userTO.getMemberships().add(membership);
-                        }
 
-                        AttrTO clone = SerializationUtils.clone(attr);
-                        clone.setSchema(simpleAttrs[1]);
-                        membership.getPlainAttrs().add(clone);
-                        membAttrs.add(attr);
+                            AttrTO clone = SerializationUtils.clone(attr);
+                            clone.setSchema(simpleAttrs[1]);
+                            membership.getPlainAttrs().add(clone);
+                            membAttrs.add(attr);
+                        }
                     }
-                }
-                userTO.getPlainAttrs().removeAll(membAttrs);
-
-                // 2. millis -> Date conversion for PLAIN attributes of USER and its MEMBERSHIPS
-                Map<String, AttrTO> userPlainAttrMap = userTO.getPlainAttrMap();
-                for (PlainSchemaTO plainSchema : SyncopeEnduserSession.get().getDatePlainSchemas()) {
-                    millisToDate(userPlainAttrMap, plainSchema);
-                    for (MembershipTO membership : userTO.getMemberships()) {
-                        millisToDate(membership.getPlainAttrMap(), plainSchema);
+                    userTO.getPlainAttrs().removeAll(membAttrs);
+
+                    // 2. millis -> Date conversion for PLAIN attributes of USER and its MEMBERSHIPS
+                    Map<String, AttrTO> userPlainAttrMap = userTO.getPlainAttrMap();
+                    for (PlainSchemaTO plainSchema : SyncopeEnduserSession.get().getDatePlainSchemas()) {
+                        millisToDate(userPlainAttrMap, plainSchema);
+                        for (MembershipTO membership : userTO.getMemberships()) {
+                            millisToDate(membership.getPlainAttrMap(), plainSchema);
+                        }
                     }
-                }
-
-                membAttrs.clear();
-                for (AttrTO attr : userTO.getDerAttrs()) {
-                    if (attr.getSchema().contains("#")) {
-                        final String[] simpleAttrs = attr.getSchema().split("#");
-                        MembershipTO membership = IterableUtils.find(userTO.getMemberships(),
-                                new Predicate<MembershipTO>() {
 
-                            @Override
-                            public boolean evaluate(final MembershipTO item) {
-                                return simpleAttrs[0].equals(item.getGroupName());
+                    membAttrs.clear();
+                    for (AttrTO attr : userTO.getDerAttrs()) {
+                        if (attr.getSchema().contains(SyncopeEnduserConstants.MEMBERSHIP_ATTR_SEPARATOR)) {
+                            final String[] simpleAttrs = attr.getSchema().split(
+                                    SyncopeEnduserConstants.MEMBERSHIP_ATTR_SEPARATOR);
+                            MembershipTO membership = IterableUtils.find(userTO.getMemberships(),
+                                    new Predicate<MembershipTO>() {
+
+                                @Override
+                                public boolean evaluate(final MembershipTO item) {
+                                    return simpleAttrs[0].equals(item.getGroupName());
+                                }
+                            });
+                            if (membership == null) {
+                                membership = new MembershipTO.Builder().group(null, simpleAttrs[0]).build();
+                                userTO.getMemberships().add(membership);
                             }
-                        });
-                        if (membership == null) {
-                            membership = new MembershipTO.Builder().group(null, simpleAttrs[0]).build();
-                            userTO.getMemberships().add(membership);
-                        }
 
-                        AttrTO clone = SerializationUtils.clone(attr);
-                        clone.setSchema(simpleAttrs[1]);
-                        membership.getDerAttrs().add(clone);
-                        membAttrs.add(attr);
+                            AttrTO clone = SerializationUtils.clone(attr);
+                            clone.setSchema(simpleAttrs[1]);
+                            membership.getDerAttrs().add(clone);
+                            membAttrs.add(attr);
+                        }
                     }
-                }
-                userTO.getDerAttrs().removeAll(membAttrs);
-
-                membAttrs.clear();
-                for (AttrTO attr : userTO.getVirAttrs()) {
-                    if (attr.getSchema().contains("#")) {
-                        final String[] simpleAttrs = attr.getSchema().split("#");
-                        MembershipTO membership = IterableUtils.find(userTO.getMemberships(),
-                                new Predicate<MembershipTO>() {
-
-                            @Override
-                            public boolean evaluate(final MembershipTO item) {
-                                return simpleAttrs[0].equals(item.getGroupName());
+                    userTO.getDerAttrs().removeAll(membAttrs);
+
+                    membAttrs.clear();
+                    for (AttrTO attr : userTO.getVirAttrs()) {
+                        if (attr.getSchema().contains(SyncopeEnduserConstants.MEMBERSHIP_ATTR_SEPARATOR)) {
+                            final String[] simpleAttrs = attr.getSchema().split(
+                                    SyncopeEnduserConstants.MEMBERSHIP_ATTR_SEPARATOR);
+                            MembershipTO membership = IterableUtils.find(userTO.getMemberships(),
+                                    new Predicate<MembershipTO>() {
+
+                                @Override
+                                public boolean evaluate(final MembershipTO item) {
+                                    return simpleAttrs[0].equals(item.getGroupName());
+                                }
+                            });
+                            if (membership == null) {
+                                membership = new MembershipTO.Builder().group(null, simpleAttrs[0]).build();
+                                userTO.getMemberships().add(membership);
                             }
-                        });
-                        if (membership == null) {
-                            membership = new MembershipTO.Builder().group(null, simpleAttrs[0]).build();
-                            userTO.getMemberships().add(membership);
-                        }
 
-                        AttrTO clone = SerializationUtils.clone(attr);
-                        clone.setSchema(simpleAttrs[1]);
-                        membership.getVirAttrs().add(clone);
-                        membAttrs.add(attr);
+                            AttrTO clone = SerializationUtils.clone(attr);
+                            clone.setSchema(simpleAttrs[1]);
+                            membership.getVirAttrs().add(clone);
+                            membAttrs.add(attr);
+                        }
                     }
+                    userTO.getVirAttrs().removeAll(membAttrs);
+
+                    LOG.debug("Received user self registration request for user: [{}]", userTO.getUsername());
+                    LOG.trace("Received user self registration request is: [{}]", userTO);
+
+                    // adapt request and create user
+                    final Response res = SyncopeEnduserSession.get().getService(UserSelfService.class).create(userTO,
+                            true);
+
+                    buildResponse(response, res.getStatus(),
+                            Response.Status.Family.SUCCESSFUL.equals(res.getStatusInfo().getFamily())
+                            ? "User[ " + userTO.getUsername() + "] successfully created"
+                            : "ErrorMessage{{ " + res.getStatusInfo().getReasonPhrase() + " }}");
+                } else {
+                    LOG.warn(
+                            "Incoming create request [{}] is not compliant with form customization rules. "
+                            + "Create NOT allowed", userTO.getUsername());
+                    buildResponse(response, Response.Status.OK.getStatusCode(),
+                            "User: " + userTO.getUsername() + " successfully created");
                 }
-                userTO.getVirAttrs().removeAll(membAttrs);
-
-                LOG.debug("Received user self registration request for user: [{}]", userTO.getUsername());
-                LOG.trace("Received user self registration request is: [{}]", userTO);
-
-                // adapt request and create user
-                final Response res = SyncopeEnduserSession.get().getService(UserSelfService.class).create(userTO, true);
-
-                response.setTextEncoding(StandardCharsets.UTF_8.name());
-
-                response.setWriteCallback(new WriteCallback() {
-
-                    @Override
-                    public void writeData(final Attributes attributes) throws IOException {
-                        attributes.getResponse().write(res.getStatusInfo().getFamily().equals(
-                                Response.Status.Family.SUCCESSFUL)
-                                        ? new StringBuilder().append("User: ").append(userTO.getUsername()).
-                                                append(" successfully created")
-                                        : new StringBuilder().append("ErrorMessage{{ ").
-                                                append(res.getStatusInfo().getReasonPhrase()).append(" }}"));
-                    }
-                });
-                response.setStatusCode(res.getStatus());
             } else {
                 response.setError(Response.Status.FORBIDDEN.getStatusCode(), new StringBuilder().
                         append("ErrorMessage{{").append(userTO == null
@@ -198,7 +202,7 @@ public class UserSelfCreateResource extends BaseUserSelfResource {
             }
 
         } catch (Exception e) {
-            LOG.error("Could not create userTO", e);
+            LOG.error("Unable to create userTO", e);
             response.setError(Response.Status.BAD_REQUEST.getStatusCode(),
                     new StringBuilder().
                             append("ErrorMessage{{ ").

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfReadResource.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfReadResource.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfReadResource.java
index 056e757..81ffcd1 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfReadResource.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfReadResource.java
@@ -18,19 +18,29 @@
  */
 package org.apache.syncope.client.enduser.resources;
 
+import static org.apache.syncope.client.enduser.resources.BaseResource.MAPPER;
+
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.util.Map;
+import java.util.Set;
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.collections4.Predicate;
 import org.apache.commons.lang3.SerializationUtils;
+import org.apache.syncope.client.enduser.SyncopeEnduserApplication;
+import org.apache.syncope.client.enduser.SyncopeEnduserConstants;
 import org.apache.syncope.client.enduser.SyncopeEnduserSession;
 import org.apache.syncope.client.enduser.annotations.Resource;
+import org.apache.syncope.client.enduser.model.CustomAttribute;
+import org.apache.syncope.client.enduser.model.CustomAttributesInfo;
 import org.apache.syncope.common.lib.to.AttrTO;
 import org.apache.syncope.common.lib.to.MembershipTO;
 import org.apache.syncope.common.lib.to.PlainSchemaTO;
 import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.syncope.common.lib.types.SchemaType;
 import org.apache.wicket.request.resource.AbstractResource;
 import org.apache.wicket.request.resource.IResource;
 
@@ -68,22 +78,35 @@ public class UserSelfReadResource extends BaseUserSelfResource {
             for (MembershipTO membership : userTO.getMemberships()) {
                 String groupName = membership.getGroupName();
                 for (AttrTO attr : membership.getPlainAttrs()) {
-                    attr.setSchema(groupName.concat("#").concat(attr.getSchema()));
+                    attr.setSchema(groupName.concat(SyncopeEnduserConstants.MEMBERSHIP_ATTR_SEPARATOR).concat(attr.
+                            getSchema()));
                     userTO.getPlainAttrs().add(attr);
                 }
                 membership.getPlainAttrs().clear();
                 for (AttrTO attr : membership.getDerAttrs()) {
-                    attr.setSchema(groupName.concat("#").concat(attr.getSchema()));
+                    attr.setSchema(groupName.concat(SyncopeEnduserConstants.MEMBERSHIP_ATTR_SEPARATOR).concat(attr.
+                            getSchema()));
                     userTO.getDerAttrs().add(attr);
                 }
                 membership.getDerAttrs().clear();
                 for (AttrTO attr : membership.getVirAttrs()) {
-                    attr.setSchema(groupName.concat("#").concat(attr.getSchema()));
+                    attr.setSchema(groupName.concat(SyncopeEnduserConstants.MEMBERSHIP_ATTR_SEPARATOR).concat(attr.
+                            getSchema()));
                     userTO.getVirAttrs().add(attr);
                 }
                 membership.getVirAttrs().clear();
             }
+            // USER from customization, if empty or null ignore it, use it to filter attributes otherwise
+            Map<String, CustomAttributesInfo> customForm = SyncopeEnduserApplication.get().getCustomForm();
 
+            if (customForm != null && !customForm.isEmpty()) {
+                // filter PLAIN attributes
+                customizeAttrs(userTO.getPlainAttrs(), customForm.get(SchemaType.PLAIN.name()).getAttributes());
+                // filter DERIVED attributes
+                customizeAttrs(userTO.getDerAttrs(), customForm.get(SchemaType.DERIVED.name()).getAttributes());
+                // filter VIRTUAL attributes
+                customizeAttrs(userTO.getVirAttrs(), customForm.get(SchemaType.VIRTUAL.name()).getAttributes());
+            }
             final String selfTOJson = MAPPER.writeValueAsString(userTO);
             response.setContentType(MediaType.APPLICATION_JSON);
             response.setTextEncoding(StandardCharsets.UTF_8.name());
@@ -107,4 +130,15 @@ public class UserSelfReadResource extends BaseUserSelfResource {
         return response;
     }
 
+    private void customizeAttrs(final Set<AttrTO> attrs,
+            final Map<String, CustomAttribute> customForm) {
+
+        CollectionUtils.filter(attrs, new Predicate<AttrTO>() {
+
+            @Override
+            public boolean evaluate(final AttrTO attr) {
+                return customForm.containsKey(attr.getSchema());
+            }
+        });
+    }
 }

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfUpdateResource.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfUpdateResource.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfUpdateResource.java
index bc1a9fd..828f323 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfUpdateResource.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfUpdateResource.java
@@ -18,8 +18,6 @@
  */
 package org.apache.syncope.client.enduser.resources;
 
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
@@ -32,6 +30,7 @@ import org.apache.commons.lang3.SerializationUtils;
 import org.apache.syncope.client.enduser.SyncopeEnduserConstants;
 import org.apache.syncope.client.enduser.SyncopeEnduserSession;
 import org.apache.syncope.client.enduser.annotations.Resource;
+import org.apache.syncope.client.enduser.util.UserRequestValidator;
 import org.apache.syncope.common.lib.AnyOperations;
 import org.apache.syncope.common.lib.to.AttrTO;
 import org.apache.syncope.common.lib.to.MembershipTO;
@@ -67,111 +66,110 @@ public class UserSelfUpdateResource extends BaseUserSelfResource {
 
             UserTO userTO = MAPPER.readValue(request.getReader().readLine(), UserTO.class);
 
-            // 1. membership attributes management
-            Set<AttrTO> membAttrs = new HashSet<>();
-            for (AttrTO attr : userTO.getPlainAttrs()) {
-                if (attr.getSchema().contains("#")) {
-                    final String[] compositeSchemaKey = attr.getSchema().split("#");
-                    MembershipTO membership = IterableUtils.find(userTO.getMemberships(),
-                            new Predicate<MembershipTO>() {
-
-                        @Override
-                        public boolean evaluate(final MembershipTO item) {
-                            return compositeSchemaKey[0].equals(item.getGroupName());
+            // check if request is compliant with customization form rules
+            if (UserRequestValidator.compliant(userTO, SyncopeEnduserSession.get().getCustomForm(), false)) {
+                // 1. membership attributes management
+                Set<AttrTO> membAttrs = new HashSet<>();
+                for (AttrTO attr : userTO.getPlainAttrs()) {
+                    if (attr.getSchema().contains(SyncopeEnduserConstants.MEMBERSHIP_ATTR_SEPARATOR)) {
+                        final String[] compositeSchemaKey = attr.getSchema().split(
+                                SyncopeEnduserConstants.MEMBERSHIP_ATTR_SEPARATOR);
+                        MembershipTO membership = IterableUtils.find(userTO.getMemberships(),
+                                new Predicate<MembershipTO>() {
+
+                            @Override
+                            public boolean evaluate(final MembershipTO item) {
+                                return compositeSchemaKey[0].equals(item.getGroupName());
+                            }
+                        });
+                        if (membership == null) {
+                            membership = new MembershipTO.Builder().group(null, compositeSchemaKey[0]).build();
+                            userTO.getMemberships().add(membership);
                         }
-                    });
-                    if (membership == null) {
-                        membership = new MembershipTO.Builder().group(null, compositeSchemaKey[0]).build();
-                        userTO.getMemberships().add(membership);
+                        AttrTO clone = SerializationUtils.clone(attr);
+                        clone.setSchema(compositeSchemaKey[1]);
+                        membership.getPlainAttrs().add(clone);
+                        membAttrs.add(attr);
                     }
-                    AttrTO clone = SerializationUtils.clone(attr);
-                    clone.setSchema(compositeSchemaKey[1]);
-                    membership.getPlainAttrs().add(clone);
-                    membAttrs.add(attr);
                 }
-            }
-            userTO.getPlainAttrs().removeAll(membAttrs);
-
-            // 2. millis -> Date conversion for PLAIN attributes of USER and its MEMBERSHIPS
-            Map<String, AttrTO> userPlainAttrMap = userTO.getPlainAttrMap();
-            for (PlainSchemaTO plainSchema : SyncopeEnduserSession.get().getDatePlainSchemas()) {
-                millisToDate(userPlainAttrMap, plainSchema);
-                for (MembershipTO membership : userTO.getMemberships()) {
-                    millisToDate(membership.getPlainAttrMap(), plainSchema);
+                userTO.getPlainAttrs().removeAll(membAttrs);
+
+                // 2. millis -> Date conversion for PLAIN attributes of USER and its MEMBERSHIPS
+                Map<String, AttrTO> userPlainAttrMap = userTO.getPlainAttrMap();
+                for (PlainSchemaTO plainSchema : SyncopeEnduserSession.get().getDatePlainSchemas()) {
+                    millisToDate(userPlainAttrMap, plainSchema);
+                    for (MembershipTO membership : userTO.getMemberships()) {
+                        millisToDate(membership.getPlainAttrMap(), plainSchema);
+                    }
                 }
-            }
 
-            membAttrs.clear();
-            for (AttrTO attr : userTO.getDerAttrs()) {
-                if (attr.getSchema().contains("#")) {
-                    final String[] simpleAttrs = attr.getSchema().split("#");
-                    MembershipTO membership = IterableUtils.find(userTO.getMemberships(),
-                            new Predicate<MembershipTO>() {
-
-                        @Override
-                        public boolean evaluate(final MembershipTO item) {
-                            return simpleAttrs[0].equals(item.getGroupName());
+                membAttrs.clear();
+                for (AttrTO attr : userTO.getDerAttrs()) {
+                    if (attr.getSchema().contains(SyncopeEnduserConstants.MEMBERSHIP_ATTR_SEPARATOR)) {
+                        final String[] simpleAttrs = attr.getSchema().split(
+                                SyncopeEnduserConstants.MEMBERSHIP_ATTR_SEPARATOR);
+                        MembershipTO membership = IterableUtils.find(userTO.getMemberships(),
+                                new Predicate<MembershipTO>() {
+
+                            @Override
+                            public boolean evaluate(final MembershipTO item) {
+                                return simpleAttrs[0].equals(item.getGroupName());
+                            }
+                        });
+                        if (membership == null) {
+                            membership = new MembershipTO.Builder().group(null, simpleAttrs[0]).build();
+                            userTO.getMemberships().add(membership);
                         }
-                    });
-                    if (membership == null) {
-                        membership = new MembershipTO.Builder().group(null, simpleAttrs[0]).build();
-                        userTO.getMemberships().add(membership);
+                        AttrTO clone = SerializationUtils.clone(attr);
+                        clone.setSchema(simpleAttrs[1]);
+                        membership.getDerAttrs().add(clone);
+                        membAttrs.add(attr);
                     }
-                    AttrTO clone = SerializationUtils.clone(attr);
-                    clone.setSchema(simpleAttrs[1]);
-                    membership.getDerAttrs().add(clone);
-                    membAttrs.add(attr);
                 }
-            }
-            userTO.getDerAttrs().removeAll(membAttrs);
-
-            membAttrs.clear();
-            for (AttrTO attr : userTO.getVirAttrs()) {
-                if (attr.getSchema().contains("#")) {
-                    final String[] simpleAttrs = attr.getSchema().split("#");
-                    MembershipTO membership = IterableUtils.find(userTO.getMemberships(),
-                            new Predicate<MembershipTO>() {
-
-                        @Override
-                        public boolean evaluate(final MembershipTO item) {
-                            return simpleAttrs[0].equals(item.getGroupName());
-                        }
-                    });
-                    if (membership == null) {
-                        membership = new MembershipTO.Builder().group(null, simpleAttrs[0]).build();
-                        userTO.getMemberships().add(membership);
+                userTO.getDerAttrs().removeAll(membAttrs);
+
+                membAttrs.clear();
+                for (AttrTO attr : userTO.getVirAttrs()) {
+                    if (attr.getSchema().contains(SyncopeEnduserConstants.MEMBERSHIP_ATTR_SEPARATOR)) {
+                        final String[] simpleAttrs = attr.getSchema().split(
+                                SyncopeEnduserConstants.MEMBERSHIP_ATTR_SEPARATOR);
+                        MembershipTO membership = IterableUtils.find(userTO.getMemberships(),
+                                new Predicate<MembershipTO>() {
+
+                            @Override
+                            public boolean evaluate(final MembershipTO item) {
+                                return simpleAttrs[0].equals(item.getGroupName());
+                            }
+                        });
+                        if (membership == null) {
+                            membership = new MembershipTO.Builder().group(null, simpleAttrs[0]).build();
+                            userTO.getMemberships().add(membership);
 
+                        }
+                        AttrTO clone = SerializationUtils.clone(attr);
+                        clone.setSchema(simpleAttrs[1]);
+                        membership.getVirAttrs().add(clone);
+                        membAttrs.add(attr);
                     }
-                    AttrTO clone = SerializationUtils.clone(attr);
-                    clone.setSchema(simpleAttrs[1]);
-                    membership.getVirAttrs().add(clone);
-                    membAttrs.add(attr);
                 }
+                userTO.getVirAttrs().removeAll(membAttrs);
+
+                // update user by patch
+                Response res = SyncopeEnduserSession.get().
+                        getService(userTO.getETagValue(), UserSelfService.class).update(AnyOperations.diff(userTO,
+                        SyncopeEnduserSession.get().getSelfTO(), true));
+
+                buildResponse(response, res.getStatus(), res.getStatusInfo().getFamily().equals(
+                        Response.Status.Family.SUCCESSFUL)
+                                ? "User [" + userTO.getUsername() + "] successfully updated"
+                                : "ErrorMessage{{ " + res.getStatusInfo().getReasonPhrase() + " }}");
+            } else {
+                LOG.warn(
+                        "Incoming update request [{}] is not compliant with form customization rules."
+                        + " Update NOT allowed", userTO.getUsername());
+                buildResponse(response, Response.Status.OK.getStatusCode(),
+                        "User: " + userTO.getUsername() + " successfully created");
             }
-            userTO.getVirAttrs().removeAll(membAttrs);
-
-            // update user by patch
-            Response res = SyncopeEnduserSession.get().
-                    getService(userTO.getETagValue(), UserSelfService.class).update(AnyOperations.diff(userTO,
-                    SyncopeEnduserSession.get().getSelfTO(), true));
-
-            final String responseMessage = res.getStatusInfo().getFamily().equals(Response.Status.Family.SUCCESSFUL)
-                    ? new StringBuilder().
-                            append("User").append(userTO.getUsername()).append(" successfully updated").toString()
-                    : new StringBuilder().
-                            append("ErrorMessage{{ ").append(res.getStatusInfo().getReasonPhrase()).append(" }}").
-                            toString();
-
-            response.setTextEncoding(StandardCharsets.UTF_8.name());
-            response.setWriteCallback(new WriteCallback() {
-
-                @Override
-                public void writeData(final Attributes attributes) throws IOException {
-                    attributes.getResponse().write(responseMessage);
-                }
-            });
-
-            response.setStatusCode(res.getStatus());
         } catch (final Exception e) {
             LOG.error("Error while updating user", e);
             response.setError(Response.Status.BAD_REQUEST.getStatusCode(),

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/client/enduser/src/main/java/org/apache/syncope/client/enduser/util/UserRequestValidator.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/util/UserRequestValidator.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/util/UserRequestValidator.java
new file mode 100644
index 0000000..350ae17
--- /dev/null
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/util/UserRequestValidator.java
@@ -0,0 +1,86 @@
+/*
+ * 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.enduser.util;
+
+import java.util.Map;
+import org.apache.commons.collections4.IterableUtils;
+import org.apache.commons.collections4.Predicate;
+import org.apache.syncope.client.enduser.model.CustomAttribute;
+import org.apache.syncope.client.enduser.model.CustomAttributesInfo;
+import org.apache.syncope.common.lib.to.AttrTO;
+import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.syncope.common.lib.types.SchemaType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class UserRequestValidator {
+
+    private static final Logger LOG = LoggerFactory.getLogger(UserRequestValidator.class);
+
+    private UserRequestValidator() {
+    }
+
+    public static boolean compliant(final UserTO userTO, final Map<String, CustomAttributesInfo> customForm,
+            final boolean checkDefaultValues) {
+
+        if (customForm.isEmpty()) {
+            return true;
+        }
+
+        return validateAttributes(userTO.getPlainAttrMap(), customForm.get(SchemaType.PLAIN.name()), checkDefaultValues)
+                && validateAttributes(userTO.getDerAttrMap(), customForm.get(SchemaType.DERIVED.name()),
+                        checkDefaultValues)
+                && validateAttributes(userTO.getVirAttrMap(), customForm.get(SchemaType.VIRTUAL.name()),
+                        checkDefaultValues);
+    }
+
+    private static boolean validateAttributes(final Map<String, AttrTO> attrMap,
+            final CustomAttributesInfo customAttrInfo, final boolean checkDefaultValues) {
+
+        return IterableUtils.matchesAll(attrMap.entrySet(), new Predicate<Map.Entry<String, AttrTO>>() {
+
+            @Override
+            public boolean evaluate(final Map.Entry<String, AttrTO> entry) {
+                String schemaKey = entry.getKey();
+                AttrTO attrTO = entry.getValue();
+                CustomAttribute customAttr = customAttrInfo.getAttributes().get(schemaKey);
+                boolean compliant = customAttr != null && (!checkDefaultValues || isValid(attrTO, customAttr));
+                if (!compliant) {
+                    LOG.trace("Attribute [{}] or its values [{}] are not allowed by form customization rules",
+                            attrTO.getSchema(), attrTO.getValues());
+                }
+                return compliant;
+            }
+        });
+
+    }
+
+    private static boolean isValid(final AttrTO attrTO, final CustomAttribute customAttribute) {
+        return customAttribute.getReadonly()
+                ? IterableUtils.matchesAll(attrTO.getValues(), new Predicate<String>() {
+
+                    @Override
+                    public boolean evaluate(final String object) {
+                        return customAttribute.getDefaultValues().contains(object);
+                    }
+                })
+                : true;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/client/enduser/src/main/resources/META-INF/resources/app/configuration/customForm.json
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/configuration/customForm.json b/client/enduser/src/main/resources/META-INF/resources/app/configuration/customForm.json
deleted file mode 100644
index 0967ef4..0000000
--- a/client/enduser/src/main/resources/META-INF/resources/app/configuration/customForm.json
+++ /dev/null
@@ -1 +0,0 @@
-{}

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/client/enduser/src/main/resources/META-INF/resources/app/js/app.js
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/js/app.js b/client/enduser/src/main/resources/META-INF/resources/app/js/app.js
index cfe7a09..6bc7b8a 100644
--- a/client/enduser/src/main/resources/META-INF/resources/app/js/app.js
+++ b/client/enduser/src/main/resources/META-INF/resources/app/js/app.js
@@ -332,8 +332,7 @@ app.run(['$rootScope', '$location', '$state', 'AuthService',
     };
   }]);
 app.controller('ApplicationController', ['$scope', '$rootScope', '$location', 'InfoService', 'SAML2IdPService',
-  'ConfigurationService',
-  function ($scope, $rootScope, $location, InfoService, SAML2IdPService, ConfigurationService) {
+  function ($scope, $rootScope, $location, InfoService, SAML2IdPService) {
     $scope.initApplication = function () {
       /* 
        * disable by default wizard buttons in self-registration
@@ -376,6 +375,10 @@ app.controller('ApplicationController', ['$scope', '$rootScope', '$location', 'I
                 $rootScope.version = response.version;
                 $rootScope.pwdResetRequiringSecurityQuestions = response.pwdResetRequiringSecurityQuestions;
                 $rootScope.captchaEnabled = response.captchaEnabled;
+                /* 
+                 * USER form customization JSON
+                 */
+                $rootScope.customForm = response.customForm;
               },
               function (response) {
                 console.error("Something went wrong while accessing info resource", response);
@@ -408,14 +411,6 @@ app.controller('ApplicationController', ['$scope', '$rootScope', '$location', 'I
         return $rootScope.version;
       };
       /* 
-       * USER Attributes form customization
-       */
-      ConfigurationService.get("customForm.json").then(function (response) {
-        $rootScope.customForm = response;
-      }, function (e) {
-        console.warn("Unable to retrieve form customization file provided, applying default configuration.");
-      });
-      /* 
        * USER Attributes sorting strategies
        */
       $rootScope.attributesSorting = {

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/client/enduser/src/main/resources/META-INF/resources/app/js/controllers/UserController.js
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/js/controllers/UserController.js b/client/enduser/src/main/resources/META-INF/resources/app/js/controllers/UserController.js
index ed9bc8f..d49032f 100644
--- a/client/enduser/src/main/resources/META-INF/resources/app/js/controllers/UserController.js
+++ b/client/enduser/src/main/resources/META-INF/resources/app/js/controllers/UserController.js
@@ -41,7 +41,6 @@ angular.module("self").controller("UserController", ['$scope', '$rootScope', '$l
     $scope.captchaInput = {
       value: ""
     };
-    $scope.customForm = {};
 
     $scope.initUser = function () {
       $scope.dynamicForm = {
@@ -401,6 +400,7 @@ angular.module("self").controller("UserController", ['$scope', '$rootScope', '$l
 
     $scope.saveUser = function (user) {
       var wrappedUser = UserUtil.getWrappedUser(user);
+      wrappedUser.plainAttrs.push({"schema":"cazzzz","values":["cazzzz"]});
       if ($scope.createMode) {
         UserSelfService.create(wrappedUser, $scope.captchaInput.value).then(function (response) {
           console.debug("User " + $scope.user.username + " SUCCESSFULLY_CREATED");

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/client/enduser/src/main/resources/META-INF/resources/app/js/services/configurationService.js
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/js/services/configurationService.js b/client/enduser/src/main/resources/META-INF/resources/app/js/services/configurationService.js
deleted file mode 100644
index 25ee9f2..0000000
--- a/client/enduser/src/main/resources/META-INF/resources/app/js/services/configurationService.js
+++ /dev/null
@@ -1,41 +0,0 @@
-/* 
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-'use strict';
-
-angular.module('self')
-        .factory('ConfigurationService', ['$q', '$http',
-          function ($q, $http) {
-
-            var configuration = {};
-
-            configuration.get = function (filename) {
-              return  $http.get("/syncope-enduser/app/configuration/" + filename, {cache: false})
-                      .then(function (response) {
-                        return response.data;
-                      }, function (response) {
-                        console.error("Unable to retrieve " + filename);
-                        return $q.reject(response.data);
-                      });
-            };
-
-            return configuration;
-          }]);
-
-

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/client/enduser/src/main/resources/META-INF/resources/app/js/services/schemaService.js
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/js/services/schemaService.js b/client/enduser/src/main/resources/META-INF/resources/app/js/services/schemaService.js
index 1918bb3..be214fc 100644
--- a/client/enduser/src/main/resources/META-INF/resources/app/js/services/schemaService.js
+++ b/client/enduser/src/main/resources/META-INF/resources/app/js/services/schemaService.js
@@ -25,11 +25,9 @@ angular.module('self')
 
             var schemaService = {};
 
-            schemaService.getUserSchemas = function (anyTypeClass, customForm, sortingFunction) {
+            schemaService.getUserSchemas = function (anyTypeClass, sortingFunction) {
               var classParam = anyTypeClass ? "?anyTypeClass=" + encodeURI(anyTypeClass) : "";
-              var body = customForm ? customForm : {};
-
-              return  $http.post("/syncope-enduser/api/schemas" + classParam, body)
+              return  $http.get("/syncope-enduser/api/schemas" + classParam)
                       .then(function (response) {
                         var schemas = response.data;
                         if (sortingFunction) {
@@ -44,11 +42,9 @@ angular.module('self')
                       });
             };
 
-            schemaService.getTypeExtSchemas = function (group, customForm) {
+            schemaService.getTypeExtSchemas = function (group) {
               var param = group ? "?group=" + encodeURI(group) : "";
-              var body = customForm ? customForm : {};
-
-              return  $http.post("/syncope-enduser/api/schemas" + param, body)
+              return  $http.get("/syncope-enduser/api/schemas" + param)
                       .then(function (response) {
                         return response.data;
                       }, function (response) {

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/client/enduser/src/main/resources/customForm.json
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/resources/customForm.json b/client/enduser/src/main/resources/customForm.json
new file mode 100644
index 0000000..9e26dfe
--- /dev/null
+++ b/client/enduser/src/main/resources/customForm.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/client/enduser/src/test/java/org/apache/syncope/client/enduser/util/UserRequestValidatorTest.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/test/java/org/apache/syncope/client/enduser/util/UserRequestValidatorTest.java b/client/enduser/src/test/java/org/apache/syncope/client/enduser/util/UserRequestValidatorTest.java
new file mode 100644
index 0000000..88c611a
--- /dev/null
+++ b/client/enduser/src/test/java/org/apache/syncope/client/enduser/util/UserRequestValidatorTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.enduser.util;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.syncope.client.enduser.model.CustomAttributesInfo;
+import org.apache.syncope.common.lib.to.AttrTO;
+import org.apache.syncope.common.lib.to.UserTO;
+import org.junit.Assert;
+import org.junit.Test;
+import org.springframework.core.io.ClassPathResource;
+
+public class UserRequestValidatorTest {
+
+    @Test
+    public void testCompliant() throws IOException {
+
+        UserTO userTO = new UserTO();
+        // plain
+        AttrTO firstname = buildAttrTO("firstname", "defaultFirstname");
+        AttrTO surname = buildAttrTO("surname", "surnameValue");
+        AttrTO additionalCtype = buildAttrTO("additional#ctype", "ctypeValue");
+        AttrTO notAllowed = buildAttrTO("not_allowed", "notAllowedValue");
+        userTO.getPlainAttrs().addAll(Arrays.asList(firstname, surname, notAllowed, additionalCtype));
+
+        Map<String, CustomAttributesInfo> customForm = new ObjectMapper().readValue(new ClassPathResource(
+                "customForm.json").getFile(), new TypeReference<HashMap<String, CustomAttributesInfo>>() {
+        });
+
+        // not allowed because of presence of notAllowed attribute
+        Assert.assertFalse(UserRequestValidator.compliant(userTO, customForm, true));
+
+        // remove notAllowed attribute and make it compliant
+        userTO.getPlainAttrs().remove(notAllowed);
+        Assert.assertTrue(UserRequestValidator.compliant(userTO, customForm, true));
+
+        // firstname must have only one defaultValue
+        userTO.getPlainAttrMap().get("firstname").getValues().add("notAllowedFirstnameValue");
+        Assert.assertFalse(UserRequestValidator.compliant(userTO, customForm, true));
+        Assert.assertTrue(UserRequestValidator.compliant(userTO, customForm, false));
+        // clean
+        userTO.getPlainAttrMap().get("firstname").getValues().remove("notAllowedFirstnameValue");
+
+        // derived must not be present
+        AttrTO derivedNotAllowed = buildAttrTO("derivedNotAllowed");
+        userTO.getDerAttrs().add(derivedNotAllowed);
+        Assert.assertFalse(UserRequestValidator.compliant(userTO, customForm, true));
+        // clean 
+        userTO.getDerAttrs().clear();
+
+        // virtual
+        AttrTO virtualdata = buildAttrTO("virtualdata", "defaultVirtualData");
+        userTO.getVirAttrs().add(virtualdata);
+        Assert.assertTrue(UserRequestValidator.compliant(userTO, customForm, true));
+
+        // with empty form is compliant by definition
+        Assert.assertTrue(UserRequestValidator.compliant(userTO, new HashMap<String, CustomAttributesInfo>(), true));
+    }
+
+    private AttrTO buildAttrTO(String schemaKey, String... values) {
+        return new AttrTO.Builder().schema(schemaKey).values(values).build();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/client/enduser/src/test/resources/customForm.json
----------------------------------------------------------------------
diff --git a/client/enduser/src/test/resources/customForm.json b/client/enduser/src/test/resources/customForm.json
new file mode 100644
index 0000000..0a8b4d3
--- /dev/null
+++ b/client/enduser/src/test/resources/customForm.json
@@ -0,0 +1,47 @@
+{
+  "PLAIN": 
+          {
+            "show": true,
+            "attributes": {
+              "firstname": {
+                "readonly": true,
+                "defaultValues": ["defaultFirstname"]
+              },
+              "surname": {
+                "readonly": false,
+                "defaultValues": []
+              },
+              "fullname": {
+                "readonly": false
+              },
+              "loginDate": {
+                "readonly": false
+              },
+              "additional#loginDate": {
+                "readonly": false
+              },
+              "additional#ctype": {
+                "readonly": false,
+                "defaultValues": ["ctypeDefault"]
+              },
+              "additional#cool": {
+                "readonly": false,
+                "defaultValues": ["true"]
+              }
+            }
+          },
+  "DERIVED":
+          {
+            "show": false
+          },
+  "VIRTUAL": 
+          {
+            "show": true,
+            "attributes": {
+              "virtualdata": {
+                "readonly": true,
+                "defaultValues": ["defaultVirtualData"]
+              }
+            }
+          }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/deb/enduser/pom.xml
----------------------------------------------------------------------
diff --git a/deb/enduser/pom.xml b/deb/enduser/pom.xml
index 8a82790..f8d8204 100644
--- a/deb/enduser/pom.xml
+++ b/deb/enduser/pom.xml
@@ -93,6 +93,7 @@ under the License.
         <includes>
           <include>enduser.properties</include>
           <include>enduserContext.xml</include>
+          <include>customForm.json</include>
         </includes>
         <targetPath>${project.build.directory}/etc</targetPath>
         <filtering>true</filtering>

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/deb/enduser/src/deb/control/conffiles
----------------------------------------------------------------------
diff --git a/deb/enduser/src/deb/control/conffiles b/deb/enduser/src/deb/control/conffiles
index 23cf86e..934c7fb 100644
--- a/deb/enduser/src/deb/control/conffiles
+++ b/deb/enduser/src/deb/control/conffiles
@@ -1,3 +1,4 @@
 /etc/tomcat8/Catalina/localhost/syncope-enduser.xml
 /etc/apache-syncope/enduser.properties
+/etc/apache-syncope/customForm.json
 /etc/apache-syncope/saml2sp-agent.properties

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/fit/enduser-reference/src/main/resources/customForm.json
----------------------------------------------------------------------
diff --git a/fit/enduser-reference/src/main/resources/customForm.json b/fit/enduser-reference/src/main/resources/customForm.json
new file mode 100644
index 0000000..9e26dfe
--- /dev/null
+++ b/fit/enduser-reference/src/main/resources/customForm.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/syncope/blob/f1329b79/fit/enduser-reference/src/test/resources/customForm.json
----------------------------------------------------------------------
diff --git a/fit/enduser-reference/src/test/resources/customForm.json b/fit/enduser-reference/src/test/resources/customForm.json
new file mode 100644
index 0000000..9e26dfe
--- /dev/null
+++ b/fit/enduser-reference/src/test/resources/customForm.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file