blob: 4150302cfc8ee0930ca1c913d6ecebc9a66b4bf7 [file] [log] [blame]
/*
* 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.sling.jcr.repoinit.impl;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.jcr.PathNotFoundException;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.jcr.ValueFactory;
import javax.jcr.security.AccessControlEntry;
import javax.jcr.security.AccessControlException;
import javax.jcr.security.AccessControlManager;
import javax.jcr.security.AccessControlPolicy;
import javax.jcr.security.Privilege;
import org.apache.jackrabbit.api.security.JackrabbitAccessControlEntry;
import org.apache.jackrabbit.api.security.JackrabbitAccessControlList;
import org.apache.jackrabbit.api.security.JackrabbitAccessControlManager;
import org.apache.jackrabbit.api.security.authorization.PrincipalAccessControlList;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.commons.jackrabbit.authorization.AccessControlUtils;
import org.apache.jackrabbit.util.Text;
import org.apache.sling.repoinit.parser.operations.AclLine;
import org.apache.sling.repoinit.parser.operations.RestrictionClause;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.sling.repoinit.parser.operations.AclLine.ID_DELIMINATOR;
import static org.apache.sling.repoinit.parser.operations.AclLine.PATH_HOME;
import static org.apache.sling.repoinit.parser.operations.AclLine.PATH_REPOSITORY;
import static org.apache.sling.repoinit.parser.operations.AclLine.PROP_PATHS;
import static org.apache.sling.repoinit.parser.operations.AclLine.PROP_PRIVILEGES;
import static org.apache.sling.repoinit.parser.operations.AclLine.SUBTREE_DELIMINATOR;
/** Utilities for ACL management */
public class AclUtil {
private static final Logger LOG = LoggerFactory.getLogger(AclUtil.class);
public static JackrabbitAccessControlManager getJACM(Session s) throws RepositoryException {
final AccessControlManager acm = s.getAccessControlManager();
checkState((acm instanceof JackrabbitAccessControlManager), "AccessControlManager is not a JackrabbitAccessControlManager:" + acm.getClass().getName());
return (JackrabbitAccessControlManager) acm;
}
/**
* Converts RestrictionClauses to structure consumable by
* jackrabbit
* @param list
* @param jacl
* @param s
* @return
* @throws RepositoryException
*/
private static LocalRestrictions createLocalRestrictions(List<RestrictionClause> list, JackrabbitAccessControlList jacl, Session s) throws RepositoryException {
Map<String,Value> restrictions = new HashMap<>();
Map<String,Value[]> mvrestrictions = new HashMap<>();
if(list != null && !list.isEmpty()){
ValueFactory vf = s.getValueFactory();
for(RestrictionClause rc : list){
String restrictionName = rc.getName();
int type = jacl.getRestrictionType(restrictionName);
boolean isMvRestriction = jacl.isMultiValueRestriction(restrictionName);
Value[] values = new Value[rc.getValues().size()];
for(int i=0;i<values.length;i++) {
values[i] = vf.createValue(rc.getValues().get(i),type);
}
if("rep:glob".equals(restrictionName) && values.length == 0) {
// SLING-7280 - special case for rep:glob which supports an empty string
// to mean "no values"
restrictions.put(restrictionName, vf.createValue(""));
} else if (isMvRestriction) {
mvrestrictions.put(restrictionName, values);
} else {
checkState(values.length == 1, "Expected just one value for single valued restriction with name " + restrictionName);
restrictions.put(restrictionName, values[0]);
}
}
}
return new LocalRestrictions(restrictions,mvrestrictions);
}
public static void setAcl(Session session, List<String> principals, List<String> paths, List<String> privileges, boolean isAllow)
throws RepositoryException {
setAcl(session,principals,paths,privileges,isAllow,Arrays.asList(new RestrictionClause[]{}));
}
public static void setAcl(Session session, List<String> principals, List<String> paths, List<String> privileges, boolean isAllow, List<RestrictionClause> restrictionClauses)
throws RepositoryException {
for (String jcrPath : getJcrPaths(session, paths)) {
if (jcrPath != null && !session.nodeExists(jcrPath)) {
throw new PathNotFoundException("Cannot set ACL on non-existent path " + jcrPath);
}
setAcl(session, principals, jcrPath, privileges, isAllow, restrictionClauses);
}
}
private static void setAcl(Session session, List<String> principals, String jcrPath, List<String> privileges, boolean isAllow, List<RestrictionClause> restrictionClauses)
throws RepositoryException {
final String [] privArray = privileges.toArray(new String[privileges.size()]);
final Privilege[] jcrPriv = AccessControlUtils.privilegesFromNames(session, privArray);
JackrabbitAccessControlList acl = AccessControlUtils.getAccessControlList(session, jcrPath);
checkState(acl != null, "No JackrabbitAccessControlList available for path " + jcrPath);
LocalRestrictions localRestrictions = createLocalRestrictions(restrictionClauses, acl, session);
AccessControlEntry[] existingAces = acl.getAccessControlEntries();
boolean changed = false;
for (String name : principals) {
Principal principal = AccessControlUtils.getPrincipal(session, name);
if (principal == null) {
// backwards compatibility: fallback to original code treating principal name as authorizable ID (see SLING-8604)
final Authorizable authorizable = UserUtil.getAuthorizable(session, name);
checkState(authorizable != null, "Authorizable not found:" + name);
principal = authorizable.getPrincipal();
}
checkState(principal != null, "Principal not found: " + name);
LocalAccessControlEntry newAce = new LocalAccessControlEntry(principal, jcrPriv, isAllow, localRestrictions);
if (contains(existingAces, newAce)) {
LOG.info("Not adding {} to path {} since an equivalent access control entry already exists", newAce, jcrPath);
continue;
}
acl.addEntry(newAce.principal, newAce.privileges, newAce.isAllow,
newAce.restrictions.getRestrictions(), newAce.restrictions.getMVRestrictions());
changed = true;
}
if ( changed ) {
session.getAccessControlManager().setPolicy(jcrPath, acl);
}
}
public static void setRepositoryAcl(Session session, List<String> principals, List<String> privileges, boolean isAllow, List<RestrictionClause> restrictionClauses)
throws RepositoryException {
setAcl(session, principals, (String)null, privileges, isAllow, restrictionClauses);
}
public static void setPrincipalAcl(Session session, String principalName, Collection<AclLine> lines) throws RepositoryException {
JackrabbitAccessControlManager acMgr = getJACM(session);
Principal principal = AccessControlUtils.getPrincipal(session, principalName);
checkState(principal != null, "Principal not found: " + principalName);
PrincipalAccessControlList acl = getPrincipalAccessControlList(acMgr, principal);
boolean modified = false;
for (AclLine line : lines) {
if (line.getAction() == AclLine.Action.DENY) {
throw new AccessControlException("PrincipalAccessControlList doesn't support 'deny' entries.");
}
LocalRestrictions restrictions = createLocalRestrictions(line.getRestrictions(), acl, session);
Privilege[] privileges = AccessControlUtils.privilegesFromNames(session, line.getProperty(PROP_PRIVILEGES).toArray(new String[0]));
for (String effectivePath : getJcrPaths(session, line.getProperty(PROP_PATHS))) {
boolean added = acl.addEntry(effectivePath, privileges, restrictions.getRestrictions(), restrictions.getMVRestrictions());
if (!added) {
LOG.info("Equivalent principal-based entry already exists for principal {} and effective path {} ", principalName, effectivePath);
} else {
modified = true;
}
}
}
if (modified) {
acMgr.setPolicy(acl.getPath(), acl);
}
}
private static PrincipalAccessControlList getPrincipalAccessControlList(JackrabbitAccessControlManager acMgr, Principal principal) throws RepositoryException {
PrincipalAccessControlList acl = null;
for (AccessControlPolicy policy : acMgr.getPolicies(principal)) {
if (policy instanceof PrincipalAccessControlList) {
acl = (PrincipalAccessControlList) policy;
break;
}
}
if (acl == null) {
for (AccessControlPolicy policy : acMgr.getApplicablePolicies(principal)) {
if (policy instanceof PrincipalAccessControlList) {
acl = (PrincipalAccessControlList) policy;
break;
}
}
}
checkState(acl != null, "No PrincipalAccessControlList available for principal " + principal);
return acl;
}
@NotNull
private static List<String> getJcrPaths(@NotNull Session session, @NotNull List<String> paths) throws RepositoryException {
List<String> jcrPaths = new ArrayList<>(paths.size());
for (String path : paths) {
if (PATH_REPOSITORY.equals(path) || path == null || path.isEmpty()) {
jcrPaths.add(null);
} else if (path.startsWith(PATH_HOME)) {
int lastHashIndex = path.lastIndexOf(SUBTREE_DELIMINATOR);
checkState(lastHashIndex > -1, "Invalid format of home path: # deliminator expected.");
String subTreePath = path.substring(lastHashIndex+1);
for (String aPath : getAuthorizablePaths(session, path.substring(PATH_HOME.length(), lastHashIndex))) {
jcrPaths.add(aPath + subTreePath);
}
} else {
jcrPaths.add(path);
}
}
return jcrPaths;
}
@NotNull
private static Iterable<String> getAuthorizablePaths(@NotNull Session session, @NotNull String ids) throws RepositoryException {
List<String> paths = new ArrayList<>();
for (String id : Text.explode(ids, ID_DELIMINATOR)) {
Authorizable a = UserUtil.getAuthorizable(session, id);
if (a == null) {
throw new PathNotFoundException("Cannot resolve path of user/group with id '" + id + "'.");
}
paths.add(a.getPath());
}
return paths;
}
// visible for testing
static boolean contains(AccessControlEntry[] existingAces, LocalAccessControlEntry newAce) throws RepositoryException {
for (int i = 0 ; i < existingAces.length; i++) {
JackrabbitAccessControlEntry existingEntry = (JackrabbitAccessControlEntry) existingAces[i];
LOG.debug("Comparing {} with {}", newAce, toString(existingEntry));
if (newAce.isContainedIn(existingEntry)) {
return true;
}
}
return false;
}
private static String toString(JackrabbitAccessControlEntry entry) throws RepositoryException {
return "[" + entry.getClass().getSimpleName() + "# principal: "
+ "" + entry.getPrincipal() + ", privileges: " + Arrays.toString(entry.getPrivileges()) +
", isAllow: " + entry.isAllow() + ", restrictionNames: " + entry.getRestrictionNames() + "]";
}
private static void checkState(boolean expression, String msg) {
if (!expression) {
throw new IllegalStateException(msg);
}
}
/** Compare arrays a and b, which do not need to be ordered
* but are expected to be small.
* @param a might be sorted by this method
* @param b might be sorted by this method
* @return true if both arrays contain the same elements,
* in whatever order. Also true if both arrays are null
* or empty.
*/
static boolean compareArrays(Object[] a, Object[] b) {
if(a== null && b == null){
return true;
}
if(a== null || b == null){
return false;
}
if(a.length != b.length){
return false;
}
Arrays.sort(a);
Arrays.sort(b);
for(int i=0;i<a.length;i++){
if(!a[i].equals(b[i])){
return false;
}
}
return true;
}
/**
* Helper class which allows easy comparison of a local (proposed) access control entry with an existing one
*/
static class LocalAccessControlEntry {
private final Principal principal;
private final Privilege[] privileges;
private final boolean isAllow;
private final LocalRestrictions restrictions;
LocalAccessControlEntry(Principal principal, Privilege[] privileges, boolean isAllow) {
this(principal, privileges, isAllow, null);
}
LocalAccessControlEntry(Principal principal, Privilege[] privileges,
boolean isAllow, LocalRestrictions restrictions) {
this.principal = principal;
this.privileges = privileges;
this.isAllow = isAllow;
this.restrictions = restrictions != null ? restrictions : new LocalRestrictions();
}
public boolean isContainedIn(JackrabbitAccessControlEntry other) throws RepositoryException {
return other.getPrincipal().equals(principal) &&
contains(other.getPrivileges(), privileges) &&
other.isAllow() == isAllow &&
sameRestrictions(other);
}
private Set<Privilege> expandPrivileges(Privilege[] privileges){
Set<Privilege> expandedSet = new HashSet<>();
if(privileges != null){
for(Privilege privilege : privileges){
if(privilege.isAggregate()){
expandedSet.addAll(Arrays.asList(privilege.getAggregatePrivileges()));
} else {
expandedSet.add(privilege);
}
}
}
return expandedSet;
}
/**
* compares if restrictions present in jackrabbit access control entry
* is same as specified restrictions in repo init
* @param jace
* @return
* @throws RepositoryException
*/
private boolean sameRestrictions(JackrabbitAccessControlEntry jace) throws RepositoryException {
// total (multivalue and simple) number of restrictions should be same
if(jace.getRestrictionNames().length == (restrictions.size())){
for(String rn : jace.getRestrictionNames()){
Value[] oldValues = jace.getRestrictions(rn);
Value[] newValues = restrictions.getRestrictions().get(rn) != null
? new Value[]{restrictions.getRestrictions().get(rn)}
: restrictions.getMVRestrictions().get(rn);
if((newValues == null || newValues.length == 0) && (oldValues == null || oldValues.length == 0)){
continue;
}
if(!compareArrays(newValues, oldValues)) {
return false;
}
}
return true;
}
return false;
}
private boolean contains(Privilege[] first, Privilege[] second) {
// we need to ensure that the privilege order is not taken into account, so we use sets
Set<Privilege> set1 = expandPrivileges(first);
Set<Privilege> set2 = expandPrivileges(second);
return set1.containsAll(set2);
}
@Override
public String toString() {
return "[" + getClass().getSimpleName() + "# principal " + principal+ ", privileges: " + Arrays.toString(privileges) + ", isAllow : " + isAllow + "]";
}
}
/**
* Helper class to store both restrictions and multi value restrictions
* in ready to consume structure expected by jackrabbit
*/
private static class LocalRestrictions {
private Map<String,Value> restrictions;
private Map<String,Value[]> mvRestrictions;
public LocalRestrictions(){
restrictions = new HashMap<>();
mvRestrictions = new HashMap<>();
}
public LocalRestrictions(Map<String,Value> restrictions,Map<String,Value[]> mvRestrictions){
this.restrictions = restrictions != null ? restrictions : new HashMap<String,Value>();
this.mvRestrictions = mvRestrictions != null ? mvRestrictions : new HashMap<String,Value[]>();
}
public Map<String,Value> getRestrictions(){
return this.restrictions;
}
public Map<String,Value[]> getMVRestrictions(){
return this.mvRestrictions;
}
public int size(){
return this.restrictions.size() + this.mvRestrictions.size();
}
}
}