ome.security.basic.BasicSecuritySystem.java Source code

Java tutorial

Introduction

Here is the source code for ome.security.basic.BasicSecuritySystem.java

Source

/*
 * ome.security.basic.BasicSecuritySystem
 *
 *   Copyright 2006 University of Dundee. All rights reserved.
 *   Use is subject to license terms supplied in LICENSE.txt
 */

package ome.security.basic;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import ome.annotations.RevisionDate;
import ome.annotations.RevisionNumber;
import ome.api.local.LocalAdmin;
import ome.api.local.LocalQuery;
import ome.api.local.LocalUpdate;
import ome.conditions.ApiUsageException;
import ome.conditions.InternalException;
import ome.conditions.SecurityViolation;
import ome.conditions.SessionTimeoutException;
import ome.model.IObject;
import ome.model.enums.EventType;
import ome.model.internal.Details;
import ome.model.internal.GraphHolder;
import ome.model.internal.Permissions;
import ome.model.internal.Permissions.Right;
import ome.model.internal.Permissions.Role;
import ome.model.internal.Token;
import ome.model.meta.Event;
import ome.model.meta.EventLog;
import ome.model.meta.Experimenter;
import ome.model.meta.ExperimenterGroup;
import ome.model.meta.GroupExperimenterMap;
import ome.model.roi.Shape;
import ome.security.AdminAction;
import ome.security.SecureAction;
import ome.security.SecuritySystem;
import ome.security.SystemTypes;
import ome.services.messages.EventLogMessage;
import ome.services.messages.ShapeChangeMessage;
import ome.services.sessions.SessionManager;
import ome.services.sessions.events.UserGroupUpdateEvent;
import ome.services.sessions.stats.PerSessionStats;
import ome.system.EventContext;
import ome.system.OmeroContext;
import ome.system.Principal;
import ome.system.Roles;
import ome.system.ServiceFactory;
import ome.tools.hibernate.ExtendedMetadata;
import ome.tools.hibernate.SecurityFilter;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Filter;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.proxy.HibernateProxy;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.util.Assert;

/**
 * simplest implementation of {@link SecuritySystem}. Uses an ctor-injected
 * {@link EventContext} and the {@link ThreadLocal ThreadLocal-}based
 * {@link CurrentDetails} to provide the security infrastructure.
 * 
 * @author Josh Moore, josh.moore at gmx.de
 * @version $Revision: 1581 $, $Date: 2007-06-02 12:31:30 +0200 (Sat, 02 Jun
 *          2007) $
 * @see Token
 * @see SecuritySystem
 * @see Details
 * @see Permissions
 * @since 3.0-M3
 */
@RevisionDate("$Date: 2007-06-02 12:31:30 +0200 (Sat, 02 Jun 2007) $")
@RevisionNumber("$Revision: 1581 $")
public class BasicSecuritySystem
        implements SecuritySystem, ApplicationContextAware, ApplicationListener<EventLogMessage> {

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

    protected final OmeroInterceptor interceptor;

    protected final SystemTypes sysTypes;

    protected final CurrentDetails cd;

    protected final TokenHolder tokenHolder;

    protected final Roles roles;

    protected final SessionManager sessionManager;

    protected final ServiceFactory sf;

    protected/* final */OmeroContext ctx;

    /**
     * Simpilifed factory method which generates all the security primitives
     * internally. Primarily useful for generated testing instances.
     */
    public static BasicSecuritySystem selfConfigure(SessionManager sm, ServiceFactory sf) {
        CurrentDetails cd = new CurrentDetails();
        SystemTypes st = new SystemTypes();
        TokenHolder th = new TokenHolder();
        OmeroInterceptor oi = new OmeroInterceptor(new Roles(), st, new ExtendedMetadata.Impl(), cd, th,
                new PerSessionStats(cd));
        BasicSecuritySystem sec = new BasicSecuritySystem(oi, st, cd, sm, new Roles(), sf, new TokenHolder());
        return sec;
    }

    /**
     * Main public constructor for this {@link SecuritySystem} implementation.
     */
    public BasicSecuritySystem(OmeroInterceptor interceptor, SystemTypes sysTypes, CurrentDetails cd,
            SessionManager sessionManager, Roles roles, ServiceFactory sf, TokenHolder tokenHolder) {
        this.sessionManager = sessionManager;
        this.tokenHolder = tokenHolder;
        this.interceptor = interceptor;
        this.sysTypes = sysTypes;
        this.roles = roles;
        this.cd = cd;
        this.sf = sf;
    }

    public void setApplicationContext(ApplicationContext arg0) throws BeansException {
        this.ctx = (OmeroContext) arg0;
    }

    // ~ Login/logout
    // =========================================================================

    public void login(Principal principal) {
        cd.login(principal);
    }

    public int logout() {
        return cd.logout();
    }

    // ~ Checks
    // =========================================================================
    /**
     * implements {@link SecuritySystem#isReady()}. Simply checks for null
     * values in all the relevant fields of {@link CurrentDetails}
     */
    public boolean isReady() {
        return cd.isReady();
    }

    /**
     * classes which cannot be created by regular users.
     * 
     * @see <a
     *      href="https://trac.openmicroscopy.org.uk/omero/ticket/156">ticket156</a>
     */
    public boolean isSystemType(Class<? extends IObject> klass) {
        return sysTypes.isSystemType(klass);
    }

    /**
     * tests whether or not the current user is either the owner of this entity,
     * or the superivsor of this entity, for example as root or as group owner.
     * 
     * @param iObject
     *            Non-null managed entity.
     * @return true if the current user is owner or supervisor of this entity
     */
    public boolean isOwnerOrSupervisor(IObject iObject) {
        return cd.isOwnerOrSupervisor(iObject);
    }

    // ~ Read security
    // =========================================================================
    /**
     * enables the read filter such that graph queries will have non-visible
     * entities silently removed from the return value. This filter does <em>
     * not</em>
     * apply to single value loads from the database. See
     * {@link #allowLoad(Class, Details)} for more.
     * 
     * Note: this filter must be disabled on logout, otherwise the necessary
     * parameters (current user, current group, etc.) for building the filters
     * will not be available. Similarly, while enabling this filter, no calls
     * should be made on the given session object.
     * 
     * @param session
     *            a generic session object which can be used to enable this
     *            filter. Each {@link SecuritySystem} implementation will
     *            require a specific session type.
     * @see EventHandler#invoke(org.aopalliance.intercept.MethodInvocation)
     */
    public void enableReadFilter(Object session) {
        if (session == null || !(session instanceof Session)) {
            throw new ApiUsageException("The Object argument to enableReadFilter"
                    + " in the BasicSystemSecurity implementation must be a " + " non-null org.hibernate.Session.");
        }

        checkReady("enableReadFilter");
        // beware
        // http://opensource.atlassian.com/projects/hibernate/browse/HHH-1932
        EventContext ec = getEventContext();
        Session sess = (Session) session;
        Filter filter = sess.enableFilter(SecurityFilter.filterName);

        Long shareId = ec.getCurrentShareId();
        int share01 = shareId != null ? 1 : 0;

        int admin01 = (ec.isCurrentUserAdmin() || ec.getLeaderOfGroupsList().contains(ec.getCurrentGroupId())) ? 1
                : 0;

        int nonpriv01 = (ec.getCurrentGroupPermissions().isGranted(Role.GROUP, Right.READ)
                || ec.getCurrentGroupPermissions().isGranted(Role.WORLD, Right.READ)) ? 1 : 0;

        filter.setParameter(SecurityFilter.is_share, share01); // ticket:2219, not checking -1 here.
        filter.setParameter(SecurityFilter.is_adminorpi, admin01);
        filter.setParameter(SecurityFilter.is_nonprivate, nonpriv01);
        filter.setParameter(SecurityFilter.current_group, ec.getCurrentGroupId());
        filter.setParameter(SecurityFilter.current_user, ec.getCurrentUserId());
    }

    public void updateReadFilter(Session session) {
        session.disableFilter(SecurityFilter.filterName);
        enableReadFilter(session);
    }

    /**
     * disable this filer. All future queries will have no security context
     * associated with them and all items will be visible.
     * 
     * @param session
     *            a generic session object which can be used to disable this
     *            filter. Each {@link SecuritySystem} implementation will
     *            require a specifc session type.
     * @see EventHandler#invoke(org.aopalliance.intercept.MethodInvocation)
     */
    public void disableReadFilter(Object session) {
        // Session system doesn't seem to provide this
        // i.e. isReady() is false here. Disabling but need to review
        // checkReady("disableReadFilter");

        Session sess = (Session) session;
        sess.disableFilter(SecurityFilter.filterName);
    }

    // ~ Subsystem disabling
    // =========================================================================

    public void disable(String... ids) {
        if (ids == null || ids.length == 0) {
            throw new ApiUsageException("Ids should not be empty.");
        }
        cd.addAllDisabled(ids);
    }

    public void enable(String... ids) {
        if (ids == null || ids.length == 0) {
            cd.clearDisabled();
        }
        cd.removeAllDisabled(ids);
    }

    public boolean isDisabled(String id) {
        if (id == null) {
            throw new ApiUsageException("Id should not be null.");
        }
        return cd.isDisabled(id);
    }

    // OmeroInterceptor delegation
    // =========================================================================

    public Details newTransientDetails(IObject object) throws ApiUsageException, SecurityViolation {
        checkReady("transientDetails");
        return interceptor.newTransientDetails(object);
    }

    public Details checkManagedDetails(IObject object, Details trustedDetails)
            throws ApiUsageException, SecurityViolation {
        checkReady("managedDetails");
        return interceptor.checkManagedDetails(object, trustedDetails);
    }

    // ~ CurrentDetails delegation (ensures proper settings of Tokens)
    // =========================================================================

    public boolean isGraphCritical() {
        checkReady("isGraphCritical");
        return cd.isGraphCritical();
    }

    public void loadEventContext(boolean isReadOnly) {
        loadEventContext(isReadOnly, false);
    }

    public void loadEventContext(boolean isReadOnly, boolean isClose) {

        final LocalAdmin admin = (LocalAdmin) sf.getAdminService();
        final LocalUpdate update = (LocalUpdate) sf.getUpdateService();

        // Call to session manager throws an exception on failure
        final Principal p = clearAndCheckPrincipal();

        // ticket:1855 - Catching SessionTimeoutException in order to permit
        // the close of a stateful service.
        EventContext ec;
        try {
            ec = sessionManager.getEventContext(p);
        } catch (SessionTimeoutException ste) {
            if (!isClose) {
                throw ste;
            }
            ec = (EventContext) ste.sessionContext;
        }

        // Refill current details
        cd.copy(ec);

        // Experimenter
        Experimenter exp;
        if (isReadOnly) {
            exp = new Experimenter(ec.getCurrentUserId(), false);
        } else {
            exp = admin.userProxy(ec.getCurrentUserId());
        }
        tokenHolder.setToken(exp.getGraphHolder());

        // isAdmin
        boolean isAdmin = false;
        for (long gid : ec.getMemberOfGroupsList()) {
            if (roles.getSystemGroupId() == gid) {
                isAdmin = true;
                break;
            }
        }

        // Active group
        ExperimenterGroup grp;
        Long groupId = cd.getCallGroup();
        Long shareId = ec.getCurrentShareId();
        if (groupId == null) {
            groupId = ec.getCurrentGroupId();
        } else {
            if (groupId >= 0) {
                log.debug("Using call-requested group: " + groupId);
            } else {
                // ticket:2950
                if (!isAdmin) {
                    throw new SecurityViolation("Only administrators can use negative groups!");
                }
                log.info("Setting share id to -1");
                shareId = -1L;
                groupId = ec.getCurrentGroupId();
            }
        }

        if (isReadOnly) {
            grp = new ExperimenterGroup(groupId, false);
        } else {
            grp = admin.groupProxy(groupId);
        }

        long sessionId = ec.getCurrentSessionId().longValue();
        ome.model.meta.Session sess = null;
        if (isReadOnly) {
            sess = new ome.model.meta.Session(sessionId, false);
        } else {
            sess = sf.getQueryService().get(ome.model.meta.Session.class, sessionId);
        }

        // public groups (ticket:1940)
        if (!isAdmin && !ec.getMemberOfGroupsList().contains(grp.getId())) {
            // Only force loading the group if we would otherwise throw an exception.
            // The extra performance hit on READ is just the price of browsing
            // public data
            ExperimenterGroup publicGroup = admin.groupProxy(groupId);
            if (!publicGroup.getDetails().getPermissions().isGranted(Role.WORLD, Right.READ)) {
                throw new SecurityViolation(String.format("User %s is not a member of group %s and cannot login",
                        ec.getCurrentUserId(), grp.getId()));
            }
        }
        tokenHolder.setToken(grp.getGraphHolder());

        // In order to less frequently access the ThreadLocal in CurrentDetails
        // All properities are now set in one shot, except for Event.
        cd.setValues(exp, grp, isAdmin, isReadOnly, shareId);

        // Event
        String t = p.getEventType();
        if (t == null) {
            t = ec.getCurrentEventType();
        }
        EventType type = new EventType(t);
        tokenHolder.setToken(type.getGraphHolder());
        Event event = cd.newEvent(sess, type, tokenHolder);
        tokenHolder.setToken(event.getGraphHolder());

        // If this event is not read only, then lets save this event to prevent
        // flushing issues later.
        if (!isReadOnly) {
            cd.updateEvent(update.saveAndReturnObject(event)); // TODO use merge
        }

    }

    private Principal clearAndCheckPrincipal() {

        // clear even if this fails. (make SecuritySystem unusable)
        invalidateEventContext();

        if (cd.size() == 0) {
            throw new SecurityViolation("Principal is null. Not logged in to SecuritySystem.");
        }

        final Principal p = cd.getLast();

        if (p.getName() == null) {
            throw new InternalException("Principal.name is null. Security system failure.");
        }

        return p;
    }

    public void addLog(String action, Class klass, Long id) {
        cd.addLog(action, klass, id);
    }

    public List<EventLog> getLogs() {
        return cd.getLogs();
    }

    public void clearLogs() {
        if (log.isDebugEnabled()) {
            log.debug("Clearing EventLogs.");
        }

        boolean foundAdminType = false;
        List<EventLog> foundShapes = new ArrayList<EventLog>();
        for (EventLog log : getLogs()) {
            String t = log.getEntityType();
            String a = log.getAction();
            if (Experimenter.class.getName().equals(t) || ExperimenterGroup.class.getName().equals(t)
                    || GroupExperimenterMap.class.getName().equals(t)) {
                foundAdminType = true;
            }
            try {
                if (Shape.class.isAssignableFrom(Class.forName(t))) {
                    if ("INSERT".equals(a) || "UPDATE".equals(a)) {
                        foundShapes.add(log);
                    }
                }
            } catch (ClassNotFoundException e) {
                throw new InternalException("Shape != Class.forName: " + t);
            }
        }
        // publish message if administrative type is modified
        if (foundAdminType) {
            if (ctx == null) {
                log.error("No context found for publishing");
            } else {
                this.ctx.publishEvent(new UserGroupUpdateEvent(this));
            }
        }
        // publish message if shape is created or updated
        if (foundShapes.size() > 0) {
            this.ctx.publishEvent(new ShapeChangeMessage(this, foundShapes));
        }

        cd.clearLogs();
    }

    public void invalidateEventContext() {
        if (log.isDebugEnabled()) {
            log.debug("Invalidating current EventContext.");
        }
        cd.invalidateCurrentEventContext();
    }

    // ~ Tokens & Actions
    // =========================================================================

    /**
     * 
     * It would be better to catch the
     * {@link SecureAction#updateObject(IObject)} method in a try/finally block,
     * but since flush can be so poorly controlled that's not possible. instead,
     * we use the one time token which is removed this Object is checked for
     * {@link #hasPrivilegedToken(IObject) privileges}.
     * 
     * @param obj
     *            A managed (non-detached) entity. Not null.
     * @param action
     *            A code-block that will be given the entity argument with a
     *            {@link #hasPrivilegedToken(IObject)} privileged token}.
     */
    public <T extends IObject> T doAction(SecureAction action, T... objs) {
        Assert.notNull(objs);
        Assert.notEmpty(objs);
        Assert.notNull(action);

        final LocalQuery query = (LocalQuery) sf.getQueryService();
        final List<GraphHolder> ghs = new ArrayList<GraphHolder>();

        for (T obj : objs) {

            // TODO inject
            if (obj.getId() != null && !query.contains(obj)) {
                throw new SecurityViolation(
                        "Services are not allowed to call " + "doAction() on non-Session-managed entities.");
            }

            // ticket:1794 - use of IQuery.get along with doAction() creates
            // two objects (outer proxy and inner target) and only the outer
            // proxy has its graph holder modified without this block, leading
            // to security violations on flush since no token is present.
            if (obj instanceof HibernateProxy) {
                HibernateProxy hp = (HibernateProxy) obj;
                IObject obj2 = (IObject) hp.getHibernateLazyInitializer().getImplementation();
                ghs.add(obj2.getGraphHolder());
            }

            // FIXME
            // Token oneTimeToken = new Token();
            // oneTimeTokens.put(oneTimeToken);
            ghs.add(obj.getGraphHolder());

        }

        // Holding onto the graph holders since they protect the access
        // to their tokens
        for (GraphHolder graphHolder : ghs) {
            tokenHolder.setToken(graphHolder); // oneTimeToken
        }

        T retVal;
        try {
            retVal = action.updateObject(objs);
        } finally {
            for (GraphHolder graphHolder : ghs) {
                tokenHolder.clearToken(graphHolder);
            }
        }
        return retVal;
    }

    /**
     * merge event is disabled for {@link #runAsAdmin(AdminAction)} because
     * passing detached (client-side) entities to this method is particularly
     * dangerous.
     */
    public void runAsAdmin(final AdminAction action) {
        Assert.notNull(action);

        // Need to check here so that no exception is thrown
        // during the try block below
        checkReady("runAsAdmin");

        final LocalQuery query = (LocalQuery) sf.getQueryService();
        query.execute(new HibernateCallback() {
            public Object doInHibernate(Session session) throws HibernateException, SQLException {

                BasicEventContext c = cd.current();
                boolean wasAdmin = c.isCurrentUserAdmin();

                try {
                    c.setAdmin(true);
                    disable(MergeEventListener.MERGE_EVENT);
                    enableReadFilter(session);
                    action.runAsAdmin();
                } finally {
                    c.setAdmin(wasAdmin);
                    enable(MergeEventListener.MERGE_EVENT);
                    enableReadFilter(session); // Now as non-admin
                }
                return null;
            }
        });
    }

    /**
     * See {@link TokenHolder#copyToken(IObject, IObject)
        
     */
    public void copyToken(IObject source, IObject copy) {
        tokenHolder.copyToken(source, copy);
    }

    /**
     * See {@link TokenHolder#hasPrivilegedToken(IObject)
        
     */
    public boolean hasPrivilegedToken(IObject obj) {
        return tokenHolder.hasPrivilegedToken(obj);
    }

    // ~ Configured Elements
    // =========================================================================

    public Roles getSecurityRoles() {
        return roles;
    }

    public EventContext getEventContext(boolean refresh) {
        EventContext ec = cd.getCurrentEventContext();
        if (refresh) {
            String uuid = ec.getCurrentSessionUuid();
            ec = sessionManager.reload(uuid);
        }
        return ec;
    }

    public EventContext getEventContext() {
        return getEventContext(false);
    }

    // ~ Helpers
    // =========================================================================

    /**
     * calls {@link #isReady()} and if not throws an {@link ApiUsageException}.
     * The {@link SecuritySystem} must be in a valid state to perform several
     * functions.
     */
    protected void checkReady(String method) {
        if (!isReady()) {
            throw new ApiUsageException("The security system is not ready.\n" + "Cannot execute: " + method);
        }

    }

    public void onApplicationEvent(EventLogMessage elm) {
        if (elm != null) {
            for (Long id : elm.entityIds) {
                addLog(elm.action, elm.entityType, id);
            }
        }
    }

}