/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.jackrabbit.core.security.user;
import org.apache.jackrabbit.api.security.principal.ItemBasedPrincipal;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.api.security.user.AuthorizableExistsException;
import org.apache.jackrabbit.api.security.user.AuthorizableTypeException;
import org.apache.jackrabbit.api.security.user.Group;
import org.apache.jackrabbit.api.security.user.Query;
import org.apache.jackrabbit.api.security.user.User;
import org.apache.jackrabbit.api.security.user.UserManager;
import org.apache.jackrabbit.core.ItemImpl;
import org.apache.jackrabbit.core.NodeImpl;
import org.apache.jackrabbit.core.ProtectedItemModifier;
import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.SessionListener;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.security.principal.EveryonePrincipal;
import org.apache.jackrabbit.core.security.principal.PrincipalImpl;
import org.apache.jackrabbit.core.security.user.action.AuthorizableAction;
import org.apache.jackrabbit.core.session.SessionOperation;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.util.Text;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.AccessDeniedException;
import javax.jcr.ItemExistsException;
import javax.jcr.ItemNotFoundException;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.Value;
import javax.jcr.lock.LockException;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.version.VersionException;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.util.HashSet;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
/**
* <h2>Implementation Characteristics</h2>
*
* 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} (unless otherwise configured).</li>
* <li>The Id of an authorizable is stored in the jcr:uuid property (md5 hash).</li>
* <li>In order to structure the users and groups tree and avoid creating a flat
* hierarchy, additional hierarchy nodes of type "rep:AuthorizableFolder" are
* introduced using
* <ul>
* <li>the specified intermediate path passed to the create methods</li>
* <li>or some built-in logic if the intermediate path is missing.</li>
* </ul>
* </li>
* </ul>
*
* <h3>Authorizable Creation</h3>
*
* The built-in logic applies the following rules:
* <ul>
* <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 #getUsersPath()}
* or {@link #getGroupsPath()}.</li>
* <li>By default 2 levels (depth == 2) are created.</li>
* <li>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>
* </ul>
* Examples:
* Creating an non-existing user with ID 'aSmith' without specifying an
* intermediate path 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>
*
* Creating a non-existing user with ID 'aSmith' specifying an intermediate
* path 'some/tree' would result in the following structure:
*
* <pre>
* + rep:security [nt:unstructured]
* + rep:authorizables [rep:AuthorizableFolder]
* + rep:users [rep:AuthorizableFolder]
* + some [rep:AuthorizableFolder]
* + tree [rep:AuthorizableFolder]
* + aSmith [rep:User]
* </pre>
*
* <h3>Configuration</h3>
*
* This <code>UserManager</code> is able to handle the following configuration
* options:
*
* <h4>Configuration Parameters</h4>
* <ul>
* <li>{@link #PARAM_USERS_PATH}: Defines where user nodes are created.
* If missing set to {@link #USERS_PATH}.</li>
* <li>{@link #PARAM_GROUPS_PATH}. Defines where group nodes are created.
* If missing set to {@link #GROUPS_PATH}.</li>
* <li>{@link #PARAM_COMPATIBLE_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 to store authorizable nodes.<br>The value is
* expected to be a positive integer greater than zero. The default
* number of levels is 2.
* </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 value is expected to be
* a positive long greater than zero. The default value is 1000.</li>
* <li>{@link #PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE}: If this parameter is present
* group memberships are collected in a node structure below {@link UserConstants#N_MEMBERS}
* instead of the default multi valued property {@link UserConstants#P_MEMBERS}.
* Its value determines the maximum number of member properties until additional
* intermediate nodes are inserted. Valid parameter values are integers > 4.</li>
* <li>{@link #PARAM_PASSWORD_HASH_ALGORITHM}: Optional parameter to configure
* the algorithm used for password hash generation. The default value is
* {@link PasswordUtility#DEFAULT_ALGORITHM}.</li>
* <li>{@link #PARAM_PASSWORD_HASH_ITERATIONS}: Optional parameter to configure
* the number of iterations used for password hash generations. The default
* value is {@link PasswordUtility#DEFAULT_ITERATIONS}.</li>
* </ul>
*
* <h4>Authorizable Actions</h4>
* In addition to the specified configuration parameters this user manager
* implementation allows to define zero to many {@link AuthorizableAction}s.
* Authorizable actions provide the ability to execute additional validation or
* tasks upon authorizable creation, removal and upon changing a users password.<br>
* See also {@link org.apache.jackrabbit.core.config.UserManagerConfig#getAuthorizableActions()}
*/
public class UserManagerImpl extends ProtectedItemModifier
implements UserManager, UserConstants, SessionListener {
/**
* Configuration option to change the
* {@link UserConstants#USERS_PATH default path} for creating users.
*/
public static final String PARAM_USERS_PATH = "usersPath";
/**
* Configuration option to change the
* {@link UserConstants#GROUPS_PATH default path} for creating groups.
*/
public static final String PARAM_GROUPS_PATH = "groupsPath";
/**
* @deprecated Use {@link #PARAM_COMPATIBLE_JR16} instead.
*/
public static final String PARAM_COMPATIBILE_JR16 = "compatibleJR16";
/**
* 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_COMPATIBLE_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";
/**
* If this parameter is present group members are collected in a node
* structure below {@link UserConstants#N_MEMBERS} instead of the default
* multi valued property {@link UserConstants#P_MEMBERS}. Its value determines
* the maximum number of member properties until additional intermediate nodes
* are inserted. Valid values are integers > 4. The default value is 0 and
* indicates that the {@link UserConstants#P_MEMBERS} property is used to
* record group members.
*/
public static final String PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE = "groupMembershipSplitSize";
/**
* Configuration parameter to change the default algorithm used to generate
* password hashes. The default value is {@link PasswordUtility#DEFAULT_ALGORITHM}.
*/
public static final String PARAM_PASSWORD_HASH_ALGORITHM = "passwordHashAlgorithm";
/**
* Configuration parameter to change the number of iterations used for
* password hash generation. The default value is {@link PasswordUtility#DEFAULT_ITERATIONS}.
*/
public static final String PARAM_PASSWORD_HASH_ITERATIONS = "passwordHashIterations";
private static final Logger log = LoggerFactory.getLogger(UserManagerImpl.class);
private final SessionImpl session;
private final String adminId;
private final NodeResolver authResolver;
private final NodeCreator nodeCreator;
private final UserManagerConfig config;
private final String usersPath;
private final String groupsPath;
private final MembershipCache membershipCache;
/**
* Create a new <code>UserManager</code> with the default configuration.
*
* @param session The editing/reading session.
* @param adminId The user ID of the administrator.
* @throws javax.jcr.RepositoryException If an error occurs.
*/
public UserManagerImpl(SessionImpl session, String adminId) throws RepositoryException {
this(session, adminId, null, null);
}
/**
* Create a new <code>UserManager</code>
*
* @param session The editing/reading session.
* @param adminId The user ID of the administrator.
* @param config The configuration parameters.
* @throws javax.jcr.RepositoryException If an error occurs.
*/
public UserManagerImpl(SessionImpl session, String adminId, Properties config) throws RepositoryException {
this(session, adminId, config, null);
}
/**
* Create a new <code>UserManager</code> for the given <code>session</code>.
* Currently the following configuration options are respected:
*
* <ul>
* <li>{@link #PARAM_USERS_PATH}. If missing set to {@link UserConstants#USERS_PATH}.</li>
* <li>{@link #PARAM_GROUPS_PATH}. If missing set to {@link UserConstants#GROUPS_PATH}.</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>
* <li>{@link #PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE}. The default is 0 which means use
* {@link UserConstants#P_MEMBERS}.</li>
* </ul>
*
* See the overall {@link UserManagerImpl introduction} for details.
*
* @param session The editing/reading session.
* @param adminId The user ID of the administrator.
* @param config The configuration parameters.
* @param mCache Shared membership cache.
* @throws javax.jcr.RepositoryException If an error occurs.
*/
public UserManagerImpl(SessionImpl session, String adminId, Properties config,
MembershipCache mCache) throws RepositoryException {
this(session, new UserManagerConfig(config, adminId, null), mCache);
}
/**
* Create a new <code>UserManager</code> for the given <code>session</code>.
*
* @param session The editing/reading session.
* @param config The user manager configuration.
* @param mCache The shared membership cache.
* @throws RepositoryException If an error occurs.
*/
private UserManagerImpl(SessionImpl session, UserManagerConfig config, MembershipCache mCache) throws RepositoryException {
this.session = session;
this.adminId = config.getAdminId();
this.config = config;
nodeCreator = new NodeCreator(config);
this.usersPath = config.getConfigValue(PARAM_USERS_PATH, USERS_PATH);
this.groupsPath = config.getConfigValue(PARAM_GROUPS_PATH, GROUPS_PATH);
if (mCache != null) {
membershipCache = mCache;
} else {
membershipCache = new MembershipCache(session, groupsPath, hasMemberSplitSize());
}
NodeResolver nr;
try {
nr = new IndexNodeResolver(session, session);
} catch (RepositoryException e) {
log.debug("UserManager: no QueryManager available for workspace '" + session.getWorkspace().getName() + "' -> Use traversing node resolver.");
nr = new TraversingNodeResolver(session, session);
}
authResolver = nr;
authResolver.setSearchRoots(usersPath, groupsPath);
}
/**
* Implementation specific methods revealing where users are created within
* the content.
*
* @return root path for user content.
* @see #PARAM_USERS_PATH For the corresponding configuration parameter.
*/
public String getUsersPath() {
return usersPath;
}
/**
* Implementation specific methods revealing where groups are created within
* the content.
*
* @return root path for group content.
* @see #PARAM_GROUPS_PATH For the corresponding configuration parameter.
*/
public String getGroupsPath() {
return groupsPath;
}
/**
* @return The membership cache present with this user manager instance.
*/
public MembershipCache getMembershipCache() {
return membershipCache;
}
/**
* Maximum number of properties on the group membership node structure under
* {@link UserConstants#N_MEMBERS} until additional intermediate nodes are inserted.
* If 0 (default), {@link UserConstants#P_MEMBERS} is used to record group
* memberships.
*
* @return The maximum number of group members before splitting up the structure.
*/
public int getMemberSplitSize() {
int splitSize = config.getConfigValue(PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE, 0);
if (splitSize != 0 && splitSize < 4) {
log.warn("Invalid value {} for {}. Expected integer >= 4", splitSize, PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE);
splitSize = 0;
}
return splitSize;
}
/**
* Returns <code>true</code> if the split-member configuration parameter
* is greater or equal than 4 indicating that group members should be stored
* in a tree instead of a single multivalued property.
*
* @return true if group members are being stored in a tree instead of a
* single multivalued property.
*/
public boolean hasMemberSplitSize() {
return getMemberSplitSize() >= 4;
}
/**
* Set the authorizable actions that will be invoked upon authorizable
* creation and removal.
*
* @param authorizableActions An array of authorizable actions.
*/
public void setAuthorizableActions(AuthorizableAction[] authorizableActions) {
config.setAuthorizableActions(authorizableActions);
}
//--------------------------------------------------------< UserManager >---
/**
* @see UserManager#getAuthorizable(String)
*/
public Authorizable getAuthorizable(String id) throws RepositoryException {
if (id == null || id.length() == 0) {
throw new IllegalArgumentException("Invalid authorizable name '" + id + "'");
}
Authorizable a = internalGetAuthorizable(id);
/**
* Extra check for the existence of the administrator user that must
* always exist.
* In case it got removed if must be recreated using a system session.
* Since a regular session may lack read permission on the admin-user's
* node an explicit test for the current editing session being
* a system session is performed.
*/
if (a == null && adminId.equals(id) && session.isSystem()) {
log.info("Admin user does not exist.");
a = createAdmin();
}
return a;
}
/**
* @see UserManager#getAuthorizable(String, Class)
*/
public <T extends Authorizable> T getAuthorizable(String id, Class<T> authorizableClass) throws AuthorizableTypeException, RepositoryException {
Authorizable authorizable = getAuthorizable(id);
if (authorizable == null) {
return null;
} else {
if (authorizableClass != null && authorizableClass.isInstance(authorizable)) {
return authorizableClass.cast(authorizable);
} else {
throw new AuthorizableTypeException("Invalid authorizable type for authorizable '" + id + "'");
}
}
}
/**
* @see UserManager#getAuthorizable(Principal)
*/
public Authorizable getAuthorizable(Principal principal) throws RepositoryException {
NodeImpl n = null;
// shortcuts that avoids executing a query.
if (principal instanceof AuthorizableImpl.NodeBasedPrincipal) {
NodeId nodeId = ((AuthorizableImpl.NodeBasedPrincipal) principal).getNodeId();
try {
n = session.getNodeById(nodeId);
} catch (ItemNotFoundException e) {
// no such authorizable -> null
}
} else if (principal instanceof ItemBasedPrincipal) {
String authPath = ((ItemBasedPrincipal) principal).getPath();
if (session.nodeExists(authPath)) {
n = (NodeImpl) session.getNode(authPath);
}
} else {
// another Principal implementation.
// a) try short-cut that works in case of ID.equals(principalName) only.
// b) execute query in case of pName mismatch or exception. however, query
// requires persisted user nodes (see known issue of UserImporter).
String name = principal.getName();
try {
Authorizable a = internalGetAuthorizable(name);
if (a != null && name.equals(a.getPrincipal().getName())) {
return a;
}
} catch (RepositoryException e) {
// ignore and execute the query.
}
// authorizable whose ID matched the principal name -> search.
n = (NodeImpl) authResolver.findNode(P_PRINCIPAL_NAME, name, NT_REP_AUTHORIZABLE);
}
// build the corresponding authorizable object
return getAuthorizable(n);
}
/**
* Always throws <code>UnsupportedRepositoryOperationException</code> since
* this implementation of the user management API does not allow to retrieve
* the path of an authorizable.
*
* @see UserManager#getAuthorizableByPath(String)
*/
public Authorizable getAuthorizableByPath(String path) throws UnsupportedRepositoryOperationException, RepositoryException {
throw new UnsupportedRepositoryOperationException();
}
/**
* @see UserManager#findAuthorizables(String,String)
*/
public Iterator<Authorizable> findAuthorizables(String relPath, String value) throws RepositoryException {
return findAuthorizables(relPath, value, SEARCH_TYPE_AUTHORIZABLE);
}
/**
* @see UserManager#findAuthorizables(String,String, int)
*/
public Iterator<Authorizable> findAuthorizables(String relPath, String value, int searchType)
throws RepositoryException {
if (searchType < SEARCH_TYPE_USER || searchType > SEARCH_TYPE_AUTHORIZABLE) {
throw new IllegalArgumentException("Invalid search type " + searchType);
}
Path path = session.getQPath(relPath);
NodeIterator nodes;
if (relPath.indexOf('/') == -1) {
// search for properties somewhere below an authorizable node
nodes = authResolver.findNodes(path, value, searchType, true, Long.MAX_VALUE);
} else {
path = path.getNormalizedPath();
if (path.getLength() == 1) {
// only search below the authorizable node
Name ntName;
switch (searchType) {
case SEARCH_TYPE_GROUP:
ntName = NT_REP_GROUP;
break;
case SEARCH_TYPE_USER:
ntName = NT_REP_USER;
break;
default:
ntName = NT_REP_AUTHORIZABLE;
}
nodes = authResolver.findNodes(path.getName(), value, ntName, true);
} else {
// search below authorizable nodes but take some path constraints
// into account.
nodes = authResolver.findNodes(path, value, searchType, true, Long.MAX_VALUE);
}
}
return new AuthorizableIterator(nodes);
}
/**
* @see UserManager#findAuthorizables(Query)
*/
public Iterator<Authorizable> findAuthorizables(Query query) throws RepositoryException {
XPathQueryBuilder builder = new XPathQueryBuilder();
query.build(builder);
return new XPathQueryEvaluator(builder, this, session).eval();
}
/**
* @see UserManager#createUser(String,String)
*/
public User createUser(String userID, String password) throws RepositoryException {
return createUser(userID, password, new PrincipalImpl(userID), null);
}
/**
* @see UserManager#createUser(String, String, java.security.Principal, String)
*/
public User createUser(String userID, String password,
Principal principal, String intermediatePath)
throws AuthorizableExistsException, RepositoryException {
checkValidID(userID);
// NOTE: password validation during setPassword and onCreate.
// NOTE: principal validation during setPrincipal call.
try {
NodeImpl userNode = (NodeImpl) nodeCreator.createUserNode(userID, intermediatePath);
setPrincipal(userNode, principal);
setPassword(userNode, password, true);
User user = createUser(userNode);
onCreate(user, password);
if (isAutoSave()) {
session.save();
}
log.debug("User created: " + userID + "; " + userNode.getPath());
return user;
} catch (RepositoryException e) {
// something went wrong -> revert changes and re-throw
session.refresh(false);
log.debug("Failed to create new User, reverting changes.");
throw e;
}
}
public User createSystemUser(String userID, String intermediatePath) throws AuthorizableExistsException, RepositoryException {
throw new UnsupportedRepositoryOperationException("Not yet implemented.");
}
/**
* @see UserManager#createGroup(String)
*/
public Group createGroup(String groupID) throws AuthorizableExistsException, RepositoryException {
return createGroup(groupID, new PrincipalImpl(groupID), null);
}
/**
* Same as {@link #createGroup(java.security.Principal, String)} where the
* intermediate path is <code>null</code>.
* @see UserManager#createGroup(Principal)
*/
public Group createGroup(Principal principal) throws RepositoryException {
return createGroup(principal, null);
}
/**
* Same as {@link #createGroup(String, Principal, String)} where a groupID
* is generated from the principal name. If the name conflicts with an
* existing authorizable ID (may happen in cases where
* principal name != ID) the principal name is expanded by a suffix;
* otherwise the resulting group ID equals the principal name.
*
* @param principal A principal that doesn't yet represent an existing user
* or group.
* @param intermediatePath Is always ignored.
* @return A new group.
* @throws AuthorizableExistsException
* @throws RepositoryException
* @see UserManager#createGroup(java.security.Principal, String)
*/
public Group createGroup(Principal principal, String intermediatePath) throws AuthorizableExistsException, RepositoryException {
checkValidPrincipal(principal, true);
String groupID = getGroupId(principal.getName());
return createGroup(groupID, principal, intermediatePath);
}
/**
* Create a new <code>Group</code> from the given <code>groupID</code> and
* <code>principal</code>. It will be created below the defined
* {@link #getGroupsPath() group path}.<br>
* Non-existent elements of the Path will be created as nodes
* of type {@link #NT_REP_AUTHORIZABLE_FOLDER rep:AuthorizableFolder}.
*
* @param groupID A groupID that hasn't been used before for another
* user or group.
* @param principal A principal that doesn't yet represent an existing user
* or group.
* @param intermediatePath Is always ignored.
* @return A new group.
* @throws AuthorizableExistsException
* @throws RepositoryException
* @see UserManager#createGroup(String, java.security.Principal, String)
*/
public Group createGroup(String groupID, Principal principal, String intermediatePath) throws AuthorizableExistsException, RepositoryException {
checkValidID(groupID);
// NOTE: principal validation during setPrincipal call.
try {
NodeImpl groupNode = (NodeImpl) nodeCreator.createGroupNode(groupID, intermediatePath);
if (principal != null) {
setPrincipal(groupNode, principal);
}
Group group = createGroup(groupNode);
onCreate(group);
if (isAutoSave()) {
session.save();
}
log.debug("Group created: " + groupID + "; " + groupNode.getPath());
return group;
} catch (RepositoryException e) {
session.refresh(false);
log.debug("newInstance new Group failed, revert changes on parent");
throw e;
}
}
/**
* Always returns <code>true</code> as by default the autoSave behavior
* cannot be altered (see also {@link #autoSave(boolean)}.
*
* @return Always <code>true</code>.
* @see org.apache.jackrabbit.api.security.user.UserManager#isAutoSave()
*/
public boolean isAutoSave() {
return true;
}
/**
* Always throws <code>unsupportedRepositoryOperationException</code> as
* modification of the autosave behavior is not supported.
*
* @see UserManager#autoSave(boolean)
*/
public void autoSave(boolean enable) throws UnsupportedRepositoryOperationException, RepositoryException {
throw new UnsupportedRepositoryOperationException("Cannot change autosave behavior.");
}
//--------------------------------------------------------------------------
/**
*
* @param node The new user/group node.
* @param principal A valid non-null principal.
* @throws AuthorizableExistsException If there is already another user/group
* with the same principal name.
* @throws RepositoryException If another error occurs.
*/
void setPrincipal(NodeImpl node, Principal principal) throws AuthorizableExistsException, RepositoryException {
checkValidPrincipal(principal, node.isNodeType(NT_REP_GROUP));
/*
Check if there is *another* authorizable with the same principal.
The additional validation (nodes not be same) is required in order to
circumvent problems with re-importing existing authorizable in which
case the original user/group node is being recreated but the search
used to look for an colliding authorizable still finds the persisted
node.
*/
Authorizable existing = getAuthorizable(principal);
if (existing != null && !((AuthorizableImpl) existing).getNode().isSame(node)) {
throw new AuthorizableExistsException("Authorizable for '" + principal.getName() + "' already exists: ");
}
if (!node.isNew() || node.hasProperty(P_PRINCIPAL_NAME)) {
throw new RepositoryException("rep:principalName can only be set once on a new node.");
}
setProperty(node, P_PRINCIPAL_NAME, getValue(principal.getName()), true);
}
/**
* Generate a password value from the specified string and set the
* {@link UserConstants#P_PASSWORD} property to the given user node.
*
* @param userNode A user node.
* @param password The password value.
* @param forceHash If <code>true</code> the specified password string will
* always be hashed; otherwise the hash will only be generated if it appears
* to be a {@link PasswordUtility#isPlainTextPassword(String) plain text} password.
* @throws RepositoryException If an exception occurs.
*/
void setPassword(NodeImpl userNode, String password, boolean forceHash) throws RepositoryException {
if (password == null) {
if (userNode.isNew()) {
// allow creation of system-only users with 'null' passwords that cannot login
return;
} else {
throw new IllegalArgumentException("Password may not be null.");
}
}
String pwHash;
if (forceHash || PasswordUtility.isPlainTextPassword(password)) {
try {
String algorithm = config.getConfigValue(PARAM_PASSWORD_HASH_ALGORITHM, PasswordUtility.DEFAULT_ALGORITHM);
int iterations = config.getConfigValue(PARAM_PASSWORD_HASH_ITERATIONS, PasswordUtility.DEFAULT_ITERATIONS);
pwHash = PasswordUtility.buildPasswordHash(password, algorithm, PasswordUtility.DEFAULT_SALT_SIZE, iterations);
} catch (NoSuchAlgorithmException e) {
throw new RepositoryException(e);
} catch (UnsupportedEncodingException e) {
throw new RepositoryException(e);
}
} else {
pwHash = password;
}
setProperty(userNode, P_PASSWORD, getValue(pwHash), userNode.isNew());
}
void setProtectedProperty(NodeImpl node, Name propName, Value value) throws RepositoryException, LockException, ConstraintViolationException, ItemExistsException, VersionException {
setProperty(node, propName, value);
if (isAutoSave()) {
node.save();
}
}
void setProtectedProperty(NodeImpl node, Name propName, Value[] values) throws RepositoryException, LockException, ConstraintViolationException, ItemExistsException, VersionException {
setProperty(node, propName, values);
if (isAutoSave()) {
node.save();
}
}
void setProtectedProperty(NodeImpl node, Name propName, Value[] values, int type) throws RepositoryException, LockException, ConstraintViolationException, ItemExistsException, VersionException {
setProperty(node, propName, values, type);
if (isAutoSave()) {
node.save();
}
}
void removeProtectedItem(ItemImpl item, Node parent) throws RepositoryException, AccessDeniedException, VersionException {
removeItem(item);
if (isAutoSave()) {
parent.save();
}
}
NodeImpl addProtectedNode(NodeImpl parent, Name name, Name ntName) throws RepositoryException {
NodeImpl n = addNode(parent, name, ntName);
if (isAutoSave()) {
parent.save();
}
return n;
}
<T> T performProtectedOperation(SessionImpl session, SessionOperation<T> operation) throws RepositoryException {
return performProtected(session, operation);
}
/**
* Implementation specific method used to retrieve a user/group by Node.
* <code>Null</code> is returned if
* <pre>
* - the passed node is <code>null</code>,
* - doesn't have the correct node type or
* - isn't placed underneath the configured user/group tree.
* </pre>
*
* @param n A user/group node.
* @return An authorizable or <code>null</code>.
* @throws RepositoryException If an error occurs.
*/
Authorizable getAuthorizable(NodeImpl n) throws RepositoryException {
Authorizable authorz = null;
if (n != null) {
String path = n.getPath();
if (n.isNodeType(NT_REP_USER)) {
if (Text.isDescendant(usersPath, path)) {
authorz = createUser(n);
} else {
/* user node outside of configured tree -> return null */
log.error("User node '" + path + "' outside of configured user tree ('" + usersPath + "') -> Not a valid user.");
}
} else if (n.isNodeType(NT_REP_GROUP)) {
if (Text.isDescendant(groupsPath, path)) {
authorz = createGroup(n);
} else {
/* group node outside of configured tree -> return null */
log.error("Group node '" + path + "' outside of configured group tree ('" + groupsPath + "') -> Not a valid group.");
}
} else {
/* else some other node type -> return null. */
log.warn("Unexpected user/group node type " + n.getPrimaryNodeType().getName());
}
} /* else no matching node -> return null */
return authorz;
}
/**
* Always throws <code>UnsupportedRepositoryOperationException</code> since
* the node may reside in a different workspace than the editing <code>Session</code>.
*/
String getPath(Node authorizableNode) throws UnsupportedRepositoryOperationException, RepositoryException {
throw new UnsupportedRepositoryOperationException();
}
/**
* Returns the session associated with this user manager.
*
* @return the session.
*/
SessionImpl getSession() {
return session;
}
/**
* 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 group id.
* @return a group id.
* @throws RepositoryException If an error occurs.
*/
private String getGroupId(String principalName) throws RepositoryException {
String groupID = principalName;
int i = 0;
while (internalGetAuthorizable(groupID) != null) {
groupID = principalName + "_" + i;
i++;
}
return groupID;
}
/**
* @param id The user or group ID.
* @return The authorizable with the given <code>id</code> or <code>null</code>.
* @throws RepositoryException If an error occurs.
*/
private Authorizable internalGetAuthorizable(String id) throws RepositoryException {
NodeId nodeId = buildNodeId(id);
NodeImpl n = null;
try {
n = session.getNodeById(nodeId);
} catch (ItemNotFoundException e) {
boolean compatibleJR16 = config.getConfigValue(PARAM_COMPATIBLE_JR16, false);
if (compatibleJR16) {
// backwards-compatibility with JR < 2.0 user/group structure that doesn't
// allow to determine existence of an authorizable from the id directly.
// search for it the node belonging to that id
n = (NodeImpl) authResolver.findNode(P_USERID, id, NT_REP_USER);
if (n == null) {
// no user -> look for group.
// NOTE: JR < 2.0 always returned groupIDs that didn't contain any
// illegal JCR chars. Since Group.getID() 'unescapes' the node
// name additional escaping is required.
Name nodeName = session.getQName(Text.escapeIllegalJcrChars(id));
n = (NodeImpl) authResolver.findNode(nodeName, NT_REP_GROUP);
}
} // else: no matching node found -> ignore exception.
}
return getAuthorizable(n);
}
private Value getValue(String strValue) {
return session.getValueFactory().createValue(strValue);
}
/**
* @param userID A userID.
* @return true if the given userID belongs to the administrator user.
*/
boolean isAdminId(String userID) {
return (adminId != null) && adminId.equals(userID);
}
/**
* Build the User object from the given user node.
*
* @param userNode The new user node.
* @return An instance of <code>User</code>.
* @throws RepositoryException If the node isn't a child of the configured
* usersPath-node or if another error occurs.
*/
User createUser(NodeImpl userNode) throws RepositoryException {
if (userNode == null || !userNode.isNodeType(NT_REP_USER)) {
throw new IllegalArgumentException();
}
if (!Text.isDescendant(usersPath, userNode.getPath())) {
throw new RepositoryException("User has to be within the User Path");
}
return doCreateUser(userNode);
}
/**
* Build the user object from the given user node. May be overridden to
* return a custom implementation.
*
* @param node user node
* @return the user object
* @throws RepositoryException if an error occurs
*/
protected User doCreateUser(NodeImpl node) throws RepositoryException {
return new UserImpl(node, this);
}
/**
* Build the Group object from the given group node.
*
* @param groupNode The new group node.
* @return An instance of <code>Group</code>.
* @throws RepositoryException If the node isn't a child of the configured
* groupsPath-node or if another error occurs.
*/
Group createGroup(NodeImpl groupNode) throws RepositoryException {
if (groupNode == null || !groupNode.isNodeType(NT_REP_GROUP)) {
throw new IllegalArgumentException();
}
if (!Text.isDescendant(groupsPath, groupNode.getPath())) {
throw new RepositoryException("Group has to be within the Group Path");
}
return doCreateGroup(groupNode);
}
/**
* Build the group object from the given group node. May be overridden to
* return a custom implementation.
*
* @param node group node
* @return A group
* @throws RepositoryException if an error occurs
*/
protected Group doCreateGroup(NodeImpl node) throws RepositoryException {
return new GroupImpl(node, this);
}
/**
* Create the administrator user. If the node to be created collides
* with an existing node (ItemExistsException) the existing node gets removed
* and the admin user node is (re)created.
* <p>
* Collision with an existing node may occur under the following circumstances:
*
* <ul>
* <li>The <code>usersPath</code> has been modified in the user manager
* configuration after a successful repository start that already created
* the administrator user.</li>
* <li>The NodeId created by {@link #buildNodeId(String)} by coincidence
* collides with another NodeId created during the regular node creation
* process.</li>
* </ul>
*
* @return The admin user.
* @throws RepositoryException If an error occurs.
*/
private User createAdmin() throws RepositoryException {
User admin;
try {
admin = createUser(adminId, adminId);
if (!isAutoSave()) {
session.save();
}
log.info("... created admin user with id \'" + adminId + "\' and default pw.");
} catch (ItemExistsException e) {
NodeImpl conflictingNode = session.getNodeById(buildNodeId(adminId));
String conflictPath = conflictingNode.getPath();
log.error("Detected conflicting node " + conflictPath + " of node type " + conflictingNode.getPrimaryNodeType().getName() + ".");
// TODO move conflicting node of type rep:User instead of removing and recreating.
conflictingNode.remove();
log.info("Removed conflicting node at " + conflictPath);
admin = createUser(adminId, adminId);
if (!isAutoSave()) {
session.save();
}
log.info("Resolved conflict and (re)created admin user with id \'" + adminId + "\' and default pw.");
}
return admin;
}
/**
* Creates a UUID from the given <code>id</code> String that is converted
* to lower case before.
*
* @param id The user/group id that needs to be converted to a valid NodeId.
* @return a new <code>NodeId</code>.
* @throws RepositoryException If an error occurs.
*/
private NodeId buildNodeId(String id) throws RepositoryException {
try {
UUID uuid = UUID.nameUUIDFromBytes(id.toLowerCase().getBytes("UTF-8"));
return new NodeId(uuid);
} catch (UnsupportedEncodingException e) {
throw new RepositoryException("Unexpected error while build ID hash", e);
}
}
/**
* Checks if the specified <code>id</code> is a non-empty string and not yet
* in use for another user or group.
*
* @param id The id of the user or group to be created.
* @throws IllegalArgumentException If the specified id is null or empty string.
* @throws AuthorizableExistsException If the id is already in use.
* @throws RepositoryException If another error occurs.
*/
private void checkValidID(String id) throws IllegalArgumentException, AuthorizableExistsException, RepositoryException {
if (id == null || id.length() == 0) {
throw new IllegalArgumentException("Cannot create authorizable: ID can neither be null nor empty String.");
}
if (internalGetAuthorizable(id) != null) {
throw new AuthorizableExistsException("User or Group for '" + id + "' already exists");
}
}
/**
* Throws <code>IllegalArgumentException</code> if the specified principal
* is <code>null</code> or if it's name is <code>null</code> or empty string.
* @param principal The principal to be validated.
* @param isGroup Flag indicating if the principal represents a group.
*/
private static void checkValidPrincipal(Principal principal, boolean isGroup) {
if (principal == null || principal.getName() == null || "".equals(principal.getName())) {
throw new IllegalArgumentException("Principal may not be null and must have a valid name.");
}
if (!isGroup && EveryonePrincipal.NAME.equals(principal.getName())) {
throw new IllegalArgumentException("'everyone' is a reserved group principal name.");
}
}
//--------------------------------------------------------------------------
/**
* Let the configured <code>AuthorizableAction</code>s perform additional
* tasks associated with the creation of the new user before the
* corresponding new node is persisted.
*
* @param user The new user.
* @param pw The password.
* @throws RepositoryException If an exception occurs.
*/
void onCreate(User user, String pw) throws RepositoryException {
for (AuthorizableAction action : config.getAuthorizableActions()) {
action.onCreate(user, pw, session);
}
}
/**
* Let the configured <code>AuthorizableAction</code>s perform additional
* tasks associated with the creation of the new group before the
* corresponding new node is persisted.
*
* @param group The new group.
* @throws RepositoryException If an exception occurs.
*/
void onCreate(Group group) throws RepositoryException {
for (AuthorizableAction action : config.getAuthorizableActions()) {
action.onCreate(group, session);
}
}
/**
* Let the configured <code>AuthorizableAction</code>s perform any clean
* up tasks related to the authorizable removal (before the corresponding
* node gets removed).
*
* @param authorizable The authorizable to be removed.
* @throws RepositoryException If an exception occurs.
*/
void onRemove(Authorizable authorizable) throws RepositoryException {
for (AuthorizableAction action : config.getAuthorizableActions()) {
action.onRemove(authorizable, session);
}
}
/**
* Let the configured <code>AuthorizableAction</code>s perform additional
* tasks associated with password changing of a given user before the
* corresponding property is being changed.
*
* @param user The target user.
* @param password The new password.
* @throws RepositoryException If an exception occurs.
*/
void onPasswordChange(User user, String password) throws RepositoryException {
for (AuthorizableAction action : config.getAuthorizableActions()) {
action.onPasswordChange(user, password, session);
}
}
//----------------------------------------------------< SessionListener >---
/**
* @see SessionListener#loggingOut(org.apache.jackrabbit.core.SessionImpl)
*/
public void loggingOut(SessionImpl session) {
// nothing to do.
}
/**
* @see SessionListener#loggedOut(org.apache.jackrabbit.core.SessionImpl)
*/
public void loggedOut(SessionImpl session) {
// and logout the session unless it is the logged-out session itself.
if (session != this.session) {
this.session.logout();
}
}
//------------------------------------------------------< inner classes >---
/**
* Inner class
*/
private final class AuthorizableIterator implements Iterator<Authorizable> {
private final Set<String> served = new HashSet<String>();
private Authorizable next;
private final NodeIterator authNodeIter;
private AuthorizableIterator(NodeIterator authNodeIter) {
this.authNodeIter = authNodeIter;
next = seekNext();
}
//-------------------------------------------------------< Iterator >---
/**
* @see Iterator#hasNext()
*/
public boolean hasNext() {
return next != null;
}
/**
* @see Iterator#next()
*/
public Authorizable next() {
Authorizable authr = next;
if (authr == null) {
throw new NoSuchElementException();
}
next = seekNext();
return authr;
}
/**
* @see Iterator#remove()
*/
public void remove() {
throw new UnsupportedOperationException();
}
//----------------------------------------------------------------------
private Authorizable seekNext() {
while (authNodeIter.hasNext()) {
NodeImpl node = (NodeImpl) authNodeIter.nextNode();
try {
if (!served.contains(node.getUUID())) {
Authorizable authr = getAuthorizable(node);
served.add(node.getUUID());
if (authr != null) {
return authr;
}
}
} catch (RepositoryException e) {
log.debug(e.getMessage());
// continue seeking next authorizable
}
}
// no next authorizable -> iteration is completed.
return null;
}
}
//--------------------------------------------------------------------------
/**
* Inner class creating 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 or
* the corresponding path configured.</li>
* <li>Groups are created below /rep:security/rep:authorizables/rep:groups or
* the corresponding path configured.</li>
* <li>Below each category authorizables are created within a human readable
* structure based on the defined intermediate path or some internal logic
* with a depth defined by the <code>defaultDepth</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>If no intermediate path is passed the names of the intermediate
* folders are calculated from the leading chars of the escaped node name.</li>
* <li>If the escaped node name is shorter than the <code>defaultDepth</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>
* <li>Special case: If <code>autoExpandTree</code> is enabled later on
* AND any of the existing authorizable nodes collides with an intermediate
* folder to be created the auto-expansion is aborted and the new
* authorizable is inserted at the last valid level irrespective of
* max-size being reached.
* </li>
* </ul>
*
* The configuration options:
* <ul>
* <li><strong>defaultDepth</strong>:<br>
* A positive <code>integer</code> greater than zero defining the depth of
* the default structure that is always created.<br>
* Default value: 2</li>
* <li><strong>autoExpandTree</strong>:<br>
* <code>boolean</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>
* A positive <code>long</code> greater than zero defining the maximum
* number of child nodes that are allowed at a given level.<br>
* Default value: 1000<br>
* NOTE: that total number of child nodes may still be greater that
* autoExpandSize.</li>
* </ul>
*/
private class NodeCreator {
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 NodeCreator(UserManagerConfig config) {
int d = DEFAULT_DEPTH;
boolean expand = false;
long size = DEFAULT_SIZE;
if (config != null) {
d = config.getConfigValue(PARAM_DEFAULT_DEPTH, DEFAULT_DEPTH);
if (d <= 0) {
log.warn("Invalid defaultDepth '" + d + "' -> using default.");
d = DEFAULT_DEPTH;
}
expand = config.getConfigValue(PARAM_AUTO_EXPAND_TREE, false);
size = config.getConfigValue(PARAM_AUTO_EXPAND_SIZE, DEFAULT_SIZE);
if (expand && size <= 0) {
log.warn("Invalid autoExpandSize '" + size + "' -> using default.");
size = DEFAULT_SIZE;
}
}
defaultDepth = d;
autoExpandTree = expand;
autoExpandSize = size;
}
public Node createUserNode(String userID, String intermediatePath) throws RepositoryException {
return createAuthorizableNode(userID, false, intermediatePath);
}
public Node createGroupNode(String groupID, String intermediatePath) throws RepositoryException {
return createAuthorizableNode(groupID, true, intermediatePath);
}
private Node createAuthorizableNode(String id, boolean isGroup, String intermediatePath) throws RepositoryException {
String escapedId = Text.escapeIllegalJcrChars(id);
Node folder;
// first create the default folder nodes, that are always present.
folder = createDefaultFolderNodes(id, escapedId, isGroup, intermediatePath);
// eventually create additional intermediate folders.
if (intermediatePath == null) {
// internal logic only
folder = createIntermediateFolderNodes(id, escapedId, folder);
}
Name nodeName = session.getQName(escapedId);
Name ntName = (isGroup) ? NT_REP_GROUP : NT_REP_USER;
NodeId nid = buildNodeId(id);
// check if there exists an colliding folder child node.
while (((NodeImpl) folder).hasNode(nodeName)) {
NodeImpl colliding = ((NodeImpl) folder).getNode(nodeName);
if (colliding.isNodeType(NT_REP_AUTHORIZABLE_FOLDER)) {
log.warn("Existing folder node collides with user/group to be created. Expanding path: " + colliding.getPath());
folder = colliding;
} else {
// should never get here as folder creation above already
// asserts that only rep:authorizable folders exist.
// similarly collisions with existing authorizable have been
// checked.
String msg = "Failed to create authorizable with id '" + id + "' : Detected conflicting node of unexpected nodetype '" + colliding.getPrimaryNodeType().getName() + "'.";
log.error(msg);
throw new ConstraintViolationException(msg);
}
}
// check for collision with existing node outside of the user/group tree
if (session.getItemManager().itemExists(nid)) {
String msg = "Failed to create authorizable with id '" + id + "' : Detected conflict with existing node (NodeID: " + nid + ")";
log.error(msg);
throw new ItemExistsException(msg);
}
// finally create the authorizable node
return addNode((NodeImpl) folder, nodeName, ntName, nid);
}
private Node createDefaultFolderNodes(String id, String escapedId,
boolean isGroup, String intermediatePath) throws RepositoryException {
String defaultPath = getDefaultFolderPath(id, isGroup, intermediatePath);
// make sure users/groups are never nested and exclusively created
// under a tree of rep:AuthorizableFolder(s) starting at usersPath
// or groupsPath, respectively. ancestors of the usersPath/groupsPath
// may or may not be rep:AuthorizableFolder(s).
// therefore the shortcut Session.getNode(defaultPath) is omitted.
String[] segmts = defaultPath.split("/");
NodeImpl folder = (NodeImpl) session.getRootNode();
String authRoot = (isGroup) ? groupsPath : usersPath;
for (String segment : segmts) {
if (segment.length() < 1) {
continue;
}
if (folder.hasNode(segment)) {
folder = (NodeImpl) folder.getNode(segment);
if (Text.isDescendantOrEqual(authRoot, folder.getPath()) &&
!folder.isNodeType(NT_REP_AUTHORIZABLE_FOLDER)) {
throw new ConstraintViolationException("Invalid intermediate path. Must be of type rep:AuthorizableFolder.");
}
} else {
folder = addNode(folder, session.getQName(segment), NT_REP_AUTHORIZABLE_FOLDER);
}
}
// validation check if authorizable to be created doesn't conflict.
checkAuthorizableNodeExists(escapedId, folder);
return folder;
}
private String getDefaultFolderPath(String id, boolean isGroup, String intermediatePath) {
StringBuilder bld = new StringBuilder();
if (isGroup) {
bld.append(groupsPath);
} else {
bld.append(usersPath);
}
if (intermediatePath == null) {
// internal logic
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()));
}
} else {
// structure defined by intermediate path
if (intermediatePath.startsWith(bld.toString())) {
intermediatePath = intermediatePath.substring(bld.toString().length());
}
if (intermediatePath.length() > 0 && !"/".equals(intermediatePath)) {
if (!intermediatePath.startsWith("/")) {
bld.append("/");
}
bld.append(intermediatePath);
}
}
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 authorizable node to be created potentially collides with
// any of the intermediate nodes.
int segmLength = defaultDepth +1;
while (intermediateFolderNeeded(escapedId, folder)) {
String folderName = Text.escapeIllegalJcrChars(id.substring(0, segmLength));
if (folder.hasNode(folderName)) {
NodeImpl n = (NodeImpl) folder.getNode(folderName);
// validation check: folder must be of type rep:AuthorizableFolder
// and not an authorizable node.
if (n.isNodeType(NT_REP_AUTHORIZABLE_FOLDER)) {
// expected nodetype -> no violation
folder = n;
} else if (n.isNodeType(NT_REP_AUTHORIZABLE)){
/*
an authorizable node has been created before with the
name of the intermediate folder to be created.
this may only occur if the 'autoExpandTree' option has
been enabled later on.
Resolution:
- abort auto-expanding and create the authorizable
at the current level, ignoring that max-size is reached.
- note, that this behavior has been preferred over tmp.
removing and recreating the colliding authorizable node.
*/
log.warn("Auto-expanding aborted. An existing authorizable node '" + n.getName() +"'conflicts with intermediate folder to be created.");
break;
} else {
// should never get here: some other, unexpected node type
String msg = "Failed to create authorizable node: Detected conflict with node of unexpected nodetype '" + n.getPrimaryNodeType().getName() + "'.";
log.error(msg);
throw new ConstraintViolationException(msg);
}
} else {
// folder doesn't exist nor does another colliding child node.
folder = addNode((NodeImpl) folder, session.getQName(folderName), NT_REP_AUTHORIZABLE_FOLDER);
}
segmLength++;
}
// final validation check if authorizable to be created doesn't conflict.
checkAuthorizableNodeExists(escapedId, folder);
return folder;
}
private void checkAuthorizableNodeExists(String nodeName, Node folder) throws AuthorizableExistsException, RepositoryException {
if (folder.hasNode(nodeName) &&
((NodeImpl) folder.getNode(nodeName)).isNodeType(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 collision that would result from
// existing folder structure: a/ab/abt
// authID to be create : abt
return true;
}
// last possibility: max-size is reached.
if (folder.getNodes().getSize() >= autoExpandSize) {
return true;
}
// no collision and no need to create an additional intermediate
// folder due to max-size reached
return false;
}
}
}