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);
+ }
+
+}