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.Constants.THRIFT_SUPPORT_PROXYUSER_KEY;
022import static org.junit.Assert.assertFalse;
023import static org.junit.Assert.assertNotNull;
024import static org.junit.Assert.assertTrue;
025
026import java.io.File;
027import java.security.Principal;
028import java.security.PrivilegedExceptionAction;
029import java.util.Set;
030
031import javax.security.auth.Subject;
032import javax.security.auth.kerberos.KerberosTicket;
033
034import org.apache.commons.io.FileUtils;
035import org.apache.hadoop.conf.Configuration;
036import org.apache.hadoop.hbase.HBaseClassTestRule;
037import org.apache.hadoop.hbase.HBaseTestingUtility;
038import org.apache.hadoop.hbase.HConstants;
039import org.apache.hadoop.hbase.security.HBaseKerberosUtils;
040import org.apache.hadoop.hbase.testclassification.ClientTests;
041import org.apache.hadoop.hbase.testclassification.LargeTests;
042import org.apache.hadoop.hbase.thrift.generated.Hbase;
043import org.apache.hadoop.hbase.util.TableDescriptorChecker;
044import org.apache.hadoop.security.authentication.util.KerberosName;
045import org.apache.http.HttpHeaders;
046import org.apache.http.auth.AuthSchemeProvider;
047import org.apache.http.auth.AuthScope;
048import org.apache.http.auth.KerberosCredentials;
049import org.apache.http.client.config.AuthSchemes;
050import org.apache.http.config.Lookup;
051import org.apache.http.config.RegistryBuilder;
052import org.apache.http.impl.auth.SPNegoSchemeFactory;
053import org.apache.http.impl.client.BasicCredentialsProvider;
054import org.apache.http.impl.client.CloseableHttpClient;
055import org.apache.http.impl.client.HttpClients;
056import org.apache.kerby.kerberos.kerb.KrbException;
057import org.apache.kerby.kerberos.kerb.client.JaasKrbUtil;
058import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer;
059import org.apache.thrift.protocol.TBinaryProtocol;
060import org.apache.thrift.protocol.TProtocol;
061import org.apache.thrift.transport.THttpClient;
062import org.ietf.jgss.GSSCredential;
063import org.ietf.jgss.GSSManager;
064import org.ietf.jgss.GSSName;
065import org.ietf.jgss.Oid;
066import org.junit.AfterClass;
067import org.junit.BeforeClass;
068import org.junit.ClassRule;
069import org.junit.experimental.categories.Category;
070import org.slf4j.Logger;
071import org.slf4j.LoggerFactory;
072
073/**
074 * Start the HBase Thrift HTTP server on a random port through the command-line
075 * interface and talk to it from client side with SPNEGO security enabled.
076 */
077@Category({ClientTests.class, LargeTests.class})
078public class TestThriftSpnegoHttpServer extends TestThriftHttpServer {
079  @ClassRule
080  public static final HBaseClassTestRule CLASS_RULE =
081    HBaseClassTestRule.forClass(TestThriftSpnegoHttpServer.class);
082
083  private static final Logger LOG =
084    LoggerFactory.getLogger(TestThriftSpnegoHttpServer.class);
085
086  private static SimpleKdcServer kdc;
087  private static File serverKeytab;
088  private static File spnegoServerKeytab;
089  private static File clientKeytab;
090
091  private static String clientPrincipal;
092  private static String serverPrincipal;
093  private static String spnegoServerPrincipal;
094
095  private static void setupUser(SimpleKdcServer kdc, File keytab, String principal)
096      throws KrbException {
097    kdc.createPrincipal(principal);
098    kdc.exportPrincipal(principal, keytab);
099  }
100
101  private static SimpleKdcServer buildMiniKdc() throws Exception {
102    SimpleKdcServer kdc = new SimpleKdcServer();
103
104    final File target = new File(System.getProperty("user.dir"), "target");
105    File kdcDir = new File(target, TestThriftSpnegoHttpServer.class.getSimpleName());
106    if (kdcDir.exists()) {
107      FileUtils.deleteDirectory(kdcDir);
108    }
109    kdcDir.mkdirs();
110    kdc.setWorkDir(kdcDir);
111
112    kdc.setKdcHost(HConstants.LOCALHOST);
113    int kdcPort = HBaseTestingUtility.randomFreePort();
114    kdc.setAllowTcp(true);
115    kdc.setAllowUdp(false);
116    kdc.setKdcTcpPort(kdcPort);
117
118    LOG.info("Starting KDC server at " + HConstants.LOCALHOST + ":" + kdcPort);
119
120    kdc.init();
121
122    return kdc;
123  }
124
125  private static void addSecurityConfigurations(Configuration conf) {
126    KerberosName.setRules("DEFAULT");
127
128    HBaseKerberosUtils.setKeytabFileForTesting(serverKeytab.getAbsolutePath());
129    HBaseKerberosUtils.setSecuredConfiguration(conf, serverPrincipal, spnegoServerPrincipal);
130
131    conf.setBoolean(THRIFT_SUPPORT_PROXYUSER_KEY, true);
132    conf.setBoolean(Constants.USE_HTTP_CONF_KEY, true);
133    conf.set("hadoop.proxyuser.hbase.hosts", "*");
134    conf.set("hadoop.proxyuser.hbase.groups", "*");
135
136    conf.set(Constants.THRIFT_KERBEROS_PRINCIPAL_KEY, serverPrincipal);
137    conf.set(Constants.THRIFT_KEYTAB_FILE_KEY, serverKeytab.getAbsolutePath());
138    conf.set(Constants.THRIFT_SPNEGO_PRINCIPAL_KEY, spnegoServerPrincipal);
139    conf.set(Constants.THRIFT_SPNEGO_KEYTAB_FILE_KEY, spnegoServerKeytab.getAbsolutePath());
140  }
141
142  @BeforeClass
143  public static void setUpBeforeClass() throws Exception {
144    final File target = new File(System.getProperty("user.dir"), "target");
145    assertTrue(target.exists());
146
147    File keytabDir = new File(target, TestThriftSpnegoHttpServer.class.getSimpleName() +
148      "_keytabs");
149    if (keytabDir.exists()) {
150      FileUtils.deleteDirectory(keytabDir);
151    }
152    keytabDir.mkdirs();
153
154    kdc = buildMiniKdc();
155    kdc.start();
156
157    clientPrincipal = "client@" + kdc.getKdcConfig().getKdcRealm();
158    clientKeytab = new File(keytabDir, clientPrincipal + ".keytab");
159    setupUser(kdc, clientKeytab, clientPrincipal);
160
161    serverPrincipal = "hbase/" + HConstants.LOCALHOST + "@" + kdc.getKdcConfig().getKdcRealm();
162    serverKeytab = new File(keytabDir, serverPrincipal.replace('/', '_') + ".keytab");
163    setupUser(kdc, serverKeytab, serverPrincipal);
164
165    spnegoServerPrincipal = "HTTP/" + HConstants.LOCALHOST + "@" + kdc.getKdcConfig().getKdcRealm();
166    spnegoServerKeytab = new File(keytabDir, spnegoServerPrincipal.replace('/', '_') + ".keytab");
167    setupUser(kdc, spnegoServerKeytab, spnegoServerPrincipal);
168
169    TEST_UTIL.getConfiguration().setBoolean(Constants.USE_HTTP_CONF_KEY, true);
170    TEST_UTIL.getConfiguration().setBoolean(TableDescriptorChecker.TABLE_SANITY_CHECKS, false);
171    addSecurityConfigurations(TEST_UTIL.getConfiguration());
172
173    TestThriftHttpServer.setUpBeforeClass();
174  }
175
176  @AfterClass
177  public static void tearDownAfterClass() throws Exception {
178    TestThriftHttpServer.tearDownAfterClass();
179
180    try {
181      if (null != kdc) {
182        kdc.stop();
183      }
184    } catch (Exception e) {
185      LOG.info("Failed to stop mini KDC", e);
186    }
187  }
188
189  @Override
190  protected void talkToThriftServer(String url, int customHeaderSize) throws Exception {
191    // Close httpClient and THttpClient automatically on any failures
192    try (
193        CloseableHttpClient httpClient = createHttpClient();
194        THttpClient tHttpClient = new THttpClient(url, httpClient)
195    ) {
196      tHttpClient.open();
197      if (customHeaderSize > 0) {
198        StringBuilder sb = new StringBuilder();
199        for (int i = 0; i < customHeaderSize; i++) {
200          sb.append("a");
201        }
202        tHttpClient.setCustomHeader(HttpHeaders.USER_AGENT, sb.toString());
203      }
204
205      TProtocol prot = new TBinaryProtocol(tHttpClient);
206      Hbase.Client client = new Hbase.Client(prot);
207      if (!tableCreated) {
208        TestThriftServer.createTestTables(client);
209        tableCreated = true;
210      }
211      TestThriftServer.checkTableList(client);
212    }
213  }
214
215  private CloseableHttpClient createHttpClient() throws Exception {
216    final Subject clientSubject = JaasKrbUtil.loginUsingKeytab(clientPrincipal, clientKeytab);
217    final Set<Principal> clientPrincipals = clientSubject.getPrincipals();
218    // Make sure the subject has a principal
219    assertFalse(clientPrincipals.isEmpty());
220
221    // Get a TGT for the subject (might have many, different encryption types). The first should
222    // be the default encryption type.
223    Set<KerberosTicket> privateCredentials =
224        clientSubject.getPrivateCredentials(KerberosTicket.class);
225    assertFalse(privateCredentials.isEmpty());
226    KerberosTicket tgt = privateCredentials.iterator().next();
227    assertNotNull(tgt);
228
229    // The name of the principal
230    final String clientPrincipalName = clientPrincipals.iterator().next().getName();
231
232    return Subject.doAs(clientSubject, new PrivilegedExceptionAction<CloseableHttpClient>() {
233      @Override
234      public CloseableHttpClient run() throws Exception {
235        // Logs in with Kerberos via GSS
236        GSSManager gssManager = GSSManager.getInstance();
237        // jGSS Kerberos login constant
238        Oid oid = new Oid("1.2.840.113554.1.2.2");
239        GSSName gssClient = gssManager.createName(clientPrincipalName, GSSName.NT_USER_NAME);
240        GSSCredential credential = gssManager.createCredential(gssClient,
241            GSSCredential.DEFAULT_LIFETIME, oid, GSSCredential.INITIATE_ONLY);
242
243        Lookup<AuthSchemeProvider> authRegistry = RegistryBuilder.<AuthSchemeProvider>create()
244            .register(AuthSchemes.SPNEGO, new SPNegoSchemeFactory(true, true))
245            .build();
246
247        BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
248        credentialsProvider.setCredentials(AuthScope.ANY, new KerberosCredentials(credential));
249
250        return HttpClients.custom()
251            .setDefaultAuthSchemeRegistry(authRegistry)
252            .setDefaultCredentialsProvider(credentialsProvider)
253            .build();
254      }
255    });
256  }
257}