blob: 8c56c34951fc8ee79a1d81e15c19d26a02735c93 [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.core;
import java.io.IOException;
import java.io.InputStream;
import com.google.common.base.Predicate;
import org.apache.jackrabbit.oak.api.Blob;
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.commons.LazyValue;
import org.apache.jackrabbit.oak.plugins.tree.factories.TreeFactory;
import org.apache.jackrabbit.oak.spi.security.authorization.permission.PermissionProvider;
import org.apache.jackrabbit.oak.spi.security.authorization.permission.TreePermission;
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.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Iterables.size;
import static java.util.Collections.emptyList;
import static org.apache.jackrabbit.oak.api.Type.BOOLEAN;
import static org.apache.jackrabbit.oak.api.Type.NAME;
import static org.apache.jackrabbit.oak.api.Type.NAMES;
import static org.apache.jackrabbit.oak.api.Type.STRING;
class SecureNodeBuilder implements NodeBuilder {
/**
* Root builder, or {@code this} for the root builder itself.
*/
private final SecureNodeBuilder rootBuilder;
/**
* Parent builder, or {@code null} for a root builder.
*/
private final SecureNodeBuilder parent;
/**
* Name of this child node within the parent builder,
* or {@code null} for a root builder.
*/
private final String name;
/**
* Permissions provider for evaluating access rights to the underlying raw builder
*/
private final LazyValue<PermissionProvider> permissionProvider;
/**
* Underlying node builder.
*/
private final NodeBuilder builder;
/**
* Security context of this subtree, updated whenever the base revision
* changes.
*/
private TreePermission treePermission = null; // initialized lazily
/**
* Possibly outdated reference to the tree permission of the root
* builder. Used to detect when the base state (and thus the security
* context) of the root builder has changed, and trigger an update of
* the tree permission of this builder.
*
* @see #treePermission
*/
private TreePermission rootPermission = null; // initialized lazily
/**
* Create the {@code SecureNodeBuilder} for the root node.
*
* @param builder The {@code NodeBuilder} of the root node.
* @param permissionProvider The {@code PermissionProvider} used to evaluation read access.
*/
SecureNodeBuilder(
@NotNull NodeBuilder builder,
@NotNull LazyValue<PermissionProvider> permissionProvider) {
this.rootBuilder = this;
this.parent = null;
this.name = null;
this.permissionProvider = checkNotNull(permissionProvider);
this.builder = checkNotNull(builder);
}
private SecureNodeBuilder(SecureNodeBuilder parent, String name) {
this.rootBuilder = parent.rootBuilder;
this.parent = parent;
this.name = name;
this.permissionProvider = parent.permissionProvider;
this.builder = parent.builder.getChildNode(name);
}
@Override @NotNull
public NodeState getBaseState() {
return new SecureNodeState(builder.getBaseState(), getTreePermission());
}
@Override @NotNull
public NodeState getNodeState() {
return new SecureNodeState(builder.getNodeState(), getTreePermission());
}
@Override
public boolean exists() {
return builder.exists()
&& (builder.isReplaced() || getTreePermission().canRead());
}
@Override
public boolean isNew() {
return builder.isNew()
|| (builder.isReplaced() && !getTreePermission().canRead());
}
@Override
public boolean isNew(String name) {
return builder.isNew(name)
|| (builder.isReplaced(name)
&& !getTreePermission().canRead(builder.getProperty(name)));
}
@Override
public boolean isModified() {
return builder.isModified();
}
@Override
public boolean isReplaced() {
return builder.isReplaced() && !isNew();
}
@Override
public boolean isReplaced(String name) {
return builder.isReplaced(name) && !isNew(name);
}
public void baseChanged() {
checkState(parent == null);
treePermission = null; // trigger re-evaluation
rootPermission = null;
}
@Override
public boolean remove() {
return exists() && builder.remove();
}
@Override
public boolean moveTo(@NotNull NodeBuilder newParent, @NotNull String newName) {
return exists() && builder.moveTo(newParent, newName);
}
@Nullable
@Override
public PropertyState getProperty(String name) {
PropertyState property = builder.getProperty(name);
if (new ReadablePropertyPredicate().apply(property)) {
return property;
} else {
return null;
}
}
@Override
public boolean hasProperty(String name) {
return getProperty(name) != null;
}
@Override
public synchronized long getPropertyCount() {
if (getTreePermission().canReadProperties() || isNew()) {
return builder.getPropertyCount();
} else {
return size(filter(
builder.getProperties(),
new ReadablePropertyPredicate()));
}
}
@NotNull
@Override
public Iterable<? extends PropertyState> getProperties() {
if (getTreePermission().canReadProperties() || isNew()) {
return builder.getProperties();
} else {
return filter(
builder.getProperties(),
new ReadablePropertyPredicate());
}
}
@Override
public boolean getBoolean(@NotNull String name) {
PropertyState property = getProperty(name);
return isType(property, BOOLEAN) && property.getValue(BOOLEAN);
}
@Nullable
@Override
public String getString(@NotNull String name) {
PropertyState property = getProperty(name);
if (isType(property, STRING)) {
return property.getValue(STRING);
} else {
return null;
}
}
@Nullable
@Override
public String getName(@NotNull String name) {
PropertyState property = getProperty(name);
if (isType(property, NAME)) {
return property.getValue(NAME);
} else {
return null;
}
}
@NotNull
@Override
public Iterable<String> getNames(@NotNull String name) {
PropertyState property = getProperty(name);
if (isType(property, NAMES)) {
return property.getValue(NAMES);
} else {
return emptyList();
}
}
@NotNull
@Override
public NodeBuilder setProperty(@NotNull PropertyState property) {
builder.setProperty(property);
return this;
}
@NotNull
@Override
public <T> NodeBuilder setProperty(String name, @NotNull T value) {
builder.setProperty(name, value);
return this;
}
@NotNull
@Override
public <T> NodeBuilder setProperty(
String name, @NotNull T value, Type<T> type) {
builder.setProperty(name, value, type);
return this;
}
@NotNull
@Override
public NodeBuilder removeProperty(String name) {
if (hasProperty(name)) { // only remove properties that we can see
builder.removeProperty(name);
}
return this;
}
@NotNull
@Override
public Iterable<String> getChildNodeNames() {
return filter(
builder.getChildNodeNames(),
input -> input != null && getChildNode(input).exists());
}
@Override
public boolean hasChildNode(@NotNull String name) {
if (builder.hasChildNode(name)) {
return getChildNode(name).exists();
} else {
return false;
}
}
@NotNull
@Override
public NodeBuilder child(@NotNull String name) {
if (hasChildNode(name)) {
return getChildNode(name);
} else {
return setChildNode(name);
}
}
@NotNull
@Override
public NodeBuilder setChildNode(@NotNull String name) {
builder.setChildNode(name);
return new SecureNodeBuilder(this, name);
}
@NotNull
@Override
public NodeBuilder setChildNode(@NotNull String name, @NotNull NodeState nodeState) {
builder.setChildNode(name, nodeState);
return new SecureNodeBuilder(this, name);
}
@NotNull
@Override
public NodeBuilder getChildNode(@NotNull String name) {
return new SecureNodeBuilder(this, name);
}
@Override
public synchronized long getChildNodeCount(long max) {
if (getTreePermission().canReadAll()) {
return builder.getChildNodeCount(max);
} else {
return size(getChildNodeNames());
}
}
@Override
public Blob createBlob(InputStream stream) throws IOException {
return builder.createBlob(stream);
}
/**
* Permissions of this tree.
*
* @return The permissions for this tree.
*/
@NotNull
private TreePermission getTreePermission() {
if (treePermission == null
|| rootPermission != rootBuilder.treePermission) {
NodeState base = builder.getBaseState();
if (parent == null) {
Tree baseTree = TreeFactory.createReadOnlyTree(base);
treePermission = permissionProvider.get().getTreePermission(baseTree, TreePermission.EMPTY);
rootPermission = treePermission;
} else {
treePermission = parent.getTreePermission().getChildPermission(name, base);
rootPermission = parent.rootPermission;
}
}
return treePermission;
}
private static boolean isType(@Nullable PropertyState property, Type<?> type) {
Type<?> t = (property == null) ? null : property.getType();
return t == type;
}
//------------------------------------------------------< inner classes >---
/**
* Predicate for testing whether a given property is readable.
*/
private class ReadablePropertyPredicate implements Predicate<PropertyState> {
@Override
public boolean apply(@Nullable PropertyState property) {
if (property != null) {
String propertyName = property.getName();
return NodeStateUtils.isHidden(propertyName) ||
getTreePermission().canRead(property) ||
isNew(propertyName);
} else {
return false;
}
}
}
}