/*
* 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.api.security.principal.PrincipalManager;
import org.apache.jackrabbit.core.NodeImpl;
import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.nodetype.NodeTypeImpl;
import org.apache.jackrabbit.core.security.SecurityConstants;
import org.apache.jackrabbit.core.security.authorization.AbstractAccessControlProvider;
import org.apache.jackrabbit.core.security.authorization.AccessControlConstants;
import org.apache.jackrabbit.core.security.authorization.AccessControlEditor;
import org.apache.jackrabbit.core.security.authorization.CompiledPermissions;
import org.apache.jackrabbit.core.security.authorization.Permission;
import org.apache.jackrabbit.core.security.authorization.UnmodifiableAccessControlList;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.util.ISO9075;
import org.apache.jackrabbit.util.Text;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.AccessDeniedException;
import javax.jcr.ItemNotFoundException;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import javax.jcr.query.QueryResult;
import javax.jcr.security.AccessControlEntry;
import javax.jcr.security.AccessControlList;
import javax.jcr.security.AccessControlManager;
import javax.jcr.security.AccessControlPolicy;
import javax.jcr.security.Privilege;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* The ACLProvider generates access control policies out of the items stored
* in the workspace applying the following rules:
* <ul>
* <li>A <code>Node</code> is considered <i>access controlled</i> if an ACL has
* been explicitly assigned to it by adding the mixin type
* <code>rep:AccessControllable</code> and adding child node of type
* <code>rep:acl</code> that forms the acl.</li>
* <li>a Property is considered 'access controlled' if its parent Node is.</li>
* <li>An ACL is never assigned to a <code>Property</code> item.</li>
* <li>A <code>Node</code> that is not access controlled may inherit the ACL.
* The ACL is inherited from the closest access controlled ancestor.</li>
* <li>It may be possible that a given <code>Node</code> has no effective ACL, in
* which case some a default policy is returned that grants READ privilege to
* any principal and denies all other privileges.</li>
* <li>an item is considered an <i>ACL item</i> if it is used to define an ACL.
* ACL items inherit the ACL from node they defined the ACL for.</li>
* </ul>
*
* @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider for additional information.
*/
public class ACLProvider extends AbstractAccessControlProvider implements AccessControlConstants {
/**
* the default logger
*/
private static final Logger log = LoggerFactory.getLogger(ACLProvider.class);
/**
* The node id of the root node
*/
private NodeId rootNodeId;
/**
* Cache to ease the retrieval of ACEs defined for a given node. This cache
* is used by the ACLPermissions created individually for each Session
* instance.
*/
private EntryCollector entryCollector;
//----------------------------------------------< AccessControlProvider >---
/**
* @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#init(Session, Map)
*/
@Override
public void init(Session systemSession, Map configuration) throws RepositoryException {
super.init(systemSession, configuration);
// make sure the workspace of the given systemSession has a
// minimal protection on the root node.
NodeImpl root = (NodeImpl) session.getRootNode();
rootNodeId = root.getNodeId();
ACLEditor systemEditor = new ACLEditor(session, this);
// TODO: replace by configurable default policy (see JCR-2331)
boolean initializedWithDefaults = !configuration.containsKey(PARAM_OMIT_DEFAULT_PERMISSIONS);
if (initializedWithDefaults && !isAccessControlled(root)) {
initRootACL(session, systemEditor);
}
entryCollector = createEntryCollector(session);
}
@Override
public void close() {
super.close();
entryCollector.close();
}
/**
* @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#getEffectivePolicies(org.apache.jackrabbit.spi.Path,org.apache.jackrabbit.core.security.authorization.CompiledPermissions)
*/
public AccessControlPolicy[] getEffectivePolicies(Path absPath, CompiledPermissions permissions) throws ItemNotFoundException, RepositoryException {
checkInitialized();
NodeImpl targetNode;
List<AccessControlList> acls = new ArrayList<AccessControlList>();
if (absPath == null) {
targetNode = (NodeImpl) session.getRootNode();
if (isRepoAccessControlled(targetNode)) {
if (permissions.grants(targetNode.getPrimaryPath(), Permission.READ_AC)) {
// retrieve the entries for the access controlled node
List<AccessControlEntry> entries = entryCollector.collectEntries(null, new EntryFilterImpl(null, (NodeId) null, session));
acls.add(new UnmodifiableAccessControlList(entries));
} else {
throw new AccessDeniedException("Access denied at " + targetNode.getPath());
}
}
} else {
targetNode = (NodeImpl) session.getNode(session.getJCRPath(absPath));
NodeImpl node = getNode(targetNode, isAcItem(targetNode));
// collect all ACLs effective at node
collectAcls(node, permissions, acls);
}
// if no effective ACLs are present -> add a default, empty acl.
if (acls.isEmpty()) {
// no access control information can be retrieved for the specified
// node, since neither the node nor any of its parents is access
// controlled. TODO: there should be a default policy in this case (see JCR-2331)
log.warn("No access controlled node present in item hierarchy starting from " + targetNode.getPath());
}
return acls.toArray(new AccessControlList[acls.size()]);
}
/**
* @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#getEffectivePolicies(java.util.Set, CompiledPermissions)
*/
public AccessControlPolicy[] getEffectivePolicies(Set<Principal> principals, CompiledPermissions permissions) throws RepositoryException {
String propName = ISO9075.encode(session.getJCRName(P_PRINCIPAL_NAME));
StringBuilder stmt = new StringBuilder("/jcr:root");
stmt.append("//element(*,");
stmt.append(session.getJCRName(NT_REP_ACE));
stmt.append(")[");
int i = 0;
for (Principal principal : principals) {
if (i > 0) {
stmt.append(" or ");
}
stmt.append("@");
stmt.append(propName);
stmt.append("='");
stmt.append(principal.getName().replaceAll("'", "''"));
stmt.append("'");
i++;
}
stmt.append("]");
QueryResult result;
try {
QueryManager qm = session.getWorkspace().getQueryManager();
Query q = qm.createQuery(stmt.toString(), Query.XPATH);
result = q.execute();
} catch (RepositoryException e) {
log.error("Unexpected error while searching effective policies.", e.getMessage());
throw new UnsupportedOperationException("Retrieve effective policies for set of principals not supported.", e);
}
Set<AccessControlPolicy> acls = new LinkedHashSet<AccessControlPolicy>();
for (NodeIterator it = result.getNodes(); it.hasNext();) {
NodeImpl aclNode = (NodeImpl) it.nextNode().getParent();
Name aclName = aclNode.getQName();
NodeImpl accessControlledNode = (NodeImpl) aclNode.getParent();
if (N_POLICY.equals(aclName) && isAccessControlled(accessControlledNode)) {
if (permissions.canRead(aclNode.getPrimaryPath(), aclNode.getNodeId())) {
List<AccessControlEntry> aces = entryCollector.getEntries(accessControlledNode).getACEs();
acls.add(new UnmodifiableAccessControlList(aces, accessControlledNode.getPath(), Collections.<String, Integer>emptyMap()));
} else {
throw new AccessDeniedException("Access denied at " + Text.getRelativeParent(aclNode.getPath(), 1));
}
} else if (N_REPO_POLICY.equals(aclName) && isRepoAccessControlled(accessControlledNode)) {
if (permissions.canRead(aclNode.getPrimaryPath(), aclNode.getNodeId())) {
List<AccessControlEntry> aces = entryCollector.collectEntries(null, new EntryFilterImpl(null, (NodeId) null, session));
acls.add(new UnmodifiableAccessControlList(aces));
} else {
throw new AccessDeniedException("Access denied at " + Text.getRelativeParent(aclNode.getPath(), 1));
}
} // else: not a regular policy node -> ignore.
}
return acls.toArray(new AccessControlPolicy[acls.size()]);
}
/**
* @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#getEditor(Session)
*/
public AccessControlEditor getEditor(Session session) {
checkInitialized();
return new ACLEditor(session, this);
}
/**
* @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 CompiledPermissionsImpl(principals, session, entryCollector, this, true);
}
}
/**
* @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 {
CompiledPermissions cp = new CompiledPermissionsImpl(principals, session, entryCollector, this, false);
try {
return cp.canRead(null, rootNodeId);
} finally {
cp.close();
}
}
}
//----------------------------------------------------------< protected >---
/**
* Create the <code>EntryCollector</code> instance that is used by this
* provider to gather the effective ACEs for a given list of principals at a
* given node during AC evaluation.
*
* @param systemSession The system session to create the entry collector for.
* @return A new instance of <code>CachingEntryCollector</code>.
* @throws RepositoryException If an error occurs.
*/
protected EntryCollector createEntryCollector(SessionImpl systemSession) throws RepositoryException {
return new CachingEntryCollector(systemSession, rootNodeId);
}
//------------------------------------------------------------< private >---
/**
* Recursively collects all ACLs that are effective on the specified node.
*
* @param node the Node to collect the ACLs for, which must NOT be part of the
* structure defined by mix:AccessControllable.
* @param permissions
* @param acls List used to collect the effective acls.
* @throws RepositoryException if an error occurs
*/
private void collectAcls(NodeImpl node, CompiledPermissions permissions, List<AccessControlList> acls) throws RepositoryException {
// if the given node is access-controlled, construct a new ACL and add
// it to the list
if (isAccessControlled(node)) {
if (permissions.grants(node.getPrimaryPath(), Permission.READ_AC)) {
// retrieve the entries for the access controlled node
List<AccessControlEntry> aces = entryCollector.getEntries(node).getACEs();
acls.add(new UnmodifiableAccessControlList(aces, node.getPath(), Collections.<String, Integer>emptyMap()));
} else {
throw new AccessDeniedException("Access denied at " + node.getPath());
}
}
// then, recursively look for access controlled parents up the hierarchy.
if (!rootNodeId.equals(node.getId())) {
NodeImpl parentNode = (NodeImpl) node.getParent();
collectAcls(parentNode, permissions, acls);
}
}
/**
* Set-up minimal permissions for the workspace:
*
* <ul>
* <li>'adminstrators' principal -> all privileges</li>
* <li>'everyone' -> read privilege</li>
* </ul>
*
* @param session to the workspace to set-up initial ACL to
* @param editor for the specified session.
* @throws RepositoryException If an error occurs.
*/
private static void initRootACL(SessionImpl session, AccessControlEditor editor) throws RepositoryException {
try {
log.debug("Install initial ACL:...");
String rootPath = session.getRootNode().getPath();
AccessControlPolicy[] acls = editor.editAccessControlPolicies(rootPath);
if (acls.length > 0) {
ACLTemplate acl = (ACLTemplate) acls[0];
PrincipalManager pMgr = session.getPrincipalManager();
AccessControlManager acMgr = session.getAccessControlManager();
String pName = SecurityConstants.ADMINISTRATORS_NAME;
if (pMgr.hasPrincipal(pName)) {
Principal administrators = pMgr.getPrincipal(pName);
log.debug("... Privilege.ALL for administrators.");
Privilege[] privs = new Privilege[]{acMgr.privilegeFromName(Privilege.JCR_ALL)};
acl.addAccessControlEntry(administrators, privs);
} else {
log.info("Administrators principal group is missing -> omitting initialization of default permissions.");
}
Principal everyone = pMgr.getEveryone();
log.debug("... Privilege.READ for everyone.");
Privilege[] privs = new Privilege[]{acMgr.privilegeFromName(Privilege.JCR_READ)};
acl.addAccessControlEntry(everyone, privs);
editor.setPolicy(rootPath, acl);
session.save();
} else {
log.info("No applicable ACL available for the root node -> skip initialization of the root node's ACL.");
}
} catch (RepositoryException e) {
log.error("Failed to set-up minimal access control for root node of workspace " + session.getWorkspace().getName());
session.getRootNode().refresh(false);
}
}
/**
* Test if the given node is access controlled. The node is access
* controlled if it is of node type
* {@link AccessControlConstants#NT_REP_ACCESS_CONTROLLABLE "rep:AccessControllable"}
* and if it has a child node named
* {@link AccessControlConstants#N_POLICY}.
*
* @param node the node to be tested
* @return <code>true</code> if the node is access controlled and has a
* rep:policy child; <code>false</code> otherwise.
* @throws RepositoryException if an error occurs
*/
static boolean isAccessControlled(NodeImpl node) throws RepositoryException {
return node.hasNode(N_POLICY) && node.isNodeType(NT_REP_ACCESS_CONTROLLABLE);
}
/**
* Test if the given node is access controlled. The node is access
* controlled if it is of node type
* {@link AccessControlConstants#NT_REP_REPO_ACCESS_CONTROLLABLE "rep:RepoAccessControllable"}
* and if it has a child node named
* {@link AccessControlConstants#N_REPO_POLICY}.
*
* @param node the node to be tested
* @return <code>true</code> if the node is access controlled and has a
* rep:policy child; <code>false</code> otherwise.
* @throws RepositoryException if an error occurs
*/
static boolean isRepoAccessControlled(NodeImpl node) throws RepositoryException {
return node.hasNode(N_REPO_POLICY) && node.isNodeType(NT_REP_REPO_ACCESS_CONTROLLABLE);
}
/**
* Returns the given <code>targetNode</code> unless the node itself stores
* access control information in which case it's nearest non-ac-parent is
* searched and returned.
*
* @param targetNode The node for which AC information needs to be retrieved.
* @param isAcItem true if the specified target node defines access control
* content; false otherwise.
* @return the given <code>targetNode</code> or the nearest non-ac-parent
* in case the <code>targetNode</code> itself defines access control content.
* @throws RepositoryException if an error occurs
*/
static NodeImpl getNode(NodeImpl targetNode, boolean isAcItem) throws RepositoryException {
NodeImpl node;
if (isAcItem) {
Name ntName = ((NodeTypeImpl) targetNode.getPrimaryNodeType()).getQName();
if (ntName.equals(NT_REP_ACL)) {
node = (NodeImpl) targetNode.getParent();
} else if (ntName.equals(NT_REP_GRANT_ACE) || ntName.equals(NT_REP_DENY_ACE)) {
node = (NodeImpl) targetNode.getParent().getParent();
} else {
// target node already points to the nearest existing ancestor of the ac-item
node = targetNode;
}
} else {
node = targetNode;
}
return node;
}
}