blob: 9219a6018b8054e57f30b2924b9367badc0547ba [file] [log] [blame]
/*
* 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;
}
}
}