You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@taverna.apache.org by st...@apache.org on 2018/01/09 23:30:45 UTC

[29/42] incubator-taverna-server git commit: package org.taverna -> org.apache.taverna

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/identity/StrippedDownAuthProvider.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/identity/StrippedDownAuthProvider.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/identity/StrippedDownAuthProvider.java
new file mode 100644
index 0000000..dc489ae
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/identity/StrippedDownAuthProvider.java
@@ -0,0 +1,294 @@
+package org.taverna.server.master.identity;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+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/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/identity/User.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/identity/User.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/identity/User.java
new file mode 100644
index 0000000..1fdf2bf
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/identity/User.java
@@ -0,0 +1,166 @@
+/*
+ */
+package org.taverna.server.master.identity;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+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/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/identity/UserStore.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/identity/UserStore.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/identity/UserStore.java
new file mode 100644
index 0000000..3177d5c
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/identity/UserStore.java
@@ -0,0 +1,402 @@
+/*
+ */
+package org.taverna.server.master.identity;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+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/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/identity/UserStoreAPI.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/identity/UserStoreAPI.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/identity/UserStoreAPI.java
new file mode 100644
index 0000000..c4caf3c
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/identity/UserStoreAPI.java
@@ -0,0 +1,107 @@
+package org.taverna.server.master.identity;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+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/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/identity/WorkflowInternalAuthProvider.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/identity/WorkflowInternalAuthProvider.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/identity/WorkflowInternalAuthProvider.java
new file mode 100644
index 0000000..c733d89
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/identity/WorkflowInternalAuthProvider.java
@@ -0,0 +1,317 @@
+/*
+ */
+package org.taverna.server.master.identity;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+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/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/identity/package-info.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/identity/package-info.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/identity/package-info.java
new file mode 100644
index 0000000..14ad7db
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/identity/package-info.java
@@ -0,0 +1,23 @@
+/*
+ */
+/**
+ * Implementations of beans that map global user identities to local
+ * usernames.
+ */
+package org.taverna.server.master.identity;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interaction/InteractionFeedSupport.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interaction/InteractionFeedSupport.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interaction/InteractionFeedSupport.java
new file mode 100644
index 0000000..4b297dc
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interaction/InteractionFeedSupport.java
@@ -0,0 +1,329 @@
+/*
+ */
+package org.taverna.server.master.interaction;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+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/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interaction/package-info.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interaction/package-info.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interaction/package-info.java
new file mode 100644
index 0000000..54ec630
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interaction/package-info.java
@@ -0,0 +1,23 @@
+/*
+ */
+/**
+ * This package contains the Atom feed implementation for interactions for a particular workflow run.
+ * @author Donal Fellows
+ */
+package org.taverna.server.master.interaction;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */

http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/Directory.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/Directory.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/Directory.java
new file mode 100644
index 0000000..bb74f5a
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/Directory.java
@@ -0,0 +1,95 @@
+/*
+ */
+package org.taverna.server.master.interfaces;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+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/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/DirectoryEntry.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/DirectoryEntry.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/DirectoryEntry.java
new file mode 100644
index 0000000..e1a0865
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/DirectoryEntry.java
@@ -0,0 +1,60 @@
+/*
+ */
+package org.taverna.server.master.interfaces;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+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/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/File.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/File.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/File.java
new file mode 100644
index 0000000..97510e4
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/File.java
@@ -0,0 +1,82 @@
+/*
+ */
+package org.taverna.server.master.interfaces;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+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/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/Input.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/Input.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/Input.java
new file mode 100644
index 0000000..5d92f67
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/Input.java
@@ -0,0 +1,105 @@
+/*
+ */
+package org.taverna.server.master.interfaces;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+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/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/Listener.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/Listener.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/Listener.java
new file mode 100644
index 0000000..5fee6cc
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/Listener.java
@@ -0,0 +1,77 @@
+/*
+ */
+package org.taverna.server.master.interfaces;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+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/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/LocalIdentityMapper.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/LocalIdentityMapper.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/LocalIdentityMapper.java
new file mode 100644
index 0000000..becc55c
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/LocalIdentityMapper.java
@@ -0,0 +1,42 @@
+/*
+ */
+package org.taverna.server.master.interfaces;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+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/00397eff/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/MessageDispatcher.java
----------------------------------------------------------------------
diff --git a/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/MessageDispatcher.java b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/MessageDispatcher.java
new file mode 100644
index 0000000..b3e0260
--- /dev/null
+++ b/taverna-server-webapp/src/main/java/org/apache/taverna/server/master/interfaces/MessageDispatcher.java
@@ -0,0 +1,58 @@
+/*
+ */
+package org.taverna.server.master.interfaces;
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+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