/*
* 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.api.security.principal.PrincipalManager;
import org.apache.jackrabbit.core.NodeImpl;
import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.id.ItemId;
import org.apache.jackrabbit.core.security.SecurityConstants;
import org.apache.jackrabbit.core.security.authorization.AbstractAccessControlProvider;
import org.apache.jackrabbit.core.security.authorization.AbstractCompiledPermissions;
import org.apache.jackrabbit.core.security.authorization.AccessControlConstants;
import org.apache.jackrabbit.core.security.authorization.AccessControlEditor;
import org.apache.jackrabbit.core.security.authorization.AccessControlEntryImpl;
import org.apache.jackrabbit.core.security.authorization.AccessControlListener;
import org.apache.jackrabbit.core.security.authorization.AccessControlModifications;
import org.apache.jackrabbit.core.security.authorization.CompiledPermissions;
import org.apache.jackrabbit.core.security.authorization.Permission;
import org.apache.jackrabbit.core.security.authorization.PrivilegeRegistry;
import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.util.Text;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.ItemNotFoundException;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.jcr.ValueFactory;
import javax.jcr.security.AccessControlEntry;
import javax.jcr.security.AccessControlException;
import javax.jcr.security.AccessControlManager;
import javax.jcr.security.AccessControlPolicy;
import javax.jcr.security.Privilege;
import java.security.Principal;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* <code>CombinedProvider</code>...
*/
public class ACLProvider extends AbstractAccessControlProvider implements AccessControlConstants {
private static Logger log = LoggerFactory.getLogger(ACLProvider.class);
// TODO: add means to show effective-policy to a user.
private static final AccessControlPolicy effectivePolicy = EffectivePrincipalBasedPolicy.getInstance();
private NodeImpl acRoot;
private ACLEditor editor;
private EntriesCache entriesCache;
private int readBits;
//----------------------------------------------< AccessControlProvider >---
/**
* @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#init(javax.jcr.Session, java.util.Map)
*/
@Override
public void init(Session systemSession, Map configuration) throws RepositoryException {
super.init(systemSession, configuration);
NodeImpl root = (NodeImpl) session.getRootNode();
if (root.hasNode(N_ACCESSCONTROL)) {
acRoot = root.getNode(N_ACCESSCONTROL);
if (!acRoot.isNodeType(NT_REP_ACCESS_CONTROL)) {
throw new RepositoryException("Error while initializing Access Control Provider: Found ac-root to be wrong node type " + acRoot.getPrimaryNodeType().getName());
}
} else {
acRoot = root.addNode(N_ACCESSCONTROL, NT_REP_ACCESS_CONTROL, null);
}
editor = new ACLEditor(session, resolver.getQPath(acRoot.getPath()));
entriesCache = new EntriesCache(session, editor, acRoot.getPath());
readBits = PrivilegeRegistry.getBits(new Privilege[] {session.getAccessControlManager().privilegeFromName(Privilege.JCR_READ)});
// TODO: replace by configurable default policy (see JCR-2331)
if (!configuration.containsKey(PARAM_OMIT_DEFAULT_PERMISSIONS)) {
try {
log.debug("Install initial permissions: ...");
ValueFactory vf = session.getValueFactory();
Map<String, Value> restrictions = new HashMap<String, Value>();
restrictions.put(session.getJCRName(ACLTemplate.P_NODE_PATH), vf.createValue(root.getPath(), PropertyType.PATH));
restrictions.put(session.getJCRName(ACLTemplate.P_GLOB), vf.createValue(GlobPattern.WILDCARD_ALL));
PrincipalManager pMgr = session.getPrincipalManager();
AccessControlManager acMgr = session.getAccessControlManager();
// initial default permissions for the administrators group
String pName = SecurityConstants.ADMINISTRATORS_NAME;
if (pMgr.hasPrincipal(pName)) {
Principal administrators = pMgr.getPrincipal(pName);
installDefaultPermissions(administrators,
new Privilege[] {acMgr.privilegeFromName(Privilege.JCR_ALL)},
restrictions, editor);
} else {
log.info("Administrators principal group is missing -> Not adding default permissions.");
}
// initialize default permissions for the everyone group
installDefaultPermissions(pMgr.getEveryone(),
new Privilege[] {acMgr.privilegeFromName(Privilege.JCR_READ)},
restrictions, editor);
session.save();
} catch (RepositoryException e) {
log.error("Failed to set-up minimal access control for root node of workspace " + session.getWorkspace().getName());
session.getRootNode().refresh(false);
}
}
}
private static void installDefaultPermissions(Principal principal,
Privilege[] privs,
Map<String, Value> restrictions,
AccessControlEditor editor)
throws RepositoryException, AccessControlException {
AccessControlPolicy[] acls = editor.editAccessControlPolicies(principal);
if (acls.length > 0) {
ACLTemplate acl = (ACLTemplate) acls[0];
if (acl.isEmpty()) {
acl.addEntry(principal, privs, true, restrictions);
editor.setPolicy(acl.getPath(), acl);
} else {
log.debug("... policy for principal '"+principal.getName()+"' already present.");
}
} else {
log.debug("... policy for principal '"+principal.getName()+"' already present.");
}
}
@Override
public void close() {
super.close();
entriesCache.close();
}
/**
* @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#getEffectivePolicies(Path)
*/
public AccessControlPolicy[] getEffectivePolicies(Path absPath)
throws ItemNotFoundException, RepositoryException {
/*
since the per-node effect of the policies is defined by the
rep:nodePath restriction present with the individual access control
entries, returning the principal-based policy at 'absPath' (which for
most nodes in the repository isn't available anyway) doesn't
provide the desired information.
As tmp. solution some default policy is returned instead.
TODO: add proper evaluation and return a set of ACLs that take effect on the node at abs path
*/
return new AccessControlPolicy[] {effectivePolicy};
}
/**
* @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#getEditor(Session)
*/
public AccessControlEditor getEditor(Session editingSession) {
checkInitialized();
if (editingSession instanceof SessionImpl) {
try {
return new ACLEditor((SessionImpl) editingSession, session.getQPath(acRoot.getPath()));
} catch (RepositoryException e) {
// should never get here
log.error("Internal error: ", e.getMessage());
}
}
log.debug("Unable to build access control editor " + ACLEditor.class.getName() + ".");
return null;
}
/**
* @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#compilePermissions(Set)
*/
public CompiledPermissions compilePermissions(Set<Principal> principals) throws RepositoryException {
checkInitialized();
if (isAdminOrSystem(principals)) {
return getAdminPermissions();
} else if (isReadOnly(principals)) {
return getReadOnlyPermissions();
} else {
return new CompiledPermissionImpl(principals);
}
}
/**
* @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#canAccessRoot(Set)
*/
public boolean canAccessRoot(Set<Principal> principals) throws RepositoryException {
checkInitialized();
if (isAdminOrSystem(principals)) {
return true;
} else {
CompiledPermissionImpl cp = new CompiledPermissionImpl(principals, false);
return cp.canRead(((NodeImpl) session.getRootNode()).getPrimaryPath());
}
}
//-----------------------------------------------------< CompiledPolicy >---
/**
*
*/
private class CompiledPermissionImpl extends AbstractCompiledPermissions
implements AccessControlListener {
private final Set<Principal> principals;
private final Set<String> acPaths;
private List<AccessControlEntry> entries;
private boolean canReadAll;
private final Map<ItemId, Boolean> readCache = new LRUMap(2000);
private final Object monitor = new Object();
/**
* @param principals the underlying principals
* @throws RepositoryException if an error occurs
*/
private CompiledPermissionImpl(Set<Principal> principals) throws RepositoryException {
this(principals, true);
}
/**
* @param principals the underlying principals
* @param listenToEvents if <code>true</code> listens to events
* @throws RepositoryException if an error occurs
*/
private CompiledPermissionImpl(Set<Principal> principals, boolean listenToEvents) throws RepositoryException {
this.principals = principals;
acPaths = new HashSet<String>(principals.size());
reload();
if (listenToEvents) {
/*
Make sure this AclPermission recalculates the permissions if
any ACL concerning it is modified.
*/
entriesCache.addListener(this);
}
}
/**
* @throws RepositoryException if an error occurs
*/
private void reload() throws RepositoryException {
// reload the paths
acPaths.clear();
for (Principal p : principals) {
acPaths.add(editor.getPathToAcNode(p));
}
// and retrieve the entries from the entry-collector.
entries = entriesCache.getEntries(principals);
// in addition: trivial check if read access is deny somewhere
// as as shortcut in #canRead(Path)
canReadAll = canRead(session.getQPath("/"));
if (canReadAll) {
for (AccessControlEntry entry : entries) {
AccessControlEntryImpl ace = (AccessControlEntryImpl) entry;
if (!ace.isAllow() && ((ace.getPrivilegeBits() & readBits) == readBits)) {
// found an ace that defines read deny for a sub tree
// -> canReadAll is false.
canReadAll = false;
break;
}
}
}
}
//------------------------------------< AbstractCompiledPermissions >---
/**
* @see AbstractCompiledPermissions#buildResult(Path)
*/
@Override
protected synchronized Result buildResult(Path absPath) throws RepositoryException {
if (!absPath.isAbsolute()) {
throw new RepositoryException("Absolute path expected.");
}
boolean isAcItem = isAcItem(absPath);
String jcrPath = session.getJCRPath(absPath);
// retrieve principal-based permissions and privileges
return buildResult(jcrPath, isAcItem);
}
/**
* Loop over all entries and evaluate allows/denies for those matching
* the given jcrPath.
*
* @param targetPath Path used for the evaluation; pointing to an
* existing or non-existing item.
* @param isAcItem the item
* @return the result
* @throws RepositoryException if an error occurs
*/
private Result buildResult(String targetPath,
boolean isAcItem) throws RepositoryException {
int allows = Permission.NONE;
int denies = Permission.NONE;
int allowPrivileges = PrivilegeRegistry.NO_PRIVILEGE;
int denyPrivileges = PrivilegeRegistry.NO_PRIVILEGE;
int parentAllows = PrivilegeRegistry.NO_PRIVILEGE;
int parentDenies = PrivilegeRegistry.NO_PRIVILEGE;
String parentPath = Text.getRelativeParent(targetPath, 1);
for (AccessControlEntry entry : entries) {
if (!(entry instanceof ACLTemplate.Entry)) {
log.warn("Unexpected AccessControlEntry instance -> ignore");
continue;
}
ACLTemplate.Entry entr = (ACLTemplate.Entry) entry;
int privs = entr.getPrivilegeBits();
if (!"".equals(parentPath) && entr.matches(parentPath)) {
if (entr.isAllow()) {
parentAllows |= Permission.diff(privs, parentDenies);
} else {
parentDenies |= Permission.diff(privs, parentAllows);
}
}
boolean matches = entr.matches(targetPath);
if (matches) {
if (entr.isAllow()) {
allowPrivileges |= Permission.diff(privs, denyPrivileges);
int permissions = PrivilegeRegistry.calculatePermissions(allowPrivileges, parentAllows, true, isAcItem);
allows |= Permission.diff(permissions, denies);
} else {
denyPrivileges |= Permission.diff(privs, allowPrivileges);
int permissions = PrivilegeRegistry.calculatePermissions(denyPrivileges, parentDenies, false, isAcItem);
denies |= Permission.diff(permissions, allows);
}
}
}
return new Result(allows, denies, allowPrivileges, denyPrivileges);
}
//--------------------------------------------< CompiledPermissions >---
/**
* @see CompiledPermissions#close()
*/
@Override
public void close() {
entriesCache.removeListener(this);
super.close();
}
/**
* @see CompiledPermissions#canRead(Path, ItemId)
*/
public boolean canRead(Path path, ItemId itemId) throws RepositoryException {
boolean canRead;
if (path == null) {
// only itemId: try to avoid expensive resolution from itemID to path
synchronized (monitor) {
if (readCache.containsKey(itemId)) {
// id has been evaluated before -> shortcut
canRead = readCache.get(itemId);
} else {
canRead = canRead(session.getHierarchyManager().getPath(itemId));
readCache.put(itemId, canRead);
return canRead;
}
}
} else {
// path param present:
canRead = canRead(path);
}
return canRead;
}
private boolean canRead(Path path) throws RepositoryException {
// first try if reading non-ac-items was always granted -> no eval
// otherwise evaluate the permissions.
return (canReadAll && !isAcItem(path)) || grants(path, Permission.READ);
}
//------------------------------------------< AccessControlListener >---
/**
* @see AccessControlListener#acModified(org.apache.jackrabbit.core.security.authorization.AccessControlModifications)
*/
public void acModified(AccessControlModifications modifications) {
try {
boolean reload = false;
Iterator keys = modifications.getNodeIdentifiers().iterator();
while (keys.hasNext() && !reload) {
String path = keys.next().toString();
reload = acPaths.contains(path);
}
// eventually reload the ACL and clear the cache
if (reload) {
clearCache();
// reload the ac-path list and the list of aces
reload();
}
} catch (RepositoryException e) {
// should never get here
log.warn("Internal error: ", e.getMessage());
}
}
}
//--------------------------------------------------------------------------
/**
* Dummy effective policy
*/
private static final class EffectivePrincipalBasedPolicy implements AccessControlPolicy {
private static EffectivePrincipalBasedPolicy INSTANCE = new EffectivePrincipalBasedPolicy();
private EffectivePrincipalBasedPolicy() {
}
private static EffectivePrincipalBasedPolicy getInstance() {
return INSTANCE;
}
}
}