/*
* 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.principalbased;
import org.apache.commons.collections.map.LRUMap;
import org.apache.jackrabbit.core.NodeImpl;
import org.apache.jackrabbit.core.SessionImpl;
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.util.Text;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.RepositoryException;
import javax.jcr.observation.Event;
import javax.jcr.observation.EventIterator;
import javax.jcr.observation.ObservationManager;
import javax.jcr.security.AccessControlEntry;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* <code>EntriesCache</code>...
*/
class EntriesCache extends AccessControlObserver implements AccessControlConstants {
/**
* logger instance
*/
private static final Logger log = LoggerFactory.getLogger(EntriesCache.class);
private final SessionImpl systemSession;
private final ACLEditor systemEditor;
private final String repPolicyName;
/**
* Cache to look up the list of access control entries defined for a given
* set of principals.
*/
private final Map<Object, List<AccessControlEntry>> cache;
private final Object monitor = new Object();
/**
*
* @param systemSession
* @param systemEditor
* @param accessControlRootPath
* @throws javax.jcr.RepositoryException
*/
EntriesCache(SessionImpl systemSession, ACLEditor systemEditor,
String accessControlRootPath) throws RepositoryException {
this.systemSession = systemSession;
this.systemEditor = systemEditor;
repPolicyName = systemSession.getJCRName(N_POLICY);
cache = new LRUMap(1000);
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)
};
observationMgr.addEventListener(this, events, accessControlRootPath, true, null, ntNames, false);
}
/**
* 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 {
systemSession.getWorkspace().getObservationManager().removeEventListener(this);
} catch (RepositoryException e) {
log.error("Unexpected error while closing CachingEntryCollector", e);
}
}
List<AccessControlEntry> getEntries(Collection<Principal> principals) throws RepositoryException {
String key = getCacheKey(principals);
List<AccessControlEntry> entries;
synchronized (monitor) {
entries = cache.get(key);
if (entries == null) {
// acNodes must be ordered in the same order as the principals
// in order to obtain proper acl-evaluation in case the given
// principal-set is ordered.
entries = new ArrayList<AccessControlEntry>();
// build acl-hierarchy assuming that principal-order determines
// the acl-inheritance.
for (Principal p : principals) {
ACLTemplate acl = systemEditor.getACL(p);
if (acl != null && !acl.isEmpty()) {
AccessControlEntry[] aces = acl.getAccessControlEntries();
entries.addAll(Arrays.asList(aces));
}
}
cache.put(key, entries);
}
}
return entries;
}
private static String getCacheKey(Collection<Principal> principals) {
StringBuilder sb = new StringBuilder();
for (Principal p : principals) {
sb.append(p.getName()).append('/');
}
return sb.toString();
}
//------------------------------------------------------< EventListener >---
/**
* @see javax.jcr.observation.EventListener#onEvent(javax.jcr.observation.EventIterator)
*/
public synchronized void onEvent(EventIterator events) {
/* map of path-string to modification type.
the path identifies the access-controlled node for a given principal.*/
Map<String,Integer> modMap = new HashMap<String,Integer>();
// collect the paths of all access controlled nodes that have been affected
// by the events and thus need their cache entries updated or cleared.
while (events.hasNext()) {
try {
Event ev = events.nextEvent();
String identifier = ev.getIdentifier();
String path = ev.getPath();
switch (ev.getType()) {
case Event.NODE_ADDED:
NodeImpl n = (NodeImpl) systemSession.getNodeByIdentifier(identifier);
if (n.isNodeType(NT_REP_ACL)) {
// a new ACL was added -> use the added node to update
// the cache.
modMap.put(Text.getRelativeParent(path, 1), POLICY_ADDED);
} else if (n.isNodeType(NT_REP_ACE)) {
// a new ACE was added -> use the parent node (acl)
// to update the cache.
modMap.put(Text.getRelativeParent(path, 2), POLICY_MODIFIED);
} /* else: some other node added below an access controlled
parent node -> not interested. */
break;
case Event.NODE_REMOVED:
String parentPath = Text.getRelativeParent(path, 1);
if (systemSession.nodeExists(parentPath)) {
NodeImpl parent = (NodeImpl) systemSession.getNode(parentPath);
if (repPolicyName.equals(Text.getName(path))){
// the complete acl was removed -> clear cache entry
modMap.put(parentPath, POLICY_REMOVED);
} else if (parent.isNodeType(NT_REP_ACL)) {
// an ace was removed -> refresh cache for the
// containing access control list upon next access
modMap.put(Text.getRelativeParent(path, 2), 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 " + parentPath + " doesn't exist (anymore).");
}
break;
case Event.PROPERTY_CHANGED:
// test if the changed prop belongs to an ACE
NodeImpl parent = (NodeImpl) systemSession.getNodeByIdentifier(identifier);
if (parent.isNodeType(NT_REP_ACE)) {
modMap.put(Text.getRelativeParent(path, 3), POLICY_MODIFIED);
} /* some other property below an access controlled node
changed -> not interested. (NOTE: rep:ACL doesn't
define any properties. */
break;
default:
// illegal event-type: should never occur. ignore
}
} catch (RepositoryException e) {
// should never get here
log.warn("Internal error: {}", e.getMessage());
}
}
if (!modMap.isEmpty()) {
// notify listeners and eventually clean up internal caches.
synchronized (monitor) {
cache.clear();
}
notifyListeners(new AccessControlModifications<String>(modMap));
}
}
}