http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/StrippedDownAuthProvider.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/StrippedDownAuthProvider.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/StrippedDownAuthProvider.java new file mode 100644 index 0000000..06202dc --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/StrippedDownAuthProvider.java @@ -0,0 +1,278 @@ +package org.taverna.server.master.identity; + +import java.util.HashMap; +import java.util.Map; + +import javax.annotation.PreDestroy; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Required; +import org.springframework.security.authentication.AccountExpiredException; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.CredentialsExpiredException; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.authentication.LockedException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.taverna.server.master.utils.CallTimeLogger.PerfLogged; + +/** + * A stripped down version of a + * {@link org.springframework.security.authentication.dao.DaoAuthenticationProvider + * DaoAuthenticationProvider}/ + * {@link org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider + * AbstractUserDetailsAuthenticationProvider} that avoids much of the overhead + * associated with that class. + */ +public class StrippedDownAuthProvider implements AuthenticationProvider { + /** + * The plaintext password used to perform + * {@link PasswordEncoder#isPasswordValid(String, String, Object)} on when + * the user is not found to avoid SEC-2056. + */ + private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword"; + + /** + * The password used to perform + * {@link PasswordEncoder#isPasswordValid(String, String, Object)} on when + * the user is not found to avoid SEC-2056. This is necessary, because some + * {@link PasswordEncoder} implementations will short circuit if the + * password is not in a valid format. + */ + private String userNotFoundEncodedPassword; + private UserDetailsService userDetailsService; + private PasswordEncoder passwordEncoder; + private Map<String, AuthCacheEntry> authCache = new HashMap<>(); + protected final Log logger = LogFactory.getLog(getClass()); + + private static class AuthCacheEntry { + private String creds; + private long timestamp; + private static final long VALIDITY = 1000 * 60 * 20; + AuthCacheEntry(String credentials) { + creds = credentials; + timestamp = System.currentTimeMillis(); + } + boolean valid(String password) { + return creds.equals(password) && timestamp+VALIDITY > System.currentTimeMillis(); + } + } + + @PerfLogged + @Override + public Authentication authenticate(Authentication authentication) + throws AuthenticationException { + + if (!(authentication instanceof UsernamePasswordAuthenticationToken)) + throw new IllegalArgumentException( + "can only authenticate against username+password"); + UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication; + + // Determine username + String username = (auth.getPrincipal() == null) ? "NONE_PROVIDED" + : auth.getName(); + + UserDetails user; + + try { + user = retrieveUser(username, auth); + if (user == null) + throw new IllegalStateException( + "retrieveUser returned null - a violation of the interface contract"); + } catch (UsernameNotFoundException notFound) { + if (logger.isDebugEnabled()) + logger.debug("User '" + username + "' not found", notFound); + throw new BadCredentialsException("Bad credentials"); + } + + // Pre-auth + if (!user.isAccountNonLocked()) + throw new LockedException("User account is locked"); + if (!user.isEnabled()) + throw new DisabledException("User account is disabled"); + if (!user.isAccountNonExpired()) + throw new AccountExpiredException("User account has expired"); + Object credentials = auth.getCredentials(); + if (credentials == null) { + logger.debug("Authentication failed: no credentials provided"); + + throw new BadCredentialsException("Bad credentials"); + } + + String providedPassword = credentials.toString(); + boolean matched = false; + synchronized (authCache) { + AuthCacheEntry pw = authCache.get(username); + if (pw != null && providedPassword != null) { + if (pw.valid(providedPassword)) + matched = true; + else + authCache.remove(username); + } + } + // Auth + if (!matched) { + if (!passwordEncoder.matches(providedPassword, user.getPassword())) { + logger.debug("Authentication failed: password does not match stored value"); + + throw new BadCredentialsException("Bad credentials"); + } + if (providedPassword != null) + synchronized (authCache) { + authCache.put(username, new AuthCacheEntry(providedPassword)); + } + } + + // Post-auth + if (!user.isCredentialsNonExpired()) + throw new CredentialsExpiredException( + "User credentials have expired"); + + return createSuccessAuthentication(user, auth, user); + } + + @PreDestroy + void clearCache() { + authCache.clear(); + } + + /** + * Creates a successful {@link Authentication} object. + * <p> + * Protected so subclasses can override. + * </p> + * <p> + * Subclasses will usually store the original credentials the user supplied + * (not salted or encoded passwords) in the returned + * <code>Authentication</code> object. + * </p> + * + * @param principal + * that should be the principal in the returned object (defined + * by the {@link #isForcePrincipalAsString()} method) + * @param authentication + * that was presented to the provider for validation + * @param user + * that was loaded by the implementation + * + * @return the successful authentication token + */ + private Authentication createSuccessAuthentication(Object principal, + Authentication authentication, UserDetails user) { + /* + * Ensure we return the original credentials the user supplied, so + * subsequent attempts are successful even with encoded passwords. Also + * ensure we return the original getDetails(), so that future + * authentication events after cache expiry contain the details + */ + UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken( + principal, authentication.getCredentials(), + user.getAuthorities()); + result.setDetails(authentication.getDetails()); + + return result; + } + + @Override + public boolean supports(Class<?> authentication) { + return UsernamePasswordAuthenticationToken.class + .isAssignableFrom(authentication); + } + + /** + * Allows subclasses to actually retrieve the <code>UserDetails</code> from + * an implementation-specific location, with the option of throwing an + * <code>AuthenticationException</code> immediately if the presented + * credentials are incorrect (this is especially useful if it is necessary + * to bind to a resource as the user in order to obtain or generate a + * <code>UserDetails</code>). + * <p> + * Subclasses are not required to perform any caching, as the + * <code>AbstractUserDetailsAuthenticationProvider</code> will by default + * cache the <code>UserDetails</code>. The caching of + * <code>UserDetails</code> does present additional complexity as this means + * subsequent requests that rely on the cache will need to still have their + * credentials validated, even if the correctness of credentials was assured + * by subclasses adopting a binding-based strategy in this method. + * Accordingly it is important that subclasses either disable caching (if + * they want to ensure that this method is the only method that is capable + * of authenticating a request, as no <code>UserDetails</code> will ever be + * cached) or ensure subclasses implement + * {@link #additionalAuthenticationChecks(UserDetails, UsernamePasswordAuthenticationToken)} + * to compare the credentials of a cached <code>UserDetails</code> with + * subsequent authentication requests. + * </p> + * <p> + * Most of the time subclasses will not perform credentials inspection in + * this method, instead performing it in + * {@link #additionalAuthenticationChecks(UserDetails, UsernamePasswordAuthenticationToken)} + * so that code related to credentials validation need not be duplicated + * across two methods. + * </p> + * + * @param username + * The username to retrieve + * @param authentication + * The authentication request, which subclasses <em>may</em> need + * to perform a binding-based retrieval of the + * <code>UserDetails</code> + * + * @return the user information (never <code>null</code> - instead an + * exception should the thrown) + * + * @throws AuthenticationException + * if the credentials could not be validated (generally a + * <code>BadCredentialsException</code>, an + * <code>AuthenticationServiceException</code> or + * <code>UsernameNotFoundException</code>) + */ + private UserDetails retrieveUser(String username, + UsernamePasswordAuthenticationToken authentication) + throws AuthenticationException { + try { + return userDetailsService.loadUserByUsername(username); + } catch (UsernameNotFoundException notFound) { + if (authentication.getCredentials() != null) { + String presentedPassword = authentication.getCredentials() + .toString(); + passwordEncoder.matches(presentedPassword, + userNotFoundEncodedPassword); + } + throw notFound; + } catch (AuthenticationException e) { + throw e; + } catch (Exception repositoryProblem) { + throw new AuthenticationServiceException( + repositoryProblem.getMessage(), repositoryProblem); + } + } + + /** + * Sets the PasswordEncoder instance to be used to encode and validate + * passwords. + */ + @Required + public void setPasswordEncoder(PasswordEncoder passwordEncoder) { + if (passwordEncoder == null) + throw new IllegalArgumentException("passwordEncoder cannot be null"); + + this.passwordEncoder = passwordEncoder; + this.userNotFoundEncodedPassword = passwordEncoder + .encode(USER_NOT_FOUND_PASSWORD); + } + + @Required + public void setUserDetailsService(UserDetailsService userDetailsService) { + if (userDetailsService == null) + throw new IllegalStateException("A UserDetailsService must be set"); + this.userDetailsService = userDetailsService; + } +}
http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/User.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/User.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/User.java new file mode 100644 index 0000000..bdb6e40 --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/User.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2011-2012 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.identity; + +import static org.taverna.server.master.common.Roles.ADMIN; +import static org.taverna.server.master.common.Roles.USER; +import static org.taverna.server.master.defaults.Default.AUTHORITY_PREFIX; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javax.jdo.annotations.PersistenceCapable; +import javax.jdo.annotations.Persistent; +import javax.jdo.annotations.Query; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +/** + * The representation of a user in the database. + * <p> + * A user consists logically of a (non-ordered) tuple of items: + * <ul> + * <li>The {@linkplain #getUsername() user name}, + * <li>The {@linkplain #getPassword() user's password} (salted, encoded), + * <li>Whether the user is {@linkplain #isEnabled() enabled} (i.e., able to log + * in), + * <li>Whether the user has {@linkplain #isAdmin() administrative privileges}, and + * <li>What {@linkplain #getLocalUsername() system (Unix) account} the user's + * workflows will run as; separation between different users that are mapped to + * the same system account is nothing like as strongly enforced. + * </ul> + * + * @author Donal Fellows + */ +@PersistenceCapable(schema = "USERS", table = "LIST") +@Query(name = "users", language = "SQL", value = "SELECT id FROM USERS.LIST ORDER BY id", resultClass = String.class) +@XmlRootElement +@XmlType(name = "User", propOrder = {}) +@SuppressWarnings("serial") +public class User implements UserDetails { + @XmlElement + @Persistent + private boolean disabled; + @XmlElement(name = "username", required = true) + @Persistent(primaryKey = "true") + private String id; + @XmlElement(name = "password", required = true) + @Persistent(column = "password") + private String encodedPassword; + @XmlElement + @Persistent + private boolean admin; + @XmlElement + @Persistent + private String localUsername; + + @Override + public Collection<GrantedAuthority> getAuthorities() { + List<GrantedAuthority> auths = new ArrayList<>(); + auths.add(new LiteralGrantedAuthority(USER)); + if (admin) + auths.add(new LiteralGrantedAuthority(ADMIN)); + if (localUsername != null) + auths.add(new LiteralGrantedAuthority(AUTHORITY_PREFIX + + localUsername)); + return auths; + } + + @Override + public String getPassword() { + return encodedPassword; + } + + @Override + public String getUsername() { + return id; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return !disabled; + } + + void setDisabled(boolean disabled) { + this.disabled = disabled; + } + + void setUsername(String username) { + this.id = username; + } + + void setEncodedPassword(String password) { + this.encodedPassword = password; + } + + void setAdmin(boolean admin) { + this.admin = admin; + } + + public boolean isAdmin() { + return admin; + } + + void setLocalUsername(String localUsername) { + this.localUsername = localUsername; + } + + public String getLocalUsername() { + return localUsername; + } +} + +@SuppressWarnings("serial") +class LiteralGrantedAuthority implements GrantedAuthority { + private String auth; + + LiteralGrantedAuthority(String auth) { + this.auth = auth; + } + + @Override + public String getAuthority() { + return auth; + } + + @Override + public String toString() { + return "AUTHORITY(" + auth + ")"; + } +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/UserStore.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/UserStore.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/UserStore.java new file mode 100644 index 0000000..054d932 --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/UserStore.java @@ -0,0 +1,389 @@ +/* + * Copyright (C) 2011-2012 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.identity; + +import static org.apache.commons.logging.LogFactory.getLog; +import static org.taverna.server.master.TavernaServer.JMX_ROOT; +import static org.taverna.server.master.common.Roles.ADMIN; +import static org.taverna.server.master.common.Roles.USER; +import static org.taverna.server.master.defaults.Default.AUTHORITY_PREFIX; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.jdo.annotations.PersistenceAware; + +import org.apache.commons.logging.Log; +import org.springframework.beans.factory.annotation.Required; +import org.springframework.dao.DataAccessException; +import org.springframework.jmx.export.annotation.ManagedAttribute; +import org.springframework.jmx.export.annotation.ManagedOperation; +import org.springframework.jmx.export.annotation.ManagedOperationParameter; +import org.springframework.jmx.export.annotation.ManagedOperationParameters; +import org.springframework.jmx.export.annotation.ManagedResource; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.core.userdetails.memory.UserAttribute; +import org.springframework.security.core.userdetails.memory.UserAttributeEditor; +import org.taverna.server.master.utils.CallTimeLogger.PerfLogged; +import org.taverna.server.master.utils.JDOSupport; + +/** + * The bean class that is responsible for managing the users in the database. + * + * @author Donal Fellows + */ +@PersistenceAware +@ManagedResource(objectName = JMX_ROOT + "Users", description = "The user database.") +public class UserStore extends JDOSupport<User> implements UserDetailsService, + UserStoreAPI { + /** The logger for the user store. */ + private transient Log log = getLog("Taverna.Server.UserDB"); + + public UserStore() { + super(User.class); + } + + @PreDestroy + void closeLog() { + log = null; + } + + private Map<String, BootstrapUserInfo> base = new HashMap<>(); + private String defLocalUser; + private PasswordEncoder encoder; + private volatile int epoch; + + /** + * Install the encoder that will be used to turn a plaintext password into + * something that it is safe to store in the database. + * + * @param encoder + * The password encoder bean to install. + */ + public void setEncoder(PasswordEncoder encoder) { + this.encoder = encoder; + } + + public void setBaselineUserProperties(Properties props) { + UserAttributeEditor parser = new UserAttributeEditor(); + + for (Object name : props.keySet()) { + String username = (String) name; + String value = props.getProperty(username); + + // Convert value to a password, enabled setting, and list of granted + // authorities + parser.setAsText(value); + + UserAttribute attr = (UserAttribute) parser.getValue(); + if (attr != null && attr.isEnabled()) + base.put(username, new BootstrapUserInfo(username, attr)); + } + } + + private void installPassword(User u, String password) { + u.setEncodedPassword(encoder.encode(password)); + } + + public void setDefaultLocalUser(String defLocalUser) { + this.defLocalUser = defLocalUser; + } + + @SuppressWarnings("unchecked") + private List<String> getUsers() { + return (List<String>) namedQuery("users").execute(); + } + + @WithinSingleTransaction + @PostConstruct + void initDB() { + if (base == null || base.isEmpty()) + log.warn("no baseline user collection"); + else if (!getUsers().isEmpty()) + log.info("using existing users from database"); + else + for (String username : base.keySet()) { + BootstrapUserInfo ud = base.get(username); + if (ud == null) + continue; + User u = ud.get(encoder); + if (u == null) + continue; + log.info("bootstrapping user " + username + " in the database"); + persist(u); + } + base = null; + epoch++; + } + + @Override + @PerfLogged + @WithinSingleTransaction + @ManagedAttribute(description = "The list of server accounts known about.", currencyTimeLimit = 30) + public List<String> getUserNames() { + return getUsers(); + } + + @Override + @PerfLogged + @WithinSingleTransaction + public User getUser(String userName) { + return detach(getById(userName)); + } + + /** + * Get information about a server account. + * + * @param userName + * The username to look up. + * @return A description map intended for use by a server admin over JMX. + */ + @PerfLogged + @WithinSingleTransaction + @ManagedOperation(description = "Get information about a server account.") + @ManagedOperationParameters(@ManagedOperationParameter(name = "userName", description = "The username to look up.")) + public Map<String, String> getUserInfo(String userName) { + User u = getById(userName); + Map<String, String> info = new HashMap<>(); + info.put("name", u.getUsername()); + info.put("admin", u.isAdmin() ? "yes" : "no"); + info.put("enabled", u.isEnabled() ? "yes" : "no"); + info.put("localID", u.getLocalUsername()); + return info; + } + + /** + * Get a list of all the users in the database. + * + * @return A list of user details, <i>copied</i> out of the database. + */ + @PerfLogged + @WithinSingleTransaction + public List<UserDetails> listUsers() { + ArrayList<UserDetails> result = new ArrayList<>(); + for (String id : getUsers()) + result.add(detach(getById(id))); + return result; + } + + @Override + @PerfLogged + @WithinSingleTransaction + @ManagedOperation(description = "Create a new user account; the account will be disabled and " + + "non-administrative by default. Does not create any underlying system account.") + @ManagedOperationParameters({ + @ManagedOperationParameter(name = "username", description = "The username to create."), + @ManagedOperationParameter(name = "password", description = "The password to use."), + @ManagedOperationParameter(name = "coupleLocalUsername", description = "Whether to set the local user name to the 'main' one.") }) + public void addUser(String username, String password, + boolean coupleLocalUsername) { + if (username.matches(".*[^a-zA-Z0-9].*")) + throw new IllegalArgumentException( + "bad user name; must be pure alphanumeric"); + if (getById(username) != null) + throw new IllegalArgumentException("user name already exists"); + User u = new User(); + u.setDisabled(true); + u.setAdmin(false); + u.setUsername(username); + installPassword(u, password); + if (coupleLocalUsername) + u.setLocalUsername(username); + else + u.setLocalUsername(defLocalUser); + log.info("creating user for " + username); + persist(u); + epoch++; + } + + @Override + @PerfLogged + @WithinSingleTransaction + @ManagedOperation(description = "Set or clear whether this account is enabled. " + + "Disabled accounts cannot be used to log in.") + @ManagedOperationParameters({ + @ManagedOperationParameter(name = "username", description = "The username to adjust."), + @ManagedOperationParameter(name = "enabled", description = "Whether to enable the account.") }) + public void setUserEnabled(String username, boolean enabled) { + User u = getById(username); + if (u != null) { + u.setDisabled(!enabled); + log.info((enabled ? "enabling" : "disabling") + " user " + username); + epoch++; + } + } + + @Override + @PerfLogged + @WithinSingleTransaction + @ManagedOperation(description = "Set or clear the mark on an account that indicates " + + "that it has administrative privileges.") + @ManagedOperationParameters({ + @ManagedOperationParameter(name = "username", description = "The username to adjust."), + @ManagedOperationParameter(name = "admin", description = "Whether the account has admin privileges.") }) + public void setUserAdmin(String username, boolean admin) { + User u = getById(username); + if (u != null) { + u.setAdmin(admin); + log.info((admin ? "enabling" : "disabling") + " user " + username + + " admin status"); + epoch++; + } + } + + @Override + @PerfLogged + @WithinSingleTransaction + @ManagedOperation(description = "Change the password for an account.") + @ManagedOperationParameters({ + @ManagedOperationParameter(name = "username", description = "The username to adjust."), + @ManagedOperationParameter(name = "password", description = "The new password to use.") }) + public void setUserPassword(String username, String password) { + User u = getById(username); + if (u != null) { + installPassword(u, password); + log.info("changing password for user " + username); + epoch++; + } + } + + @Override + @PerfLogged + @WithinSingleTransaction + @ManagedOperation(description = "Change what local system account to use for a server account.") + @ManagedOperationParameters({ + @ManagedOperationParameter(name = "username", description = "The username to adjust."), + @ManagedOperationParameter(name = "password", description = "The new local user account use.") }) + public void setUserLocalUser(String username, String localUsername) { + User u = getById(username); + if (u != null) { + u.setLocalUsername(localUsername); + log.info("mapping user " + username + " to local account " + + localUsername); + epoch++; + } + } + + @Override + @PerfLogged + @WithinSingleTransaction + @ManagedOperation(description = "Delete a server account. The underlying " + + "system account is not modified.") + @ManagedOperationParameters(@ManagedOperationParameter(name = "username", description = "The username to delete.")) + public void deleteUser(String username) { + delete(getById(username)); + log.info("deleting user " + username); + epoch++; + } + + @Override + @PerfLogged + @WithinSingleTransaction + public UserDetails loadUserByUsername(String username) + throws UsernameNotFoundException, DataAccessException { + User u; + if (base != null) { + log.warn("bootstrap user store still installed!"); + BootstrapUserInfo ud = base.get(username); + if (ud != null) { + log.warn("retrieved production credentials for " + username + + " from bootstrap store"); + u = ud.get(encoder); + if (u != null) + return u; + } + } + try { + u = detach(getById(username)); + } catch (NullPointerException npe) { + throw new UsernameNotFoundException("who are you?"); + } catch (Exception ex) { + throw new UsernameNotFoundException("who are you?", ex); + } + if (u != null) + return u; + throw new UsernameNotFoundException("who are you?"); + } + + int getEpoch() { + return epoch; + } + + public static class CachedUserStore implements UserDetailsService { + private int epoch; + private Map<String, UserDetails> cache = new HashMap<>(); + private UserStore realStore; + + @Required + public void setRealStore(UserStore store) { + this.realStore = store; + } + + @Override + @PerfLogged + public UserDetails loadUserByUsername(String username) { + int epoch = realStore.getEpoch(); + UserDetails details; + synchronized (cache) { + if (epoch != this.epoch) { + cache.clear(); + this.epoch = epoch; + details = null; + } else + details = cache.get(username); + } + if (details == null) { + details = realStore.loadUserByUsername(username); + synchronized (cache) { + cache.put(username, details); + } + } + return details; + } + } + + private static class BootstrapUserInfo { + private String user; + private String pass; + private Collection<GrantedAuthority> auth; + + BootstrapUserInfo(String username, UserAttribute attr) { + user = username; + pass = attr.getPassword(); + auth = attr.getAuthorities(); + } + + User get(PasswordEncoder encoder) { + User u = new User(); + boolean realUser = false; + for (GrantedAuthority ga : auth) { + String a = ga.getAuthority(); + if (a.startsWith(AUTHORITY_PREFIX)) + u.setLocalUsername(a.substring(AUTHORITY_PREFIX.length())); + else if (a.equals(USER)) + realUser = true; + else if (a.equals(ADMIN)) + u.setAdmin(true); + } + if (!realUser) + return null; + u.setUsername(user); + u.setEncodedPassword(encoder.encode(pass)); + u.setDisabled(false); + return u; + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/UserStoreAPI.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/UserStoreAPI.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/UserStoreAPI.java new file mode 100644 index 0000000..a048da9 --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/UserStoreAPI.java @@ -0,0 +1,91 @@ +package org.taverna.server.master.identity; + +import java.util.List; + +/** + * The API that is exposed by the DAO that exposes user management. + * + * @author Donal Fellows + * @see User + */ +public interface UserStoreAPI { + /** + * List the currently-known account names. + * + * @return A list of users in the database. Note that this is a snapshot. + */ + List<String> getUserNames(); + + /** + * Get a particular user's description. + * + * @param userName + * The username to look up. + * @return A <i>copy</i> of the user description. + */ + User getUser(String userName); + + /** + * Create a new user account; the account will be disabled and + * non-administrative by default. Does not create any underlying system + * account. + * + * @param username + * The username to create. + * @param password + * The password to use. + * @param coupleLocalUsername + * Whether to set the local user name to the 'main' one. + */ + void addUser(String username, String password, boolean coupleLocalUsername); + + /** + * Set or clear whether this account is enabled. Disabled accounts cannot be + * used to log in. + * + * @param username + * The username to adjust. + * @param enabled + * Whether to enable the account. + */ + void setUserEnabled(String username, boolean enabled); + + /** + * Set or clear the mark on an account that indicates that it has + * administrative privileges. + * + * @param username + * The username to adjust. + * @param admin + * Whether the account has admin privileges. + */ + void setUserAdmin(String username, boolean admin); + + /** + * Change the password for an account. + * + * @param username + * The username to adjust. + * @param password + * The new password to use. + */ + void setUserPassword(String username, String password); + + /** + * Change what local system account to use for a server account. + * + * @param username + * The username to adjust. + * @param localUsername + * The new local user account use. + */ + void setUserLocalUser(String username, String localUsername); + + /** + * Delete a server account. The underlying system account is not modified. + * + * @param username + * The username to delete. + */ + void deleteUser(String username); +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/WorkflowInternalAuthProvider.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/WorkflowInternalAuthProvider.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/WorkflowInternalAuthProvider.java new file mode 100644 index 0000000..9219a60 --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/WorkflowInternalAuthProvider.java @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2013 The University of Manchester + * + * See the file "LICENSE.txt" for license terms. + */ +package org.taverna.server.master.identity; + +import static java.util.Collections.synchronizedMap; +import static org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes; +import static org.taverna.server.master.common.Roles.SELF; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Nonnull; +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Required; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.web.authentication.WebAuthenticationDetails; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.taverna.server.master.exceptions.UnknownRunException; +import org.taverna.server.master.interfaces.LocalIdentityMapper; +import org.taverna.server.master.interfaces.RunStore; +import org.taverna.server.master.utils.CallTimeLogger.PerfLogged; +import org.taverna.server.master.utils.UsernamePrincipal; +import org.taverna.server.master.worker.RunDatabaseDAO; + +/** + * A special authentication provider that allows a workflow to authenticate to + * itself. This is used to allow the workflow to publish to its own interaction + * feed. + * + * @author Donal Fellows + */ +public class WorkflowInternalAuthProvider extends + AbstractUserDetailsAuthenticationProvider { + private Log log = LogFactory.getLog("Taverna.Server.UserDB"); + private static final boolean logDecisions = true; + public static final String PREFIX = "wfrun_"; + private RunDatabaseDAO dao; + private Map<String, String> cache; + + @Required + public void setDao(RunDatabaseDAO dao) { + this.dao = dao; + } + + @Required + @SuppressWarnings("serial") + public void setCacheBound(final int bound) { + cache = synchronizedMap(new LinkedHashMap<String, String>() { + @Override + protected boolean removeEldestEntry(Map.Entry<String, String> eldest) { + return size() > bound; + } + }); + } + + public void setAuthorizedAddresses(String[] addresses) { + authorizedAddresses = new HashSet<>(localAddresses); + for (String s : addresses) + authorizedAddresses.add(s); + } + + @PostConstruct + public void logConfig() { + log.info("authorized addresses for automatic access: " + + authorizedAddresses); + } + + @PreDestroy + void closeLog() { + log = null; + } + + private final Set<String> localAddresses = new HashSet<>(); + private Set<String> authorizedAddresses; + { + localAddresses.add("127.0.0.1"); // IPv4 + localAddresses.add("::1"); // IPv6 + try { + InetAddress addr = InetAddress.getLocalHost(); + if (!addr.isLoopbackAddress()) + localAddresses.add(addr.getHostAddress()); + } catch (UnknownHostException e) { + // Ignore the exception + } + authorizedAddresses = new HashSet<>(localAddresses); + } + + /** + * Check that the authentication request is actually valid for the given + * user record. + * + * @param userRecord + * as retrieved from the + * {@link #retrieveUser(String, UsernamePasswordAuthenticationToken)} + * or <code>UserCache</code> + * @param principal + * the principal that is trying to authenticate (and that we're + * trying to bind) + * @param credentials + * the credentials (e.g., password) presented by the principal + * + * @throws AuthenticationException + * AuthenticationException if the credentials could not be + * validated (generally a <code>BadCredentialsException</code>, + * an <code>AuthenticationServiceException</code>) + * @throws Exception + * If something goes wrong. Will be logged and converted to a + * generic AuthenticationException. + */ + protected void additionalAuthenticationChecks(UserDetails userRecord, + @Nonnull Object principal, @Nonnull Object credentials) + throws Exception { + @Nonnull + HttpServletRequest req = ((ServletRequestAttributes) currentRequestAttributes()) + .getRequest(); + + // Are we coming from a "local" address? + if (!req.getLocalAddr().equals(req.getRemoteAddr()) + && !authorizedAddresses.contains(req.getRemoteAddr())) { + if (logDecisions) + log.info("attempt to use workflow magic token from untrusted address:" + + " token=" + + userRecord.getUsername() + + ", address=" + + req.getRemoteAddr()); + throw new BadCredentialsException("bad login token"); + } + + // Does the password match? + if (!credentials.equals(userRecord.getPassword())) { + if (logDecisions) + log.info("workflow magic token is untrusted due to password mismatch:" + + " wanted=" + + userRecord.getPassword() + + ", got=" + + credentials); + throw new BadCredentialsException("bad login token"); + } + + if (logDecisions) + log.info("granted role " + SELF + " to user " + + userRecord.getUsername()); + } + + /** + * Retrieve the <code>UserDetails</code> from the relevant store, with the + * option of throwing an <code>AuthenticationException</code> immediately if + * the presented credentials are incorrect (this is especially useful if it + * is necessary to bind to a resource as the user in order to obtain or + * generate a <code>UserDetails</code>). + * + * @param username + * The username to retrieve + * @param details + * The details from the authentication request. + * @see #retrieveUser(String,UsernamePasswordAuthenticationToken) + * @return the user information (never <code>null</code> - instead an + * exception should the thrown) + * @throws AuthenticationException + * if the credentials could not be validated (generally a + * <code>BadCredentialsException</code>, an + * <code>AuthenticationServiceException</code> or + * <code>UsernameNotFoundException</code>) + * @throws Exception + * If something goes wrong. It will be logged and converted into + * a general AuthenticationException. + */ + @Nonnull + protected UserDetails retrieveUser(String username, Object details) + throws Exception { + if (details == null || !(details instanceof WebAuthenticationDetails)) + throw new UsernameNotFoundException("context unsupported"); + if (!username.startsWith(PREFIX)) + throw new UsernameNotFoundException( + "unsupported username for this provider"); + if (logDecisions) + log.info("request for auth for user " + username); + String wfid = username.substring(PREFIX.length()); + String securityToken; + try { + securityToken = cache.get(wfid); + if (securityToken == null) { + securityToken = dao.getSecurityToken(wfid); + if (securityToken == null) + throw new UsernameNotFoundException("no such user"); + cache.put(wfid, securityToken); + } + } catch (NullPointerException npe) { + throw new UsernameNotFoundException("no such user"); + } + return new User(username, securityToken, true, true, true, true, + Arrays.asList(new LiteralGrantedAuthority(SELF), + new WorkflowSelfAuthority(wfid))); + } + + @Override + @PerfLogged + protected final void additionalAuthenticationChecks(UserDetails userRecord, + UsernamePasswordAuthenticationToken token) { + try { + additionalAuthenticationChecks(userRecord, token.getPrincipal(), + token.getCredentials()); + } catch (AuthenticationException e) { + throw e; + } catch (Exception e) { + log.warn("unexpected failure in authentication", e); + throw new AuthenticationServiceException( + "unexpected failure in authentication", e); + } + } + + @Override + @Nonnull + @PerfLogged + protected final UserDetails retrieveUser(String username, + UsernamePasswordAuthenticationToken token) { + try { + return retrieveUser(username, token.getDetails()); + } catch (AuthenticationException e) { + throw e; + } catch (Exception e) { + log.warn("unexpected failure in authentication", e); + throw new AuthenticationServiceException( + "unexpected failure in authentication", e); + } + } + + @SuppressWarnings("serial") + public static class WorkflowSelfAuthority extends LiteralGrantedAuthority { + public WorkflowSelfAuthority(String wfid) { + super(wfid); + } + + public String getWorkflowID() { + return getAuthority(); + } + + @Override + public String toString() { + return "WORKFLOW(" + getAuthority() + ")"; + } + } + + public static class WorkflowSelfIDMapper implements LocalIdentityMapper { + private Log log = LogFactory.getLog("Taverna.Server.UserDB"); + private RunStore runStore; + + @PreDestroy + void closeLog() { + log = null; + } + + @Required + public void setRunStore(RunStore runStore) { + this.runStore = runStore; + } + + private String getUsernameForSelfAccess(WorkflowSelfAuthority authority) + throws UnknownRunException { + return runStore.getRun(authority.getWorkflowID()) + .getSecurityContext().getOwner().getName(); + } + + @Override + @PerfLogged + public String getUsernameForPrincipal(UsernamePrincipal user) { + Authentication auth = SecurityContextHolder.getContext() + .getAuthentication(); + if (auth == null || !auth.isAuthenticated()) + return null; + try { + for (GrantedAuthority authority : auth.getAuthorities()) + if (authority instanceof WorkflowSelfAuthority) + return getUsernameForSelfAccess((WorkflowSelfAuthority) authority); + } catch (UnknownRunException e) { + log.warn("workflow run disappeared during computation of workflow map identity"); + } + return null; + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/package-info.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/package-info.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/package-info.java new file mode 100644 index 0000000..dd1500a --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/identity/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright (C) 2011 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +/** + * Implementations of beans that map global user identities to local + * usernames. + */ +package org.taverna.server.master.identity; http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/interaction/InteractionFeedSupport.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/interaction/InteractionFeedSupport.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/interaction/InteractionFeedSupport.java new file mode 100644 index 0000000..99e1d99 --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/interaction/InteractionFeedSupport.java @@ -0,0 +1,316 @@ +/* + * Copyright (C) 2013 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.interaction; + +import static java.lang.management.ManagementFactory.getPlatformMBeanServer; +import static java.util.Collections.reverse; +import static javax.management.Query.attr; +import static javax.management.Query.match; +import static javax.management.Query.value; +import static org.apache.commons.logging.LogFactory.getLog; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.annotation.Nullable; +import javax.annotation.PostConstruct; +import javax.management.MBeanServer; +import javax.management.ObjectName; + +import org.apache.abdera.Abdera; +import org.apache.abdera.factory.Factory; +import org.apache.abdera.i18n.iri.IRI; +import org.apache.abdera.model.Document; +import org.apache.abdera.model.Entry; +import org.apache.abdera.model.Feed; +import org.apache.abdera.parser.Parser; +import org.apache.abdera.writer.Writer; +import org.springframework.beans.factory.annotation.Required; +import org.taverna.server.master.TavernaServerSupport; +import org.taverna.server.master.exceptions.FilesystemAccessException; +import org.taverna.server.master.exceptions.NoDirectoryEntryException; +import org.taverna.server.master.exceptions.NoUpdateException; +import org.taverna.server.master.interfaces.Directory; +import org.taverna.server.master.interfaces.DirectoryEntry; +import org.taverna.server.master.interfaces.File; +import org.taverna.server.master.interfaces.TavernaRun; +import org.taverna.server.master.interfaces.UriBuilderFactory; +import org.taverna.server.master.utils.FilenameUtils; + +/** + * Bean that supports interaction feeds. This glues together the Abdera + * serialization engine and the directory-based model used inside the server. + * + * @author Donal Fellows + */ +public class InteractionFeedSupport { + /** + * The name of the resource within the run resource that is the run's + * interaction feed resource. + */ + public static final String FEED_URL_DIR = "interaction"; + /** + * The name of the directory below the run working directory that will + * contain the entries of the interaction feed. + */ + public static final String FEED_DIR = "feed"; + /** + * Should the contents of the entry be stripped when describing the overall + * feed? This makes sense if (and only if) large entries are being pushed + * through the feed. + */ + private static final boolean STRIP_CONTENTS = false; + /** Maximum size of an entry before truncation. */ + private static final long MAX_ENTRY_SIZE = 50 * 1024; + /** Extension for entry files. */ + private static final String EXT = ".atom"; + + private TavernaServerSupport support; + private FilenameUtils utils; + private Writer writer; + private Parser parser; + private Factory factory; + private UriBuilderFactory uriBuilder; + + private AtomicInteger counter = new AtomicInteger(); + + @Required + public void setSupport(TavernaServerSupport support) { + this.support = support; + } + + @Required + public void setUtils(FilenameUtils utils) { + this.utils = utils; + } + + @Required + public void setAbdera(Abdera abdera) { + this.factory = abdera.getFactory(); + this.parser = abdera.getParser(); + this.writer = abdera.getWriterFactory().getWriter("prettyxml"); + } + + @Required + // webapp + public void setUriBuilder(UriBuilderFactory uriBuilder) { + this.uriBuilder = uriBuilder; + } + + private final Map<String, URL> endPoints = new HashMap<>(); + + @PostConstruct + void determinePorts() { + try { + MBeanServer mbs = getPlatformMBeanServer(); + for (ObjectName obj : mbs.queryNames(new ObjectName( + "*:type=Connector,*"), + match(attr("protocol"), value("HTTP/1.1")))) { + String scheme = mbs.getAttribute(obj, "scheme").toString(); + String port = obj.getKeyProperty("port"); + endPoints.put(scheme, new URL(scheme + "://localhost:" + port)); + } + getLog(getClass()).info( + "installed feed port publication mapping for " + + endPoints.keySet()); + } catch (Exception e) { + getLog(getClass()).error( + "failure in determining local port mapping", e); + } + } + + /** + * @param run + * The workflow run that defines which feed we are operating on. + * @return The URI of the feed + */ + public URI getFeedURI(TavernaRun run) { + return uriBuilder.getRunUriBuilder(run).path(FEED_URL_DIR).build(); + } + + @Nullable + public URL getLocalFeedBase(URI feedURI) { + if (feedURI == null) + return null; + return endPoints.get(feedURI.getScheme()); + } + + /** + * @param run + * The workflow run that defines which feed we are operating on. + * @param id + * The ID of the entry. + * @return The URI of the entry. + */ + public URI getEntryURI(TavernaRun run, String id) { + return uriBuilder.getRunUriBuilder(run) + .path(FEED_URL_DIR + "/{entryID}").build(id); + } + + private Entry getEntryFromFile(File f) throws FilesystemAccessException { + long size = f.getSize(); + if (size > MAX_ENTRY_SIZE) + throw new FilesystemAccessException("entry larger than 50kB"); + byte[] contents = f.getContents(0, (int) size); + Document<Entry> doc = parser.parse(new ByteArrayInputStream(contents)); + return doc.getRoot(); + } + + private void putEntryInFile(Directory dir, String name, Entry contents) + throws FilesystemAccessException, NoUpdateException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + writer.writeTo(contents, baos); + } catch (IOException e) { + throw new NoUpdateException("failed to serialize the ATOM entry", e); + } + File f = dir.makeEmptyFile(support.getPrincipal(), name); + f.appendContents(baos.toByteArray()); + } + + private List<DirectoryEntry> listPossibleEntries(TavernaRun run) + throws FilesystemAccessException, NoDirectoryEntryException { + List<DirectoryEntry> entries = new ArrayList<>(utils.getDirectory(run, + FEED_DIR).getContentsByDate()); + reverse(entries); + return entries; + } + + private String getRunURL(TavernaRun run) { + return new IRI(uriBuilder.getRunUriBuilder(run).build()).toString(); + } + + /** + * Get the interaction feed for a partciular run. + * + * @param run + * The workflow run that defines which feed we are operating on. + * @return The Abdera feed descriptor. + * @throws FilesystemAccessException + * If the feed directory can't be read for some reason. + * @throws NoDirectoryEntryException + * If the feed directory doesn't exist or an entry is + * unexpectedly removed. + */ + public Feed getRunFeed(TavernaRun run) throws FilesystemAccessException, + NoDirectoryEntryException { + URI feedURI = getFeedURI(run); + Feed feed = factory.newFeed(); + feed.setTitle("Interactions for Taverna Run \"" + run.getName() + "\""); + feed.addLink(new IRI(feedURI).toString(), "self"); + feed.addLink(getRunURL(run), "workflowrun"); + boolean fetchedDate = false; + for (DirectoryEntry de : listPossibleEntries(run)) { + if (!(de instanceof File)) + continue; + try { + Entry e = getEntryFromFile((File) de); + if (STRIP_CONTENTS) + e.setContentElement(null); + feed.addEntry(e); + if (fetchedDate) + continue; + Date last = e.getUpdated(); + if (last == null) + last = e.getPublished(); + if (last == null) + last = de.getModificationDate(); + feed.setUpdated(last); + fetchedDate = true; + } catch (FilesystemAccessException e) { + // Can't do anything about it, so we'll just drop the entry. + } + } + return feed; + } + + /** + * Gets the contents of a particular feed entry. + * + * @param run + * The workflow run that defines which feed we are operating on. + * @param entryID + * The identifier (from the path) of the entry to read. + * @return The description of the entry. + * @throws FilesystemAccessException + * If the entry can't be read or is too large. + * @throws NoDirectoryEntryException + * If the entry can't be found. + */ + public Entry getRunFeedEntry(TavernaRun run, String entryID) + throws FilesystemAccessException, NoDirectoryEntryException { + File entryFile = utils.getFile(run, FEED_DIR + "/" + entryID + EXT); + return getEntryFromFile(entryFile); + } + + /** + * Given a partial feed entry, store a complete feed entry in the filesystem + * for a particular run. Note that this does not permit update of an + * existing entry; the entry is always created new. + * + * @param run + * The workflow run that defines which feed we are operating on. + * @param entry + * The partial entry to store + * @return A link to the entry. + * @throws FilesystemAccessException + * If the entry can't be stored. + * @throws NoDirectoryEntryException + * If the run is improperly configured. + * @throws NoUpdateException + * If the user isn't allowed to do the write. + * @throws MalformedURLException + * If a generated URL is illegal (shouldn't happen). + */ + public Entry addRunFeedEntry(TavernaRun run, Entry entry) + throws FilesystemAccessException, NoDirectoryEntryException, + NoUpdateException { + support.permitUpdate(run); + Date now = new Date(); + entry.newId(); + String localId = "entry_" + counter.incrementAndGet(); + IRI selfLink = new IRI(getEntryURI(run, localId)); + entry.addLink(selfLink.toString(), "self"); + entry.addLink(getRunURL(run), "workflowrun"); + entry.setUpdated(now); + entry.setPublished(now); + putEntryInFile(utils.getDirectory(run, FEED_DIR), localId + EXT, entry); + return getEntryFromFile(utils.getFile(run, FEED_DIR + "/" + localId + + EXT)); + } + + /** + * Deletes an entry from a feed. + * + * @param run + * The workflow run that defines which feed we are operating on. + * @param entryID + * The ID of the entry to delete. + * @throws FilesystemAccessException + * If the entry can't be deleted + * @throws NoDirectoryEntryException + * If the entry can't be found. + * @throws NoUpdateException + * If the current user is not permitted to modify the run's + * characteristics. + */ + public void removeRunFeedEntry(TavernaRun run, String entryID) + throws FilesystemAccessException, NoDirectoryEntryException, + NoUpdateException { + support.permitUpdate(run); + utils.getFile(run, FEED_DIR + "/" + entryID + EXT).destroy(); + } +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/interaction/package-info.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/interaction/package-info.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/interaction/package-info.java new file mode 100644 index 0000000..9efc30d --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/interaction/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright (C) 2013 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +/** + * This package contains the Atom feed implementation for interactions for a particular workflow run. + * @author Donal Fellows + */ +package org.taverna.server.master.interaction; http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Directory.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Directory.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Directory.java new file mode 100644 index 0000000..9a0a84e --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Directory.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2010-2011 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.interfaces; + +import java.io.PipedInputStream; +import java.security.Principal; +import java.util.Collection; + +import org.taverna.server.master.exceptions.FilesystemAccessException; + +/** + * Represents a directory that is the working directory of a workflow run, or a + * sub-directory of it. + * + * @author Donal Fellows + * @see File + */ +public interface Directory extends DirectoryEntry { + /** + * @return A list of the contents of the directory. + * @throws FilesystemAccessException + * If things go wrong. + */ + Collection<DirectoryEntry> getContents() throws FilesystemAccessException; + + /** + * @return A list of the contents of the directory, in guaranteed date + * order. + * @throws FilesystemAccessException + * If things go wrong. + */ + Collection<DirectoryEntry> getContentsByDate() + throws FilesystemAccessException; + + /** + * @return The contents of the directory (and its sub-directories) as a zip. + * @throws FilesystemAccessException + * If things go wrong. + */ + ZipStream getContentsAsZip() throws FilesystemAccessException; + + /** + * Creates a sub-directory of this directory. + * + * @param actor + * Who this is being created by. + * @param name + * The name of the sub-directory. + * @return A handle to the newly-created directory. + * @throws FilesystemAccessException + * If the name is the same as some existing entry in the + * directory, or if something else goes wrong during creation. + */ + Directory makeSubdirectory(Principal actor, String name) + throws FilesystemAccessException; + + /** + * Creates an empty file in this directory. + * + * @param actor + * Who this is being created by. + * @param name + * The name of the file to create. + * @return A handle to the newly-created file. + * @throws FilesystemAccessException + * If the name is the same as some existing entry in the + * directory, or if something else goes wrong during creation. + */ + File makeEmptyFile(Principal actor, String name) + throws FilesystemAccessException; + + /** + * A simple pipe that produces the zipped contents of a directory. + * + * @author Donal Fellows + */ + public static class ZipStream extends PipedInputStream { + } +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/DirectoryEntry.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/DirectoryEntry.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/DirectoryEntry.java new file mode 100644 index 0000000..b098152 --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/DirectoryEntry.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2010-2011 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.interfaces; + +import java.util.Date; + +import org.taverna.server.master.exceptions.FilesystemAccessException; + +/** + * An entry in a {@link Directory} representing a file or sub-directory. + * + * @author Donal Fellows + * @see Directory + * @see File + */ +public interface DirectoryEntry extends Comparable<DirectoryEntry> { + /** + * @return The "local" name of the entry. This will never be "<tt>..</tt>" + * or contain the character "<tt>/</tt>". + */ + public String getName(); + + /** + * @return The "full" name of the entry. This is computed relative to the + * workflow run's working directory. It may contain the "<tt>/</tt>" + * character. + */ + public String getFullName(); + + /** + * @return The time that the entry was last modified. + */ + public Date getModificationDate(); + + /** + * Destroy this directory entry, deleting the file or sub-directory. The + * workflow run's working directory can never be manually destroyed. + * + * @throws FilesystemAccessException + * If the destroy fails for some reason. + */ + public void destroy() throws FilesystemAccessException; + // TODO: Permissions (or decide not to do anything about them) +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/File.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/File.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/File.java new file mode 100644 index 0000000..e4e6590 --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/File.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2010-2011 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.interfaces; + +import org.taverna.server.master.exceptions.FilesystemAccessException; + +/** + * Represents a file in the working directory of a workflow instance run, or in + * some sub-directory of it. + * + * @author Donal Fellows + * @see Directory + */ +public interface File extends DirectoryEntry { + /** + * @param offset + * Where in the file to start reading. + * @param length + * The length of file to read, or -1 to read to the end of the + * file. + * @return The literal byte contents of the section of the file, or null if + * the section doesn't exist. + * @throws FilesystemAccessException + * If the read of the file goes wrong. + */ + public byte[] getContents(int offset, int length) + throws FilesystemAccessException; + + /** + * Write the data to the file, totally replacing what was there before. + * + * @param data + * The literal bytes that will form the new contents of the file. + * @throws FilesystemAccessException + * If the write to the file goes wrong. + */ + public void setContents(byte[] data) throws FilesystemAccessException; + + /** + * Append the data to the file. + * + * @param data + * The literal bytes that will be added on to the end of the + * file. + * @throws FilesystemAccessException + * If the write to the file goes wrong. + */ + public void appendContents(byte[] data) throws FilesystemAccessException; + + /** + * @return The length of the file, in bytes. + * @throws FilesystemAccessException + * If the read of the file size goes wrong. + */ + public long getSize() throws FilesystemAccessException; + + /** + * Asks for the argument file to be copied to this one. + * + * @param from + * The source file. + * @throws FilesystemAccessException + * If anything goes wrong. + */ + public void copy(File from) throws FilesystemAccessException; +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Input.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Input.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Input.java new file mode 100644 index 0000000..31cb7cb --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Input.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2010 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.interfaces; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.taverna.server.master.common.Status; +import org.taverna.server.master.exceptions.BadStateChangeException; +import org.taverna.server.master.exceptions.FilesystemAccessException; + +/** + * This represents the assignment of inputs to input ports of the workflow. Note + * that the <tt>file</tt> and <tt>value</tt> properties are never set at the + * same time. + * + * @author Donal Fellows + */ +public interface Input { + /** + * @return The file currently assigned to this input port, or <tt>null</tt> + * if no file is assigned. + */ + @Nullable + public String getFile(); + + /** + * @return The name of this input port. This may not be changed. + */ + @Nonnull + public String getName(); + + /** + * @return The value currently assigned to this input port, or <tt>null</tt> + * if no value is assigned. + */ + @Nullable + public String getValue(); + + /** + * @return The delimiter for the input port, or <tt>null</tt> if the value + * is not to be split. + */ + @Nullable + public String getDelimiter(); + + /** + * Sets the file to use for this input. This overrides the use of the + * previous file and any set value. + * + * @param file + * The filename to use. Must not start with a <tt>/</tt> or + * contain any <tt>..</tt> segments. Will be interpreted relative + * to the run's working directory. + * @throws FilesystemAccessException + * If the filename is invalid. + * @throws BadStateChangeException + * If the run isn't in the {@link Status#Initialized + * Initialized} state. + */ + public void setFile(String file) throws FilesystemAccessException, + BadStateChangeException; + + /** + * Sets the value to use for this input. This overrides the use of the + * previous value and any set file. + * + * @param value + * The value to use. + * @throws BadStateChangeException + * If the run isn't in the {@link Status#Initialized + * Initialized} state. + */ + public void setValue(String value) throws BadStateChangeException; + + /** + * Sets (or clears) the delimiter for the input port. + * + * @param delimiter + * The delimiter character, or <tt>null</tt> if the value is not + * to be split. + * @throws BadStateChangeException + * If the run isn't in the {@link Status#Initialized + * Initialized} state. + */ + @Nullable + public void setDelimiter(String delimiter) throws BadStateChangeException; + +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Listener.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Listener.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Listener.java new file mode 100644 index 0000000..d7998bc --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Listener.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2010 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.interfaces; + +import org.taverna.server.master.exceptions.BadPropertyValueException; +import org.taverna.server.master.exceptions.NoListenerException; + +/** + * An event listener that can be attached to a {@link TavernaRun}. + * + * @author Donal Fellows + */ +public interface Listener { + /** + * @return The name of the listener. + */ + public String getName(); + + /** + * @return The type of the listener. + */ + public String getType(); + + /** + * @return The configuration document for the listener. + */ + public String getConfiguration(); + + /** + * @return The supported properties of the listener. + */ + public String[] listProperties(); + + /** + * Get the value of a particular property, which should be listed in the + * {@link #listProperties()} method. + * + * @param propName + * The name of the property to read. + * @return The value of the property. + * @throws NoListenerException + * If no property with that name exists. + */ + public String getProperty(String propName) throws NoListenerException; + + /** + * Set the value of a particular property, which should be listed in the + * {@link #listProperties()} method. + * + * @param propName + * The name of the property to write. + * @param value + * The value to set the property to. + * @throws NoListenerException + * If no property with that name exists. + * @throws BadPropertyValueException + * If the value of the property is bad (e.g., wrong syntax). + */ + public void setProperty(String propName, String value) + throws NoListenerException, BadPropertyValueException; +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/LocalIdentityMapper.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/LocalIdentityMapper.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/LocalIdentityMapper.java new file mode 100644 index 0000000..37b104e --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/LocalIdentityMapper.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2010-2011 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.interfaces; + +import org.taverna.server.master.utils.UsernamePrincipal; + +/** + * This interface describes how to map from the identity understood by the + * webapp to the identity understood by the local execution system. + * + * @author Donal Fellows + */ +public interface LocalIdentityMapper { + /** + * Given a user's identity, get the local identity to use for executing + * their workflows. Note that it is assumed that there will never be a + * failure from this interface; it is <i>not</i> a security policy + * decision or enforcement point. + * + * @param user + * An identity token. + * @return A user name, which must be defined in the context that workflows + * will be running in. + */ + public String getUsernameForPrincipal(UsernamePrincipal user); +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/MessageDispatcher.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/MessageDispatcher.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/MessageDispatcher.java new file mode 100644 index 0000000..37dbf2c --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/MessageDispatcher.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2010-2011 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.interfaces; + +import javax.annotation.Nonnull; + +/** + * The interface supported by all notification message dispatchers. + * @author Donal Fellows + */ +public interface MessageDispatcher { + /** + * @return Whether this message dispatcher is actually available (fully + * configured, etc.) + */ + boolean isAvailable(); + + /** + * @return The name of this dispatcher, which must match the protocol + * supported by it (for a non-universal dispatcher) and the name of + * the message generator used to produce the message. + */ + String getName(); + + /** + * Dispatch a message to a recipient. + * + * @param originator + * The workflow run that produced the message. + * @param messageSubject + * The subject of the message to send. + * @param messageContent + * The plain-text content of the message to send. + * @param targetParameter + * A description of where it is to go. + * @throws Exception + * If anything goes wrong. + */ + void dispatch(@Nonnull TavernaRun originator, + @Nonnull String messageSubject, @Nonnull String messageContent, + @Nonnull String targetParameter) throws Exception; +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Policy.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Policy.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Policy.java new file mode 100644 index 0000000..f57fe71 --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/Policy.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2010 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.interfaces; + +import java.net.URI; +import java.util.List; + +import org.taverna.server.master.common.Status; +import org.taverna.server.master.common.Workflow; +import org.taverna.server.master.exceptions.NoCreateException; +import org.taverna.server.master.exceptions.NoDestroyException; +import org.taverna.server.master.exceptions.NoUpdateException; +import org.taverna.server.master.utils.UsernamePrincipal; + +/** + * Simple policy interface. + * + * @author Donal Fellows + */ +public interface Policy { + /** + * @return The maximum number of runs that the system can support. + */ + int getMaxRuns(); + + /** + * Get the limit on the number of runs for this user. + * + * @param user + * Who to get the limit for + * @return The maximum number of runs for this user, or <tt>null</tt> if no + * per-user limit is imposed and only system-wide limits are to be + * enforced. + */ + Integer getMaxRuns(UsernamePrincipal user); + + /** + * Test whether the user can create an instance of the given workflow. + * + * @param user + * Who wants to do the creation. + * @param workflow + * The workflow they wish to instantiate. + * @throws NoCreateException + * If they may not instantiate it. + */ + void permitCreate(UsernamePrincipal user, Workflow workflow) + throws NoCreateException; + + /** + * Test whether the user can destroy a workflow instance run or manipulate + * its expiry date. + * + * @param user + * Who wants to do the deletion. + * @param run + * What they want to delete. + * @throws NoDestroyException + * If they may not destroy it. + */ + void permitDestroy(UsernamePrincipal user, TavernaRun run) + throws NoDestroyException; + + /** + * Return whether the user has access to a particular workflow run. + * <b>Note</b> that this does not throw any exceptions! + * + * @param user + * Who wants to read the workflow's state. + * @param run + * What do they want to read from. + * @return Whether they can read it. Note that this check is always applied + * before testing whether the workflow can be updated or deleted by + * the user. + */ + boolean permitAccess(UsernamePrincipal user, TavernaRun run); + + /** + * Test whether the user can modify a workflow run (other than for its + * expiry date). + * + * @param user + * Who wants to do the modification. + * @param run + * What they want to modify. + * @throws NoUpdateException + * If they may not modify it. + */ + void permitUpdate(UsernamePrincipal user, TavernaRun run) + throws NoUpdateException; + + /** + * Get the URIs of the workflows that the given user may execute. + * + * @param user + * Who are we finding out on behalf of. + * @return A list of workflow URIs that they may instantiate, or + * <tt>null</tt> if any workflow may be submitted. + */ + List<URI> listPermittedWorkflowURIs(UsernamePrincipal user); + + /** + * @return The maximum number of {@linkplain Status#Operating operating} + * runs that the system can support. + */ + int getOperatingLimit(); + + /** + * Set the URIs of the workflows that the given user may execute. + * + * @param user + * Who are we finding out on behalf of. + * @param permitted + * A list of workflow URIs that they may instantiate. + */ + void setPermittedWorkflowURIs(UsernamePrincipal user, List<URI> permitted); +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/RunStore.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/RunStore.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/RunStore.java new file mode 100644 index 0000000..b5e84c5 --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/RunStore.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2010 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.interfaces; + +import java.util.Map; + +import org.taverna.server.master.exceptions.UnknownRunException; +import org.taverna.server.master.utils.UsernamePrincipal; + +/** + * Interface to the mechanism that looks after the mapping of names to runs. + * Instances of this class may also be responsible for enforcing timely cleanup + * of expired workflows. + * + * @author Donal Fellows. + */ +public interface RunStore { + /** + * Obtain the workflow run for a given user and name. + * + * @param user + * Who wants to do the lookup. + * @param p + * The general policy system context. + * @param uuid + * The handle for the run. + * @return The workflow instance run. + * @throws UnknownRunException + * If the lookup fails (either because it does not exist or + * because it is not permitted for the user by the policy). + */ + TavernaRun getRun(UsernamePrincipal user, Policy p, String uuid) + throws UnknownRunException; + + /** + * Obtain the named workflow run. + * + * @param uuid + * The handle for the run. + * @return The workflow instance run. + * @throws UnknownRunException + * If the lookup fails (either because it does not exist or + * because it is not permitted for the user by the policy). + */ + public TavernaRun getRun(String uuid) throws UnknownRunException; + + /** + * List the runs that a particular user may access. + * + * @param user + * Who wants to do the lookup, or <code>null</code> if it is + * being done "by the system" when the full mapping should be + * returned. + * @param p + * The general policy system context. + * @return A mapping from run names to run instances. + */ + Map<String, TavernaRun> listRuns(UsernamePrincipal user, Policy p); + + /** + * Adds a workflow instance run to the store. Note that this operation is + * <i>not</i> expected to be security-checked; that is the callers' + * responsibility. + * + * @param run + * The run itself. + * @return The name of the run. + */ + String registerRun(TavernaRun run); + + /** + * Removes a run from the store. Note that this operation is <i>not</i> + * expected to be security-checked; that is the callers' responsibility. + * + * @param uuid + * The name of the run. + */ + void unregisterRun(String uuid); +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-server/blob/2c71f9a9/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/SecurityContextFactory.java ---------------------------------------------------------------------- diff --git a/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/SecurityContextFactory.java b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/SecurityContextFactory.java new file mode 100644 index 0000000..902c4d0 --- /dev/null +++ b/taverna-server-webapp/src/main/java/org/taverna/server/master/interfaces/SecurityContextFactory.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2011 The University of Manchester + * + * See the file "LICENSE" for license terms. + */ +package org.taverna.server.master.interfaces; + +import java.io.Serializable; + +import org.taverna.server.master.utils.UsernamePrincipal; + +/** + * How to create instances of a security context. + * + * @author Donal Fellows + */ +public interface SecurityContextFactory extends Serializable { + /** + * Creates a security context. + * + * @param run + * Handle to remote run. Allows the security context to know how + * to apply itself to the workflow run. + * @param owner + * The identity of the owner of the workflow run. + * @return The security context. + * @throws Exception + * If anything goes wrong. + */ + TavernaSecurityContext create(TavernaRun run, UsernamePrincipal owner) + throws Exception; +}
