/*
* 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 java.security.Principal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.jcr.NamespaceException;
import javax.jcr.NodeIterator;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.jcr.ValueFactory;
import javax.jcr.security.AccessControlEntry;
import javax.jcr.security.AccessControlException;
import javax.jcr.security.Privilege;
import org.apache.jackrabbit.api.JackrabbitWorkspace;
import org.apache.jackrabbit.api.security.JackrabbitAccessControlList;
import org.apache.jackrabbit.api.security.authorization.PrivilegeManager;
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.security.authorization.AbstractACLTemplate;
import org.apache.jackrabbit.core.security.authorization.AccessControlEntryImpl;
import org.apache.jackrabbit.core.security.authorization.PrivilegeBits;
import org.apache.jackrabbit.core.security.authorization.PrivilegeManagerImpl;
import org.apache.jackrabbit.core.security.principal.PrincipalImpl;
import org.apache.jackrabbit.core.security.principal.UnknownPrincipal;
import org.apache.jackrabbit.core.value.InternalValue;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver;
import org.apache.jackrabbit.spi.commons.conversion.NameResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implementation of the
* {@link org.apache.jackrabbit.api.security.JackrabbitAccessControlList}
* interface that is detached from the effective access control content.
* Consequently, any modifications applied to this ACL only take effect, if
* the policy gets
* {@link javax.jcr.security.AccessControlManager#setPolicy(String, javax.jcr.security.AccessControlPolicy) reapplied}
* to the <code>AccessControlManager</code> and the changes are saved.
*/
class ACLTemplate extends AbstractACLTemplate {
private static final Logger log = LoggerFactory.getLogger(ACLTemplate.class);
/**
* List containing the entries of this ACL Template.
*/
private final List<AccessControlEntry> entries = new ArrayList<AccessControlEntry>();
/**
* The principal manager used for validation checks
*/
private final PrincipalManager principalMgr;
/**
* The privilege mgr
*/
private final PrivilegeManagerImpl privilegeMgr;
/**
* The name resolver
*/
private final NameResolver resolver;
/**
* Namespace sensitive name of the REP_GLOB property in standard JCR form.
*/
private final String jcrRepGlob;
/**
* controls if unknown principals can be used in access control entries.
*/
private final boolean allowUnknownPrincipals;
/**
* Construct a new empty {@link ACLTemplate}.
*
* @param path path
* @param privilegeMgr
* @param valueFactory value factory
* @param resolver
* @param principalMgr manager
* @throws javax.jcr.NamespaceException
*/
ACLTemplate(String path, PrincipalManager principalMgr,
PrivilegeManager privilegeMgr, ValueFactory valueFactory,
NamePathResolver resolver, boolean allowUnknownPrincipals) throws NamespaceException {
super(path, valueFactory);
this.principalMgr = principalMgr;
this.privilegeMgr = (PrivilegeManagerImpl) privilegeMgr;
this.resolver = resolver;
this.allowUnknownPrincipals = allowUnknownPrincipals;
jcrRepGlob = resolver.getJCRName(P_GLOB);
}
/**
* Create a {@link ACLTemplate} that is used to edit an existing ACL
* node.
*
* @param aclNode node
* @param path The path as exposed by {@link JackrabbitAccessControlList#getPath()}
* @throws RepositoryException if an error occurs
*/
ACLTemplate(NodeImpl aclNode, String path, boolean allowUnknownPrincipals) throws RepositoryException {
super(path, (aclNode != null) ? aclNode.getSession().getValueFactory() : null);
if (aclNode == null || !NT_REP_ACL.equals(aclNode.getPrimaryNodeTypeName())) {
throw new IllegalArgumentException("Node must be of type 'rep:ACL'");
}
SessionImpl sImpl = (SessionImpl) aclNode.getSession();
principalMgr = sImpl.getPrincipalManager();
privilegeMgr = (PrivilegeManagerImpl) ((JackrabbitWorkspace) sImpl.getWorkspace()).getPrivilegeManager();
this.allowUnknownPrincipals = allowUnknownPrincipals;
this.resolver = sImpl;
jcrRepGlob = sImpl.getJCRName(P_GLOB);
// load the entries:
NodeIterator itr = aclNode.getNodes();
while (itr.hasNext()) {
NodeImpl aceNode = (NodeImpl) itr.nextNode();
try {
String principalName = aceNode.getProperty(P_PRINCIPAL_NAME).getString();
Principal princ = principalMgr.getPrincipal(principalName);
if (princ == null) {
log.debug("Principal with name " + principalName + " unknown to PrincipalManager.");
princ = new PrincipalImpl(principalName);
}
InternalValue[] privValues = aceNode.getProperty(P_PRIVILEGES).internalGetValues();
Name[] privNames = new Name[privValues.length];
for (int i = 0; i < privValues.length; i++) {
privNames[i] = privValues[i].getName();
}
Map<String,Value> restrictions = null;
if (aceNode.hasProperty(P_GLOB)) {
restrictions = Collections.singletonMap(jcrRepGlob, aceNode.getProperty(P_GLOB).getValue());
}
// create a new ACEImpl (omitting validation check)
boolean isAllow = NT_REP_GRANT_ACE.equals(aceNode.getPrimaryNodeTypeName());
Entry ace = new Entry(princ, privilegeMgr.getBits(privNames), isAllow, restrictions);
// add the entry omitting any validation.
entries.add(ace);
} catch (RepositoryException e) {
log.debug("Failed to build ACE from content. {}", e.getMessage());
}
}
}
/**
* Create a new entry omitting any validation checks.
*
* @param principal
* @param privileges
* @param isAllow
* @param restrictions
* @return A new entry
*/
Entry createEntry(Principal principal, Privilege[] privileges, boolean isAllow, Map<String,Value> restrictions) throws RepositoryException {
return new Entry(principal, privileges, isAllow, restrictions);
}
Entry createEntry(Entry base, Privilege[] newPrivileges, boolean isAllow) throws RepositoryException {
return new Entry(base, newPrivileges, isAllow);
}
private List<Entry> internalGetEntries(Principal principal) {
String principalName = principal.getName();
List<Entry> entriesPerPrincipal = new ArrayList<Entry>(2);
for (AccessControlEntry entry : entries) {
if (principalName.equals(entry.getPrincipal().getName())) {
entriesPerPrincipal.add((Entry) entry);
}
}
return entriesPerPrincipal;
}
private synchronized boolean internalAdd(Entry entry) throws RepositoryException {
Principal principal = entry.getPrincipal();
List<Entry> entriesPerPrincipal = internalGetEntries(principal);
if (entriesPerPrincipal.isEmpty()) {
// simple case: just add the new entry at the end of the list.
entries.add(entry);
return true;
} else {
if (entriesPerPrincipal.contains(entry)) {
// the same entry is already contained -> no modification
return false;
}
// check if need to adjust existing entries
int updateIndex = -1;
Entry complementEntry = null;
for (Entry e : entriesPerPrincipal) {
if (equalRestriction(entry, e)) {
if (entry.isAllow() == e.isAllow()) {
// need to update an existing entry
if (e.getPrivilegeBits().includes(entry.getPrivilegeBits())) {
// all privileges to be granted/denied are already present
// in the existing entry -> not modified
return false;
}
// remember the index of the existing entry to be updated later on.
updateIndex = entries.indexOf(e);
// remove the existing entry and create a new one that
// includes both the new privileges and the existing ones.
entries.remove(e);
PrivilegeBits mergedBits = PrivilegeBits.getInstance(e.getPrivilegeBits());
mergedBits.add(entry.getPrivilegeBits());
// omit validation check.
entry = new Entry(entry, mergedBits, entry.isAllow());
} else {
complementEntry = e;
}
}
}
// make sure, that the complement entry (if existing) does not
// grant/deny the same privileges -> remove privileges that are now
// denied/granted.
if (complementEntry != null) {
PrivilegeBits complPrivs = complementEntry.getPrivilegeBits();
PrivilegeBits diff = PrivilegeBits.getInstance(complPrivs);
diff.diff(entry.getPrivilegeBits());
if (diff.isEmpty()) {
// remove the complement entry as the new entry covers
// all privileges granted by the existing entry.
entries.remove(complementEntry);
updateIndex--;
} else if (!diff.equals(complPrivs)) {
// replace the existing entry having the privileges adjusted
int index = entries.indexOf(complementEntry);
entries.remove(complementEntry);
// combine set of new builtin and custom privileges
// and create a new entry.
Entry tmpl = new Entry(entry, diff, !entry.isAllow());
entries.add(index, tmpl);
} /* else: does not need to be modified.*/
}
// finally update the existing entry or add the new entry passed
// to this method at the end.
if (updateIndex < 0) {
entries.add(entry);
} else {
entries.add(updateIndex, entry);
}
return true;
}
}
private boolean equalRestriction(Entry entry1, Entry entry2) throws RepositoryException {
Value v1 = entry1.getRestriction(jcrRepGlob);
Value v2 = entry2.getRestriction(jcrRepGlob);
return (v1 == null) ? v2 == null : v1.equals(v2);
}
//------------------------------------------------< AbstractACLTemplate >---
/**
* @see AbstractACLTemplate#checkValidEntry(java.security.Principal, javax.jcr.security.Privilege[], boolean, java.util.Map)
*/
@Override
protected void checkValidEntry(Principal principal, Privilege[] privileges,
boolean isAllow, Map<String, Value> restrictions)
throws AccessControlException {
// validate principal
if (principal instanceof UnknownPrincipal) {
log.debug("Consider fallback principal as valid: {}", principal.getName());
} else if (!principalMgr.hasPrincipal(principal.getName())) {
if (!allowUnknownPrincipals) {
throw new AccessControlException("Principal " + principal.getName() + " does not exist.");
}
log.debug("Consider fallback principal as valid: {}", principal.getName());
}
if (path == null && restrictions != null && !restrictions.isEmpty()) {
throw new AccessControlException("Repository level policy does not support restrictions.");
}
}
/**
* @see org.apache.jackrabbit.core.security.authorization.AbstractACLTemplate#getEntries()
*/
@Override
protected List<AccessControlEntry> getEntries() {
return entries;
}
//--------------------------------------------------< AccessControlList >---
/**
* @see javax.jcr.security.AccessControlList#removeAccessControlEntry(AccessControlEntry)
*/
public synchronized void removeAccessControlEntry(AccessControlEntry ace)
throws AccessControlException, RepositoryException {
if (!(ace instanceof Entry)) {
throw new AccessControlException("Invalid AccessControlEntry implementation " + ace.getClass().getName() + ".");
}
if (entries.contains(ace)) {
entries.remove(ace);
} else {
throw new AccessControlException("AccessControlEntry " + ace + " cannot be removed from ACL defined at " + getPath());
}
}
//----------------------------------------< JackrabbitAccessControlList >---
/**
* @see JackrabbitAccessControlList#getRestrictionNames()
*/
public String[] getRestrictionNames() {
return (path == null) ? new String[0] : new String[] {jcrRepGlob};
}
/**
* @see JackrabbitAccessControlList#getRestrictionType(String)
*/
public int getRestrictionType(String restrictionName) {
if (jcrRepGlob.equals(restrictionName) || P_GLOB.toString().equals(restrictionName)) {
return PropertyType.STRING;
} else {
return PropertyType.UNDEFINED;
}
}
/**
* The only known restriction is:
* <pre>
* rep:glob (optional) value-type: STRING
* </pre>
*
* @see org.apache.jackrabbit.api.security.JackrabbitAccessControlList#addEntry(Principal, Privilege[], boolean, Map)
*/
public boolean addEntry(Principal principal, Privilege[] privileges,
boolean isAllow, Map<String, Value> restrictions)
throws AccessControlException, RepositoryException {
checkValidEntry(principal, privileges, isAllow, restrictions);
Entry ace = createEntry(principal, privileges, isAllow, restrictions);
return internalAdd(ace);
}
//-------------------------------------------------------------< Object >---
/**
* Returns zero to satisfy the Object equals/hashCode contract.
* This class is mutable and not meant to be used as a hash key.
*
* @return always zero
* @see Object#hashCode()
*/
@Override
public int hashCode() {
return 0;
}
/**
* Returns true if the path and the entries are equal; false otherwise.
*
* @param obj Object to be tested.
* @return true if the path and the entries are equal; false otherwise.
* @see Object#equals(Object)
*/
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj instanceof ACLTemplate) {
ACLTemplate acl = (ACLTemplate) obj;
return path.equals(acl.path) && entries.equals(acl.entries);
}
return false;
}
//--------------------------------------------------------------------------
/**
*
*/
class Entry extends AccessControlEntryImpl {
private Entry(Principal principal, PrivilegeBits privilegeBits, boolean allow, Map<String,Value> restrictions)
throws RepositoryException {
super(principal, privilegeBits, allow, restrictions);
}
private Entry(Principal principal, Privilege[] privileges, boolean allow, Map<String,Value> restrictions)
throws RepositoryException {
super(principal, privileges, allow, restrictions);
}
private Entry(Entry base, PrivilegeBits newPrivilegeBits, boolean isAllow) throws RepositoryException {
super(base, newPrivilegeBits, isAllow);
}
private Entry(Entry base, Privilege[] newPrivileges, boolean isAllow) throws RepositoryException {
super(base, newPrivileges, isAllow);
}
@Override
protected NameResolver getResolver() {
return resolver;
}
@Override
protected ValueFactory getValueFactory() {
return valueFactory;
}
@Override
protected PrivilegeManagerImpl getPrivilegeManager() {
return privilegeMgr;
}
}
}