ubic.gemma.security.authorization.acl.AclAdvice.java Source code

Java tutorial

Introduction

Here is the source code for ubic.gemma.security.authorization.acl.AclAdvice.java

Source

/*
 * The Gemma project
 * 
 * Copyright (c) 2006 University of British Columbia
 * 
 * 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 ubic.gemma.security.authorization.acl;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.hibernate.LazyInitializationException;
import org.hibernate.engine.CascadeStyle;
import org.hibernate.persister.entity.EntityPersister;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.security.acls.domain.BasePermission;
import org.springframework.security.acls.domain.GrantedAuthoritySid;
import org.springframework.security.acls.domain.PrincipalSid;
import org.springframework.security.acls.model.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.GrantedAuthorityImpl;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import ubic.gemma.model.analysis.SingleExperimentAnalysis;
import ubic.gemma.model.common.auditAndSecurity.*;
import ubic.gemma.model.expression.arrayDesign.ArrayDesign;
import ubic.gemma.model.expression.bioAssay.BioAssay;
import ubic.gemma.model.expression.experiment.BioAssaySet;
import ubic.gemma.model.expression.experiment.ExpressionExperiment;
import ubic.gemma.persistence.CrudUtils;
import ubic.gemma.persistence.CrudUtilsImpl;
import ubic.gemma.security.SecurityServiceImpl;
import ubic.gemma.security.audit.AuditAdvice;
import ubic.gemma.util.AuthorityConstants;
import ubic.gemma.util.ReflectionUtil;

import java.beans.PropertyDescriptor;
import java.util.Collection;

/**
 * Adds security controls to newly created objects, and removes them for objects that are deleted. Methods in this
 * interceptor are run for all new objects (to add security if needed) and when objects are deleted. This is not used to
 * modify permissions on existing objects.
 * <p>
 * Implementation Note: For permissions modification to be triggered, the method name must match certain patterns, which
 * include "create", or "remove". These patterns are defined in the {@link AclPointcut}. Other methods that would
 * require changes to permissions will not work without modifying the source code.
 * 
 * @author keshav
 * @author pavlidis
 * @version $Id: AclAdvice.java,v 1.25 2013/03/30 04:19:37 paul Exp $
 * @see ubic.gemma.security.authorization.acl.AclPointcut
 */
@Component
public class AclAdvice {

    private static Log log = LogFactory.getLog(AclAdvice.class);

    @Autowired
    private MutableAclService aclService;

    @Autowired
    private CrudUtils crudUtils;

    private ObjectIdentityRetrievalStrategy objectIdentityRetrievalStrategy = new ValueObjectAwareIdentityRetrievalStrategyImpl();

    /**
     * @param jp
     * @param retValue
     * @throws Throwable
     */
    public void doAclAdvice(JoinPoint jp, Object retValue) throws Throwable {

        final Object[] args = jp.getArgs();
        Signature signature = jp.getSignature();
        final String methodName = signature.getName();

        assert args != null;
        final Object persistentObject = getPersistentObject(retValue, methodName, args);

        if (persistentObject == null)
            return;

        final boolean isUpdate = CrudUtilsImpl.methodIsUpdate(methodName);
        final boolean isDelete = CrudUtilsImpl.methodIsDelete(methodName);

        // Case 1: collection of securables.
        if (Collection.class.isAssignableFrom(persistentObject.getClass())) {
            for (final Object o : (Collection<?>) persistentObject) {
                if (!isEligibleForAcl(o)) {
                    continue; // possibly could return, if we assume collection is homogeneous in type.
                }
                process(o, methodName, isUpdate, isDelete);
            }
        } else {
            // Case 2: single securable
            if (!isEligibleForAcl(persistentObject)) {
                return;
            }
            process(persistentObject, methodName, isUpdate, isDelete);
        }

    }

    public void setAclService(MutableAclService mutableAclService) {
        this.aclService = mutableAclService;
    }

    /**
     * @param crudUtils the crudUtils to set
     */
    public void setCrudUtils(CrudUtils crudUtils) {
        this.crudUtils = crudUtils;
    }

    /**
     * Creates the acl_permission object and the acl_object_identity object.
     * 
     * @param object The domain object.
     * @return true if an ACL was created, false otherwise.
     */
    private AuditableAcl addOrUpdateAcl(Securable object, Acl parentAcl) {

        if (object.getId() == null) {
            log.warn("ACLs cannot be added or updated on non-persistent object: " + object);
            return null;
        }

        ObjectIdentity oi = makeObjectIdentity(object);

        AuditableAcl acl = null;

        boolean exists = false;
        try {
            acl = (AuditableAcl) aclService.readAclById(oi); // throws exception if not found
            exists = true;
        } catch (NotFoundException nfe) {
            acl = (AuditableAcl) aclService.createAcl(oi);
        }

        if (exists) {
            /*
             * Could be findOrCreate, or could be a second pass that will let us fill in parent ACLs for associated
             * objects missed earlier in a persist cycle. E.g. BioMaterial
             */
            try {
                maybeSetParentACL(object, acl, parentAcl);
                return acl;
            } catch (NotFoundException nfe) {
                log.error(nfe, nfe);
            }
        }

        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        if (authentication == null) {
            throw new IllegalStateException("No authentication found in the security context");
        }

        Object p = authentication.getPrincipal();

        if (p == null) {
            throw new IllegalStateException("Principal was null for " + authentication);
        }

        Sid sid = new PrincipalSid(p.toString());

        boolean isAdmin = SecurityServiceImpl.isUserAdmin();

        boolean isRunningAsAdmin = SecurityServiceImpl.isRunningAsAdmin();

        boolean isAnonymous = SecurityServiceImpl.isUserAnonymous();

        boolean objectIsAUser = User.class.isAssignableFrom(object.getClass());

        boolean objectIsAGroup = UserGroup.class.isAssignableFrom(object.getClass());

        /*
         * The only case where we absolutely disallow inheritance is for SecuredNotChild.
         */
        boolean inheritFromParent = parentAcl != null && !SecuredNotChild.class.isAssignableFrom(object.getClass());

        boolean missingParent = parentAcl == null & SecuredChild.class.isAssignableFrom(object.getClass());

        if (missingParent) {
            // This easily happens, it's not a problem as we go back through to recheck objects.
            log.debug("Object should have a parent during ACL setup: " + object);
        }

        acl.setEntriesInheriting(inheritFromParent);

        /*
         * The logic here is: if we're supposed to inherit from the parent, but none is provided (can easily happen), we
         * have to put in ACEs. Same goes if we're not supposed to inherit. Objects which are not supposed to have their
         * own ACLs (SecurableChild)
         */
        if (!inheritFromParent || parentAcl == null) {

            /*
             * All objects must have administration permissions on them.
             */
            if (log.isDebugEnabled())
                log.debug("Making administratable by GROUP_ADMIN: " + oi);
            grant(acl, BasePermission.ADMINISTRATION,
                    new GrantedAuthoritySid(new GrantedAuthorityImpl(AuthorityConstants.ADMIN_GROUP_AUTHORITY)));

            /*
             * Let agent read anything
             */
            if (log.isDebugEnabled())
                log.debug("Making readable by GROUP_AGENT: " + oi);
            grant(acl, BasePermission.READ,
                    new GrantedAuthoritySid(new GrantedAuthorityImpl(AuthorityConstants.AGENT_GROUP_AUTHORITY)));

            /*
             * If admin, and the object is not a user or group, make it readable by anonymous.
             */
            boolean makeAnonymousReadable = isAdmin && !objectIsAUser && !objectIsAGroup;

            if (makeAnonymousReadable) {
                if (log.isDebugEnabled())
                    log.debug("Making readable by IS_AUTHENTICATED_ANONYMOUSLY: " + oi);
                grant(acl, BasePermission.READ, new GrantedAuthoritySid(
                        new GrantedAuthorityImpl(AuthorityConstants.IS_AUTHENTICATED_ANONYMOUSLY)));
            }

            /*
             * Don't add more permissions for the administrator. But whatever it is, the person who created it can
             * read/write it. User will only be anonymous if they are registering (AFAIK)
             */
            if (!isAdmin && !isAnonymous) {

                if (log.isDebugEnabled())
                    log.debug("Giving read/write permissions on " + oi + " to " + sid);
                grant(acl, BasePermission.READ, sid);

                /*
                 * User who created something can edit it.
                 */
                grant(acl, BasePermission.WRITE, sid);

            }
        }

        /*
         * If the object is a user, make sure that user gets permissions even if the current user is not the same! In
         * fact, user creation runs with GROUP_RUN_AS_ADMIN privileges.
         */

        if (objectIsAUser) {
            User u = (User) object;
            if (((PrincipalSid) sid).getPrincipal().equals(u.getUserName())) {
                /*
                 * This case should actually never happen. "we" are the user who is creating this user. We've already
                 * adding the READ/WRITE permissions above.
                 */
                log.warn("Somehow...a user created themselves: " + oi);

            } else {

                if (log.isDebugEnabled())
                    log.debug("New User: given read/write permissions on " + oi + " to " + sid);

                if (isRunningAsAdmin) {
                    /*
                     * Important: we expect this to normally be the case.
                     */
                    sid = new PrincipalSid(u.getUserName());
                }

                /*
                 * See org.springframework.security.acls.domain.AclAuthorizationStrategy.
                 */
                grant(acl, BasePermission.READ, sid);
                grant(acl, BasePermission.WRITE, sid);

            }
        }

        // Treating Analyses as special case. It'll inherit ACL from ExpressionExperiment
        // If aclParent is passed to this method we overwrite it.

        if (SingleExperimentAnalysis.class.isAssignableFrom(object.getClass())) {
            SingleExperimentAnalysis experimentAnalysis = (SingleExperimentAnalysis) object;
            BioAssaySet bioAssaySet = experimentAnalysis.getExperimentAnalyzed();
            ObjectIdentity oi_temp = makeObjectIdentity(bioAssaySet);

            try {
                parentAcl = aclService.readAclById(oi_temp);
            } catch (NotFoundException nfe) {
                // This is possible if making an EESubSet is part of the transaction.
                parentAcl = aclService.createAcl(oi_temp);
            }

            acl.setEntriesInheriting(true);
            acl.setParent(parentAcl);
            // Owner of the experiment owns analyses even if administrator ran them.
            sid = parentAcl.getOwner();
        }

        acl.setOwner(sid); // this might be the 'user' now.

        assert !acl.equals(parentAcl);

        if (parentAcl != null && inheritFromParent) {
            if (log.isTraceEnabled())
                log.trace("Setting parent to: " + parentAcl + " <--- " + acl);
            acl.setParent(parentAcl);
        }

        return (AuditableAcl) aclService.updateAcl(acl);

    }

    /**
     * Check for special cases of objects that don't need to be examined.
     * 
     * @param object
     * @return
     */
    private boolean canSkipAclCheck(Object object) {
        return AuditTrail.class.isAssignableFrom(object.getClass());
    }

    /**
     * Check if the association may be skipped.
     * 
     * @param object
     * @param propertyName
     * @return
     */
    private boolean canSkipAssociationCheck(Object object, String propertyName) {

        /*
         * If this is an expression experiment, don't go down the data vectors - it has no securable associations and
         * would be expensive to traverse.F
         */
        if (ExpressionExperiment.class.isAssignableFrom(object.getClass())
                && (propertyName.equals("rawExpressionDataVectors")
                        || propertyName.equals("processedExpressionDataVectors"))) {
            log.trace("Skipping vectors");
            return true;
        }

        /*
         * Array design has some non (directly) securable associations that would be expensive to load
         */
        if (ArrayDesign.class.isAssignableFrom(object.getClass())
                && (propertyName.equals("compositeSequences") || propertyName.equals("reporters"))) {
            log.trace("Skipping probes");
            return true;
        }

        return false;
    }

    /**
     * Determine which ACL is going to be the parent of the associations of the given object.
     * <p>
     * If the object is a SecuredNotChild, then it will be treated as the parent. For example, ArrayDesigns associated
     * with an Experiment has 'parent status' for securables associated with the AD, such as LocalFiles.
     * 
     * @param object
     * @param previousParent
     * @return
     */
    private Acl chooseParentForAssociations(Object object, Acl previousParent) {
        Acl parentAcl;
        if (SecuredNotChild.class.isAssignableFrom(object.getClass())
                || (previousParent == null && Securable.class.isAssignableFrom(object.getClass())
                        && !SecuredChild.class.isAssignableFrom(object.getClass()))) {

            parentAcl = getAcl((Securable) object);
        } else {
            /*
             * Keep the previous parent. This means we 'pass through' and the parent is basically going to be the
             * top-most object: there isn't a hierarchy of parenthood. This also means that the parent might be kept as
             * null.
             */
            parentAcl = previousParent;

        }
        return parentAcl;
    }

    /**
     * Delete acl permissions for an object.
     * 
     * @param object
     * @throws IllegalArgumentException
     * @throws DataAccessException
     */
    private void deleteAcl(Securable object) throws DataAccessException, IllegalArgumentException {
        ObjectIdentity oi = makeObjectIdentity(object);

        if (oi == null) {
            log.warn("Null object identity for : " + object);
        }

        if (log.isDebugEnabled()) {
            log.debug("Deleting ACL for " + object);
        }

        /*
         * This deletes children with the second parameter = true.
         */
        this.aclService.deleteAcl(oi, true);
    }

    /**
     * @param retValue
     * @param m
     * @param args
     * @return
     */
    private Object getPersistentObject(Object retValue, String methodName, Object[] args) {
        if (CrudUtilsImpl.methodIsDelete(methodName) || CrudUtilsImpl.methodIsUpdate(methodName)) {

            /*
             * Only deal with single-argument update methods.
             */
            if (args.length > 1)
                return null;

            assert args.length > 0;
            return args[0];
        }
        return retValue;
    }

    /**
     * Add ACE granting permission to sid to ACL (does not persist the change, you have to call update!)
     * 
     * @param acl which object
     * @param permission which permission
     * @param sid which principal
     */
    private void grant(AuditableAcl acl, Permission permission, Sid sid) {
        acl.insertAce(acl.getEntries().size(), permission, sid, true);

        /*
         * This is a problem if the object is created by a regular user. Only admins can set auditing on objects.
         */

        // acl.updateAuditing( acl.getEntries().size() - 1, true, true );
    }

    /**
     * @param class1
     * @return
     */
    private boolean isEligibleForAcl(Object c) {

        if (c == null)
            return false;

        if (Securable.class.isAssignableFrom(c.getClass())) {
            return true;
        }

        return false;
    }

    /**
     * Forms the object identity to be inserted in acl_object_identity table. Note that this does not add an
     * ObjectIdentity to the database; it just calls 'new'.
     * 
     * @param object A persistent object
     * @return object identity.
     */
    private ObjectIdentity makeObjectIdentity(Securable object) {

        assert object.getId() != null : "Object checked for ACLs before it has an ID: " + object;

        return objectIdentityRetrievalStrategy.getObjectIdentity(object);
    }

    /**
     * When setting the parent, we check to see if we can delete the ACEs on the 'child', if any. This is because we
     * want permissions to be managed by the parent. Check that the ACEs on the child are exactly equivalent to the ones
     * on the parent.
     * 
     * @param parentAcl -- careful with the order!
     * @param object
     * @param acl
     * @param true if ACEs were cleared.
     */
    private boolean maybeClearACEsOnChild(Securable object, MutableAcl childAcl, Acl parentAcl) {
        int aceCount = childAcl.getEntries().size();

        if (aceCount == 0) {

            if (parentAcl.getEntries().size() == 0) {
                throw new IllegalStateException("Either the child or the parent has to have ACEs");
            }
            return false;
        }

        if (parentAcl.getEntries().size() == aceCount) {

            boolean oktoClearACEs = true;

            // check for exact match of all ACEs
            for (AccessControlEntry ace : parentAcl.getEntries()) {
                boolean found = false;
                for (AccessControlEntry childAce : childAcl.getEntries()) {
                    if (childAce.getPermission().equals(ace.getPermission())
                            && childAce.getSid().equals(ace.getSid())) {
                        found = true;
                        break;
                    }
                }

                if (!found) {
                    oktoClearACEs = false;
                    break;
                }
            }

            if (oktoClearACEs) {
                if (log.isTraceEnabled())
                    log.trace("Erasing ACEs from child " + object);

                while (childAcl.getEntries().size() > 0) {
                    childAcl.deleteAce(0);
                }

                return true;
            }

        }
        return false;
    }

    /**
     * This is used when rechecking objects that are detached from a parent. Typically these are {@link SecuredChild}ren
     * like BioAssays.
     * <p>
     * Be careful with the argument order!
     * 
     * @param object
     * @param acl - the potential child
     * @param parentAcl - the potential parent
     * @return the parentAcl (can be null)
     */
    private Acl maybeSetParentACL(final Securable object, MutableAcl childAcl, final Acl parentAcl) {
        if (parentAcl != null && !SecuredNotChild.class.isAssignableFrom(object.getClass())) {

            Acl currentParentAcl = childAcl.getParentAcl();

            if (currentParentAcl != null && !currentParentAcl.equals(parentAcl)) {
                throw new IllegalStateException("Cannot change parentAcl once it has ben set: Current parent: "
                        + currentParentAcl + " != Proposed parent:" + parentAcl);
            }

            boolean changedParentAcl = false;
            if (currentParentAcl == null) {
                childAcl.setParent(parentAcl);
                childAcl.setEntriesInheriting(true);
                changedParentAcl = true;
            }

            boolean clearedACEs = maybeClearACEsOnChild(object, childAcl, parentAcl);

            if (changedParentAcl || clearedACEs) {
                aclService.updateAcl(childAcl);
            }
        }
        return childAcl.getParentAcl();
    }

    /**
     * Do necessary ACL operations on the object.
     * 
     * @param o
     * @param methodName
     * @param isUpdate
     * @param isDelete
     */
    private void process(final Object o, final String methodName, final boolean isUpdate, final boolean isDelete) {
        if (log.isTraceEnabled())
            log.trace("***********  Start ACL *************");

        Securable s = (Securable) o;

        assert s != null;

        if (isUpdate) {
            startUpdate(methodName, s);
        } else if (isDelete) {
            deleteAcl(s);
        } else {
            startCreate(methodName, s);
        }

        if (log.isTraceEnabled())
            log.trace("*========* End ACL *=========*");
    }

    /**
     * Walk the tree of associations and add (or update) acls.
     * 
     * @param methodName method name
     * @param object
     * @param previousParent The parent ACL of the given object (if it is a Securable) or of the last visited Securable.
     * @see AuditAdvice for similar code for Auditing
     */
    @SuppressWarnings("unchecked")
    private void processAssociations(String methodName, Object object, Acl previousParent) {

        if (canSkipAclCheck(object)) {
            return;
        }

        EntityPersister persister = crudUtils.getEntityPersister(object);
        if (persister == null) {
            log.error("No Entity Persister found for " + object.getClass().getName());
            return;
        }
        CascadeStyle[] cascadeStyles = persister.getPropertyCascadeStyles();
        String[] propertyNames = persister.getPropertyNames();

        Acl parentAcl = chooseParentForAssociations(object, previousParent);

        for (int j = 0; j < propertyNames.length; j++) {

            CascadeStyle cs = cascadeStyles[j];
            String propertyName = propertyNames[j];

            // log.warn( propertyName );

            /*
             * The goal here is to avoid following associations that don't need to be checked. Unfortunately, this can
             * be a bit tricky because there are exceptions. This is kind of inelegant, but the alternative is to check
             * _every_ association, which will often not be reachable.
             */
            if (!specialCaseForAssociationFollow(object, propertyName)
                    && (canSkipAssociationCheck(object, propertyName) || !crudUtils.needCascade(methodName, cs))) {
                continue;
            }

            PropertyDescriptor descriptor = BeanUtils.getPropertyDescriptor(object.getClass(), propertyName);

            Object associatedObject = null;
            try {
                associatedObject = ReflectionUtil.getProperty(object, descriptor);
            } catch (Exception e) {
                log.error("Error while processing: " + object.getClass() + " --> " + propertyName);
                throw (new RuntimeException(e));
            }

            if (associatedObject == null)
                continue;

            Class<?> propertyType = descriptor.getPropertyType();

            if (associatedObject instanceof Collection) {
                Collection<Object> associatedObjects = (Collection<Object>) associatedObject;

                try {
                    for (Object object2 : associatedObjects) {

                        if (Securable.class.isAssignableFrom(object2.getClass())) {
                            addOrUpdateAcl((Securable) object2, parentAcl);
                        }
                        processAssociations(methodName, object2, parentAcl);
                    }
                } catch (LazyInitializationException ok) {
                    /*
                     * This is not a problem. If this was reached via a create, the associated objects must not be new
                     * so they should already have acls.
                     */
                    // log.warn( "oops" );
                }

            } else {

                if (Securable.class.isAssignableFrom(propertyType)) {
                    addOrUpdateAcl((Securable) associatedObject, parentAcl);
                }
                processAssociations(methodName, associatedObject, parentAcl);
            }
        }
    }

    /**
     * For cases where don't have a cascade but the other end is securable, so we <em>must</em> check the association.
     * For example, when we persist an EE we also persist any new ADs in the same transaction. Thus the ADs need ACL
     * attention at the same time (via the BioAssays).
     * 
     * @param object we are checking
     * @param property of the object
     * @return true if the association should be followed (even though it might not be based on cascade status)
     * @see AuditAdvice for similar code for Auditing
     */
    private boolean specialCaseForAssociationFollow(Object object, String property) {

        if (BioAssay.class.isAssignableFrom(object.getClass())
                && (property.equals("sampleUsed") || property.equals("arrayDesignUsed"))) {
            return true;
        }

        return false;

    }

    /**
     * @param methodName
     * @param s
     */
    private void startCreate(String methodName, Securable s) {

        /*
         * Note that if the method is findOrCreate, we'll return quickly.
         */

        ObjectIdentity oi = makeObjectIdentity(s);

        if (oi == null) {
            throw new IllegalStateException(
                    "On 'create' methods, object should have a valid objectIdentity available. Method=" + methodName
                            + " on " + s);
        }

        addOrUpdateAcl(s, null);

        processAssociations(methodName, s, null);
    }

    /**
     * Kick off an update. This is executed when we call fooService.update(s). The basic issue is to add permissions for
     * any <em>new</em> associated objects.
     * 
     * @param m the update method
     * @param s the securable being updated.
     */
    private void startUpdate(String m, Securable s) {

        ObjectIdentity oi = makeObjectIdentity(s);

        if (oi == null) {
            throw new IllegalStateException(
                    "On 'update' methods, object should have a valid objectIdentity available. Method=" + m + " on "
                            + s);
        }

        Acl parentAcl = null;
        try {
            Acl acl = aclService.readAclById(oi);
            parentAcl = acl.getParentAcl(); // can be null.

        } catch (NotFoundException nfe) {
            /*
             * Then, this shouldn't be an update.
             */
            log.warn("On 'update' methods, there should be a ACL on the passed object already. Method=" + m + " on "
                    + s);
        }

        addOrUpdateAcl(s, parentAcl);
        processAssociations(m, s, parentAcl);
    }

    private MutableAcl getAcl(Securable s) {
        ObjectIdentity oi = objectIdentityRetrievalStrategy.getObjectIdentity(s);

        try {
            return (MutableAcl) aclService.readAclById(oi);
        } catch (NotFoundException e) {
            return null;
        }
    }

}