You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jackrabbit.apache.org by an...@apache.org on 2009/07/16 16:54:09 UTC
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...
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 < 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 < default depth are not reachable any more.</li>
+ * <li>If default depth is decreased:<br>
+ * All authorizables on levels > 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 <
+ * 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 < 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 < default depth are not reachable any more.</li>
+ * <li>If default depth is decreased:<br>
+ * All authorizables on levels > 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