From 8990eaf5925afa533fbd9c3641859a146dc5a22c Mon Sep 17 00:00:00 2001 From: Eric Yang Date: Thu, 16 Aug 2018 12:46:37 -0400 Subject: [PATCH] YARN-8474. Fixed ApiServiceClient kerberos negotiation. Contributed by Billie Rinaldi --- .../hadoop-yarn-services-api/pom.xml | 57 +++++++++++++ .../yarn/service/client/ApiServiceClient.java | 83 +++++++++++++++++-- .../client/TestSecureApiServiceClient.java | 83 +++++++++++++++++++ 3 files changed, 217 insertions(+), 6 deletions(-) create mode 100644 hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/src/test/java/org/apache/hadoop/yarn/service/client/TestSecureApiServiceClient.java diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/pom.xml b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/pom.xml index ab762187830..7386e4158e9 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/pom.xml +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/pom.xml @@ -91,10 +91,18 @@ org.apache.hadoop hadoop-yarn-api + + org.apache.hadoop + hadoop-yarn-client + org.apache.hadoop hadoop-yarn-common + + org.apache.hadoop + hadoop-yarn-registry + org.apache.hadoop hadoop-yarn-server-common @@ -103,6 +111,14 @@ org.apache.hadoop hadoop-common + + org.apache.hadoop + hadoop-annotations + + + org.apache.hadoop + hadoop-auth + org.slf4j slf4j-api @@ -119,6 +135,42 @@ javax.ws.rs jsr311-api + + javax.servlet + javax.servlet-api + + + commons-codec + commons-codec + + + commons-io + commons-io + + + org.apache.commons + commons-lang3 + + + com.google.guava + guava + + + com.sun.jersey + jersey-client + + + org.eclipse.jetty + jetty-server + + + org.eclipse.jetty + jetty-util + + + org.eclipse.jetty + jetty-servlet + org.mockito mockito-all @@ -155,6 +207,11 @@ curator-test test + + org.apache.hadoop + hadoop-minikdc + test + diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/src/main/java/org/apache/hadoop/yarn/service/client/ApiServiceClient.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/src/main/java/org/apache/hadoop/yarn/service/client/ApiServiceClient.java index f5162e9102f..92294468105 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/src/main/java/org/apache/hadoop/yarn/service/client/ApiServiceClient.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/src/main/java/org/apache/hadoop/yarn/service/client/ApiServiceClient.java @@ -20,21 +20,28 @@ import static org.apache.hadoop.yarn.service.utils.ServiceApiUtil.jsonSerDeser; import java.io.File; import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.PrivilegedExceptionAction; import java.text.MessageFormat; import java.util.List; import java.util.Map; +import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.UriBuilder; import com.google.common.base.Preconditions; + +import org.apache.commons.codec.binary.Base64; import com.google.common.base.Strings; import org.apache.commons.lang3.StringUtils; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.security.UserGroupInformation; -import org.apache.hadoop.security.authentication.client.AuthenticatedURL; +import org.apache.hadoop.security.authentication.client.AuthenticationException; +import org.apache.hadoop.security.authentication.util.KerberosUtil; import org.apache.hadoop.yarn.api.ApplicationConstants; import org.apache.hadoop.yarn.api.records.ApplicationId; import org.apache.hadoop.yarn.api.records.ApplicationReport; @@ -53,6 +60,11 @@ import org.apache.hadoop.yarn.service.conf.RestApiConstants; import org.apache.hadoop.yarn.service.utils.ServiceApiUtil; import org.apache.hadoop.yarn.util.RMHAUtils; import org.eclipse.jetty.util.UrlEncoded; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.GSSName; +import org.ietf.jgss.Oid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -71,6 +83,7 @@ import static org.apache.hadoop.yarn.service.exceptions.LauncherExitCodes.*; public class ApiServiceClient extends AppAdminClient { private static final Logger LOG = LoggerFactory.getLogger(ApiServiceClient.class); + private static final Base64 BASE_64_CODEC = new Base64(0); protected YarnClient yarnClient; @Override protected void serviceInit(Configuration configuration) @@ -80,6 +93,54 @@ public class ApiServiceClient extends AppAdminClient { super.serviceInit(configuration); } + /** + * Generate SPNEGO challenge request token. + * + * @param server - hostname to contact + * @throws IOException + * @throws InterruptedException + */ + String generateToken(String server) throws IOException, InterruptedException { + UserGroupInformation currentUser = UserGroupInformation.getCurrentUser(); + LOG.debug("The user credential is {}", currentUser); + String challenge = currentUser + .doAs(new PrivilegedExceptionAction() { + @Override + public String run() throws Exception { + try { + // This Oid for Kerberos GSS-API mechanism. + Oid mechOid = KerberosUtil.getOidInstance("GSS_KRB5_MECH_OID"); + GSSManager manager = GSSManager.getInstance(); + // GSS name for server + GSSName serverName = manager.createName("HTTP@" + server, + GSSName.NT_HOSTBASED_SERVICE); + // Create a GSSContext for authentication with the service. + // We're passing client credentials as null since we want them to + // be read from the Subject. + GSSContext gssContext = manager.createContext( + serverName.canonicalize(mechOid), mechOid, null, + GSSContext.DEFAULT_LIFETIME); + gssContext.requestMutualAuth(true); + gssContext.requestCredDeleg(true); + // Establish context + byte[] inToken = new byte[0]; + byte[] outToken = gssContext.initSecContext(inToken, 0, + inToken.length); + gssContext.dispose(); + // Base64 encoded and stringified token for server + LOG.debug("Got valid challenge for host {}", serverName); + return new String(BASE_64_CODEC.encode(outToken), + StandardCharsets.US_ASCII); + } catch (GSSException | IllegalAccessException + | NoSuchFieldException | ClassNotFoundException e) { + LOG.error("Error: {}", e); + throw new AuthenticationException(e); + } + } + }); + return challenge; + } + /** * Calculate Resource Manager address base on working REST API. */ @@ -100,6 +161,7 @@ public class ApiServiceClient extends AppAdminClient { for (String host : rmServers) { try { Client client = Client.create(); + client.setFollowRedirects(false); StringBuilder sb = new StringBuilder(); sb.append(scheme); sb.append(host); @@ -116,8 +178,11 @@ public class ApiServiceClient extends AppAdminClient { WebResource webResource = client .resource(sb.toString()); if (useKerberos) { - AuthenticatedURL.Token token = new AuthenticatedURL.Token(); - webResource.header("WWW-Authenticate", token); + String[] server = host.split(":"); + String challenge = generateToken(server[0]); + webResource.header(HttpHeaders.AUTHORIZATION, "Negotiate " + + challenge); + LOG.debug("Authorization: Negotiate {}", challenge); } ClientResponse test = webResource.get(ClientResponse.class); if (test.getStatus() == 200) { @@ -125,7 +190,8 @@ public class ApiServiceClient extends AppAdminClient { break; } } catch (Exception e) { - LOG.debug("Fail to connect to: "+host, e); + LOG.info("Fail to connect to: "+host); + LOG.debug("Root cause: {}", e); } } return scheme+rmAddress; @@ -218,8 +284,13 @@ public class ApiServiceClient extends AppAdminClient { Builder builder = client .resource(requestPath).type(MediaType.APPLICATION_JSON); if (conf.get("hadoop.http.authentication.type").equals("kerberos")) { - AuthenticatedURL.Token token = new AuthenticatedURL.Token(); - builder.header("WWW-Authenticate", token); + try { + URI url = new URI(requestPath); + String challenge = generateToken(url.getHost()); + builder.header(HttpHeaders.AUTHORIZATION, "Negotiate " + challenge); + } catch (Exception e) { + throw new IOException(e); + } } return builder .accept("application/json;charset=utf-8"); diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/src/test/java/org/apache/hadoop/yarn/service/client/TestSecureApiServiceClient.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/src/test/java/org/apache/hadoop/yarn/service/client/TestSecureApiServiceClient.java new file mode 100644 index 00000000000..4f3b46189fa --- /dev/null +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/src/test/java/org/apache/hadoop/yarn/service/client/TestSecureApiServiceClient.java @@ -0,0 +1,83 @@ +/** + * 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.yarn.service.client; + +import static org.junit.Assert.*; + +import java.io.File; + +import javax.security.sasl.Sasl; + +import java.util.Map; +import java.util.HashMap; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.minikdc.KerberosSecurityTestcase; +import org.apache.hadoop.security.SecurityUtil; +import org.apache.hadoop.security.UserGroupInformation; +import org.apache.hadoop.security.SaslRpcServer.QualityOfProtection; +import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod; +import org.junit.Before; +import org.junit.Test; + +/** + * Test Spnego Client Login. + */ +public class TestSecureApiServiceClient extends KerberosSecurityTestcase { + + private String clientPrincipal = "client"; + + private String server1Protocol = "HTTP"; + + 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 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(); + props.put(Sasl.QOP, QualityOfProtection.AUTHENTICATION.saslQop); + } + + @Test + public void testHttpSpnegoChallenge() throws Exception { + UserGroupInformation.loginUserFromKeytab(clientPrincipal, keytabFile + .getCanonicalPath()); + ApiServiceClient asc = new ApiServiceClient(); + String challenge = asc.generateToken("localhost"); + assertNotNull(challenge); + } + +}