blob: a7c080ee7d6d94a637620fba4e76a08b1b53c2b9 [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.spi.security.authorization.cug.impl;
import java.util.List;
import java.util.Set;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.apache.jackrabbit.oak.api.CommitFailedException;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.plugins.memory.PropertyBuilder;
import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
import org.apache.jackrabbit.oak.spi.commit.PostValidationHook;
import org.apache.jackrabbit.oak.spi.state.DefaultNodeStateDiff;
import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.apache.jackrabbit.oak.spi.state.NodeStateUtils;
import org.apache.jackrabbit.util.Text;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE;
/**
* {@code PostValidationHook} implementation responsible for keeping track of
* nested CUGs to simplify evaluation. The information about the nested CUGs is
* stored in a hidden property associated with the policy node.
*
* Note, that this hook does _not_ respect the configured supported paths
* and keeps track of all CUG policies irrespective of their validity.
* Consequently all optimization considering the nested CUG information must
* also verify the supported paths.
*/
class NestedCugHook implements PostValidationHook, CugConstants {
/**
* logger instance
*/
private static final Logger log = LoggerFactory.getLogger(NestedCugHook.class);
private Set<String> deletedCUGs = Sets.newHashSet();
//-------------------------------------------------< PostValidationHook >---
@NotNull
@Override
public NodeState processCommit(NodeState before, NodeState after, CommitInfo info) throws CommitFailedException {
NodeBuilder builder = after.builder();
after.compareAgainstBaseState(before, new Diff(before, builder));
deletedCUGs.clear();
return builder.getNodeState();
}
//-------------------------------------------------------------< Object >---
@Override
public String toString() {
return "NestedCugHook";
}
//------------------------------------------------------------< private >---
private static long addNestedCugPath(@NotNull NodeBuilder parentBuilder, @NotNull NodeBuilder builder, @NotNull String pathWithNewCug) {
PropertyState ps = parentBuilder.getProperty(HIDDEN_NESTED_CUGS);
PropertyBuilder<String> pb = getHiddenPropertyBuilder(ps);
if (ps != null) {
List<String> moveToNestedCug = Lists.newArrayList();
for (String p : ps.getValue(Type.STRINGS)) {
if (Text.isDescendant(pathWithNewCug, p)) {
pb.removeValue(p);
moveToNestedCug.add(p);
} else if (p.equals(pathWithNewCug)) {
// already present with parent -> remove to avoid duplicate entries
log.debug("Path of node holding a new nested CUG is already listed with the parent CUG.");
pb.removeValue(p);
}
}
if (!moveToNestedCug.isEmpty()) {
PropertyBuilder<String> pb2 = getHiddenPropertyBuilder(builder.getProperty(HIDDEN_NESTED_CUGS));
pb2.addValues(moveToNestedCug);
builder.setProperty(pb2.getPropertyState());
}
}
// update the nested-cug property of the parent
pb.addValue(pathWithNewCug);
parentBuilder.setProperty(pb.getPropertyState());
return pb.count();
}
private static int removeNestedCugPath(@NotNull NodeBuilder parentBuilder, @NotNull String toRemove, @NotNull Iterable<String> toReconnect) {
PropertyState ps = parentBuilder.getProperty(HIDDEN_NESTED_CUGS);
PropertyBuilder<String> pb = getHiddenPropertyBuilder(ps);
if (pb.hasValue(toRemove)) {
pb.removeValue(toRemove);
pb.addValues(toReconnect);
if (pb.isEmpty()) {
parentBuilder.removeProperty(HIDDEN_NESTED_CUGS);
return 0;
} else {
parentBuilder.setProperty(pb.getPropertyState());
return pb.count();
}
} else {
log.debug("Parent CUG doesn't contain expected entry for removed nested CUG");
return -1;
}
}
private static PropertyBuilder<String> getHiddenPropertyBuilder(@Nullable PropertyState ps) {
return PropertyBuilder.copy(Type.STRING, ps).setName(HIDDEN_NESTED_CUGS).setArray();
}
private final class Diff extends DefaultNodeStateDiff {
private final Diff parentDiff;
private final boolean isRoot;
private String path;
private NodeState beforeState = null;
private NodeBuilder afterBuilder;
private boolean afterHoldsCug;
private Diff(@NotNull NodeState rootBefore, @NotNull NodeBuilder rootAfter) {
parentDiff = null;
isRoot = true;
path = PathUtils.ROOT_PATH;
beforeState = rootBefore;
afterBuilder = rootAfter;
afterHoldsCug = CugUtil.hasCug(rootAfter);
}
private Diff(@NotNull Diff parentDiff, @NotNull String name, @Nullable NodeState before, @Nullable NodeBuilder after) {
this.parentDiff = parentDiff;
isRoot = false;
path = PathUtils.concat(parentDiff.path, name);
this.beforeState = before;
afterBuilder = after;
afterHoldsCug = CugUtil.hasCug(after);
}
@Override
public boolean childNodeAdded(String name, NodeState after) {
if (!NodeStateUtils.isHidden(name)) {
if (CugUtil.definesCug(name, after)) {
if (isRoot) {
PropertyState alt = afterBuilder.getProperty(HIDDEN_NESTED_CUGS);
if (alt != null) {
NodeBuilder cugNode = afterBuilder.getChildNode(REP_CUG_POLICY);
cugNode.setProperty(alt);
afterBuilder.removeProperty(HIDDEN_NESTED_CUGS);
afterBuilder.removeProperty(HIDDEN_TOP_CUG_CNT);
}
} else {
Diff diff = parentDiff;
while (diff != null) {
if (diff.afterHoldsCug) {
NodeBuilder cugNode = diff.afterBuilder.getChildNode(REP_CUG_POLICY);
addNestedCugPath(cugNode, afterBuilder.getChildNode(REP_CUG_POLICY), path);
break;
} else if (diff.isRoot) {
long cnt = addNestedCugPath(diff.afterBuilder, afterBuilder.getChildNode(name), path);
diff.afterBuilder.setProperty(HIDDEN_TOP_CUG_CNT, cnt, Type.LONG);
}
diff = diff.parentDiff;
}
}
// no need to traverse down the CUG policy node.
} else {
after.compareAgainstBaseState(EMPTY_NODE, new Diff(this, name, null, afterBuilder.getChildNode(name)));
}
}
return true;
}
@Override
public boolean childNodeChanged(String name, NodeState before, NodeState after) {
if (!NodeStateUtils.isHidden(name)) {
after.compareAgainstBaseState(before, new Diff(this, name, before, afterBuilder.getChildNode(name)));
}
return true;
}
@Override
public boolean childNodeDeleted(String name, NodeState before) {
if (!NodeStateUtils.isHidden(name)) {
if (CugUtil.definesCug(name, before)) {
deletedCUGs.add(path);
// reconnect information about nested CUGs at a parent if
// only the CUG got removed but the whole subtree including CUGs
// are still present.
Set<String> reconnect = Sets.newHashSet();
if (afterBuilder != null) {
for (String nestedCug : before.getStrings(HIDDEN_NESTED_CUGS)) {
if (!deletedCUGs.contains(nestedCug)) {
String relPath = PathUtils.relativize(path, nestedCug);
NodeState ns = NodeStateUtils.getNode(afterBuilder.getNodeState(), relPath);
if (CugUtil.hasCug(ns)) {
reconnect.add(nestedCug);
}
}
}
}
if (isRoot) {
if (!Iterables.isEmpty(reconnect)) {
afterBuilder.setProperty(HIDDEN_NESTED_CUGS, reconnect, Type.STRINGS);
afterBuilder.setProperty(HIDDEN_TOP_CUG_CNT, reconnect.size());
}
} else {
Diff diff = parentDiff;
while (diff != null) {
if (diff.afterHoldsCug) {
// found an existing parent CUG
NodeBuilder cugNode = diff.afterBuilder.getChildNode(REP_CUG_POLICY);
if (removeNestedCugPath(cugNode, path, reconnect) > -1) {
break;
}
}
if (CugUtil.hasCug(diff.beforeState)) {
// parent CUG got removed -> no removal/reconnect required if current path is listed.
NodeState cugNode = diff.beforeState.getChildNode(REP_CUG_POLICY);
PropertyState ps = cugNode.getProperty(HIDDEN_NESTED_CUGS);
if (ps != null && Iterables.contains(ps.getValue(Type.STRINGS), path)) {
log.debug("Nested cug property containing " + path + " has also been removed; no reconnect required.");
break;
}
}
if (diff.isRoot) {
long cnt = removeNestedCugPath(diff.afterBuilder, path, reconnect);
if (cnt < 0) {
log.warn("Failed to updated nested CUG info for path '" + path + "'.");
} else if (cnt == 0) {
diff.afterBuilder.removeProperty(HIDDEN_TOP_CUG_CNT);
} else {
diff.afterBuilder.setProperty(HIDDEN_TOP_CUG_CNT, cnt, Type.LONG);
}
}
diff = diff.parentDiff;
}
}
// no need to traverse down the CUG policy node
} else {
EMPTY_NODE.compareAgainstBaseState(before, new Diff(this, name, before, null));
}
}
return true;
}
}
}