blob: 119218c3bed99f714d93fe2bd47368a2372b9a47 [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.jackrabbit.oak.security.authorization.accesscontrol;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import javax.jcr.RepositoryException;
import javax.jcr.security.AccessControlException;
import javax.jcr.security.Privilege;
import com.google.common.base.Objects;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.api.security.authorization.PrivilegeManager;
import org.apache.jackrabbit.oak.api.CommitFailedException;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Tree;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.plugins.nodetype.TypePredicate;
import org.apache.jackrabbit.oak.plugins.tree.TreeConstants;
import org.apache.jackrabbit.oak.plugins.tree.TreeProvider;
import org.apache.jackrabbit.oak.plugins.tree.TreeUtil;
import org.apache.jackrabbit.oak.security.authorization.ProviderCtx;
import org.apache.jackrabbit.oak.spi.commit.DefaultValidator;
import org.apache.jackrabbit.oak.spi.commit.Validator;
import org.apache.jackrabbit.oak.spi.commit.VisibleValidator;
import org.apache.jackrabbit.oak.spi.security.authorization.accesscontrol.AccessControlConstants;
import org.apache.jackrabbit.oak.spi.security.authorization.restriction.Restriction;
import org.apache.jackrabbit.oak.spi.security.authorization.restriction.RestrictionProvider;
import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeBits;
import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeBitsProvider;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.apache.jackrabbit.util.Text;
import org.jetbrains.annotations.NotNull;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.apache.jackrabbit.oak.api.CommitFailedException.ACCESS_CONTROL;
import static org.apache.jackrabbit.oak.api.CommitFailedException.OAK;
/**
* Validation for access control information changed by regular JCR (and Jackrabbit)
* access control management API.
*/
class AccessControlValidator extends DefaultValidator implements AccessControlConstants {
private final TreeProvider treeProvider;
private final Tree parentAfter;
private final PrivilegeBitsProvider privilegeBitsProvider;
private final PrivilegeManager privilegeManager;
private final RestrictionProvider restrictionProvider;
private final TypePredicate isRepoAccessControllable;
private final TypePredicate isAccessControllable;
AccessControlValidator(@NotNull NodeState parentAfter,
@NotNull PrivilegeManager privilegeManager,
@NotNull PrivilegeBitsProvider privilegeBitsProvider,
@NotNull RestrictionProvider restrictionProvider,
@NotNull ProviderCtx providerCtx) {
treeProvider = providerCtx.getTreeProvider();
this.parentAfter = treeProvider.createReadOnlyTree(parentAfter);
this.privilegeBitsProvider = privilegeBitsProvider;
this.privilegeManager = privilegeManager;
this.restrictionProvider = restrictionProvider;
this.isRepoAccessControllable = new TypePredicate(parentAfter, MIX_REP_REPO_ACCESS_CONTROLLABLE);
this.isAccessControllable = new TypePredicate(parentAfter, MIX_REP_ACCESS_CONTROLLABLE);
}
private AccessControlValidator(AccessControlValidator parent, Tree parentAfter) {
this.treeProvider = parent.treeProvider;
this.parentAfter = parentAfter;
this.privilegeBitsProvider = parent.privilegeBitsProvider;
this.privilegeManager = parent.privilegeManager;
this.restrictionProvider = parent.restrictionProvider;
this.isRepoAccessControllable = parent.isRepoAccessControllable;
this.isAccessControllable = parent.isAccessControllable;
}
//----------------------------------------------------------< Validator >---
@Override
public void propertyAdded(PropertyState after) throws CommitFailedException {
if (isAccessControlEntry(parentAfter)) {
checkValidAccessControlEntry(parentAfter);
}
if (JcrConstants.JCR_MIXINTYPES.equals(after.getName())) {
checkMixinTypes(parentAfter);
}
}
@Override
public void propertyChanged(PropertyState before, PropertyState after) throws CommitFailedException {
if (isAccessControlEntry(parentAfter)) {
checkValidAccessControlEntry(parentAfter);
}
if (JcrConstants.JCR_MIXINTYPES.equals(after.getName())) {
checkMixinTypes(parentAfter);
}
}
@Override
public void propertyDeleted(PropertyState before) throws CommitFailedException {
// nothing to do: mandatory properties will be enforced by node type validator
}
@Override
public Validator childNodeAdded(String name, NodeState after) throws CommitFailedException {
Tree treeAfter = checkNotNull(parentAfter.getChild(name));
checkValidTree(parentAfter, treeAfter, after);
return newValidator(this, treeAfter);
}
@Override
public Validator childNodeChanged(String name, NodeState before, NodeState after) throws CommitFailedException {
Tree treeAfter = checkNotNull(parentAfter.getChild(name));
checkValidTree(parentAfter, treeAfter, after);
return newValidator(this, treeAfter);
}
@Override
public Validator childNodeDeleted(String name, NodeState before) throws CommitFailedException {
// nothing to do
return null;
}
//------------------------------------------------------------< private >---
private static Validator newValidator(AccessControlValidator parent,
Tree parentAfter) {
return new VisibleValidator(
new AccessControlValidator(parent, parentAfter),
true,
true);
}
private void checkValidTree(Tree parentAfter, Tree treeAfter, NodeState nodeAfter) throws CommitFailedException {
if (isPolicy(treeAfter)) {
checkValidPolicy(parentAfter, treeAfter, nodeAfter);
} else if (isAccessControlEntry(treeAfter)) {
checkValidAccessControlEntry(treeAfter);
} else if (NT_REP_RESTRICTIONS.equals(TreeUtil.getPrimaryTypeName(treeAfter))) {
checkIsAccessControlEntry(parentAfter);
checkValidRestrictions(parentAfter);
}
}
private static boolean isPolicy(Tree tree) {
return NT_REP_ACL.equals(TreeUtil.getPrimaryTypeName(tree));
}
private static boolean isAccessControlEntry(Tree tree) {
String ntName = TreeUtil.getPrimaryTypeName(tree);
return NT_REP_DENY_ACE.equals(ntName) || NT_REP_GRANT_ACE.equals(ntName);
}
private static void checkIsAccessControlEntry(Tree tree) throws CommitFailedException {
if (!isAccessControlEntry(tree)) {
throw accessViolation(2, "Access control entry node expected at " + tree.getPath());
}
}
private void checkValidPolicy(Tree parent, Tree policyTree, NodeState policyNode) throws CommitFailedException {
if (REP_REPO_POLICY.equals(policyTree.getName())) {
checkValidAccessControlledNode(parent, isRepoAccessControllable, treeProvider);
checkValidRepoAccessControlled(parent);
} else {
checkValidAccessControlledNode(parent, isAccessControllable, treeProvider);
}
Collection<String> validPolicyNames = (parent.isRoot()) ?
POLICY_NODE_NAMES :
Collections.singleton(REP_POLICY);
if (!validPolicyNames.contains(policyTree.getName())) {
throw accessViolation(3, "Invalid policy name " + policyTree.getName() + " at " + parent.getPath());
}
if (!policyNode.hasProperty(TreeConstants.OAK_CHILD_ORDER)) {
throw accessViolation(4, "Invalid policy node at " + policyTree.getPath() + ": Order of children is not stable.");
}
Set<Entry> aceSet = Sets.newHashSet();
for (Tree child : policyTree.getChildren()) {
if (isAccessControlEntry(child)) {
if (!aceSet.add(new Entry(parent.getPath(), child))) {
throw accessViolation(13, "Duplicate ACE '" + child.getPath() + "' found in policy");
}
}
}
}
private static void checkValidAccessControlledNode(@NotNull Tree accessControlledTree,
@NotNull TypePredicate requiredMixin,
@NotNull TreeProvider treeProvider) throws CommitFailedException {
if (AC_NODETYPE_NAMES.contains(TreeUtil.getPrimaryTypeName(accessControlledTree))) {
throw accessViolation(5, "Access control policy within access control content (" + accessControlledTree.getPath() + ')');
}
if (!requiredMixin.apply(treeProvider.asNodeState(accessControlledTree))) {
String msg = "Isolated policy node (" + accessControlledTree.getPath() + "). Parent is not of type " + requiredMixin;
throw accessViolation(6, msg);
}
}
private void checkValidAccessControlEntry(@NotNull Tree aceNode) throws CommitFailedException {
Tree parent = aceNode.getParent();
if (!parent.exists() || !NT_REP_ACL.equals(TreeUtil.getPrimaryTypeName(parent))) {
throw accessViolation(7, "Isolated access control entry at " + aceNode.getPath());
}
checkValidPrincipal(aceNode);
checkValidPrivileges(aceNode);
checkValidRestrictions(aceNode);
}
private void checkValidPrincipal(@NotNull Tree aceNode) throws CommitFailedException {
String principalName = TreeUtil.getString(aceNode, REP_PRINCIPAL_NAME);
if (principalName == null || principalName.isEmpty()) {
throw accessViolation(8, "Missing principal name at " + aceNode.getPath());
}
// validity of principal is only a JCR specific contract and will not be
// enforced on the oak level.
}
private void checkValidPrivileges(@NotNull Tree aceNode) throws CommitFailedException {
Iterable<String> privilegeNames = TreeUtil.getStrings(aceNode, REP_PRIVILEGES);
if (privilegeNames == null || Iterables.isEmpty(privilegeNames)) {
throw accessViolation(9, "Missing privileges at " + aceNode.getPath());
}
for (String privilegeName : privilegeNames) {
try {
Privilege privilege = privilegeManager.getPrivilege(privilegeName);
if (privilege.isAbstract()) {
throw accessViolation(11, "Abstract privilege " + privilegeName + " at " + aceNode.getPath());
}
} catch (AccessControlException e) {
throw accessViolation(10, "Invalid privilege " + privilegeName + " at " + aceNode.getPath());
} catch (RepositoryException e) {
throw new IllegalStateException("Failed to read privileges", e);
}
}
}
private void checkValidRestrictions(@NotNull Tree aceTree) throws CommitFailedException {
String path;
Tree aclTree = checkNotNull(aceTree.getParent());
String aclPath = aclTree.getPath();
if (REP_REPO_POLICY.equals(Text.getName(aclPath))) {
path = null;
} else {
path = Text.getRelativeParent(aclPath, 1);
}
try {
restrictionProvider.validateRestrictions(path, aceTree);
} catch (AccessControlException e) {
throw new CommitFailedException(ACCESS_CONTROL, 1, "Access control violation", e);
} catch (RepositoryException e) {
throw new CommitFailedException(OAK, 13, "Internal error", e);
}
}
private static void checkMixinTypes(Tree parentTree) throws CommitFailedException {
Iterable<String> mixinNames = TreeUtil.getNames(parentTree, JcrConstants.JCR_MIXINTYPES);
if (Iterables.contains(mixinNames, MIX_REP_REPO_ACCESS_CONTROLLABLE)) {
checkValidRepoAccessControlled(parentTree);
}
}
private static void checkValidRepoAccessControlled(Tree accessControlledTree) throws CommitFailedException {
if (!accessControlledTree.isRoot()) {
throw accessViolation(12, "Only root can store repository level policies (" + accessControlledTree.getPath() + ')');
}
}
private static CommitFailedException accessViolation(int code, String message) {
return new CommitFailedException(ACCESS_CONTROL, code, message);
}
private final class Entry {
private final String principalName;
private final PrivilegeBits privilegeBits;
private final Set<Restriction> restrictions;
private Entry(String path, Tree aceTree) {
principalName = aceTree.getProperty(REP_PRINCIPAL_NAME).getValue(Type.STRING);
privilegeBits = privilegeBitsProvider.getBits(aceTree.getProperty(REP_PRIVILEGES).getValue(Type.NAMES));
restrictions = restrictionProvider.readRestrictions(path, aceTree);
}
@Override
public int hashCode() {
return Objects.hashCode(principalName, privilegeBits, restrictions);
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o instanceof Entry) {
Entry other = (Entry) o;
return Objects.equal(principalName, other.principalName)
&& privilegeBits.equals(other.privilegeBits)
&& restrictions.equals(other.restrictions);
}
return false;
}
}
}