001/*
002 * Copyright The Apache Software Foundation
003 *
004 * Licensed to the Apache Software Foundation (ASF) under one or more
005 * contributor license agreements. See the NOTICE file distributed with this
006 * work for additional information regarding copyright ownership. The ASF
007 * licenses this file to you under the Apache License, Version 2.0 (the
008 * "License"); you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 * http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
015 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
016 * License for the specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.hadoop.hbase.thrift;
020
021import static org.apache.hadoop.hbase.thrift.ThriftServerRunner.THRIFT_KERBEROS_PRINCIPAL_KEY;
022import static org.apache.hadoop.hbase.thrift.ThriftServerRunner.THRIFT_KEYTAB_FILE_KEY;
023import static org.apache.hadoop.hbase.thrift.ThriftServerRunner.THRIFT_SPNEGO_KEYTAB_FILE_KEY;
024import static org.apache.hadoop.hbase.thrift.ThriftServerRunner.THRIFT_SPNEGO_PRINCIPAL_KEY;
025import static org.apache.hadoop.hbase.thrift.ThriftServerRunner.THRIFT_SUPPORT_PROXYUSER_KEY;
026import static org.apache.hadoop.hbase.thrift.ThriftServerRunner.USE_HTTP_CONF_KEY;
027import static org.junit.Assert.assertFalse;
028import static org.junit.Assert.assertNotNull;
029import static org.junit.Assert.assertTrue;
030
031import java.io.File;
032import java.security.Principal;
033import java.security.PrivilegedExceptionAction;
034import java.util.Set;
035
036import javax.security.auth.Subject;
037import javax.security.auth.kerberos.KerberosTicket;
038
039import org.apache.commons.io.FileUtils;
040import org.apache.hadoop.conf.Configuration;
041import org.apache.hadoop.hbase.HBaseClassTestRule;
042import org.apache.hadoop.hbase.HBaseTestingUtility;
043import org.apache.hadoop.hbase.HConstants;
044import org.apache.hadoop.hbase.security.HBaseKerberosUtils;
045import org.apache.hadoop.hbase.testclassification.ClientTests;
046import org.apache.hadoop.hbase.testclassification.LargeTests;
047import org.apache.hadoop.hbase.thrift.generated.Hbase;
048import org.apache.hadoop.hdfs.DFSConfigKeys;
049import org.apache.hadoop.security.authentication.util.KerberosName;
050import org.apache.http.HttpHeaders;
051import org.apache.http.auth.AuthSchemeProvider;
052import org.apache.http.auth.AuthScope;
053import org.apache.http.auth.KerberosCredentials;
054import org.apache.http.client.config.AuthSchemes;
055import org.apache.http.config.Lookup;
056import org.apache.http.config.RegistryBuilder;
057import org.apache.http.impl.auth.SPNegoSchemeFactory;
058import org.apache.http.impl.client.BasicCredentialsProvider;
059import org.apache.http.impl.client.CloseableHttpClient;
060import org.apache.http.impl.client.HttpClients;
061import org.apache.kerby.kerberos.kerb.KrbException;
062import org.apache.kerby.kerberos.kerb.client.JaasKrbUtil;
063import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer;
064import org.apache.thrift.protocol.TBinaryProtocol;
065import org.apache.thrift.protocol.TProtocol;
066import org.apache.thrift.transport.THttpClient;
067import org.ietf.jgss.GSSCredential;
068import org.ietf.jgss.GSSManager;
069import org.ietf.jgss.GSSName;
070import org.ietf.jgss.Oid;
071import org.junit.AfterClass;
072import org.junit.BeforeClass;
073import org.junit.ClassRule;
074import org.junit.experimental.categories.Category;
075import org.slf4j.Logger;
076import org.slf4j.LoggerFactory;
077
078/**
079 * Start the HBase Thrift HTTP server on a random port through the command-line
080 * interface and talk to it from client side with SPNEGO security enabled.
081 */
082@Category({ClientTests.class, LargeTests.class})
083public class TestThriftSpnegoHttpServer extends TestThriftHttpServer {
084  @ClassRule
085  public static final HBaseClassTestRule CLASS_RULE =
086    HBaseClassTestRule.forClass(TestThriftSpnegoHttpServer.class);
087
088  private static final Logger LOG =
089    LoggerFactory.getLogger(TestThriftSpnegoHttpServer.class);
090
091  private static SimpleKdcServer kdc;
092  private static File serverKeytab;
093  private static File spnegoServerKeytab;
094  private static File clientKeytab;
095
096  private static String clientPrincipal;
097  private static String serverPrincipal;
098  private static String spnegoServerPrincipal;
099
100  private static void setupUser(SimpleKdcServer kdc, File keytab, String principal)
101      throws KrbException {
102    kdc.createPrincipal(principal);
103    kdc.exportPrincipal(principal, keytab);
104  }
105
106  private static SimpleKdcServer buildMiniKdc() throws Exception {
107    SimpleKdcServer kdc = new SimpleKdcServer();
108
109    final File target = new File(System.getProperty("user.dir"), "target");
110    File kdcDir = new File(target, TestThriftSpnegoHttpServer.class.getSimpleName());
111    if (kdcDir.exists()) {
112      FileUtils.deleteDirectory(kdcDir);
113    }
114    kdcDir.mkdirs();
115    kdc.setWorkDir(kdcDir);
116
117    kdc.setKdcHost(HConstants.LOCALHOST);
118    int kdcPort = HBaseTestingUtility.randomFreePort();
119    kdc.setAllowTcp(true);
120    kdc.setAllowUdp(false);
121    kdc.setKdcTcpPort(kdcPort);
122
123    LOG.info("Starting KDC server at " + HConstants.LOCALHOST + ":" + kdcPort);
124
125    kdc.init();
126
127    return kdc;
128  }
129
130  private static void addSecurityConfigurations(Configuration conf) {
131    KerberosName.setRules("DEFAULT");
132
133    HBaseKerberosUtils.setKeytabFileForTesting(serverKeytab.getAbsolutePath());
134    HBaseKerberosUtils.setPrincipalForTesting(serverPrincipal);
135    HBaseKerberosUtils.setSecuredConfiguration(conf);
136
137    // if we drop support for hadoop-2.4.0 and hadoop-2.4.1,
138    // the following key should be changed.
139    // 1) DFS_NAMENODE_USER_NAME_KEY -> DFS_NAMENODE_KERBEROS_PRINCIPAL_KEY
140    // 2) DFS_DATANODE_USER_NAME_KEY -> DFS_DATANODE_KERBEROS_PRINCIPAL_KEY
141    conf.set(DFSConfigKeys.DFS_NAMENODE_USER_NAME_KEY, serverPrincipal);
142    conf.set(DFSConfigKeys.DFS_NAMENODE_KEYTAB_FILE_KEY, serverKeytab.getAbsolutePath());
143    conf.set(DFSConfigKeys.DFS_DATANODE_USER_NAME_KEY, serverPrincipal);
144    conf.set(DFSConfigKeys.DFS_DATANODE_KEYTAB_FILE_KEY, serverKeytab.getAbsolutePath());
145
146    conf.setBoolean(DFSConfigKeys.DFS_BLOCK_ACCESS_TOKEN_ENABLE_KEY, true);
147
148    conf.set(DFSConfigKeys.DFS_WEB_AUTHENTICATION_KERBEROS_PRINCIPAL_KEY, spnegoServerPrincipal);
149    conf.set(DFSConfigKeys.DFS_WEB_AUTHENTICATION_KERBEROS_KEYTAB_KEY,
150        spnegoServerKeytab.getAbsolutePath());
151
152    conf.setBoolean("ignore.secure.ports.for.testing", true);
153
154    conf.setBoolean(THRIFT_SUPPORT_PROXYUSER_KEY, true);
155    conf.setBoolean(USE_HTTP_CONF_KEY, true);
156    conf.set("hadoop.proxyuser.hbase.hosts", "*");
157    conf.set("hadoop.proxyuser.hbase.groups", "*");
158
159    conf.set(THRIFT_KERBEROS_PRINCIPAL_KEY, serverPrincipal);
160    conf.set(THRIFT_KEYTAB_FILE_KEY, serverKeytab.getAbsolutePath());
161    conf.set(THRIFT_SPNEGO_PRINCIPAL_KEY, spnegoServerPrincipal);
162    conf.set(THRIFT_SPNEGO_KEYTAB_FILE_KEY, spnegoServerKeytab.getAbsolutePath());
163  }
164
165  @BeforeClass
166  public static void setUpBeforeClass() throws Exception {
167    final File target = new File(System.getProperty("user.dir"), "target");
168    assertTrue(target.exists());
169
170    File keytabDir = new File(target, TestThriftSpnegoHttpServer.class.getSimpleName() +
171      "_keytabs");
172    if (keytabDir.exists()) {
173      FileUtils.deleteDirectory(keytabDir);
174    }
175    keytabDir.mkdirs();
176
177    kdc = buildMiniKdc();
178    kdc.start();
179
180    clientPrincipal = "client@" + kdc.getKdcConfig().getKdcRealm();
181    clientKeytab = new File(keytabDir, clientPrincipal + ".keytab");
182    setupUser(kdc, clientKeytab, clientPrincipal);
183
184    serverPrincipal = "hbase/" + HConstants.LOCALHOST + "@" + kdc.getKdcConfig().getKdcRealm();
185    serverKeytab = new File(keytabDir, serverPrincipal.replace('/', '_') + ".keytab");
186    setupUser(kdc, serverKeytab, serverPrincipal);
187
188    spnegoServerPrincipal = "HTTP/" + HConstants.LOCALHOST + "@" + kdc.getKdcConfig().getKdcRealm();
189    spnegoServerKeytab = new File(keytabDir, spnegoServerPrincipal.replace('/', '_') + ".keytab");
190    setupUser(kdc, spnegoServerKeytab, spnegoServerPrincipal);
191
192    TEST_UTIL.getConfiguration().setBoolean(USE_HTTP_CONF_KEY, true);
193    TEST_UTIL.getConfiguration().setBoolean("hbase.table.sanity.checks", false);
194    addSecurityConfigurations(TEST_UTIL.getConfiguration());
195
196    TestThriftHttpServer.setUpBeforeClass();
197  }
198
199  @AfterClass
200  public static void tearDownAfterClass() throws Exception {
201    TestThriftHttpServer.tearDownAfterClass();
202
203    try {
204      if (null != kdc) {
205        kdc.stop();
206      }
207    } catch (Exception e) {
208      LOG.info("Failed to stop mini KDC", e);
209    }
210  }
211
212  @Override
213  void talkToThriftServer(String url, int customHeaderSize) throws Exception {
214    // Close httpClient and THttpClient automatically on any failures
215    try (
216        CloseableHttpClient httpClient = createHttpClient();
217        THttpClient tHttpClient = new THttpClient(url, httpClient)
218    ) {
219      tHttpClient.open();
220      if (customHeaderSize > 0) {
221        StringBuilder sb = new StringBuilder();
222        for (int i = 0; i < customHeaderSize; i++) {
223          sb.append("a");
224        }
225        tHttpClient.setCustomHeader(HttpHeaders.USER_AGENT, sb.toString());
226      }
227
228      TProtocol prot = new TBinaryProtocol(tHttpClient);
229      Hbase.Client client = new Hbase.Client(prot);
230      if (!tableCreated) {
231        TestThriftServer.createTestTables(client);
232        tableCreated = true;
233      }
234      TestThriftServer.checkTableList(client);
235    }
236  }
237
238  private CloseableHttpClient createHttpClient() throws Exception {
239    final Subject clientSubject = JaasKrbUtil.loginUsingKeytab(clientPrincipal, clientKeytab);
240    final Set<Principal> clientPrincipals = clientSubject.getPrincipals();
241    // Make sure the subject has a principal
242    assertFalse(clientPrincipals.isEmpty());
243
244    // Get a TGT for the subject (might have many, different encryption types). The first should
245    // be the default encryption type.
246    Set<KerberosTicket> privateCredentials =
247        clientSubject.getPrivateCredentials(KerberosTicket.class);
248    assertFalse(privateCredentials.isEmpty());
249    KerberosTicket tgt = privateCredentials.iterator().next();
250    assertNotNull(tgt);
251
252    // The name of the principal
253    final String clientPrincipalName = clientPrincipals.iterator().next().getName();
254
255    return Subject.doAs(clientSubject, new PrivilegedExceptionAction<CloseableHttpClient>() {
256      @Override
257      public CloseableHttpClient run() throws Exception {
258        // Logs in with Kerberos via GSS
259        GSSManager gssManager = GSSManager.getInstance();
260        // jGSS Kerberos login constant
261        Oid oid = new Oid("1.2.840.113554.1.2.2");
262        GSSName gssClient = gssManager.createName(clientPrincipalName, GSSName.NT_USER_NAME);
263        GSSCredential credential = gssManager.createCredential(gssClient,
264            GSSCredential.DEFAULT_LIFETIME, oid, GSSCredential.INITIATE_ONLY);
265
266        Lookup<AuthSchemeProvider> authRegistry = RegistryBuilder.<AuthSchemeProvider>create()
267            .register(AuthSchemes.SPNEGO, new SPNegoSchemeFactory(true, true))
268            .build();
269
270        BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
271        credentialsProvider.setCredentials(AuthScope.ANY, new KerberosCredentials(credential));
272
273        return HttpClients.custom()
274            .setDefaultAuthSchemeRegistry(authRegistry)
275            .setDefaultCredentialsProvider(credentialsProvider)
276            .build();
277      }
278    });
279  }
280}