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

Java tutorial

Introduction

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

Source

/*
 */
package org.taverna.server.master.worker;
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import static java.lang.String.format;
import static java.util.Arrays.fill;
import static java.util.UUID.randomUUID;
import static org.taverna.server.master.defaults.Default.CERTIFICATE_FIELD_NAMES;
import static org.taverna.server.master.defaults.Default.CERTIFICATE_TYPE;
import static org.taverna.server.master.defaults.Default.CREDENTIAL_FILE_SIZE_LIMIT;
import static org.taverna.server.master.identity.WorkflowInternalAuthProvider.PREFIX;

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.Map;
import java.util.Set;

import javax.annotation.Nullable;
import javax.security.auth.x500.X500Principal;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.UriBuilder;
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 {
    Log log = LogFactory.getLog("Taverna.Server.Worker");
    private final UsernamePrincipal owner;
    private final List<Credential> credentials = new ArrayList<>();
    private final List<Trust> trusted = new ArrayList<>();
    private final RemoteRunDelegate run;
    private final Object lock = new Object();
    final SecurityContextFactory factory;

    private transient Keystore keystore;
    private transient Map<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, CERTIFICATE_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 = CERTIFICATE_TYPE;
        t.fileType = t.fileType.trim();
        try {
            t.loadedCertificates = CertificateFactory.getInstance(t.fileType)
                    .generateCertificates(contentsAsStream);
            t.serverName = new ArrayList<>(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
    }

    private UriBuilder getUB() {
        return factory.uriSource.getRunUriBuilder(run);
    }

    private RunDatabaseDAO getDAO() {
        return ((RunDatabase) factory.db).dao;
    }

    @Nullable
    private List<X509Certificate> getCerts(URI uri) throws IOException, GeneralSecurityException {
        return factory.certFetcher.getTrustsForURI(uri);
    }

    private void installLocalPasswordCredential(List<Credential> credentials, List<Trust> trusts)
            throws InvalidCredentialException, IOException, GeneralSecurityException {
        Credential.Password pw = new Credential.Password();
        pw.id = "run:self";
        pw.username = PREFIX + run.id;
        pw.password = getDAO().getSecurityToken(run.id);
        UriBuilder ub = getUB().segment("").fragment(factory.httpRealm);
        pw.serviceURI = ub.build();
        validateCredential(pw);
        log.info("issuing self-referential credential for " + pw.serviceURI);
        credentials.add(pw);
        List<X509Certificate> myCerts = getCerts(pw.serviceURI);
        if (myCerts != null && myCerts.size() > 0) {
            Trust t = new Trust();
            t.loadedCertificates = getCerts(pw.serviceURI);
            trusts.add(t);
        }
    }

    /**
     * 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();

        List<Trust> trusted = new ArrayList<>(this.trusted);
        this.trusted.clear();
        List<Credential> credentials = new ArrayList<>(this.credentials);
        this.credentials.clear();

        try {
            installLocalPasswordCredential(credentials, trusted);
        } catch (Exception e) {
            log.warn("failed to construct local credential: " + "interaction service will fail", e);
        }

        char[] password = null;
        try {
            password = generateNewPassword();

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

            synchronized (lock) {
                try {
                    for (Trust t : trusted) {
                        if (t == null || t.loadedCertificates == null)
                            continue;
                        for (Certificate cert : t.loadedCertificates)
                            if (cert != null) {
                                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 (c.loadedKey == null)
            throw new KeyStoreException("critical: credential was not verified");
        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 > CREDENTIAL_FILE_SIZE_LIMIT * 1024)
                throw new InvalidCredentialException(CREDENTIAL_FILE_SIZE_LIMIT + "kB limit hit");
            return new ByteArrayInputStream(f.getContents(0, (int) size));
        } catch (NoDirectoryEntryException | 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);
                    }
        }
    }

    static class SecurityStore {
        private KeyStore ks;
        private char[] password;

        SecurityStore(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);
            }
        }

        final synchronized void setCertificate(String alias, Certificate c) throws KeyStoreException {
            if (ks == null)
                throw new IllegalStateException("store already written");
            ks.setCertificateEntry(alias, c);
        }

        final synchronized void setKey(String alias, Key key, Certificate[] trustChain) throws KeyStoreException {
            if (ks == null)
                throw new IllegalStateException("store already written");
            ks.setKeyEntry(alias, key, password, trustChain);
        }

        final synchronized byte[] serialize(boolean logIt) throws GeneralSecurityException {
            if (ks == null)
                throw new IllegalStateException("store already written");
            try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) {
                ks.store(stream, password);
                if (logIt)
                    LogFactory.getLog("Taverna.Server.Worker").debug("serialized UBER/BC truststore (size: "
                            + ks.size() + ") with password \"" + new String(password) + "\"");
                return stream.toByteArray();
            } catch (IOException e) {
                throw new GeneralSecurityException("problem serializing keystore", e);
            } finally {
                ks = null;
                fill(password, ' ');
            }
        }

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

    /**
     * A trust store that can only be added to or serialized. Only trusted
     * certificates can be placed in it.
     * 
     * @author Donal Fellows
     */
    class Truststore extends SecurityStore {
        Truststore(char[] password) throws GeneralSecurityException {
            super(password);
        }

        /**
         * 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 {
            X509Certificate c = (X509Certificate) cert;
            String alias = format("trustedcert#%s#%s#%s", getPrincipalName(c.getSubjectX500Principal()),
                    getPrincipalName(c.getIssuerX500Principal()), factory.x500Utils.getSerial(c));
            setCertificate(alias, c);
            if (log.isDebugEnabled() && 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 {
            return serialize(log.isDebugEnabled() && factory.logSecurityDetails);
        }
    }

    /**
     * A key store that can only be added to or serialized. Only keys can be
     * placed in it.
     * 
     * @author Donal Fellows
     */
    class Keystore extends SecurityStore {
        Keystore(char[] password) throws GeneralSecurityException {
            super(password);
        }

        /**
         * 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 {
            setKey(alias, key, trustChain);
            if (log.isDebugEnabled() && 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 {
            return serialize(log.isDebugEnabled() && factory.logSecurityDetails);
        }
    }
}