ubic.gemma.core.security.audit.AuditAdvice.java Source code

Java tutorial

Introduction

Here is the source code for ubic.gemma.core.security.audit.AuditAdvice.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.core.security.audit;

import gemma.gsec.authentication.UserManager;
import gemma.gsec.model.User;
import gemma.gsec.util.CrudUtils;
import gemma.gsec.util.CrudUtilsImpl;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.hibernate.Hibernate;
import org.hibernate.LazyInitializationException;
import org.hibernate.LockOptions;
import org.hibernate.SessionFactory;
import org.hibernate.classic.Session;
import org.hibernate.engine.CascadeStyle;
import org.hibernate.persister.entity.EntityPersister;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import ubic.gemma.core.security.authorization.acl.AclAdvice;
import ubic.gemma.model.common.AbstractAuditable;
import ubic.gemma.model.common.auditAndSecurity.AuditTrail;
import ubic.gemma.model.expression.arrayDesign.ArrayDesign;
import ubic.gemma.model.expression.experiment.ExpressionExperiment;
import ubic.gemma.persistence.service.common.auditAndSecurity.AuditHelper;
import ubic.gemma.persistence.util.ReflectionUtil;
import ubic.gemma.persistence.util.Settings;

import javax.annotation.PostConstruct;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.NoSuchElementException;

/**
 * Manage audit trails on objects.
 *
 * @author pavlidis
 */
@Component
public class AuditAdvice {

    // Note that we have a special logger configured for this class, so remove events get stored.
    private static final Logger log = LoggerFactory.getLogger(AuditAdvice.class.getName());

    private boolean AUDIT_CREATE = true;

    private boolean AUDIT_DELETE = true;

    private boolean AUDIT_UPDATE = true;

    @Autowired
    private AuditHelper auditHelper;

    @Autowired
    private CrudUtils crudUtils;
    @Autowired
    private SessionFactory sessionFactory;
    @Autowired
    private UserManager userManager;

    /**
     * Entry point. This only takes action if the method involves AbstractAuditables.
     *
     * @param pjp pjp
     * @param retValue return value
     */
    @SuppressWarnings("unused") // entry point
    public void doAuditAdvice(JoinPoint pjp, Object retValue) {

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

        Object object = this.getPersistentObject(retValue, methodName, args);

        if (object == null)
            return;

        User user = userManager.getCurrentUser();

        if (user == null) {
            AuditAdvice.log.info("User could not be determined (anonymous?), audit will be skipped.");
            return;
        }

        if (object instanceof Collection) {
            for (final Object o : (Collection<?>) object) {
                if (AbstractAuditable.class.isAssignableFrom(o.getClass())) {
                    this.process(methodName, (AbstractAuditable) o, user);
                }
            }
        } else if ((AbstractAuditable.class.isAssignableFrom(object.getClass()))) {
            this.process(methodName, (AbstractAuditable) object, user);
        }
    }

    @PostConstruct
    protected void init() {

        try {
            AUDIT_UPDATE = Settings.getBoolean("audit.update");
            AUDIT_DELETE = Settings.getBoolean("audit.delete");
            AUDIT_CREATE = Settings.getBoolean("audit.create") || AUDIT_UPDATE;
        } catch (NoSuchElementException e) {
            AuditAdvice.log.error("Configuration error: " + e.getMessage() + "; will use default values");
        }
    }

    private boolean canSkipAssociationCheck(Object object, String propertyName) {

        /*
         * If this is an expression experiment, don't go down the data vectors.
         */
        if (ExpressionExperiment.class.isAssignableFrom(object.getClass())
                && (propertyName.equals("rawExpressionDataVectors")
                        || propertyName.equals("processedExpressionDataVectors"))) {
            AuditAdvice.log.trace("Skipping vectors");
            return true;
        }

        /*
         * Array designs...
         */
        if (ArrayDesign.class.isAssignableFrom(object.getClass())
                && (propertyName.equals("compositeSequences") || propertyName.equals("reporters"))) {
            AuditAdvice.log.trace("Skipping probes");
            return true;
        }

        return false;
    }

    /**
     * Adds 'create' AuditEvent to audit trail of the passed AbstractAuditable.
     *
     * @param note Additional text to add to the automatically generated note.
     */
    private void addCreateAuditEvent(final AbstractAuditable auditable, User user, final String note) {

        if (this.isNullOrTransient(auditable))
            return;

        AuditTrail auditTrail = auditable.getAuditTrail();

        this.ensureInSession(auditTrail);

        if (auditTrail != null && !auditTrail.getEvents().isEmpty()) {
            // This can happen when we persist objects and then let this interceptor look at them again
            // while persisting parent objects. That's okay.
            if (AuditAdvice.log.isDebugEnabled())
                AuditAdvice.log
                        .debug("Call to addCreateAuditEvent but the auditTrail already has events. AuditTrail id: "
                                + auditTrail.getId());
            return;
        }

        String details = "Create " + auditable.getClass().getSimpleName() + " " + auditable.getId() + note;

        try {
            auditHelper.addCreateAuditEvent(auditable, details, user);
            if (AuditAdvice.log.isDebugEnabled()) {
                AuditAdvice.log.debug("Audited event: " + (note.length() > 0 ? note : "[no note]") + " on "
                        + auditable.getClass().getSimpleName() + ":" + auditable.getId() + " by "
                        + user.getUserName());
            }

        } catch (UsernameNotFoundException e) {
            AuditAdvice.log.warn("No user, cannot add 'create' event");
        }
    }

    private void addDeleteAuditEvent(AbstractAuditable d, User user) {
        assert d != null;
        // what else could we do? But need to keep this record in a good place. See log4j.properties.
        if (AuditAdvice.log.isInfoEnabled()) {
            String un = "";
            if (user != null) {
                un = "by " + user.getUserName();
            }
            AuditAdvice.log.info(
                    "Delete event on entity " + d.getClass().getName() + ":" + d.getId() + "  [" + d + "] " + un);
        }
    }

    private void addUpdateAuditEvent(final AbstractAuditable auditable, User user) {
        assert auditable != null;

        AuditTrail auditTrail = auditable.getAuditTrail();

        this.ensureInSession(auditTrail);

        if (auditTrail == null || auditTrail.getEvents().isEmpty()) {
            /*
             * Note: This can happen for ExperimentalFactors when loading from GEO etc. because of the bidirectional
             * association and the way we persist them. See ExpressionPersister. (actually this seems to be fixed...)
             */
            AuditAdvice.log.error(
                    "No create event for update method call on " + auditable + ", performing 'create' instead");
            this.addCreateAuditEvent(auditable, user, " - Event added on update of existing object.");
        } else {
            String note = "Updated " + auditable.getClass().getSimpleName() + " " + auditable.getId();
            auditHelper.addUpdateAuditEvent(auditable, note, user);
            if (AuditAdvice.log.isDebugEnabled()) {
                AuditAdvice.log.debug("Audited event: " + note + " on " + auditable.getClass().getSimpleName() + ":"
                        + auditable.getId() + " by " + user.getUserName());
            }
        }
    }

    private void ensureInSession(AuditTrail auditTrail) {
        if (auditTrail == null)
            return;
        /*
         * Ensure we have the object in the session. It might not be, if we have flushed the session.
         */
        Session session = sessionFactory.getCurrentSession();
        if (!session.contains(auditTrail)) {
            session.buildLockRequest(LockOptions.NONE).lock(auditTrail);
        }
    }

    private Object getPersistentObject(Object retValue, String methodName, Object[] args) {
        if (retValue == null
                && (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;
    }

    private boolean isNullOrTransient(final AbstractAuditable auditable) {
        return auditable == null || auditable.getId() == null;
    }

    /**
     * Check if the associated object needs to be 'create audited'. Example: gene products are created by cascade when
     * calling update on a gene.
     */
    private void maybeAddCascadeCreateEvent(Object object, AbstractAuditable auditable, User user) {
        if (AuditAdvice.log.isDebugEnabled())
            AuditAdvice.log
                    .debug("Checking for whether to cascade create event from " + auditable + " to " + object);

        if (auditable.getAuditTrail() == null || auditable.getAuditTrail().getEvents().isEmpty()) {
            this.addCreateAuditEvent(auditable, user, " - created by cascade from " + object);
        }
    }

    /**
     * Process auditing on the object.
     */
    private void process(final String methodName, final AbstractAuditable auditable, User user) {

        // do this here, when we are sure to be in a transaction. But might be repetitive when working on a collection.
        this.sessionFactory.getCurrentSession().setReadOnly(user, true);

        if (AuditAdvice.log.isTraceEnabled()) {
            AuditAdvice.log
                    .trace("***********  Start Audit of " + methodName + " on " + auditable + " *************");
        }
        assert auditable != null : "Null entity passed to auditing [" + methodName + " on " + null + "]";
        assert auditable.getId() != null : "Transient instance passed to auditing [" + methodName + " on "
                + auditable + "]";

        if (AUDIT_CREATE && CrudUtilsImpl.methodIsCreate(methodName)) {
            this.addCreateAuditEvent(auditable, user, "");
            this.processAssociations(methodName, auditable, user);
        } else if (AUDIT_UPDATE && CrudUtilsImpl.methodIsUpdate(methodName)) {
            this.addUpdateAuditEvent(auditable, user);

            /*
             * Do not process associations during an update except to add creates to new objects. Otherwise this would
             * result in update events getting added to all child objects, which is silly; and in any case they might be
             * proxies.
             */
            this.processAssociations(methodName, auditable, user);
        } else if (AUDIT_DELETE && CrudUtilsImpl.methodIsDelete(methodName)) {
            this.addDeleteAuditEvent(auditable, user);
        }

        if (AuditAdvice.log.isTraceEnabled())
            AuditAdvice.log.trace("============  End Audit ==============");
    }

    /**
     * Fills in audit trails on newly created child objects after a 'create' or 'update'. It does not add 'update'
     * events on the child objects.
     * Thus if the update is on an expression experiment that has a new Characteristic, the Characteristic will have a
     * 'create' event, and the EEE will get an added update event (via the addUpdateAuditEvent call elsewhere, not here)
     *
     * @see AclAdvice for similar code for ACLs
     */
    private void processAssociations(String methodName, Object object, User user) {

        if (object instanceof AuditTrail)
            return; // don't audit audit trails.

        EntityPersister persister = crudUtils.getEntityPersister(object);
        if (persister == null) {
            throw new IllegalArgumentException("No persister found for " + object.getClass().getName());
        }
        CascadeStyle[] cascadeStyles = persister.getPropertyCascadeStyles();
        String[] propertyNames = persister.getPropertyNames();
        try {
            for (int j = 0; j < propertyNames.length; j++) {
                CascadeStyle cs = cascadeStyles[j];

                String propertyName = propertyNames[j];

                if (this.canSkipAssociationCheck(object, propertyName) || !crudUtils.needCascade(methodName, cs)) {
                    continue;
                }

                PropertyDescriptor descriptor = BeanUtils.getPropertyDescriptor(object.getClass(), propertyName);
                Object associatedObject = ReflectionUtil.getProperty(object, descriptor);

                if (associatedObject == null)
                    continue;

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

                if (AbstractAuditable.class.isAssignableFrom(propertyType)) {

                    AbstractAuditable auditable = (AbstractAuditable) associatedObject;
                    try {

                        this.maybeAddCascadeCreateEvent(object, auditable, user);

                        this.processAssociations(methodName, auditable, user);
                    } catch (LazyInitializationException e) {
                        // If this happens, it means the object can't be 'new' so adding audit trail can't
                        // be necessary.
                        if (AuditAdvice.log.isDebugEnabled())
                            AuditAdvice.log.debug("Caught lazy init error while processing " + auditable + ": "
                                    + e.getMessage() + " - skipping creation of cascade event.");
                    }

                } else if (Collection.class.isAssignableFrom(propertyType)) {
                    Collection<?> associatedObjects = (Collection<?>) associatedObject;

                    try {
                        Hibernate.initialize(associatedObjects);
                        for (Object collectionMember : associatedObjects) {

                            if (AbstractAuditable.class.isAssignableFrom(collectionMember.getClass())) {
                                AbstractAuditable auditable = (AbstractAuditable) collectionMember;
                                try {
                                    Hibernate.initialize(auditable);
                                    this.maybeAddCascadeCreateEvent(object, auditable, user);
                                    this.processAssociations(methodName, collectionMember, user);
                                } catch (LazyInitializationException e) {

                                    if (AuditAdvice.log.isDebugEnabled())
                                        AuditAdvice.log.debug("Caught lazy init error while processing " + auditable
                                                + ": " + e.getMessage() + " - skipping creation of cascade event.");
                                    // If this happens, it means the object can't be 'new' so adding audit trail can't
                                    // be necessary. But keep checking.
                                }

                            }
                        }
                    } catch (LazyInitializationException e) {

                        // If this happens, it means the object can't be 'new' so adding audit trail can't
                        // be necessary.
                        if (AuditAdvice.log.isDebugEnabled())
                            AuditAdvice.log.debug("Caught lazy init error while processing " + object + ": "
                                    + e.getMessage() + " - skipping creation of cascade event.");
                    }

                }
            }
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }

}