/*
* 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.xml;
import static org.apache.jackrabbit.core.security.authorization.AccessControlConstants.NT_REP_ACCESS_CONTROL;
import static org.apache.jackrabbit.core.security.authorization.AccessControlConstants.NT_REP_PRINCIPAL_ACCESS_CONTROL;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import javax.jcr.AccessDeniedException;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.Value;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.security.AccessControlEntry;
import javax.jcr.security.AccessControlManager;
import javax.jcr.security.AccessControlPolicy;
import javax.jcr.security.Privilege;
import org.apache.jackrabbit.api.JackrabbitSession;
import org.apache.jackrabbit.api.security.JackrabbitAccessControlList;
import org.apache.jackrabbit.api.security.JackrabbitAccessControlManager;
import org.apache.jackrabbit.core.NodeImpl;
import org.apache.jackrabbit.core.security.principal.PrincipalImpl;
import org.apache.jackrabbit.core.util.ReferenceChangeTracker;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.security.authorization.AccessControlConstants;
import org.apache.jackrabbit.core.security.principal.UnknownPrincipal;
import org.apache.jackrabbit.core.state.NodeState;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <code>AccessControlImporter</code> implements a
* <code>ProtectedNodeImporter</code> that is able to deal with access control
* content as defined by the default ac related node types present with
* jackrabbit-core.
*/
public class AccessControlImporter extends DefaultProtectedNodeImporter {
/**
* logger instance
*/
private static final Logger log = LoggerFactory.getLogger(AccessControlImporter.class);
private static final int STATUS_UNDEFINED = 0;
private static final int STATUS_AC_FOLDER = 1;
private static final int STATUS_PRINCIPAL_AC = 2;
private static final int STATUS_ACL = 3;
private static final int STATUS_ACE = 4;
private static final Set<Name> ACE_NODETYPES = new HashSet<Name>(2);
static {
ACE_NODETYPES.add(AccessControlConstants.NT_REP_DENY_ACE);
ACE_NODETYPES.add(AccessControlConstants.NT_REP_GRANT_ACE);
}
private final Stack<Integer> prevStatus = new Stack<Integer>();
private AccessControlManager acMgr;
private int status = STATUS_UNDEFINED;
private NodeImpl parent = null;
private boolean principalbased = false;
private boolean initialized = false;
// keep best-effort for backward compatibility reasons
private ImportBehavior importBehavior = ImportBehavior.BEST_EFFORT;
/**
* the ACL for non-principal based
*/
private JackrabbitAccessControlList acl = null;
@Override
public boolean init(JackrabbitSession session, NamePathResolver resolver,
boolean isWorkspaceImport, int uuidBehavior,
ReferenceChangeTracker referenceTracker) {
if (super.init(session, resolver, isWorkspaceImport, uuidBehavior, referenceTracker)) {
if (initialized) {
throw new IllegalStateException("Already initialized");
}
try {
acMgr = session.getAccessControlManager();
initialized = true;
} catch (RepositoryException e) {
// initialization failed. ac-import not possible
}
}
return initialized;
}
@Override
public boolean start(NodeImpl protectedParent) throws RepositoryException {
if (!initialized) {
throw new IllegalStateException("Not initialized");
}
if (isStarted()) {
// only ok if same parent
if (!protectedParent.isSame(parent)) {
throw new IllegalStateException();
}
return true;
}
if (isWorkspaceImport) {
log.debug("AccessControlImporter may not be used with the WorkspaceImporter");
return false;
}
if (!protectedParent.getDefinition().isProtected()) {
log.debug("AccessControlImporter may not be started with a non-protected parent.");
return false;
}
if (isPolicyNode(protectedParent)) {
String parentPath = protectedParent.getParent().getPath();
acl = getACL(parentPath);
if (acl == null) {
log.warn("AccessControlImporter cannot be started: no ACL for {}.", parentPath);
return false;
}
status = STATUS_ACL;
} else if (isRepoPolicyNode(protectedParent)) {
acl = getACL(null);
if (acl == null) {
log.warn("AccessControlImporter cannot be started: no Repo ACL.");
return false;
}
status = STATUS_ACL;
} else if (protectedParent.isNodeType(AccessControlConstants.NT_REP_ACCESS_CONTROL)) {
status = STATUS_AC_FOLDER;
principalbased = true;
acl = null;
} // else: nothing this importer can deal with.
if (isStarted()) {
parent = protectedParent;
return true;
} else {
return false;
}
}
private JackrabbitAccessControlList getACL(String path) throws RepositoryException, AccessDeniedException {
JackrabbitAccessControlList acl = null;
for (AccessControlPolicy p: acMgr.getPolicies(path)) {
if (p instanceof JackrabbitAccessControlList) {
acl = (JackrabbitAccessControlList) p;
break;
}
}
if (acl != null) {
// clear all existing entries
for (AccessControlEntry ace: acl.getAccessControlEntries()) {
acl.removeAccessControlEntry(ace);
}
}
return acl;
}
@Override
public boolean start(NodeState protectedParent) throws IllegalStateException, RepositoryException {
if (isStarted()) {
throw new IllegalStateException();
}
if (isWorkspaceImport) {
log.debug("AccessControlImporter may not be used with the WorkspaceImporter");
return false;
}
return false;
}
@Override
public void end(NodeImpl protectedParent) throws RepositoryException {
if (!isStarted()) {
return;
}
if (!principalbased) {
checkStatus(STATUS_ACL, "STATUS_ACL expected.");
acMgr.setPolicy(acl.getPath(), acl);
} else {
checkStatus(STATUS_AC_FOLDER, "STATUS_AC_FOLDER expected.");
if (!prevStatus.isEmpty()) {
throw new ConstraintViolationException("Incomplete protected item tree: "+ prevStatus.size()+ " calls to 'endChildInfo' missing.");
}
}
reset();
}
@Override
public void end(NodeState protectedParent) throws IllegalStateException, ConstraintViolationException, RepositoryException {
// nothing to do. will never get here.
}
@Override
public void startChildInfo(NodeInfo childInfo, List<PropInfo> propInfos) throws RepositoryException {
if (!isStarted()) {
return;
}
Name ntName = childInfo.getNodeTypeName();
int previousStatus = status;
if (!principalbased) {
checkStatus(STATUS_ACL, "Cannot handle childInfo " + childInfo + "; rep:ACL may only contain a single level of child nodes representing the ACEs");
addACE(childInfo, propInfos);
status = STATUS_ACE;
} else {
switch (status) {
case STATUS_AC_FOLDER:
if (NT_REP_ACCESS_CONTROL.equals(ntName)) {
// yet another intermediate node -> keep status
status = STATUS_AC_FOLDER;
} else if (NT_REP_PRINCIPAL_ACCESS_CONTROL.equals(ntName)) {
// the start of a principal-based acl
status = STATUS_PRINCIPAL_AC;
} else {
// illegal node type -> throw exception
throw new ConstraintViolationException("Unexpected node type " + ntName + ". Should be rep:AccessControl or rep:PrincipalAccessControl.");
}
checkIdMixins(childInfo);
break;
case STATUS_PRINCIPAL_AC:
if (NT_REP_ACCESS_CONTROL.equals(ntName)) {
// some intermediate path between principal paths.
status = STATUS_AC_FOLDER;
} else if (NT_REP_PRINCIPAL_ACCESS_CONTROL.equals(ntName)) {
// principal-based ac node underneath another one -> keep status
status = STATUS_PRINCIPAL_AC;
} else {
// the start the acl definition itself
checkDefinition(childInfo, AccessControlConstants.N_POLICY, AccessControlConstants.NT_REP_ACL);
status = STATUS_ACL;
}
checkIdMixins(childInfo);
break;
case STATUS_ACL:
// nodeinfo must define an ACE
addACE(childInfo, propInfos);
status = STATUS_ACE;
break;
default:
throw new ConstraintViolationException("Cannot handle childInfo " + childInfo + "; unexpected status " + status + " .");
}
}
prevStatus.push(previousStatus);
}
@Override
public void endChildInfo() throws RepositoryException {
if (!isStarted()) {
return;
}
// if the protected imported is started at an existing protected node
// SessionImporter does not remember it on the stack of parents node.
if (!principalbased) {
// childInfo + props have already been handled
// -> assert valid status
// -> no further actions required.
checkStatus(STATUS_ACE, "Upon completion of a NodeInfo the status must be STATUS_ACE.");
}
// reset the status
status = prevStatus.pop();
}
private boolean isStarted() {
return status > STATUS_UNDEFINED;
}
private void reset() {
status = STATUS_UNDEFINED;
parent = null;
acl = null;
}
private void checkStatus(int expectedStatus, String message) throws ConstraintViolationException {
if (status != expectedStatus) {
throw new ConstraintViolationException(message);
}
}
private static boolean isPolicyNode(NodeImpl node) throws RepositoryException {
Name nodeName = node.getQName();
return AccessControlConstants.N_POLICY.equals(nodeName) && node.isNodeType(AccessControlConstants.NT_REP_ACL);
}
/**
* @param node The node to be tested.
* @return <code>true</code> if the specified node is the 'rep:repoPolicy'
* acl node underneath the root node; <code>false</code> otherwise.
* @throws RepositoryException If an error occurs.
*/
private static boolean isRepoPolicyNode(NodeImpl node) throws RepositoryException {
Name nodeName = node.getQName();
return AccessControlConstants.N_REPO_POLICY.equals(nodeName) &&
node.isNodeType(AccessControlConstants.NT_REP_ACL) &&
node.getDepth() == 1;
}
private static void checkDefinition(NodeInfo nInfo, Name expName, Name expNodeTypeName) throws ConstraintViolationException {
if (expName != null && !expName.equals(nInfo.getName())) {
// illegal name
throw new ConstraintViolationException("Unexpected Node name "+ nInfo.getName() +". Node name should be " + expName + ".");
}
if (expNodeTypeName != null && !expNodeTypeName.equals(nInfo.getNodeTypeName())) {
// illegal name
throw new ConstraintViolationException("Unexpected node type " + nInfo.getNodeTypeName() + ". Node type should be " + expNodeTypeName + ".");
}
}
private static void checkIdMixins(NodeInfo nInfo) throws ConstraintViolationException {
// neither explicit id NOR mixin types may be present.
Name[] mixins = nInfo.getMixinNames();
NodeId id = nInfo.getId();
if (id != null || mixins != null) {
throw new ConstraintViolationException("The node represented by NodeInfo " + nInfo + " may neither be referenceable nor have mixin types.");
}
}
private void addACE(NodeInfo childInfo, List<PropInfo> propInfos) throws RepositoryException, UnsupportedRepositoryOperationException {
// node type may only be rep:GrantACE or rep:DenyACE
Name ntName = childInfo.getNodeTypeName();
if (!ACE_NODETYPES.contains(ntName)) {
throw new ConstraintViolationException("Cannot handle childInfo " + childInfo + "; expected a valid, applicable rep:ACE node definition.");
}
checkIdMixins(childInfo);
boolean isAllow = AccessControlConstants.NT_REP_GRANT_ACE.equals(ntName);
Principal principal = null;
Privilege[] privileges = null;
Map<String, TextValue> restrictions = new HashMap<String, TextValue>();
for (PropInfo pInfo : propInfos) {
Name name = pInfo.getName();
if (AccessControlConstants.P_PRINCIPAL_NAME.equals(name)) {
Value[] values = pInfo.getValues(PropertyType.STRING, resolver);
if (values == null || values.length != 1) {
throw new ConstraintViolationException("");
}
String pName = values[0].getString();
principal = session.getPrincipalManager().getPrincipal(pName);
if (principal == null) {
if (importBehavior == ImportBehavior.BEST_EFFORT) {
// create "fake" principal that is always accepted in ACLTemplate.checkValidEntry()
principal = new UnknownPrincipal(pName);
} else {
// create "fake" principal. this is checked again in ACLTemplate.checkValidEntry()
principal = new PrincipalImpl(pName);
}
}
} else if (AccessControlConstants.P_PRIVILEGES.equals(name)) {
Value[] values = pInfo.getValues(PropertyType.NAME, resolver);
privileges = new Privilege[values.length];
for (int i = 0; i < values.length; i++) {
privileges[i] = acMgr.privilegeFromName(values[i].getString());
}
} else {
TextValue[] txtVls = pInfo.getTextValues();
for (TextValue txtV : txtVls) {
restrictions.put(resolver.getJCRName(name), txtV);
}
}
}
if (principalbased) {
// try to access policies
List<AccessControlPolicy> policies = new ArrayList<AccessControlPolicy>();
if (acMgr instanceof JackrabbitAccessControlManager) {
JackrabbitAccessControlManager jacMgr = (JackrabbitAccessControlManager) acMgr;
policies.addAll(Arrays.asList(jacMgr.getPolicies(principal)));
policies.addAll(Arrays.asList(jacMgr.getApplicablePolicies(principal)));
}
for (AccessControlPolicy policy : policies) {
if (policy instanceof JackrabbitAccessControlList) {
JackrabbitAccessControlList acl = (JackrabbitAccessControlList) policy;
Map<String, Value> restr = new HashMap<String, Value>();
for (String restName : acl.getRestrictionNames()) {
TextValue txtVal = restrictions.remove(restName);
if (txtVal != null) {
restr.put(restName, txtVal.getValue(acl.getRestrictionType(restName), resolver));
}
}
if (!restrictions.isEmpty()) {
throw new ConstraintViolationException("ACE childInfo contained restrictions that could not be applied.");
}
acl.addEntry(principal, privileges, isAllow, restr);
acMgr.setPolicy(acl.getPath(), acl);
return;
}
}
} else {
Map<String, Value> restr = new HashMap<String, Value>();
for (String restName : acl.getRestrictionNames()) {
TextValue txtVal = restrictions.remove(restName);
if (txtVal != null) {
restr.put(restName, txtVal.getValue(acl.getRestrictionType(restName), resolver));
}
}
if (!restrictions.isEmpty()) {
throw new ConstraintViolationException("ACE childInfo contained restrictions that could not be applied.");
}
acl.addEntry(principal, privileges, isAllow, restr);
return;
}
// could not apply the ACE. No suitable ACL found.
throw new ConstraintViolationException("Cannot handle childInfo " + childInfo + "; No policy found to apply the ACE.");
}
//---------------------------------------------------------< BeanConfig >---
/**
* @return human readable representation of the <code>importBehavior</code> value.
*/
public String getImportBehavior() {
return importBehavior.getString();
}
/**
*
* @param importBehaviorStr
*/
public void setImportBehavior(String importBehaviorStr) {
this.importBehavior = ImportBehavior.fromString(importBehaviorStr);
}
public static enum ImportBehavior {
/**
* Default behavior that does not try to prevent errors or incompatibilities between the content
* and the ACL manager (eg. does not try to fix missing principals)
*/
DEFAULT("default"),
/**
* Tries to minimize errors by adapting the content and bypassing validation checks (e.g. allows adding
* ACEs with missing principals, even if ACL manager would not allow this).
*/
BEST_EFFORT("bestEffort");
private final String value;
ImportBehavior(String value) {
this.value = value;
}
public static ImportBehavior fromString(String str) {
if (str.equals("bestEffort")) {
return BEST_EFFORT;
} else {
return ImportBehavior.valueOf(str.toUpperCase());
}
}
public String getString() {
return value;
}
}
}