http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/TavernaServer.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/TavernaServer.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/TavernaServer.java new file mode 100644 index 0000000..0f98da6 --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/TavernaServer.java @@ -0,0 +1,1425 @@ +/* + * Copyright (C) 2010-2011 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master; + +import static java.lang.Math.min; +import static java.util.Collections.emptyMap; +import static java.util.Collections.sort; +import static java.util.UUID.randomUUID; +import static javax.ws.rs.core.Response.created; +import static javax.ws.rs.core.UriBuilder.fromUri; +import static javax.xml.ws.handler.MessageContext.HTTP_REQUEST_HEADERS; +import static javax.xml.ws.handler.MessageContext.PATH_INFO; +import static org.apache.commons.io.IOUtils.toByteArray; +import static org.apache.commons.logging.LogFactory.getLog; +import static org.taverna.server.master.TavernaServerSupport.PROV_BUNDLE; +import static org.taverna.server.master.common.DirEntryReference.newInstance; +import static org.taverna.server.master.common.Namespaces.SERVER_SOAP; +import static org.taverna.server.master.common.Roles.ADMIN; +import static org.taverna.server.master.common.Roles.SELF; +import static org.taverna.server.master.common.Roles.USER; +import static org.taverna.server.master.common.Status.Initialized; +import static org.taverna.server.master.common.Uri.secure; +import static org.taverna.server.master.soap.DirEntry.convert; +import static org.taverna.server.master.utils.RestUtils.opt; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.net.URI; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.PreDestroy; +import javax.annotation.Resource; +import javax.annotation.security.DeclareRoles; +import javax.annotation.security.RolesAllowed; +import javax.jws.WebService; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; +import javax.xml.bind.JAXBException; +import javax.xml.ws.WebServiceContext; + +import org.apache.commons.logging.Log; +import org.apache.cxf.annotations.WSDLDocumentation; +import org.ogf.usage.JobUsageRecord; +import org.springframework.beans.factory.annotation.Required; +import org.taverna.server.master.api.SupportAware; +import org.taverna.server.master.api.TavernaServerBean; +import org.taverna.server.master.common.Capability; +import org.taverna.server.master.common.Credential; +import org.taverna.server.master.common.DirEntryReference; +import org.taverna.server.master.common.InputDescription; +import org.taverna.server.master.common.Permission; +import org.taverna.server.master.common.ProfileList; +import org.taverna.server.master.common.RunReference; +import org.taverna.server.master.common.Status; +import org.taverna.server.master.common.Trust; +import org.taverna.server.master.common.Workflow; +import org.taverna.server.master.common.version.Version; +import org.taverna.server.master.exceptions.BadPropertyValueException; +import org.taverna.server.master.exceptions.BadStateChangeException; +import org.taverna.server.master.exceptions.FilesystemAccessException; +import org.taverna.server.master.exceptions.InvalidCredentialException; +import org.taverna.server.master.exceptions.NoCreateException; +import org.taverna.server.master.exceptions.NoCredentialException; +import org.taverna.server.master.exceptions.NoDirectoryEntryException; +import org.taverna.server.master.exceptions.NoListenerException; +import org.taverna.server.master.exceptions.NoUpdateException; +import org.taverna.server.master.exceptions.NotOwnerException; +import org.taverna.server.master.exceptions.OverloadedException; +import org.taverna.server.master.exceptions.UnknownRunException; +import org.taverna.server.master.factories.ListenerFactory; +import org.taverna.server.master.interfaces.Directory; +import org.taverna.server.master.interfaces.DirectoryEntry; +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.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.notification.NotificationEngine; +import org.taverna.server.master.notification.atom.EventDAO; +import org.taverna.server.master.rest.TavernaServerREST; +import org.taverna.server.master.rest.TavernaServerREST.EnabledNotificationFabrics; +import org.taverna.server.master.rest.TavernaServerREST.PermittedListeners; +import org.taverna.server.master.rest.TavernaServerREST.PermittedWorkflows; +import org.taverna.server.master.rest.TavernaServerREST.PolicyView; +import org.taverna.server.master.rest.TavernaServerRunREST; +import org.taverna.server.master.soap.DirEntry; +import org.taverna.server.master.soap.FileContents; +import org.taverna.server.master.soap.PermissionList; +import org.taverna.server.master.soap.TavernaServerSOAP; +import org.taverna.server.master.soap.WrappedWorkflow; +import org.taverna.server.master.soap.ZippedDirectory; +import org.taverna.server.master.utils.CallTimeLogger.PerfLogged; +import org.taverna.server.master.utils.FilenameUtils; +import org.taverna.server.master.utils.InvocationCounter.CallCounted; +import org.taverna.server.port_description.OutputDescription; + +/** + * The core implementation of the web application. + * + * @author Donal Fellows + */ +@Path("/") +@DeclareRoles({ USER, ADMIN }) +@WebService(endpointInterface = "org.taverna.server.master.soap.TavernaServerSOAP", serviceName = "TavernaServer", targetNamespace = SERVER_SOAP) +@WSDLDocumentation("An instance of Taverna " + Version.JAVA + " Server.") +public abstract class TavernaServer implements TavernaServerSOAP, + TavernaServerREST, TavernaServerBean { + /** + * The root of descriptions of the server in JMX. + */ + public static final String JMX_ROOT = "Taverna:group=Server-" + + Version.JAVA + ",name="; + + /** The logger for the server framework. */ + public Log log = getLog("Taverna.Server.Webapp"); + + @PreDestroy + void closeLog() { + log = null; + } + + // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + // CONNECTIONS TO JMX, SPRING AND CXF + + @Resource + WebServiceContext jaxws; + @Context + private HttpHeaders jaxrsHeaders; + + // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + // STATE VARIABLES AND SPRING SETTERS + + /** + * For building descriptions of the expected inputs and actual outputs of a + * workflow. + */ + private ContentsDescriptorBuilder cdBuilder; + /** + * Utilities for accessing files on the local-worker. + */ + private FilenameUtils fileUtils; + /** How notifications are dispatched. */ + private NotificationEngine notificationEngine; + /** Main support class. */ + private TavernaServerSupport support; + /** A storage facility for workflow runs. */ + private RunStore runStore; + /** Encapsulates the policies applied by this server. */ + private Policy policy; + /** Where Atom events come from. */ + EventDAO eventSource; + /** Reference to the main interaction feed. */ + private String interactionFeed; + + @Override + @Required + public void setFileUtils(FilenameUtils converter) { + this.fileUtils = converter; + } + + @Override + @Required + public void setContentsDescriptorBuilder(ContentsDescriptorBuilder cdBuilder) { + this.cdBuilder = cdBuilder; + } + + @Override + @Required + public void setNotificationEngine(NotificationEngine notificationEngine) { + this.notificationEngine = notificationEngine; + } + + /** + * @param support + * the support to set + */ + @Override + @Required + public void setSupport(TavernaServerSupport support) { + this.support = support; + } + + @Override + @Required + public void setRunStore(RunStore runStore) { + this.runStore = runStore; + } + + @Override + @Required + public void setPolicy(Policy policy) { + this.policy = policy; + } + + @Override + @Required + public void setEventSource(EventDAO eventSource) { + this.eventSource = eventSource; + } + + /** + * The location of a service-wide interaction feed, derived from a + * properties file. Expected to be <i>actually</i> not set (to a real + * value). + * + * @param interactionFeed + * The URL, which will be resolved relative to the location of + * the webapp, or the string "<tt>none</tt>" (which corresponds + * to a <tt>null</tt>). + */ + public void setInteractionFeed(String interactionFeed) { + if ("none".equals(interactionFeed)) + interactionFeed = null; + else if (interactionFeed != null && interactionFeed.startsWith("${")) + interactionFeed = null; + this.interactionFeed = interactionFeed; + } + + // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + // REST INTERFACE + + @Override + @CallCounted + @PerfLogged + public ServerDescription describeService(UriInfo ui) { + jaxrsUriInfo.set(new WeakReference<>(ui)); + return new ServerDescription(ui, resolve(interactionFeed)); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public RunList listUsersRuns(UriInfo ui) { + jaxrsUriInfo.set(new WeakReference<>(ui)); + return new RunList(runs(), secure(ui).path("{name}")); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public Response submitWorkflow(Workflow workflow, UriInfo ui) + throws NoUpdateException { + jaxrsUriInfo.set(new WeakReference<>(ui)); + checkCreatePolicy(workflow); + String name = support.buildWorkflow(workflow); + return created(secure(ui).path("{uuid}").build(name)).build(); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public Response submitWorkflowByURL(List<URI> referenceList, UriInfo ui) + throws NoCreateException { + jaxrsUriInfo.set(new WeakReference<>(ui)); + if (referenceList == null || referenceList.size() == 0) + throw new NoCreateException("no workflow URI supplied"); + URI workflowURI = referenceList.get(0); + checkCreatePolicy(workflowURI); + Workflow workflow; + try { + workflow = support.getWorkflowDocumentFromURI(workflowURI); + } catch (IOException e) { + throw new NoCreateException("could not read workflow", e); + } + String name = support.buildWorkflow(workflow); + return created(secure(ui).path("{uuid}").build(name)).build(); + } + + @Override + @CallCounted + @PerfLogged + public int getServerMaxRuns() { + return support.getMaxSimultaneousRuns(); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed({ USER, SELF }) + public TavernaServerRunREST getRunResource(String runName, UriInfo ui) + throws UnknownRunException { + jaxrsUriInfo.set(new WeakReference<>(ui)); + RunREST rr = makeRunInterface(); + rr.setRun(support.getRun(runName)); + rr.setRunName(runName); + return rr; + } + + private ThreadLocal<Reference<UriInfo>> jaxrsUriInfo = new InheritableThreadLocal<>(); + + private UriInfo getUriInfo() { + if (jaxrsUriInfo.get() == null) + return null; + return jaxrsUriInfo.get().get(); + } + + @Override + @CallCounted + public abstract PolicyView getPolicyDescription(); + + @Override + @CallCounted + public Response serviceOptions() { + return opt(); + } + + @Override + @CallCounted + public Response runsOptions() { + return opt("POST"); + } + + /** + * Construct a RESTful interface to a run. + * + * @return The handle to the interface, as decorated by Spring. + */ + protected abstract RunREST makeRunInterface(); + + // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + // SOAP INTERFACE + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public RunReference[] listRuns() { + ArrayList<RunReference> ws = new ArrayList<>(); + UriBuilder ub = getRunUriBuilder(); + for (String runName : runs().keySet()) + ws.add(new RunReference(runName, ub)); + return ws.toArray(new RunReference[ws.size()]); + } + + private void checkCreatePolicy(Workflow workflow) throws NoCreateException { + List<URI> pwu = policy + .listPermittedWorkflowURIs(support.getPrincipal()); + if (pwu == null || pwu.size() == 0) + return; + throw new NoCreateException("server policy: will only start " + + "workflows sourced from permitted URI list"); + } + + private void checkCreatePolicy(URI workflowURI) throws NoCreateException { + List<URI> pwu = policy + .listPermittedWorkflowURIs(support.getPrincipal()); + if (pwu == null || pwu.size() == 0 || pwu.contains(workflowURI)) + return; + throw new NoCreateException("workflow URI not on permitted list"); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public RunReference submitWorkflow(Workflow workflow) + throws NoUpdateException { + checkCreatePolicy(workflow); + String name = support.buildWorkflow(workflow); + return new RunReference(name, getRunUriBuilder()); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public RunReference submitWorkflowMTOM(WrappedWorkflow workflow) + throws NoUpdateException { + Workflow wf; + try { + wf = workflow.getWorkflow(); + } catch (IOException e) { + throw new NoCreateException(e.getMessage(), e); + } + checkCreatePolicy(wf); + String name = support.buildWorkflow(wf); + return new RunReference(name, getRunUriBuilder()); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public RunReference submitWorkflowByURI(URI workflowURI) + throws NoCreateException { + checkCreatePolicy(workflowURI); + Workflow workflow; + try { + workflow = support.getWorkflowDocumentFromURI(workflowURI); + } catch (IOException e) { + throw new NoCreateException("could not read workflow", e); + } + String name = support.buildWorkflow(workflow); + return new RunReference(name, getRunUriBuilder()); + } + + @Override + @CallCounted + @PerfLogged + public URI[] getServerWorkflows() { + return support.getPermittedWorkflowURIs(); + } + + @Override + @CallCounted + @PerfLogged + public String[] getServerListeners() { + List<String> types = support.getListenerTypes(); + return types.toArray(new String[types.size()]); + } + + @Override + @CallCounted + @PerfLogged + public String[] getServerNotifiers() { + List<String> dispatchers = notificationEngine + .listAvailableDispatchers(); + return dispatchers.toArray(new String[dispatchers.size()]); + } + + @Override + @CallCounted + @PerfLogged + public List<Capability> getServerCapabilities() { + return support.getCapabilities(); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public void destroyRun(String runName) throws UnknownRunException, + NoUpdateException { + support.unregisterRun(runName, null); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public String getRunDescriptiveName(String runName) + throws UnknownRunException { + return support.getRun(runName).getName(); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public void setRunDescriptiveName(String runName, String descriptiveName) + throws UnknownRunException, NoUpdateException { + TavernaRun run = support.getRun(runName); + support.permitUpdate(run); + run.setName(descriptiveName); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public Workflow getRunWorkflow(String runName) throws UnknownRunException { + return support.getRun(runName).getWorkflow(); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public WrappedWorkflow getRunWorkflowMTOM(String runName) + throws UnknownRunException { + WrappedWorkflow ww = new WrappedWorkflow(); + ww.setWorkflow(support.getRun(runName).getWorkflow()); + return ww; + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public ProfileList getRunWorkflowProfiles(String runName) + throws UnknownRunException { + return support.getProfileDescriptor(support.getRun(runName) + .getWorkflow()); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public Date getRunExpiry(String runName) throws UnknownRunException { + return support.getRun(runName).getExpiry(); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public void setRunExpiry(String runName, Date d) + throws UnknownRunException, NoUpdateException { + support.updateExpiry(support.getRun(runName), d); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public Date getRunCreationTime(String runName) throws UnknownRunException { + return support.getRun(runName).getCreationTimestamp(); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public Date getRunFinishTime(String runName) throws UnknownRunException { + return support.getRun(runName).getFinishTimestamp(); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public Date getRunStartTime(String runName) throws UnknownRunException { + return support.getRun(runName).getStartTimestamp(); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public Status getRunStatus(String runName) throws UnknownRunException { + return support.getRun(runName).getStatus(); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public String setRunStatus(String runName, Status s) + throws UnknownRunException, NoUpdateException { + TavernaRun w = support.getRun(runName); + support.permitUpdate(w); + if (s == Status.Operating && w.getStatus() == Status.Initialized) { + if (!support.getAllowStartWorkflowRuns()) + throw new OverloadedException(); + try { + String issue = w.setStatus(s); + if (issue == null) + return ""; + if (issue.isEmpty()) + return "unknown reason for partial change"; + return issue; + } catch (RuntimeException | NoUpdateException e) { + log.info("failed to start run " + runName, e); + throw e; + } + } else { + w.setStatus(s); + return ""; + } + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public String getRunStdout(String runName) throws UnknownRunException { + try { + return support.getProperty(runName, "io", "stdout"); + } catch (NoListenerException e) { + return ""; + } + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public String getRunStderr(String runName) throws UnknownRunException { + try { + return support.getProperty(runName, "io", "stderr"); + } catch (NoListenerException e) { + return ""; + } + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public JobUsageRecord getRunUsageRecord(String runName) + throws UnknownRunException { + try { + String ur = support.getProperty(runName, "io", "usageRecord"); + if (ur.isEmpty()) + return null; + return JobUsageRecord.unmarshal(ur); + } catch (NoListenerException e) { + return null; + } catch (JAXBException e) { + log.info("failed to deserialize non-empty usage record", e); + return null; + } + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public String getRunLog(String runName) throws UnknownRunException { + try { + return support.getLogs(support.getRun(runName)).get("UTF-8"); + } catch (UnsupportedEncodingException e) { + log.warn("unexpected encoding problem", e); + return ""; + } + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public FileContents getRunBundle(String runName) + throws UnknownRunException, FilesystemAccessException, + NoDirectoryEntryException { + File f = fileUtils.getFile(support.getRun(runName), PROV_BUNDLE); + FileContents fc = new FileContents(); + // We *know* the content type, by definition + fc.setFile(f, "application/vnd.wf4ever.robundle+zip"); + return fc; + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public boolean getRunGenerateProvenance(String runName) + throws UnknownRunException { + return support.getRun(runName).getGenerateProvenance(); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public void setRunGenerateProvenance(String runName, boolean generate) + throws UnknownRunException, NoUpdateException { + TavernaRun run = support.getRun(runName); + support.permitUpdate(run); + run.setGenerateProvenance(generate); + } + + // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + // SOAP INTERFACE - Security + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public String getRunOwner(String runName) throws UnknownRunException { + return support.getRun(runName).getSecurityContext().getOwner() + .getName(); + } + + /** + * Look up a security context, applying access control rules for access to + * the parts of the context that are only open to the owner. + * + * @param runName + * The name of the workflow run. + * @param initialOnly + * Whether to check if we're in the initial state. + * @return The security context. Never <tt>null</tt>. + * @throws UnknownRunException + * @throws NotOwnerException + * @throws BadStateChangeException + */ + private TavernaSecurityContext getRunSecurityContext(String runName, + boolean initialOnly) throws UnknownRunException, NotOwnerException, + BadStateChangeException { + TavernaRun run = support.getRun(runName); + TavernaSecurityContext c = run.getSecurityContext(); + if (!c.getOwner().equals(support.getPrincipal())) + throw new NotOwnerException(); + if (initialOnly && run.getStatus() != Initialized) + throw new BadStateChangeException(); + return c; + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public Credential[] getRunCredentials(String runName) + throws UnknownRunException, NotOwnerException { + try { + return getRunSecurityContext(runName, false).getCredentials(); + } catch (BadStateChangeException e) { + Error e2 = new Error("impossible"); + e2.initCause(e); + throw e2; + } + } + + private Credential findCredential(TavernaSecurityContext c, String id) + throws NoCredentialException { + for (Credential t : c.getCredentials()) + if (t.id.equals(id)) + return t; + throw new NoCredentialException(); + } + + private Trust findTrust(TavernaSecurityContext c, String id) + throws NoCredentialException { + for (Trust t : c.getTrusted()) + if (t.id.equals(id)) + return t; + throw new NoCredentialException(); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public String setRunCredential(String runName, String credentialID, + Credential credential) throws UnknownRunException, + NotOwnerException, InvalidCredentialException, + NoCredentialException, BadStateChangeException { + TavernaSecurityContext c = getRunSecurityContext(runName, true); + if (credentialID == null || credentialID.isEmpty()) { + credential.id = randomUUID().toString(); + } else { + credential.id = findCredential(c, credentialID).id; + } + URI uri = getRunUriBuilder().path("security/credentials/{credid}") + .build(runName, credential.id); + credential.href = uri.toString(); + c.validateCredential(credential); + c.addCredential(credential); + return credential.id; + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public void deleteRunCredential(String runName, String credentialID) + throws UnknownRunException, NotOwnerException, + NoCredentialException, BadStateChangeException { + getRunSecurityContext(runName, true).deleteCredential( + new Credential.Dummy(credentialID)); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public Trust[] getRunCertificates(String runName) + throws UnknownRunException, NotOwnerException { + try { + return getRunSecurityContext(runName, false).getTrusted(); + } catch (BadStateChangeException e) { + Error e2 = new Error("impossible"); + e2.initCause(e); + throw e2; + } + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public String setRunCertificates(String runName, String certificateID, + Trust certificate) throws UnknownRunException, NotOwnerException, + InvalidCredentialException, NoCredentialException, + BadStateChangeException { + TavernaSecurityContext c = getRunSecurityContext(runName, true); + if (certificateID == null || certificateID.isEmpty()) { + certificate.id = randomUUID().toString(); + } else { + certificate.id = findTrust(c, certificateID).id; + } + URI uri = getRunUriBuilder().path("security/trusts/{certid}").build( + runName, certificate.id); + certificate.href = uri.toString(); + c.validateTrusted(certificate); + c.addTrusted(certificate); + return certificate.id; + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public void deleteRunCertificates(String runName, String certificateID) + throws UnknownRunException, NotOwnerException, + NoCredentialException, BadStateChangeException { + TavernaSecurityContext c = getRunSecurityContext(runName, true); + Trust toDelete = new Trust(); + toDelete.id = certificateID; + c.deleteTrusted(toDelete); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public PermissionList listRunPermissions(String runName) + throws UnknownRunException, NotOwnerException { + PermissionList pl = new PermissionList(); + pl.permission = new ArrayList<>(); + Map<String, Permission> perm; + try { + perm = support.getPermissionMap(getRunSecurityContext(runName, + false)); + } catch (BadStateChangeException e) { + log.error("unexpected error from internal API", e); + perm = emptyMap(); + } + List<String> users = new ArrayList<>(perm.keySet()); + sort(users); + for (String user : users) + pl.permission.add(new PermissionList.SinglePermissionMapping(user, + perm.get(user))); + return pl; + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public void setRunPermission(String runName, String userName, + Permission permission) throws UnknownRunException, + NotOwnerException { + try { + support.setPermission(getRunSecurityContext(runName, false), + userName, permission); + } catch (BadStateChangeException e) { + log.error("unexpected error from internal API", e); + } + } + + // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + // SOAP INTERFACE - Filesystem connection + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public OutputDescription getRunOutputDescription(String runName) + throws UnknownRunException, BadStateChangeException, + FilesystemAccessException, NoDirectoryEntryException { + TavernaRun run = support.getRun(runName); + if (run.getStatus() == Initialized) + throw new BadStateChangeException( + "may not get output description in initial state"); + return cdBuilder.makeOutputDescriptor(run, null); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public DirEntry[] getRunDirectoryContents(String runName, DirEntry d) + throws UnknownRunException, FilesystemAccessException, + NoDirectoryEntryException { + List<DirEntry> result = new ArrayList<>(); + for (DirectoryEntry e : fileUtils.getDirectory(support.getRun(runName), + convert(d)).getContents()) + result.add(convert(newInstance(null, e))); + return result.toArray(new DirEntry[result.size()]); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public byte[] getRunDirectoryAsZip(String runName, DirEntry d) + throws UnknownRunException, FilesystemAccessException, + NoDirectoryEntryException { + try { + return toByteArray(fileUtils.getDirectory(support.getRun(runName), + convert(d)).getContentsAsZip()); + } catch (IOException e) { + throw new FilesystemAccessException("problem serializing ZIP data", + e); + } + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public ZippedDirectory getRunDirectoryAsZipMTOM(String runName, DirEntry d) + throws UnknownRunException, FilesystemAccessException, + NoDirectoryEntryException { + return new ZippedDirectory(fileUtils.getDirectory( + support.getRun(runName), convert(d))); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public DirEntry makeRunDirectory(String runName, DirEntry parent, + String name) throws UnknownRunException, NoUpdateException, + FilesystemAccessException, NoDirectoryEntryException { + TavernaRun w = support.getRun(runName); + support.permitUpdate(w); + Directory dir = fileUtils.getDirectory(w, convert(parent)) + .makeSubdirectory(support.getPrincipal(), name); + return convert(newInstance(null, dir)); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public DirEntry makeRunFile(String runName, DirEntry parent, String name) + throws UnknownRunException, NoUpdateException, + FilesystemAccessException, NoDirectoryEntryException { + TavernaRun w = support.getRun(runName); + support.permitUpdate(w); + File f = fileUtils.getDirectory(w, convert(parent)).makeEmptyFile( + support.getPrincipal(), name); + return convert(newInstance(null, f)); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public void destroyRunDirectoryEntry(String runName, DirEntry d) + throws UnknownRunException, NoUpdateException, + FilesystemAccessException, NoDirectoryEntryException { + TavernaRun w = support.getRun(runName); + support.permitUpdate(w); + fileUtils.getDirEntry(w, convert(d)).destroy(); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public byte[] getRunFileContents(String runName, DirEntry d) + throws UnknownRunException, FilesystemAccessException, + NoDirectoryEntryException { + File f = fileUtils.getFile(support.getRun(runName), convert(d)); + return f.getContents(0, -1); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public void setRunFileContents(String runName, DirEntry d, + byte[] newContents) throws UnknownRunException, NoUpdateException, + FilesystemAccessException, NoDirectoryEntryException { + TavernaRun w = support.getRun(runName); + support.permitUpdate(w); + fileUtils.getFile(w, convert(d)).setContents(newContents); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public FileContents getRunFileContentsMTOM(String runName, DirEntry d) + throws UnknownRunException, FilesystemAccessException, + NoDirectoryEntryException { + File f = fileUtils.getFile(support.getRun(runName), convert(d)); + FileContents fc = new FileContents(); + fc.setFile(f, support.getEstimatedContentType(f)); + return fc; + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public void setRunFileContentsFromURI(String runName, + DirEntryReference file, URI reference) + throws UnknownRunException, NoUpdateException, + FilesystemAccessException, NoDirectoryEntryException { + TavernaRun run = support.getRun(runName); + support.permitUpdate(run); + File f = fileUtils.getFile(run, file); + try { + support.copyDataToFile(reference, f); + } catch (IOException e) { + throw new FilesystemAccessException( + "problem transferring data from URI", e); + } + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public void setRunFileContentsMTOM(String runName, FileContents newContents) + throws UnknownRunException, NoUpdateException, + FilesystemAccessException, NoDirectoryEntryException { + TavernaRun run = support.getRun(runName); + support.permitUpdate(run); + File f = fileUtils.getFile(run, newContents.name); + f.setContents(new byte[0]); + support.copyDataToFile(newContents.fileData, f); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public String getRunFileType(String runName, DirEntry d) + throws UnknownRunException, FilesystemAccessException, + NoDirectoryEntryException { + return support.getEstimatedContentType(fileUtils.getFile( + support.getRun(runName), convert(d))); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public long getRunFileLength(String runName, DirEntry d) + throws UnknownRunException, FilesystemAccessException, + NoDirectoryEntryException { + return fileUtils.getFile(support.getRun(runName), convert(d)).getSize(); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public Date getRunFileModified(String runName, DirEntry d) + throws UnknownRunException, FilesystemAccessException, + NoDirectoryEntryException { + return fileUtils.getFile(support.getRun(runName), convert(d)) + .getModificationDate(); + } + + // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + // SOAP INTERFACE - Run listeners + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public String[] getRunListeners(String runName) throws UnknownRunException { + TavernaRun w = support.getRun(runName); + List<String> result = new ArrayList<>(); + for (Listener l : w.getListeners()) + result.add(l.getName()); + return result.toArray(new String[result.size()]); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public String addRunListener(String runName, String listenerType, + String configuration) throws UnknownRunException, + NoUpdateException, NoListenerException { + return support.makeListener(support.getRun(runName), listenerType, + configuration).getName(); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public String getRunListenerConfiguration(String runName, + String listenerName) throws UnknownRunException, + NoListenerException { + return support.getListener(runName, listenerName).getConfiguration(); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public String[] getRunListenerProperties(String runName, String listenerName) + throws UnknownRunException, NoListenerException { + return support.getListener(runName, listenerName).listProperties() + .clone(); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public String getRunListenerProperty(String runName, String listenerName, + String propName) throws UnknownRunException, NoListenerException { + return support.getListener(runName, listenerName).getProperty(propName); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public void setRunListenerProperty(String runName, String listenerName, + String propName, String value) throws UnknownRunException, + NoUpdateException, NoListenerException { + TavernaRun w = support.getRun(runName); + support.permitUpdate(w); + Listener l = support.getListener(w, listenerName); + try { + l.getProperty(propName); // sanity check! + l.setProperty(propName, value); + } catch (RuntimeException e) { + throw new NoListenerException("problem setting property: " + + e.getMessage(), e); + } + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public InputDescription getRunInputs(String runName) + throws UnknownRunException { + return new InputDescription(support.getRun(runName)); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public String getRunOutputBaclavaFile(String runName) + throws UnknownRunException { + return support.getRun(runName).getOutputBaclavaFile(); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public void setRunInputBaclavaFile(String runName, String fileName) + throws UnknownRunException, NoUpdateException, + FilesystemAccessException, BadStateChangeException { + TavernaRun w = support.getRun(runName); + support.permitUpdate(w); + w.setInputBaclavaFile(fileName); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public void setRunInputPortFile(String runName, String portName, + String portFilename) throws UnknownRunException, NoUpdateException, + FilesystemAccessException, BadStateChangeException { + TavernaRun w = support.getRun(runName); + support.permitUpdate(w); + Input i = support.getInput(w, portName); + if (i == null) + i = w.makeInput(portName); + i.setFile(portFilename); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public void setRunInputPortValue(String runName, String portName, + String portValue) throws UnknownRunException, NoUpdateException, + BadStateChangeException { + TavernaRun w = support.getRun(runName); + support.permitUpdate(w); + Input i = support.getInput(w, portName); + if (i == null) + i = w.makeInput(portName); + i.setValue(portValue); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public void setRunInputPortListDelimiter(String runName, String portName, + String delimiter) throws UnknownRunException, NoUpdateException, + BadStateChangeException, BadPropertyValueException { + TavernaRun w = support.getRun(runName); + support.permitUpdate(w); + Input i = support.getInput(w, portName); + if (i == null) + i = w.makeInput(portName); + if (delimiter != null && delimiter.isEmpty()) + delimiter = null; + if (delimiter != null) { + if (delimiter.length() > 1) + throw new BadPropertyValueException("delimiter too long"); + if (delimiter.charAt(0) < 1 || delimiter.charAt(0) > 127) + throw new BadPropertyValueException( + "delimiter character must be non-NUL ASCII"); + } + i.setDelimiter(delimiter); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public void setRunOutputBaclavaFile(String runName, String outputFile) + throws UnknownRunException, NoUpdateException, + FilesystemAccessException, BadStateChangeException { + TavernaRun w = support.getRun(runName); + support.permitUpdate(w); + w.setOutputBaclavaFile(outputFile); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public org.taverna.server.port_description.InputDescription getRunInputDescriptor( + String runName) throws UnknownRunException { + return cdBuilder.makeInputDescriptor(support.getRun(runName), null); + } + + @Override + @CallCounted + @PerfLogged + @RolesAllowed(USER) + public String getServerStatus() { + return support.getAllowNewWorkflowRuns() ? "operational" : "suspended"; + } + + // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + // SUPPORT METHODS + + @Override + public boolean initObsoleteSOAPSecurity(TavernaSecurityContext c) { + try { + javax.xml.ws.handler.MessageContext msgCtxt = (jaxws == null ? null + : jaxws.getMessageContext()); + if (msgCtxt == null) + return true; + c.initializeSecurityFromSOAPContext(msgCtxt); + return false; + } catch (IllegalStateException e) { + /* ignore; not much we can do */ + return true; + } + } + + @Override + public boolean initObsoleteRESTSecurity(TavernaSecurityContext c) { + if (jaxrsHeaders == null) + return true; + c.initializeSecurityFromRESTContext(jaxrsHeaders); + return false; + } + + /** + * A creator of substitute {@link URI} builders. + * + * @return A URI builder configured so that it takes a path parameter that + * corresponds to the run ID (but with no such ID applied). + */ + UriBuilder getRunUriBuilder() { + return getBaseUriBuilder().path("runs/{uuid}"); + } + + @Override + public UriBuilder getRunUriBuilder(TavernaRun run) { + return fromUri(getRunUriBuilder().build(run.getId())); + } + + private final String DEFAULT_HOST = "localhost:8080"; // Crappy default + + private String getHostLocation() { + @java.lang.SuppressWarnings("unchecked") + Map<String, List<String>> headers = (Map<String, List<String>>) jaxws + .getMessageContext().get(HTTP_REQUEST_HEADERS); + if (headers != null) { + List<String> host = headers.get("HOST"); + if (host != null && !host.isEmpty()) + return host.get(0); + } + return DEFAULT_HOST; + } + + @Nonnull + private URI getPossiblyInsecureBaseUri() { + // See if JAX-RS can supply the info + UriInfo ui = getUriInfo(); + if (ui != null && ui.getBaseUri() != null) + return ui.getBaseUri(); + // See if JAX-WS *cannot* supply the info + if (jaxws == null || jaxws.getMessageContext() == null) + // Hack to make the test suite work + return URI.create("http://" + DEFAULT_HOST + + "/taverna-server/rest/"); + String pathInfo = (String) jaxws.getMessageContext().get(PATH_INFO); + pathInfo = pathInfo.replaceFirst("/soap$", "/rest/"); + pathInfo = pathInfo.replaceFirst("/rest/.+$", "/rest/"); + return URI.create("http://" + getHostLocation() + pathInfo); + } + + @Override + public UriBuilder getBaseUriBuilder() { + return secure(fromUri(getPossiblyInsecureBaseUri())); + } + + @Override + @Nullable + public String resolve(@Nullable String uri) { + if (uri == null) + return null; + return secure(getPossiblyInsecureBaseUri(), uri).toString(); + } + + private Map<String, TavernaRun> runs() { + return runStore.listRuns(support.getPrincipal(), policy); + } +} + +/** + * RESTful interface to the policies of a Taverna Server installation. + * + * @author Donal Fellows + */ +class PolicyREST implements PolicyView, SupportAware { + private TavernaServerSupport support; + private Policy policy; + private ListenerFactory listenerFactory; + private NotificationEngine notificationEngine; + + @Override + public void setSupport(TavernaServerSupport support) { + this.support = support; + } + + @Required + public void setPolicy(Policy policy) { + this.policy = policy; + } + + @Required + public void setListenerFactory(ListenerFactory listenerFactory) { + this.listenerFactory = listenerFactory; + } + + @Required + public void setNotificationEngine(NotificationEngine notificationEngine) { + this.notificationEngine = notificationEngine; + } + + @Override + @CallCounted + @PerfLogged + public PolicyDescription getDescription(UriInfo ui) { + return new PolicyDescription(ui); + } + + @Override + @CallCounted + @PerfLogged + public int getMaxSimultaneousRuns() { + Integer limit = policy.getMaxRuns(support.getPrincipal()); + if (limit == null) + return policy.getMaxRuns(); + return min(limit.intValue(), policy.getMaxRuns()); + } + + @Override + @CallCounted + @PerfLogged + public PermittedListeners getPermittedListeners() { + return new PermittedListeners( + listenerFactory.getSupportedListenerTypes()); + } + + @Override + @CallCounted + @PerfLogged + public PermittedWorkflows getPermittedWorkflows() { + return new PermittedWorkflows(policy.listPermittedWorkflowURIs(support + .getPrincipal())); + } + + @Override + @CallCounted + @PerfLogged + public EnabledNotificationFabrics getEnabledNotifiers() { + return new EnabledNotificationFabrics( + notificationEngine.listAvailableDispatchers()); + } + + @Override + @CallCounted + @PerfLogged + public int getMaxOperatingRuns() { + return policy.getOperatingLimit(); + } + + @Override + @CallCounted + @PerfLogged + public CapabilityList getCapabilities() { + CapabilityList cl = new CapabilityList(); + cl.capability.addAll(support.getCapabilities()); + return cl; + } +}
http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/TavernaServerSupport.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/TavernaServerSupport.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/TavernaServerSupport.java new file mode 100644 index 0000000..ce36bd3 --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/TavernaServerSupport.java @@ -0,0 +1,957 @@ +/* + * Copyright (C) 2010-2011 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master; + +import static eu.medsea.util.MimeUtil.UNKNOWN_MIME_TYPE; +import static eu.medsea.util.MimeUtil.getExtensionMimeTypes; +import static eu.medsea.util.MimeUtil.getMimeType; +import static java.lang.Math.min; +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.TavernaServer.JMX_ROOT; +import static org.taverna.server.master.common.Roles.ADMIN; +import static org.taverna.server.master.rest.handler.T2FlowDocumentHandler.T2FLOW; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.activation.DataHandler; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.PreDestroy; +import javax.ws.rs.WebApplicationException; +import javax.xml.bind.JAXBException; + +import org.apache.commons.logging.Log; +import org.springframework.beans.factory.annotation.Autowired; +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.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.taverna.server.master.api.ManagementModel; +import org.taverna.server.master.api.TavernaServerBean; +import org.taverna.server.master.common.Capability; +import org.taverna.server.master.common.Permission; +import org.taverna.server.master.common.ProfileList; +import org.taverna.server.master.common.VersionedElement; +import org.taverna.server.master.common.Workflow; +import org.taverna.server.master.common.version.Version; +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.NoDirectoryEntryException; +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.identity.WorkflowInternalAuthProvider.WorkflowSelfAuthority; +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.rest.handler.T2FlowDocumentHandler; +import org.taverna.server.master.utils.CapabilityLister; +import org.taverna.server.master.utils.FilenameUtils; +import org.taverna.server.master.utils.InvocationCounter; +import org.taverna.server.master.utils.UsernamePrincipal; + +import uk.org.taverna.scufl2.api.profiles.Profile; + +/** + * Web application support utilities. + * + * @author Donal Fellows + */ +@ManagedResource(objectName = JMX_ROOT + "Webapp", description = "The main Taverna Server " + + Version.JAVA + " web-application interface.") +public class TavernaServerSupport { + /** The main webapp log. */ + private 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 TavernaServerBean webapp; + /** How to handle files. */ + private FilenameUtils fileUtils; + /** How to get the server capabilities. */ + private CapabilityLister capabilitySource; + /** + * 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; + + @PreDestroy + void closeLog() { + log = null; + } + + /** + * @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); + } + + /** + * @return The server's version identifier. + */ + @ManagedAttribute(description = "The installed version of the server.") + public String getServerVersion() { + return VersionedElement.VERSION + " " + VersionedElement.REVISION + " " + + VersionedElement.TIMESTAMP; + } + + @ManagedAttribute(description = "The URIs of the workfows that this server will allow to be instantiated.") + public URI[] getPermittedWorkflowURIs() { + List<URI> pw = policy.listPermittedWorkflowURIs(null); + if (pw == null) + return new URI[0]; + return pw.toArray(new URI[pw.size()]); + } + + @ManagedAttribute(description = "The URIs of the workfows that this server will allow to be instantiated.") + public void setPermittedWorkflowURIs(URI[] pw) { + if (pw == null) + policy.setPermittedWorkflowURIs(null, new ArrayList<URI>()); + else + policy.setPermittedWorkflowURIs(null, Arrays.asList(pw)); + } + + public int getMaxSimultaneousRuns() { + Integer limit = policy.getMaxRuns(getPrincipal()); + if (limit == null) + return policy.getMaxRuns(); + return min(limit.intValue(), policy.getMaxRuns()); + } + + @Autowired + private T2FlowDocumentHandler t2flowHandler; + + public Workflow getWorkflowDocumentFromURI(URI uri) + throws WebApplicationException, IOException { + URLConnection conn = uri.toURL().openConnection(); + conn.setRequestProperty("Accept", T2FLOW); + conn.connect(); + // Tricky point: we know the reader part of the handler only cares + // about the stream argument. + return t2flowHandler.readFrom(null, null, null, null, null, + conn.getInputStream()); + } + + 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(TavernaServerBean webapp) { + this.webapp = webapp; + } + + /** + * @param fileUtils + * The file handling utilities. + */ + @Required + public void setFileUtils(FilenameUtils fileUtils) { + this.fileUtils = fileUtils; + } + + /** + * @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; + } + + @Required + public void setCapabilitySource(CapabilityLister capabilitySource) { + this.capabilitySource = capabilitySource; + } + + /** + * 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 + } + if (getSelfAuthority() != null) { + // At this point, must already be accessing self as that is checked + // in getRun(). + return; + } + 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 + } + if (getSelfAuthority() != null) + throw new NoDestroyException(); + 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; + } + } + + private WorkflowSelfAuthority getSelfAuthority() { + try { + Authentication a = SecurityContextHolder.getContext() + .getAuthentication(); + for (GrantedAuthority ga : a.getAuthorities()) + if (ga instanceof WorkflowSelfAuthority) + return (WorkflowSelfAuthority) ga; + } catch (RuntimeException e) { + } + return null; + } + + /** + * 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); + } + WorkflowSelfAuthority wsa = getSelfAuthority(); + if (wsa != null) { + if (wsa.getWorkflowID().equals(name)) + return runStore.getRun(name); + throw new UnknownRunException(); + } + 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(); + } + + /** + * Obtain a property from a listener that is already attached to a workflow + * run. + * + * @param runName + * The ID of the workflow run to search. + * @param listenerName + * The name of the listener to look up in. + * @param propertyName + * The name of the property to fetch. + * @return The property value. + * @throws NoListenerException + * If no listener with that name exists, or no property with + * that name exists. + * @throws UnknownRunException + * If no run with that name exists. + */ + @Nonnull + public String getProperty(String runName, String listenerName, + String propertyName) throws NoListenerException, + UnknownRunException { + return getListener(runName, listenerName).getProperty(propertyName); + } + + /** + * Obtain a property from 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 in. + * @param propertyName + * The name of the property to fetch. + * @return The property value. + * @throws NoListenerException + * If no listener with that name exists, or no property with + * that name exists. + */ + @Nonnull + public String getProperty(TavernaRun run, String listenerName, + String propertyName) throws NoListenerException { + return getListener(run, listenerName).getProperty(propertyName); + } + + /** + * 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. + */ + 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; + default: + break; + } + + permSet = context.getPermittedReaders(); + if (doRead) { + if (!permSet.contains(userName)) { + permSet = new HashSet<>(permSet); + permSet.add(userName); + context.setPermittedReaders(permSet); + } + } else if (permSet.contains(userName)) { + permSet = new HashSet<>(permSet); + permSet.remove(userName); + context.setPermittedReaders(permSet); + } + + permSet = context.getPermittedUpdaters(); + if (doWrite) { + if (!permSet.contains(userName)) { + permSet = new HashSet<>(permSet); + permSet.add(userName); + context.setPermittedUpdaters(permSet); + } + } else if (permSet.contains(userName)) { + permSet = new HashSet<>(permSet); + permSet.remove(userName); + context.setPermittedUpdaters(permSet); + } + + permSet = context.getPermittedDestroyers(); + if (doKill) { + if (!permSet.contains(userName)) { + permSet = new HashSet<>(permSet); + permSet.add(userName); + context.setPermittedDestroyers(permSet); + } + } else if (permSet.contains(userName)) { + permSet = new HashSet<>(permSet); + permSet.remove(userName); + context.setPermittedDestroyers(permSet); + } + } + + public Map<String, Permission> getPermissionMap( + TavernaSecurityContext context) { + Map<String, Permission> perm = new HashMap<>(); + for (String u : context.getPermittedReaders()) + perm.put(u, Permission.Read); + for (String u : context.getPermittedUpdaters()) + perm.put(u, Permission.Update); + for (String u : context.getPermittedDestroyers()) + perm.put(u, Permission.Destroy); + return perm; + } + + /** + * 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); + permitDestroy(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 { + permitDestroy(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 (getSelfAuthority() != null) + throw new NoCreateException( + "runs may not create workflows on their host server"); + 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()); + /* + * These next pieces of security initialisation are (hopefully) + * obsolete now that we use Spring Security, but we keep them Just + * In Case. + */ + boolean doRESTinit = webapp.initObsoleteSOAPSecurity(c); + if (doRESTinit) + webapp.initObsoleteRESTSecurity(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; + } + @Nonnull + String type = getExtensionMimeTypes(name); + if (!type.equals(UNKNOWN_MIME_TYPE)) + return type; + try { + return getMimeType(new ByteArrayInputStream(f.getContents(0, + SAMPLE_SIZE))); + } catch (FilesystemAccessException e) { + return type; + } + } + + 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 copyDataToFile(URI uri, File file) + throws MalformedURLException, FilesystemAccessException, + IOException { + copyStreamToFile(uri.toURL().openStream(), file); + } + + public void copyStreamToFile(InputStream stream, File file) + throws FilesystemAccessException { + String name = file.getFullName(); + long total = 0; + try { + byte[] buffer = new byte[TRANSFER_SIZE]; + boolean first = true; + while (true) { + int len = stream.read(buffer); + if (len < 0) + break; + total += len; + if (log.isDebugEnabled()) + log.debug("read " + len + + " bytes from source stream (total: " + total + + ") bound for " + name); + if (len == buffer.length) { + if (first) + file.setContents(buffer); + else + file.appendContents(buffer); + } else { + byte[] newBuf = new byte[len]; + System.arraycopy(buffer, 0, newBuf, 0, len); + if (first) + file.setContents(newBuf); + else + file.appendContents(newBuf); + } + first = false; + } + } catch (IOException exn) { + throw new FilesystemAccessException("failed to transfer bytes", exn); + } + } + + /** + * Build a description of the profiles supported by a workflow. Note that we + * expect the set of profiles to be fairly small. + * + * @param workflow + * The workflow to describe the profiles of. + * @return The descriptor (which might be empty). + */ + public ProfileList getProfileDescriptor(Workflow workflow) { + ProfileList result = new ProfileList(); + String main = workflow.getMainProfileName(); + for (Profile p : workflow.getProfiles()) { + ProfileList.Info i = new ProfileList.Info(); + i.name = p.getName(); + if (main != null && main.equals(i.name)) + i.main = true; + result.profile.add(i); + } + return result; + } + + public boolean getAllowStartWorkflowRuns() { + return runFactory.isAllowingRunsToStart(); + } + + /** + * The list of filenames that logs may occupy. + */ + private static final String[] LOGS = { "logs/detail.log.4", + "logs/detail.log.3", "logs/detail.log.2", "logs/detail.log.1", + "logs/detail.log" }; + + public FileConcatenation getLogs(TavernaRun run) { + FileConcatenation fc = new FileConcatenation(); + for (String name : LOGS) { + try { + fc.add(fileUtils.getFile(run, name)); + } catch (FilesystemAccessException | NoDirectoryEntryException e) { + // Ignore + } + } + return fc; + } + + @Nonnull + public List<Capability> getCapabilities() { + return capabilitySource.getCapabilities(); + } + + static final String PROV_BUNDLE = "out.bundle.zip"; + + public FileConcatenation getProv(TavernaRun run) { + FileConcatenation fc = new FileConcatenation(); + try { + fc.add(fileUtils.getFile(run, PROV_BUNDLE)); + } catch (FilesystemAccessException | NoDirectoryEntryException e) { + // Ignore + } + return fc; + } +}
