jackrabbit-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ang...@apache.org
Subject svn commit: r794702 [1/2] - in /jackrabbit/trunk/jackrabbit-core/src: main/java/org/apache/jackrabbit/core/ main/java/org/apache/jackrabbit/core/config/ main/java/org/apache/jackrabbit/core/security/user/ test/java/org/apache/jackrabbit/core/config/ te...
Date Thu, 16 Jul 2009 14:54:09 GMT
Author: angela
Date: Thu Jul 16 14:54:08 2009
New Revision: 794702

URL: http://svn.apache.org/viewvc?rev=794702&view=rev
Log:
JCR-2199: Improvements to user management

- change content structure to allow for fast lookup by ID
- groupID: unescaped before being exposed in the API.
- adjust defautl ac-provider on the security workspace according to the new structure
- same for impl tests
- extend security config. user mgr impl defines a couple of config options

Added:
    jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/IdResolverTest.java
Modified:
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/DefaultSecurityManager.java
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/RepositoryConfigurationParser.java
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/SecurityManagerConfig.java
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/GroupImpl.java
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserAccessControlProvider.java
    jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserManagerImpl.java
    jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/config/SecurityConfigTest.java
    jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/GroupAdministratorTest.java
    jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/TestAll.java
    jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserAdministratorTest.java
    jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserManagerImplTest.java

Modified: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/DefaultSecurityManager.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/DefaultSecurityManager.java?rev=794702&r1=794701&r2=794702&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/DefaultSecurityManager.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/DefaultSecurityManager.java Thu Jul 16 14:54:08 2009
@@ -27,6 +27,7 @@
 import org.apache.jackrabbit.core.config.WorkspaceConfig;
 import org.apache.jackrabbit.core.config.WorkspaceSecurityConfig;
 import org.apache.jackrabbit.core.config.SecurityManagerConfig;
+import org.apache.jackrabbit.core.config.BeanConfig;
 import org.apache.jackrabbit.core.security.AMContext;
 import org.apache.jackrabbit.core.security.AccessManager;
 import org.apache.jackrabbit.core.security.JackrabbitSecurityManager;
@@ -76,7 +77,6 @@
  */
 public class DefaultSecurityManager implements JackrabbitSecurityManager {
 
-    // TODO: should rather be placed in the core.security package. However protected access to SystemSession required to move here.
     /**
      * the default logger
      */
@@ -246,10 +246,18 @@
      * @return user manager
      * @throws RepositoryException if an error occurs
      */
-    protected UserManagerImpl createUserManager(SessionImpl session)
-            throws RepositoryException {
-
-        return new UserManagerImpl(session, adminId);
+    protected UserManagerImpl createUserManager(SessionImpl session) throws RepositoryException {
+        BeanConfig umc = repository.getConfig().getSecurityConfig().getSecurityManagerConfig().getUserManagerConfig();
+        Properties config = null;
+        if (umc != null) {
+            // TODO: deal with other umgr implementations.
+            String clName = umc.getClassName();
+            if (clName != null && !(UserManagerImpl.class.getName().equals(clName) || clName.length() == 0)) {
+                log.warn("Unsupported custom UserManager implementation: '" + clName + "' -> Ignored.");
+            }
+            config = umc.getParameters();
+        }
+        return new UserManagerImpl(session, adminId, config);
     }
 
     /**

Modified: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/RepositoryConfigurationParser.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/RepositoryConfigurationParser.java?rev=794702&r1=794701&r2=794702&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/RepositoryConfigurationParser.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/RepositoryConfigurationParser.java Thu Jul 16 14:54:08 2009
@@ -83,6 +83,12 @@
      */
     private static final String WORKSPACE_ACCESS_ELEMENT = "WorkspaceAccessManager";
 
+    /**
+     * Name of the optional UserManagerConfig element that defines the
+     * configuration options for the user manager.
+     */
+    private static final String USER_MANAGER_ELEMENT = "UserManager";
+
     /** Name of the general workspace configuration element. */
     public static final String WORKSPACES_ELEMENT = "Workspaces";
 
@@ -324,7 +330,13 @@
             if (element != null) {
                 wac = parseBeanConfig(smElement, WORKSPACE_ACCESS_ELEMENT);
             }
-            return new SecurityManagerConfig(bc, wspAttr, wac);
+
+            BeanConfig umc = null;
+            element = getElement(smElement, USER_MANAGER_ELEMENT, false);
+            if (element != null) {
+                umc = parseBeanConfig(smElement, USER_MANAGER_ELEMENT);
+            }
+            return new SecurityManagerConfig(bc, wspAttr, wac, umc);
         } else {
             return null;
         }

Modified: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/SecurityManagerConfig.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/SecurityManagerConfig.java?rev=794702&r1=794701&r2=794702&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/SecurityManagerConfig.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/SecurityManagerConfig.java Thu Jul 16 14:54:08 2009
@@ -28,19 +28,38 @@
 public class SecurityManagerConfig extends BeanConfig {
 
     private final String workspaceName;
-    private final BeanConfig workspaceAccessConfig; 
+    private final BeanConfig workspaceAccessConfig;
+    private final BeanConfig userManagerConfig;
 
     /**
      * Creates an security manager configuration object from the
      * given bean configuration.
      *
      * @param config bean configuration
+     * @param workspaceName the security workspace name
+     * @param workspaceAccessConfig the configuration for the workspace access.
      */
     public SecurityManagerConfig(BeanConfig config, String workspaceName,
                                  BeanConfig workspaceAccessConfig) {
+        this(config, workspaceName, workspaceAccessConfig, null);
+    }
+
+    /**
+     * Creates an security manager configuration object from the
+     * given bean configuration.
+     *
+     * @param config bean configuration
+     * @param workspaceName the security workspace name
+     * @param workspaceAccessConfig the configuration for the workspace access.
+     * @param userManagerConfig Configuration options for the user manager.
+     */
+    public SecurityManagerConfig(BeanConfig config, String workspaceName,
+                                 BeanConfig workspaceAccessConfig,
+                                 BeanConfig userManagerConfig) {
         super(config);
         this.workspaceName = workspaceName;
         this.workspaceAccessConfig = workspaceAccessConfig;
+        this.userManagerConfig = userManagerConfig;
     }
 
     /**
@@ -61,4 +80,13 @@
     public BeanConfig getWorkspaceAccessConfig() {
         return workspaceAccessConfig;
     }
+
+    /**
+     * @return the configuration for the user manager.
+     * May be <code>null</code> if the configuration entry is missing (i.e.
+     * the system default should be used).
+     */
+    public BeanConfig getUserManagerConfig() {
+        return userManagerConfig;
+    }
 }

Modified: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/GroupImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/GroupImpl.java?rev=794702&r1=794701&r2=794702&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/GroupImpl.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/GroupImpl.java Thu Jul 16 14:54:08 2009
@@ -65,13 +65,15 @@
     //-------------------------------------------------------< Authorizable >---
     /**
      * Returns the name of the node that defines this <code>Group</code>, that
-     * has been used taking the principal name as hint.
+     * has been used taking the principal name as hint, unescaping any chars
+     * that have been escaped to circumvent incompatitibilities with JCR name
+     * limitations.
      *
      * @return name of the node that defines this <code>Group</code>.
      * @see Authorizable#getID()
      */
     public String getID() throws RepositoryException {
-        return getNode().getName();
+        return Text.unescapeIllegalJcrChars(getNode().getName());
     }
 
     /**

Modified: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserAccessControlProvider.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserAccessControlProvider.java?rev=794702&r1=794701&r2=794702&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserAccessControlProvider.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserAccessControlProvider.java Thu Jul 16 14:54:08 2009
@@ -72,16 +72,13 @@
  * her/his group membership,</li>
  *
  * <li>members of the 'User administrator' group are allowed to create, modify
- * and remove those users whose node representation is within the subtree
- * defined by the node representation of the editing user,</li>
+ * and remove users,</li>
  *
  * <li>members of the 'Group administrator' group are allowed to create, modify
  * and remove groups,</li>
  *
  * <li>group membership can only be edited by members of the 'Group administrator'
- * and the 'User administrator' group. The range of users that can be added
- * as member to any Group is limited to those that are editable according to
- * the restrictions described above for the 'User administrator'.</li>
+ * and the 'User administrator' group.</li>
  * </ul>
  */
 public class UserAccessControlProvider extends AbstractAccessControlProvider
@@ -127,7 +124,7 @@
 
     //----------------------------------------------< AccessControlProvider >---
     /**
-     * @see AccessControlProvider#init(Session, Map)
+     * @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#init(Session, Map)
      */
     public void init(Session systemSession, Map configuration) throws RepositoryException {
         super.init(systemSession, configuration);
@@ -155,7 +152,7 @@
     }
 
     /**
-     * @see AccessControlProvider#getEffectivePolicies(Path)
+     * @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#getEffectivePolicies(Path)
      */
     public AccessControlPolicy[] getEffectivePolicies(Path absPath) throws ItemNotFoundException, RepositoryException {
         checkInitialized();
@@ -165,7 +162,7 @@
     /**
      * Always returns <code>null</code>.
      *
-     * @see AccessControlProvider#getEditor(Session)
+     * @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#getEditor(Session)
      */
     public AccessControlEditor getEditor(Session session) {
         checkInitialized();
@@ -175,7 +172,7 @@
     }
 
     /**
-     * @see AccessControlProvider#compilePermissions(Set)
+     * @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#compilePermissions(Set)
      */
     public CompiledPermissions compilePermissions(Set principals) throws RepositoryException {
         checkInitialized();
@@ -195,7 +192,7 @@
     }
 
     /**
-     * @see AccessControlProvider#canAccessRoot(Set)
+     * @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#canAccessRoot(Set)
      */
     public boolean canAccessRoot(Set principals) throws RepositoryException {
         checkInitialized();
@@ -347,100 +344,81 @@
             if (usersPath.equals(abs2Path)) {
                 /*
                  below the user-tree
-                 - determine position of target relative to the node of the editing user
+                 - determine position of target relative
+                 - target may not be below an existing user but only below an
+                   authorizable folder.
                  - determine if the editing user is user/group-admin
                  - special treatment for rep:groups property
                  */
                 NodeImpl node = (NodeImpl) getExistingNode(path);
-                NodeImpl authN = null;
-                // seek next rep:authorizable parent
-                if (node.isNodeType(NT_REP_AUTHORIZABLE)) {
-                    authN = node;
-                } else if (node.isNodeType(NT_REP_AUTHORIZABLE_FOLDER)) {
-                    NodeImpl parent = node;
-                    while (authN == null && parent.getDepth() > 0) {
-                        parent = (NodeImpl) parent.getParent();
-                        if (parent.isNodeType(NT_REP_AUTHORIZABLE)) {
-                            authN = parent;
-                        } else if (!parent.isNodeType(NT_REP_AUTHORIZABLE_FOLDER)) {
-                            // outside of user/group-tree
-                            break;
-                        }
-                    }
-                } // else: outside of user tree -> authN = null
 
-                if (authN != null && authN.isNodeType(NT_REP_USER)) {
-                    int relDepth = session.getHierarchyManager().getRelativeDepth(userNode.getNodeId(), authN.getNodeId());
+                if (node.isNodeType(NT_REP_AUTHORIZABLE) || node.isNodeType(NT_REP_AUTHORIZABLE_FOLDER)) {
+                    boolean editingHimSelf = node.isSame(userNode);
                     boolean isGroupProp = P_GROUPS.equals(path.getNameElement().getName());
                     // only user-admin is allowed to modify users.
                     // for group membership (rep:groups) group-admin is required
                     // in addition.
-                    boolean requiredGroups = isUserAdmin;
-                    if (requiredGroups && isGroupProp) {
-                        requiredGroups = isGroupAdmin;
+                    boolean memberOfRequiredGroups = isUserAdmin;
+                    if (memberOfRequiredGroups && isGroupProp) {
+                        memberOfRequiredGroups = isGroupAdmin;
                     }
-                    switch (relDepth) {
-                        case -1:
-                            // authN is not below the userNode -> can't write anyway.
-                            break;
-                        case 0:
-                            /*
-                            authN is same node as userNode. 3 cases to distinguish
-                            1) user is User-Admin -> R, W
-                            2) user is NOT U-admin but nodeID is its own node.
-                            3) special treatment for rep:group property which can
-                               only be modified by group-administrators
-                            */
-                            Path aPath = session.getQPath(authN.getPath());
-                            if (requiredGroups) {
-                                // principals contain 'user-admin'
-                                // -> user can modify items below the user-node except rep:group.
-                                // principals contains 'user-admin' + 'group-admin'
-                                // -> user can modify rep:group property as well.
-                                if (path.equals(aPath)) {
-                                    allows |= (Permission.ADD_NODE | Permission.REMOVE_PROPERTY | Permission.SET_PROPERTY);
-                                } else {
-                                    allows |= Permission.ALL;
-                                }
-                                if (calcPrivs) {
-                                    // grant WRITE privilege
-                                    // note: ac-read/modification is not included
-                                    //       remove_node is not included
-                                    privs |= getPrivilegeBits(PrivilegeRegistry.REP_WRITE);
-                                    if (!path.equals(aPath)) {
-                                       privs |= getPrivilegeBits(Privilege.JCR_REMOVE_NODE);
-                                    }
-                                }
-                            } else if (userNode.isSame(node) && (!isGroupProp || isGroupAdmin)) {
-                                // user can only read && write his own props
-                                // except for the rep:group property.
-                                allows |= (Permission.SET_PROPERTY | Permission.REMOVE_PROPERTY);
-                                if (calcPrivs) {
-                                    privs |= getPrivilegeBits(Privilege.JCR_MODIFY_PROPERTIES);
-                                }
-                            } // else some other node below but not U-admin -> read-only.
-                            break;
-                        default:
-                            /*
-                            authN is somewhere below the userNode, i.e.
-                            1) nodeId points to an authorizable below userNode
-                            2) nodeId points to an auth-folder below some authorizable below userNode.
-
-                            In either case user-admin group-membership is
-                            required in order to get write permission.
-                            group-admin group-membership is required in addition
-                            if rep:groups is the target item.
-                            */
-                            if (requiredGroups) {
-                                allows = Permission.ALL;
-                                if (calcPrivs) {
-                                    // grant WRITE privilege
-                                    // note: ac-read/modification is not included
-                                    privs |= getPrivilegeBits(PrivilegeRegistry.REP_WRITE);
+                    if (editingHimSelf) {
+                        /*
+                        node to be modified is same node as userNode. 3 cases to distinguish
+                        1) user is User-Admin -> R, W
+                        2) user is NOT U-admin but nodeID is its own node.
+                        3) special treatment for rep:group property which can
+                           only be modified by group-administrators
+                        */
+                        Path aPath = session.getQPath(node.getPath());
+                        if (memberOfRequiredGroups) {
+                            // principals contain 'user-admin'
+                            // -> user can modify items below the user-node except rep:group.
+                            // principals contains 'user-admin' + 'group-admin'
+                            // -> user can modify rep:group property as well.
+                            if (path.equals(aPath)) {
+                                allows |= (Permission.ADD_NODE | Permission.REMOVE_PROPERTY | Permission.SET_PROPERTY);
+                            } else {
+                                allows |= Permission.ALL;
+                            }
+                            if (calcPrivs) {
+                                // grant WRITE privilege
+                                // note: ac-read/modification is not included
+                                //       remove_node is not included
+                                privs |= getPrivilegeBits(PrivilegeRegistry.REP_WRITE);
+                                if (!path.equals(aPath)) {
+                                    privs |= getPrivilegeBits(Privilege.JCR_REMOVE_NODE);
                                 }
                             }
+                        } else if (userNode.isSame(node) && (!isGroupProp || isGroupAdmin)) {
+                            // user can only read && write his own props
+                            // except for the rep:group property.
+                            allows |= (Permission.SET_PROPERTY | Permission.REMOVE_PROPERTY);
+                            if (calcPrivs) {
+                                privs |= getPrivilegeBits(Privilege.JCR_MODIFY_PROPERTIES);
+                            }
+                        } // else some other node below but not U-admin -> read-only.
+                    } else {
+                        /*
+                        authN points to some other user-node, i.e.
+                        1) nodeId points to an authorizable that isn't the editing user
+                        2) nodeId points to an auth-folder within the user-tree
+
+                        In either case user-admin group-membership is
+                        required in order to get write permission.
+                        group-admin group-membership is required in addition
+                        if rep:groups is the target item.
+                        */
+                        if (memberOfRequiredGroups) {
+                            allows = Permission.ALL;
+                            if (calcPrivs) {
+                                // grant WRITE privilege
+                                // note: ac-read/modification is not included
+                                privs |= getPrivilegeBits(PrivilegeRegistry.REP_WRITE);
+                            }
+                        }
                     }
-                } // no rep:User parent node found.
+                } // outside of the user tree
             } else if (groupsPath.equals(abs2Path)) {
                 /*
                 below group-tree:

Modified: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserManagerImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserManagerImpl.java?rev=794702&r1=794701&r2=794702&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserManagerImpl.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserManagerImpl.java Thu Jul 16 14:54:08 2009
@@ -31,7 +31,6 @@
 import org.apache.jackrabbit.spi.Name;
 import org.apache.jackrabbit.spi.commons.name.NameConstants;
 import org.apache.jackrabbit.util.Text;
-import org.apache.commons.collections.map.LRUMap;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -50,29 +49,184 @@
 import java.util.Iterator;
 import java.util.NoSuchElementException;
 import java.util.Set;
-import java.util.Map;
+import java.util.Properties;
 
 /**
- * UserManagerImpl
+ * Default implementation of the <code>UserManager</code> interface with the
+ * following characteristics:
+ *
+ * <ul>
+ * <li>Users and Groups are stored in the repository as JCR nodes.</li>
+ * <li>Users are created below {@link UserConstants#USERS_PATH},<br>Groups are
+ * created below {@link UserConstants#GROUPS_PATH}.</li>
+ * <li>In order to structure the users and groups tree and void creating a flat
+ * hierarchy, additional hierarchy nodes of type "rep:AuthorizableFolder" are
+ * introduced.</li>
+ * <li>The names of the hierarchy folders is determined from ID of the
+ * authorizable to be created, consisting of the leading N chars where N is
+ * the relative depth starting from the node at {@link UserConstants#USERS_PATH}
+ * or {@link UserConstants#GROUPS_PATH}.</li>
+ * <li>By default 2 levels (depth == 2) are created.</li>
+ * <li>Searching authorizables by ID always starts looking at that specific
+ * hierarchy level. Parent nodes are expected to consist of folder structure only.</li>
+ * <li>If the ID contains invalid JCR chars that would prevent the creation of
+ * a Node with that name, the names of authorizable node and the intermediate
+ * hierarchy nodes are {@link Text#escapeIllegalJcrChars(String) escaped}.</li>
+ * <li>Any intermediate path passed to either
+ * {@link #createUser(String, String, Principal, String) createUser} or
+ * {@link #createGroup(Principal, String) createGroup} are ignored. This allows
+ * to directly find authorizables by ID without having to search or traverse
+ * the complete tree.<br>
+ * See also {@link #PARAM_COMPATIBILE_JR16}.
+ * </li>
+ * </ul>
+ * Example: Creating an non-existing authorizable with ID 'aSmith' would result
+ * in the following structure:
+ * 
+ * <pre>
+ * + rep:security            [nt:unstructured]
+ *   + rep:authorizables     [rep:AuthorizableFolder]
+ *     + rep:users           [rep:AuthorizableFolder]
+ *       + a                 [rep:AuthorizableFolder]
+ *         + aS              [rep:AuthorizableFolder]
+ *           + aSmith        [rep:User]
+ * </pre>
+ *
+ * This <code>UserManager</code> is able to handle the following configuration
+ * options:
+ *
+ * <ul>
+ * <li>{@link #PARAM_COMPATIBILE_JR16}: If the param is present and its
+ * value is <code>true</code> looking up authorizables by ID will use the
+ * <code>NodeResolver</code> if not found otherwise.<br>
+ * If the parameter is missing (or false) users and groups created
+ * with a Jackrabbit repository &lt; v2.0 will not be found any more.<br>
+ * By default this option is disabled.</li>
+ * <li>{@link #PARAM_DEFAULT_DEPTH}: 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</code>.</li>
+ * </ul>
+ * </li>
+ * <li>{@link #PARAM_AUTO_EXPAND_TREE}: If this parameter is present and its
+ * value is <code>true</code>, 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}.
+ * <br>By default this option is disabled.</li>
+ * <li>{@link #PARAM_AUTO_EXPAND_SIZE}: This parameter only takes effect
+ * if {@link #PARAM_AUTO_EXPAND_TREE} is enabled.<br>The default value is
+ * 1000.</li>
+ * </ul>
  */
-public class UserManagerImpl extends ProtectedItemModifier implements UserManager, UserConstants, SessionListener {
+public class UserManagerImpl extends ProtectedItemModifier
+        implements UserManager, UserConstants, SessionListener {
+
+    /**
+     * Flag to enable a minimal backwards compatibility with Jackrabbit &lt;
+     * v2.0<br>
+     * If the param is present and its value is <code>true</code> looking up
+     * authorizables by ID will use the <code>NodeResolver</code> if not found
+     * otherwise.<br>
+     * If the parameter is missing (or false) users and groups created
+     * with a Jackrabbit repository &lt; v2.0 will not be found any more.<br>
+     * By default this option is disabled.
+     */
+    public static final String PARAM_COMPATIBILE_JR16 = "compatibleJR16";
+
+    /**
+     * 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</code>.</li>
+     * </ul>
+     */
+    public static final String PARAM_DEFAULT_DEPTH = "defaultDepth";
+
+    /**
+     * If this parameter is present and its value is <code>true</code>, 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}.
+     * <br>By default this option is disabled.
+     */
+    public static final String PARAM_AUTO_EXPAND_TREE = "autoExpandTree";
+
+    /**
+     * This parameter only takes effect if {@link #PARAM_AUTO_EXPAND_TREE} is
+     * enabled.<br>The default value is 1000.
+     */
+    public static final String PARAM_AUTO_EXPAND_SIZE = "autoExpandSize";
 
     private static final Logger log = LoggerFactory.getLogger(UserManagerImpl.class);
 
     private final SessionImpl session;
     private final String adminId;
     private final NodeResolver authResolver;
+    private final IdResolver idResolver;
 
     /**
-     * Simple unmanaged map from authorizableID to nodePath (node representing
-     * the authorizable) used limit the number of calls to the
-     * <code>NodeResolver</code> in order to find authorizable nodes by the
-     * authorizable id.
+     * Flag indicating if {@link #getAuthorizable(String)} should find users or
+     * groups created with Jackrabbit < 2.0.<br>
+     * As of 2.0 authorizables are created using a defined logic that allows
+     * to retrieve them without searching/traversing. If this flag is
+     * <code>true</code> this method will try to find authorizables using the
+     * <code>authResolver</code> if not found otherwise.
      */
-    private final Map idPathMap = new LRUMap(1000);
+    private final boolean compatibleJR16;
 
+    /**
+     * Create a new <code>UserManager</code> with the default configuration.
+     *
+     * @param session
+     * @param adminId
+     * @throws RepositoryException
+     */
     public UserManagerImpl(SessionImpl session, String adminId) throws RepositoryException {
-        super();
+        this(session, adminId, null);
+    }
+
+    /**
+     * Create a new <code>UserManager</code> for the given <code>session</code>.
+     * Currently the following configuration options are respected:
+     *
+     * <ul>
+     * <li>{@link #PARAM_COMPATIBILE_JR16}. By default this option is disabled.</li>
+     * <li>{@link #PARAM_DEFAULT_DEPTH}. The default number of levels is 2.</li>
+     * <li>{@link #PARAM_AUTO_EXPAND_TREE}. By default this option is disabled.</li>
+     * <li>{@link #PARAM_AUTO_EXPAND_SIZE}. The default value is 1000.</li>
+     * </ul>
+     *
+     * See the overall {@link UserManagerImpl introduction} for details.
+     *
+     * @param session
+     * @param adminId
+     * @param config
+     * @throws RepositoryException
+     */
+    public UserManagerImpl(SessionImpl session, String adminId, Properties config) throws RepositoryException {
         this.session = session;
         this.adminId = adminId;
 
@@ -80,10 +234,17 @@
         try {
             nr = new IndexNodeResolver(session, session);
         } catch (RepositoryException e) {
-            log.debug("UserManger: no QueryManager available for workspace '" + session.getWorkspace().getName() + "' -> Use traversing node resolver.");
+            log.debug("UserManager: no QueryManager available for workspace '" + session.getWorkspace().getName() + "' -> Use traversing node resolver.");
             nr = new TraversingNodeResolver(session, session);
         }
         authResolver = nr;
+
+        idResolver = new IdResolver(config);
+        boolean compatMode = false;
+        if (config != null && config.containsKey(PARAM_COMPATIBILE_JR16)) {
+            compatMode = Boolean.parseBoolean(config.get(PARAM_COMPATIBILE_JR16).toString());
+        }
+        compatibleJR16 = compatMode;
     }
 
     //--------------------------------------------------------< UserManager >---
@@ -181,7 +342,6 @@
      * @param userID
      * @param password
      * @see UserManager#createUser(String,String)
-     * @inheritDoc
      */
     public User createUser(String userID, String password) throws RepositoryException {
         return createUser(userID, password, new PrincipalImpl(userID), null);
@@ -192,7 +352,7 @@
      * @param userID
      * @param password
      * @param principal
-     * @param intermediatePath
+     * @param intermediatePath Is always ignored.
      * @return
      * @throws AuthorizableExistsException
      * @throws RepositoryException
@@ -215,28 +375,25 @@
         if (hasAuthorizableOrReferee(principal)) {
             throw new AuthorizableExistsException("Authorizable for '" + principal.getName() + "' already exists");
         }
+        if (intermediatePath != null) {
+            log.debug("Intermediate path param " + intermediatePath + " is ignored.");
+        }
 
         NodeImpl parent = null;
         try {
-            String parentPath = getParentPath(intermediatePath, getCurrentUserPath());
-            parent = createParentNode(parentPath);
-
-            Name nodeName = session.getQName(Text.escapeIllegalJcrChars(userID));
-            NodeImpl userNode = addNode(parent, nodeName, NT_REP_USER);
+            NodeImpl userNode = (NodeImpl) idResolver.createUserNode(userID);
 
             setProperty(userNode, P_USERID, getValue(userID), true);
             setProperty(userNode, P_PASSWORD, getValue(UserImpl.buildPasswordValue(password)), true);
             setProperty(userNode, P_PRINCIPAL_NAME, getValue(principal.getName()), true);
-            parent.save();
+            session.save();
 
             log.debug("User created: " + userID + "; " + userNode.getPath());
             return createUser(userNode);
         } catch (RepositoryException e) {
             // something went wrong -> revert changes and rethrow
-            if (parent != null) {
-                parent.refresh(false);
-                log.debug("Failed to create new User, reverting changes.");
-            }
+            session.refresh(false);
+            log.debug("Failed to create new User, reverting changes.");
             throw e;
         }
     }
@@ -258,7 +415,7 @@
     /**
      *
      * @param principal
-     * @param intermediatePath
+     * @param intermediatePath Is always ignored.
      * @return
      * @throws AuthorizableExistsException
      * @throws RepositoryException
@@ -270,25 +427,24 @@
         if (hasAuthorizableOrReferee(principal)) {
             throw new AuthorizableExistsException("Authorizable for '" + principal.getName() + "' already exists: ");
         }
-
+        if (intermediatePath != null) {
+            log.debug("Intermediate path param " + intermediatePath + " is ignored.");
+        }
+        
         NodeImpl parent = null;
         try {
-            String parentPath = getParentPath(intermediatePath, GROUPS_PATH);
-            parent = createParentNode(parentPath);
-            Name groupID = getGroupId(principal.getName());
+            String groupID = getGroupId(principal.getName());
+            NodeImpl groupNode = (NodeImpl) idResolver.createGroupNode(groupID);
 
-            NodeImpl groupNode = addNode(parent, groupID, NT_REP_GROUP);
             setProperty(groupNode, P_PRINCIPAL_NAME, getValue(principal.getName()));
-            parent.save();
+            session.save();
 
             log.debug("Group created: " + groupID + "; " + groupNode.getPath());
 
             return createGroup(groupNode);
         } catch (RepositoryException e) {
-            if (parent != null) {
-                parent.refresh(false);
-                log.debug("newInstance new Group failed, revert changes on parent");
-            }
+            session.refresh(false);
+            log.debug("newInstance new Group failed, revert changes on parent");
             throw e;
         }
     }
@@ -324,23 +480,23 @@
     }
 
     /**
-     * Escape illegal JCR characters and test if a user exists that has the
-     * principals name as userId, which might happen if userID != principal-name.
+     * Test if a user or group exists that has the given principals name as ID,
+     * which might happen if userID != principal-name.
      * In this case: generate another ID for the group to be created.
      *
      * @param principalName to be used as hint for the groupid.
      * @return a group id.
      * @throws RepositoryException
      */
-    private Name getGroupId(String principalName) throws RepositoryException {
-        String escHint = Text.escapeIllegalJcrChars(principalName);
+    private String getGroupId(String principalName) throws RepositoryException {
+        String escHint = principalName;
         String groupID = escHint;
         int i = 0;
         while (getAuthorizable(groupID) != null) {
             groupID = escHint + "_" + i;
             i++;
         }
-        return session.getQName(groupID);
+        return groupID;
     }
 
     private Value getValue(String strValue) throws RepositoryException {
@@ -348,10 +504,11 @@
     }
 
     /**
+     * @param userID
      * @return true if the given userID belongs to the administrator user.
      */
     boolean isAdminId(String userID) {
-        return (adminId == null) ? false : adminId.equals(userID);
+        return (adminId != null) && adminId.equals(userID);
     }
 
     /**
@@ -369,7 +526,6 @@
             throw new IllegalArgumentException("User has to be within the User Path");
         }
         User user = doCreateUser(userNode);
-        idPathMap.put(user.getID(), userNode.getPath());
         return user;
     }
 
@@ -395,141 +551,53 @@
      */
     Group createGroup(NodeImpl groupNode) throws RepositoryException {
         Group group = GroupImpl.create(groupNode, this);
-        idPathMap.put(group.getID(), groupNode.getPath());
         return group;
     }
 
     /**
-     * @param userID
+     * Resolve the given <code>userID</code> to an rep:user node in the repository.
+     *
+     * @param userID A valid userID.
      * @return the node associated with the given userID or <code>null</code>.
+     * @throws RepositoryException If an error occurs.
      */
     private NodeImpl getUserNode(String userID) throws RepositoryException {
-        NodeImpl n = null;
-        if (idPathMap.containsKey(userID)) {
-            String path = idPathMap.get(userID).toString();
-            if (session.itemExists(path)) {
-                Item itm = session.getItem(path);
-                // make sure the item really represents the node associated with
-                // the given userID. if not the search below is execute.
-                if (itm.isNode()) {
-                    NodeImpl tmp = (NodeImpl) itm;
-                    if (tmp.isNodeType(NT_REP_USER) && userID.equals(((NodeImpl) itm).getProperty(P_USERID).getString())) {
-                        n = (NodeImpl) itm;
-                    }
-                }
-            }
-        }
-
-        if (n == null) {
-            // clear eventual previous entry
-            idPathMap.remove(userID);
+        NodeImpl n = (NodeImpl) idResolver.findNode(userID, false);
+        if (n == null && compatibleJR16) {
+            // backwards-compatibiltiy with JR < 2.0 user structure that doesn't
+            // allow to determine the auth-path from the id directly.
             // search for it the node belonging to that userID
             n = (NodeImpl) authResolver.findNode(P_USERID, userID, NT_REP_USER);
-        }
+        } // else: no such user -> return null.
         return n;
     }
 
+    /**
+     * Resolve the given <code>groupID</code> to an rep:group node in the repository.
+     *
+     * @param groupID A valid groupID.
+     * @return the node associated with the given userID or <code>null</code>.
+     * @throws RepositoryException If an error occurs.
+     */
     private NodeImpl getGroupNode(String groupID) throws RepositoryException {
-        NodeImpl n = null;
-        if (idPathMap.containsKey(groupID)) {
-            String path = idPathMap.get(groupID).toString();
-            if (session.itemExists(path)) {
-                Item itm = session.getItem(path);
-                // make sure the item really represents the node associated with
-                // the given userID. if not the search below is execute.
-                if (itm.isNode()) {
-                    NodeImpl tmp = (NodeImpl) itm;
-                    if (tmp.isNodeType(NT_REP_GROUP) && groupID.equals(tmp.getName())) {
-                        n = (NodeImpl) itm;
-                    }
-                }
-            }
-        }
-        if (n == null) {
-            // clear eventual previous entry
-            idPathMap.remove(groupID);
-            // search for it the node belonging to that groupID
-            Name nodeName = session.getQName(groupID);
+        NodeImpl n = (NodeImpl) idResolver.findNode(groupID, true);
+        if (n == null && compatibleJR16) {
+            // backwards-compatibiltiy with JR < 2.0 group structure that doesn't
+            // allow to determine the auth-path from the id directly
+            // search for it the node belonging to that groupID.
+            // NOTE: JR < 2.0 always returned groupIDs that didn't contain any
+            // illegal JCR chars. Since Group.getID() now unescapes the node
+            // name additional escaping is required.
+            Name nodeName = session.getQName(Text.escapeIllegalJcrChars(groupID));
             n = (NodeImpl) authResolver.findNode(nodeName, NT_REP_GROUP);
-        }
+        } // else: no such group -> return null.
         return n;
     }
 
-    /**
-     * @return the path refering to the node associated with the user this
-     * <code>UserManager</code> has been built for.
-     */
-    private String getCurrentUserPath() {
-        // fallback: default user-path
-        String currentUserPath = USERS_PATH;
-        String userId = session.getUserID();
-
-        if (idPathMap.containsKey(userId)) {
-            currentUserPath = idPathMap.get(userId).toString();
-        } else {
-            try {
-                Node n = getUserNode(userId);
-                if (n != null) {
-                    currentUserPath = n.getPath();
-                }
-            } catch (RepositoryException e) {
-                // should never get here
-                log.error("Internal error: unable to build current user path.", e.getMessage());
-            }
-        }
-        return currentUserPath;
-    }
-
     private static boolean isValidPrincipal(Principal principal) {
         return principal != null && principal.getName() != null && principal.getName().length() > 0;
     }
 
-    private static String getParentPath(String hint, String root) {
-        StringBuffer b = new StringBuffer();
-        if (hint == null || !hint.startsWith(root)) {
-            b.append(root);
-        }
-        if (hint != null && hint.length() > 1) {
-            if (!hint.startsWith("/")) {
-                b.append("/");
-            }
-            b.append(hint);
-        }
-        return b.toString();
-    }
-
-    /**
-     * @param path to the authorizable node to be created
-     * @return
-     * @throws RepositoryException
-     */
-    private NodeImpl createParentNode(String path) throws RepositoryException {
-        NodeImpl parent = (NodeImpl) session.getRootNode();
-        String[] elem = path.split("/");
-        for (int i = 0; i < elem.length; i++) {
-            String name = elem[i];
-            if (name.length() < 1) {
-                continue;
-            }
-            Name nName = session.getQName(name);
-            if (!parent.hasNode(nName)) {
-                Name ntName;
-                if (i == 0) {
-                    // rep:security node
-                    ntName = NameConstants.NT_UNSTRUCTURED;
-                } else {
-                    ntName = NT_REP_AUTHORIZABLE_FOLDER;
-                }
-                NodeImpl added = addNode(parent, nName, ntName);
-                parent.save();
-                parent = added;
-            } else {
-                parent = parent.getNode(nName);
-            }
-        }
-        return parent;
-    }
-
     //----------------------------------------------------< SessionListener >---
     /**
      * @see SessionListener#loggingOut(org.apache.jackrabbit.core.SessionImpl)
@@ -542,8 +610,6 @@
      * @see SessionListener#loggedOut(org.apache.jackrabbit.core.SessionImpl)
      */
     public void loggedOut(SessionImpl session) {
-        // clear the map
-        idPathMap.clear();
         // and logout the session unless it is the loggedout session itself.
         if (session != this.session) {
             this.session.logout();
@@ -621,4 +687,374 @@
             return null;
         }
     }
+
+    //--------------------------------------------------------------------------
+    /**
+     * Inner class creating and finding the JCR nodes corresponding the a given
+     * authorizable ID with the following behavior:
+     * <ul>
+     * <li>Users are created below /rep:security/rep:authorizables/rep:users</li>
+     * <li>Groups are created below /rep:security/rep:authorizables/rep:users</li>
+     * <li>Below each category authorizables are created within a human readable
+     * structure, whose depth is defined by the <code>defaultDepth</code> 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            [nt:unstructured]
+     *   + 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>The names of the intermediate folders are caculated from the leading
+     * chars of the escaped node name.</li>
+     * <li>If the escaped node name is shorter than the <code>defaultDepth</code>
+     * 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            [nt:unstructured]
+     *   + rep:authorizables     [rep:AuthorizableFolder]
+     *     + rep:users           [rep:AuthorizableFolder]
+     *       + a                 [rep:AuthorizableFolder]
+     *         + aa              [rep:AuthorizableFolder]
+     * ->        + a             [rep:User]
+     * </pre>
+     * </li>
+     * <li>If the <code>autoExpandTree</code> option is <code>true</code> the
+     * user tree will be automatically expanded using additional levels if
+     * <code>autoExpandSize</code> 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</code> isn't reached authorizable
+     * nodes are created within the structure defined by the
+     * <code>defaultDepth</code>. (see above)</li>
+     * <li>If <code>autoExpandSize</code> 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            [nt:unstructured]
+     *   + 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            [nt:unstructured]
+     *   + 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            [nt:unstructured]
+     *   + 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>
+     * </ul>
+     *
+     * The configuration options:
+     * <ul>
+     * <li><strong>defaultDepth</strong>:<br>
+     * <code>integer</code> defining the depth of the default structure that is
+     * always created.<br>
+     * Default value: 2</li>
+     * <li><strong>autoExpandTree</strong>:<br>
+     * <code>boolean</code> defining if the tree gets automatically expanded
+     * if within a level the maximum number of child nodes is reached.<br>
+     * Default value: <code>false</code></li>
+     * <li><strong>autoExpandSize</strong>:<br>
+     * <code>long</code> 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>
+     */
+    private class IdResolver {
+
+        private static final String DELIMITER = "/";
+        private static final int DEFAULT_DEPTH = 2;
+        private static final long DEFAULT_SIZE = 1000;
+        
+        private final int defaultDepth;
+        private final boolean autoExpandTree;
+        // best effort max-size of authorizables per folder. there may be
+        // more nodes created if the editing session isn't allowed to see
+        // all child nodes.
+        private final long autoExpandSize;
+
+        private IdResolver(Properties config) {
+            int d = DEFAULT_DEPTH;
+            boolean expand = false;
+            long size = DEFAULT_SIZE;
+
+            if (config != null) {
+                if (config.containsKey(PARAM_DEFAULT_DEPTH)) {
+                    try {
+                        d = Integer.parseInt(config.get(PARAM_DEFAULT_DEPTH).toString());
+                    } catch (NumberFormatException e) {
+                        log.warn("Unable to parse defaultDepth config option", e);
+                    }
+                }
+                if (config.containsKey(PARAM_AUTO_EXPAND_TREE)) {
+                    expand = Boolean.parseBoolean(config.get(PARAM_AUTO_EXPAND_TREE).toString());
+                }
+                if (config.containsKey(PARAM_AUTO_EXPAND_SIZE)) {
+                    try {
+                        size = Integer.parseInt(config.get(PARAM_AUTO_EXPAND_SIZE).toString());
+                    } catch (NumberFormatException e) {
+                        log.warn("Unable to parse autoExpandSize config option", e);
+                    }
+                }
+            }
+
+            defaultDepth = d;
+            autoExpandTree = expand;
+            autoExpandSize = size;
+        }
+
+        public Node createUserNode(String userID) throws RepositoryException {
+            return createAuthorizableNode(userID, false);
+        }
+
+        public Node createGroupNode(String groupID) throws RepositoryException {
+            return createAuthorizableNode(groupID, true);
+        }
+
+        public Node findNode(String id, boolean isGroup) throws RepositoryException {
+            String defaultFolderPath = getDefaultFolderPath(id, isGroup);
+            String escapedId = Text.escapeIllegalJcrChars(id);
+
+            if (session.nodeExists(defaultFolderPath)) {
+                Node folder = session.getNode(defaultFolderPath);
+                Name expectedNt = (isGroup) ? NT_REP_GROUP : NT_REP_USER;
+
+                // traverse the potentially existing hierarchy looking for the
+                // authorizable node.
+                int segmLength = defaultDepth +1;
+                while (folder != null) {
+                    if (folder.hasNode(escapedId)) {
+                        NodeImpl aNode = (NodeImpl) folder.getNode(escapedId);
+                        if (aNode.isNodeType(expectedNt)) {
+                            // done. found the right auth-node
+                            return aNode;
+                        } else {
+                            folder = aNode;
+                        }
+                    } else {
+                        // no child node with name 'escapedId' -> look for
+                        // additional levels that may exist.
+                        Node parent = folder;
+                        folder = null;
+                        if (id.length() >= segmLength) {
+                            String folderName = Text.escapeIllegalJcrChars(id.substring(0, segmLength));
+                            if (parent.hasNode(folderName)) {
+                                NodeImpl f = (NodeImpl) parent.getNode(folderName);
+                                if (f.isNodeType(NT_REP_AUTHORIZABLE_FOLDER)) {
+                                    folder = f;
+                                } // else: matching node isn't an authorizable-folder
+                            } // else: failed to find a suitable next level
+                        } // else: id is shorter than required length at the current level.
+                    }
+                    segmLength++;
+                }
+            } // else: no node at default-path
+
+            // no matching node found -> authorizable doesn't exist.
+            return null;
+        }
+
+        private Node createAuthorizableNode(String id, boolean isGroup) throws RepositoryException {
+            String escapedId = Text.escapeIllegalJcrChars(id);
+
+            // first create the default folder nodes, that are always present.
+            Node folder = createDefaultFolderNodes(id, escapedId, isGroup);
+            // eventually create additional intermediate folders.
+            folder = createIntermediateFolderNodes(id, escapedId, folder);
+
+            // finally create the authorizable node
+            Name nodeName = session.getQName(escapedId);
+            Name ntName = (isGroup) ? NT_REP_GROUP : NT_REP_USER;
+            Node authNode = addNode((NodeImpl) folder, nodeName, ntName);
+
+            return authNode;
+        }
+
+        private Node createDefaultFolderNodes(String id, String escapedId, boolean isGroup) throws RepositoryException {
+            NodeImpl folder;
+            // first create the levels that are always present -> see #getDefaultFolderPath
+            String defaultPath = getDefaultFolderPath(id, isGroup);
+            if (session.nodeExists(defaultPath)) {
+                folder = (NodeImpl) session.getNode(defaultPath);
+            } else {
+                String[] segmts = defaultPath.split("/");
+                folder = (NodeImpl) session.getRootNode();
+                String repSecurity = SECURITY_ROOT_PATH.substring(1);
+
+                for (String segment : segmts) {
+                    if (segment.length() < 1) {
+                        continue;
+                    }
+                    if (folder.hasNode(segment)) {
+                        folder = (NodeImpl) folder.getNode(segment);
+                    } else {
+                        Name ntName;
+                        if (repSecurity.equals(segment)) {
+                            // rep:security node
+                            ntName = NameConstants.NT_UNSTRUCTURED;
+                        } else {
+                            ntName = NT_REP_AUTHORIZABLE_FOLDER;
+                        }
+                        NodeImpl added = addNode(folder, session.getQName(segment), ntName);
+                        folder.save();
+                        folder = added;
+                    }
+                }
+            }
+
+            // validation check if authorizable to be create doesn't conflict.
+            checkExists(escapedId, folder);
+            return folder;
+        }
+
+        private String getDefaultFolderPath(String id, boolean isGroup) {
+            StringBuilder bld = new StringBuilder();
+            if (isGroup) {
+                bld.append(GROUPS_PATH);
+            } else {
+                bld.append(USERS_PATH);
+            }
+            StringBuilder lastSegment = new StringBuilder(defaultDepth);
+            int idLength = id.length();
+            for (int i = 0; i < defaultDepth; i++) {
+                if (idLength > i) {
+                    lastSegment.append(id.charAt(i));
+                } else {
+                    // escapedID is too short -> append the last char again
+                    lastSegment.append(id.charAt(idLength-1));
+                }
+                bld.append(DELIMITER).append(Text.escapeIllegalJcrChars(lastSegment.toString()));
+            }
+            return bld.toString();
+        }
+
+        private Node createIntermediateFolderNodes(String id, String escapedId, Node folder) throws RepositoryException {
+            if (!autoExpandTree) {
+                // additional folders are never created
+                return folder;
+            }
+
+            // additional folders needs be created if
+            // - the maximal size of child nodes is reached
+            // - if the auth-node to be created potentially collides with any
+            //   of the intermediate nodes.
+            int segmLength = defaultDepth +1;
+            int idLength = id.length();
+
+            while (intermediateFolderNeeded(escapedId, folder)) {
+                String folderName = Text.escapeIllegalJcrChars(id.substring(0, segmLength));
+                // validation check on each intermediate level if authorizable
+                // to be created doesn't conflict.
+                checkExists(folderName, folder);
+
+                if (folder.hasNode(folderName)) {
+                    folder = folder.getNode(folderName);
+                } else {
+                    folder = addNode((NodeImpl) folder, session.getQName(folderName), NT_REP_AUTHORIZABLE_FOLDER);
+                }
+                segmLength++;
+            }
+
+            // final validation check if authorizable to be created doesn't conflict.
+            checkExists(escapedId, folder);
+            return folder;
+        }
+
+        private void checkExists(String nodeName, Node folder) throws RepositoryException {
+            if (folder.hasNode(nodeName) &&
+                    folder.getNode(nodeName).isNodeType(session.getJCRName(NT_REP_AUTHORIZABLE))) {
+                throw new AuthorizableExistsException("Unable to create Group/User: Collision with existing authorizable.");
+            }
+        }
+
+        private boolean intermediateFolderNeeded(String nodeName, Node folder) throws RepositoryException {
+            // 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 (nodeName.length() <= folder.getName().length()) {
+                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 (nodeName.length() == folder.getName().length()+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 collition 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;
+        }
+    }
 }

Modified: jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/config/SecurityConfigTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/config/SecurityConfigTest.java?rev=794702&r1=794701&r2=794702&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/config/SecurityConfigTest.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/config/SecurityConfigTest.java Thu Jul 16 14:54:08 2009
@@ -18,6 +18,7 @@
 
 import org.apache.jackrabbit.core.DefaultSecurityManager;
 import org.apache.jackrabbit.core.security.DefaultAccessManager;
+import org.apache.jackrabbit.core.security.user.UserManagerImpl;
 import org.apache.jackrabbit.core.security.authentication.DefaultLoginModule;
 import org.apache.jackrabbit.core.security.simple.SimpleAccessManager;
 import org.apache.jackrabbit.core.security.simple.SimpleSecurityManager;
@@ -88,6 +89,8 @@
         assertNull(smc.getWorkspaceAccessConfig());
         assertEquals("security", smc.getWorkspaceName());
 
+        assertNull(smc.getUserManagerConfig());
+
         AccessManagerConfig amc = config.getAccessManagerConfig();
         assertNotNull(amc);
         assertTrue(amc.newInstance() instanceof DefaultAccessManager);
@@ -102,6 +105,24 @@
         assertEquals("org.apache.jackrabbit.TestPrincipalProvider", options.getProperty("principalProvider"));
     }
 
+    public void testConfig3() throws ConfigurationException {
+        Element xml = parseXML(new InputSource(new StringReader(CONFIG_3)), true);
+        SecurityConfig config = parser.parseSecurityConfig(xml);
+
+        SecurityManagerConfig smc = config.getSecurityManagerConfig();
+
+        assertNotNull(smc.getUserManagerConfig());
+        BeanConfig umc = smc.getUserManagerConfig();
+
+        Properties params = umc.getParameters();
+        assertNotNull(params);
+
+        assertFalse(params.containsKey(UserManagerImpl.PARAM_COMPATIBILE_JR16));
+        assertTrue(Boolean.parseBoolean(params.getProperty(UserManagerImpl.PARAM_AUTO_EXPAND_TREE)));
+        assertEquals(4, Integer.parseInt(params.getProperty(UserManagerImpl.PARAM_DEFAULT_DEPTH)));
+        assertEquals(2000, Long.parseLong(params.getProperty(UserManagerImpl.PARAM_AUTO_EXPAND_SIZE)));
+    }
+
     public void testInvalidConfig() {
         List invalid = new ArrayList();
         invalid.add(new InputSource(new StringReader(INVALID_CONFIG_1)));
@@ -160,6 +181,24 @@
             "        </LoginModule>\n" +
             "    </Security>";
 
+    private static final String CONFIG_3 =
+            "    <Security appName=\"Jackrabbit\">" +
+            "        <SecurityManager class=\"org.apache.jackrabbit.core.DefaultSecurityManager\" workspaceName=\"security\">" +
+            "           <UserManager class=\"\">" +
+            "           <param name=\"defaultDepth\" value=\"4\"/>" +
+            "           <param name=\"autoExpandTree\" value=\"true\"/>" +
+            "           <param name=\"autoExpandSize\" value=\"2000\"/>" +
+            "           </UserManager>" +
+            "        </SecurityManager>" +
+            "        <AccessManager class=\"org.apache.jackrabbit.core.security.DefaultAccessManager\">" +
+            "        </AccessManager>" +
+            "        <LoginModule class=\"org.apache.jackrabbit.core.security.authentication.DefaultLoginModule\">" +
+            "           <param name=\"anonymousId\" value=\"anonymous\"/>" +
+            "           <param name=\"adminId\" value=\"admin\"/>" +
+            "           <param name=\"principalProvider\" value=\"org.apache.jackrabbit.TestPrincipalProvider\"/>" +
+            "        </LoginModule>\n" +
+            "    </Security>";
+
     private static final String INVALID_CONFIG_1 =
             "    <Security appName=\"Jackrabbit\">" +
             "        <SecurityManager class=\"org.apache.jackrabbit.core.security.simple.SimpleSecurityManager\"></SecurityManager>" +

Modified: jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/GroupAdministratorTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/GroupAdministratorTest.java?rev=794702&r1=794701&r2=794702&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/GroupAdministratorTest.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/GroupAdministratorTest.java Thu Jul 16 14:54:08 2009
@@ -42,8 +42,8 @@
     private String uPath;
     private Session uSession;
 
-    private String parentUID;
-    private String childUID;
+    private String otherUID;
+    private String otherUID2;
     private String grID;
 
 
@@ -55,13 +55,13 @@
         // create a first user
         Principal p = getTestPrincipal();
         UserImpl pUser = (UserImpl) userMgr.createUser(p.getName(), buildPassword(p));
-        parentUID = pUser.getID();
+        otherUID = pUser.getID();
 
-        // create a second user 'below' the first user and make it group-admin
+        // create a second user and make it group-admin
         p = getTestPrincipal();
         String pw = buildPassword(p);
         Credentials creds = buildCredentials(p.getName(), pw);
-        User user = userMgr.createUser(p.getName(), pw, p, pUser.getNode().getPath());
+        User user = userMgr.createUser(p.getName(), pw);
         uID = user.getID();
         uPath = ((UserImpl) user).getNode().getPath();
 
@@ -88,7 +88,7 @@
             groupAdmin.removeMember(userMgr.getAuthorizable(uID));
 
             // remove all users that have been created
-            Authorizable a = userMgr.getAuthorizable(parentUID);
+            Authorizable a = userMgr.getAuthorizable(otherUID);
             if (a != null) {
                 a.remove();
             }
@@ -97,13 +97,13 @@
         super.tearDown();
     }
 
-    private String getChildID() throws RepositoryException {
-        if (childUID == null) {
-            // create a third child user below
+    private String getYetAnotherID() throws RepositoryException {
+        if (otherUID2 == null) {
+            // create a third user
             Principal p = getTestPrincipal();
-            childUID = userMgr.createUser(p.getName(), buildPassword(p), p, uPath).getID();
+            otherUID2 = userMgr.createUser(p.getName(), buildPassword(p), p, uPath).getID();
         }
-        return childUID;
+        return otherUID2;
     }
 
     public void testIsGroupAdmin() throws RepositoryException, NotExecutableException {
@@ -157,7 +157,7 @@
         Group testGroup = null;
         try {
             testGroup = umgr.createGroup(getTestPrincipal(), "/any/intermediate/path");
-            assertTrue(Text.isDescendant(UserConstants.GROUPS_PATH + "/any/intermediate/path", ((GroupImpl)testGroup).getNode().getPath()));
+            assertEquals("Intermediate path must be ignored.",-1, ((GroupImpl)testGroup).getNode().getPath().indexOf("/any/intermediate/path"));
         } finally {
             if (testGroup != null) {
                 testGroup.remove();
@@ -165,15 +165,15 @@
         }
     }
 
-    public void testAddChildToGroup() throws RepositoryException, NotExecutableException {
+    public void testAddToGroup() throws RepositoryException, NotExecutableException {
         UserManager umgr = getUserManager(uSession);
-        Authorizable cU = umgr.getAuthorizable(getChildID());
+        Authorizable cU = umgr.getAuthorizable(getYetAnotherID());
         Group gr = (Group) umgr.getAuthorizable(grID);
 
-        // adding and removing the child-user as member of a group not
+        // adding and removing the child-user as member of a group must not 
         // succeed as long editing session is not user-admin.
         try {
-            assertFalse(gr.addMember(cU));
+            assertFalse("Modifying group membership requires GroupAdmin and UserAdmin.",gr.addMember(cU));
         } catch (AccessDeniedException e) {
             // ok
         } finally {
@@ -181,9 +181,9 @@
         }
     }
 
-    public void testAddChildToGroup2() throws RepositoryException, NotExecutableException {
+    public void testAddToGroup2() throws RepositoryException, NotExecutableException {
         UserManager umgr = getUserManager(uSession);
-        Authorizable cU = umgr.getAuthorizable(getChildID());
+        Authorizable cU = umgr.getAuthorizable(getYetAnotherID());
 
         Authorizable auth = umgr.getAuthorizable(UserConstants.USER_ADMIN_GROUP_NAME);
         if (auth == null || !auth.isGroup()) {
@@ -223,7 +223,7 @@
             assertTrue(userAdmin.isMember(self));
 
             // add child-user to test group
-            Authorizable testUser = umgr.getAuthorizable(getChildID());
+            Authorizable testUser = umgr.getAuthorizable(getYetAnotherID());
             assertFalse(testGroup.isMember(testUser));
             assertTrue(testGroup.addMember(testUser));
         } finally {
@@ -241,7 +241,7 @@
         try {
             // let superuser create child user below the user with uID.
             UserManager umgr = getUserManager(uSession);
-            Authorizable cU = umgr.getAuthorizable(getChildID());
+            Authorizable cU = umgr.getAuthorizable(getYetAnotherID());
             Group uadminGr = (Group) umgr.getAuthorizable(UserConstants.USER_ADMIN_GROUP_NAME);
             if (uadminGr.isMember(cU)) {
                 throw new RepositoryException("Test user is already member -> cannot execute.");
@@ -257,14 +257,14 @@
         }
     }
 
-    public void testAddParentToGroup() throws RepositoryException, NotExecutableException {
+    public void testAddOtherUserToGroup() throws RepositoryException, NotExecutableException {
         UserManager umgr = getUserManager(uSession);
 
-        Authorizable pU = umgr.getAuthorizable(parentUID);
+        Authorizable pU = umgr.getAuthorizable(otherUID);
         Group gr = (Group) umgr.getAuthorizable(groupAdmin.getID());
 
-        // adding and removing the parent-user as member of a group must
-        // never succeed.
+        // adding and removing the parent-user as member of a group must not
+        // succeed: editing session isn't UserAdmin
         try {
             assertFalse(gr.addMember(pU));
         } catch (AccessDeniedException e) {
@@ -273,7 +273,8 @@
             gr.removeMember(pU);
         }
 
-        // ... even if the editing user becomes member of the user-admin group
+        // ... if the editing user becomes member of the user-admin group it
+        // must work.
         Group uAdministrators = null;
         try {
             Authorizable userAdmin = userMgr.getAuthorizable(UserConstants.USER_ADMIN_GROUP_NAME);
@@ -283,12 +284,8 @@
             uAdministrators = (Group) userAdmin;
             uAdministrators.addMember(userMgr.getAuthorizable(uID));
 
-            try {
-                assertFalse(gr.addMember(pU));
-                gr.removeMember(pU);
-            } catch (AccessDeniedException e) {
-                // fine as well.
-            }
+            assertTrue(gr.addMember(pU));
+            gr.removeMember(pU);
         } finally {
             // let superuser do the clean up.
             // remove testuser from u-admin group again.
@@ -410,7 +407,7 @@
         UserManager umgr = getUserManager(uSession);
         Principal selfPrinc = umgr.getAuthorizable(uID).getPrincipal();
 
-        User child = (User) umgr.getAuthorizable(getChildID());
+        User child = (User) umgr.getAuthorizable(getYetAnotherID());
         Impersonation impers = child.getImpersonation();
         assertFalse(impers.allows(buildSubject(selfPrinc)));
         try {
@@ -420,7 +417,7 @@
         }
         assertFalse(impers.allows(buildSubject(selfPrinc)));
 
-        User parent = (User) umgr.getAuthorizable(parentUID);
+        User parent = (User) umgr.getAuthorizable(otherUID);
         impers = parent.getImpersonation();
         assertFalse(impers.allows(buildSubject(selfPrinc)));
         try {
@@ -430,4 +427,21 @@
         }
         assertFalse(impers.allows(buildSubject(selfPrinc)));
     }
+
+    public void testPersisted() throws NotExecutableException, RepositoryException {
+        UserManager umgr = getUserManager(uSession);
+        Group gr = null;
+        try {
+            Principal p = getTestPrincipal();
+            gr = umgr.createGroup(p);
+
+            Authorizable az = userMgr.getAuthorizable(gr.getID());
+            assertNotNull(az);
+            assertEquals(gr.getID(), az.getID());
+        } finally {
+            if (gr != null) {
+                gr.remove();
+            }
+        }
+    }
 }
\ No newline at end of file



Mime
View raw message