HADOOP-13433 Race in UGI.reloginFromKeytab. Contributed by Duo Zhang.

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

Branch: refs/heads/HADOOP-13345
Commit: 7fc3e68a876132563aa2321519fc6941e37b2cae
Parents: abedb8a
Author: Steve Loughran <ste...@apache.org>
Authored: Wed Jan 25 21:29:27 2017 +0000
Committer: Steve Loughran <ste...@apache.org>
Committed: Wed Jan 25 21:29:27 2017 +0000

----------------------------------------------------------------------
 hadoop-common-project/hadoop-common/pom.xml     |   5 +
 .../hadoop/security/UserGroupInformation.java   |  65 ++++++--
 .../security/TestFixKerberosTicketOrder.java    | 158 ++++++++++++++++++
 .../hadoop/security/TestRaceWhenRelogin.java    | 162 +++++++++++++++++++
 4 files changed, 375 insertions(+), 15 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/hadoop/blob/7fc3e68a/hadoop-common-project/hadoop-common/pom.xml
----------------------------------------------------------------------
diff --git a/hadoop-common-project/hadoop-common/pom.xml 
b/hadoop-common-project/hadoop-common/pom.xml
index b69de55..909cd78 100644
--- a/hadoop-common-project/hadoop-common/pom.xml
+++ b/hadoop-common-project/hadoop-common/pom.xml
@@ -46,6 +46,11 @@
       <scope>compile</scope>
     </dependency>
     <dependency>
+      <groupId>org.apache.hadoop</groupId>
+      <artifactId>hadoop-minikdc</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
       <groupId>com.google.guava</groupId>
       <artifactId>guava</artifactId>
       <scope>compile</scope>

http://git-wip-us.apache.org/repos/asf/hadoop/blob/7fc3e68a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/UserGroupInformation.java
----------------------------------------------------------------------
diff --git 
a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/UserGroupInformation.java
 
b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/UserGroupInformation.java
index cf240ff..6574e55 100644
--- 
a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/UserGroupInformation.java
+++ 
b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/UserGroupInformation.java
@@ -24,6 +24,8 @@ import static 
org.apache.hadoop.fs.CommonConfigurationKeysPublic.HADOOP_TOKEN_FI
 import static org.apache.hadoop.security.UGIExceptionMessages.*;
 import static org.apache.hadoop.util.PlatformName.IBM_JAVA;
 
+import com.google.common.annotations.VisibleForTesting;
+
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
@@ -45,6 +47,7 @@ import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
+import javax.security.auth.DestroyFailedException;
 import javax.security.auth.Subject;
 import javax.security.auth.callback.CallbackHandler;
 import javax.security.auth.kerberos.KerberosPrincipal;
@@ -76,8 +79,6 @@ import org.apache.hadoop.security.token.TokenIdentifier;
 import org.apache.hadoop.util.Shell;
 import org.apache.hadoop.util.StringUtils;
 import org.apache.hadoop.util.Time;
-
-import com.google.common.annotations.VisibleForTesting;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -1165,10 +1166,41 @@ public class UserGroupInformation {
     reloginFromKeytab();
   }
 
+  // if the first kerberos ticket is not TGT, then remove and destroy it since
+  // the kerberos library of jdk always use the first kerberos ticket as TGT.
+  // See HADOOP-13433 for more details.
+  @VisibleForTesting
+  void fixKerberosTicketOrder() {
+    Set<Object> creds = getSubject().getPrivateCredentials();
+    synchronized (creds) {
+      for (Iterator<Object> iter = creds.iterator(); iter.hasNext();) {
+        Object cred = iter.next();
+        if (cred instanceof KerberosTicket) {
+          KerberosTicket ticket = (KerberosTicket) cred;
+          if (!ticket.getServer().getName().startsWith("krbtgt")) {
+            LOG.warn(
+                "The first kerberos ticket is not TGT"
+                    + "(the server principal is {}), remove and destroy it.",
+                ticket.getServer());
+            iter.remove();
+            try {
+              ticket.destroy();
+            } catch (DestroyFailedException e) {
+              LOG.warn("destroy ticket failed", e);
+            }
+          } else {
+            return;
+          }
+        }
+      }
+    }
+    LOG.warn("Warning, no kerberos ticket found while attempting to renew 
ticket");
+  }
+
   /**
    * Re-Login a user in from a keytab file. Loads a user identity from a keytab
    * file and logs them in. They become the currently logged-in user. This
-   * method assumes that {@link #loginUserFromKeytab(String, String)} had 
+   * method assumes that {@link #loginUserFromKeytab(String, String)} had
    * happened already.
    * The Subject field of this UserGroupInformation object is updated to have
    * the new credentials.
@@ -1178,11 +1210,12 @@ public class UserGroupInformation {
   @InterfaceAudience.Public
   @InterfaceStability.Evolving
   public synchronized void reloginFromKeytab() throws IOException {
-    if (!isSecurityEnabled() ||
-         user.getAuthenticationMethod() != AuthenticationMethod.KERBEROS ||
-         !isKeytab)
+    if (!isSecurityEnabled()
+        || user.getAuthenticationMethod() != AuthenticationMethod.KERBEROS
+        || !isKeytab) {
       return;
-    
+    }
+
     long now = Time.now();
     if (!shouldRenewImmediatelyForTests && !hasSufficientTimeElapsed(now)) {
       return;
@@ -1194,12 +1227,12 @@ public class UserGroupInformation {
         now < getRefreshTime(tgt)) {
       return;
     }
-    
+
     LoginContext login = getLogin();
     if (login == null || keytabFile == null) {
       throw new KerberosAuthException(MUST_FIRST_LOGIN_FROM_KEYTAB);
     }
-    
+
     long start = 0;
     // register most recent relogin attempt
     user.setLastLogin(now);
@@ -1222,6 +1255,7 @@ public class UserGroupInformation {
         }
         start = Time.now();
         login.login();
+        fixKerberosTicketOrder();
         metrics.loginSuccess.add(Time.now() - start);
         setLogin(login);
       }
@@ -1233,7 +1267,7 @@ public class UserGroupInformation {
       kae.setPrincipal(keytabPrincipal);
       kae.setKeytabFile(keytabFile);
       throw kae;
-    } 
+    }
   }
 
   /**
@@ -1247,10 +1281,11 @@ public class UserGroupInformation {
   @InterfaceAudience.Public
   @InterfaceStability.Evolving
   public synchronized void reloginFromTicketCache() throws IOException {
-    if (!isSecurityEnabled() || 
-        user.getAuthenticationMethod() != AuthenticationMethod.KERBEROS ||
-        !isKrbTkt)
+    if (!isSecurityEnabled()
+        || user.getAuthenticationMethod() != AuthenticationMethod.KERBEROS
+        || !isKrbTkt) {
       return;
+    }
     LoginContext login = getLogin();
     if (login == null) {
       throw new KerberosAuthException(MUST_FIRST_LOGIN);
@@ -1278,15 +1313,15 @@ public class UserGroupInformation {
         LOG.debug("Initiating re-login for " + getUserName());
       }
       login.login();
+      fixKerberosTicketOrder();
       setLogin(login);
     } catch (LoginException le) {
       KerberosAuthException kae = new KerberosAuthException(LOGIN_FAILURE, le);
       kae.setUser(getUserName());
       throw kae;
-    } 
+    }
   }
 
-
   /**
    * Log a user in from a keytab file. Loads a user identity from a keytab
    * file and login them in. This new user does not affect the currently

http://git-wip-us.apache.org/repos/asf/hadoop/blob/7fc3e68a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestFixKerberosTicketOrder.java
----------------------------------------------------------------------
diff --git 
a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestFixKerberosTicketOrder.java
 
b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestFixKerberosTicketOrder.java
new file mode 100644
index 0000000..4b75a36
--- /dev/null
+++ 
b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestFixKerberosTicketOrder.java
@@ -0,0 +1,158 @@
+/**
+ * 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.hadoop.security;
+
+import static org.apache.hadoop.test.LambdaTestUtils.intercept;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.security.PrivilegedExceptionAction;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.security.auth.Subject;
+import javax.security.auth.kerberos.KerberosTicket;
+import javax.security.sasl.Sasl;
+import javax.security.sasl.SaslClient;
+import javax.security.sasl.SaslException;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.minikdc.KerberosSecurityTestcase;
+import org.apache.hadoop.security.SaslRpcServer.AuthMethod;
+import org.apache.hadoop.security.SaslRpcServer.QualityOfProtection;
+import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Testcase for HADOOP-13433 that verifies the logic of fixKerberosTicketOrder.
+ */
+public class TestFixKerberosTicketOrder extends KerberosSecurityTestcase {
+
+  private String clientPrincipal = "client";
+
+  private String server1Protocol = "server1";
+
+  private String server2Protocol = "server2";
+
+  private String host = "localhost";
+
+  private String server1Principal = server1Protocol + "/" + host;
+
+  private String server2Principal = server2Protocol + "/" + host;
+
+  private File keytabFile;
+
+  private Configuration conf = new Configuration();
+
+  private Map<String, String> props;
+
+  @Before
+  public void setUp() throws Exception {
+    keytabFile = new File(getWorkDir(), "keytab");
+    getKdc().createPrincipal(keytabFile, clientPrincipal, server1Principal,
+        server2Principal);
+    SecurityUtil.setAuthenticationMethod(AuthenticationMethod.KERBEROS, conf);
+    UserGroupInformation.setConfiguration(conf);
+    UserGroupInformation.setShouldRenewImmediatelyForTests(true);
+    props = new HashMap<String, String>();
+    props.put(Sasl.QOP, QualityOfProtection.AUTHENTICATION.saslQop);
+  }
+
+  @Test
+  public void test() throws Exception {
+    UserGroupInformation ugi =
+        UserGroupInformation.loginUserFromKeytabAndReturnUGI(clientPrincipal,
+            keytabFile.getCanonicalPath());
+    ugi.doAs(new PrivilegedExceptionAction<Void>() {
+
+      @Override
+      public Void run() throws Exception {
+        SaslClient client = Sasl.createSaslClient(
+            new String[] {AuthMethod.KERBEROS.getMechanismName()},
+            clientPrincipal, server1Protocol, host, props, null);
+        client.evaluateChallenge(new byte[0]);
+        client.dispose();
+        return null;
+      }
+    });
+
+    Subject subject = ugi.getSubject();
+
+    // move tgt to the last
+    for (KerberosTicket ticket : subject
+        .getPrivateCredentials(KerberosTicket.class)) {
+      if (ticket.getServer().getName().startsWith("krbtgt")) {
+        subject.getPrivateCredentials().remove(ticket);
+        subject.getPrivateCredentials().add(ticket);
+        break;
+      }
+    }
+    // make sure the first ticket is not tgt
+    assertFalse(
+        "The first ticket is still tgt, "
+            + "the implementation in jdk may have been changed, "
+            + "please reconsider the problem in HADOOP-13433",
+        subject.getPrivateCredentials().stream()
+            .filter(c -> c instanceof KerberosTicket)
+            .map(c -> ((KerberosTicket) c).getServer().getName()).findFirst()
+            .get().startsWith("krbtgt"));
+    // should fail as we send a service ticket instead of tgt to KDC.
+    intercept(SaslException.class,
+        () -> ugi.doAs(new PrivilegedExceptionAction<Void>() {
+
+          @Override
+          public Void run() throws Exception {
+            SaslClient client = Sasl.createSaslClient(
+                new String[] {AuthMethod.KERBEROS.getMechanismName()},
+                clientPrincipal, server2Protocol, host, props, null);
+            client.evaluateChallenge(new byte[0]);
+            client.dispose();
+            return null;
+          }
+        }));
+
+    ugi.fixKerberosTicketOrder();
+
+    // check if TGT is the first ticket after the fix.
+    assertTrue("The first ticket is not tgt",
+        subject.getPrivateCredentials().stream()
+            .filter(c -> c instanceof KerberosTicket)
+            .map(c -> ((KerberosTicket) c).getServer().getName()).findFirst()
+            .get().startsWith("krbtgt"));
+
+    // make sure we can still get new service ticket after the fix.
+    ugi.doAs(new PrivilegedExceptionAction<Void>() {
+
+      @Override
+      public Void run() throws Exception {
+        SaslClient client = Sasl.createSaslClient(
+            new String[] {AuthMethod.KERBEROS.getMechanismName()},
+            clientPrincipal, server2Protocol, host, props, null);
+        client.evaluateChallenge(new byte[0]);
+        client.dispose();
+        return null;
+      }
+    });
+    assertTrue("No service ticket for " + server2Protocol + " found",
+        subject.getPrivateCredentials(KerberosTicket.class).stream()
+            .filter(t -> t.getServer().getName().startsWith(server2Protocol))
+            .findAny().isPresent());
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/hadoop/blob/7fc3e68a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestRaceWhenRelogin.java
----------------------------------------------------------------------
diff --git 
a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestRaceWhenRelogin.java
 
b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestRaceWhenRelogin.java
new file mode 100644
index 0000000..4f9946c
--- /dev/null
+++ 
b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestRaceWhenRelogin.java
@@ -0,0 +1,162 @@
+/**
+ * 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.hadoop.security;
+
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.security.PrivilegedExceptionAction;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.security.auth.kerberos.KerberosTicket;
+import javax.security.sasl.Sasl;
+import javax.security.sasl.SaslClient;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.minikdc.KerberosSecurityTestcase;
+import org.apache.hadoop.security.SaslRpcServer.AuthMethod;
+import org.apache.hadoop.security.SaslRpcServer.QualityOfProtection;
+import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Testcase for HADOOP-13433 that confirms that tgt will always be the first
+ * ticket after relogin.
+ */
+public class TestRaceWhenRelogin extends KerberosSecurityTestcase {
+
+  private int numThreads = 10;
+
+  private String clientPrincipal = "client";
+
+  private String serverProtocol = "server";
+
+  private String[] serverProtocols;
+
+  private String host = "localhost";
+
+  private String serverPrincipal = serverProtocol + "/" + host;
+
+  private String[] serverPrincipals;
+
+  private File keytabFile;
+
+  private Configuration conf = new Configuration();
+
+  private Map<String, String> props;
+
+  private UserGroupInformation ugi;
+
+  @Before
+  public void setUp() throws Exception {
+    keytabFile = new File(getWorkDir(), "keytab");
+    serverProtocols = new String[numThreads];
+    serverPrincipals = new String[numThreads];
+    for (int i = 0; i < numThreads; i++) {
+      serverProtocols[i] = serverProtocol + i;
+      serverPrincipals[i] = serverProtocols[i] + "/" + host;
+    }
+    String[] principals =
+        Arrays.copyOf(serverPrincipals, serverPrincipals.length + 2);
+    principals[numThreads] = serverPrincipal;
+    principals[numThreads + 1] = clientPrincipal;
+    getKdc().createPrincipal(keytabFile, principals);
+    SecurityUtil.setAuthenticationMethod(AuthenticationMethod.KERBEROS, conf);
+    UserGroupInformation.setConfiguration(conf);
+    UserGroupInformation.setShouldRenewImmediatelyForTests(true);
+    props = new HashMap<String, String>();
+    props.put(Sasl.QOP, QualityOfProtection.AUTHENTICATION.saslQop);
+    ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(clientPrincipal,
+        keytabFile.getAbsolutePath());
+  }
+
+  private void relogin(AtomicBoolean pass) {
+    for (int i = 0; i < 100; i++) {
+      try {
+        ugi.reloginFromKeytab();
+      } catch (IOException e) {
+      }
+      KerberosTicket tgt = ugi.getSubject().getPrivateCredentials().stream()
+          .filter(c -> c instanceof KerberosTicket).map(c -> (KerberosTicket) 
c)
+          .findFirst().get();
+      if (!tgt.getServer().getName().startsWith("krbtgt")) {
+        pass.set(false);
+        return;
+      }
+      try {
+        Thread.sleep(50);
+      } catch (InterruptedException e) {
+      }
+    }
+  }
+
+  private void getServiceTicket(AtomicBoolean running, String serverProtocol) {
+    while (running.get()) {
+      try {
+        ugi.doAs(new PrivilegedExceptionAction<Void>() {
+
+          @Override
+          public Void run() throws Exception {
+            SaslClient client = Sasl.createSaslClient(
+                new String[] {AuthMethod.KERBEROS.getMechanismName()},
+                clientPrincipal, serverProtocol, host, props, null);
+            client.evaluateChallenge(new byte[0]);
+            client.dispose();
+            return null;
+          }
+        });
+      } catch (Exception e) {
+      }
+      try {
+        Thread.sleep(ThreadLocalRandom.current().nextInt(100));
+      } catch (InterruptedException e) {
+      }
+    }
+  }
+
+  @Test
+  public void test() throws InterruptedException, IOException {
+    AtomicBoolean pass = new AtomicBoolean(true);
+    Thread reloginThread = new Thread(() -> relogin(pass), "Relogin");
+
+    AtomicBoolean running = new AtomicBoolean(true);
+    Thread[] getServiceTicketThreads = new Thread[numThreads];
+    for (int i = 0; i < numThreads; i++) {
+      String serverProtocol = serverProtocols[i];
+      getServiceTicketThreads[i] =
+          new Thread(() -> getServiceTicket(running, serverProtocol),
+              "GetServiceTicket-" + i);
+    }
+    for (Thread getServiceTicketThread : getServiceTicketThreads) {
+      getServiceTicketThread.start();
+    }
+    reloginThread.start();
+    reloginThread.join();
+    running.set(false);
+    for (Thread getServiceTicketThread : getServiceTicketThreads) {
+      getServiceTicketThread.join();
+    }
+    assertTrue("tgt is not the first ticket after relogin", pass.get());
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: common-commits-unsubscr...@hadoop.apache.org
For additional commands, e-mail: common-commits-h...@hadoop.apache.org

Reply via email to