org.apache.accumulo.test.functional.KerberosProxyIT.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.accumulo.test.functional.KerberosProxyIT.java

Source

/*
 * 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.
 */
package org.apache.accumulo.test.functional;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.apache.accumulo.cluster.ClusterUser;
import org.apache.accumulo.core.client.security.tokens.KerberosToken;
import org.apache.accumulo.core.client.security.tokens.PasswordToken;
import org.apache.accumulo.core.conf.Property;
import org.apache.accumulo.core.rpc.UGIAssumingTransport;
import org.apache.accumulo.harness.AccumuloIT;
import org.apache.accumulo.harness.MiniClusterConfigurationCallback;
import org.apache.accumulo.harness.MiniClusterHarness;
import org.apache.accumulo.harness.TestingKdc;
import org.apache.accumulo.minicluster.impl.MiniAccumuloClusterImpl;
import org.apache.accumulo.minicluster.impl.MiniAccumuloConfigImpl;
import org.apache.accumulo.proxy.Proxy;
import org.apache.accumulo.proxy.ProxyServer;
import org.apache.accumulo.proxy.thrift.AccumuloProxy;
import org.apache.accumulo.proxy.thrift.AccumuloProxy.Client;
import org.apache.accumulo.proxy.thrift.AccumuloSecurityException;
import org.apache.accumulo.proxy.thrift.ColumnUpdate;
import org.apache.accumulo.proxy.thrift.Key;
import org.apache.accumulo.proxy.thrift.KeyValue;
import org.apache.accumulo.proxy.thrift.ScanOptions;
import org.apache.accumulo.proxy.thrift.ScanResult;
import org.apache.accumulo.proxy.thrift.TimeType;
import org.apache.accumulo.proxy.thrift.WriterOptions;
import org.apache.accumulo.server.util.PortUtils;
import org.apache.accumulo.test.categories.MiniClusterOnlyTests;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.CommonConfigurationKeysPublic;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.thrift.protocol.TCompactProtocol;
import org.apache.thrift.transport.TSaslClientTransport;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransportException;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.rules.ExpectedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Tests impersonation of clients by the proxy over SASL
 */
@Category(MiniClusterOnlyTests.class)
public class KerberosProxyIT extends AccumuloIT {
    private static final Logger log = LoggerFactory.getLogger(KerberosProxyIT.class);

    @Rule
    public ExpectedException thrown = ExpectedException.none();

    private static TestingKdc kdc;
    private static String krbEnabledForITs = null;
    private static File proxyKeytab;
    private static String hostname, proxyPrimary, proxyPrincipal;

    @Override
    protected int defaultTimeoutSeconds() {
        return 60 * 5;
    }

    @BeforeClass
    public static void startKdc() throws Exception {
        kdc = new TestingKdc();
        kdc.start();
        krbEnabledForITs = System.getProperty(MiniClusterHarness.USE_KERBEROS_FOR_IT_OPTION);
        if (null == krbEnabledForITs || !Boolean.parseBoolean(krbEnabledForITs)) {
            System.setProperty(MiniClusterHarness.USE_KERBEROS_FOR_IT_OPTION, "true");
        }

        // Create a principal+keytab for the proxy
        proxyKeytab = new File(kdc.getKeytabDir(), "proxy.keytab");
        hostname = InetAddress.getLocalHost().getCanonicalHostName();
        // Set the primary because the client needs to know it
        proxyPrimary = "proxy";
        // Qualify with an instance
        proxyPrincipal = proxyPrimary + "/" + hostname;
        kdc.createPrincipal(proxyKeytab, proxyPrincipal);
        // Tack on the realm too
        proxyPrincipal = kdc.qualifyUser(proxyPrincipal);
    }

    @AfterClass
    public static void stopKdc() throws Exception {
        if (null != kdc) {
            kdc.stop();
        }
        if (null != krbEnabledForITs) {
            System.setProperty(MiniClusterHarness.USE_KERBEROS_FOR_IT_OPTION, krbEnabledForITs);
        }
    }

    private MiniAccumuloClusterImpl mac;
    private Process proxyProcess;
    private int proxyPort;

    @Before
    public void startMac() throws Exception {
        MiniClusterHarness harness = new MiniClusterHarness();
        mac = harness.create(getClass().getName(), testName.getMethodName(), new PasswordToken("unused"),
                new MiniClusterConfigurationCallback() {

                    @Override
                    public void configureMiniCluster(MiniAccumuloConfigImpl cfg, Configuration coreSite) {
                        cfg.setNumTservers(1);
                        Map<String, String> siteCfg = cfg.getSiteConfig();
                        // Allow the proxy to impersonate the client user, but no one else
                        siteCfg.put(Property.INSTANCE_RPC_SASL_ALLOWED_USER_IMPERSONATION.getKey(),
                                proxyPrincipal + ":" + kdc.getRootUser().getPrincipal());
                        siteCfg.put(Property.INSTANCE_RPC_SASL_ALLOWED_HOST_IMPERSONATION.getKey(), "*");
                        cfg.setSiteConfig(siteCfg);
                    }

                }, kdc);

        mac.start();
        MiniAccumuloConfigImpl cfg = mac.getConfig();

        // Generate Proxy configuration and start the proxy
        proxyProcess = startProxy(cfg);

        // Enabled kerberos auth
        Configuration conf = new Configuration(false);
        conf.set(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION, "kerberos");
        UserGroupInformation.setConfiguration(conf);

        boolean success = false;
        ClusterUser rootUser = kdc.getRootUser();
        // Rely on the junit timeout rule
        while (!success) {
            UserGroupInformation ugi;
            try {
                UserGroupInformation.loginUserFromKeytab(rootUser.getPrincipal(),
                        rootUser.getKeytab().getAbsolutePath());
                ugi = UserGroupInformation.getCurrentUser();
            } catch (IOException ex) {
                log.info("Login as root is failing", ex);
                Thread.sleep(3000);
                continue;
            }

            TSocket socket = new TSocket(hostname, proxyPort);
            log.info("Connecting to proxy with server primary '" + proxyPrimary + "' running on " + hostname);
            TSaslClientTransport transport = new TSaslClientTransport("GSSAPI", null, proxyPrimary, hostname,
                    Collections.singletonMap("javax.security.sasl.qop", "auth"), null, socket);

            final UGIAssumingTransport ugiTransport = new UGIAssumingTransport(transport, ugi);

            try {
                // UGI transport will perform the doAs for us
                ugiTransport.open();
                success = true;
            } catch (TTransportException e) {
                Throwable cause = e.getCause();
                if (null != cause && cause instanceof ConnectException) {
                    log.info("Proxy not yet up, waiting");
                    Thread.sleep(3000);
                    proxyProcess = checkProxyAndRestart(proxyProcess, cfg);
                    continue;
                }
            } finally {
                if (null != ugiTransport) {
                    ugiTransport.close();
                }
            }
        }

        assertTrue("Failed to connect to the proxy repeatedly", success);
    }

    /**
     * Starts the thrift proxy using the given MAConfig.
     *
     * @param cfg
     *          configuration for MAC
     * @return Process for the thrift proxy
     */
    private Process startProxy(MiniAccumuloConfigImpl cfg) throws IOException {
        File proxyPropertiesFile = generateNewProxyConfiguration(cfg);
        return mac.exec(Proxy.class, "-p", proxyPropertiesFile.getCanonicalPath());
    }

    /**
     * Generates a proxy configuration file for the MAC instance. Implicitly updates {@link #proxyPort} when choosing the port the proxy will listen on.
     *
     * @param cfg
     *          The MAC configuration
     * @return The proxy's configuration file
     */
    private File generateNewProxyConfiguration(MiniAccumuloConfigImpl cfg) throws IOException {
        // Chooses a new port for the proxy as side-effect
        proxyPort = PortUtils.getRandomFreePort();

        // Proxy configuration
        File proxyPropertiesFile = new File(cfg.getConfDir(), "proxy.properties");
        if (proxyPropertiesFile.exists()) {
            assertTrue("Failed to delete proxy.properties file", proxyPropertiesFile.delete());
        }
        Properties proxyProperties = new Properties();
        proxyProperties.setProperty("useMockInstance", "false");
        proxyProperties.setProperty("useMiniAccumulo", "false");
        proxyProperties.setProperty("protocolFactory", TCompactProtocol.Factory.class.getName());
        proxyProperties.setProperty("tokenClass", KerberosToken.class.getName());
        proxyProperties.setProperty("port", Integer.toString(proxyPort));
        proxyProperties.setProperty("maxFrameSize", "16M");
        proxyProperties.setProperty("instance", mac.getInstanceName());
        proxyProperties.setProperty("zookeepers", mac.getZooKeepers());
        proxyProperties.setProperty("thriftServerType", "sasl");
        proxyProperties.setProperty("kerberosPrincipal", proxyPrincipal);
        proxyProperties.setProperty("kerberosKeytab", proxyKeytab.getCanonicalPath());

        // Write out the proxy.properties file
        FileWriter writer = new FileWriter(proxyPropertiesFile);
        proxyProperties.store(writer, "Configuration for Accumulo proxy");
        writer.close();

        log.info("Created configuration for proxy listening on {}", proxyPort);

        return proxyPropertiesFile;
    }

    /**
     * Restarts the thrift proxy if the previous instance is no longer running. If the proxy is still running, this method does nothing.
     *
     * @param proxy
     *          The thrift proxy process
     * @param cfg
     *          The MAC configuration
     * @return The process for the Proxy, either the previous instance or a new instance.
     */
    private Process checkProxyAndRestart(Process proxy, MiniAccumuloConfigImpl cfg) throws IOException {
        try {
            // Get the return code
            proxy.exitValue();
        } catch (IllegalThreadStateException e) {
            log.info("Proxy is still running");
            // OK, process is still running, don't restart
            return proxy;
        }

        log.info("Restarting proxy because it is no longer alive");

        // We got a return code which means the proxy exited. We'll assume this is because it failed
        // to bind the port due to the known race condition between choosing a port and having the
        // proxy bind it.
        return startProxy(cfg);
    }

    @After
    public void stopMac() throws Exception {
        if (null != proxyProcess) {
            log.info("Destroying proxy process");
            proxyProcess.destroy();
            log.info("Waiting for proxy termination");
            proxyProcess.waitFor();
            log.info("Proxy terminated");
        }
        if (null != mac) {
            mac.stop();
        }
    }

    @Test
    public void testProxyClient() throws Exception {
        ClusterUser rootUser = kdc.getRootUser();
        UserGroupInformation.loginUserFromKeytab(rootUser.getPrincipal(), rootUser.getKeytab().getAbsolutePath());
        UserGroupInformation ugi = UserGroupInformation.getCurrentUser();

        TSocket socket = new TSocket(hostname, proxyPort);
        log.info("Connecting to proxy with server primary '" + proxyPrimary + "' running on " + hostname);
        TSaslClientTransport transport = new TSaslClientTransport("GSSAPI", null, proxyPrimary, hostname,
                Collections.singletonMap("javax.security.sasl.qop", "auth"), null, socket);

        final UGIAssumingTransport ugiTransport = new UGIAssumingTransport(transport, ugi);

        // UGI transport will perform the doAs for us
        ugiTransport.open();

        AccumuloProxy.Client.Factory factory = new AccumuloProxy.Client.Factory();
        Client client = factory.getClient(new TCompactProtocol(ugiTransport), new TCompactProtocol(ugiTransport));

        // Will fail if the proxy can impersonate the client
        ByteBuffer login = client.login(rootUser.getPrincipal(), Collections.<String, String>emptyMap());

        // For all of the below actions, the proxy user doesn't have permission to do any of them, but the client user does.
        // The fact that any of them actually run tells us that impersonation is working.

        // Create a table
        String table = "table";
        if (!client.tableExists(login, table)) {
            client.createTable(login, table, true, TimeType.MILLIS);
        }

        // Write two records to the table
        String writer = client.createWriter(login, table, new WriterOptions());
        Map<ByteBuffer, List<ColumnUpdate>> updates = new HashMap<>();
        ColumnUpdate update = new ColumnUpdate(ByteBuffer.wrap("cf1".getBytes(UTF_8)),
                ByteBuffer.wrap("cq1".getBytes(UTF_8)));
        update.setValue(ByteBuffer.wrap("value1".getBytes(UTF_8)));
        updates.put(ByteBuffer.wrap("row1".getBytes(UTF_8)), Collections.<ColumnUpdate>singletonList(update));
        update = new ColumnUpdate(ByteBuffer.wrap("cf2".getBytes(UTF_8)), ByteBuffer.wrap("cq2".getBytes(UTF_8)));
        update.setValue(ByteBuffer.wrap("value2".getBytes(UTF_8)));
        updates.put(ByteBuffer.wrap("row2".getBytes(UTF_8)), Collections.<ColumnUpdate>singletonList(update));
        client.update(writer, updates);

        // Flush and close the writer
        client.flush(writer);
        client.closeWriter(writer);

        // Open a scanner to the table
        String scanner = client.createScanner(login, table, new ScanOptions());
        ScanResult results = client.nextK(scanner, 10);
        assertEquals(2, results.getResults().size());

        // Check the first key-value
        KeyValue kv = results.getResults().get(0);
        Key k = kv.key;
        ByteBuffer v = kv.value;
        assertEquals(ByteBuffer.wrap("row1".getBytes(UTF_8)), k.row);
        assertEquals(ByteBuffer.wrap("cf1".getBytes(UTF_8)), k.colFamily);
        assertEquals(ByteBuffer.wrap("cq1".getBytes(UTF_8)), k.colQualifier);
        assertEquals(ByteBuffer.wrap(new byte[0]), k.colVisibility);
        assertEquals(ByteBuffer.wrap("value1".getBytes(UTF_8)), v);

        // And then the second
        kv = results.getResults().get(1);
        k = kv.key;
        v = kv.value;
        assertEquals(ByteBuffer.wrap("row2".getBytes(UTF_8)), k.row);
        assertEquals(ByteBuffer.wrap("cf2".getBytes(UTF_8)), k.colFamily);
        assertEquals(ByteBuffer.wrap("cq2".getBytes(UTF_8)), k.colQualifier);
        assertEquals(ByteBuffer.wrap(new byte[0]), k.colVisibility);
        assertEquals(ByteBuffer.wrap("value2".getBytes(UTF_8)), v);

        // Close the scanner
        client.closeScanner(scanner);

        ugiTransport.close();
    }

    @Test
    public void testDisallowedClientForImpersonation() throws Exception {
        String user = testName.getMethodName();
        File keytab = new File(kdc.getKeytabDir(), user + ".keytab");
        kdc.createPrincipal(keytab, user);

        // Login as the new user
        UserGroupInformation.loginUserFromKeytab(user, keytab.getAbsolutePath());
        UserGroupInformation ugi = UserGroupInformation.getCurrentUser();

        log.info("Logged in as " + ugi);

        // Expect an AccumuloSecurityException
        thrown.expect(AccumuloSecurityException.class);
        // Error msg would look like:
        //
        // org.apache.accumulo.core.client.AccumuloSecurityException: Error BAD_CREDENTIALS for user Principal in credentials object should match kerberos
        // principal.
        // Expected 'proxy/hw10447.local@EXAMPLE.COM' but was 'testDisallowedClientForImpersonation@EXAMPLE.COM' - Username or Password is Invalid)
        thrown.expect(new ThriftExceptionMatchesPattern(".*Error BAD_CREDENTIALS.*"));
        thrown.expect(new ThriftExceptionMatchesPattern(
                ".*Expected '" + proxyPrincipal + "' but was '" + kdc.qualifyUser(user) + "'.*"));

        TSocket socket = new TSocket(hostname, proxyPort);
        log.info("Connecting to proxy with server primary '" + proxyPrimary + "' running on " + hostname);

        // Should fail to open the tran
        TSaslClientTransport transport = new TSaslClientTransport("GSSAPI", null, proxyPrimary, hostname,
                Collections.singletonMap("javax.security.sasl.qop", "auth"), null, socket);

        final UGIAssumingTransport ugiTransport = new UGIAssumingTransport(transport, ugi);

        // UGI transport will perform the doAs for us
        ugiTransport.open();

        AccumuloProxy.Client.Factory factory = new AccumuloProxy.Client.Factory();
        Client client = factory.getClient(new TCompactProtocol(ugiTransport), new TCompactProtocol(ugiTransport));

        // Will fail because the proxy can't impersonate this user (per the site configuration)
        try {
            client.login(kdc.qualifyUser(user), Collections.<String, String>emptyMap());
        } finally {
            if (null != ugiTransport) {
                ugiTransport.close();
            }
        }
    }

    @Test
    public void testMismatchPrincipals() throws Exception {
        ClusterUser rootUser = kdc.getRootUser();
        // Should get an AccumuloSecurityException and the given message
        thrown.expect(AccumuloSecurityException.class);
        thrown.expect(new ThriftExceptionMatchesPattern(ProxyServer.RPC_ACCUMULO_PRINCIPAL_MISMATCH_MSG));

        // Make a new user
        String user = testName.getMethodName();
        File keytab = new File(kdc.getKeytabDir(), user + ".keytab");
        kdc.createPrincipal(keytab, user);

        // Login as the new user
        UserGroupInformation.loginUserFromKeytab(user, keytab.getAbsolutePath());
        UserGroupInformation ugi = UserGroupInformation.getCurrentUser();

        log.info("Logged in as " + ugi);

        TSocket socket = new TSocket(hostname, proxyPort);
        log.info("Connecting to proxy with server primary '" + proxyPrimary + "' running on " + hostname);

        // Should fail to open the tran
        TSaslClientTransport transport = new TSaslClientTransport("GSSAPI", null, proxyPrimary, hostname,
                Collections.singletonMap("javax.security.sasl.qop", "auth"), null, socket);

        final UGIAssumingTransport ugiTransport = new UGIAssumingTransport(transport, ugi);

        // UGI transport will perform the doAs for us
        ugiTransport.open();

        AccumuloProxy.Client.Factory factory = new AccumuloProxy.Client.Factory();
        Client client = factory.getClient(new TCompactProtocol(ugiTransport), new TCompactProtocol(ugiTransport));

        // The proxy needs to recognize that the requested principal isn't the same as the SASL principal and fail
        // Accumulo should let this through -- we need to rely on the proxy to dump me before talking to accumulo
        try {
            client.login(rootUser.getPrincipal(), Collections.<String, String>emptyMap());
        } finally {
            if (null != ugiTransport) {
                ugiTransport.close();
            }
        }
    }

    private static class ThriftExceptionMatchesPattern extends TypeSafeMatcher<AccumuloSecurityException> {
        private String pattern;

        public ThriftExceptionMatchesPattern(String pattern) {
            this.pattern = pattern;
        }

        @Override
        protected boolean matchesSafely(AccumuloSecurityException item) {
            return item.isSetMsg() && item.msg.matches(pattern);
        }

        @Override
        public void describeTo(Description description) {
            description.appendText("matches pattern ").appendValue(pattern);
        }

        @Override
        protected void describeMismatchSafely(AccumuloSecurityException item, Description mismatchDescription) {
            mismatchDescription.appendText("does not match");
        }
    }
}