ome.security.basic.CurrentDetails.java Source code

Java tutorial

Introduction

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

Source

/*
 *   $Id$
 *
 *   Copyright 2006 University of Dundee. All rights reserved.
 *   Use is subject to license terms supplied in LICENSE.txt
 */

package ome.security.basic;

// Java imports
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import ome.conditions.ApiUsageException;
import ome.conditions.InternalException;
import ome.model.IObject;
import ome.model.enums.EventType;
import ome.model.internal.Details;
import ome.model.internal.Permissions;
import ome.model.internal.Permissions.Right;
import ome.model.internal.Permissions.Role;
import ome.model.meta.Event;
import ome.model.meta.EventLog;
import ome.model.meta.Experimenter;
import ome.model.meta.ExperimenterGroup;
import ome.model.meta.Session;
import ome.security.SecuritySystem;
import ome.services.messages.RegisterServiceCleanupMessage;
import ome.services.sessions.SessionContext;
import ome.services.sessions.state.SessionCache;
import ome.services.sessions.stats.SessionStats;
import ome.services.util.ServiceHandler;
import ome.system.EventContext;
import ome.system.Principal;
import ome.tools.hibernate.HibernateUtils;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.Assert;

/**
 * Stores information related to the security context of the current thread.
 * Code calling into the server must setup CurrentDetails properly. An existing
 * user must be set (the creation of a new user is only allowed if the current
 * user is set to root; root always exists. QED.) The event must also be set.
 * Umask is optional.
 * 
 * This information is stored in a Details object, but unlike Details which
 * assumes that an empty value signifies increased security levels, empty values
 * here signifiy reduced security levels. E.g.,
 * 
 * Details: user == null ==> object belongs to root CurrentDetails: user == null
 * ==> current user is "nobody" (anonymous)
 * 
 */
public class CurrentDetails implements PrincipalHolder {

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

    private final SessionCache cache;

    private final ThreadLocal<LinkedList<BasicEventContext>> contexts = new ThreadLocal<LinkedList<BasicEventContext>>();

    private final ThreadLocal<Map<String, String>> callContext = new ThreadLocal<Map<String, String>>();

    /**
     * Default constructor. Should only be used for testing, since the stats
     * used will not be correct.
     */
    public CurrentDetails() {
        this.cache = null;
    }

    public CurrentDetails(SessionCache cache) {
        this.cache = cache;
    }

    private LinkedList<BasicEventContext> list() {
        LinkedList<BasicEventContext> list = contexts.get();
        if (list == null) {
            list = new LinkedList<BasicEventContext>();
            contexts.set(list);
        }
        return list;
    }

    // Method call context methods
    // =================================================================

    public Map<String, String> setContext(Map<String, String> ctx) {
        Map<String, String> rv = callContext.get();
        callContext.set(ctx);
        return rv;
    }

    public Long getCallGroup() {
        Map<String, String> ctx = callContext.get();
        if (ctx != null && ctx.containsKey("omero.group")) {
            String s = ctx.get("omero.group");
            try {
                return Long.valueOf(ctx.get("omero.group"));
            } catch (Exception e) {
                log.debug("Ignoring invalid omero.group context: " + s);
                return null;
            }
        }
        return null;
    }

    public void setCallGroup(Long id) {
        Map<String, String> ctx = callContext.get();
        if (ctx == null) {
            ctx = new HashMap<String, String>();
            callContext.set(ctx);
        }

        String old = ctx.get("omero.group.old");
        String curr = ctx.get("omero.group");
        if (old != null) {
            throw new RuntimeException(
                    "Recursive call! " + String.format("Old: %s Current: %s New: %s", old, curr, id));
        }

        ctx.put("omero.group.old", curr);
        ctx.put("omero.group", "" + id);
    }

    public void resetCallGroup() {
        Map<String, String> ctx = callContext.get();
        if (ctx != null) {
            String old = ctx.get("omero.group.old");
            ctx.remove("omero.group.old");
            ctx.put("omero.group", old);
        }
    }

    // PrincipalHolder methods
    // =================================================================

    public int size() {
        return list().size();
    }

    public Principal getLast() {
        return list().getLast().getPrincipal();
    }

    public void login(Principal principal) {
        // Can't use the method in SessionManager since that leads to a
        // circular reference in Spring.
        final String uuid = principal.getName();
        final SessionContext ctx = cache.getSessionContext(uuid);
        final SessionStats stats = ctx.stats();
        final BasicEventContext c = new BasicEventContext(principal, stats);
        login(c);
    }

    /**
     * Login method which can be used by the security system to replace the
     * existing {@link BasicEventContext}.
     */
    public void login(BasicEventContext bec) {
        if (log.isDebugEnabled()) {
            log.debug("Logging in :" + bec);
        }
        list().add(bec);
        bec.getStats().methodIn();
    }

    public int logout() {
        LinkedList<BasicEventContext> list = list();
        BasicEventContext bec = list.removeLast();
        bec.getStats().methodOut();
        if (log.isDebugEnabled()) {
            log.debug("Logged out: " + bec);
        }
        return list.size();
    }

    // High-level methods used to fulfill {@link SecuritySystem}
    // =================================================================

    /**
     * Checks if the current {@link Thread} has non-null {@link Experimenter},
     * {@link Event}, and {@linkExperimenterGroup}, required for proper
     * functioning of the security system.
     */
    public boolean isReady() {
        BasicEventContext c = current();
        if (c.getEvent() != null && c.getGroup() != null && c.getOwner() != null) {
            return true;
        }
        return false;
    }

    /**
     * @see SecuritySystem#isGraphCritical()
     * @return
     */
    public boolean isGraphCritical() {
        EventContext ec = getCurrentEventContext();
        long gid = ec.getCurrentGroupId();
        Permissions perms = ec.getCurrentGroupPermissions();

        boolean admin = ec.isCurrentUserAdmin();
        boolean pi = ec.getLeaderOfGroupsList().contains(gid);

        if (perms.isGranted(Role.WORLD, Right.READ)) {
            // Public groups (rwrwrw) are always non-critical
            return false;
        } else if (perms.isGranted(Role.GROUP, Right.READ)) {
            // Since the object will be contained in the group,
            // then it will be readable regardless.
            return false;
        } else {
            // This is a private group. Any form of admin modification is
            // critical.
            return admin || pi;
        }
    }

    public boolean isOwnerOrSupervisor(IObject object) {
        if (object == null) {
            throw new ApiUsageException("Object can't be null");
        }
        final Long o = HibernateUtils.nullSafeOwnerId(object);
        final Long g = HibernateUtils.nullSafeGroupId(object);

        final EventContext ec = getCurrentEventContext();
        final boolean isAdmin = ec.isCurrentUserAdmin();
        final boolean isPI = ec.getLeaderOfGroupsList().contains(g);
        final boolean isOwner = ec.getCurrentUserId().equals(o);

        if (isAdmin || isPI || isOwner) {
            return true;
        }
        return false;
    }

    // State management
    // =================================================================

    /**
     * Replaces all the simple-valued fields in the {@link BasicEventContext}.
     */
    void copy(EventContext ec) {
        current().copyContext(ec);
    }

    /**
     * Returns the current {@link BasicEventContext instance} throwing an
     * exception if there isn't one.
     */
    BasicEventContext current() {
        BasicEventContext c = list().getLast();
        return c;
    }

    /**
     * Public view on the data contained here. Used to create the
     * 
     * @return
     */
    public EventContext getCurrentEventContext() {
        return current();
    }

    /**
     * It suffices to set the {@link Details} to a new instance to make this
     * context unusable. {@link #isReady()} will return false.
     */
    public void invalidateCurrentEventContext() {
        BasicEventContext c = current();
        c.invalidate();
        if (log.isDebugEnabled()) {
            log.debug("Invalidated login: " + c);
        }
    }

    // ~ Events and Details
    // =================================================================

    public Event newEvent(Session session, EventType type, TokenHolder tokenHolder) {
        BasicEventContext c = current();
        Event e = new Event();
        e.setType(type);
        e.setTime(new Timestamp(System.currentTimeMillis()));
        tokenHolder.setToken(e.getGraphHolder());
        e.getDetails().setPermissions(Permissions.READ_ONLY);
        // Proxied if necessary
        e.setExperimenter(c.getOwner());
        e.setExperimenterGroup(c.getGroup());
        e.setSession(session);

        c.setEvent(e);
        return e;
    }

    public void addLog(String action, Class klass, Long id) {

        Assert.notNull(action);
        Assert.notNull(klass);
        Assert.notNull(id);

        if (Event.class.isAssignableFrom(klass) || EventLog.class.isAssignableFrom(klass)) {
            if (log.isDebugEnabled()) {
                log.debug("Not logging creation of logging type:" + klass);
            }
            return; // EARLY EXIT
        } else {
            if (!isReady()) {
                throw new InternalException("Not ready to add EventLog");
            }
        }

        if (log.isInfoEnabled()) {
            log.info("Adding log:" + action + "," + klass + "," + id);
        }

        BasicEventContext c = current();
        List<EventLog> list = current().getLogs();
        if (list == null) {
            list = new ArrayList<EventLog>();
            c.setLogs(list);
        }

        EventLog l = new EventLog();
        l.setAction(action);
        l.setEntityType(klass.getName()); // TODO could be id to Type entity
        l.setEntityId(id);
        l.setEvent(c.getEvent());
        Details d = Details.create();
        d.setPermissions(new Permissions());
        l.getDetails().copy(d);
        list.add(l);
    }

    public SessionStats getStats() {
        return current().getStats();
    }

    public List<EventLog> getLogs() { // TODO defensive copy
        List<EventLog> logs = current().getLogs();
        return logs == null ? new ArrayList<EventLog>() : logs;
    }

    public void clearLogs() {
        current().setLogs(null);
    }

    /**
     * Creates a {@link Details} object for the current security context.
     *
     * The {@link Permissions} on the instance are calculated from the current
     * group as well as the user's umask.
     *
     * @return
     * @see <a href="https://trac.openmicroscopy.org.uk/trac/omero/ticket:1434">ticket:1434</a>
     */
    public Details createDetails() {
        BasicEventContext c = current();
        Details d = Details.create();
        d.setCreationEvent(c.getEvent());
        d.setUpdateEvent(c.getEvent());
        d.setOwner(c.getOwner());
        d.setGroup(c.getGroup());
        // ticket:1434
        Permissions groupPerms = c.getCurrentGroupPermissions();
        Permissions userUmask = c.getCurrentUmask();
        Permissions p = new Permissions(groupPerms);
        p.revokeAll(userUmask);
        d.setPermissions(p);
        return d;
    }

    public Experimenter getOwner() {
        return current().getOwner();
    }

    public ExperimenterGroup getGroup() {
        return current().getGroup();
    }

    public Event getEvent() {
        return current().getEvent();
    }

    /**
     * Take all values during loadEventContext() in one shot. Event is set
     * during {@link #newEvent(long, EventType, TokenHolder)} and possibly
     * updated via {@link #updateEvent(Event)}.
     */
    void setValues(Experimenter owner, ExperimenterGroup group, boolean isAdmin, boolean isReadOnly, Long shareId) {
        BasicEventContext c = current();
        c.setOwner(owner);
        c.setGroup(group);
        c.setAdmin(isAdmin);
        c.setReadOnly(isReadOnly);
        c.setShareId(shareId);
    }

    /**
     * Allows for updating the {@link Event} if the session is not readOnly.
     */
    void updateEvent(Event event) {
        current().setEvent(event);
    }

    // ~ Cleanups
    // =========================================================================

    /**
     * Add a {@link RegisterServiceCleanupMessage} to the current thread for
     * cleanup by the {@link ServiceHandler} on exit.
     */
    public void addCleanup(RegisterServiceCleanupMessage cleanup) {
        Set<RegisterServiceCleanupMessage> cleanups = current().getServiceCleanups();
        if (cleanups == null) {
            cleanups = new HashSet<RegisterServiceCleanupMessage>();
            current().setServiceCleanups(cleanups);
        }
        cleanups.add(cleanup);
    }

    /**
     * Returns the current cleanups and resets the {@link Set}. Instances can
     * most likely only be closed once, so it doesn't make sense to keep them
     * around. The first caller of this method is responsible for closing all of
     * them.
     */
    public Set<RegisterServiceCleanupMessage> emptyCleanups() {
        Set<RegisterServiceCleanupMessage> set = current().getServiceCleanups();
        if (current().getServiceCleanups() == null) {
            return Collections.emptySet();
        } else {
            Set<RegisterServiceCleanupMessage> copy = new HashSet<RegisterServiceCleanupMessage>(set);
            set.clear();
            return copy;
        }
    }

    // ~ Subsystems
    // =========================================================================

    public boolean addDisabled(String id) {
        Set<String> s = current().getDisabledSubsystems();
        if (s == null) {
            s = new HashSet<String>();
            current().setDisabledSubsystems(s);
        }
        return s.add(id);
    }

    public boolean addAllDisabled(String... ids) {
        Set<String> s = current().getDisabledSubsystems();
        if (s == null) {
            s = new HashSet<String>();
            current().setDisabledSubsystems(s);
        }
        if (ids != null) {
            return Collections.addAll(s, ids);
        }
        return false;

    }

    public boolean removeDisabled(String id) {
        Set<String> s = current().getDisabledSubsystems();
        if (s != null && id != null) {
            return s.remove(id);
        }
        return false;
    }

    public boolean removeAllDisabled(String... ids) {
        Set<String> s = current().getDisabledSubsystems();
        if (s != null && ids != null) {
            boolean changed = false;
            for (String string : ids) {
                changed |= s.remove(string);
            }
        }
        return false;
    }

    public void clearDisabled() {
        current().setDisabledSubsystems(null);
    }

    public boolean isDisabled(String id) {
        if (size() == 0) {
            // The security system is not active, so nothing can have
            // been "disabled"
            return false;
        } else {
            Set<String> s = current().getDisabledSubsystems();
            if (s == null || id == null || !s.contains(id)) {
                return false;
            }
            return true;
        }

    }

}