Index: oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserValidator.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserValidator.java	(revision 1643122)
+++ oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserValidator.java	(revision )
@@ -102,6 +102,10 @@
                 String msg = "Invalid jcr:uuid for authorizable " + parentAfter.getName();
                 throw constraintViolation(23, msg);
             }
+        } else if (JcrConstants.JCR_PRIMARYTYPE.equals(name)) {
+            // if primary type changed to authorizable type -> need to perform
+            // validation as if a new authorizable node had been added
+            validateAuthorizable(parentAfter, UserUtil.getType(after.getValue(Type.STRING)));
         }
 
         if (isUser(parentBefore) && REP_PASSWORD.equals(name) && PasswordUtil.isPlainTextPassword(after.getValue(Type.STRING))) {
@@ -134,16 +138,7 @@
     public Validator childNodeAdded(String name, NodeState after) throws CommitFailedException {
         Tree tree = checkNotNull(parentAfter.getChild(name));
 
-        AuthorizableType type = UserUtil.getType(tree);
-        String authRoot = UserUtil.getAuthorizableRootPath(provider.getConfig(), type);
-        if (authRoot != null) {
-            assertHierarchy(tree, authRoot);
-            // assert rep:principalName is present (that should actually by covered
-            // by node type validator)
-            if (TreeUtil.getString(tree, REP_PRINCIPAL_NAME) == null) {
-                throw constraintViolation(26, "Mandatory property rep:principalName missing.");
-            }
-        }
+        validateAuthorizable(tree, UserUtil.getType(tree));
         return new VisibleValidator(new UserValidator(null, tree, provider), true, true);
     }
 
@@ -192,6 +187,18 @@
         }
     }
 
+    private void validateAuthorizable(@Nonnull Tree tree, @Nonnull AuthorizableType type) throws CommitFailedException {
+        String authRoot = UserUtil.getAuthorizableRootPath(provider.getConfig(), type);
+        if (authRoot != null) {
+            assertHierarchy(tree, authRoot);
+            // assert rep:principalName is present (that should actually by covered
+            // by node type validator)
+            if (TreeUtil.getString(tree, REP_PRINCIPAL_NAME) == null) {
+                throw constraintViolation(26, "Mandatory property rep:principalName missing.");
+            }
+        }
+    }
+
     private static boolean isValidUUID(@Nonnull Tree parent, @Nonnull String uuid) {
         String id = UserUtil.getAuthorizableId(parent);
         return id != null && uuid.equals(UserProvider.getContentID(id));
@@ -227,6 +234,6 @@
     }
 
     private static CommitFailedException constraintViolation(int code, @Nonnull String message) {
-        return new CommitFailedException("Constraint", code, message);
+        return new CommitFailedException(CommitFailedException.CONSTRAINT, code, message);
     }
 }
\ No newline at end of file
Index: oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/util/UserUtil.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/util/UserUtil.java	(revision 1643122)
+++ oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/util/UserUtil.java	(revision )
@@ -67,10 +67,15 @@
     @CheckForNull
     public static AuthorizableType getType(@Nonnull Tree authorizableNode) {
         String ntName = TreeUtil.getPrimaryTypeName(authorizableNode);
-        if (ntName != null) {
-            if (UserConstants.NT_REP_GROUP.equals(ntName)) {
+        return getType(ntName);
+    }
+
+    @CheckForNull
+    public static AuthorizableType getType(@CheckForNull String primaryTypeName) {
+        if (primaryTypeName != null) {
+            if (NT_REP_GROUP.equals(primaryTypeName)) {
                 return AuthorizableType.GROUP;
-            } else if (UserConstants.NT_REP_USER.equals(ntName)) {
+            } else if (NT_REP_USER.equals(primaryTypeName)) {
                 return AuthorizableType.USER;
             }
         }
\ No newline at end of file
Index: oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserValidatorTest.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserValidatorTest.java	(revision 1643122)
+++ oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserValidatorTest.java	(revision )
@@ -33,13 +33,16 @@
 import org.apache.jackrabbit.oak.api.PropertyState;
 import org.apache.jackrabbit.oak.api.Tree;
 import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.plugins.identifier.IdentifierManager;
 import org.apache.jackrabbit.oak.plugins.memory.PropertyStates;
 import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
 import org.apache.jackrabbit.oak.spi.security.user.UserConstants;
+import org.apache.jackrabbit.oak.util.NodeUtil;
 import org.apache.jackrabbit.util.Text;
 import org.junit.Before;
 import org.junit.Test;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.fail;
 
@@ -326,6 +329,49 @@
             if (group2 != null) group2.remove();
             if (group3 != null) group3.remove();
             root.commit();
+        }
+    }
+
+    @Test
+    public void testCreateNestedUser() throws Exception {
+        Tree userTree = root.getTree(getTestUser().getPath());
+        NodeUtil userNode = new NodeUtil(userTree);
+        NodeUtil profile = userNode.addChild("profile", JcrConstants.NT_UNSTRUCTURED);
+        NodeUtil nested = profile.addChild("nested", UserConstants.NT_REP_USER);
+        nested.setString(UserConstants.REP_PRINCIPAL_NAME, "nested");
+        nested.setString(UserConstants.REP_AUTHORIZABLE_ID, "nested");
+        nested.setString(JcrConstants.JCR_UUID, IdentifierManager.generateUUID("nested"));
+        try {
+            root.commit();
+            fail("Creating nested users must be detected.");
+        } catch (CommitFailedException e) {
+            // success
+            assertEquals(29, e.getCode());
+        } finally {
+            root.refresh();
+        }
+    }
+
+    @Test
+    public void testCreateNestedUser2Steps() throws Exception {
+        Tree userTree = root.getTree(getTestUser().getPath());
+        NodeUtil userNode = new NodeUtil(userTree);
+        NodeUtil profile = userNode.addChild("profile", JcrConstants.NT_UNSTRUCTURED);
+        NodeUtil nested = profile.addChild("nested", JcrConstants.NT_UNSTRUCTURED);
+        nested.setString(UserConstants.REP_PRINCIPAL_NAME, "nested");
+        nested.setString(UserConstants.REP_AUTHORIZABLE_ID, "nested");
+        nested.setString(JcrConstants.JCR_UUID, IdentifierManager.generateUUID("nested"));
+        root.commit();
+
+        try {
+            nested.setString(JcrConstants.JCR_PRIMARYTYPE, UserConstants.NT_REP_USER);
+            root.commit();
+            fail("Creating nested users must be detected.");
+        } catch (CommitFailedException e) {
+            // success
+            assertEquals(29, e.getCode());
+        } finally {
+            root.refresh();
         }
     }
 
\ No newline at end of file
