package com.thinkbiganalytics.metadata.modeshape.security;
/*-
* #%L
* thinkbig-metadata-modeshape
* %%
* Copyright (C) 2017 ThinkBig Analytics
* %%
* Licensed 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.
* #L%
*/
import com.thinkbiganalytics.metadata.modeshape.MetadataRepositoryException;
import com.thinkbiganalytics.metadata.modeshape.support.JcrUtil;
import com.thinkbiganalytics.security.UsernamePrincipal;
import org.modeshape.jcr.ModeShapeRoles;
import org.modeshape.jcr.security.SimplePrincipal;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import java.security.Principal;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.jcr.AccessDeniedException;
import javax.jcr.Node;
import javax.jcr.PathNotFoundException;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.security.AccessControlEntry;
import javax.jcr.security.AccessControlException;
import javax.jcr.security.AccessControlList;
import javax.jcr.security.AccessControlManager;
import javax.jcr.security.AccessControlPolicy;
import javax.jcr.security.AccessControlPolicyIterator;
import javax.jcr.security.Privilege;
/**
* Utilities to apply JCR access control changes to nodes and node hierarchies.
*/
public final class JcrAccessControlUtil {
private JcrAccessControlUtil() {
throw new AssertionError(JcrAccessControlUtil.class + " is a static utility class");
}
public Optional<UsernamePrincipal> getCurrentUser() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) {
return Optional.of(new UsernamePrincipal(auth.getName()));
} else {
return Optional.empty();
}
}
public static boolean addPermissions(Node node, Principal principal, String... privilegeNames) {
return addPermissions(node, principal, Arrays.asList(privilegeNames));
}
public static boolean setPermissions(Node node, Principal principal, Collection<String> privilegeNames) {
try {
return setPermissions(node.getSession(), node.getPath(), principal, privilegeNames);
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to set permission(s) on node " + node + ": " + privilegeNames, e);
}
}
public static boolean setPermissions(Session session, String path, Principal principal, String... privilegeNames) {
return setPermissions(session, path, principal, Arrays.asList(privilegeNames));
}
public static boolean setPermissions(Session session, String path, Principal principal, Collection<String> privilegeNames) {
try {
return setPermissions(session, path, principal, asPrivileges(session, privilegeNames));
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to set permission(s) on node " + path + ": " + privilegeNames, e);
}
}
public static boolean setPermissions(Session session, String path, Principal principal, Privilege... privileges) {
return updatePermissions(session, path, principal, true, privileges);
}
public static boolean addPermissions(Node node, Principal principal, Collection<String> privilegeNames) {
try {
return addPermissions(node.getSession(), node.getPath(), principal, privilegeNames);
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to add permission(s) to node " + node + ": " + privilegeNames, e);
}
}
public static boolean addPermissions(Node node, Principal principal, Privilege... privileges) {
try {
return addPermissions(node.getSession(), node.getPath(), principal, privileges);
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to add permission(s) to node " + node + ": "
+ Arrays.toString(privileges), e);
}
}
public static boolean addPermissions(Session session, String path, Principal principal, String... privilegeNames) {
return addPermissions(session, path, principal, Arrays.asList(privilegeNames));
}
public static boolean addPermissions(Session session, String path, Principal principal, Collection<String> privilegeNames) {
try {
// No privileges results in a no-op.
if (privilegeNames.size() > 0) {
return addPermissions(session, path, principal, asPrivileges(session, privilegeNames));
} else {
return false;
}
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to add permission(s) to node " + path + ": " + privilegeNames, e);
}
}
public static boolean addPermissions(Session session, String path, Principal principal, Privilege... privileges) {
// No privileges results in a no-op.
if (privileges.length > 0) {
return updatePermissions(session, path, principal, false, privileges);
} else {
return false;
}
}
public static boolean hasAnyPermission(Node node, Principal principal, String... privileges) {
try {
return hasAnyPermission(node.getSession(), node.getPath(), principal, privileges);
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to check permission(s) for node: " + node);
}
}
public static boolean hasAnyPermission(Node node, Principal principal, Privilege... privileges) {
try {
return hasAnyPermission(node.getSession(), node.getPath(), principal, privileges);
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to check permission(s) of node " + node + ": "
+ Arrays.toString(privileges), e);
}
}
public static boolean hasAnyPermission(Session session, String path, Principal principal, String... privilegeNames) {
try {
return hasAnyPermission(session, path, principal, asPrivileges(session, privilegeNames));
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to check permission(s) for node: " + path);
}
}
public static boolean hasAnyPermission(Session session, String path, Principal principal, Privilege... privileges) {
return hasAnyPermission(session, path, principal, Arrays.asList(privileges));
}
public static boolean hasAnyPermission(Session session, String path, Principal principal, Collection<Privilege> privileges) {
return getPrivileges(session, principal, path).stream().anyMatch(p -> privileges.contains(p));
}
public static Set<Privilege> getPrivileges(Session session, Principal principal, String path) {
try {
AccessControlManager acm = session.getAccessControlManager();
AccessControlList acl = getAccessControlList(path, acm);
for (AccessControlEntry entry : acl.getAccessControlEntries()) {
if (entry.getPrincipal().getName().equals(principal.getName())) {
return new HashSet<>(Arrays.asList(entry.getPrivileges()));
}
}
return Collections.emptySet();
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to get the privileges for node " + path, e);
}
}
/**
* Adds the specified privilege to the node hierarchy starting at a child node and proceeding through its parents until
* the destination node is reached.
*
* @param node the starting node on which the privilege is assigned
* @param principal the principal being given the privilege
* @param toNode the ending parent node
* @param privilegeNames the privilege being assigned
* @return true if any of the nodes had their privilege change for the principle (i.e. the privilege had not already existed)
*/
public static boolean addHierarchyPermissions(Node node, Principal principal, Node toNode, String... privilegeNames) {
return addHierarchyPermissions(node, principal, toNode, Arrays.asList(privilegeNames));
}
/**
* Adds the specified privilege to the node hierarchy starting at a child node and proceeding through its parents until
* the destination node is reached.
*
* @param node the starting node on which the privilege is assigned
* @param principal the principal being given the privilege
* @param toNode the ending parent node
* @param privilegeNames the privilege being assigned
* @return true if any of the nodes had their privilege change for the principle (i.e. the privilege had not already existed)
*/
public static boolean addHierarchyPermissions(Node node, Principal principal, Node toNode, Collection<String> privilegeNames) {
try {
Node current = node;
Node rootNode = toNode.getSession().getRootNode();
AtomicBoolean added = new AtomicBoolean(false);
Deque<Node> stack = new ArrayDeque<>();
while (!current.equals(toNode) && !current.equals(rootNode)) {
stack.push(current);
current = current.getParent();
}
if (current.equals(rootNode) && !toNode.equals(rootNode)) {
throw new IllegalArgumentException("addHierarchyPermissions: The \"toNode\" argument is not in the \"node\" argument's hierarchy: " + toNode);
} else {
stack.push(current);
}
stack.stream().forEach((n) -> added.compareAndSet(false, addPermissions(n, principal, privilegeNames)));
return added.get();
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to add permission(s) to hierarchy from node " + node + " up to " + toNode, e);
}
}
public static boolean removePermissions(Session session, String path, Principal principal, String... privilegeNames) {
try {
return removePermissions(session, path, principal, asPrivileges(session, privilegeNames));
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to remove permission(s) from node " + path + ": "
+ Arrays.toString(privilegeNames), e);
}
}
public static Privilege[] asPrivileges(Session session, String... privilegeNames) throws UnsupportedRepositoryOperationException, RepositoryException, AccessControlException {
return asPrivileges(session, Arrays.asList(privilegeNames));
}
public static Privilege[] asPrivileges(Session session, Collection<String> privilegeNames) throws UnsupportedRepositoryOperationException, RepositoryException, AccessControlException {
Privilege[] privs = new Privilege[privilegeNames.size()];
AccessControlManager acm = session.getAccessControlManager();
int i = 0;
for (String name : privilegeNames) {
privs[i++] = acm.privilegeFromName(name);
}
return privs;
}
public static boolean removePermissions(Node node, Principal principal, String... privilegeNames) {
try {
return removePermissions(node.getSession(), node.getPath(), principal, privilegeNames);
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to remove permission(s) from node " + node + ": " + Arrays.toString(privilegeNames), e);
}
}
public static boolean removePermissions(Node node, Principal principal, Privilege... privileges) {
try {
return removePermissions(node.getSession(), node.getPath(), principal, privileges);
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to remove permission(s) from node " + node + ": " + Arrays.toString(privileges), e);
}
}
public static boolean removePermissions(Session session, String path, Principal principal, Privilege... removes) {
try {
// if there are no permissions specified or the principal is "admin" then do nothing.
// There should alwasy be an ACL entry for "admin".
if (removes.length > 0 && ! principal.getName().equals(ModeShapeRoles.ADMIN)) {
AccessControlManager acm = session.getAccessControlManager();
AccessControlPolicy[] aclArray = acm.getPolicies(path);
if (aclArray.length > 0) {
AccessControlList acl = (AccessControlList) aclArray[0];
boolean removed = false;
for (AccessControlEntry entry : acl.getAccessControlEntries()) {
if (entry.getPrincipal().getName().equals(principal.getName())) {
Privilege[] newPrivs = Arrays.stream(entry.getPrivileges())
.filter(p -> !Arrays.stream(removes).anyMatch(r -> r.equals(p)))
.toArray(Privilege[]::new);
if (entry.getPrivileges().length != newPrivs.length) {
acl.removeAccessControlEntry(entry);
if (newPrivs.length != 0) {
acl.addAccessControlEntry(entry.getPrincipal(), newPrivs);
}
removed = true;
}
}
}
acm.setPolicy(path, acl);
return removed;
} else {
return false;
}
} else {
return false;
}
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to remove permission(s) from node " + path + ": "
+ Arrays.toString(removes), e);
}
}
public static boolean removeAllPermissions(Node node, Principal principal) {
try {
return removeAllPermissions(node.getSession(), node.getPath(), principal);
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to renove all permission(s) from node: " + node);
}
}
public static boolean removeAllPermissions(Session session, String path, Principal principal) {
try {
AccessControlManager acm = session.getAccessControlManager();
AccessControlPolicy[] aclArray = acm.getPolicies(path);
// Never remove permissions for "admin".
if (aclArray.length > 0 && ! principal.getName().equals(ModeShapeRoles.ADMIN)) {
AccessControlList acl = (AccessControlList) aclArray[0];
boolean removed = removeEntry(acl, principal);
acm.setPolicy(path, acl);
return removed;
} else {
return false;
}
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to remove all permission(s) from node " + path, e);
}
}
public static boolean removeHierarchyPermissions(Node node, Principal principal, Node toNode, String... privilegeNames) {
try {
Node current = node;
Node rootNode = toNode.getSession().getRootNode();
boolean removed = false;
while (!current.equals(toNode) && !current.equals(rootNode)) {
removed |= removePermissions(node.getSession(), current.getPath(), principal, privilegeNames);
current = current.getParent();
}
if (current.equals(rootNode) && !toNode.equals(rootNode)) {
throw new IllegalArgumentException("removeHierarchyPermissions: The \"toNode\" argument is not in the \"node\" argument's hierarchy: " + toNode);
} else {
removed |= removePermissions(node.getSession(), current.getPath(), principal, privilegeNames);
}
return removed;
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to remove permission(s) from hierarch from node " + node + " up to " + toNode, e);
}
}
public static boolean removeHierarchyAllPermissions(Node node, Principal principal, Node toNode) {
try {
Node current = node;
Node rootNode = toNode.getSession().getRootNode();
boolean removed = false;
while (!current.equals(toNode) && !current.equals(rootNode)) {
removed |= removeAllPermissions(node.getSession(), current.getPath(), principal);
current = current.getParent();
}
if (current.equals(rootNode) && !toNode.equals(rootNode)) {
throw new IllegalArgumentException("removeHierarchyAllPermissions: The \"toNode\" argument is not in the \"node\" argument's hierarchy: " + toNode);
} else {
removed |= removeAllPermissions(node.getSession(), current.getPath(), principal);
}
return removed;
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to remove all permission(s) from hierarch from node " + node + " up to " + toNode, e);
}
}
public static boolean clearPermissions(Node node) {
try {
return clearPermissions(node.getSession(), node.getPath());
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to remove all permission(s) from node " + node, e);
}
}
public static boolean clearPermissions(Session session, String path) {
try {
AccessControlManager acm = session.getAccessControlManager();
AccessControlPolicy[] acls = acm.getPolicies(path);
if (acls.length > 0) {
for (AccessControlPolicy policy : acm.getPolicies(path)) {
acm.removePolicy(path, policy);
}
return true;
} else {
return false;
}
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to remove all permission(s) from node " + path, e);
}
}
public static boolean clearRecursivePermissions(Node node, String nodeType) {
boolean cleared = false;
for (Node child : JcrUtil.getNodesOfType(node, nodeType)) {
cleared |= clearRecursivePermissions(child, nodeType);
cleared |= clearPermissions(child);
}
return cleared;
}
public static boolean clearHierarchyPermissions(Node node, Node toNode) {
try {
Node current = node;
Node rootNode = toNode.getSession().getRootNode();
boolean removed = false;
while (!current.equals(toNode) && !current.equals(rootNode)) {
removed |= clearPermissions(current);
current = current.getParent();
}
if (current.equals(rootNode) && !toNode.equals(rootNode)) {
throw new IllegalArgumentException("clearHierarchyPermissions: The \"toNode\" argument is not in the \"node\" argument's hierarchy: " + toNode);
} else {
removed |= clearPermissions(current);
}
return removed;
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to add permission(s) to hierarch from node " + node + " up to " + toNode, e);
}
}
public static boolean addRecursivePermissions(Node node, String nodeType, Principal principal, String... privilegeNames) {
try {
// No privileges results in a no-op.
if (privilegeNames.length > 0) {
return addRecursivePermissions(node, nodeType, principal, asPrivileges(node.getSession(), privilegeNames));
} else {
return false;
}
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to add permission(s) to node tree " + node, e);
}
}
public static boolean addRecursivePermissions(Node node, String nodeType, Principal principal, Privilege... privileges) {
// No privileges results in a no-op.
if (privileges.length > 0) {
boolean added = addPermissions(node, principal, privileges);
for (Node child : JcrUtil.getNodesOfType(node, nodeType)) {
added |= addRecursivePermissions(child, nodeType, principal, privileges);
}
return added;
} else {
return false;
}
}
public static boolean removeRecursivePermissions(Node node, String nodeType, Principal principal, String... privilegeNames) {
try {
// No privileges results in a no-op.
if (privilegeNames.length > 0) {
return removeRecursivePermissions(node, nodeType, principal, asPrivileges(node.getSession(), privilegeNames));
} else {
return false;
}
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to remove permission(s) from node tree " + node, e);
}
}
public static boolean removeRecursivePermissions(Node node, String nodeType, Principal principal, Privilege... privileges) {
try {
// No privileges results in a no-op.
if (privileges.length > 0) {
boolean removed = false;
for (Node child : JcrUtil.getNodesOfType(node, nodeType)) {
removed |= removeRecursivePermissions(child, nodeType, principal, privileges);
}
return removePermissions(node.getSession(), node.getPath(), principal, privileges) || removed;
} else {
return false;
}
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to remove permission(s) from node tree " + node, e);
}
}
public static boolean removeRecursiveAllPermissions(Node node, String nodeType, Principal principal) {
try {
boolean removed = false;
for (Node child : JcrUtil.getNodesOfType(node, nodeType)) {
removed |= removeRecursiveAllPermissions(child, nodeType, principal);
}
return removeAllPermissions(node.getSession(), node.getPath(), principal) || removed;
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to remove permission(s) from node tree " + node, e);
}
}
private static boolean updatePermissions(Session session, String path, Principal principal, boolean replace, Privilege... privileges) {
try {
AccessControlManager acm = session.getAccessControlManager();
AccessControlList acl = getAccessControlList(path, acm);
boolean changed = false;
if (replace) {
changed |= removeEntry(acl, principal);
}
if (privileges.length > 0) {
changed |= addEntry(session, acl, principal, privileges);
}
acm.setPolicy(path, acl);
return changed;
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed to add permission(s) to node " + path + ": "
+ Arrays.toString(privileges), e);
}
}
private static boolean addEntry(Session session, AccessControlList acl, Principal principal, Privilege... privileges) throws RepositoryException, AccessControlException,
UnsupportedRepositoryOperationException {
// Ensure admin is always included in the ACL
if (acl.getAccessControlEntries().length == 0) {
SimplePrincipal simple = SimplePrincipal.newInstance(ModeShapeRoles.ADMIN);
acl.addAccessControlEntry(simple, asPrivileges(session, Privilege.JCR_ALL));
}
// ModeShape reads back all principals as SimplePrincipals after they are stored, so we have to use
// the same principal type here or the entry will treated as a new one instead of adding privileges to the
// to an existing principal. This can be considered a bug in ModeShape.
SimplePrincipal simple = SimplePrincipal.newInstance(principal.getName());
boolean added = acl.addAccessControlEntry(simple, privileges);
return added;
}
private static boolean removeEntry(AccessControlList acl, Principal principal) throws RepositoryException {
boolean removed = false;
for (AccessControlEntry entry : acl.getAccessControlEntries()) {
if (entry.getPrincipal().getName().equals(principal.getName())) {
acl.removeAccessControlEntry(entry);
removed = true;
}
}
return removed;
}
private static AccessControlList getAccessControlList(String path, AccessControlManager acm) throws PathNotFoundException, AccessDeniedException, RepositoryException {
AccessControlList acl = null;
AccessControlPolicyIterator it = acm.getApplicablePolicies(path);
if (it.hasNext()) {
acl = (AccessControlList) it.nextAccessControlPolicy();
} else {
acl = (AccessControlList) acm.getPolicies(path)[0];
}
return acl;
}
/**
* Tests if the specified privilege implies, either directly or as an aggregate, the named privilege.
* @param checked the privilege being checked
* @param implied the name of the privilege to be implied
* @return true if the check privilege implies the given privilege name
*/
private static boolean implies(Privilege checked, String implied) {
if (checked.getName().equals(implied)) {
return true;
} else if (checked.isAggregate()) {
return impliesAny(checked.getAggregatePrivileges(), implied);
} else {
return false;
}
}
/**
* Tests if any of the specified privileges imply, either directly or as an aggregate, the named privilege.
* @param privileges an array of privileges
* @param implied the name of the privilege to be implied
* @return true if the any of the privileges implies the given privilege name
*/
private static boolean impliesAny(Privilege[] privileges, String implied) {
for (Privilege checked : privileges) {
if (implies(checked, implied)) {
return true;
}
}
return false;
}
}