commit 5667a27dd614687cad3f1f793691d54483d61ca2 Author: Eric Yang Date: Fri Jun 29 14:54:39 2018 -0400 YARN-8474. Fixed ApiServiceClient Kerberos challenge header. Contributed by Eric Yang 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 ab76218..56513a8 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 @@ -155,6 +155,17 @@ curator-test test + + org.apache.hadoop + hadoop-minikdc + test + + + org.apache.hadoop + hadoop-auth + test-jar + 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 9232fc8..24360b5 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,6 +20,9 @@ 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; @@ -27,12 +30,16 @@ import javax.ws.rs.core.MediaType; import com.google.common.base.Preconditions; + +import org.apache.commons.codec.binary.Base64; 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; @@ -51,8 +58,14 @@ import org.apache.hadoop.yarn.service.utils.JsonSerDeser; import org.apache.hadoop.yarn.service.utils.ServiceApiUtil; import org.apache.hadoop.yarn.util.RMHAUtils; +import org.apache.http.HttpHeaders; import org.codehaus.jackson.map.PropertyNamingStrategy; 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 +84,7 @@ public class ApiServiceClient extends AppAdminClient { private static final Logger LOG = LoggerFactory.getLogger(ApiServiceClient.class); + private static final Base64 base64codec = new Base64(0); protected YarnClient yarnClient; @Override protected void serviceInit(Configuration configuration) @@ -81,6 +95,46 @@ } /** + * 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(base64codec.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. */ private String getRMWebAddress() { @@ -116,8 +170,10 @@ private String getRMWebAddress() { 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) { @@ -206,8 +262,13 @@ private Builder getApiClient(String requestPath) 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 0000000..d1583ff --- /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,82 @@ +/** + * 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); + } + +}