/*!
* Copyright 2010 - 2016 Pentaho Corporation. All rights reserved.
*
* Licensed 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.io.InputStream;
import java.security.Principal;
import java.security.acl.Group;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.jcr.RepositoryException;
import javax.jcr.security.AccessControlEntry;
import javax.jcr.security.Privilege;
import javax.jcr.version.VersionHistory;
import org.apache.commons.lang.ArrayUtils;
import org.apache.jackrabbit.api.JackrabbitWorkspace;
import org.apache.jackrabbit.api.security.JackrabbitAccessControlManager;
import org.apache.jackrabbit.core.NodeImpl;
import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.security.authorization.PrivilegeBits;
import org.apache.jackrabbit.core.security.authorization.PrivilegeManagerImpl;
import org.pentaho.platform.api.engine.IAuthorizationPolicy;
import org.pentaho.platform.api.engine.IPentahoSession;
import org.pentaho.platform.api.engine.ObjectFactoryException;
import org.pentaho.platform.api.mt.ITenant;
import org.pentaho.platform.engine.core.system.PentahoSessionHolder;
import org.pentaho.platform.engine.core.system.PentahoSystem;
import org.pentaho.platform.engine.security.SecurityHelper;
import org.pentaho.platform.repository2.unified.jcr.IAclMetadataStrategy.AclMetadata;
import org.pentaho.platform.repository2.unified.jcr.JcrRepositoryFileAclUtils;
import org.pentaho.platform.repository2.unified.jcr.JcrTenantUtils;
import org.pentaho.platform.security.policy.rolebased.IRoleAuthorizationPolicyRoleBindingDao;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.Assert;
/**
* Copy-and-paste of {@code org.apache.jackrabbit.core.security.authorization.acl.EntryCollector} in Jackrabbit 2.4.0.
* This class is in {@code org.apache.jackrabbit.core.security.authorization.acl} package due to the scope of
* collaborating classes.
* <p/>
* <p> Changes to original: </p> <ul> <li>{@code Entries} always have {@code null} {@code nextId}.</li> <li>{@code
* collectEntries()} copied from {@code EntryCollector} uses {@code entries.getNextId()} instead of {@code
* node.getParentId()}</li> <li>{@code filterEntries()} copied from {@code EntryCollector} as it was {@code static} and
* {@code private}.</li> <li>No caching is done in the presence of dynamic ACEs. This may need to be revisited but due
* to the short lifetime of the way we use Sessions, it may be acceptable.</li> <li>Understands {@code
* AclMetadataPrincipal}.</li> <li>Adds {@code MagicPrincipal}s on the fly.</li> <li>If access decision on
* versionStorage, then find the associated file node and use that ACL.</li>
* <p/>
* </ul>
*
* @author mlowery
*/
public class PentahoEntryCollector extends EntryCollector {
/**
* logger instance
*/
private static final Logger log = LoggerFactory.getLogger( PentahoEntryCollector.class );
private List<MagicAceDefinition> magicAceDefinitions = new ArrayList<MagicAceDefinition>();
@SuppressWarnings( "rawtypes" )
public PentahoEntryCollector( final SessionImpl systemSession, final NodeId rootID, final Map configuration )
throws RepositoryException {
super( systemSession, rootID );
ClassLoader loader = this.getClass().getClassLoader();
InputStream yamlFileInputStream = loader.getResourceAsStream( "jcr/config.yaml" );
magicAceDefinitions = MagicAceDefinition.parseYamlMagicAceDefinitions( yamlFileInputStream, systemSession );
}
/**
* Find the ancestor (maybe the node itself) that is access-controlled.
*/
protected NodeImpl findAccessControlledNode( final NodeImpl node ) throws RepositoryException {
NodeImpl currentNode = node;
// skip all nodes that are not access-controlled; might eventually hit root which is always access-controlled
while ( !ACLProvider.isAccessControlled( currentNode ) ) {
currentNode = (NodeImpl) currentNode.getParent();
}
return currentNode;
}
/**
* Find the ancestor (maybe the node itself) that is not inheriting ACEs.
*/
protected NodeImpl findNonInheritingNode( final NodeImpl node ) throws RepositoryException {
NodeImpl currentNode = node;
ACLTemplate acl;
while ( true ) {
currentNode = findAccessControlledNode( currentNode );
NodeImpl aclNode = currentNode.getNode( N_POLICY );
String path = aclNode != null ? aclNode.getParent().getPath() : null;
acl = new ACLTemplate( aclNode, path, false /* allowUnknownPrincipals */ );
// skip all nodes that are inheriting
AclMetadata aclMetadata = JcrRepositoryFileAclUtils.getAclMetadata( systemSession, currentNode.getPath(), acl );
if ( aclMetadata != null && aclMetadata.isEntriesInheriting() ) {
currentNode = (NodeImpl) currentNode.getParent();
continue;
}
break;
}
return currentNode;
}
/**
* Returns an {@code Entries} for the given node. This is where most of the customization lives.
*/
@Override
protected PentahoEntries getEntries( final NodeImpl node ) throws RepositoryException {
// find nearest node with an ACL that is not inheriting ACEs
NodeImpl currentNode = node;
ACLTemplate acl;
// version history governed by ACL on "versionable" which could be the root if no version history exists for
// file;
// if we do hit the root, then you get jcr:read for everyone which is acceptable
if ( currentNode.getPath().startsWith( "/jcr:system/jcr:versionStorage" ) ) { //$NON-NLS-1$
currentNode = getVersionable( currentNode );
}
// find first access-controlled node
currentNode = findAccessControlledNode( currentNode );
acl = new ACLTemplate( currentNode.getNode( N_POLICY ), currentNode.getPath(), false /* allowUnknownPrincipals */ );
// owner comes from the first access-controlled node
String owner = null;
AclMetadata aclMetadata = JcrRepositoryFileAclUtils.getAclMetadata( systemSession, currentNode.getPath(), acl );
if ( aclMetadata != null ) {
owner = aclMetadata.getOwner();
}
// find the ACL
NodeImpl firstAccessControlledNode = currentNode;
currentNode = findNonInheritingNode( currentNode );
acl = new ACLTemplate( currentNode.getNode( N_POLICY ), currentNode.getPath(), false /* allowUnknownPrincipals */ );
// If we're inheriting from another node, check to see if that node has removeChildNodes or addChildNodes
// permissions. This needs to transform to become addChild removeChild
if ( !currentNode.isSame( node ) ) {
Privilege removeNodePrivilege =
systemSession.getAccessControlManager().privilegeFromName( Privilege.JCR_REMOVE_NODE );
Privilege removeChildNodesPrivilege =
systemSession.getAccessControlManager().privilegeFromName( Privilege.JCR_REMOVE_CHILD_NODES );
for ( AccessControlEntry entry : acl.getEntries() ) {
Privilege[] expandedPrivileges = JcrRepositoryFileAclUtils.expandPrivileges( entry.getPrivileges(), false );
if ( ArrayUtils.contains( expandedPrivileges, removeChildNodesPrivilege )
&& !ArrayUtils.contains( expandedPrivileges, removeNodePrivilege ) ) {
if ( !acl.addAccessControlEntry( entry.getPrincipal(), new Privilege[] { removeNodePrivilege } ) ) {
// we can never fail to add this entry because it means we may be giving more permission than the above
// two
throw new RuntimeException();
}
break;
}
}
}
// find first ancestor that is not inheriting; its ACEs will be used if the ACL is not inheriting
ACLTemplate ancestorAcl = null;
if ( firstAccessControlledNode.isSame( currentNode ) && !rootID.equals( currentNode.getNodeId() ) ) {
NodeImpl ancestorNode = findNonInheritingNode( (NodeImpl) currentNode.getParent() );
ancestorAcl = new ACLTemplate( ancestorNode.getNode( N_POLICY ), ancestorNode.getPath(),
false /* allowUnknownPrincipals */ );
}
// now acl points to the nearest ancestor that is access-controlled and is not inheriting;
// ancestorAcl points to first ancestor of ACL that is access-controlled and is not inheriting--possibly null
// owner is an owner string--possibly null
return new PentahoEntries( getAcesIncludingMagicAces( currentNode.getPath(), owner, ancestorAcl, acl ), null );
}
/**
* Incoming node is in versionStorage. Find its associated versionable--the node associated with this version history
* node.
*/
protected NodeImpl getVersionable( final NodeImpl node ) throws RepositoryException {
NodeImpl currentNode = node;
while ( !currentNode.isNodeType( "nt:versionHistory" ) && !rootID
.equals( currentNode.getNodeId() ) ) { //$NON-NLS-1$
currentNode = (NodeImpl) currentNode.getParent();
}
if ( rootID.equals( currentNode.getNodeId() ) ) {
return currentNode;
} else {
return (NodeImpl) systemSession.getNodeByIdentifier( ( (VersionHistory) currentNode )
.getVersionableIdentifier() );
}
}
/**
* {@link IAuthorizationPolicy} is used in magic ACE definitions.
*/
protected IAuthorizationPolicy getAuthorizationPolicy() {
IAuthorizationPolicy authorizationPolicy = PentahoSystem.get( IAuthorizationPolicy.class );
if ( authorizationPolicy == null ) {
throw new IllegalStateException();
}
return authorizationPolicy;
}
protected IRoleAuthorizationPolicyRoleBindingDao getRoleBindingDao() {
return PentahoSystem.get( IRoleAuthorizationPolicyRoleBindingDao.class );
}
/**
* Extracts ACEs including magic aces. Magic ACEs are added for (1) the owner, (2) as a result of magic ACE
* definitions, and (3) as a result of ancestor ACL contributions.
* <p/>
* <p> Modifications to these ACLs are not persisted. </p>
*/
@SuppressWarnings( "unchecked" )
protected List<PentahoEntry> getAcesIncludingMagicAces( final String path, final String owner,
final ACLTemplate ancestorAcl, final ACLTemplate acl )
throws RepositoryException {
if ( PentahoSessionHolder.getSession() == null || PentahoSessionHolder.getSession().getId() == null
|| PentahoSessionHolder.getSession().getId().trim().equals( "" ) ) { //$NON-NLS-1$
if ( log.isDebugEnabled() ) {
log.debug( "no PentahoSession so no magic ACEs" ); //$NON-NLS-1$
}
return Collections.emptyList();
}
if ( owner != null ) {
addOwnerAce( owner, acl );
}
boolean match = false;
IRoleAuthorizationPolicyRoleBindingDao roleBindingDao = null;
try {
roleBindingDao =
PentahoSystem.getObjectFactory().get( IRoleAuthorizationPolicyRoleBindingDao.class,
"roleAuthorizationPolicyRoleBindingDaoTarget", PentahoSessionHolder.getSession() );
} catch ( ObjectFactoryException e ) {
e.printStackTrace();
}
ITenant tenant = JcrTenantUtils.getTenant();
for ( final MagicAceDefinition def : magicAceDefinitions ) {
match = false;
String substitutedPath = MessageFormat.format( def.path, tenant.getRootFolderAbsolutePath() );
if ( isAllowed( roleBindingDao, def.logicalRole ) ) {
if ( def.applyToTarget ) {
match = path.equals( substitutedPath );
}
if ( !match && def.applyToChildren ) {
match = path.startsWith( substitutedPath + "/" );
// check to see if we should exclude the match due to the exclude list
if ( match && def.exceptChildren != null ) {
for ( String childPath : def.exceptChildren ) {
String substitutedChildPath = MessageFormat.format( childPath, tenant.getRootFolderAbsolutePath() );
if ( path.startsWith( substitutedChildPath + "/" ) ) {
match = false;
break;
}
}
}
}
if ( !match && def.applyToAncestors ) {
match = substitutedPath.startsWith( path + "/" );
}
}
if ( match ) {
Principal principal =
new MagicPrincipal( JcrTenantUtils.getTenantedUser( PentahoSessionHolder.getSession().getName() ) );
// unfortunately, we need the ACLTemplate because it alone can create ACEs that can be cast successfully
// later;
// changed never persisted
acl.addAccessControlEntry( principal, def.privileges );
}
}
@SuppressWarnings( "rawtypes" )
List acEntries = new ArrayList();
acEntries.addAll( buildPentahoEntries( acl ) ); // leaf ACEs go first so ACL metadata ACE stays first
acEntries.addAll( getRelevantAncestorAces( ancestorAcl ) );
return acEntries;
}
/**
* Selects (and modifies) ACEs containing JCR_ADD_CHILD_NODES or JCR_REMOVE_CHILD_NODES privileges from the given
* ACL.
* <p/>
* <p> Modifications to this ACL are not persisted. ACEs must be created in the given ACL because the path embedded in
* the given ACL plays into authorization decisions using parentPrivs. </p>
*/
@SuppressWarnings( "unchecked" )
protected List<PentahoEntry> getRelevantAncestorAces( final ACLTemplate ancestorAcl )
throws RepositoryException {
if ( ancestorAcl == null ) {
return Collections.emptyList();
}
NodeImpl ancestorNode = (NodeImpl) systemSession.getNode( ancestorAcl.getPath() );
PentahoEntries fullEntriesIncludingMagicACEs = this.getEntries( ancestorNode );
JackrabbitAccessControlManager acMgr = (JackrabbitAccessControlManager) systemSession.getAccessControlManager();
PrivilegeManagerImpl privMrg =
(PrivilegeManagerImpl) ( ( (JackrabbitWorkspace) systemSession.getWorkspace() ).getPrivilegeManager() );
Privilege addChildNodesPrivilege = acMgr.privilegeFromName( Privilege.JCR_ADD_CHILD_NODES );
PrivilegeBits addChildNodesPrivilegeBits = privMrg.getBits( addChildNodesPrivilege );
Privilege removeChildNodesPrivilege = acMgr.privilegeFromName( Privilege.JCR_REMOVE_CHILD_NODES );
PrivilegeBits removeChildNodesPrivilegeBits = privMrg.getBits( removeChildNodesPrivilege );
for ( PentahoEntry entry : (List<PentahoEntry>) fullEntriesIncludingMagicACEs.getACEs() ) {
List<Privilege> privs = new ArrayList<Privilege>( 2 );
if ( entry.getPrivilegeBits().includes( addChildNodesPrivilegeBits ) ) {
privs.add( addChildNodesPrivilege );
}
if ( entry.getPrivilegeBits().includes( removeChildNodesPrivilegeBits ) ) {
privs.add( removeChildNodesPrivilege );
}
// remove all physical entries from the ACL. MagicAces will not be present in the ACL Entries, so we check
// before trying to remove
AccessControlEntry[] ancestorACEs = ancestorAcl.getEntries().toArray( new AccessControlEntry[] {} );
for ( AccessControlEntry ace : ancestorACEs ) {
PentahoEntry pe = buildPentahoEntry( ancestorNode.getNodeId(), ancestorAcl.getPath(), ace );
if ( entry.equals( pe ) ) {
ancestorAcl.removeAccessControlEntry( ace );
}
}
// remove existing ACE since (1) it doesn't have the privs we're looking for and (2) the following
// addAccessControlEntry will silently fail to add a new ACE if perms already exist
if ( !privs.isEmpty() ) {
// create new ACE with same principal but only privs relevant to child operations
// clone to new list to allow concurrent modification
List<AccessControlEntry> entries = new LinkedList<AccessControlEntry>( ancestorAcl.getEntries() );
for ( AccessControlEntry ace : entries ) {
if ( ace.getPrincipal().getName().equals( entry.getPrincipalName() ) ) {
ancestorAcl.removeAccessControlEntry( ace );
}
}
if ( !ancestorAcl.addAccessControlEntry( entry.isGroupEntry() ? new MagicGroup( entry.getPrincipalName() )
: new MagicPrincipal( entry.getPrincipalName() ), privs.toArray( new Privilege[privs.size()] ) ) ) {
// we can never fail to add this entry because it means we may be giving more permission than the above two
throw new RuntimeException();
}
}
}
return buildPentahoEntries( ancestorAcl );
}
/**
* Creates an ACE that gives full access to the owner.
* <p/>
* <p> Modifications to this ACL are not persisted. </p>
*/
protected void addOwnerAce( final String owner, final ACLTemplate acl ) throws RepositoryException {
Principal ownerPrincipal = systemSession.getPrincipalManager().getPrincipal( owner );
if ( ownerPrincipal != null ) {
Principal magicPrincipal = null;
if ( ownerPrincipal instanceof Group ) {
magicPrincipal = new MagicGroup( JcrTenantUtils.getTenantedUser( ownerPrincipal.getName() ) );
} else {
magicPrincipal = new MagicPrincipal( JcrTenantUtils.getTenantedUser( ownerPrincipal.getName() ) );
}
// unfortunately, we need the ACLTemplate because it alone can create ACEs that can be cast successfully
// later;
// changed never persisted
acl.addAccessControlEntry( magicPrincipal, new Privilege[] { systemSession.getAccessControlManager()
.privilegeFromName( "jcr:all" ) } ); //$NON-NLS-1$
} else {
// if the Principal doesn't exist anymore, then there's no reason to add an ACE for it
if ( log.isDebugEnabled() ) {
log.debug( "PrincipalManager cannot find owner=" + owner ); //$NON-NLS-1$
}
}
}
/**
* Overridden since {@code collectEntries()} from {@code EntryCollector} called {@code node.getParentId()} instead of
* {@code entries.getNextId()}.
*/
@SuppressWarnings( { "unchecked", "rawtypes" } )
@Override
protected List collectEntries( NodeImpl node, EntryFilter filter ) throws RepositoryException {
LinkedList userAces = new LinkedList();
LinkedList groupAces = new LinkedList();
if ( node == null ) {
// repository level permissions
NodeImpl root = (NodeImpl) systemSession.getRootNode();
if ( ACLProvider.isRepoAccessControlled( root ) ) {
NodeImpl aclNode = root.getNode( N_REPO_POLICY );
String path = aclNode != null ? aclNode.getParent().getPath() : null;
if ( filter instanceof PentahoEntryFilter ) {
filterEntries( filter, PentahoEntry.readEntries( aclNode, path ), userAces, groupAces );
} else {
filterEntries( filter, Entry.readEntries( aclNode, path ), userAces, groupAces );
}
}
} else {
Entries entries = getEntries( node );
filterEntries( filter, entries.getACEs(), userAces, groupAces );
NodeId next = entries.getNextId();
while ( next != null ) {
entries = getEntries( next );
filterEntries( filter, entries.getACEs(), userAces, groupAces );
next = entries.getNextId();
}
}
List entries = new ArrayList( userAces.size() + groupAces.size() );
entries.addAll( userAces );
entries.addAll( groupAces );
return entries;
}
/**
* Copied from {@link EntryCollector} since that method was {@code private}.
*/
@SuppressWarnings( { "rawtypes", "unchecked" } )
protected void filterEntries( EntryFilter filter, List aces, LinkedList userAces, LinkedList groupAces ) {
if ( !aces.isEmpty() && filter != null ) {
filter.filterEntries( aces, userAces, groupAces );
}
}
protected List<String> getRuntimeRoleNames() {
IPentahoSession pentahoSession = PentahoSessionHolder.getSession();
List<String> runtimeRoles = new ArrayList<String>();
Assert.state( pentahoSession != null );
Authentication authentication = SecurityHelper.getInstance().getAuthentication();
if ( authentication != null ) {
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for ( GrantedAuthority auth : authorities ) {
runtimeRoles.add( auth.getAuthority() );
}
}
return runtimeRoles;
}
protected boolean isAllowed( IRoleAuthorizationPolicyRoleBindingDao roleBindingDao, String logicalRoleName )
throws RepositoryException {
return roleBindingDao.getBoundLogicalRoleNames( systemSession, getRuntimeRoleNames() ).contains(
logicalRoleName );
}
private List<PentahoEntry> buildPentahoEntries( ACLTemplate acl ) throws RepositoryException {
List<PentahoEntry> aces = new ArrayList<PentahoEntry>();
if ( acl != null && acl.getEntries() != null && acl.getEntries().size() > 0 ) {
NodeImpl aclNode = ( (NodeImpl) systemSession.getNode( acl.getPath() ) );
for ( AccessControlEntry ace : acl.getEntries() ) {
aces.add( buildPentahoEntry( aclNode.getNodeId(), acl.getPath(), ace ) );
}
}
return aces;
}
private PentahoEntry buildPentahoEntry( NodeId nodeId, String path, AccessControlEntry ace )
throws RepositoryException {
PentahoEntry entry = null;
if ( ace != null ) {
Principal principal = ace.getPrincipal();
boolean isGroupEntry = principal instanceof Group;
PrivilegeBits bits = ( (ACLTemplate.Entry) ace ).getPrivilegeBits();
boolean isAllow = ( (ACLTemplate.Entry) ace ).isAllow();
entry = new PentahoEntry( nodeId, principal.getName(), isGroupEntry, bits, isAllow, path,
( (ACLTemplate.Entry) ace ).getRestrictions() );
}
return entry;
}
static class PentahoEntries extends Entries {
private List<PentahoEntry> aces;
@SuppressWarnings( { "rawtypes", "unchecked" } )
PentahoEntries( List aces, NodeId nextId ) {
super( null, nextId );
this.aces = (List<PentahoEntry>) aces;
}
PentahoEntries( Entries e ) {
super( e.getACEs(), e.getNextId() );
this.aces = new ArrayList<PentahoEntry>();
}
@SuppressWarnings( { "unchecked", "rawtypes" } )
@Override
public List getACEs() {
return this.aces;
}
@Override
public boolean isEmpty() {
return this.aces == null || this.aces.isEmpty();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append( "size = " ).append( getACEs() != null ? getACEs().size() : 0 ).append( ", " );
sb.append( "nextNodeId = " ).append( getNextId() );
return sb.toString();
}
}
}