ACCUMULO-4665 Use UGI with real Kerberos credentials

UGI supports the notion of users without credentials being
"proxied" (riding on top of) another user which does have
credentials. This is authorized via configuration. These
changes allow this scenario more naturally and remove
unnecessarily strict assertions in KerberosToken.

Closes #273


Project: http://git-wip-us.apache.org/repos/asf/accumulo/repo
Commit: http://git-wip-us.apache.org/repos/asf/accumulo/commit/efc5a98b
Tree: http://git-wip-us.apache.org/repos/asf/accumulo/tree/efc5a98b
Diff: http://git-wip-us.apache.org/repos/asf/accumulo/diff/efc5a98b

Branch: refs/heads/master
Commit: efc5a98bb4f27503bf82328d2e68d29c04860d14
Parents: 722d5eb
Author: Josh Elser <els...@apache.org>
Authored: Mon Jun 26 17:46:29 2017 -0400
Committer: Josh Elser <els...@apache.org>
Committed: Mon Jun 26 17:46:29 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/efc5a98b/core/src/main/java/org/apache/accumulo/core/client/security/tokens/KerberosToken.java
----------------------------------------------------------------------
diff --git 
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
index 284a838..5bcab1a 100644
--- 
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
@@ -29,6 +29,7 @@ import java.util.Set;
 import javax.security.auth.DestroyFailedException;
 
 import org.apache.hadoop.security.UserGroupInformation;
+import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod;
 
 /**
  * Authentication token for Kerberos authenticated clients
@@ -51,11 +52,11 @@ public class KerberosToken implements AuthenticationToken {
    *          The user that is logged in
    */
   public KerberosToken(String principal) throws IOException {
-    requireNonNull(principal);
+    this.principal = requireNonNull(principal);
     final UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
-    checkArgument(ugi.hasKerberosCredentials(), "Subject is not logged in via 
Kerberos");
-    checkArgument(principal.equals(ugi.getUserName()), "Provided principal 
does not match currently logged-in user");
-    this.principal = ugi.getUserName();
+    if (AuthenticationMethod.KERBEROS == ugi.getAuthenticationMethod()) {
+      checkArgument(ugi.hasKerberosCredentials(), "Subject is not logged in 
via Kerberos");
+    }
   }
 
   /**
@@ -70,18 +71,12 @@ public class KerberosToken implements AuthenticationToken {
    *          Should the current Hadoop user be replaced with this user
    */
   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/efc5a98b/core/src/main/java/org/apache/accumulo/core/rpc/ThriftUtil.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/accumulo/core/rpc/ThriftUtil.java 
b/core/src/main/java/org/apache/accumulo/core/rpc/ThriftUtil.java
index deee3fe..97d0735 100644
--- a/core/src/main/java/org/apache/accumulo/core/rpc/ThriftUtil.java
+++ b/core/src/main/java/org/apache/accumulo/core/rpc/ThriftUtil.java
@@ -35,6 +35,7 @@ import 
org.apache.accumulo.core.client.impl.ThriftTransportPool;
 import org.apache.accumulo.core.rpc.SaslConnectionParams.SaslMechanism;
 import org.apache.accumulo.core.tabletserver.thrift.TabletClientService;
 import org.apache.hadoop.security.UserGroupInformation;
+import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod;
 import org.apache.thrift.TException;
 import org.apache.thrift.TServiceClient;
 import org.apache.thrift.TServiceClientFactory;
@@ -282,13 +283,31 @@ public class ThriftUtil {
         try {
           // Log in via UGI, ensures we have logged in with our KRB credentials
           final UserGroupInformation currentUser = 
UserGroupInformation.getCurrentUser();
+          final UserGroupInformation userForRpc;
+          if (AuthenticationMethod.PROXY == 
currentUser.getAuthenticationMethod()) {
+            // A "proxy" user is when the real (Kerberos) credentials are for 
a user
+            // other than the one we're acting as. When we make an RPC though, 
we need to make sure
+            // that the current user is the user that has some credentials.
+            if (currentUser.getRealUser() != null) {
+              userForRpc = currentUser.getRealUser();
+              log.trace("{} is a proxy user, using real user instead {}", 
currentUser, userForRpc);
+            } else {
+              // The current user has no credentials, let it fail naturally at 
the RPC layer (no ticket)
+              // We know this won't work, but we can't do anything else
+              log.warn("The current user is a proxy user but there is no 
underlying real user (likely that RPCs will fail): {}", currentUser);
+              userForRpc = currentUser;
+            }
+          } else {
+            // The normal case: the current user has its own ticket
+            userForRpc = currentUser;
+          }
 
           // Is this pricey enough that we want to cache it?
           final String hostname = 
InetAddress.getByName(address.getHostText()).getCanonicalHostName();
 
           final SaslMechanism mechanism = saslParams.getMechanism();
 
-          log.trace("Opening transport to server as {} to {}/{} using {}", 
currentUser, saslParams.getKerberosServerPrimary(), hostname, mechanism);
+          log.trace("Opening transport to server as {} to {}/{} using {}", 
userForRpc, saslParams.getKerberosServerPrimary(), hostname, mechanism);
 
           // Create the client SASL transport using the information for the 
server
           // Despite the 'protocol' argument seeming to be useless, it *must* 
be the primary of the server being connected to
@@ -296,7 +315,7 @@ public class ThriftUtil {
               saslParams.getSaslProperties(), saslParams.getCallbackHandler(), 
transport);
 
           // Wrap it all in a processor which will run with a doAs the current 
user
-          transport = new UGIAssumingTransport(transport, currentUser);
+          transport = new UGIAssumingTransport(transport, userForRpc);
 
           // Open the transport
           transport.open();

http://git-wip-us.apache.org/repos/asf/accumulo/blob/efc5a98b/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
index e03a1a8..94491d4 100644
--- 
a/test/src/test/java/org/apache/accumulo/test/functional/KerberosProxyIT.java
+++ 
b/test/src/test/java/org/apache/accumulo/test/functional/KerberosProxyIT.java
@@ -18,6 +18,7 @@ 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;
@@ -26,6 +27,7 @@ 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;
@@ -33,10 +35,15 @@ 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.AccumuloIT;
 import org.apache.accumulo.harness.MiniClusterConfigurationCallback;
 import org.apache.accumulo.harness.MiniClusterHarness;
@@ -68,6 +75,7 @@ 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;
@@ -77,12 +85,17 @@ 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 AccumuloIT {
   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();
@@ -141,8 +154,9 @@ public class KerberosProxyIT extends AccumuloIT {
       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);
       }
@@ -463,6 +477,95 @@ public class KerberosProxyIT extends AccumuloIT {
     }
   }
 
+  @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;
 

Reply via email to