/*
* 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.core.security.authorization.acl;
import org.apache.jackrabbit.core.NodeImpl;
import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.observation.SynchronousEventListener;
import org.apache.jackrabbit.core.security.authorization.AccessControlConstants;
import org.apache.jackrabbit.core.security.authorization.AccessControlModifications;
import org.apache.jackrabbit.core.security.authorization.AccessControlObserver;
import org.apache.jackrabbit.spi.commons.conversion.NameResolver;
import org.apache.jackrabbit.util.Text;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.ItemNotFoundException;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.observation.Event;
import javax.jcr.observation.EventIterator;
import javax.jcr.observation.EventListener;
import javax.jcr.observation.ObservationManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* <code>EntryCollector</code> collects ACEs defined and effective for a
* given <code>Node</code> and listens to access control modifications in order
* to inform listeners.
*/
public class EntryCollector extends AccessControlObserver implements AccessControlConstants {
/**
* logger instance
*/
private static final Logger log = LoggerFactory.getLogger(EntryCollector.class);
/**
* The system session used to register an event listener and process the
* events as well as collect AC entries.
*/
protected final SessionImpl systemSession;
/**
* The root id.
*/
protected final NodeId rootID;
private final EventListener moveListener;
/**
*
* @param systemSession
* @param rootID
* @throws RepositoryException
*/
protected EntryCollector(SessionImpl systemSession, NodeId rootID) throws RepositoryException {
this.systemSession = systemSession;
this.rootID = rootID;
ObservationManager observationMgr = systemSession.getWorkspace().getObservationManager();
/*
Make sure the collector and all subscribed listeners are informed upon
ACL modifications. Interesting events are:
- new ACL (NODE_ADDED)
- new ACE (NODE_ADDED)
- changing ACE (PROPERTY_CHANGED)
- removed ACL (NODE_REMOVED)
- removed ACE (NODE_REMOVED)
*/
int events = Event.PROPERTY_CHANGED | Event.NODE_ADDED | Event.NODE_REMOVED;
String[] ntNames = new String[] {
systemSession.getJCRName(NT_REP_ACCESS_CONTROLLABLE),
systemSession.getJCRName(NT_REP_ACL),
systemSession.getJCRName(NT_REP_ACE)
};
String rootPath = systemSession.getRootNode().getPath();
observationMgr.addEventListener(this, events, rootPath, true, null, ntNames, true);
/*
In addition both the collector and all subscribed listeners should be
informed about any kind of move events.
*/
moveListener = new MoveListener();
observationMgr.addEventListener(moveListener, Event.NODE_MOVED, rootPath, true, null, null, true);
}
/**
* Release all resources contained by this instance. It will no longer be
* used. This implementation only stops listening to ac modification events.
*/
@Override
protected void close() {
super.close();
try {
ObservationManager observationMgr = systemSession.getWorkspace().getObservationManager();
observationMgr.removeEventListener(this);
observationMgr.removeEventListener(moveListener);
} catch (RepositoryException e) {
log.error("Unexpected error while closing CachingEntryCollector", e);
}
}
/**
* Collect the ACEs effective at the given node applying the specified
* filter.
*
* @param node
* @param filter
* @return
* @throws RepositoryException
*/
protected List<Entry> collectEntries(NodeImpl node, EntryFilter filter) throws RepositoryException {
LinkedList<Entry> userAces = new LinkedList<Entry>();
LinkedList<Entry> groupAces = new LinkedList<Entry>();
if (node == null) {
// repository level permissions
NodeImpl root = (NodeImpl) systemSession.getRootNode();
if (ACLProvider.isRepoAccessControlled(root)) {
NodeImpl aclNode = root.getNode(N_REPO_POLICY);
filterEntries(filter, Entry.readEntries(aclNode, null), userAces, groupAces);
}
} else {
filterEntries(filter, getEntries(node).getACEs(), userAces, groupAces);
NodeId next = node.getParentId();
while (next != null) {
Entries entries = getEntries(next);
filterEntries(filter, entries.getACEs(), userAces, groupAces);
next = entries.getNextId();
}
}
List<Entry> entries = new ArrayList<Entry>(userAces.size() + groupAces.size());
entries.addAll(userAces);
entries.addAll(groupAces);
return entries;
}
/**
* Filter the specified access control <code>entries</code>
*
* @param filter
* @param aces
* @param userAces
* @param groupAces
*/
@SuppressWarnings("unchecked")
private static void filterEntries(EntryFilter filter, List<Entry> aces,
LinkedList<Entry> userAces,
LinkedList<Entry> groupAces) {
if (!aces.isEmpty() && filter != null) {
filter.filterEntries(aces, userAces, groupAces);
}
}
/**
* Retrieve the access control entries defined for the given node. If the
* node is not access controlled or if the ACL is empty this method returns
* an empty list.
*
* @param node
* @return
* @throws RepositoryException
*/
protected Entries getEntries(NodeImpl node) throws RepositoryException {
List<Entry> aces;
if (ACLProvider.isAccessControlled(node)) {
// collect the aces of that node.
NodeImpl aclNode = node.getNode(N_POLICY);
aces = Entry.readEntries(aclNode, node.getPath());
} else {
// not access controlled
aces = Collections.emptyList();
}
return new Entries(aces, node.getParentId());
}
/**
*
* @param nodeId
* @return
* @throws RepositoryException
*/
protected Entries getEntries(NodeId nodeId) throws RepositoryException {
NodeImpl node = getNodeById(nodeId);
return getEntries(node);
}
/**
*
* @param nodeId
* @return
* @throws javax.jcr.RepositoryException
*/
NodeImpl getNodeById(NodeId nodeId) throws RepositoryException {
return ((NodeImpl) systemSession.getItemManager().getItem(nodeId));
}
//------------------------------------------------------< EventListener >---
/**
* Collects access controlled nodes that are effected by access control
* changes together with the corresponding modification types, and
* notifies access control listeners about the modifications.
*
* @param events
*/
public void onEvent(EventIterator events) {
try {
// JCR-2890: We need to use a fresh new session here to avoid
// deadlocks caused by concurrent threads possibly using the
// systemSession instance for other purposes.
String workspaceName = systemSession.getWorkspace().getName();
Session session = systemSession.createSession(workspaceName);
try {
// Sift through the events to find access control modifications
ACLEventSieve sieve = new ACLEventSieve(session, (NameResolver) session);
sieve.siftEvents(events);
// Notify listeners and eventually clean up internal caches
AccessControlModifications<NodeId> mods = sieve.getModifications();
if (!mods.getNodeIdentifiers().isEmpty()) {
notifyListeners(mods);
}
} finally {
session.logout();
}
} catch (RepositoryException e) {
log.error("Failed to process access control modifications", e);
}
}
//--------------------------------------------------------< inner class >---
/**
* Private utility class for sifting through observation events on
* ACL, ACE and Policy nodes to find out the nodes whose access controls
* have changed. Used by the {@link EntryCollector#onEvent(EventIterator)}
* method.
*/
private static class ACLEventSieve {
/** Session with system privileges. */
private final Session session;
/**
* Standard JCR name form of the
* {@link AccessControlConstants#N_POLICY} constant.
*/
private final String repPolicyName;
/**
* Map of access-controlled nodeId to type of access control modification.
*/
private final Map<NodeId, Integer> modMap = new HashMap<NodeId,Integer>();
private ACLEventSieve(Session session, NameResolver resolver) throws RepositoryException {
this.session = session;
this.repPolicyName = resolver.getJCRName(AccessControlConstants.N_POLICY);
}
/**
* Collects the identifiers of all access controlled nodes that have
* been affected by the events, and thus need their cache entries
* updated or cleared.
*
* @param events access control modification events
*/
private void siftEvents(EventIterator events) {
while (events.hasNext()) {
Event event = events.nextEvent();
try {
switch (event.getType()) {
case Event.NODE_ADDED:
siftNodeAdded(event.getIdentifier());
break;
case Event.NODE_REMOVED:
siftNodeRemoved(event.getPath());
break;
case Event.PROPERTY_CHANGED:
siftPropertyChanged(event.getIdentifier());
break;
default:
// illegal event-type: should never occur. ignore
}
} catch (RepositoryException e) {
// should not get here
log.warn("Failed to process ACL event: " + event, e);
}
}
}
/**
* Returns the access control modifications collected from
* related observation events.
*
* @return access control modifications
*/
private AccessControlModifications<NodeId> getModifications() {
return new AccessControlModifications<NodeId>(modMap);
}
private void siftNodeAdded(String identifier) throws RepositoryException {
try {
NodeImpl n = (NodeImpl) session.getNodeByIdentifier(identifier);
if (n.isNodeType(EntryCollector.NT_REP_ACL)) {
// a new ACL was added -> use the added node to update
// the cache.
addModification(
accessControlledIdFromAclNode(n),
AccessControlObserver.POLICY_ADDED);
} else if (n.isNodeType(EntryCollector.NT_REP_ACE)) {
// a new ACE was added -> use the parent node (acl)
// to update the cache.
addModification(
accessControlledIdFromAceNode(n),
AccessControlObserver.POLICY_MODIFIED);
} /* else: some other node added below an access controlled
parent node -> not interested. */
} catch (ItemNotFoundException e) {
log.debug("Cannot process NODE_ADDED event. Node {} doesn't exist (anymore).", identifier);
}
}
private void siftNodeRemoved(String path) throws RepositoryException {
String parentPath = Text.getRelativeParent(path, 1);
if (session.nodeExists(parentPath)) {
NodeImpl parent = (NodeImpl) session.getNode(parentPath);
if (repPolicyName.equals(Text.getName(path))){
// the complete ACL was removed -> clear cache entry
addModification(
parent.getNodeId(),
AccessControlObserver.POLICY_REMOVED);
} else if (parent.isNodeType(EntryCollector.NT_REP_ACL)) {
// an ace was removed -> refresh cache for the
// containing access control list upon next access
addModification(
accessControlledIdFromAclNode(parent),
AccessControlObserver.POLICY_MODIFIED);
} /* else:
a) some other child node of an access controlled
node -> not interested.
b) a child node of an ACE. not relevant for this
implementation -> ignore
*/
} else {
log.debug("Cannot process NODE_REMOVED event. Parent {} doesn't exist (anymore).", parentPath);
}
}
private void siftPropertyChanged(String identifier) throws RepositoryException {
try {
// test if the changed prop belongs to an ACE
NodeImpl parent = (NodeImpl) session.getNodeByIdentifier(identifier);
if (parent.isNodeType(EntryCollector.NT_REP_ACE)) {
addModification(
accessControlledIdFromAceNode(parent),
AccessControlObserver.POLICY_MODIFIED);
} /* some other property below an access controlled node
changed -> not interested. (NOTE: rep:ACL doesn't
define any properties. */
} catch (ItemNotFoundException e) {
log.debug("Cannot process PROPERTY_CHANGED event. Node {} doesn't exist (anymore).", identifier);
}
}
private NodeId accessControlledIdFromAclNode(Node aclNode) throws RepositoryException {
return ((NodeImpl) aclNode.getParent()).getNodeId();
}
private NodeId accessControlledIdFromAceNode(Node aceNode) throws RepositoryException {
return accessControlledIdFromAclNode(aceNode.getParent());
}
private void addModification(NodeId accessControllNodeId, int modType) {
if (modMap.containsKey(accessControllNodeId)) {
// update modMap
modType |= modMap.get(accessControllNodeId);
}
modMap.put(accessControllNodeId, modType);
}
}
/**
* Listening to any kind of move events in the hierarchy. Since ac content
* is associated with individual nodes the caches need to be informed about
* any kind of move as well even if the target node is not access control
* content s.str.
*/
private class MoveListener implements SynchronousEventListener {
public void onEvent(EventIterator events) {
// NOTE: simplified event handling as all listeners just clear
// the cache in case of any move event. therefore there is currently
// no need to process all events and using the rootID as marker.
while (events.hasNext()) {
Event event = events.nextEvent();
if (event.getType() == Event.NODE_MOVED) {
Map<NodeId, Integer> m = Collections.singletonMap(rootID, AccessControlObserver.MOVE);
AccessControlModifications<NodeId> mods = new AccessControlModifications<NodeId>(m);
notifyListeners(mods);
break;
} //else: illegal event-type: should never occur. ignore
}
}
}
//--------------------------------------------------------------------------
/**
* Inner class combining a list of access control entries with the information
* where to start looking for inherited entries.
*
* Thus <code>nextId</code> either points to the parent of the access
* controlled node associated with <code>aces</code> or to the next
* access controlled ancestor. It is <code>null</code> if the root node has
* been reached and there is no additional ancestor to retrieve access control
* entries from.
*/
static class Entries {
private final List<Entry> aces;
private NodeId nextId;
Entries(List<Entry> aces, NodeId nextId) {
this.aces = aces;
this.nextId = nextId;
}
List<Entry> getACEs() {
return aces;
}
NodeId getNextId() {
return nextId;
}
void setNextId(NodeId nextId) {
this.nextId = nextId;
}
boolean isEmpty() {
return aces.isEmpty();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("size = ").append(aces.size()).append(", ");
sb.append("nextNodeId = ").append(nextId);
return sb.toString();
}
}
}