You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@accumulo.apache.org by el...@apache.org on 2015/01/22 00:36:51 UTC

[2/4] accumulo git commit: ACCUMULO-3452 Enables KRB authentication to proxy and impersonation.

http://git-wip-us.apache.org/repos/asf/accumulo/blob/98ced20a/test/src/test/java/org/apache/accumulo/test/functional/KerberosProxyIT.java
----------------------------------------------------------------------
diff --git a/test/src/test/java/org/apache/accumulo/test/functional/KerberosProxyIT.java b/test/src/test/java/org/apache/accumulo/test/functional/KerberosProxyIT.java
new file mode 100644
index 0000000..e4c46d6
--- /dev/null
+++ b/test/src/test/java/org/apache/accumulo/test/functional/KerberosProxyIT.java
@@ -0,0 +1,362 @@
+/*
+ * 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.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.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.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.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.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Tests impersonation of clients by the proxy over SASL
+ */
+public class KerberosProxyIT extends AccumuloIT {
+  private static final Logger log = LoggerFactory.getLogger(KerberosProxyIT.class);
+
+  private static TestingKdc kdc;
+  private static String krbEnabledForITs = null;
+  private static File proxyKeytab;
+  private static String hostname, proxyPrimary, proxyPrincipal;
+
+  @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_PROXYUSERS.getKey() + proxyPrincipal + ".users", kdc.getClientPrincipal());
+        siteCfg.put(Property.INSTANCE_RPC_SASL_PROXYUSERS.getKey() + proxyPrincipal + ".hosts", "*");
+        cfg.setSiteConfig(siteCfg);
+      }
+
+    }, kdc);
+
+    mac.start();
+    MiniAccumuloConfigImpl cfg = mac.getConfig();
+
+    // Proxy configuration
+    proxyPort = PortUtils.getRandomFreePort();
+    File proxyPropertiesFile = new File(cfg.getConfDir(), "proxy.properties");
+    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();
+
+    proxyProcess = mac.exec(Proxy.class, "-p", proxyPropertiesFile.getCanonicalPath());
+
+    // Enabled kerberos auth
+    Configuration conf = new Configuration(false);
+    conf.set(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION, "kerberos");
+    UserGroupInformation.setConfiguration(conf);
+
+    boolean success = false;
+    UserGroupInformation.loginUserFromKeytab(kdc.getClientPrincipal(), kdc.getClientKeytab().getAbsolutePath());
+    UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
+    for (int i = 0; i < 10 && !success; i++) {
+      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(1000);
+          continue;
+        }
+      } finally {
+        if (null != ugiTransport) {
+          ugiTransport.close();
+        }
+      }
+    }
+
+    assertTrue("Failed to connect to the proxy repeatedly", success);
+  }
+
+  @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 {
+    UserGroupInformation.loginUserFromKeytab(kdc.getClientPrincipal(), kdc.getClientKeytab().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(kdc.getClientPrincipal(), 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(expected = AccumuloSecurityException.class)
+  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);
+
+    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(expected = AccumuloSecurityException.class)
+  public void testMismatchPrincipals() throws Exception {
+    // 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(kdc.getClientPrincipal(), Collections.<String,String> emptyMap());
+    } finally {
+      if (null != ugiTransport) {
+        ugiTransport.close();
+      }
+    }
+  }
+}