org.taverna.server.master.localworker.SecurityContextDelegate.java Source code

Java tutorial

Introduction

Here is the source code for org.taverna.server.master.localworker.SecurityContextDelegate.java

Source

/*
 * Copyright (C) 2010-2012 The University of Manchester
 * 
 * See the file "LICENSE.txt" for license terms.
 */
package org.taverna.server.master.localworker;

import static java.lang.String.format;
import static java.util.Arrays.fill;
import static java.util.UUID.randomUUID;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.rmi.RemoteException;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Set;

import javax.security.auth.x500.X500Principal;
import javax.ws.rs.core.HttpHeaders;
import javax.xml.ws.handler.MessageContext;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.taverna.server.localworker.remote.ImplementationException;
import org.taverna.server.localworker.remote.RemoteSecurityContext;
import org.taverna.server.master.common.Credential;
import org.taverna.server.master.common.Trust;
import org.taverna.server.master.exceptions.FilesystemAccessException;
import org.taverna.server.master.exceptions.InvalidCredentialException;
import org.taverna.server.master.exceptions.NoDirectoryEntryException;
import org.taverna.server.master.interfaces.File;
import org.taverna.server.master.interfaces.TavernaSecurityContext;
import org.taverna.server.master.utils.UsernamePrincipal;

/**
 * Implementation of a security context.
 * 
 * @author Donal Fellows
 */
public abstract class SecurityContextDelegate implements TavernaSecurityContext {
    /**
     * What fields of a certificate we look at when understanding who it is
     * talking about, in the order that we look.
     */
    private static final String[] DEFAULT_CERT_FIELD_NAMES = { "CN", "COMMONNAME", "COMMON NAME", "COMMON_NAME",
            "OU", "ORGANIZATIONALUNITNAME", "ORGANIZATIONAL UNIT NAME", "O", "ORGANIZATIONNAME",
            "ORGANIZATION NAME" };
    /** The type of certificates that are processed if we don't say otherwise. */
    private static final String DEFAULT_CERTIFICATE_TYPE = "X.509";
    /** Max size of credential file, in kiB. */
    private static final int FILE_SIZE_LIMIT = 20;

    Log log = LogFactory.getLog("Taverna.Server.LocalWorker");
    private final UsernamePrincipal owner;
    private final List<Credential> credentials = new ArrayList<Credential>();
    private final List<Trust> trusted = new ArrayList<Trust>();
    private final RemoteRunDelegate run;
    private final Object lock = new Object();
    final SecurityContextFactory factory;

    private transient Keystore keystore;
    private transient HashMap<URI, String> uriToAliasMap;

    /**
     * Initialise the context delegate.
     * 
     * @param run
     *            What workflow run is this for?
     * @param owner
     *            Who owns the workflow run?
     * @param factory
     *            What class built this object?
     */
    protected SecurityContextDelegate(RemoteRunDelegate run, UsernamePrincipal owner,
            SecurityContextFactory factory) {
        this.run = run;
        this.owner = owner;
        this.factory = factory;
    }

    @Override
    public SecurityContextFactory getFactory() {
        return factory;
    }

    @Override
    public UsernamePrincipal getOwner() {
        return owner;
    }

    @Override
    public Credential[] getCredentials() {
        synchronized (lock) {
            return credentials.toArray(new Credential[credentials.size()]);
        }
    }

    /**
     * Get the human-readable name of a principal.
     * 
     * @param principal
     *            The principal being decoded.
     * @return A name.
     */
    protected final String getPrincipalName(X500Principal principal) {
        return factory.x500Utils.getName(principal, DEFAULT_CERT_FIELD_NAMES);
    }

    /**
     * Cause the current state to be flushed to the database.
     */
    protected final void flushToDB() {
        factory.db.flushToDisk(run);
    }

    @Override
    public void addCredential(Credential toAdd) {
        synchronized (lock) {
            int idx = credentials.indexOf(toAdd);
            if (idx != -1)
                credentials.set(idx, toAdd);
            else
                credentials.add(toAdd);
            flushToDB();
        }
    }

    @Override
    public void deleteCredential(Credential toDelete) {
        synchronized (lock) {
            credentials.remove(toDelete);
            flushToDB();
        }
    }

    @Override
    public Trust[] getTrusted() {
        synchronized (lock) {
            return trusted.toArray(new Trust[trusted.size()]);
        }
    }

    @Override
    public void addTrusted(Trust toAdd) {
        synchronized (lock) {
            int idx = trusted.indexOf(toAdd);
            if (idx != -1)
                trusted.set(idx, toAdd);
            else
                trusted.add(toAdd);
            flushToDB();
        }
    }

    @Override
    public void deleteTrusted(Trust toDelete) {
        synchronized (lock) {
            trusted.remove(toDelete);
            flushToDB();
        }
    }

    @Override
    public abstract void validateCredential(Credential c) throws InvalidCredentialException;

    @Override
    public void validateTrusted(Trust t) throws InvalidCredentialException {
        InputStream contentsAsStream;
        if (t.certificateBytes != null && t.certificateBytes.length > 0) {
            contentsAsStream = new ByteArrayInputStream(t.certificateBytes);
            t.certificateFile = null;
        } else if (t.certificateFile == null || t.certificateFile.trim().isEmpty())
            throw new InvalidCredentialException("absent or empty certificateFile");
        else {
            contentsAsStream = contents(t.certificateFile);
            t.certificateBytes = null;
        }
        t.serverName = null;
        if (t.fileType == null || t.fileType.trim().isEmpty())
            t.fileType = DEFAULT_CERTIFICATE_TYPE;
        t.fileType = t.fileType.trim();
        try {
            t.loadedCertificates = CertificateFactory.getInstance(t.fileType)
                    .generateCertificates(contentsAsStream);
            t.serverName = new ArrayList<String>(t.loadedCertificates.size());
            for (Certificate c : t.loadedCertificates)
                t.serverName.add(getPrincipalName(((X509Certificate) c).getSubjectX500Principal()));
        } catch (CertificateException e) {
            throw new InvalidCredentialException(e);
        } catch (ClassCastException e) {
            // Do nothing; truncates the list of server names
        }
    }

    @Override
    public void initializeSecurityFromContext(SecurityContext securityContext) throws Exception {
        // This is how to get the info from Spring Security
        Authentication auth = securityContext.getAuthentication();
        if (auth == null)
            return;
        auth.getPrincipal();
        // do nothing else in this implementation
    }

    @Override
    public void initializeSecurityFromSOAPContext(MessageContext context) {
        // do nothing in this implementation
    }

    @Override
    public void initializeSecurityFromRESTContext(HttpHeaders context) {
        // do nothing in this implementation
    }

    /**
     * Builds and transfers a keystore with suitable credentials to the back-end
     * workflow execution engine.
     * 
     * @throws GeneralSecurityException
     *             If the manipulation of the keystore, keys or certificates
     *             fails.
     * @throws IOException
     *             If there are problems building the data (should not happen).
     * @throws RemoteException
     *             If the conveyancing fails.
     */
    @Override
    public final void conveySecurity() throws GeneralSecurityException, IOException, ImplementationException {
        RemoteSecurityContext rc = run.run.getSecurityContext();

        synchronized (lock) {
            if (credentials.isEmpty() && trusted.isEmpty())
                return;
        }
        char[] password = null;
        try {
            password = generateNewPassword();

            log.info("constructing merged keystore");
            Truststore truststore = new Truststore(password);
            Keystore keystore = new Keystore(password);
            HashMap<URI, String> uriToAliasMap = new HashMap<URI, String>();
            int trustedCount = 0, keyCount = 0;

            synchronized (lock) {
                try {
                    for (Trust t : trusted)
                        for (Certificate cert : t.loadedCertificates) {
                            truststore.addCertificate(cert);
                            trustedCount++;
                        }

                    this.uriToAliasMap = uriToAliasMap;
                    this.keystore = keystore;
                    for (Credential c : credentials) {
                        addCredentialToKeystore(c);
                        keyCount++;
                    }
                } finally {
                    this.uriToAliasMap = null;
                    this.keystore = null;
                    credentials.clear();
                    trusted.clear();
                    flushToDB();
                }
            }

            byte[] trustbytes = null, keybytes = null;
            try {
                trustbytes = truststore.serialize();
                keybytes = keystore.serialize();

                // Now we've built the security information, ship it off...

                log.info("transfering merged truststore with " + trustedCount + " entries");
                rc.setTruststore(trustbytes);

                log.info("transfering merged keystore with " + keyCount + " entries");
                rc.setKeystore(keybytes);
            } finally {
                if (trustbytes != null)
                    fill(trustbytes, (byte) 0);
                if (keybytes != null)
                    fill(keybytes, (byte) 0);
            }
            rc.setPassword(password);

            log.info("transferring serviceURL->alias map with " + uriToAliasMap.size() + " entries");
            rc.setUriToAliasMap(uriToAliasMap);
        } finally {
            if (password != null)
                fill(password, ' ');
        }

        synchronized (lock) {
            conveyExtraSecuritySettings(rc);
        }
    }

    /**
     * Hook that allows additional information to be conveyed to the remote run.
     * 
     * @param remoteSecurityContext
     *            The remote resource that information would be passed to.
     * @throws IOException
     *             If anything goes wrong with the communication.
     */
    protected void conveyExtraSecuritySettings(RemoteSecurityContext remoteSecurityContext) throws IOException {
        // Does nothing by default; overrideable
    }

    /**
     * @return A new password with a reasonable level of randomness.
     */
    protected final char[] generateNewPassword() {
        return randomUUID().toString().toCharArray();
    }

    /**
     * Adds a credential to the current keystore.
     * 
     * @param alias
     *            The alias to create within the keystore.
     * @param c
     *            The key-pair.
     * @throws KeyStoreException
     */
    protected final void addKeypairToKeystore(String alias, Credential c) throws KeyStoreException {
        if (uriToAliasMap.containsKey(c.serviceURI))
            log.warn("duplicate URI in alias mapping: " + c.serviceURI);
        keystore.addKey(alias, c.loadedKey, c.loadedTrustChain);
        uriToAliasMap.put(c.serviceURI, alias);
    }

    /**
     * Adds a credential to the current keystore.
     * 
     * @param c
     *            The credential to add.
     * @throws KeyStoreException
     */
    public abstract void addCredentialToKeystore(Credential c) throws KeyStoreException;

    /**
     * Read a file up to {@value #FILE_SIZE_LIMIT}kB in size.
     * 
     * @param name
     *            The path name of the file, relative to the context run's
     *            working directory.
     * @return A stream of the file's contents.
     * @throws InvalidCredentialException
     *             If anything goes wrong.
     */
    final InputStream contents(String name) throws InvalidCredentialException {
        try {
            File f = (File) factory.fileUtils.getDirEntry(run, name);
            long size = f.getSize();
            if (size > FILE_SIZE_LIMIT * 1024)
                throw new InvalidCredentialException(FILE_SIZE_LIMIT + "kB limit hit");
            return new ByteArrayInputStream(f.getContents(0, (int) size));
        } catch (NoDirectoryEntryException e) {
            throw new InvalidCredentialException(e);
        } catch (FilesystemAccessException e) {
            throw new InvalidCredentialException(e);
        } catch (ClassCastException e) {
            throw new InvalidCredentialException("not a file", e);
        }
    }

    @Override
    public Set<String> getPermittedDestroyers() {
        return run.getDestroyers();
    }

    @Override
    public void setPermittedDestroyers(Set<String> destroyers) {
        run.setDestroyers(destroyers);
    }

    @Override
    public Set<String> getPermittedUpdaters() {
        return run.getWriters();
    }

    @Override
    public void setPermittedUpdaters(Set<String> updaters) {
        run.setWriters(updaters);
    }

    @Override
    public Set<String> getPermittedReaders() {
        return run.getReaders();
    }

    @Override
    public void setPermittedReaders(Set<String> readers) {
        run.setReaders(readers);
    }

    /**
     * Reinstall the credentials and the trust extracted from serialization to
     * the database.
     * 
     * @param credentials
     *            The credentials to reinstall.
     * @param trust
     *            The trusted certificates to reinstall.
     */
    void setCredentialsAndTrust(Credential[] credentials, Trust[] trust) {
        synchronized (lock) {
            this.credentials.clear();
            if (credentials != null)
                for (Credential c : credentials)
                    try {
                        validateCredential(c);
                        this.credentials.add(c);
                    } catch (InvalidCredentialException e) {
                        log.warn("failed to revalidate credential: " + c, e);
                    }
            this.trusted.clear();
            if (trust != null)
                for (Trust t : trust)
                    try {
                        validateTrusted(t);
                        this.trusted.add(t);
                    } catch (InvalidCredentialException e) {
                        log.warn("failed to revalidate trust assertion: " + t, e);
                    }
        }
    }

    /**
     * A trust store that can only be added to or serialized. Only trusted
     * certificates can be placed in it.
     * 
     * @author Donal Fellows
     */
    class Truststore {
        private KeyStore ks;
        private char[] password;

        Truststore(char[] password) throws GeneralSecurityException {
            this.password = password.clone();
            ks = KeyStore.getInstance("UBER", "BC");
            try {
                ks.load(null, this.password);
            } catch (IOException e) {
                throw new GeneralSecurityException("problem initializing blank truststore", e);
            }
        }

        /**
         * Add a trusted certificate to the truststore. No certificates can be
         * added after the truststore is serialized.
         * 
         * @param cert
         *            The certificate (typically belonging to a root CA) to add.
         * @throws KeyStoreException
         *             If anything goes wrong.
         */
        public void addCertificate(Certificate cert) throws KeyStoreException {
            if (ks == null)
                throw new IllegalStateException("truststore already written");
            X509Certificate c = (X509Certificate) cert;
            String alias = format("trustedcert#%s#%s#%s", getPrincipalName(c.getSubjectX500Principal()),
                    getPrincipalName(c.getIssuerX500Principal()), factory.x500Utils.getSerial(c));
            ks.setCertificateEntry(alias, c);
            if (factory.logSecurityDetails)
                log.debug("added cert with alias \"" + alias + "\" of type " + c.getClass().getCanonicalName());
        }

        /**
         * Get the byte serialization of this truststore. This can only be
         * fetched exactly once.
         * 
         * @return The serialization.
         * @throws GeneralSecurityException
         *             If anything goes wrong.
         */
        public byte[] serialize() throws GeneralSecurityException, IOException {
            if (ks == null)
                throw new IllegalStateException("truststore already written");
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            try {
                ks.store(stream, password);
                stream.close();
                if (factory.logSecurityDetails)
                    log.debug("serialized UBER/BC truststore (size: " + ks.size() + ") with password \""
                            + new String(password) + "\"");
            } catch (IOException e) {
                throw new GeneralSecurityException("problem serializing truststore", e);
            }
            fill(password, ' ');
            ks = null;
            return stream.toByteArray();
        }

        @Override
        protected void finalize() {
            fill(password, ' ');
            ks = null;
        }
    }

    /**
     * A key store that can only be added to or serialized. Only keys can be
     * placed in it.
     * 
     * @author Donal Fellows
     */
    class Keystore {
        private KeyStore ks;
        private char[] password;

        Keystore(char[] password) throws GeneralSecurityException {
            this.password = password.clone();

            ks = KeyStore.getInstance("UBER", "BC");
            try {
                ks.load(null, password);
            } catch (IOException e) {
                throw new GeneralSecurityException("problem initializing blank keystore", e);
            }
        }

        /**
         * Add a key to the keystore. No keys can be added after the keystore is
         * serialized.
         * 
         * @param alias
         *            The alias of the key.
         * @param key
         *            The secret/private key to add.
         * @param trustChain
         *            The trusted certificate chain of the key. Should be
         *            <tt>null</tt> for secret keys.
         * @throws KeyStoreException
         *             If anything goes wrong.
         */
        public void addKey(String alias, Key key, Certificate[] trustChain) throws KeyStoreException {
            if (ks == null)
                throw new IllegalStateException("keystore already written");
            ks.setKeyEntry(alias, key, password, trustChain);
            if (factory.logSecurityDetails)
                log.debug("added key with alias \"" + alias + "\" of type " + key.getClass().getCanonicalName());
        }

        /**
         * Get the byte serialization of this keystore. This can only be fetched
         * exactly once.
         * 
         * @return The serialization.
         * @throws GeneralSecurityException
         *             If anything goes wrong.
         */
        public byte[] serialize() throws GeneralSecurityException {
            if (ks == null)
                throw new IllegalStateException("keystore already written");
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            try {
                ks.store(stream, password);
                stream.close();
                if (factory.logSecurityDetails)
                    log.debug("serialized UBER/BC keystore (size: " + ks.size() + ") with password \""
                            + new String(password) + "\"");
            } catch (IOException e) {
                throw new GeneralSecurityException("problem serializing keystore", e);
            }
            fill(password, ' ');
            ks = null;
            return stream.toByteArray();
        }

        @Override
        protected void finalize() {
            fill(password, ' ');
            ks = null;
        }
    }
}