| /* |
| * 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; |
| } |
| } |
| } |