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