org.taverna.server.master.TavernaServerSupport.java Source code

Java tutorial

Introduction

Here is the source code for org.taverna.server.master.TavernaServerSupport.java

Source

/*
 * Copyright (C) 2010-2011 The University of Manchester
 * 
 * See the file "LICENSE.txt" for license terms.
 */
package org.taverna.server.master;

import static eu.medsea.util.MimeUtil.getMimeType;
import static java.lang.Math.min;
import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM;
import static org.apache.commons.logging.LogFactory.getLog;
import static org.springframework.jmx.support.MetricType.COUNTER;
import static org.springframework.jmx.support.MetricType.GAUGE;
import static org.taverna.server.master.TavernaServerImpl.JMX_ROOT;
import static org.taverna.server.master.common.Roles.ADMIN;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.activation.DataHandler;
import javax.xml.bind.JAXBException;

import org.apache.commons.logging.Log;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedMetric;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.taverna.server.master.common.Permission;
import org.taverna.server.master.common.Workflow;
import org.taverna.server.master.exceptions.FilesystemAccessException;
import org.taverna.server.master.exceptions.NoCreateException;
import org.taverna.server.master.exceptions.NoDestroyException;
import org.taverna.server.master.exceptions.NoListenerException;
import org.taverna.server.master.exceptions.NoUpdateException;
import org.taverna.server.master.exceptions.UnknownRunException;
import org.taverna.server.master.factories.ListenerFactory;
import org.taverna.server.master.factories.RunFactory;
import org.taverna.server.master.interfaces.File;
import org.taverna.server.master.interfaces.Input;
import org.taverna.server.master.interfaces.Listener;
import org.taverna.server.master.interfaces.LocalIdentityMapper;
import org.taverna.server.master.interfaces.Policy;
import org.taverna.server.master.interfaces.RunStore;
import org.taverna.server.master.interfaces.TavernaRun;
import org.taverna.server.master.interfaces.TavernaSecurityContext;
import org.taverna.server.master.utils.InvocationCounter;
import org.taverna.server.master.utils.UsernamePrincipal;

import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import edu.umd.cs.findbugs.annotations.SuppressWarnings;

/**
 * Web application support utilities.
 * 
 * @author Donal Fellows
 */
@ManagedResource(objectName = JMX_ROOT
        + "Webapp", description = "The main web-application interface to Taverna Server.")
public class TavernaServerSupport {
    /** The main webapp log. */
    public static final Log log = getLog("Taverna.Server.Webapp");
    private Log accessLog = getLog("Taverna.Server.Webapp.Access");;
    /** Bean used to log counts of external calls. */
    private InvocationCounter counter;
    /** A storage facility for workflow runs. */
    private RunStore runStore;
    /** Encapsulates the policies applied by this server. */
    private Policy policy;
    /** Connection to the persistent state of this service. */
    private ManagementModel stateModel;
    /** A factory for event listeners to attach to workflow runs. */
    private ListenerFactory listenerFactory;
    /** A factory for workflow runs. */
    private RunFactory runFactory;
    /** How to map the user ID to who to run as. */
    private LocalIdentityMapper idMapper;
    /** The code that is coupled to CXF. */
    private TavernaServer webapp;
    /**
     * Whether to log failures during principal retrieval. Should be normally on
     * as it indicates a serious problem, but can be switched off for testing.
     */
    private boolean logGetPrincipalFailures = true;
    private Map<String, String> contentTypeMap;
    /** Number of bytes to read when guessing the MIME type. */
    private static final int SAMPLE_SIZE = 1024;
    /** Number of bytes to ask for when copying a stream to a file. */
    private static final int TRANSFER_SIZE = 32768;

    /**
     * @return Count of the number of external calls into this webapp.
     */
    @ManagedMetric(description = "Count of the number of external calls into this webapp.", metricType = COUNTER, category = "throughput")
    public int getInvocationCount() {
        return counter.getCount();
    }

    /**
     * @return Current number of runs.
     */
    @ManagedMetric(description = "Current number of runs.", metricType = GAUGE, category = "utilization")
    public int getCurrentRunCount() {
        return runStore.listRuns(null, policy).size();
    }

    /**
     * @return Whether to write submitted workflows to the log.
     */
    @ManagedAttribute(description = "Whether to write submitted workflows to the log.")
    public boolean getLogIncomingWorkflows() {
        return stateModel.getLogIncomingWorkflows();
    }

    /**
     * @param logIncomingWorkflows
     *            Whether to write submitted workflows to the log.
     */
    @ManagedAttribute(description = "Whether to write submitted workflows to the log.")
    public void setLogIncomingWorkflows(boolean logIncomingWorkflows) {
        stateModel.setLogIncomingWorkflows(logIncomingWorkflows);
    }

    /**
     * @return Whether outgoing exceptions should be logged before being
     *         converted to responses.
     */
    @ManagedAttribute(description = "Whether outgoing exceptions should be logged before being converted to responses.")
    public boolean getLogOutgoingExceptions() {
        return stateModel.getLogOutgoingExceptions();
    }

    /**
     * @param logOutgoing
     *            Whether outgoing exceptions should be logged before being
     *            converted to responses.
     */
    @ManagedAttribute(description = "Whether outgoing exceptions should be logged before being converted to responses.")
    public void setLogOutgoingExceptions(boolean logOutgoing) {
        stateModel.setLogOutgoingExceptions(logOutgoing);
    }

    /**
     * @return Whether to permit any new workflow runs to be created.
     */
    @ManagedAttribute(description = "Whether to permit any new workflow runs to be created; has no effect on existing runs.")
    public boolean getAllowNewWorkflowRuns() {
        return stateModel.getAllowNewWorkflowRuns();
    }

    /**
     * @param allowNewWorkflowRuns
     *            Whether to permit any new workflow runs to be created.
     */
    @ManagedAttribute(description = "Whether to permit any new workflow runs to be created; has no effect on existing runs.")
    public void setAllowNewWorkflowRuns(boolean allowNewWorkflowRuns) {
        stateModel.setAllowNewWorkflowRuns(allowNewWorkflowRuns);
    }

    public int getMaxSimultaneousRuns() {
        Integer limit = policy.getMaxRuns(getPrincipal());
        if (limit == null)
            return policy.getMaxRuns();
        return min(limit.intValue(), policy.getMaxRuns());
    }

    public List<Workflow> getPermittedWorkflows() {
        return policy.listPermittedWorkflows(getPrincipal());
    }

    public List<String> getListenerTypes() {
        return listenerFactory.getSupportedListenerTypes();
    }

    /**
     * @param policy
     *            The policy being installed by Spring.
     */
    @Required
    public void setPolicy(Policy policy) {
        this.policy = policy;
    }

    /**
     * @param listenerFactory
     *            The listener factory being installed by Spring.
     */
    @Required
    public void setListenerFactory(ListenerFactory listenerFactory) {
        this.listenerFactory = listenerFactory;
    }

    /**
     * @param runFactory
     *            The run factory being installed by Spring.
     */
    @Required
    public void setRunFactory(RunFactory runFactory) {
        this.runFactory = runFactory;
    }

    /**
     * @param runStore
     *            The run store being installed by Spring.
     */
    @Required
    public void setRunStore(RunStore runStore) {
        this.runStore = runStore;
    }

    /**
     * @param stateModel
     *            The state model engine being installed by Spring.
     */
    @Required
    public void setStateModel(ManagementModel stateModel) {
        this.stateModel = stateModel;
    }

    /**
     * @param mapper
     *            The identity mapper being installed by Spring.
     */
    @Required
    public void setIdMapper(LocalIdentityMapper mapper) {
        this.idMapper = mapper;
    }

    /**
     * @param counter
     *            The object whose job it is to manage the counting of
     *            invocations. Installed by Spring.
     */
    @Required
    public void setInvocationCounter(InvocationCounter counter) {
        this.counter = counter;
    }

    /**
     * @param webapp
     *            The web-app being installed by Spring.
     */
    @Required
    public void setWebapp(TavernaServer webapp) {
        this.webapp = webapp;
    }

    /**
     * @param logthem
     *            Whether to log failures relating to principals.
     */
    public void setLogGetPrincipalFailures(boolean logthem) {
        logGetPrincipalFailures = logthem;
    }

    public Map<String, String> getContentTypeMap() {
        return contentTypeMap;
    }

    /**
     * Mapping from filename suffixes (e.g., "baclava") to content types.
     * 
     * @param contentTypeMap
     *            The mapping to install.
     */
    @Required
    public void setContentTypeMap(Map<String, String> contentTypeMap) {
        this.contentTypeMap = contentTypeMap;
    }

    /**
     * Test whether the current user can do updates to the given run.
     * 
     * @param run
     *            The workflow run to do the test on.
     * @throws NoUpdateException
     *             If the current user is not permitted to update the run.
     */
    public void permitUpdate(@NonNull TavernaRun run) throws NoUpdateException {
        if (isSuperUser()) {
            accessLog.warn("check for admin powers passed; elevated access rights granted for update");
            return; // Superusers are fully authorized to access others things
        }
        policy.permitUpdate(getPrincipal(), run);
    }

    /**
     * Test whether the current user can destroy or control the lifespan of the
     * given run.
     * 
     * @param run
     *            The workflow run to do the test on.
     * @throws NoDestroyException
     *             If the current user is not permitted to destroy the run.
     */
    public void permitDestroy(TavernaRun run) throws NoDestroyException {
        if (isSuperUser()) {
            accessLog.warn("check for admin powers passed; elevated access rights granted for destroy");
            return; // Superusers are fully authorized to access others things
        }
        policy.permitDestroy(getPrincipal(), run);
    }

    /**
     * Gets the identity of the user currently accessing the webapp, which is
     * stored in a thread-safe way in the webapp's container's context.
     * 
     * @return The identity of the user accessing the webapp.
     */
    @NonNull
    public UsernamePrincipal getPrincipal() {
        try {
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            if (auth == null || !auth.isAuthenticated()) {
                if (logGetPrincipalFailures)
                    log.warn("failed to get auth; going with <NOBODY>");
                return new UsernamePrincipal("<NOBODY>");
            }
            return new UsernamePrincipal(auth);
        } catch (RuntimeException e) {
            if (logGetPrincipalFailures)
                log.info("failed to map principal", e);
            throw e;
        }
    }

    /**
     * Obtain the workflow run with a particular name.
     * 
     * @param name
     *            The name of the run to look up.
     * @return A workflow run handle that the current user has at least
     *         permission to read.
     * @throws UnknownRunException
     *             If the workflow run doesn't exist or the current user doesn't
     *             have permission to see it.
     */
    @NonNull
    public TavernaRun getRun(@NonNull String name) throws UnknownRunException {
        if (isSuperUser()) {
            accessLog.info("check for admin powers passed; elevated access rights granted for read");
            return runStore.getRun(name);
        }
        return runStore.getRun(getPrincipal(), policy, name);
    }

    /**
     * Construct a listener attached to the given run.
     * 
     * @param run
     *            The workflow run to attach the listener to.
     * @param type
     *            The name of the type of run to create.
     * @param configuration
     *            The configuration description to pass into the listener. The
     *            format of this string is up to the listener to define.
     * @return A handle to the listener which can be used to further configure
     *         any properties.
     * @throws NoListenerException
     *             If the listener type is unrecognized or the configuration is
     *             invalid.
     * @throws NoUpdateException
     *             If the run does not permit the current user to add listeners
     *             (or perform other types of update).
     */
    @NonNull
    public Listener makeListener(@NonNull TavernaRun run, @NonNull String type, @NonNull String configuration)
            throws NoListenerException, NoUpdateException {
        permitUpdate(run);
        return listenerFactory.makeListener(run, type, configuration);
    }

    /**
     * Obtain a listener that is already attached to a workflow run.
     * 
     * @param run
     *            The workflow run to search.
     * @param listenerName
     *            The name of the listener to look up.
     * @return The listener instance interface.
     * @throws NoListenerException
     *             If no listener with that name exists.
     */
    @NonNull
    public Listener getListener(TavernaRun run, String listenerName) throws NoListenerException {
        for (Listener l : run.getListeners())
            if (l.getName().equals(listenerName))
                return l;
        throw new NoListenerException();
    }

    /**
     * Get the permission description for the given user.
     * 
     * @param context
     *            A security context associated with a particular workflow run.
     *            Note that only the owner of a workflow run may get the
     *            security context in the first place.
     * @param userName
     *            The name of the user to look up the permission for.
     * @return A permission description.
     */
    @NonNull
    public Permission getPermission(@NonNull TavernaSecurityContext context, @NonNull String userName) {
        if (context.getPermittedDestroyers().contains(userName))
            return Permission.Destroy;
        if (context.getPermittedUpdaters().contains(userName))
            return Permission.Update;
        if (context.getPermittedReaders().contains(userName))
            return Permission.Read;
        return Permission.None;
    }

    /**
     * Set the permissions for the given user.
     * 
     * @param context
     *            A security context associated with a particular workflow run.
     *            Note that only the owner of a workflow run may get the
     *            security context in the first place.
     * @param userName
     *            The name of the user to set the permission for.
     * @param permission
     *            The description of the permission to grant. Note that the
     *            owner of a workflow run always has the equivalent of
     *            {@link Permission#Destroy}; this is always enforced before
     *            checking for other permissions.
     */
    @SuppressWarnings("SF_SWITCH_FALLTHROUGH")
    public void setPermission(TavernaSecurityContext context, String userName, Permission permission) {
        Set<String> permSet;
        boolean doRead = false, doWrite = false, doKill = false;

        switch (permission) {
        case Destroy:
            doKill = true;
        case Update:
            doWrite = true;
        case Read:
            doRead = true;
        }

        permSet = context.getPermittedReaders();
        if (doRead) {
            if (!permSet.contains(userName)) {
                permSet = new HashSet<String>(permSet);
                permSet.add(userName);
                context.setPermittedReaders(permSet);
            }
        } else if (permSet.contains(userName)) {
            permSet = new HashSet<String>(permSet);
            permSet.remove(userName);
            context.setPermittedReaders(permSet);
        }

        permSet = context.getPermittedUpdaters();
        if (doWrite) {
            if (!permSet.contains(userName)) {
                permSet = new HashSet<String>(permSet);
                permSet.add(userName);
                context.setPermittedUpdaters(permSet);
            }
        } else if (permSet.contains(userName)) {
            permSet = new HashSet<String>(permSet);
            permSet.remove(userName);
            context.setPermittedUpdaters(permSet);
        }

        permSet = context.getPermittedDestroyers();
        if (doKill) {
            if (!permSet.contains(userName)) {
                permSet = new HashSet<String>(permSet);
                permSet.add(userName);
                context.setPermittedDestroyers(permSet);
            }
        } else if (permSet.contains(userName)) {
            permSet = new HashSet<String>(permSet);
            permSet.remove(userName);
            context.setPermittedDestroyers(permSet);
        }
    }

    /**
     * Stops a run from being possible to be looked up and destroys it.
     * 
     * @param runName
     *            The name of the run.
     * @param run
     *            The workflow run. <i>Must</i> correspond to the name.
     * @throws NoDestroyException
     *             If the user is not permitted to destroy the workflow run.
     * @throws UnknownRunException
     *             If the run is unknown (e.g., because it is already
     *             destroyed).
     */
    public void unregisterRun(@NonNull String runName, @NonNull TavernaRun run)
            throws NoDestroyException, UnknownRunException {
        if (run == null)
            run = getRun(runName);
        policy.permitDestroy(getPrincipal(), run);
        runStore.unregisterRun(runName);
        run.destroy();
    }

    /**
     * Changes the expiry date of a workflow run. The expiry date is when the
     * workflow run becomes eligible for automated destruction.
     * 
     * @param run
     *            The handle to the workflow run.
     * @param date
     *            When the workflow run should be expired.
     * @return When the workflow run will actually be expired.
     * @throws NoDestroyException
     *             If the user is not permitted to destroy the workflow run.
     *             (Note that lifespan management requires the ability to
     *             destroy.)
     */
    @NonNull
    public Date updateExpiry(@NonNull TavernaRun run, @NonNull Date date) throws NoDestroyException {
        policy.permitDestroy(getPrincipal(), run);
        run.setExpiry(date);
        return run.getExpiry();
    }

    /**
     * Manufacture a workflow run instance.
     * 
     * @param workflow
     *            The workflow document (t2flow, scufl2?) to instantiate.
     * @return The ID of the created workflow run.
     * @throws NoCreateException
     *             If the user is not permitted to create workflows.
     */
    public String buildWorkflow(Workflow workflow) throws NoCreateException {
        UsernamePrincipal p = getPrincipal();
        if (!stateModel.getAllowNewWorkflowRuns())
            throw new NoCreateException("run creation not currently enabled");
        try {
            if (stateModel.getLogIncomingWorkflows()) {
                log.info(workflow.marshal());
            }
        } catch (JAXBException e) {
            log.warn("problem when logging workflow", e);
        }

        // Security checks
        policy.permitCreate(p, workflow);
        if (idMapper != null && idMapper.getUsernameForPrincipal(p) == null) {
            log.error("cannot map principal to local user id");
            throw new NoCreateException("failed to map security token to local user id");
        }

        TavernaRun run;
        try {
            run = runFactory.create(p, workflow);
            TavernaSecurityContext c = run.getSecurityContext();
            c.initializeSecurityFromContext(SecurityContextHolder.getContext());
            webapp.initObsoleteSecurity(c);
        } catch (Exception e) {
            log.error("failed to build workflow run worker", e);
            throw new NoCreateException("failed to build workflow run worker");
        }

        return runStore.registerRun(run);
    }

    private boolean isSuperUser() {
        try {
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            if (auth == null || !auth.isAuthenticated())
                return false;
            UserDetails details = (UserDetails) auth.getPrincipal();
            if (log.isDebugEnabled())
                log.debug("checking for admin role for user <" + auth.getName() + "> in collection "
                        + details.getAuthorities());
            return details.getAuthorities().contains(ADMIN);
        } catch (ClassCastException e) {
            return false;
        }
    }

    /**
     * Get a particular input to a workflow run.
     * 
     * @param run
     *            The workflow run to search.
     * @param portName
     *            The name of the input.
     * @return The handle of the input, or <tt>null</tt> if no such handle
     *         exists.
     */
    @Nullable
    public Input getInput(TavernaRun run, String portName) {
        for (Input i : run.getInputs())
            if (i.getName().equals(portName))
                return i;
        return null;
    }

    /**
     * Get a listener attached to a run.
     * 
     * @param runName
     *            The name of the run to look up
     * @param listenerName
     *            The name of the listener.
     * @return The handle of the listener.
     * @throws NoListenerException
     *             If no such listener exists.
     * @throws UnknownRunException
     *             If no such workflow run exists, or if the user does not have
     *             permission to access it.
     */
    public Listener getListener(String runName, String listenerName)
            throws NoListenerException, UnknownRunException {
        return getListener(getRun(runName), listenerName);
    }

    /**
     * Given a file, produce a guess at its content type. This uses the content
     * type map property, and if that search fails it falls back on the Medsea
     * mime type library.
     * 
     * @param f
     *            The file handle.
     * @return The content type. If all else fails, produces good old
     *         "application/octet-stream".
     */
    @NonNull
    public String getEstimatedContentType(@NonNull File f) {
        String name = f.getName();
        for (int idx = name.indexOf('.'); idx != -1; idx = name.indexOf('.', idx + 1)) {
            String mt = contentTypeMap.get(name.substring(idx + 1));
            if (mt != null)
                return mt;
        }
        try {
            return getMimeType(new ByteArrayInputStream(f.getContents(0, SAMPLE_SIZE)));
        } catch (FilesystemAccessException e) {
            // Ignore; fall back to just serving as bytes
            return APPLICATION_OCTET_STREAM;
        }
    }

    public void copyDataToFile(DataHandler handler, File file) throws FilesystemAccessException {
        try {
            copyStreamToFile(handler.getInputStream(), file);
        } catch (IOException e) {
            throw new FilesystemAccessException("problem constructing stream from data source", e);
        }
    }

    public void copyStreamToFile(InputStream stream, File file) throws FilesystemAccessException {
        String name = file.getFullName();
        long total = 0;
        try {
            byte[] buffer = new byte[TRANSFER_SIZE];
            while (true) {
                int len = stream.read(buffer);
                if (len < 0)
                    break;
                total += len;
                log.debug("read " + len + " bytes from source stream (total: " + total + ") bound for " + name);
                if (len == buffer.length)
                    file.appendContents(buffer);
                else {
                    byte[] newBuf = new byte[len];
                    System.arraycopy(buffer, 0, newBuf, 0, len);
                    file.appendContents(newBuf);
                }
            }
        } catch (IOException exn) {
            throw new FilesystemAccessException("failed to transfer bytes", exn);
        }
    }
}