http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/StrippedDownAuthProvider.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/StrippedDownAuthProvider.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/StrippedDownAuthProvider.java
new file mode 100644
index 0000000..06202dc
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/StrippedDownAuthProvider.java
@@ -0,0 +1,278 @@
+package org.taverna.server.master.identity;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.annotation.PreDestroy;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.annotation.Required;
+import org.springframework.security.authentication.AccountExpiredException;
+import org.springframework.security.authentication.AuthenticationProvider;
+import 
org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.CredentialsExpiredException;
+import org.springframework.security.authentication.DisabledException;
+import org.springframework.security.authentication.LockedException;
+import 
org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.taverna.server.master.utils.CallTimeLogger.PerfLogged;
+
+/**
+ * A stripped down version of a
+ * {@link 
org.springframework.security.authentication.dao.DaoAuthenticationProvider
+ * DaoAuthenticationProvider}/
+ * {@link 
org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider
+ * AbstractUserDetailsAuthenticationProvider} that avoids much of the overhead
+ * associated with that class.
+ */
+public class StrippedDownAuthProvider implements AuthenticationProvider {
+       /**
+        * The plaintext password used to perform
+        * {@link PasswordEncoder#isPasswordValid(String, String, Object)} on 
when
+        * the user is not found to avoid SEC-2056.
+        */
+       private static final String USER_NOT_FOUND_PASSWORD = 
"userNotFoundPassword";
+
+       /**
+        * The password used to perform
+        * {@link PasswordEncoder#isPasswordValid(String, String, Object)} on 
when
+        * the user is not found to avoid SEC-2056. This is necessary, because 
some
+        * {@link PasswordEncoder} implementations will short circuit if the
+        * password is not in a valid format.
+        */
+       private String userNotFoundEncodedPassword;
+       private UserDetailsService userDetailsService;
+       private PasswordEncoder passwordEncoder;
+       private Map<String, AuthCacheEntry> authCache = new HashMap<>();
+       protected final Log logger = LogFactory.getLog(getClass());
+
+       private static class AuthCacheEntry {
+               private String creds;
+               private long timestamp;
+               private static final long VALIDITY = 1000 * 60 * 20;
+               AuthCacheEntry(String credentials) {
+                       creds = credentials;
+                       timestamp = System.currentTimeMillis();
+               }
+               boolean valid(String password) {
+                       return creds.equals(password) && timestamp+VALIDITY > 
System.currentTimeMillis();
+               }
+       }
+
+       @PerfLogged
+       @Override
+       public Authentication authenticate(Authentication authentication)
+                       throws AuthenticationException {
+
+               if (!(authentication instanceof 
UsernamePasswordAuthenticationToken))
+                       throw new IllegalArgumentException(
+                                       "can only authenticate against 
username+password");
+               UsernamePasswordAuthenticationToken auth = 
(UsernamePasswordAuthenticationToken) authentication;
+
+               // Determine username
+               String username = (auth.getPrincipal() == null) ? 
"NONE_PROVIDED"
+                               : auth.getName();
+
+               UserDetails user;
+
+               try {
+                       user = retrieveUser(username, auth);
+                       if (user == null)
+                               throw new IllegalStateException(
+                                               "retrieveUser returned null - a 
violation of the interface contract");
+               } catch (UsernameNotFoundException notFound) {
+                       if (logger.isDebugEnabled())
+                               logger.debug("User '" + username + "' not 
found", notFound);
+                       throw new BadCredentialsException("Bad credentials");
+               }
+
+               // Pre-auth
+               if (!user.isAccountNonLocked())
+                       throw new LockedException("User account is locked");
+               if (!user.isEnabled())
+                       throw new DisabledException("User account is disabled");
+               if (!user.isAccountNonExpired())
+                       throw new AccountExpiredException("User account has 
expired");
+               Object credentials = auth.getCredentials();
+               if (credentials == null) {
+                       logger.debug("Authentication failed: no credentials 
provided");
+
+                       throw new BadCredentialsException("Bad credentials");
+               }
+
+               String providedPassword = credentials.toString();
+               boolean matched = false;
+               synchronized (authCache) {
+                       AuthCacheEntry pw = authCache.get(username);
+                       if (pw != null && providedPassword != null) {
+                               if (pw.valid(providedPassword))
+                                       matched = true;
+                               else
+                                       authCache.remove(username);
+                       }
+               }
+               // Auth
+               if (!matched) {
+                       if (!passwordEncoder.matches(providedPassword, 
user.getPassword())) {
+                               logger.debug("Authentication failed: password 
does not match stored value");
+
+                               throw new BadCredentialsException("Bad 
credentials");
+                       }
+                       if (providedPassword != null)
+                               synchronized (authCache) {
+                                       authCache.put(username, new 
AuthCacheEntry(providedPassword));
+                               }
+               }
+
+               // Post-auth
+               if (!user.isCredentialsNonExpired())
+                       throw new CredentialsExpiredException(
+                                       "User credentials have expired");
+
+               return createSuccessAuthentication(user, auth, user);
+       }
+
+       @PreDestroy
+       void clearCache() {
+               authCache.clear();
+       }
+
+       /**
+        * Creates a successful {@link Authentication} object.
+        * <p>
+        * Protected so subclasses can override.
+        * </p>
+        * <p>
+        * Subclasses will usually store the original credentials the user 
supplied
+        * (not salted or encoded passwords) in the returned
+        * <code>Authentication</code> object.
+        * </p>
+        * 
+        * @param principal
+        *            that should be the principal in the returned object 
(defined
+        *            by the {@link #isForcePrincipalAsString()} method)
+        * @param authentication
+        *            that was presented to the provider for validation
+        * @param user
+        *            that was loaded by the implementation
+        * 
+        * @return the successful authentication token
+        */
+       private Authentication createSuccessAuthentication(Object principal,
+                       Authentication authentication, UserDetails user) {
+               /*
+                * Ensure we return the original credentials the user supplied, 
so
+                * subsequent attempts are successful even with encoded 
passwords. Also
+                * ensure we return the original getDetails(), so that future
+                * authentication events after cache expiry contain the details
+                */
+               UsernamePasswordAuthenticationToken result = new 
UsernamePasswordAuthenticationToken(
+                               principal, authentication.getCredentials(),
+                               user.getAuthorities());
+               result.setDetails(authentication.getDetails());
+
+               return result;
+       }
+
+       @Override
+       public boolean supports(Class<?> authentication) {
+               return UsernamePasswordAuthenticationToken.class
+                               .isAssignableFrom(authentication);
+       }
+
+       /**
+        * Allows subclasses to actually retrieve the <code>UserDetails</code> 
from
+        * an implementation-specific location, with the option of throwing an
+        * <code>AuthenticationException</code> immediately if the presented
+        * credentials are incorrect (this is especially useful if it is 
necessary
+        * to bind to a resource as the user in order to obtain or generate a
+        * <code>UserDetails</code>).
+        * <p>
+        * Subclasses are not required to perform any caching, as the
+        * <code>AbstractUserDetailsAuthenticationProvider</code> will by 
default
+        * cache the <code>UserDetails</code>. The caching of
+        * <code>UserDetails</code> does present additional complexity as this 
means
+        * subsequent requests that rely on the cache will need to still have 
their
+        * credentials validated, even if the correctness of credentials was 
assured
+        * by subclasses adopting a binding-based strategy in this method.
+        * Accordingly it is important that subclasses either disable caching 
(if
+        * they want to ensure that this method is the only method that is 
capable
+        * of authenticating a request, as no <code>UserDetails</code> will 
ever be
+        * cached) or ensure subclasses implement
+        * {@link #additionalAuthenticationChecks(UserDetails, 
UsernamePasswordAuthenticationToken)}
+        * to compare the credentials of a cached <code>UserDetails</code> with
+        * subsequent authentication requests.
+        * </p>
+        * <p>
+        * Most of the time subclasses will not perform credentials inspection 
in
+        * this method, instead performing it in
+        * {@link #additionalAuthenticationChecks(UserDetails, 
UsernamePasswordAuthenticationToken)}
+        * so that code related to credentials validation need not be duplicated
+        * across two methods.
+        * </p>
+        * 
+        * @param username
+        *            The username to retrieve
+        * @param authentication
+        *            The authentication request, which subclasses <em>may</em> 
need
+        *            to perform a binding-based retrieval of the
+        *            <code>UserDetails</code>
+        * 
+        * @return the user information (never <code>null</code> - instead an
+        *         exception should the thrown)
+        * 
+        * @throws AuthenticationException
+        *             if the credentials could not be validated (generally a
+        *             <code>BadCredentialsException</code>, an
+        *             <code>AuthenticationServiceException</code> or
+        *             <code>UsernameNotFoundException</code>)
+        */
+       private UserDetails retrieveUser(String username,
+                       UsernamePasswordAuthenticationToken authentication)
+                       throws AuthenticationException {
+               try {
+                       return userDetailsService.loadUserByUsername(username);
+               } catch (UsernameNotFoundException notFound) {
+                       if (authentication.getCredentials() != null) {
+                               String presentedPassword = 
authentication.getCredentials()
+                                               .toString();
+                               passwordEncoder.matches(presentedPassword,
+                                               userNotFoundEncodedPassword);
+                       }
+                       throw notFound;
+               } catch (AuthenticationException e) {
+                       throw e;
+               } catch (Exception repositoryProblem) {
+                       throw new AuthenticationServiceException(
+                                       repositoryProblem.getMessage(), 
repositoryProblem);
+               }
+       }
+
+       /**
+        * Sets the PasswordEncoder instance to be used to encode and validate
+        * passwords.
+        */
+       @Required
+       public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
+               if (passwordEncoder == null)
+                       throw new IllegalArgumentException("passwordEncoder 
cannot be null");
+
+               this.passwordEncoder = passwordEncoder;
+               this.userNotFoundEncodedPassword = passwordEncoder
+                               .encode(USER_NOT_FOUND_PASSWORD);
+       }
+
+       @Required
+       public void setUserDetailsService(UserDetailsService 
userDetailsService) {
+               if (userDetailsService == null)
+                       throw new IllegalStateException("A UserDetailsService 
must be set");
+               this.userDetailsService = userDetailsService;
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/User.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/User.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/User.java
new file mode 100644
index 0000000..bdb6e40
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/User.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2011-2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.identity;
+
+import static org.taverna.server.master.common.Roles.ADMIN;
+import static org.taverna.server.master.common.Roles.USER;
+import static org.taverna.server.master.defaults.Default.AUTHORITY_PREFIX;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import javax.jdo.annotations.PersistenceCapable;
+import javax.jdo.annotations.Persistent;
+import javax.jdo.annotations.Query;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlType;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+/**
+ * The representation of a user in the database.
+ * <p>
+ * A user consists logically of a (non-ordered) tuple of items:
+ * <ul>
+ * <li>The {@linkplain #getUsername() user name},
+ * <li>The {@linkplain #getPassword() user's password} (salted, encoded),
+ * <li>Whether the user is {@linkplain #isEnabled() enabled} (i.e., able to log
+ * in),
+ * <li>Whether the user has {@linkplain #isAdmin() administrative privileges}, 
and
+ * <li>What {@linkplain #getLocalUsername() system (Unix) account} the user's
+ * workflows will run as; separation between different users that are mapped to
+ * the same system account is nothing like as strongly enforced.
+ * </ul>
+ * 
+ * @author Donal Fellows
+ */
+@PersistenceCapable(schema = "USERS", table = "LIST")
+@Query(name = "users", language = "SQL", value = "SELECT id FROM USERS.LIST 
ORDER BY id", resultClass = String.class)
+@XmlRootElement
+@XmlType(name = "User", propOrder = {})
+@SuppressWarnings("serial")
+public class User implements UserDetails {
+       @XmlElement
+       @Persistent
+       private boolean disabled;
+       @XmlElement(name = "username", required = true)
+       @Persistent(primaryKey = "true")
+       private String id;
+       @XmlElement(name = "password", required = true)
+       @Persistent(column = "password")
+       private String encodedPassword;
+       @XmlElement
+       @Persistent
+       private boolean admin;
+       @XmlElement
+       @Persistent
+       private String localUsername;
+
+       @Override
+       public Collection<GrantedAuthority> getAuthorities() {
+               List<GrantedAuthority> auths = new ArrayList<>();
+               auths.add(new LiteralGrantedAuthority(USER));
+               if (admin)
+                       auths.add(new LiteralGrantedAuthority(ADMIN));
+               if (localUsername != null)
+                       auths.add(new LiteralGrantedAuthority(AUTHORITY_PREFIX
+                                       + localUsername));
+               return auths;
+       }
+
+       @Override
+       public String getPassword() {
+               return encodedPassword;
+       }
+
+       @Override
+       public String getUsername() {
+               return id;
+       }
+
+       @Override
+       public boolean isAccountNonExpired() {
+               return true;
+       }
+
+       @Override
+       public boolean isAccountNonLocked() {
+               return true;
+       }
+
+       @Override
+       public boolean isCredentialsNonExpired() {
+               return true;
+       }
+
+       @Override
+       public boolean isEnabled() {
+               return !disabled;
+       }
+
+       void setDisabled(boolean disabled) {
+               this.disabled = disabled;
+       }
+
+       void setUsername(String username) {
+               this.id = username;
+       }
+
+       void setEncodedPassword(String password) {
+               this.encodedPassword = password;
+       }
+
+       void setAdmin(boolean admin) {
+               this.admin = admin;
+       }
+
+       public boolean isAdmin() {
+               return admin;
+       }
+
+       void setLocalUsername(String localUsername) {
+               this.localUsername = localUsername;
+       }
+
+       public String getLocalUsername() {
+               return localUsername;
+       }
+}
+
+@SuppressWarnings("serial")
+class LiteralGrantedAuthority implements GrantedAuthority {
+       private String auth;
+
+       LiteralGrantedAuthority(String auth) {
+               this.auth = auth;
+       }
+
+       @Override
+       public String getAuthority() {
+               return auth;
+       }
+
+       @Override
+       public String toString() {
+               return "AUTHORITY(" + auth + ")";
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/UserStore.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/UserStore.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/UserStore.java
new file mode 100644
index 0000000..054d932
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/UserStore.java
@@ -0,0 +1,389 @@
+/*
+ * Copyright (C) 2011-2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.identity;
+
+import static org.apache.commons.logging.LogFactory.getLog;
+import static org.taverna.server.master.TavernaServer.JMX_ROOT;
+import static org.taverna.server.master.common.Roles.ADMIN;
+import static org.taverna.server.master.common.Roles.USER;
+import static org.taverna.server.master.defaults.Default.AUTHORITY_PREFIX;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.jdo.annotations.PersistenceAware;
+
+import org.apache.commons.logging.Log;
+import org.springframework.beans.factory.annotation.Required;
+import org.springframework.dao.DataAccessException;
+import org.springframework.jmx.export.annotation.ManagedAttribute;
+import org.springframework.jmx.export.annotation.ManagedOperation;
+import org.springframework.jmx.export.annotation.ManagedOperationParameter;
+import org.springframework.jmx.export.annotation.ManagedOperationParameters;
+import org.springframework.jmx.export.annotation.ManagedResource;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.core.userdetails.memory.UserAttribute;
+import 
org.springframework.security.core.userdetails.memory.UserAttributeEditor;
+import org.taverna.server.master.utils.CallTimeLogger.PerfLogged;
+import org.taverna.server.master.utils.JDOSupport;
+
+/**
+ * The bean class that is responsible for managing the users in the database.
+ * 
+ * @author Donal Fellows
+ */
+@PersistenceAware
+@ManagedResource(objectName = JMX_ROOT + "Users", description = "The user 
database.")
+public class UserStore extends JDOSupport<User> implements UserDetailsService,
+               UserStoreAPI {
+       /** The logger for the user store. */
+       private transient Log log = getLog("Taverna.Server.UserDB");
+
+       public UserStore() {
+               super(User.class);
+       }
+
+       @PreDestroy
+       void closeLog() {
+               log = null;
+       }
+
+       private Map<String, BootstrapUserInfo> base = new HashMap<>();
+       private String defLocalUser;
+       private PasswordEncoder encoder;
+       private volatile int epoch;
+
+       /**
+        * Install the encoder that will be used to turn a plaintext password 
into
+        * something that it is safe to store in the database.
+        * 
+        * @param encoder
+        *            The password encoder bean to install.
+        */
+       public void setEncoder(PasswordEncoder encoder) {
+               this.encoder = encoder;
+       }
+
+       public void setBaselineUserProperties(Properties props) {
+               UserAttributeEditor parser = new UserAttributeEditor();
+
+               for (Object name : props.keySet()) {
+                       String username = (String) name;
+                       String value = props.getProperty(username);
+
+                       // Convert value to a password, enabled setting, and 
list of granted
+                       // authorities
+                       parser.setAsText(value);
+
+                       UserAttribute attr = (UserAttribute) parser.getValue();
+                       if (attr != null && attr.isEnabled())
+                               base.put(username, new 
BootstrapUserInfo(username, attr));
+               }
+       }
+
+       private void installPassword(User u, String password) {
+               u.setEncodedPassword(encoder.encode(password));
+       }
+
+       public void setDefaultLocalUser(String defLocalUser) {
+               this.defLocalUser = defLocalUser;
+       }
+
+       @SuppressWarnings("unchecked")
+       private List<String> getUsers() {
+               return (List<String>) namedQuery("users").execute();
+       }
+
+       @WithinSingleTransaction
+       @PostConstruct
+       void initDB() {
+               if (base == null || base.isEmpty())
+                       log.warn("no baseline user collection");
+               else if (!getUsers().isEmpty())
+                       log.info("using existing users from database");
+               else
+                       for (String username : base.keySet()) {
+                               BootstrapUserInfo ud = base.get(username);
+                               if (ud == null)
+                                       continue;
+                               User u = ud.get(encoder);
+                               if (u == null)
+                                       continue;
+                               log.info("bootstrapping user " + username + " 
in the database");
+                               persist(u);
+                       }
+               base = null;
+               epoch++;
+       }
+
+       @Override
+       @PerfLogged
+       @WithinSingleTransaction
+       @ManagedAttribute(description = "The list of server accounts known 
about.", currencyTimeLimit = 30)
+       public List<String> getUserNames() {
+               return getUsers();
+       }
+
+       @Override
+       @PerfLogged
+       @WithinSingleTransaction
+       public User getUser(String userName) {
+               return detach(getById(userName));
+       }
+
+       /**
+        * Get information about a server account.
+        * 
+        * @param userName
+        *            The username to look up.
+        * @return A description map intended for use by a server admin over 
JMX.
+        */
+       @PerfLogged
+       @WithinSingleTransaction
+       @ManagedOperation(description = "Get information about a server 
account.")
+       @ManagedOperationParameters(@ManagedOperationParameter(name = 
"userName", description = "The username to look up."))
+       public Map<String, String> getUserInfo(String userName) {
+               User u = getById(userName);
+               Map<String, String> info = new HashMap<>();
+               info.put("name", u.getUsername());
+               info.put("admin", u.isAdmin() ? "yes" : "no");
+               info.put("enabled", u.isEnabled() ? "yes" : "no");
+               info.put("localID", u.getLocalUsername());
+               return info;
+       }
+
+       /**
+        * Get a list of all the users in the database.
+        * 
+        * @return A list of user details, <i>copied</i> out of the database.
+        */
+       @PerfLogged
+       @WithinSingleTransaction
+       public List<UserDetails> listUsers() {
+               ArrayList<UserDetails> result = new ArrayList<>();
+               for (String id : getUsers())
+                       result.add(detach(getById(id)));
+               return result;
+       }
+
+       @Override
+       @PerfLogged
+       @WithinSingleTransaction
+       @ManagedOperation(description = "Create a new user account; the account 
will be disabled and "
+                       + "non-administrative by default. Does not create any 
underlying system account.")
+       @ManagedOperationParameters({
+                       @ManagedOperationParameter(name = "username", 
description = "The username to create."),
+                       @ManagedOperationParameter(name = "password", 
description = "The password to use."),
+                       @ManagedOperationParameter(name = 
"coupleLocalUsername", description = "Whether to set the local user name to the 
'main' one.") })
+       public void addUser(String username, String password,
+                       boolean coupleLocalUsername) {
+               if (username.matches(".*[^a-zA-Z0-9].*"))
+                       throw new IllegalArgumentException(
+                                       "bad user name; must be pure 
alphanumeric");
+               if (getById(username) != null)
+                       throw new IllegalArgumentException("user name already 
exists");
+               User u = new User();
+               u.setDisabled(true);
+               u.setAdmin(false);
+               u.setUsername(username);
+               installPassword(u, password);
+               if (coupleLocalUsername)
+                       u.setLocalUsername(username);
+               else
+                       u.setLocalUsername(defLocalUser);
+               log.info("creating user for " + username);
+               persist(u);
+               epoch++;
+       }
+
+       @Override
+       @PerfLogged
+       @WithinSingleTransaction
+       @ManagedOperation(description = "Set or clear whether this account is 
enabled. "
+                       + "Disabled accounts cannot be used to log in.")
+       @ManagedOperationParameters({
+                       @ManagedOperationParameter(name = "username", 
description = "The username to adjust."),
+                       @ManagedOperationParameter(name = "enabled", 
description = "Whether to enable the account.") })
+       public void setUserEnabled(String username, boolean enabled) {
+               User u = getById(username);
+               if (u != null) {
+                       u.setDisabled(!enabled);
+                       log.info((enabled ? "enabling" : "disabling") + " user 
" + username);
+                       epoch++;
+               }
+       }
+
+       @Override
+       @PerfLogged
+       @WithinSingleTransaction
+       @ManagedOperation(description = "Set or clear the mark on an account 
that indicates "
+                       + "that it has administrative privileges.")
+       @ManagedOperationParameters({
+                       @ManagedOperationParameter(name = "username", 
description = "The username to adjust."),
+                       @ManagedOperationParameter(name = "admin", description 
= "Whether the account has admin privileges.") })
+       public void setUserAdmin(String username, boolean admin) {
+               User u = getById(username);
+               if (u != null) {
+                       u.setAdmin(admin);
+                       log.info((admin ? "enabling" : "disabling") + " user " 
+ username
+                                       + " admin status");
+                       epoch++;
+               }
+       }
+
+       @Override
+       @PerfLogged
+       @WithinSingleTransaction
+       @ManagedOperation(description = "Change the password for an account.")
+       @ManagedOperationParameters({
+                       @ManagedOperationParameter(name = "username", 
description = "The username to adjust."),
+                       @ManagedOperationParameter(name = "password", 
description = "The new password to use.") })
+       public void setUserPassword(String username, String password) {
+               User u = getById(username);
+               if (u != null) {
+                       installPassword(u, password);
+                       log.info("changing password for user " + username);
+                       epoch++;
+               }
+       }
+
+       @Override
+       @PerfLogged
+       @WithinSingleTransaction
+       @ManagedOperation(description = "Change what local system account to 
use for a server account.")
+       @ManagedOperationParameters({
+                       @ManagedOperationParameter(name = "username", 
description = "The username to adjust."),
+                       @ManagedOperationParameter(name = "password", 
description = "The new local user account use.") })
+       public void setUserLocalUser(String username, String localUsername) {
+               User u = getById(username);
+               if (u != null) {
+                       u.setLocalUsername(localUsername);
+                       log.info("mapping user " + username + " to local 
account "
+                                       + localUsername);
+                       epoch++;
+               }
+       }
+
+       @Override
+       @PerfLogged
+       @WithinSingleTransaction
+       @ManagedOperation(description = "Delete a server account. The 
underlying "
+                       + "system account is not modified.")
+       @ManagedOperationParameters(@ManagedOperationParameter(name = 
"username", description = "The username to delete."))
+       public void deleteUser(String username) {
+               delete(getById(username));
+               log.info("deleting user " + username);
+               epoch++;
+       }
+
+       @Override
+       @PerfLogged
+       @WithinSingleTransaction
+       public UserDetails loadUserByUsername(String username)
+                       throws UsernameNotFoundException, DataAccessException {
+               User u;
+               if (base != null) {
+                       log.warn("bootstrap user store still installed!");
+                       BootstrapUserInfo ud = base.get(username);
+                       if (ud != null) {
+                               log.warn("retrieved production credentials for 
" + username
+                                               + " from bootstrap store");
+                               u = ud.get(encoder);
+                               if (u != null)
+                                       return u;
+                       }
+               }
+               try {
+                       u = detach(getById(username));
+               } catch (NullPointerException npe) {
+                       throw new UsernameNotFoundException("who are you?");
+               } catch (Exception ex) {
+                       throw new UsernameNotFoundException("who are you?", ex);
+               }
+               if (u != null)
+                       return u;
+               throw new UsernameNotFoundException("who are you?");
+       }
+
+       int getEpoch() {
+               return epoch;
+       }
+
+       public static class CachedUserStore implements UserDetailsService {
+               private int epoch;
+               private Map<String, UserDetails> cache = new HashMap<>();
+               private UserStore realStore;
+
+               @Required
+               public void setRealStore(UserStore store) {
+                       this.realStore = store;
+               }
+
+               @Override
+               @PerfLogged
+               public UserDetails loadUserByUsername(String username) {
+                       int epoch = realStore.getEpoch();
+                       UserDetails details;
+                       synchronized (cache) {
+                               if (epoch != this.epoch) {
+                                       cache.clear();
+                                       this.epoch = epoch;
+                                       details = null;
+                               } else
+                                       details = cache.get(username);
+                       }
+                       if (details == null) {
+                               details = 
realStore.loadUserByUsername(username);
+                               synchronized (cache) {
+                                       cache.put(username, details);
+                               }
+                       }
+                       return details;
+               }
+       }
+
+       private static class BootstrapUserInfo {
+               private String user;
+               private String pass;
+               private Collection<GrantedAuthority> auth;
+
+               BootstrapUserInfo(String username, UserAttribute attr) {
+                       user = username;
+                       pass = attr.getPassword();
+                       auth = attr.getAuthorities();
+               }
+
+               User get(PasswordEncoder encoder) {
+                       User u = new User();
+                       boolean realUser = false;
+                       for (GrantedAuthority ga : auth) {
+                               String a = ga.getAuthority();
+                               if (a.startsWith(AUTHORITY_PREFIX))
+                                       
u.setLocalUsername(a.substring(AUTHORITY_PREFIX.length()));
+                               else if (a.equals(USER))
+                                       realUser = true;
+                               else if (a.equals(ADMIN))
+                                       u.setAdmin(true);
+                       }
+                       if (!realUser)
+                               return null;
+                       u.setUsername(user);
+                       u.setEncodedPassword(encoder.encode(pass));
+                       u.setDisabled(false);
+                       return u;
+               }
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/UserStoreAPI.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/UserStoreAPI.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/UserStoreAPI.java
new file mode 100644
index 0000000..a048da9
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/UserStoreAPI.java
@@ -0,0 +1,91 @@
+package org.taverna.server.master.identity;
+
+import java.util.List;
+
+/**
+ * The API that is exposed by the DAO that exposes user management.
+ * 
+ * @author Donal Fellows
+ * @see User
+ */
+public interface UserStoreAPI {
+       /**
+        * List the currently-known account names.
+        * 
+        * @return A list of users in the database. Note that this is a 
snapshot.
+        */
+       List<String> getUserNames();
+
+       /**
+        * Get a particular user's description.
+        * 
+        * @param userName
+        *            The username to look up.
+        * @return A <i>copy</i> of the user description.
+        */
+       User getUser(String userName);
+
+       /**
+        * Create a new user account; the account will be disabled and
+        * non-administrative by default. Does not create any underlying system
+        * account.
+        * 
+        * @param username
+        *            The username to create.
+        * @param password
+        *            The password to use.
+        * @param coupleLocalUsername
+        *            Whether to set the local user name to the 'main' one.
+        */
+       void addUser(String username, String password, boolean 
coupleLocalUsername);
+
+       /**
+        * Set or clear whether this account is enabled. Disabled accounts 
cannot be
+        * used to log in.
+        * 
+        * @param username
+        *            The username to adjust.
+        * @param enabled
+        *            Whether to enable the account.
+        */
+       void setUserEnabled(String username, boolean enabled);
+
+       /**
+        * Set or clear the mark on an account that indicates that it has
+        * administrative privileges.
+        * 
+        * @param username
+        *            The username to adjust.
+        * @param admin
+        *            Whether the account has admin privileges.
+        */
+       void setUserAdmin(String username, boolean admin);
+
+       /**
+        * Change the password for an account.
+        * 
+        * @param username
+        *            The username to adjust.
+        * @param password
+        *            The new password to use.
+        */
+       void setUserPassword(String username, String password);
+
+       /**
+        * Change what local system account to use for a server account.
+        * 
+        * @param username
+        *            The username to adjust.
+        * @param localUsername
+        *            The new local user account use.
+        */
+       void setUserLocalUser(String username, String localUsername);
+
+       /**
+        * Delete a server account. The underlying system account is not 
modified.
+        * 
+        * @param username
+        *            The username to delete.
+        */
+       void deleteUser(String username);
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/WorkflowInternalAuthProvider.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/WorkflowInternalAuthProvider.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/WorkflowInternalAuthProvider.java
new file mode 100644
index 0000000..9219a60
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/WorkflowInternalAuthProvider.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2013 The University of Manchester
+ * 
+ * See the file "LICENSE.txt" for license terms.
+ */
+package org.taverna.server.master.identity;
+
+import static java.util.Collections.synchronizedMap;
+import static 
org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes;
+import static org.taverna.server.master.common.Roles.SELF;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.annotation.Required;
+import 
org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.authentication.BadCredentialsException;
+import 
org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import 
org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import 
org.springframework.security.web.authentication.WebAuthenticationDetails;
+import org.springframework.web.context.request.ServletRequestAttributes;
+import org.taverna.server.master.exceptions.UnknownRunException;
+import org.taverna.server.master.interfaces.LocalIdentityMapper;
+import org.taverna.server.master.interfaces.RunStore;
+import org.taverna.server.master.utils.CallTimeLogger.PerfLogged;
+import org.taverna.server.master.utils.UsernamePrincipal;
+import org.taverna.server.master.worker.RunDatabaseDAO;
+
+/**
+ * A special authentication provider that allows a workflow to authenticate to
+ * itself. This is used to allow the workflow to publish to its own interaction
+ * feed.
+ * 
+ * @author Donal Fellows
+ */
+public class WorkflowInternalAuthProvider extends
+               AbstractUserDetailsAuthenticationProvider {
+       private Log log = LogFactory.getLog("Taverna.Server.UserDB");
+       private static final boolean logDecisions = true;
+       public static final String PREFIX = "wfrun_";
+       private RunDatabaseDAO dao;
+       private Map<String, String> cache;
+
+       @Required
+       public void setDao(RunDatabaseDAO dao) {
+               this.dao = dao;
+       }
+
+       @Required
+       @SuppressWarnings("serial")
+       public void setCacheBound(final int bound) {
+               cache = synchronizedMap(new LinkedHashMap<String, String>() {
+                       @Override
+                       protected boolean removeEldestEntry(Map.Entry<String, 
String> eldest) {
+                               return size() > bound;
+                       }
+               });
+       }
+
+       public void setAuthorizedAddresses(String[] addresses) {
+               authorizedAddresses = new HashSet<>(localAddresses);
+               for (String s : addresses)
+                       authorizedAddresses.add(s);
+       }
+
+       @PostConstruct
+       public void logConfig() {
+               log.info("authorized addresses for automatic access: "
+                               + authorizedAddresses);
+       }
+
+       @PreDestroy
+       void closeLog() {
+               log = null;
+       }
+
+       private final Set<String> localAddresses = new HashSet<>();
+       private Set<String> authorizedAddresses;
+       {
+               localAddresses.add("127.0.0.1"); // IPv4
+               localAddresses.add("::1"); // IPv6
+               try {
+                       InetAddress addr = InetAddress.getLocalHost();
+                       if (!addr.isLoopbackAddress())
+                               localAddresses.add(addr.getHostAddress());
+               } catch (UnknownHostException e) {
+                       // Ignore the exception
+               }
+               authorizedAddresses = new HashSet<>(localAddresses);
+       }
+
+       /**
+        * Check that the authentication request is actually valid for the given
+        * user record.
+        * 
+        * @param userRecord
+        *            as retrieved from the
+        *            {@link #retrieveUser(String, 
UsernamePasswordAuthenticationToken)}
+        *            or <code>UserCache</code>
+        * @param principal
+        *            the principal that is trying to authenticate (and that 
we're
+        *            trying to bind)
+        * @param credentials
+        *            the credentials (e.g., password) presented by the 
principal
+        * 
+        * @throws AuthenticationException
+        *             AuthenticationException if the credentials could not be
+        *             validated (generally a 
<code>BadCredentialsException</code>,
+        *             an <code>AuthenticationServiceException</code>)
+        * @throws Exception
+        *             If something goes wrong. Will be logged and converted to 
a
+        *             generic AuthenticationException.
+        */
+       protected void additionalAuthenticationChecks(UserDetails userRecord,
+                       @Nonnull Object principal, @Nonnull Object credentials)
+                       throws Exception {
+               @Nonnull
+               HttpServletRequest req = ((ServletRequestAttributes) 
currentRequestAttributes())
+                               .getRequest();
+
+               // Are we coming from a "local" address?
+               if (!req.getLocalAddr().equals(req.getRemoteAddr())
+                               && 
!authorizedAddresses.contains(req.getRemoteAddr())) {
+                       if (logDecisions)
+                               log.info("attempt to use workflow magic token 
from untrusted address:"
+                                               + " token="
+                                               + userRecord.getUsername()
+                                               + ", address="
+                                               + req.getRemoteAddr());
+                       throw new BadCredentialsException("bad login token");
+               }
+
+               // Does the password match?
+               if (!credentials.equals(userRecord.getPassword())) {
+                       if (logDecisions)
+                               log.info("workflow magic token is untrusted due 
to password mismatch:"
+                                               + " wanted="
+                                               + userRecord.getPassword()
+                                               + ", got="
+                                               + credentials);
+                       throw new BadCredentialsException("bad login token");
+               }
+
+               if (logDecisions)
+                       log.info("granted role " + SELF + " to user "
+                                       + userRecord.getUsername());
+       }
+
+       /**
+        * Retrieve the <code>UserDetails</code> from the relevant store, with 
the
+        * option of throwing an <code>AuthenticationException</code> 
immediately if
+        * the presented credentials are incorrect (this is especially useful 
if it
+        * is necessary to bind to a resource as the user in order to obtain or
+        * generate a <code>UserDetails</code>).
+        * 
+        * @param username
+        *            The username to retrieve
+        * @param details
+        *            The details from the authentication request.
+        * @see #retrieveUser(String,UsernamePasswordAuthenticationToken)
+        * @return the user information (never <code>null</code> - instead an
+        *         exception should the thrown)
+        * @throws AuthenticationException
+        *             if the credentials could not be validated (generally a
+        *             <code>BadCredentialsException</code>, an
+        *             <code>AuthenticationServiceException</code> or
+        *             <code>UsernameNotFoundException</code>)
+        * @throws Exception
+        *             If something goes wrong. It will be logged and converted 
into
+        *             a general AuthenticationException.
+        */
+       @Nonnull
+       protected UserDetails retrieveUser(String username, Object details)
+                       throws Exception {
+               if (details == null || !(details instanceof 
WebAuthenticationDetails))
+                       throw new UsernameNotFoundException("context 
unsupported");
+               if (!username.startsWith(PREFIX))
+                       throw new UsernameNotFoundException(
+                                       "unsupported username for this 
provider");
+               if (logDecisions)
+                       log.info("request for auth for user " + username);
+               String wfid = username.substring(PREFIX.length());
+               String securityToken;
+               try {
+                       securityToken = cache.get(wfid);
+                       if (securityToken == null) {
+                               securityToken = dao.getSecurityToken(wfid);
+                               if (securityToken == null)
+                                       throw new UsernameNotFoundException("no 
such user");
+                               cache.put(wfid, securityToken);
+                       }
+               } catch (NullPointerException npe) {
+                       throw new UsernameNotFoundException("no such user");
+               }
+               return new User(username, securityToken, true, true, true, true,
+                               Arrays.asList(new LiteralGrantedAuthority(SELF),
+                                               new 
WorkflowSelfAuthority(wfid)));
+       }
+
+       @Override
+       @PerfLogged
+       protected final void additionalAuthenticationChecks(UserDetails 
userRecord,
+                       UsernamePasswordAuthenticationToken token) {
+               try {
+                       additionalAuthenticationChecks(userRecord, 
token.getPrincipal(),
+                                       token.getCredentials());
+               } catch (AuthenticationException e) {
+                       throw e;
+               } catch (Exception e) {
+                       log.warn("unexpected failure in authentication", e);
+                       throw new AuthenticationServiceException(
+                                       "unexpected failure in authentication", 
e);
+               }
+       }
+
+       @Override
+       @Nonnull
+       @PerfLogged
+       protected final UserDetails retrieveUser(String username,
+                       UsernamePasswordAuthenticationToken token) {
+               try {
+                       return retrieveUser(username, token.getDetails());
+               } catch (AuthenticationException e) {
+                       throw e;
+               } catch (Exception e) {
+                       log.warn("unexpected failure in authentication", e);
+                       throw new AuthenticationServiceException(
+                                       "unexpected failure in authentication", 
e);
+               }
+       }
+
+       @SuppressWarnings("serial")
+       public static class WorkflowSelfAuthority extends 
LiteralGrantedAuthority {
+               public WorkflowSelfAuthority(String wfid) {
+                       super(wfid);
+               }
+
+               public String getWorkflowID() {
+                       return getAuthority();
+               }
+
+               @Override
+               public String toString() {
+                       return "WORKFLOW(" + getAuthority() + ")";
+               }
+       }
+
+       public static class WorkflowSelfIDMapper implements LocalIdentityMapper 
{
+               private Log log = LogFactory.getLog("Taverna.Server.UserDB");
+               private RunStore runStore;
+
+               @PreDestroy
+               void closeLog() {
+                       log = null;
+               }
+
+               @Required
+               public void setRunStore(RunStore runStore) {
+                       this.runStore = runStore;
+               }
+
+               private String getUsernameForSelfAccess(WorkflowSelfAuthority 
authority)
+                               throws UnknownRunException {
+                       return runStore.getRun(authority.getWorkflowID())
+                                       
.getSecurityContext().getOwner().getName();
+               }
+
+               @Override
+               @PerfLogged
+               public String getUsernameForPrincipal(UsernamePrincipal user) {
+                       Authentication auth = SecurityContextHolder.getContext()
+                                       .getAuthentication();
+                       if (auth == null || !auth.isAuthenticated())
+                               return null;
+                       try {
+                               for (GrantedAuthority authority : 
auth.getAuthorities())
+                                       if (authority instanceof 
WorkflowSelfAuthority)
+                                               return 
getUsernameForSelfAccess((WorkflowSelfAuthority) authority);
+                       } catch (UnknownRunException e) {
+                               log.warn("workflow run disappeared during 
computation of workflow map identity");
+                       }
+                       return null;
+               }
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/package-info.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/package-info.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/package-info.java
new file mode 100644
index 0000000..dd1500a
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/package-info.java
@@ -0,0 +1,10 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+/**
+ * Implementations of beans that map global user identities to local
+ * usernames.
+ */
+package org.taverna.server.master.identity;

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/interaction/InteractionFeedSupport.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/interaction/InteractionFeedSupport.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/interaction/InteractionFeedSupport.java
new file mode 100644
index 0000000..99e1d99
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/interaction/InteractionFeedSupport.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.interaction;
+
+import static java.lang.management.ManagementFactory.getPlatformMBeanServer;
+import static java.util.Collections.reverse;
+import static javax.management.Query.attr;
+import static javax.management.Query.match;
+import static javax.management.Query.value;
+import static org.apache.commons.logging.LogFactory.getLog;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+import org.apache.abdera.Abdera;
+import org.apache.abdera.factory.Factory;
+import org.apache.abdera.i18n.iri.IRI;
+import org.apache.abdera.model.Document;
+import org.apache.abdera.model.Entry;
+import org.apache.abdera.model.Feed;
+import org.apache.abdera.parser.Parser;
+import org.apache.abdera.writer.Writer;
+import org.springframework.beans.factory.annotation.Required;
+import org.taverna.server.master.TavernaServerSupport;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.exceptions.NoDirectoryEntryException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+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.TavernaRun;
+import org.taverna.server.master.interfaces.UriBuilderFactory;
+import org.taverna.server.master.utils.FilenameUtils;
+
+/**
+ * Bean that supports interaction feeds. This glues together the Abdera
+ * serialization engine and the directory-based model used inside the server.
+ * 
+ * @author Donal Fellows
+ */
+public class InteractionFeedSupport {
+       /**
+        * The name of the resource within the run resource that is the run's
+        * interaction feed resource.
+        */
+       public static final String FEED_URL_DIR = "interaction";
+       /**
+        * The name of the directory below the run working directory that will
+        * contain the entries of the interaction feed.
+        */
+       public static final String FEED_DIR = "feed";
+       /**
+        * Should the contents of the entry be stripped when describing the 
overall
+        * feed? This makes sense if (and only if) large entries are being 
pushed
+        * through the feed.
+        */
+       private static final boolean STRIP_CONTENTS = false;
+       /** Maximum size of an entry before truncation. */
+       private static final long MAX_ENTRY_SIZE = 50 * 1024;
+       /** Extension for entry files. */
+       private static final String EXT = ".atom";
+
+       private TavernaServerSupport support;
+       private FilenameUtils utils;
+       private Writer writer;
+       private Parser parser;
+       private Factory factory;
+       private UriBuilderFactory uriBuilder;
+
+       private AtomicInteger counter = new AtomicInteger();
+
+       @Required
+       public void setSupport(TavernaServerSupport support) {
+               this.support = support;
+       }
+
+       @Required
+       public void setUtils(FilenameUtils utils) {
+               this.utils = utils;
+       }
+
+       @Required
+       public void setAbdera(Abdera abdera) {
+               this.factory = abdera.getFactory();
+               this.parser = abdera.getParser();
+               this.writer = abdera.getWriterFactory().getWriter("prettyxml");
+       }
+
+       @Required
+       // webapp
+       public void setUriBuilder(UriBuilderFactory uriBuilder) {
+               this.uriBuilder = uriBuilder;
+       }
+
+       private final Map<String, URL> endPoints = new HashMap<>();
+
+       @PostConstruct
+       void determinePorts() {
+               try {
+                       MBeanServer mbs = getPlatformMBeanServer();
+                       for (ObjectName obj : mbs.queryNames(new ObjectName(
+                                       "*:type=Connector,*"),
+                                       match(attr("protocol"), 
value("HTTP/1.1")))) {
+                               String scheme = mbs.getAttribute(obj, 
"scheme").toString();
+                               String port = obj.getKeyProperty("port");
+                               endPoints.put(scheme, new URL(scheme + 
"://localhost:" + port));
+                       }
+                       getLog(getClass()).info(
+                                       "installed feed port publication 
mapping for "
+                                                       + endPoints.keySet());
+               } catch (Exception e) {
+                       getLog(getClass()).error(
+                                       "failure in determining local port 
mapping", e);
+               }
+       }
+       
+       /**
+        * @param run
+        *            The workflow run that defines which feed we are operating 
on.
+        * @return The URI of the feed
+        */
+       public URI getFeedURI(TavernaRun run) {
+               return 
uriBuilder.getRunUriBuilder(run).path(FEED_URL_DIR).build();
+       }
+
+       @Nullable
+       public URL getLocalFeedBase(URI feedURI) {
+               if (feedURI == null)
+                       return null;
+               return endPoints.get(feedURI.getScheme());
+       }
+
+       /**
+        * @param run
+        *            The workflow run that defines which feed we are operating 
on.
+        * @param id
+        *            The ID of the entry.
+        * @return The URI of the entry.
+        */
+       public URI getEntryURI(TavernaRun run, String id) {
+               return uriBuilder.getRunUriBuilder(run)
+                               .path(FEED_URL_DIR + "/{entryID}").build(id);
+       }
+
+       private Entry getEntryFromFile(File f) throws FilesystemAccessException 
{
+               long size = f.getSize();
+               if (size > MAX_ENTRY_SIZE)
+                       throw new FilesystemAccessException("entry larger than 
50kB");
+               byte[] contents = f.getContents(0, (int) size);
+               Document<Entry> doc = parser.parse(new 
ByteArrayInputStream(contents));
+               return doc.getRoot();
+       }
+
+       private void putEntryInFile(Directory dir, String name, Entry contents)
+                       throws FilesystemAccessException, NoUpdateException {
+               ByteArrayOutputStream baos = new ByteArrayOutputStream();
+               try {
+                       writer.writeTo(contents, baos);
+               } catch (IOException e) {
+                       throw new NoUpdateException("failed to serialize the 
ATOM entry", e);
+               }
+               File f = dir.makeEmptyFile(support.getPrincipal(), name);
+               f.appendContents(baos.toByteArray());
+       }
+
+       private List<DirectoryEntry> listPossibleEntries(TavernaRun run)
+                       throws FilesystemAccessException, 
NoDirectoryEntryException {
+               List<DirectoryEntry> entries = new 
ArrayList<>(utils.getDirectory(run,
+                               FEED_DIR).getContentsByDate());
+               reverse(entries);
+               return entries;
+       }
+
+       private String getRunURL(TavernaRun run) {
+               return new 
IRI(uriBuilder.getRunUriBuilder(run).build()).toString();
+       }
+
+       /**
+        * Get the interaction feed for a partciular run.
+        * 
+        * @param run
+        *            The workflow run that defines which feed we are operating 
on.
+        * @return The Abdera feed descriptor.
+        * @throws FilesystemAccessException
+        *             If the feed directory can't be read for some reason.
+        * @throws NoDirectoryEntryException
+        *             If the feed directory doesn't exist or an entry is
+        *             unexpectedly removed.
+        */
+       public Feed getRunFeed(TavernaRun run) throws FilesystemAccessException,
+                       NoDirectoryEntryException {
+               URI feedURI = getFeedURI(run);
+               Feed feed = factory.newFeed();
+               feed.setTitle("Interactions for Taverna Run \"" + run.getName() 
+ "\"");
+               feed.addLink(new IRI(feedURI).toString(), "self");
+               feed.addLink(getRunURL(run), "workflowrun");
+               boolean fetchedDate = false;
+               for (DirectoryEntry de : listPossibleEntries(run)) {
+                       if (!(de instanceof File))
+                               continue;
+                       try {
+                               Entry e = getEntryFromFile((File) de);
+                               if (STRIP_CONTENTS)
+                                       e.setContentElement(null);
+                               feed.addEntry(e);
+                               if (fetchedDate)
+                                       continue;
+                               Date last = e.getUpdated();
+                               if (last == null)
+                                       last = e.getPublished();
+                               if (last == null)
+                                       last = de.getModificationDate();
+                               feed.setUpdated(last);
+                               fetchedDate = true;
+                       } catch (FilesystemAccessException e) {
+                               // Can't do anything about it, so we'll just 
drop the entry.
+                       }
+               }
+               return feed;
+       }
+
+       /**
+        * Gets the contents of a particular feed entry.
+        * 
+        * @param run
+        *            The workflow run that defines which feed we are operating 
on.
+        * @param entryID
+        *            The identifier (from the path) of the entry to read.
+        * @return The description of the entry.
+        * @throws FilesystemAccessException
+        *             If the entry can't be read or is too large.
+        * @throws NoDirectoryEntryException
+        *             If the entry can't be found.
+        */
+       public Entry getRunFeedEntry(TavernaRun run, String entryID)
+                       throws FilesystemAccessException, 
NoDirectoryEntryException {
+               File entryFile = utils.getFile(run, FEED_DIR + "/" + entryID + 
EXT);
+               return getEntryFromFile(entryFile);
+       }
+
+       /**
+        * Given a partial feed entry, store a complete feed entry in the 
filesystem
+        * for a particular run. Note that this does not permit update of an
+        * existing entry; the entry is always created new.
+        * 
+        * @param run
+        *            The workflow run that defines which feed we are operating 
on.
+        * @param entry
+        *            The partial entry to store
+        * @return A link to the entry.
+        * @throws FilesystemAccessException
+        *             If the entry can't be stored.
+        * @throws NoDirectoryEntryException
+        *             If the run is improperly configured.
+        * @throws NoUpdateException
+        *             If the user isn't allowed to do the write.
+        * @throws MalformedURLException
+        *             If a generated URL is illegal (shouldn't happen).
+        */
+       public Entry addRunFeedEntry(TavernaRun run, Entry entry)
+                       throws FilesystemAccessException, 
NoDirectoryEntryException,
+                       NoUpdateException {
+               support.permitUpdate(run);
+               Date now = new Date();
+               entry.newId();
+               String localId = "entry_" + counter.incrementAndGet();
+               IRI selfLink = new IRI(getEntryURI(run, localId));
+               entry.addLink(selfLink.toString(), "self");
+               entry.addLink(getRunURL(run), "workflowrun");
+               entry.setUpdated(now);
+               entry.setPublished(now);
+               putEntryInFile(utils.getDirectory(run, FEED_DIR), localId + 
EXT, entry);
+               return getEntryFromFile(utils.getFile(run, FEED_DIR + "/" + 
localId
+                               + EXT));
+       }
+
+       /**
+        * Deletes an entry from a feed.
+        * 
+        * @param run
+        *            The workflow run that defines which feed we are operating 
on.
+        * @param entryID
+        *            The ID of the entry to delete.
+        * @throws FilesystemAccessException
+        *             If the entry can't be deleted
+        * @throws NoDirectoryEntryException
+        *             If the entry can't be found.
+        * @throws NoUpdateException
+        *             If the current user is not permitted to modify the run's
+        *             characteristics.
+        */
+       public void removeRunFeedEntry(TavernaRun run, String entryID)
+                       throws FilesystemAccessException, 
NoDirectoryEntryException,
+                       NoUpdateException {
+               support.permitUpdate(run);
+               utils.getFile(run, FEED_DIR + "/" + entryID + EXT).destroy();
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/interaction/package-info.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/interaction/package-info.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/interaction/package-info.java
new file mode 100644
index 0000000..9efc30d
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/interaction/package-info.java
@@ -0,0 +1,10 @@
+/*
+ * Copyright (C) 2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+/**
+ * This package contains the Atom feed implementation for interactions for a 
particular workflow run.
+ * @author Donal Fellows
+ */
+package org.taverna.server.master.interaction;

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Directory.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Directory.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Directory.java
new file mode 100644
index 0000000..9a0a84e
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Directory.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.interfaces;
+
+import java.io.PipedInputStream;
+import java.security.Principal;
+import java.util.Collection;
+
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+
+/**
+ * Represents a directory that is the working directory of a workflow run, or a
+ * sub-directory of it.
+ * 
+ * @author Donal Fellows
+ * @see File
+ */
+public interface Directory extends DirectoryEntry {
+       /**
+        * @return A list of the contents of the directory.
+        * @throws FilesystemAccessException
+        *             If things go wrong.
+        */
+       Collection<DirectoryEntry> getContents() throws 
FilesystemAccessException;
+
+       /**
+        * @return A list of the contents of the directory, in guaranteed date
+        *         order.
+        * @throws FilesystemAccessException
+        *             If things go wrong.
+        */
+       Collection<DirectoryEntry> getContentsByDate()
+                       throws FilesystemAccessException;
+
+       /**
+        * @return The contents of the directory (and its sub-directories) as a 
zip.
+        * @throws FilesystemAccessException
+        *             If things go wrong.
+        */
+       ZipStream getContentsAsZip() throws FilesystemAccessException;
+
+       /**
+        * Creates a sub-directory of this directory.
+        * 
+        * @param actor
+        *            Who this is being created by.
+        * @param name
+        *            The name of the sub-directory.
+        * @return A handle to the newly-created directory.
+        * @throws FilesystemAccessException
+        *             If the name is the same as some existing entry in the
+        *             directory, or if something else goes wrong during 
creation.
+        */
+       Directory makeSubdirectory(Principal actor, String name)
+                       throws FilesystemAccessException;
+
+       /**
+        * Creates an empty file in this directory.
+        * 
+        * @param actor
+        *            Who this is being created by.
+        * @param name
+        *            The name of the file to create.
+        * @return A handle to the newly-created file.
+        * @throws FilesystemAccessException
+        *             If the name is the same as some existing entry in the
+        *             directory, or if something else goes wrong during 
creation.
+        */
+       File makeEmptyFile(Principal actor, String name)
+                       throws FilesystemAccessException;
+
+       /**
+        * A simple pipe that produces the zipped contents of a directory.
+        * 
+        * @author Donal Fellows
+        */
+       public static class ZipStream extends PipedInputStream {
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/DirectoryEntry.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/DirectoryEntry.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/DirectoryEntry.java
new file mode 100644
index 0000000..b098152
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/DirectoryEntry.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.interfaces;
+
+import java.util.Date;
+
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+
+/**
+ * An entry in a {@link Directory} representing a file or sub-directory.
+ * 
+ * @author Donal Fellows
+ * @see Directory
+ * @see File
+ */
+public interface DirectoryEntry extends Comparable<DirectoryEntry> {
+       /**
+        * @return The "local" name of the entry. This will never be 
"<tt>..</tt>"
+        *         or contain the character "<tt>/</tt>".
+        */
+       public String getName();
+
+       /**
+        * @return The "full" name of the entry. This is computed relative to 
the
+        *         workflow run's working directory. It may contain the 
"<tt>/</tt>"
+        *         character.
+        */
+       public String getFullName();
+
+       /**
+        * @return The time that the entry was last modified.
+        */
+       public Date getModificationDate();
+
+       /**
+        * Destroy this directory entry, deleting the file or sub-directory. The
+        * workflow run's working directory can never be manually destroyed.
+        * 
+        * @throws FilesystemAccessException
+        *             If the destroy fails for some reason.
+        */
+       public void destroy() throws FilesystemAccessException;
+       // TODO: Permissions (or decide not to do anything about them)
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/File.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/File.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/File.java
new file mode 100644
index 0000000..e4e6590
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/File.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.interfaces;
+
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+
+/**
+ * Represents a file in the working directory of a workflow instance run, or in
+ * some sub-directory of it.
+ * 
+ * @author Donal Fellows
+ * @see Directory
+ */
+public interface File extends DirectoryEntry {
+       /**
+        * @param offset
+        *            Where in the file to start reading.
+        * @param length
+        *            The length of file to read, or -1 to read to the end of 
the
+        *            file.
+        * @return The literal byte contents of the section of the file, or 
null if
+        *         the section doesn't exist.
+        * @throws FilesystemAccessException
+        *             If the read of the file goes wrong.
+        */
+       public byte[] getContents(int offset, int length)
+                       throws FilesystemAccessException;
+
+       /**
+        * Write the data to the file, totally replacing what was there before.
+        * 
+        * @param data
+        *            The literal bytes that will form the new contents of the 
file.
+        * @throws FilesystemAccessException
+        *             If the write to the file goes wrong.
+        */
+       public void setContents(byte[] data) throws FilesystemAccessException;
+
+       /**
+        * Append the data to the file.
+        * 
+        * @param data
+        *            The literal bytes that will be added on to the end of the
+        *            file.
+        * @throws FilesystemAccessException
+        *             If the write to the file goes wrong.
+        */
+       public void appendContents(byte[] data) throws 
FilesystemAccessException;
+
+       /**
+        * @return The length of the file, in bytes.
+        * @throws FilesystemAccessException
+        *             If the read of the file size goes wrong.
+        */
+       public long getSize() throws FilesystemAccessException;
+
+       /**
+        * Asks for the argument file to be copied to this one.
+        * 
+        * @param from
+        *            The source file.
+        * @throws FilesystemAccessException
+        *             If anything goes wrong.
+        */
+       public void copy(File from) throws FilesystemAccessException;
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Input.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Input.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Input.java
new file mode 100644
index 0000000..31cb7cb
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Input.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2010 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.interfaces;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.taverna.server.master.common.Status;
+import org.taverna.server.master.exceptions.BadStateChangeException;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+
+/**
+ * This represents the assignment of inputs to input ports of the workflow. 
Note
+ * that the <tt>file</tt> and <tt>value</tt> properties are never set at the
+ * same time.
+ * 
+ * @author Donal Fellows
+ */
+public interface Input {
+       /**
+        * @return The file currently assigned to this input port, or 
<tt>null</tt>
+        *         if no file is assigned.
+        */
+       @Nullable
+       public String getFile();
+
+       /**
+        * @return The name of this input port. This may not be changed.
+        */
+       @Nonnull
+       public String getName();
+
+       /**
+        * @return The value currently assigned to this input port, or 
<tt>null</tt>
+        *         if no value is assigned.
+        */
+       @Nullable
+       public String getValue();
+
+       /**
+        * @return The delimiter for the input port, or <tt>null</tt> if the 
value
+        *         is not to be split.
+        */
+       @Nullable
+       public String getDelimiter();
+
+       /**
+        * Sets the file to use for this input. This overrides the use of the
+        * previous file and any set value.
+        * 
+        * @param file
+        *            The filename to use. Must not start with a <tt>/</tt> or
+        *            contain any <tt>..</tt> segments. Will be interpreted 
relative
+        *            to the run's working directory.
+        * @throws FilesystemAccessException
+        *             If the filename is invalid.
+        * @throws BadStateChangeException
+        *             If the run isn't in the {@link Status#Initialized
+        *             Initialized} state.
+        */
+       public void setFile(String file) throws FilesystemAccessException,
+                       BadStateChangeException;
+
+       /**
+        * Sets the value to use for this input. This overrides the use of the
+        * previous value and any set file.
+        * 
+        * @param value
+        *            The value to use.
+        * @throws BadStateChangeException
+        *             If the run isn't in the {@link Status#Initialized
+        *             Initialized} state.
+        */
+       public void setValue(String value) throws BadStateChangeException;
+
+       /**
+        * Sets (or clears) the delimiter for the input port.
+        * 
+        * @param delimiter
+        *            The delimiter character, or <tt>null</tt> if the value is 
not
+        *            to be split.
+        * @throws BadStateChangeException
+        *             If the run isn't in the {@link Status#Initialized
+        *             Initialized} state.
+        */
+       @Nullable
+       public void setDelimiter(String delimiter) throws 
BadStateChangeException;
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Listener.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Listener.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Listener.java
new file mode 100644
index 0000000..d7998bc
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Listener.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2010 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.interfaces;
+
+import org.taverna.server.master.exceptions.BadPropertyValueException;
+import org.taverna.server.master.exceptions.NoListenerException;
+
+/**
+ * An event listener that can be attached to a {@link TavernaRun}.
+ * 
+ * @author Donal Fellows
+ */
+public interface Listener {
+       /**
+        * @return The name of the listener.
+        */
+       public String getName();
+
+       /**
+        * @return The type of the listener.
+        */
+       public String getType();
+
+       /**
+        * @return The configuration document for the listener.
+        */
+       public String getConfiguration();
+
+       /**
+        * @return The supported properties of the listener.
+        */
+       public String[] listProperties();
+
+       /**
+        * Get the value of a particular property, which should be listed in the
+        * {@link #listProperties()} method.
+        * 
+        * @param propName
+        *            The name of the property to read.
+        * @return The value of the property.
+        * @throws NoListenerException
+        *             If no property with that name exists.
+        */
+       public String getProperty(String propName) throws NoListenerException;
+
+       /**
+        * Set the value of a particular property, which should be listed in the
+        * {@link #listProperties()} method.
+        * 
+        * @param propName
+        *            The name of the property to write.
+        * @param value
+        *            The value to set the property to.
+        * @throws NoListenerException
+        *             If no property with that name exists.
+        * @throws BadPropertyValueException
+        *             If the value of the property is bad (e.g., wrong syntax).
+        */
+       public void setProperty(String propName, String value)
+                       throws NoListenerException, BadPropertyValueException;
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/LocalIdentityMapper.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/LocalIdentityMapper.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/LocalIdentityMapper.java
new file mode 100644
index 0000000..37b104e
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/LocalIdentityMapper.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.interfaces;
+
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * This interface describes how to map from the identity understood by the
+ * webapp to the identity understood by the local execution system.
+ * 
+ * @author Donal Fellows
+ */
+public interface LocalIdentityMapper {
+       /**
+        * Given a user's identity, get the local identity to use for executing
+        * their workflows. Note that it is assumed that there will never be a
+        * failure from this interface; it is <i>not</i> a security policy
+        * decision or enforcement point.
+        * 
+        * @param user
+        *            An identity token.
+        * @return A user name, which must be defined in the context that 
workflows
+        *         will be running in.
+        */
+       public String getUsernameForPrincipal(UsernamePrincipal user);
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/MessageDispatcher.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/MessageDispatcher.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/MessageDispatcher.java
new file mode 100644
index 0000000..37dbf2c
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/MessageDispatcher.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.interfaces;
+
+import javax.annotation.Nonnull;
+
+/**
+ * The interface supported by all notification message dispatchers.
+ * @author Donal Fellows
+ */
+public interface MessageDispatcher {
+       /**
+        * @return Whether this message dispatcher is actually available (fully
+        *         configured, etc.)
+        */
+       boolean isAvailable();
+
+       /**
+        * @return The name of this dispatcher, which must match the protocol
+        *         supported by it (for a non-universal dispatcher) and the 
name of
+        *         the message generator used to produce the message.
+        */
+       String getName();
+
+       /**
+        * Dispatch a message to a recipient.
+        * 
+        * @param originator
+        *            The workflow run that produced the message.
+        * @param messageSubject
+        *            The subject of the message to send.
+        * @param messageContent
+        *            The plain-text content of the message to send.
+        * @param targetParameter
+        *            A description of where it is to go.
+        * @throws Exception
+        *             If anything goes wrong.
+        */
+       void dispatch(@Nonnull TavernaRun originator,
+                       @Nonnull String messageSubject, @Nonnull String 
messageContent,
+                       @Nonnull String targetParameter) throws Exception;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Policy.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Policy.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Policy.java
new file mode 100644
index 0000000..f57fe71
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Policy.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2010 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.interfaces;
+
+import java.net.URI;
+import java.util.List;
+
+import org.taverna.server.master.common.Status;
+import org.taverna.server.master.common.Workflow;
+import org.taverna.server.master.exceptions.NoCreateException;
+import org.taverna.server.master.exceptions.NoDestroyException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * Simple policy interface.
+ * 
+ * @author Donal Fellows
+ */
+public interface Policy {
+       /**
+        * @return The maximum number of runs that the system can support.
+        */
+       int getMaxRuns();
+
+       /**
+        * Get the limit on the number of runs for this user.
+        * 
+        * @param user
+        *            Who to get the limit for
+        * @return The maximum number of runs for this user, or <tt>null</tt> 
if no
+        *         per-user limit is imposed and only system-wide limits are to 
be
+        *         enforced.
+        */
+       Integer getMaxRuns(UsernamePrincipal user);
+
+       /**
+        * Test whether the user can create an instance of the given workflow.
+        * 
+        * @param user
+        *            Who wants to do the creation.
+        * @param workflow
+        *            The workflow they wish to instantiate.
+        * @throws NoCreateException
+        *             If they may not instantiate it.
+        */
+       void permitCreate(UsernamePrincipal user, Workflow workflow)
+                       throws NoCreateException;
+
+       /**
+        * Test whether the user can destroy a workflow instance run or 
manipulate
+        * its expiry date.
+        * 
+        * @param user
+        *            Who wants to do the deletion.
+        * @param run
+        *            What they want to delete.
+        * @throws NoDestroyException
+        *             If they may not destroy it.
+        */
+       void permitDestroy(UsernamePrincipal user, TavernaRun run)
+                       throws NoDestroyException;
+
+       /**
+        * Return whether the user has access to a particular workflow run.
+        * <b>Note</b> that this does not throw any exceptions!
+        * 
+        * @param user
+        *            Who wants to read the workflow's state.
+        * @param run
+        *            What do they want to read from.
+        * @return Whether they can read it. Note that this check is always 
applied
+        *         before testing whether the workflow can be updated or 
deleted by
+        *         the user.
+        */
+       boolean permitAccess(UsernamePrincipal user, TavernaRun run);
+
+       /**
+        * Test whether the user can modify a workflow run (other than for its
+        * expiry date).
+        * 
+        * @param user
+        *            Who wants to do the modification.
+        * @param run
+        *            What they want to modify.
+        * @throws NoUpdateException
+        *             If they may not modify it.
+        */
+       void permitUpdate(UsernamePrincipal user, TavernaRun run)
+                       throws NoUpdateException;
+
+       /**
+        * Get the URIs of the workflows that the given user may execute.
+        * 
+        * @param user
+        *            Who are we finding out on behalf of.
+        * @return A list of workflow URIs that they may instantiate, or
+        *         <tt>null</tt> if any workflow may be submitted.
+        */
+       List<URI> listPermittedWorkflowURIs(UsernamePrincipal user);
+
+       /**
+        * @return The maximum number of {@linkplain Status#Operating operating}
+        *         runs that the system can support.
+        */
+       int getOperatingLimit();
+
+       /**
+        * Set the URIs of the workflows that the given user may execute.
+        * 
+        * @param user
+        *            Who are we finding out on behalf of.
+        * @param permitted
+        *            A list of workflow URIs that they may instantiate.
+        */
+       void setPermittedWorkflowURIs(UsernamePrincipal user, List<URI> 
permitted);
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/RunStore.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/RunStore.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/RunStore.java
new file mode 100644
index 0000000..b5e84c5
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/RunStore.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2010 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.interfaces;
+
+import java.util.Map;
+
+import org.taverna.server.master.exceptions.UnknownRunException;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * Interface to the mechanism that looks after the mapping of names to runs.
+ * Instances of this class may also be responsible for enforcing timely cleanup
+ * of expired workflows.
+ * 
+ * @author Donal Fellows.
+ */
+public interface RunStore {
+       /**
+        * Obtain the workflow run for a given user and name.
+        * 
+        * @param user
+        *            Who wants to do the lookup.
+        * @param p
+        *            The general policy system context.
+        * @param uuid
+        *            The handle for the run.
+        * @return The workflow instance run.
+        * @throws UnknownRunException
+        *             If the lookup fails (either because it does not exist or
+        *             because it is not permitted for the user by the policy).
+        */
+       TavernaRun getRun(UsernamePrincipal user, Policy p, String uuid)
+                       throws UnknownRunException;
+
+       /**
+        * Obtain the named workflow run.
+        * 
+        * @param uuid
+        *            The handle for the run.
+        * @return The workflow instance run.
+        * @throws UnknownRunException
+        *             If the lookup fails (either because it does not exist or
+        *             because it is not permitted for the user by the policy).
+        */
+       public TavernaRun getRun(String uuid) throws UnknownRunException;
+
+       /**
+        * List the runs that a particular user may access.
+        * 
+        * @param user
+        *            Who wants to do the lookup, or <code>null</code> if it is
+        *            being done "by the system" when the full mapping should be
+        *            returned.
+        * @param p
+        *            The general policy system context.
+        * @return A mapping from run names to run instances.
+        */
+       Map<String, TavernaRun> listRuns(UsernamePrincipal user, Policy p);
+
+       /**
+        * Adds a workflow instance run to the store. Note that this operation 
is
+        * <i>not</i> expected to be security-checked; that is the callers'
+        * responsibility.
+        * 
+        * @param run
+        *            The run itself.
+        * @return The name of the run.
+        */
+       String registerRun(TavernaRun run);
+
+       /**
+        * Removes a run from the store. Note that this operation is <i>not</i>
+        * expected to be security-checked; that is the callers' responsibility.
+        * 
+        * @param uuid
+        *            The name of the run.
+        */
+       void unregisterRun(String uuid);
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/SecurityContextFactory.java
----------------------------------------------------------------------
diff --git 
a/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/SecurityContextFactory.java
 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/SecurityContextFactory.java
new file mode 100644
index 0000000..902c4d0
--- /dev/null
+++ 
b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/SecurityContextFactory.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.interfaces;
+
+import java.io.Serializable;
+
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * How to create instances of a security context.
+ * 
+ * @author Donal Fellows
+ */
+public interface SecurityContextFactory extends Serializable {
+       /**
+        * Creates a security context.
+        * 
+        * @param run
+        *            Handle to remote run. Allows the security context to know 
how
+        *            to apply itself to the workflow run.
+        * @param owner
+        *            The identity of the owner of the workflow run.
+        * @return The security context.
+        * @throws Exception
+        *             If anything goes wrong.
+        */
+       TavernaSecurityContext create(TavernaRun run, UsernamePrincipal owner)
+                       throws Exception;
+}

Reply via email to