jackrabbit-oak-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ang...@apache.org
Subject svn commit: r1344665 - in /jackrabbit/oak/trunk: oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/ oak-core/src/test/java/org/apache/jackrabbit/oak/spi/ oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/ oak-core/src/test/...
Date Thu, 31 May 2012 12:12:21 GMT
Author: angela
Date: Thu May 31 12:12:20 2012
New Revision: 1344665

URL: http://svn.apache.org/viewvc?rev=1344665&view=rev
Log:
OAK-50 : Implement User Management (WIP)

Added:
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/PasswordUtility.java   (contents, props changed)
      - copied, changed from r1341350, jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/PasswordUtility.java
    jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/
    jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/
    jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/user/
    jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/user/PasswordUtilityTest.java
    jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/UserConstants.java
Removed:
    jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/PasswordUtility.java
Modified:
    jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/AuthorizableImpl.java
    jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/AuthorizableNodeCreator.java
    jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/GroupImpl.java
    jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/ImpersonationImpl.java
    jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/InheritingAuthorizableIterator.java
    jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/MembershipManager.java
    jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/UserImpl.java
    jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/UserManagerConfig.java
    jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/UserManagerImpl.java

Copied: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/PasswordUtility.java (from r1341350, jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/PasswordUtility.java)
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/PasswordUtility.java?p2=jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/PasswordUtility.java&p1=jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/PasswordUtility.java&r1=1341350&r2=1344665&rev=1344665&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/PasswordUtility.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/PasswordUtility.java Thu May 31 12:12:20 2012
@@ -14,47 +14,226 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.jackrabbit.oak.jcr.security.user;
+package org.apache.jackrabbit.oak.spi.security.user;
 
+import org.apache.jackrabbit.util.Text;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.annotation.Nullable;
 import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
 
 /**
- * PasswordUtility...
+ * Utility to generate and compare password hashes.
  */
 public class PasswordUtility {
 
-    /**
-     * logger instance
-     */
     private static final Logger log = LoggerFactory.getLogger(PasswordUtility.class);
 
+    private static final char DELIMITER = '-';
+    private static final int NO_ITERATIONS = 1;
+    private static final String ENCODING = "UTF-8";
+
     public static final String DEFAULT_ALGORITHM = "SHA-256";
     public static final int DEFAULT_SALT_SIZE = 8;
-    public static final int DEFAULT_ITERATIONS = 10000;
+    public static final int DEFAULT_ITERATIONS = 1000;
 
     /**
      * Avoid instantiation
      */
     private PasswordUtility() {}
 
-    public static boolean isSame(String passwordHash, String toTest) {
-        // TODO
-        return false;
+    /**
+     * Generates a hash of the specified password with the default values
+     * for algorithm, salt-size and number of iterations.
+     *
+     * @param password The password to be hashed.
+     * @return The password hash.
+     * @throws NoSuchAlgorithmException If {@link #DEFAULT_ALGORITHM} is not supported.
+     * @throws UnsupportedEncodingException If utf-8 is not supported.
+     */
+    public static String buildPasswordHash(String password) throws NoSuchAlgorithmException, UnsupportedEncodingException {
+        return buildPasswordHash(password, DEFAULT_ALGORITHM, DEFAULT_SALT_SIZE, DEFAULT_ITERATIONS);
     }
 
+    /**
+     * Generates a hash of the specified password using the specified algorithm,
+     * salt size and number of iterations into account.
+     *
+     * @param password The password to be hashed.
+     * @param algorithm The desired hash algorithm.
+     * @param saltSize The desired salt size. If the specified integer is lower
+     * that {@link #DEFAULT_SALT_SIZE} the default is used.
+     * @param iterations The desired number of iterations. If the specified
+     * integer is lower than 1 the {@link #DEFAULT_ITERATIONS default} value is used.
+     * @return  The password hash.
+     * @throws NoSuchAlgorithmException If the specified algorithm is not supported.
+     * @throws UnsupportedEncodingException If utf-8 is not supported.
+     */
     public static String buildPasswordHash(String password, String algorithm,
-                                           int defaultSaltSize, int iterations)
-            throws NoSuchAlgorithmException, UnsupportedEncodingException {
-        // TODO
-        return null;
+                                           int saltSize, int iterations) throws NoSuchAlgorithmException, UnsupportedEncodingException {
+        if (password == null) {
+            throw new IllegalArgumentException("Password may not be null.");
+        }
+        if (iterations < NO_ITERATIONS) {
+            iterations = DEFAULT_ITERATIONS;
+        }
+        if (saltSize < DEFAULT_SALT_SIZE) {
+            saltSize = DEFAULT_SALT_SIZE;
+        }
+        String salt = generateSalt(saltSize);
+        String alg = (algorithm == null) ? DEFAULT_ALGORITHM : algorithm;
+        return generateHash(password, alg, salt, iterations);
     }
 
-    public static boolean isPlainTextPassword(String password) {
-        // TODO
+    /**
+     * Returns {@code true} if the specified string doesn't start with a
+     * valid algorithm name in curly brackets.
+     *
+     * @param password The string to be tested.
+     * @return {@code true} if the specified string doesn't start with a
+     * valid algorithm name in curly brackets.
+     */
+    public static boolean isPlainTextPassword(@Nullable String password) {
+        return extractAlgorithm(password) == null;
+    }
+
+    /**
+     * Returns {@code true} if hash of the specified {@code password} equals the
+     * given hashed password.
+     *
+     * @param hashedPassword Password hash.
+     * @param password The password to compare.
+     * @return If the hash of the specified {@code password} equals the given
+     * {@code hashedPassword} string.
+     */
+    public static boolean isSame(String hashedPassword, String password) {
+        try {
+            String algorithm = extractAlgorithm(hashedPassword);
+            if (algorithm != null) {
+                int startPos = algorithm.length()+2;
+                String salt = extractSalt(hashedPassword, startPos);
+                int iterations = NO_ITERATIONS;
+                if (salt != null) {
+                    startPos += salt.length()+1;
+                    iterations = extractIterations(hashedPassword, startPos);
+                }
+
+                String hash = generateHash(password, algorithm, salt, iterations);
+                return hashedPassword.equals(hash);
+            } // hashedPassword is plaintext -> return false
+        } catch (NoSuchAlgorithmException e) {
+            log.warn(e.getMessage());
+        } catch (UnsupportedEncodingException e) {
+            log.warn(e.getMessage());
+        }
         return false;
     }
+
+    //------------------------------------------------------------< private >---
+
+    private static String generateHash(String pwd, String algorithm, String salt, int iterations) throws NoSuchAlgorithmException, UnsupportedEncodingException {
+        StringBuilder passwordHash = new StringBuilder();
+        passwordHash.append('{').append(algorithm).append('}');
+        if (salt != null && salt.length() > 0) {
+            StringBuilder data = new StringBuilder();
+            data.append(salt).append(pwd);
+
+            passwordHash.append(salt).append(DELIMITER);
+            if (iterations > NO_ITERATIONS) {
+                passwordHash.append(iterations).append(DELIMITER);
+            }
+            passwordHash.append(generateDigest(data.toString(), algorithm, iterations));
+        } else {
+            // backwards compatible to jr 2.0: no salt, no iterations
+            passwordHash.append(Text.digest(algorithm, pwd.getBytes(ENCODING)));
+        }
+        return passwordHash.toString();
+    }
+
+    private static String generateSalt(int saltSize) {
+        SecureRandom random = new SecureRandom();
+        byte salt[] = new byte[saltSize];
+        random.nextBytes(salt);
+
+        StringBuilder res = new StringBuilder(salt.length * 2);
+        for (byte b : salt) {
+            res.append(Text.hexTable[(b >> 4) & 15]);
+            res.append(Text.hexTable[b & 15]);
+        }
+        return res.toString();
+    }
+
+    private static String generateDigest(String data, String algorithm, int iterations) throws UnsupportedEncodingException, NoSuchAlgorithmException {
+        byte[] bytes = data.getBytes(ENCODING);
+        MessageDigest md = MessageDigest.getInstance(algorithm);
+
+        for (int i = 0; i < iterations; i++) {
+            md.reset();
+            bytes = md.digest(bytes);
+        }
+
+        StringBuilder res = new StringBuilder(bytes.length * 2);
+        for (byte b : bytes) {
+            res.append(Text.hexTable[(b >> 4) & 15]);
+            res.append(Text.hexTable[b & 15]);
+        }
+        return res.toString();
+    }
+
+    /**
+     * Extract the algorithm from the given crypted password string. Returns the
+     * algorithm or {@code null} if the given string doesn't have a
+     * leading {@code algorithm} such as created by {@code buildPasswordHash}
+     * or if the extracted string doesn't represent an available algorithm.
+     *
+     * @param hashedPwd The password hash.
+     * @return The algorithm or {@code null} if the given string doesn't have a
+     * leading {@code algorithm} such as created by {@code buildPasswordHash}
+     * or if the extracted string isn't a supported algorithm.
+     */
+    private static String extractAlgorithm(String hashedPwd) {
+        if (hashedPwd != null && !"".equals(hashedPwd)) {
+            int end = hashedPwd.indexOf('}');
+            if (hashedPwd.charAt(0) == '{' && end > 0 && end < hashedPwd.length()-1) {
+                String algorithm = hashedPwd.substring(1, end);
+                try {
+                    MessageDigest.getInstance(algorithm);
+                    return algorithm;
+                } catch (NoSuchAlgorithmException e) {
+                    log.debug("Invalid algorithm detected " + algorithm);
+                }
+            }
+        }
+
+        // not starting with {} or invalid algorithm
+        return null;
+    }
+
+    private static String extractSalt(String hashedPwd, int start) {
+        int end = hashedPwd.indexOf(DELIMITER, start);
+        if (end > -1) {
+            return hashedPwd.substring(start, end);
+        }
+        // no salt
+        return null;
+    }
+
+    private static int extractIterations(String hashedPwd, int start) {
+        int end = hashedPwd.indexOf(DELIMITER, start);
+        if (end > -1) {
+            String str = hashedPwd.substring(start, end);
+            try {
+                return Integer.parseInt(str);
+            } catch (NumberFormatException e) {
+                log.debug("Expected number of iterations. Found: " + str);
+            }
+        }
+
+        // no extra iterations
+        return NO_ITERATIONS;
+    }
 }
\ No newline at end of file

Propchange: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/PasswordUtility.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/user/PasswordUtilityTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/user/PasswordUtilityTest.java?rev=1344665&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/user/PasswordUtilityTest.java (added)
+++ jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/user/PasswordUtilityTest.java Thu May 31 12:12:20 2012
@@ -0,0 +1,149 @@
+/*
+ * 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.jackrabbit.oak.spi.security.user;
+
+import org.junit.Test;
+
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class PasswordUtilityTest {
+
+    private static List<String> PLAIN_PWDS = new ArrayList<String>();
+    static {
+        PLAIN_PWDS.add("pw");
+        PLAIN_PWDS.add("PassWord123");
+        PLAIN_PWDS.add("_");
+        PLAIN_PWDS.add("{invalidAlgo}");
+        PLAIN_PWDS.add("{invalidAlgo}Password");
+        PLAIN_PWDS.add("{SHA-256}");
+        PLAIN_PWDS.add("pw{SHA-256}");
+        PLAIN_PWDS.add("p{SHA-256}w");
+        PLAIN_PWDS.add("");
+    }
+
+    private static Map<String, String> HASHED_PWDS = new HashMap<String, String>();
+    static {
+        for (String pw : PLAIN_PWDS) {
+            try {
+                HASHED_PWDS.put(pw, PasswordUtility.buildPasswordHash(pw));
+            } catch (Exception e) {
+                // should not get here
+            }
+        }
+    }
+
+    @Test
+    public void testBuildPasswordHash() throws Exception {
+        for (String pw : PLAIN_PWDS) {
+            String pwHash = PasswordUtility.buildPasswordHash(pw);
+            assertFalse(pw.equals(pwHash));
+        }
+
+        List<Integer[]> l = new ArrayList<Integer[]>();
+        l.add(new Integer[] {0, 1000});
+        l.add(new Integer[] {1, 10});
+        l.add(new Integer[] {8, 50});
+        l.add(new Integer[] {10, 5});
+        l.add(new Integer[] {-1, -1});
+        for (Integer[] params : l) {
+            for (String pw : PLAIN_PWDS) {
+                int saltsize = params[0];
+                int iterations = params[1];
+
+                String pwHash = PasswordUtility.buildPasswordHash(pw, PasswordUtility.DEFAULT_ALGORITHM, saltsize, iterations);
+                assertFalse(pw.equals(pwHash));
+            }
+        }
+    }
+
+    @Test
+    public void testBuildPasswordHashInvalidAlgorithm() throws Exception {
+        List<String> invalidAlgorithms = new ArrayList<String>();
+        invalidAlgorithms.add("");
+        invalidAlgorithms.add("+");
+        invalidAlgorithms.add("invalid");
+
+        for (String invalid : invalidAlgorithms) {
+            try {
+                String pwHash = PasswordUtility.buildPasswordHash("pw", invalid, PasswordUtility.DEFAULT_SALT_SIZE, PasswordUtility.DEFAULT_ITERATIONS);
+                fail("Invalid algorithm " + invalid);
+            } catch (NoSuchAlgorithmException e) {
+                // success
+            }
+        }
+
+    }
+
+    @Test
+    public void testIsPlainTextPassword() throws Exception {
+        for (String pw : PLAIN_PWDS) {
+            assertTrue(pw + " should be plain text.", PasswordUtility.isPlainTextPassword(pw));
+        }
+    }
+
+    @Test
+    public void testIsPlainTextForNull() throws Exception {
+        assertTrue(PasswordUtility.isPlainTextPassword(null));
+    }
+
+    @Test
+    public void testIsPlainTextForPwHash() throws Exception {
+        for (String pwHash : HASHED_PWDS.values()) {
+            assertFalse(pwHash + " should not be plain text.", PasswordUtility.isPlainTextPassword(pwHash));
+        }
+    }
+
+    @Test
+    public void testIsSame() throws Exception {
+        for (String pw : HASHED_PWDS.keySet()) {
+            String pwHash = HASHED_PWDS.get(pw);
+            assertTrue("Not the same " + pw + ", " + pwHash, PasswordUtility.isSame(pwHash, pw));
+        }
+
+        String pw = "password";
+        String pwHash = PasswordUtility.buildPasswordHash(pw, "SHA-1", 4, 50);
+        assertTrue("Not the same '" + pw + "', " + pwHash, PasswordUtility.isSame(pwHash, pw));
+
+        pwHash = PasswordUtility.buildPasswordHash(pw, "md5", 0, 5);
+        assertTrue("Not the same '" + pw + "', " + pwHash, PasswordUtility.isSame(pwHash, pw));
+
+        pwHash = PasswordUtility.buildPasswordHash(pw, "md5", -1, -1);
+        assertTrue("Not the same '" + pw + "', " + pwHash, PasswordUtility.isSame(pwHash, pw));
+    }
+
+    @Test
+    public void testIsNotSame() throws Exception {
+        String previous = null;
+        for (String pw : HASHED_PWDS.keySet()) {
+            String pwHash = HASHED_PWDS.get(pw);
+            assertFalse(pw, PasswordUtility.isSame(pw, pw));
+            assertFalse(pwHash, PasswordUtility.isSame(pwHash, pwHash));
+            if (previous != null) {
+                assertFalse(previous, PasswordUtility.isSame(pwHash, previous));
+            }
+            previous = pw;
+        }
+    }
+}
\ No newline at end of file

Modified: jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/AuthorizableImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/AuthorizableImpl.java?rev=1344665&r1=1344664&r2=1344665&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/AuthorizableImpl.java (original)
+++ jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/AuthorizableImpl.java Thu May 31 12:12:20 2012
@@ -28,7 +28,6 @@ import javax.jcr.Node;
 import javax.jcr.Property;
 import javax.jcr.PropertyIterator;
 import javax.jcr.RepositoryException;
-import javax.jcr.Session;
 import javax.jcr.Value;
 import javax.jcr.nodetype.NodeType;
 import javax.jcr.nodetype.PropertyDefinition;
@@ -38,34 +37,24 @@ import java.util.Iterator;
 import java.util.List;
 
 /**
- * AuthorizableImpl...
+ * AuthorizableImpl... TODO
  */
-abstract class AuthorizableImpl implements Authorizable {
+abstract class AuthorizableImpl implements Authorizable, UserConstants {
 
     /**
      * logger instance
      */
     private static final Logger log = LoggerFactory.getLogger(AuthorizableImpl.class);
 
-    static final String NT_REP_AUTHORIZABLE = "rep:Authorizable";
-    static final String NT_REP_USER = "rep:User";
-    static final String NT_REP_GROUP = "rep:Group";
-    static final String NT_REP_MEMBERS = "rep:Members";
-    static final String REP_PRINCIPAL_NAME = "rep:principalName";
-    static final String REP_PASSWORD = "rep:password";
-    static final String REP_DISABLED = "rep:disabled";
-    static final String REP_MEMBERS = "rep:members";
-    static final String REP_IMPERSONATORS = "rep:impersonators";
-
     private final Node node;
     private final UserManagerImpl userManager;
 
     private int hashCode;
 
     AuthorizableImpl(Node node, UserManagerImpl userManager) throws RepositoryException {
-        checkValidNode(node);
-        this.node = node;
         this.userManager = userManager;
+        this.node = node;
+        checkValidNode(node);
     }
 
     abstract void checkValidNode(Node node) throws RepositoryException;
@@ -109,7 +98,6 @@ abstract class AuthorizableImpl implemen
         if (!isGroup() && ((User) this).isAdmin()) {
             throw new RepositoryException("The administrator cannot be removed.");
         }
-        Session s = node.getSession();
         userManager.onRemove(this);
         node.remove();
     }
@@ -216,8 +204,6 @@ abstract class AuthorizableImpl implemen
      */
     @Override
     public boolean removeProperty(String relPath) throws RepositoryException {
-        String name = Text.getName(relPath);
-
         if (node.hasProperty(relPath)) {
             Property p = node.getProperty(relPath);
             if (isAuthorizableProperty(p, true)) {
@@ -294,6 +280,10 @@ abstract class AuthorizableImpl implemen
         return node;
     }
 
+    String getJcrName(String oakName) {
+        return userManager.getJcrName(oakName);
+    }
+
     /**
      * @return The user manager associated with this authorizable.
      */
@@ -307,8 +297,9 @@ abstract class AuthorizableImpl implemen
      */
     String getPrincipalName() throws RepositoryException {
         String principalName;
-        if (node.hasProperty(REP_PRINCIPAL_NAME)) {
-            principalName = node.getProperty(REP_PRINCIPAL_NAME).getString();
+        String propName = getJcrName(REP_PRINCIPAL_NAME);
+        if (node.hasProperty(propName)) {
+            principalName = node.getProperty(propName).getString();
         } else {
             log.debug("Authorizable without principal name -> using ID as fallback.");
             principalName = getID();
@@ -352,7 +343,7 @@ abstract class AuthorizableImpl implemen
             return false;
         } else if (node.isSame(prop.getParent())) {
             NodeType declaringNt = prop.getDefinition().getDeclaringNodeType();
-            return declaringNt.isNodeType(NT_REP_AUTHORIZABLE);
+            return declaringNt.isNodeType(getJcrName(NT_REP_AUTHORIZABLE));
         } else {
             // another non-protected property somewhere in the subtree of this
             // authorizable node -> is a property that can be set using #setProperty.

Modified: jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/AuthorizableNodeCreator.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/AuthorizableNodeCreator.java?rev=1344665&r1=1344664&r2=1344665&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/AuthorizableNodeCreator.java (original)
+++ jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/AuthorizableNodeCreator.java Thu May 31 12:12:20 2012
@@ -16,13 +16,157 @@
  */
 package org.apache.jackrabbit.oak.jcr.security.user;
 
-import org.apache.jackrabbit.oak.jcr.NodeImpl;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.oak.api.CoreValue;
 import org.apache.jackrabbit.oak.jcr.SessionDelegate;
+import org.apache.jackrabbit.oak.jcr.value.ValueConverter;
+import org.apache.jackrabbit.oak.namepath.NamePathMapper;
+import org.apache.jackrabbit.util.Text;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.jcr.Node;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.ConstraintViolationException;
+import java.io.UnsupportedEncodingException;
+import java.util.UUID;
+
 /**
- * AuthorizableNodeCreator...
+ * Utility class creating the JCR nodes corresponding the a given
+ * authorizable ID with the following behavior:
+ * <ul>
+ * <li>Users are created below /home/users or
+ * the corresponding path configured.</li>
+ * <li>Groups are created below /home/groups or
+ * the corresponding path configured.</li>
+ * <li>Below each category authorizables are created within a human readable
+ * structure based on the defined intermediate path or some internal logic
+ * with a depth defined by the {@code defaultDepth} config option.<br>
+ * E.g. creating a user node for an ID 'aSmith' would result in the following
+ * structure assuming defaultDepth == 2 is used:
+ * <pre>
+ * + rep:security            [rep:AuthorizableFolder]
+ *   + rep:authorizables     [rep:AuthorizableFolder]
+ *     + rep:users           [rep:AuthorizableFolder]
+ *       + a                 [rep:AuthorizableFolder]
+ *         + aS              [rep:AuthorizableFolder]
+ * ->        + aSmith        [rep:User]
+ * </pre>
+ * </li>
+ * <li>In case of a user the node name is calculated from the specified UserID
+ * {@link Text#escapeIllegalJcrChars(String) escaping} any illegal JCR chars.
+ * In case of a Group the node name is calculated from the specified principal
+ * name circumventing any conflicts with existing ids and escaping illegal chars.</li>
+ * <li>If no intermediate path is passed the names of the intermediate
+ * folders are calculated from the leading chars of the escaped node name.</li>
+ * <li>If the escaped node name is shorter than the {@code defaultDepth}
+ * the last char is repeated.<br>
+ * E.g. creating a user node for an ID 'a' would result in the following
+ * structure assuming defaultDepth == 2 is used:
+ * <pre>
+ * + rep:security            [rep:AuthorizableFolder]
+ *   + rep:authorizables     [rep:AuthorizableFolder]
+ *     + rep:users           [rep:AuthorizableFolder]
+ *       + a                 [rep:AuthorizableFolder]
+ *         + aa              [rep:AuthorizableFolder]
+ * ->        + a             [rep:User]
+ * </pre>
+ * </li>
+ * <li>If the {@code autoExpandTree} option is {@code true} the
+ * user tree will be automatically expanded using additional levels if
+ * {@code autoExpandSize} is exceeded within a given level.</li>
+ * </ul>
+ *
+ * The auto-expansion of the authorizable tree is defined by the following
+ * steps and exceptional cases:
+ * <ul>
+ * <li>As long as {@code autoExpandSize} isn't reached authorizable
+ * nodes are created within the structure defined by the
+ * {@code defaultDepth}. (see above)</li>
+ * <li>If {@code autoExpandSize} is reached additional intermediate
+ * folders will be created.<br>
+ * E.g. creating a user node for an ID 'aSmith1001' would result in the
+ * following structure:
+ * <pre>
+ * + rep:security            [rep:AuthorizableFolder]
+ *   + rep:authorizables     [rep:AuthorizableFolder]
+ *     + rep:users           [rep:AuthorizableFolder]
+ *       + a                 [rep:AuthorizableFolder]
+ *         + aS              [rep:AuthorizableFolder]
+ *           + aSmith1       [rep:User]
+ *           + aSmith2       [rep:User]
+ *           [...]
+ *           + aSmith1000    [rep:User]
+ * ->        + aSm           [rep:AuthorizableFolder]
+ * ->          + aSmith1001  [rep:User]
+ * </pre>
+ * </li>
+ * <li>Conflicts: In order to prevent any conflicts that would arise from
+ * creating a authorizable node that upon later expansion could conflict
+ * with an authorizable folder, intermediate levels are always created if
+ * the node name equals any of the names reserved for the next level of
+ * folders.<br>
+ * In the example above any attempt to create a user with ID 'aSm' would
+ * result in an intermediate level irrespective if max-size has been
+ * reached or not:
+ * <pre>
+ * + rep:security            [rep:AuthorizableFolder]
+ *   + rep:authorizables     [rep:AuthorizableFolder]
+ *     + rep:users           [rep:AuthorizableFolder]
+ *       + a                 [rep:AuthorizableFolder]
+ *         + aS              [rep:AuthorizableFolder]
+ * ->        + aSm           [rep:AuthorizableFolder]
+ * ->          + aSm         [rep:User]
+ * </pre>
+ * </li>
+ * <li>Special case: If the name of the authorizable node to be created is
+ * shorter or equal to the length of the folder at level N, the authorizable
+ * node is created even if max-size has been reached before.<br>
+ * An attempt to create the users 'aS' and 'aSm' in a structure containing
+ * tons of 'aSmith' users will therefore result in:
+ * <pre>
+ * + rep:security            [rep:AuthorizableFolder]
+ *   + rep:authorizables     [rep:AuthorizableFolder]
+ *     + rep:users           [rep:AuthorizableFolder]
+ *       + a                 [rep:AuthorizableFolder]
+ *         + aS              [rep:AuthorizableFolder]
+ *           + aSmith1       [rep:User]
+ *           + aSmith2       [rep:User]
+ *           [...]
+ *           + aSmith1000    [rep:User]
+ * ->        + aS            [rep:User]
+ *           + aSm           [rep:AuthorizableFolder]
+ *             + aSmith1001  [rep:User]
+ * ->          + aSm         [rep:User]
+ * </pre>
+ * </li>
+ * <li>Special case: If {@code autoExpandTree} is enabled later on
+ * AND any of the existing authorizable nodes collides with an intermediate
+ * folder to be created the auto-expansion is aborted and the new
+ * authorizable is inserted at the last valid level irrespective of
+ * max-size being reached.
+ * </li>
+ * </ul>
+ *
+ * The configuration options:
+ * <ul>
+ * <li><strong>defaultDepth</strong>:<br>
+ * A positive {@code integer} greater than zero defining the depth of
+ * the default structure that is always created.<br>
+ * Default value: 2</li>
+ * <li><strong>autoExpandTree</strong>:<br>
+ * {@code boolean} defining if the tree gets automatically expanded
+ * if within a level the maximum number of child nodes is reached.<br>
+ * Default value: {@code false}</li>
+ * <li><strong>autoExpandSize</strong>:<br>
+ * A positive {@code long} greater than zero defining the maximum
+ * number of child nodes that are allowed at a given level.<br>
+ * Default value: 1000<br>
+ * NOTE: that total number of child nodes may still be greater that
+ * autoExpandSize.</li>
+ * </ul>
  */
 class AuthorizableNodeCreator {
 
@@ -31,17 +175,239 @@ class AuthorizableNodeCreator {
      */
     private static final Logger log = LoggerFactory.getLogger(AuthorizableNodeCreator.class);
 
-    AuthorizableNodeCreator(SessionDelegate sessionDelegate) {
-        // TODO
+    private static final String DELIMITER = "/";
+    private static final int DEFAULT_DEPTH = 2;
+    private static final int DEFAULT_SIZE = 1000;
+
+    private final SessionDelegate sessionDelegate;
+
+    private final int defaultDepth;
+    private final boolean autoExpandTree;
+    private final long autoExpandSize;
+
+    private final String groupPath;
+    private final String userPath;
+
+    private final String ntAuthorizableFolder;
+    private final String ntAuthorizable;
+
+    AuthorizableNodeCreator(SessionDelegate sessionDelegate, UserManagerConfig config) {
+        this.sessionDelegate = sessionDelegate;
+
+        defaultDepth = config.getConfigValue(UserManagerConfig.PARAM_DEFAULT_DEPTH, DEFAULT_DEPTH);
+        autoExpandTree = config.getConfigValue(UserManagerConfig.PARAM_AUTO_EXPAND_TREE, false);
+        autoExpandSize = config.getConfigValue(UserManagerConfig.PARAM_AUTO_EXPAND_SIZE, DEFAULT_SIZE);
+
+        groupPath = config.getConfigValue(UserManagerConfig.PARAM_GROUP_PATH, "/rep:security/rep:authorizables/rep:groups");
+        userPath = config.getConfigValue(UserManagerConfig.PARAM_USER_PATH, "/rep:security/rep:authorizables/rep:users");
+
+        NamePathMapper namePathMapper = sessionDelegate.getNamePathMapper();
+        ntAuthorizableFolder = namePathMapper.getJcrName(UserConstants.NT_REP_AUTHORIZABLE_FOLDER);
+        ntAuthorizable = namePathMapper.getJcrName(UserConstants.NT_REP_AUTHORIZABLE);
+    }
+
+    String getNodeID(String authorizableId) throws RepositoryException {
+        try {
+            UUID uuid = UUID.nameUUIDFromBytes(authorizableId.toLowerCase().getBytes("UTF-8"));
+            return uuid.toString();
+        } catch (UnsupportedEncodingException e) {
+            throw new RepositoryException("Unexpected error while creating authorizable node", e);
+        }
+    }
+
+    Node createUserNode(String userID, String intermediatePath) throws RepositoryException {
+        return createAuthorizableNode(userID, false, intermediatePath);
+    }
+
+    Node createGroupNode(String groupID, String intermediatePath) throws RepositoryException {
+        return createAuthorizableNode(groupID, true, intermediatePath);
+    }
+
+    private Node createAuthorizableNode(String authorizableId, boolean isGroup, String intermediatePath) throws RepositoryException {
+        String nodeName = Text.escapeIllegalJcrChars(authorizableId);
+        Node folder = createFolderNodes(authorizableId, nodeName, isGroup, intermediatePath);
+
+        String ntName = (isGroup) ? UserConstants.NT_REP_GROUP : UserConstants.NT_REP_USER;
+        Node authorizableNode = folder.addNode(nodeName, ntName);
+
+        String nodeID = getNodeID(authorizableId);
+        CoreValue idValue = ValueConverter.toCoreValue(nodeID, PropertyType.STRING, sessionDelegate);
+        sessionDelegate.getNode(authorizableNode).setProperty(JcrConstants.JCR_UUID, idValue);
+
+        return folder.getNode(nodeName);
     }
 
-    NodeImpl createUserNode(String userID, String intermediatePath) {
-        // TODO
-        return null;
+    /**
+     * Create folder structure for the authorizable to be created. The structure
+     * consists of a tree of rep:AuthorizableFolder node(s) starting at the
+     * configured user or group path. Note that Authorizable nodes are never
+     * nested.
+     *
+     * @param authorizableId
+     * @param nodeName
+     * @param isGroup
+     * @param intermediatePath
+     * @return
+     * @throws RepositoryException
+     */
+    private Node createFolderNodes(String authorizableId, String nodeName,
+                                   boolean isGroup, String intermediatePath) throws RepositoryException {
+        Session session = sessionDelegate.getSession();
+        String authRoot = (isGroup) ? groupPath : userPath;
+        Node folder;
+        if (!session.nodeExists(authRoot)) {
+            folder = session.getRootNode();
+            for (String name : Text.explode(authRoot, '/', false)) {
+                if (folder.hasNode(name)) {
+                    folder = folder.getNode(name);
+                } else {
+                    folder = folder.addNode(name, ntAuthorizableFolder);
+                }
+            }
+        } else {
+            folder = session.getNode(authRoot);
+        }
+        String folderPath = getFolderPath(authorizableId, intermediatePath);
+        String[] segmts = Text.explode(folderPath, '/', false);
+        for (String segment : segmts) {
+            if (folder.hasNode(segment)) {
+                folder = folder.getNode(segment);
+                if (!folder.isNodeType(ntAuthorizableFolder)) {
+                    throw new ConstraintViolationException("Cannot create user/group: Intermediate folders must be of type rep:AuthorizableFolder.");
+                }
+            } else {
+                folder = folder.addNode(segment, ntAuthorizableFolder);
+            }
+        }
+
+        if (intermediatePath == null && autoExpandTree) {
+            folder = expandTree(authorizableId, nodeName, folder);
+        }
+
+        // test for colliding folder child node.
+        while (folder.hasNode(nodeName)) {
+            Node colliding = folder.getNode(nodeName);
+            if (colliding.isNodeType(UserConstants.NT_REP_AUTHORIZABLE_FOLDER)) {
+                log.debug("Existing folder node collides with user/group to be created. Expanding path: " + colliding.getPath());
+                folder = colliding;
+            } else {
+                String msg = "Failed to create authorizable with id '" + authorizableId + "' : Detected conflicting node of unexpected node type '" + colliding.getPrimaryNodeType().getName() + "'.";
+                log.error(msg);
+                throw new ConstraintViolationException(msg);
+            }
+        }
+
+        if (!Text.isDescendantOrEqual(authRoot, folder.getPath())) {
+            throw new ConstraintViolationException("Attempt to create user/group outside of configured scope " + authRoot);
+        }
+        return folder;
     }
 
-    NodeImpl createGroupNode(String groupID, String intermediatePath) {
-        // TODO
-        return null;
+    private String getFolderPath(String authorizableId, String intermediatePath) {
+        StringBuilder sb = new StringBuilder();
+        if (intermediatePath != null && !intermediatePath.isEmpty()) {
+            sb.append(intermediatePath);
+        } else {
+            int idLength = authorizableId.length();
+            for (int i = 0; i < defaultDepth; i++) {
+                char c;
+                if (idLength > i) {
+                    c = authorizableId.charAt(i);
+                } else {
+                    // escapedID is too short -> append the last char again
+                    c = authorizableId.charAt(idLength-1);
+                }
+                sb.append(DELIMITER).append(Text.escapeIllegalJcrChars(String.valueOf(c)));
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Expand the tree structure adding additional folders if any of the
+     * following conditions is met:
+     * <ul>
+     *     <li>number of child node exceeds the configured max value</li>
+     *     <li>the authorizable node collides with an intermediate folder</li>
+     * </ul>
+     *
+     * @param authorizableId The authorizable id
+     * @param nodeName The name of the authorizable node.
+     * @param folder The folder node.
+     * @return The node in the authorizable folder tree underneath with the
+     * authorizable node will be created.
+     * @throws RepositoryException If an error occurs.
+     */
+    private Node expandTree(String authorizableId, String nodeName, Node folder) throws RepositoryException {
+        int segmLength = defaultDepth +1;
+        while (isExpand(folder, nodeName.length())) {
+            String folderName = Text.escapeIllegalJcrChars(authorizableId.substring(0, segmLength));
+            if (folder.hasNode(folderName)) {
+                Node n = folder.getNode(folderName);
+                // assert that the folder is of type rep:AuthorizableFolder
+                if (n.isNodeType(ntAuthorizableFolder)) {
+                    folder = n;
+                } else if (n.isNodeType(ntAuthorizable)){
+                    /*
+                     an authorizable node has been created before with the
+                     name of the intermediate folder to be created.
+                     this may only occur if the 'autoExpandTree' option has
+                     been enabled later on.
+                     Resolution:
+                     - abort auto-expanding and create the authorizable
+                       at the current level, ignoring that max-size is reached.
+                     - note, that this behavior has been preferred over tmp.
+                       removing and recreating the colliding authorizable node.
+                    */
+                    log.warn("Auto-expanding aborted. An existing authorizable node '" + n.getName() +"' conflicts with intermediate folder to be created.");
+                    break;
+                } else {
+                    // should never get here: some other, unexpected node type
+                    String msg = "Failed to create authorizable node: Detected conflict with node of unexpected nodetype '" + n.getPrimaryNodeType().getName() + "'.";
+                    log.error(msg);
+                    throw new ConstraintViolationException(msg);
+                }
+            } else {
+                // folder doesn't exist nor does another colliding child node.
+                folder = folder.addNode(folderName, ntAuthorizable);
+            }
+            segmLength++;
+        }
+        return folder;
+    }
+
+    private boolean isExpand(Node folder, int nameLength) throws RepositoryException {
+        int folderNameLength = folder.getName().length();
+        // don't create additional intermediate folders for ids that are
+        // shorter or equally long as the folder name. In this case the
+        // MAX_SIZE flag is ignored.
+        if (nameLength <= folderNameLength) {
+            return false;
+        }
+
+        // test for potential (or existing) collision in which case the
+        // intermediate node is created irrespective of the MAX_SIZE and the
+        // existing number of children.
+        if (nameLength == folderNameLength+1) {
+            // max-size may not yet be reached yet on folder but the node to
+            // be created potentially collides with an intermediate folder.
+            // e.g.:
+            // existing folder structure: a/ab
+            // authID to be created     : abt
+            // OR
+            // existing collision that would result from
+            // existing folder structure: a/ab/abt
+            // authID to be create      : abt
+            return true;
+        }
+
+        // last possibility: max-size is reached.
+        if (folder.getNodes().getSize() >= autoExpandSize) {
+            return true;
+        }
+
+        // no collision and no need to create an additional intermediate
+        // folder due to max-size reached
+        return false;
     }
 }
\ No newline at end of file

Modified: jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/GroupImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/GroupImpl.java?rev=1344665&r1=1344664&r2=1344665&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/GroupImpl.java (original)
+++ jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/GroupImpl.java Thu May 31 12:12:20 2012
@@ -45,7 +45,7 @@ class GroupImpl extends AuthorizableImpl
 
     @Override
     void checkValidNode(Node node) throws RepositoryException {
-        if (node == null || !node.isNodeType(AuthorizableImpl.NT_REP_GROUP)) {
+        if (node == null || !node.isNodeType(getJcrName(NT_REP_GROUP))) {
             throw new IllegalArgumentException("Invalid group node: node type rep:Group expected.");
         }
     }
@@ -162,7 +162,8 @@ class GroupImpl extends AuthorizableImpl
      */
     private Iterator<Authorizable> getMembers(boolean includeInherited) throws RepositoryException {
         if (isEveryone()) {
-            return getUserManager().findAuthorizables(AuthorizableImpl.REP_PRINCIPAL_NAME, null, UserManager.SEARCH_TYPE_AUTHORIZABLE);
+            String propName = getJcrName(REP_PRINCIPAL_NAME);
+            return getUserManager().findAuthorizables(propName, null, UserManager.SEARCH_TYPE_AUTHORIZABLE);
         } else {
             MembershipManager mMgr = getUserManager().getMembershipManager();
             return mMgr.getMembers(this, UserManager.SEARCH_TYPE_AUTHORIZABLE, includeInherited);

Modified: jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/ImpersonationImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/ImpersonationImpl.java?rev=1344665&r1=1344664&r2=1344665&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/ImpersonationImpl.java (original)
+++ jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/ImpersonationImpl.java Thu May 31 12:12:20 2012
@@ -39,15 +39,13 @@ import java.util.Set;
 /**
  * ImpersonationImpl...
  */
-class ImpersonationImpl implements Impersonation {
+class ImpersonationImpl implements Impersonation, UserConstants {
 
     /**
      * logger instance
      */
     private static final Logger log = LoggerFactory.getLogger(ImpersonationImpl.class);
 
-    private static final String P_IMPERSONATORS = "rep:impersonators";
-
     private final UserImpl user;
 
     ImpersonationImpl(UserImpl user) {
@@ -115,13 +113,13 @@ class ImpersonationImpl implements Imper
             return false;
         }
 
-        boolean granted = false;
         Set<String> impersonators = getImpersonatorNames();
         if (impersonators.add(principalName)) {
             updateImpersonatorNames(impersonators);
-            granted = true;
+            return true;
+        } else {
+            return false;
         }
-        return granted;
     }
 
     /**
@@ -129,15 +127,15 @@ class ImpersonationImpl implements Imper
      */
     @Override
     public synchronized boolean revokeImpersonation(Principal principal) throws RepositoryException {
-        boolean revoked = false;
         String pName = principal.getName();
 
         Set<String> impersonators = getImpersonatorNames();
         if (impersonators.remove(pName)) {
             updateImpersonatorNames(impersonators);
-            revoked = true;
+            return true;
+        } else {
+            return false;
         }
-        return revoked;
     }
 
     /**
@@ -179,8 +177,9 @@ class ImpersonationImpl implements Imper
 
     private Set<String> getImpersonatorNames() throws RepositoryException {
         Set<String> princNames = new HashSet<String>();
-        if (user.getNode().hasProperty(P_IMPERSONATORS)) {
-            Value[] vs = user.getNode().getProperty(P_IMPERSONATORS).getValues();
+        String propName = user.getJcrName(REP_IMPERSONATORS);
+        if (user.getNode().hasProperty(propName)) {
+            Value[] vs = user.getNode().getProperty(propName).getValues();
             for (Value v : vs) {
                 princNames.add(v.getString());
             }
@@ -191,9 +190,9 @@ class ImpersonationImpl implements Imper
     private void updateImpersonatorNames(Set<String> principalNames) throws RepositoryException {
         String[] pNames = principalNames.toArray(new String[principalNames.size()]);
         if (pNames.length == 0) {
-            user.getUserManager().removeInternalProperty(user.getNode(), P_IMPERSONATORS);
+            user.getUserManager().removeInternalProperty(user.getNode(), REP_IMPERSONATORS);
         } else {
-            user.getUserManager().setInternalProperty(user.getNode(), P_IMPERSONATORS, pNames, PropertyType.STRING);
+            user.getUserManager().setInternalProperty(user.getNode(), REP_IMPERSONATORS, pNames, PropertyType.STRING);
         }
     }
 

Modified: jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/InheritingAuthorizableIterator.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/InheritingAuthorizableIterator.java?rev=1344665&r1=1344664&r2=1344665&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/InheritingAuthorizableIterator.java (original)
+++ jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/InheritingAuthorizableIterator.java Thu May 31 12:12:20 2012
@@ -43,7 +43,8 @@ public class InheritingAuthorizableItera
     }
 
     /**
-     *
+     * Predicate to keep track of the IDs of those groups that have already
+     * been processed.
      */
     private static final class ProcessedIdPredicate implements Predicate {
 
@@ -55,7 +56,7 @@ public class InheritingAuthorizableItera
                 try {
                     return processedIds.add(((Group) object).getID());
                 } catch (RepositoryException e) {
-                    // TODO
+                    log.warn(e.getMessage());
                 }
             }
             return false;

Modified: jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/MembershipManager.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/MembershipManager.java?rev=1344665&r1=1344664&r2=1344665&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/MembershipManager.java (original)
+++ jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/MembershipManager.java Thu May 31 12:12:20 2012
@@ -61,7 +61,7 @@ class MembershipManager {
         this.sessionDelegate = sessionDelegate;
         this.memberSplitSize = memberSplitSize;
 
-        repMembers = sessionDelegate.getNamePathMapper().getJcrName(AuthorizableImpl.REP_MEMBERS);
+        repMembers = userManager.getJcrName(UserConstants.REP_MEMBERS);
     }
 
     Iterator<Group> getMembership(AuthorizableImpl authorizable, boolean includeInherited) throws RepositoryException {

Added: jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/UserConstants.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/UserConstants.java?rev=1344665&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/UserConstants.java (added)
+++ jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/UserConstants.java Thu May 31 12:12:20 2012
@@ -0,0 +1,35 @@
+/*
+ * 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.jackrabbit.oak.jcr.security.user;
+
+/**
+ * UserConstants...
+ */
+interface UserConstants {
+
+    String NT_REP_AUTHORIZABLE = "rep:Authorizable";
+    String NT_REP_AUTHORIZABLE_FOLDER = "rep:AuthorizableFolder";
+    String NT_REP_USER = "rep:User";
+    String NT_REP_GROUP = "rep:Group";
+    String NT_REP_MEMBERS = "rep:Members";
+    String REP_PRINCIPAL_NAME = "rep:principalName";
+    String REP_PASSWORD = "rep:password";
+    String REP_DISABLED = "rep:disabled";
+    String REP_MEMBERS = "rep:members";
+    String REP_IMPERSONATORS = "rep:impersonators";
+
+}
\ No newline at end of file

Modified: jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/UserImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/UserImpl.java?rev=1344665&r1=1344664&r2=1344665&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/UserImpl.java (original)
+++ jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/UserImpl.java Thu May 31 12:12:20 2012
@@ -18,6 +18,7 @@ package org.apache.jackrabbit.oak.jcr.se
 
 import org.apache.jackrabbit.api.security.user.Impersonation;
 import org.apache.jackrabbit.api.security.user.User;
+import org.apache.jackrabbit.oak.spi.security.user.PasswordUtility;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -43,7 +44,7 @@ class UserImpl extends AuthorizableImpl 
     }
 
     void checkValidNode(Node node) throws RepositoryException {
-        if (node == null || !node.isNodeType(AuthorizableImpl.NT_REP_USER)) {
+        if (node == null || !node.isNodeType(getJcrName(NT_REP_USER))) {
             throw new IllegalArgumentException("Invalid user node: node type rep:User expected.");
         }
     }
@@ -117,8 +118,9 @@ class UserImpl extends AuthorizableImpl 
     public void changePassword(String password, String oldPassword) throws RepositoryException {
         // make sure the old password matches.
         String pwHash = null;
-        if (getNode().hasProperty(REP_PASSWORD)) {
-            pwHash = getNode().getProperty(REP_PASSWORD).getString();
+        String pwPropName = getJcrName(REP_PASSWORD);
+        if (getNode().hasProperty(pwPropName)) {
+            pwHash = getNode().getProperty(pwPropName).getString();
         }
         if (!PasswordUtility.isSame(pwHash, oldPassword)) {
             throw new RepositoryException("Failed to change password: Old password does not match.");
@@ -149,7 +151,7 @@ class UserImpl extends AuthorizableImpl 
      */
     @Override
     public boolean isDisabled() throws RepositoryException {
-        return getNode().hasProperty(REP_DISABLED);
+        return getNode().hasProperty(getJcrName(REP_DISABLED));
     }
 
     /**
@@ -158,7 +160,7 @@ class UserImpl extends AuthorizableImpl 
     @Override
     public String getDisabledReason() throws RepositoryException {
         if (isDisabled()) {
-            return getNode().getProperty(REP_DISABLED).getString();
+            return getNode().getProperty(getJcrName(REP_DISABLED)).getString();
         } else {
             return null;
         }

Modified: jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/UserManagerConfig.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/UserManagerConfig.java?rev=1344665&r1=1344664&r2=1344665&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/UserManagerConfig.java (original)
+++ jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/UserManagerConfig.java Thu May 31 12:12:20 2012
@@ -20,7 +20,9 @@ import org.apache.jackrabbit.oak.jcr.sec
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.Collections;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * UserManagerConfig...
@@ -30,42 +32,86 @@ public class UserManagerConfig {
     private static final Logger log = LoggerFactory.getLogger(UserManagerImpl.class);
 
     /**
+     * Configuration option to define the path underneath which user nodes
+     * are being created.
+     */
+    public static final String PARAM_USER_PATH = "usersPath";
+
+    /**
+     * Configuration option to define the path underneath which group nodes
+     * are being created.
+     */
+    public static final String PARAM_GROUP_PATH = "groupsPath";
+
+    /**
+     * Parameter used to change the number of levels that are used by default
+     * store authorizable nodes.<br>The default number of levels is 2.
+     * <p/>
+     * <strong>NOTE:</strong> Changing the default depth once users and groups
+     * have been created in the repository will cause inconsistencies, due to
+     * the fact that the resolution of ID to an authorizable relies on the
+     * structure defined by the default depth.<br>
+     * It is recommended to remove all authorizable nodes that will not be
+     * reachable any more, before this config option is changed.
+     * <ul>
+     * <li>If default depth is increased:<br>
+     * All authorizables on levels &lt; default depth are not reachable any more.</li>
+     * <li>If default depth is decreased:<br>
+     * All authorizables on levels &gt; default depth aren't reachable any more
+     * unless the {@link #PARAM_AUTO_EXPAND_TREE} flag is set to {@code true}.</li>
+     * </ul>
+     */
+    public static final String PARAM_DEFAULT_DEPTH = "defaultDepth";
+
+    /**
+     * If this parameter is present and its value is {@code true}, the trees
+     * containing user and group nodes will automatically created additional
+     * hierarchy levels if the number of nodes on a given level exceeds the
+     * maximal allowed {@link #PARAM_AUTO_EXPAND_SIZE size}.
+     */
+    public static final String PARAM_AUTO_EXPAND_TREE = "autoExpandTree";
+
+    /**
+     * This parameter only takes effect if {@link #PARAM_AUTO_EXPAND_TREE} is
+     * enabled.
+     */
+    public static final String PARAM_AUTO_EXPAND_SIZE = "autoExpandSize";
+
+    /**
      * If this parameter is present group members are collected in a node
      * structure below a {@link AuthorizableImpl#REP_MEMBERS} node instead of the
      * default multi valued property {@link AuthorizableImpl#REP_MEMBERS}.
      * Its value determines the maximum number of member properties until
-     * additional intermediate nodes are inserted. Valid values are integers
-     * &gt; 4. The default value is 0 and indicates that the
-     * {@link AuthorizableImpl#REP_MEMBERS} property is used to record group members.
+     * additional intermediate nodes are inserted.
      */
     public static final String PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE = "groupMembershipSplitSize";
 
     /**
      * Configuration parameter to change the default algorithm used to generate
-     * password hashes. The default value is {@link PasswordUtility#DEFAULT_ALGORITHM}.
+     * password hashes.
      */
     public static final String PARAM_PASSWORD_HASH_ALGORITHM = "passwordHashAlgorithm";
 
     /**
      * Configuration parameter to change the number of iterations used for
-     * password hash generation. The default value is {@link PasswordUtility#DEFAULT_ITERATIONS}.
+     * password hash generation.
      */
     public static final String PARAM_PASSWORD_HASH_ITERATIONS = "passwordHashIterations";
 
     /**
      * Configuration parameter to change the number of iterations used for
-     * password hash generation. The default value is {@link PasswordUtility#DEFAULT_ITERATIONS}.
+     * password hash generation.
      */
     public static final String PARAM_PASSWORD_SALT_SIZE = "passwordSaltSize";
 
     private final Map<String, Object> config;
     private final String adminId;
-    private final AuthorizableAction[] actions;
+    private final Set<AuthorizableAction> actions;
 
-    UserManagerConfig(Map<String, Object> config, String adminId, AuthorizableAction[] actions) {
+    public UserManagerConfig(Map<String, Object> config, String adminId, Set<AuthorizableAction> actions) {
         this.config = config;
         this.adminId = adminId;
-        this.actions = (actions == null) ? new AuthorizableAction[0] : actions;
+        this.actions = (actions == null) ? Collections.<AuthorizableAction>emptySet() : Collections.unmodifiableSet(actions);
     }
 
     public <T> T getConfigValue(String key, T defaultValue) {
@@ -81,7 +127,7 @@ public class UserManagerConfig {
     }
 
     public AuthorizableAction[] getAuthorizableActions() {
-        return actions;
+        return actions.toArray(new AuthorizableAction[actions.size()]);
     }
 
     //--------------------------------------------------------< private >---

Modified: jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/UserManagerImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/UserManagerImpl.java?rev=1344665&r1=1344664&r2=1344665&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/UserManagerImpl.java (original)
+++ jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/security/user/UserManagerImpl.java Thu May 31 12:12:20 2012
@@ -18,20 +18,24 @@ package org.apache.jackrabbit.oak.jcr.se
 
 import org.apache.jackrabbit.api.security.principal.ItemBasedPrincipal;
 import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.AuthorizableExistsException;
 import org.apache.jackrabbit.api.security.user.Group;
 import org.apache.jackrabbit.api.security.user.Query;
 import org.apache.jackrabbit.api.security.user.User;
 import org.apache.jackrabbit.api.security.user.UserManager;
 import org.apache.jackrabbit.oak.api.CoreValue;
-import org.apache.jackrabbit.oak.jcr.NodeImpl;
+import org.apache.jackrabbit.oak.jcr.PropertyDelegate;
 import org.apache.jackrabbit.oak.jcr.SessionDelegate;
 import org.apache.jackrabbit.oak.jcr.security.user.action.AuthorizableAction;
 import org.apache.jackrabbit.oak.jcr.value.ValueConverter;
+import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal;
+import org.apache.jackrabbit.oak.spi.security.user.PasswordUtility;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.jcr.ItemNotFoundException;
 import javax.jcr.Node;
+import javax.jcr.PathNotFoundException;
 import javax.jcr.PropertyType;
 import javax.jcr.RepositoryException;
 import javax.jcr.Session;
@@ -59,7 +63,7 @@ public class UserManagerImpl implements 
     public UserManagerImpl(SessionDelegate sessionDelegate, UserManagerConfig config) {
         this.sessionDelegate = sessionDelegate;
         this.config = config;
-        nodeCreator = new AuthorizableNodeCreator(sessionDelegate);
+        nodeCreator = new AuthorizableNodeCreator(sessionDelegate, this.config);
     }
 
     //--------------------------------------------------------< UserManager >---
@@ -70,7 +74,7 @@ public class UserManagerImpl implements 
     public Authorizable getAuthorizable(String id) throws RepositoryException {
         Authorizable authorizable = null;
         try {
-            Node node = getSession().getNodeByIdentifier(buildIdentifier(id));
+            Node node = getSession().getNodeByIdentifier(nodeCreator.getNodeID(id));
             authorizable = getAuthorizable(node);
         } catch (ItemNotFoundException e) {
             log.debug("No authorizable with ID " + id);
@@ -88,7 +92,7 @@ public class UserManagerImpl implements 
         if (principal instanceof ItemBasedPrincipal) {
             String authPath = ((ItemBasedPrincipal) principal).getPath();
             if (session.nodeExists(authPath)) {
-                NodeImpl n = (NodeImpl) session.getNode(authPath);
+                Node n = session.getNode(authPath);
                 authorizable = getAuthorizable(n);
             }
         } else {
@@ -98,7 +102,8 @@ public class UserManagerImpl implements 
             if (a != null && name.equals(a.getPrincipal().getName())) {
                 authorizable = a;
             } else {
-                Iterator<Authorizable> result = findAuthorizables(AuthorizableImpl.REP_PRINCIPAL_NAME, name, SEARCH_TYPE_AUTHORIZABLE);
+                String propName = getJcrName(UserConstants.REP_PRINCIPAL_NAME);
+                Iterator<Authorizable> result = findAuthorizables(propName, name, SEARCH_TYPE_AUTHORIZABLE);
                 if (result.hasNext()) {
                     authorizable = result.next();
                 }
@@ -124,19 +129,19 @@ public class UserManagerImpl implements 
     @Override
     public Iterator<Authorizable> findAuthorizables(String relPath, String value) throws RepositoryException {
         // TODO : create and execute a query
-        return null;
+        throw new UnsupportedOperationException("Not Implemented");
     }
 
     @Override
     public Iterator<Authorizable> findAuthorizables(String relPath, String value, int searchType) throws RepositoryException {
         // TODO : create and execute a query
-        return null;
+        throw new UnsupportedOperationException("Not Implemented");
     }
 
     @Override
     public Iterator<Authorizable> findAuthorizables(Query query) throws RepositoryException {
         // TODO : execute the specified query
-        return null;
+        throw new UnsupportedOperationException("Not Implemented");
     }
 
     @Override
@@ -153,8 +158,9 @@ public class UserManagerImpl implements 
     @Override
     public User createUser(String userID, String password, Principal principal, String intermediatePath) throws RepositoryException {
         checkValidID(userID);
+        checkValidPrincipal(principal, false);
 
-        NodeImpl userNode = nodeCreator.createUserNode(userID, intermediatePath);
+        Node userNode = nodeCreator.createUserNode(userID, intermediatePath);
         setPrincipal(userNode, principal);
         setPassword(userNode, password, true);
 
@@ -189,8 +195,9 @@ public class UserManagerImpl implements 
     @Override
     public Group createGroup(String groupID, Principal principal, String intermediatePath) throws RepositoryException {
         checkValidID(groupID);
+        checkValidPrincipal(principal, true);
 
-        NodeImpl groupNode = nodeCreator.createGroupNode(groupID, intermediatePath);
+        Node groupNode = nodeCreator.createGroupNode(groupID, intermediatePath);
         setPrincipal(groupNode, principal);
 
         Group group = new GroupImpl(groupNode, this);
@@ -301,7 +308,7 @@ public class UserManagerImpl implements 
      * @throws javax.jcr.RepositoryException If an error occurs
      */
     void setPassword(Node userNode, String password, boolean forceHash) throws RepositoryException {
-        if (password != null) {
+        if (password == null) {
             log.debug("Password is null.");
             return;
         }
@@ -320,36 +327,38 @@ public class UserManagerImpl implements 
         } else {
             pwHash = password;
         }
-        setInternalProperty(userNode, AuthorizableImpl.REP_PASSWORD, pwHash, PropertyType.STRING);
-
+        setInternalProperty(userNode, UserConstants.REP_PASSWORD, pwHash, PropertyType.STRING);
     }
 
-    void setPrincipal(NodeImpl userNode, Principal principal) throws RepositoryException {
-        // TODO: validate the principal
-
-        if (!userNode.isNew() || userNode.hasProperty(AuthorizableImpl.REP_PRINCIPAL_NAME)) {
+    void setPrincipal(Node userNode, Principal principal) throws RepositoryException {
+        if (!userNode.isNew() || userNode.hasProperty(getJcrName(UserConstants.REP_PRINCIPAL_NAME))) {
             throw new RepositoryException("rep:principalName can only be set once on a new node.");
         }
-        setInternalProperty(userNode, AuthorizableImpl.REP_PRINCIPAL_NAME, principal.getName(), PropertyType.STRING);
+        setInternalProperty(userNode, UserConstants.REP_PRINCIPAL_NAME, principal.getName(), PropertyType.STRING);
     }
 
-    void setInternalProperty(Node userNode, String name, String value, int type) throws RepositoryException {
+    void setInternalProperty(Node userNode, String oakName, String value, int type) throws RepositoryException {
         CoreValue cv = ValueConverter.toCoreValue(value, type, sessionDelegate);
-        sessionDelegate.getNode(getInternalPath(userNode)).setProperty(name, cv);
+        sessionDelegate.getNode(userNode).setProperty(oakName, cv);
     }
 
-    void setInternalProperty(Node userNode, String name, String[] values, int type) throws RepositoryException {
+    void setInternalProperty(Node userNode, String oakName, String[] values, int type) throws RepositoryException {
         List<CoreValue> cvs = ValueConverter.toCoreValues(values, type, sessionDelegate);
-        sessionDelegate.getNode(getInternalPath(userNode)).setProperty(name, cvs);
+        sessionDelegate.getNode(userNode).setProperty(oakName, cvs);
     }
 
-    void setInternalProperty(Node userNode, String name, Value[] values) throws RepositoryException {
+    void setInternalProperty(Node userNode, String oakName, Value[] values) throws RepositoryException {
         List<CoreValue> cvs = ValueConverter.toCoreValues(values, sessionDelegate);
-        sessionDelegate.getNode(getInternalPath(userNode)).setProperty(name, cvs);
+        sessionDelegate.getNode(userNode).setProperty(oakName, cvs);
     }
 
-    void removeInternalProperty(Node userNode, String name) throws RepositoryException {
-        sessionDelegate.getNode(getInternalPath(userNode)).getProperty(name).remove();
+    void removeInternalProperty(Node userNode, String oakName) throws RepositoryException {
+        PropertyDelegate pd = sessionDelegate.getNode(userNode).getProperty(oakName);
+        if (pd == null) {
+            throw new PathNotFoundException("Missing authorizable property " + oakName);
+        } else {
+            pd.remove();
+        }
     }
 
     Session getSession() {
@@ -369,26 +378,33 @@ public class UserManagerImpl implements 
     }
 
     Authorizable getAuthorizable(Node node) throws RepositoryException {
-        if (node.isNodeType(AuthorizableImpl.NT_REP_USER)) {
+        if (node.isNodeType(getJcrName(UserConstants.NT_REP_USER))) {
             return new UserImpl(node, this);
-        } else if (node.isNodeType(AuthorizableImpl.NT_REP_GROUP)) {
+        } else if (node.isNodeType(getJcrName(UserConstants.NT_REP_GROUP))) {
             return new GroupImpl(node, this);
         } else {
             throw new RepositoryException("Unexpected node type " + node.getPrimaryNodeType().getName() + ". Expected rep:User or rep:Group.");
         }
     }
 
-    private String buildIdentifier(String authorizableID) {
-        // TODO
-        return null;
+    String getJcrName(String oakName) {
+        return sessionDelegate.getNamePathMapper().getJcrName(oakName);
     }
 
-    private void checkValidID(String authorizableID) {
-        // TODO
+    private void checkValidID(String ID) throws RepositoryException {
+        if (ID == null || ID.length() == 0) {
+            throw new IllegalArgumentException("Invalid ID " + ID);
+        } else if (getAuthorizable(ID) != null) {
+            throw new AuthorizableExistsException("Authorizable with ID " + ID + " already exists");
+        }
     }
 
-    private String getInternalPath(Node node) throws RepositoryException {
-        return sessionDelegate.getOakPathOrThrow(node.getPath());
+    private void checkValidPrincipal(Principal principal, boolean isGroup) {
+        if (principal == null || principal.getName() == null || "".equals(principal.getName())) {
+            throw new IllegalArgumentException("Principal may not be null and must have a valid name.");
+        }
+        if (!isGroup && EveryonePrincipal.NAME.equals(principal.getName())) {
+            throw new IllegalArgumentException("'everyone' is a reserved group principal name.");
+        }
     }
-
 }
\ No newline at end of file



Mime
View raw message