Merge branch '1.7' into 1.8
Project: http://git-wip-us.apache.org/repos/asf/accumulo/repo Commit: http://git-wip-us.apache.org/repos/asf/accumulo/commit/6b139768 Tree: http://git-wip-us.apache.org/repos/asf/accumulo/tree/6b139768 Diff: http://git-wip-us.apache.org/repos/asf/accumulo/diff/6b139768 Branch: refs/heads/1.8 Commit: 6b139768122103aa2d7c315572da3b7852a483c7 Parents: d38685c efc5a98 Author: Josh Elser <els...@apache.org> Authored: Mon Jun 26 17:47:22 2017 -0400 Committer: Josh Elser <els...@apache.org> Committed: Mon Jun 26 17:47:22 2017 -0400 ---------------------------------------------------------------------- .../client/security/tokens/KerberosToken.java | 19 ++-- .../apache/accumulo/core/rpc/ThriftUtil.java | 23 +++- .../test/functional/KerberosProxyIT.java | 109 ++++++++++++++++++- 3 files changed, 134 insertions(+), 17 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/accumulo/blob/6b139768/core/src/main/java/org/apache/accumulo/core/client/security/tokens/KerberosToken.java ---------------------------------------------------------------------- diff --cc core/src/main/java/org/apache/accumulo/core/client/security/tokens/KerberosToken.java index 1a4869d,5bcab1a..93e1242 --- a/core/src/main/java/org/apache/accumulo/core/client/security/tokens/KerberosToken.java +++ b/core/src/main/java/org/apache/accumulo/core/client/security/tokens/KerberosToken.java @@@ -82,22 -69,14 +83,16 @@@ public class KerberosToken implements A * A keytab file * @param replaceCurrentUser * Should the current Hadoop user be replaced with this user + * @deprecated since 1.8.0, @see #KerberosToken(String, File) */ + @Deprecated public KerberosToken(String principal, File keytab, boolean replaceCurrentUser) throws IOException { - requireNonNull(principal, "Principal was null"); - requireNonNull(keytab, "Keytab was null"); + this.principal = requireNonNull(principal, "Principal was null"); + this.keytab = requireNonNull(keytab, "Keytab was null"); checkArgument(keytab.exists() && keytab.isFile(), "Keytab was not a normal file"); - UserGroupInformation ugi; if (replaceCurrentUser) { UserGroupInformation.loginUserFromKeytab(principal, keytab.getAbsolutePath()); - ugi = UserGroupInformation.getCurrentUser(); - } else { - ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(principal, keytab.getAbsolutePath()); } - this.principal = ugi.getUserName(); - this.keytab = keytab; } /** http://git-wip-us.apache.org/repos/asf/accumulo/blob/6b139768/test/src/main/java/org/apache/accumulo/test/functional/KerberosProxyIT.java ---------------------------------------------------------------------- diff --cc test/src/main/java/org/apache/accumulo/test/functional/KerberosProxyIT.java index 59a6762,0000000..562f46f mode 100644,000000..100644 --- a/test/src/main/java/org/apache/accumulo/test/functional/KerberosProxyIT.java +++ b/test/src/main/java/org/apache/accumulo/test/functional/KerberosProxyIT.java @@@ -1,485 -1,0 +1,588 @@@ +/* + * 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.assertFalse; +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.security.PrivilegedExceptionAction; +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.Connector; ++import org.apache.accumulo.core.client.Scanner; ++import org.apache.accumulo.core.client.ZooKeeperInstance; +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.core.security.Authorizations; ++import org.apache.accumulo.core.security.TablePermission; +import org.apache.accumulo.harness.AccumuloITBase; +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.Assert; +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; + ++import com.google.common.base.Throwables; ++import com.google.common.collect.Iterables; ++ +/** - * Tests impersonation of clients by the proxy over SASL ++ * Tests impersonation of clients over Kerberos+SASL. "Proxy" may be referring to the Accumulo Proxy service or it may be referring to the notion of a username ++ * overriding the real username of the actual credentials used in the system. Beware of the context of the word "proxy". + */ +@Category(MiniClusterOnlyTests.class) +public class KerberosProxyIT extends AccumuloITBase { + private static final Logger log = LoggerFactory.getLogger(KerberosProxyIT.class); ++ private static final String PROXIED_USER1 = "proxied_user1", PROXIED_USER2 = "proxied_user2", PROXIED_USER3 = "proxied_user3"; + + @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); + } + UserGroupInformation.setConfiguration(new Configuration(false)); + } + + 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()); ++ // Allow the proxy to impersonate the "root" Accumulo user and our one special user. ++ siteCfg.put(Property.INSTANCE_RPC_SASL_ALLOWED_USER_IMPERSONATION.getKey(), ++ proxyPrincipal + ":" + kdc.getRootUser().getPrincipal() + "," + kdc.qualifyUser(PROXIED_USER1) + "," + kdc.qualifyUser(PROXIED_USER2)); + 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 { + ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(rootUser.getPrincipal(), rootUser.getKeytab().getAbsolutePath()); + } 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 ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(rootUser.getPrincipal(), rootUser.getKeytab().getAbsolutePath()); + + 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 ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(user, keytab.getAbsolutePath()); + + 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.lo...@example.com' but was 'testdisallowedclientforimpersonat...@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 ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(user, keytab.getAbsolutePath()); + + 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(); + } + } + } + ++ @Test ++ public void proxiedUserAccessWithoutAccumuloProxy() throws Exception { ++ final String tableName = getUniqueNames(1)[0]; ++ ClusterUser rootUser = kdc.getRootUser(); ++ final UserGroupInformation rootUgi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(rootUser.getPrincipal(), rootUser.getKeytab().getAbsolutePath()); ++ final UserGroupInformation realUgi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(proxyPrincipal, proxyKeytab.getAbsolutePath()); ++ final String userWithoutCredentials1 = kdc.qualifyUser(PROXIED_USER1); ++ final String userWithoutCredentials2 = kdc.qualifyUser(PROXIED_USER2); ++ final String userWithoutCredentials3 = kdc.qualifyUser(PROXIED_USER3); ++ final UserGroupInformation proxyUser1 = UserGroupInformation.createProxyUser(userWithoutCredentials1, realUgi); ++ final UserGroupInformation proxyUser2 = UserGroupInformation.createProxyUser(userWithoutCredentials2, realUgi); ++ final UserGroupInformation proxyUser3 = UserGroupInformation.createProxyUser(userWithoutCredentials3, realUgi); ++ ++ // Create a table and user, grant permission to our user to read that table. ++ rootUgi.doAs(new PrivilegedExceptionAction<Void>() { ++ @Override ++ public Void run() throws Exception { ++ ZooKeeperInstance inst = new ZooKeeperInstance(mac.getClientConfig()); ++ Connector conn = inst.getConnector(rootUgi.getUserName(), new KerberosToken()); ++ conn.tableOperations().create(tableName); ++ conn.securityOperations().createLocalUser(userWithoutCredentials1, new PasswordToken("ignored")); ++ conn.securityOperations().grantTablePermission(userWithoutCredentials1, tableName, TablePermission.READ); ++ conn.securityOperations().createLocalUser(userWithoutCredentials3, new PasswordToken("ignored")); ++ conn.securityOperations().grantTablePermission(userWithoutCredentials3, tableName, TablePermission.READ); ++ return null; ++ } ++ }); ++ realUgi.doAs(new PrivilegedExceptionAction<Void>() { ++ @Override ++ public Void run() throws Exception { ++ ZooKeeperInstance inst = new ZooKeeperInstance(mac.getClientConfig()); ++ Connector conn = inst.getConnector(proxyPrincipal, new KerberosToken()); ++ try { ++ Scanner s = conn.createScanner(tableName, Authorizations.EMPTY); ++ s.iterator().hasNext(); ++ Assert.fail("Expected to see an exception"); ++ } catch (RuntimeException e) { ++ int numSecurityExceptionsSeen = Iterables.size(Iterables.filter(Throwables.getCausalChain(e), ++ org.apache.accumulo.core.client.AccumuloSecurityException.class)); ++ assertTrue("Expected to see at least one AccumuloSecurityException, but saw: " + Throwables.getStackTraceAsString(e), numSecurityExceptionsSeen > 0); ++ } ++ return null; ++ } ++ }); ++ // Allowed to be proxied and has read permission ++ proxyUser1.doAs(new PrivilegedExceptionAction<Void>() { ++ @Override ++ public Void run() throws Exception { ++ ZooKeeperInstance inst = new ZooKeeperInstance(mac.getClientConfig()); ++ Connector conn = inst.getConnector(userWithoutCredentials1, new KerberosToken(userWithoutCredentials1)); ++ Scanner s = conn.createScanner(tableName, Authorizations.EMPTY); ++ assertFalse(s.iterator().hasNext()); ++ return null; ++ } ++ }); ++ // Allowed to be proxied but does not have read permission ++ proxyUser2.doAs(new PrivilegedExceptionAction<Void>() { ++ @Override ++ public Void run() throws Exception { ++ ZooKeeperInstance inst = new ZooKeeperInstance(mac.getClientConfig()); ++ Connector conn = inst.getConnector(userWithoutCredentials2, new KerberosToken(userWithoutCredentials3)); ++ try { ++ Scanner s = conn.createScanner(tableName, Authorizations.EMPTY); ++ s.iterator().hasNext(); ++ Assert.fail("Expected to see an exception"); ++ } catch (RuntimeException e) { ++ int numSecurityExceptionsSeen = Iterables.size(Iterables.filter(Throwables.getCausalChain(e), ++ org.apache.accumulo.core.client.AccumuloSecurityException.class)); ++ assertTrue("Expected to see at least one AccumuloSecurityException, but saw: " + Throwables.getStackTraceAsString(e), numSecurityExceptionsSeen > 0); ++ } ++ return null; ++ } ++ }); ++ // Has read permission but is not allowed to be proxied ++ proxyUser3.doAs(new PrivilegedExceptionAction<Void>() { ++ @Override ++ public Void run() throws Exception { ++ ZooKeeperInstance inst = new ZooKeeperInstance(mac.getClientConfig()); ++ try { ++ inst.getConnector(userWithoutCredentials3, new KerberosToken(userWithoutCredentials3)); ++ Assert.fail("Should not be able to create a Connector as this user cannot be proxied"); ++ } catch (org.apache.accumulo.core.client.AccumuloSecurityException e) { ++ // Expected, this user cannot be proxied ++ } ++ return null; ++ } ++ }); ++ } ++ + 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"); + } + } +}