/*
* 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.user;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.jcr.RepositoryException;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import org.apache.jackrabbit.commons.iterator.RangeIteratorAdapter;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Root;
import org.apache.jackrabbit.oak.api.Tree;
import org.apache.jackrabbit.oak.plugins.memory.PropertyStates;
import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType;
import org.apache.jackrabbit.oak.spi.security.user.util.UserUtil;
import org.apache.jackrabbit.oak.spi.state.PropertyBuilder;
import org.apache.jackrabbit.oak.util.NodeUtil;
import org.apache.jackrabbit.oak.util.PropertyUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.jackrabbit.oak.api.Type.STRINGS;
import static org.apache.jackrabbit.oak.api.Type.WEAKREFERENCE;
/**
* {@code MembershipProvider} implementation storing group membership information
* with the {@code Tree} associated with a given {@link org.apache.jackrabbit.api.security.user.Group}.
* Depending on the configuration there are two variants on how group members
* are recorded:
* <p/>
* <h3>Membership stored in multi-valued property</h3>
* This is the default way of storing membership information with the following
* characteristics:
* <ul>
* <li>Multivalued property {@link #REP_MEMBERS}</li>
* <li>Property type: {@link javax.jcr.PropertyType#WEAKREFERENCE}</li>
* <li>Used if the config option {@link org.apache.jackrabbit.oak.spi.security.user.UserConstants#PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE} is missing or <4</li>
* </ul>
* <p/>
* <h3>Membership stored in individual properties</h3>
* Variant to store group membership based on the
* {@link org.apache.jackrabbit.oak.spi.security.user.UserConstants#PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE} configuration parameter:
* <p/>
* <ul>
* <li>Membership information stored underneath a {@link #REP_MEMBERS} node hierarchy</li>
* <li>Individual member information is stored each in a {@link javax.jcr.PropertyType#WEAKREFERENCE}
* property</li>
* <li>Node hierarchy is split based on the {@link org.apache.jackrabbit.oak.spi.security.user.UserConstants#PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE}
* configuration parameter.</li>
* <li>{@link org.apache.jackrabbit.oak.spi.security.user.UserConstants#PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE} must be greater than 4
* in order to turn on this behavior</li>
* </ul>
* <p/>
* <h3>Compatibility</h3>
* This membership provider is able to deal with both options being present in
* the content. If the {@link org.apache.jackrabbit.oak.spi.security.user.UserConstants#PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE} configuration
* parameter is modified later on, existing membership information is not
* modified or converted to the new structure.
*/
class MembershipProvider extends AuthorizableBaseProvider {
private static final Logger log = LoggerFactory.getLogger(MembershipProvider.class);
private final int splitSize;
MembershipProvider(Root root, ConfigurationParameters config) {
super(root, config);
int splitValue = config.getConfigValue(PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE, 0);
if (splitValue != 0 && splitValue < 4) {
log.warn("Invalid value {} for {}. Expected integer >= 4 or 0", splitValue, PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE);
splitValue = 0;
}
this.splitSize = splitValue;
}
@Nonnull
Iterator<String> getMembership(Tree authorizableTree, boolean includeInherited) {
Set<String> groupPaths = new HashSet<String>();
Set<String> refPaths = identifierManager.getReferences(true, authorizableTree, REP_MEMBERS, NT_REP_GROUP, NT_REP_MEMBERS);
for (String propPath : refPaths) {
int index = propPath.indexOf('/' + REP_MEMBERS);
if (index > 0) {
groupPaths.add(propPath.substring(0, index));
} else {
log.debug("Not a membership reference property " + propPath);
}
}
Iterator<String> it = groupPaths.iterator();
if (includeInherited && it.hasNext()) {
return getAllMembership(groupPaths.iterator());
} else {
return new RangeIteratorAdapter(it, groupPaths.size());
}
}
@Nonnull
Iterator<String> getMembers(Tree groupTree, AuthorizableType authorizableType, boolean includeInherited) {
Iterable memberPaths = Collections.emptySet();
if (useMemberNode(groupTree)) {
Tree membersTree = groupTree.getChild(REP_MEMBERS);
if (membersTree.exists()) {
throw new UnsupportedOperationException("not implemented: retrieve members from member-node hierarchy");
}
} else {
PropertyState property = groupTree.getProperty(REP_MEMBERS);
if (property != null) {
Iterable<String> vs = property.getValue(STRINGS);
memberPaths = Iterables.filter(Iterables.transform(vs, new Function<String, String>() {
@Override
public String apply(@Nullable String value) {
return identifierManager.getPath(PropertyStates.createProperty("", value, WEAKREFERENCE));
}
}), Predicates.<String>notNull());
}
}
Iterator it = memberPaths.iterator();
if (includeInherited && it.hasNext()) {
return getAllMembers(it, authorizableType);
} else {
return new RangeIteratorAdapter(it, Iterables.size(memberPaths));
}
}
boolean isMember(Tree groupTree, Tree authorizableTree, boolean includeInherited) {
if (includeInherited) {
Iterator<String> groupPaths = getMembership(authorizableTree, true);
String path = groupTree.getPath();
while (groupPaths.hasNext()) {
if (path.equals(groupPaths.next())) {
return true;
}
}
} else {
if (useMemberNode(groupTree)) {
Tree membersTree = groupTree.getChild(REP_MEMBERS);
if (membersTree.exists()) {
// FIXME: testing for property name in jr2 wasn't correct.
// TODO OAK-482: add implementation
throw new UnsupportedOperationException("not implemented: isMembers determined from member-node hierarchy");
}
} else {
PropertyState property = groupTree.getProperty(REP_MEMBERS);
if (property != null) {
Iterable<String> members = property.getValue(STRINGS);
String authorizableUUID = getContentID(authorizableTree);
for (String v : members) {
if (authorizableUUID.equals(v)) {
return true;
}
}
}
}
}
// no a member of the specified group
return false;
}
boolean addMember(Tree groupTree, Tree newMemberTree) throws RepositoryException {
return addMember(groupTree, newMemberTree.getName(), getContentID(newMemberTree));
}
boolean addMember(Tree groupTree, String treeName, String memberContentId) throws RepositoryException {
if (useMemberNode(groupTree)) {
NodeUtil groupNode = new NodeUtil(groupTree);
NodeUtil membersNode = groupNode.getOrAddChild(REP_MEMBERS, NT_REP_MEMBERS);
// TODO OAK-482: add implementation that allows to index group members
throw new UnsupportedOperationException("not implemented: addMember with member-node hierarchy");
} else {
PropertyBuilder<String> propertyBuilder = getMembersPropertyBuilder(groupTree);
if (propertyBuilder.hasValue(memberContentId)) {
return false;
} else {
propertyBuilder.addValue(memberContentId);
}
groupTree.setProperty(propertyBuilder.getPropertyState());
}
return true;
}
boolean removeMember(Tree groupTree, Tree memberTree) {
if (useMemberNode(groupTree)) {
Tree membersTree = groupTree.getChild(REP_MEMBERS);
if (membersTree.exists()) {
// TODO OAK-482: add implementation
throw new UnsupportedOperationException("not implemented: remove member from member-node hierarchy");
}
} else {
String toRemove = getContentID(memberTree);
PropertyBuilder<String> propertyBuilder = getMembersPropertyBuilder(groupTree);
if (propertyBuilder.hasValue(toRemove)) {
propertyBuilder.removeValue(toRemove);
if (propertyBuilder.isEmpty()) {
groupTree.removeProperty(REP_MEMBERS);
} else {
groupTree.setProperty(propertyBuilder.getPropertyState());
}
return true;
}
}
// nothing changed
log.debug("Authorizable {} was not member of {}", memberTree.getName(), groupTree.getName());
return false;
}
/**
* Returns {@code true} if the given {@code newMember} is a Group
* and contains {@code this} Group as declared or inherited member.
*
* @param newMemberTree The new member to be tested for cyclic membership.
* @param groupContentId The content ID of the group.
* @return true if the 'newMember' is a group and 'this' is an declared or
* inherited member of it.
*/
boolean isCyclicMembership(Tree newMemberTree, String groupContentId) {
if (UserUtil.isType(newMemberTree, AuthorizableType.GROUP)) {
for (Iterator<String> it = getMembers(newMemberTree, AuthorizableType.GROUP, true); it.hasNext(); ) {
Tree tree = root.getTree(it.next());
String contentId = getContentID(tree);
if (groupContentId.equals(contentId)) {
// found cyclic group membership
return true;
}
}
}
return false;
}
//-----------------------------------------< private MembershipProvider >---
private PropertyBuilder<String> getMembersPropertyBuilder(Tree groupTree) {
PropertyState property = groupTree.getProperty(REP_MEMBERS);
if (property == null) {
return PropertyUtil.getPropertyBuilder(WEAKREFERENCE, REP_MEMBERS, true);
} else {
return PropertyUtil.getPropertyBuilder(WEAKREFERENCE, property);
}
}
private boolean useMemberNode(Tree groupTree) {
return splitSize >= 4 && !groupTree.hasProperty(REP_MEMBERS);
}
/**
* Returns an iterator of authorizables which includes all indirect members
* of the given iterator of authorizables.
*
* @param declaredMembers Iterator containing the paths to the declared members.
* @param authorizableType Flag used to filter the result by authorizable type.
* @return Iterator of Authorizable objects
*/
private Iterator<String> getAllMembers(final Iterator<String> declaredMembers,
final AuthorizableType authorizableType) {
Iterator<Iterator<String>> inheritedMembers = new Iterator<Iterator<String>>() {
@Override
public boolean hasNext() {
return declaredMembers.hasNext();
}
@Override
public Iterator<String> next() {
String memberPath = declaredMembers.next();
if (memberPath == null) {
return Iterators.emptyIterator();
} else {
return Iterators.concat(Iterators.singletonIterator(memberPath), inherited(memberPath));
}
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
private Iterator<String> inherited(String authorizablePath) {
Tree group = getByPath(authorizablePath);
if (UserUtil.isType(group, AuthorizableType.GROUP)) {
return getMembers(group, authorizableType, true);
} else {
return Iterators.emptyIterator();
}
}
};
return Iterators.filter(Iterators.concat(inheritedMembers), new ProcessedPathPredicate());
}
private Iterator<String> getAllMembership(final Iterator<String> groupPaths) {
Iterator<Iterator<String>> inheritedMembership = new Iterator<Iterator<String>>() {
@Override
public boolean hasNext() {
return groupPaths.hasNext();
}
@Override
public Iterator<String> next() {
String groupPath = groupPaths.next();
return Iterators.concat(Iterators.singletonIterator(groupPath), inherited(groupPath));
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
private Iterator<String> inherited(String authorizablePath) {
Tree group = getByPath(authorizablePath);
if (UserUtil.isType(group, AuthorizableType.GROUP)) {
return getMembership(group, true);
} else {
return Iterators.emptyIterator();
}
}
};
return Iterators.filter(Iterators.concat(inheritedMembership), new ProcessedPathPredicate());
}
private static final class ProcessedPathPredicate implements Predicate<String> {
private final Set<String> processed = new HashSet<String>();
@Override
public boolean apply(@Nullable String path) {
return processed.add(path);
}
}
}