All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.jackrabbit.oak.security.user.UserProvider Maven / Gradle / Ivy

There is a newer version: 1.62.0
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.jackrabbit.oak.security.user;

import java.security.Principal;
import java.text.ParseException;
import java.util.Collections;
import java.util.Iterator;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.jcr.AccessDeniedException;
import javax.jcr.RepositoryException;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.query.Query;

import com.google.common.base.Strings;
import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.oak.api.QueryEngine;
import org.apache.jackrabbit.oak.api.Result;
import org.apache.jackrabbit.oak.api.ResultRow;
import org.apache.jackrabbit.oak.api.Root;
import org.apache.jackrabbit.oak.api.Tree;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.spi.query.PropertyValues;
import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
import org.apache.jackrabbit.oak.spi.security.user.AuthorizableNodeName;
import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType;
import org.apache.jackrabbit.oak.spi.security.user.UserConstants;
import org.apache.jackrabbit.oak.util.NodeUtil;
import org.apache.jackrabbit.oak.util.TreeUtil;
import org.apache.jackrabbit.util.Text;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.google.common.base.Preconditions.checkNotNull;
import static org.apache.jackrabbit.oak.api.QueryEngine.NO_MAPPINGS;
import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.NODE_TYPES_PATH;

/**
 * User provider implementation and manager for group memberships with the
 * following characteristics:
 * 

*

UserProvider

*

*

User and Group Creation

* This implementation creates the JCR nodes corresponding the a given * authorizable ID with the following behavior: *
    *
  • Users are created below /rep:security/rep:authorizables/rep:users or * the path configured in the {@link org.apache.jackrabbit.oak.spi.security.user.UserConstants#PARAM_USER_PATH} * respectively.
  • *
  • Groups are created below /rep:security/rep:authorizables/rep:groups or * the path configured in the {@link org.apache.jackrabbit.oak.spi.security.user.UserConstants#PARAM_GROUP_PATH} * respectively.
  • *
  • Below each category authorizables are created within a human readable * structure based on the defined intermediate path or some internal logic * with a depth defined by the {@code defaultDepth} config option.
    * E.g. creating a user node for an ID 'aSmith' would result in the following * structure assuming defaultDepth == 2 is used: *
     * + rep:security            [rep:AuthorizableFolder]
     *   + rep:authorizables     [rep:AuthorizableFolder]
     *     + rep:users           [rep:AuthorizableFolder]
     *       + a                 [rep:AuthorizableFolder]
     *         + aS              [rep:AuthorizableFolder]
     * ->        + aSmith        [rep:User]
     * 
    *
  • *
  • The node name is calculated from the specified authorizable ID according * to the logic provided by the configured {@link AuthorizableNodeName} * implementation. If no name generator is present in the configuration * the {@link AuthorizableNodeName#DEFAULT default} implementation is used. The * name of the configuration option is {@link UserConstants#PARAM_AUTHORIZABLE_NODE_NAME}
  • *
  • If no intermediate path is passed the names of the intermediate * folders are calculated from the leading chars of the escaped node name.
  • *
  • If the escaped node name is shorter than the {@code defaultDepth} * the last char is repeated.
    * E.g. creating a user node for an ID 'a' would result in the following * structure assuming defaultDepth == 2 is used: *
     * + rep:security            [rep:AuthorizableFolder]
     *   + rep:authorizables     [rep:AuthorizableFolder]
     *     + rep:users           [rep:AuthorizableFolder]
     *       + a                 [rep:AuthorizableFolder]
     *         + aa              [rep:AuthorizableFolder]
     * ->        + a             [rep:User]
     * 
  • * *

    Conflicts

    * *
      *
    • If the authorizable node to be created would collide with an existing * folder the conflict is resolved by using the colling folder as target.
    • *
    • The current implementation asserts that authorizable nodes are always * created underneath an node of type {@code rep:AuthorizableFolder}. If this * condition is violated a {@code ConstraintViolationException} is thrown.
    • *
    • If the specified intermediate path results in an authorizable node * being located outside of the configured content structure a * {@code ConstraintViolationException} is thrown.
    • *
    * *

    Configuration Options

    *
      *
    • {@link UserConstants#PARAM_USER_PATH}: Underneath this structure * all user nodes are created. Default value is * "/rep:security/rep:authorizables/rep:users"
    • *
    • {@link UserConstants#PARAM_GROUP_PATH}: Underneath this structure * all group nodes are created. Default value is * "/rep:security/rep:authorizables/rep:groups"
    • *
    • {@link UserConstants#PARAM_DEFAULT_DEPTH}: A positive {@code integer} * greater than zero defining the depth of the default structure that is * always created. Default value: 2
    • *
    • {@link UserConstants#PARAM_AUTHORIZABLE_NODE_NAME}: An implementation * of {@link AuthorizableNodeName} used to create a node name for a given * authorizableId. By {@link AuthorizableNodeName.Default default} the * ID itself is used as node name. (since OAK 1.0)
    • *
    * *

    Compatibility with Jackrabbit 2.x

    * * Due to the fact that this JCR implementation is expected to deal with huge amount * of child nodes the following configuration options are no longer supported: *
      *
    • autoExpandTree
    • *
    • autoExpandSize
    • *
    * *

    User and Group Access

    *

    By ID

    * Accessing authorizables by ID is achieved by calculating the ContentId * associated with that user/group and using {@link org.apache.jackrabbit.oak.api.QueryEngine} * to find the corresponding {@code Tree}. The result is validated to really * represent a user/group tree. * *

    By Path

    * Access by path consists of a simple lookup by path such as exposed by * {@link Root#getTree(String)}. The resulting tree is validated to really * represent a user/group tree. * *

    By Principal

    * If the principal instance passed to {@link #getAuthorizableByPrincipal(java.security.Principal)} * is a {@code TreeBasedPrincipal} the lookup is equivalent to * {@link #getAuthorizableByPath(String)}. Otherwise the user/group is search * for using {@link org.apache.jackrabbit.oak.api.QueryEngine} looking * for a property {@link UserConstants#REP_PRINCIPAL_NAME} that matches the * name of the specified principal. */ class UserProvider extends AuthorizableBaseProvider { private static final Logger log = LoggerFactory.getLogger(UserProvider.class); private static final String DELIMITER = "/"; private final int defaultDepth; private final String groupPath; private final String userPath; UserProvider(@Nonnull Root root, @Nonnull ConfigurationParameters config) { super(root, config); defaultDepth = config.getConfigValue(PARAM_DEFAULT_DEPTH, DEFAULT_DEPTH); groupPath = config.getConfigValue(PARAM_GROUP_PATH, DEFAULT_GROUP_PATH); userPath = config.getConfigValue(PARAM_USER_PATH, DEFAULT_USER_PATH); } @Nonnull Tree createUser(@Nonnull String userID, @Nullable String intermediateJcrPath) throws RepositoryException { return createAuthorizableNode(userID, NT_REP_USER, intermediateJcrPath); } @Nonnull Tree createGroup(@Nonnull String groupID, @Nullable String intermediateJcrPath) throws RepositoryException { return createAuthorizableNode(groupID, NT_REP_GROUP, intermediateJcrPath); } @Nonnull Tree createSystemUser(@Nonnull String userID, @Nullable String intermediateJcrPath) throws RepositoryException { String relSysPath = config.getConfigValue(PARAM_SYSTEM_RELATIVE_PATH, DEFAULT_SYSTEM_RELATIVE_PATH); String relPath; if (intermediateJcrPath == null) { relPath = relSysPath; } else { if (!(intermediateJcrPath.startsWith(relSysPath) || intermediateJcrPath.startsWith(userPath + '/' + relSysPath))) { throw new ConstraintViolationException("System users must be located in the 'system' subtree of the user root."); } relPath = intermediateJcrPath; } return createAuthorizableNode(userID, NT_REP_SYSTEM_USER, relPath); } @CheckForNull Tree getAuthorizable(@Nonnull String authorizableId) { return getByID(authorizableId, AuthorizableType.AUTHORIZABLE); } @CheckForNull Tree getAuthorizableByPath(@Nonnull String authorizableOakPath) { return getByPath(authorizableOakPath); } @CheckForNull Tree getAuthorizableByPrincipal(@Nonnull Principal principal) { if (principal instanceof TreeBasedPrincipal) { return root.getTree(((TreeBasedPrincipal) principal).getOakPath()); } // NOTE: in contrast to JR2 the extra shortcut for ID==principalName // can be omitted as principals names are stored in user defined // index as well. try { StringBuilder stmt = new StringBuilder(); stmt.append("SELECT * FROM [").append(UserConstants.NT_REP_AUTHORIZABLE).append(']'); stmt.append(" WHERE [").append(UserConstants.REP_PRINCIPAL_NAME).append("] = $principalName"); stmt.append(QueryEngine.INTERNAL_SQL2_QUERY); Result result = root.getQueryEngine().executeQuery(stmt.toString(), Query.JCR_SQL2, 1, 0, Collections.singletonMap("principalName", PropertyValues.newString(principal.getName())), NO_MAPPINGS); Iterator rows = result.getRows().iterator(); if (rows.hasNext()) { String path = rows.next().getPath(); return root.getTree(path); } } catch (ParseException ex) { log.error("Failed to retrieve authorizable by principal", ex); } return null; } //------------------------------------------------------------< private >--- private Tree createAuthorizableNode(@Nonnull String authorizableId, @Nonnull String ntName, @Nullable String intermediatePath) throws RepositoryException { String nodeName = getNodeName(authorizableId); Tree folder = createFolderNodes(nodeName, NT_REP_GROUP.equals(ntName), intermediatePath); if (folder.hasChild(nodeName)) { // collision with another authorizable node or some other node type. int i = 1; String tmp = nodeName + i; while (folder.hasChild(tmp)) { tmp = nodeName + ++i; } nodeName = tmp; } Tree typeRoot = root.getTree(NODE_TYPES_PATH); String userId = Strings.nullToEmpty(root.getContentSession().getAuthInfo().getUserID()); Tree authorizableNode = TreeUtil.addChild(folder, nodeName, ntName, typeRoot, userId); authorizableNode.setProperty(REP_AUTHORIZABLE_ID, authorizableId); authorizableNode.setProperty(JcrConstants.JCR_UUID, getContentID(authorizableId, config.getConfigValue(PARAM_ENABLE_RFC7613_USERCASE_MAPPED_PROFILE, DEFAULT_ENABLE_RFC7613_USERCASE_MAPPED_PROFILE))); return authorizableNode; } /** * Create folder structure for the authorizable to be created. The structure * consists of a tree of rep:AuthorizableFolder node(s) starting at the * configured user or group path. Note that Authorizable nodes are never * nested. * * @param nodeName The name of the authorizable node. * @param isGroup Flag indicating whether the new authorizable is a group or a user. * @param intermediatePath An optional intermediate path. * @return The folder node. * @throws RepositoryException If an error occurs */ private Tree createFolderNodes(@Nonnull String nodeName, boolean isGroup, @Nullable String intermediatePath) throws RepositoryException { String authRoot = (isGroup) ? groupPath : userPath; String folderPath = new StringBuilder() .append(authRoot) .append(getFolderPath(nodeName, intermediatePath, authRoot)).toString(); NodeUtil folder; Tree tree = root.getTree(folderPath); while (!tree.isRoot() && !tree.exists()) { tree = tree.getParent(); } if (tree.exists()) { folder = new NodeUtil(tree); String relativePath = PathUtils.relativize(tree.getPath(), folderPath); if (!relativePath.isEmpty()) { folder = folder.getOrAddTree(relativePath, NT_REP_AUTHORIZABLE_FOLDER); } } else { throw new AccessDeniedException("Missing permission to create intermediate authorizable folders."); } // test for colliding folder child node. while (folder.hasChild(nodeName)) { NodeUtil colliding = folder.getChild(nodeName); String primaryType = TreeUtil.getPrimaryTypeName(colliding.getTree()); if (NT_REP_AUTHORIZABLE_FOLDER.equals(primaryType)) { log.debug("Existing folder node collides with user/group to be created. Expanding path by: " + colliding.getName()); folder = colliding; } else { break; } } return folder.getTree(); } @Nonnull private String getFolderPath(@Nonnull String nodeName, @Nullable String intermediatePath, @Nonnull String authRoot) throws ConstraintViolationException { boolean emptyOrNull = (intermediatePath == null || intermediatePath.isEmpty() || authRoot.equals(intermediatePath)); StringBuilder sb = new StringBuilder(); if (!emptyOrNull) { // convert absolute paths into relative paths wrt the authRoot if (intermediatePath.charAt(0) == '/') { if (!intermediatePath.startsWith(authRoot)) { throw new ConstraintViolationException("Attempt to create authorizable at '" + intermediatePath +"' outside of the configured root '" + authRoot + '\''); } else { intermediatePath = intermediatePath.substring(authRoot.length() + 1); } } sb.append(DELIMITER).append(intermediatePath); } else { String hint = Text.unescapeIllegalJcrChars(nodeName); int idLength = hint.length(); StringBuilder segment = new StringBuilder(); for (int i = 0; i < defaultDepth; i++) { if (idLength > i) { segment.append(hint.charAt(i)); } else { // escapedID is too short -> append the last char again segment.append(hint.charAt(idLength - 1)); } sb.append(DELIMITER).append(Text.escapeIllegalJcrChars(segment.toString())); } } return sb.toString(); } private String getNodeName(@Nonnull String authorizableId) { AuthorizableNodeName generator = checkNotNull(config.getConfigValue(PARAM_AUTHORIZABLE_NODE_NAME, AuthorizableNodeName.DEFAULT, AuthorizableNodeName.class)); return generator.generateNodeName(authorizableId); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy