001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.http;
019
020import java.io.File;
021import java.io.IOException;
022import java.net.HttpURLConnection;
023import java.net.URL;
024import java.security.Principal;
025import java.security.PrivilegedExceptionAction;
026import java.util.Set;
027import javax.security.auth.Subject;
028import javax.security.auth.kerberos.KerberosTicket;
029import org.apache.hadoop.conf.Configuration;
030import org.apache.hadoop.hbase.HBaseClassTestRule;
031import org.apache.hadoop.hbase.http.TestHttpServer.EchoServlet;
032import org.apache.hadoop.hbase.http.resource.JerseyResource;
033import org.apache.hadoop.hbase.testclassification.MiscTests;
034import org.apache.hadoop.hbase.testclassification.SmallTests;
035import org.apache.hadoop.security.authentication.util.KerberosName;
036import org.apache.http.HttpHost;
037import org.apache.http.HttpResponse;
038import org.apache.http.auth.AuthSchemeProvider;
039import org.apache.http.auth.AuthScope;
040import org.apache.http.auth.KerberosCredentials;
041import org.apache.http.client.HttpClient;
042import org.apache.http.client.config.AuthSchemes;
043import org.apache.http.client.methods.HttpGet;
044import org.apache.http.client.protocol.HttpClientContext;
045import org.apache.http.config.Lookup;
046import org.apache.http.config.RegistryBuilder;
047import org.apache.http.impl.auth.SPNegoSchemeFactory;
048import org.apache.http.impl.client.BasicCredentialsProvider;
049import org.apache.http.impl.client.HttpClients;
050import org.apache.http.util.EntityUtils;
051import org.apache.kerby.kerberos.kerb.KrbException;
052import org.apache.kerby.kerberos.kerb.client.JaasKrbUtil;
053import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer;
054import org.ietf.jgss.GSSCredential;
055import org.ietf.jgss.GSSManager;
056import org.ietf.jgss.GSSName;
057import org.ietf.jgss.Oid;
058import org.junit.AfterClass;
059import org.junit.BeforeClass;
060import org.junit.ClassRule;
061import org.junit.Test;
062import org.junit.experimental.categories.Category;
063import org.slf4j.Logger;
064import org.slf4j.LoggerFactory;
065
066/**
067 * Test class for SPNEGO authentication on the HttpServer. Uses Kerby's MiniKDC and Apache
068 * HttpComponents to verify that a simple Servlet is reachable via SPNEGO and unreachable w/o.
069 */
070@Category({MiscTests.class, SmallTests.class})
071public class TestSpnegoHttpServer extends HttpServerFunctionalTest {
072
073  @ClassRule
074  public static final HBaseClassTestRule CLASS_RULE =
075      HBaseClassTestRule.forClass(TestSpnegoHttpServer.class);
076
077  private static final Logger LOG = LoggerFactory.getLogger(TestSpnegoHttpServer.class);
078  private static final String KDC_SERVER_HOST = "localhost";
079  private static final String CLIENT_PRINCIPAL = "client";
080
081  private static HttpServer server;
082  private static URL baseUrl;
083  private static SimpleKdcServer kdc;
084  private static File infoServerKeytab;
085  private static File clientKeytab;
086
087  @BeforeClass
088  public static void setupServer() throws Exception {
089    final String serverPrincipal = "HTTP/" + KDC_SERVER_HOST;
090    final File target = new File(System.getProperty("user.dir"), "target");
091    assertTrue(target.exists());
092
093    kdc = buildMiniKdc();
094    kdc.start();
095
096    File keytabDir = new File(target, TestSpnegoHttpServer.class.getSimpleName()
097        + "_keytabs");
098    if (keytabDir.exists()) {
099      deleteRecursively(keytabDir);
100    }
101    keytabDir.mkdirs();
102
103    infoServerKeytab = new File(keytabDir, serverPrincipal.replace('/', '_') + ".keytab");
104    clientKeytab = new File(keytabDir, CLIENT_PRINCIPAL + ".keytab");
105
106    setupUser(kdc, clientKeytab, CLIENT_PRINCIPAL);
107    setupUser(kdc, infoServerKeytab, serverPrincipal);
108
109    Configuration conf = buildSpnegoConfiguration(serverPrincipal, infoServerKeytab);
110
111    server = createTestServerWithSecurity(conf);
112    server.addServlet("echo", "/echo", EchoServlet.class);
113    server.addJerseyResourcePackage(JerseyResource.class.getPackage().getName(), "/jersey/*");
114    server.start();
115    baseUrl = getServerURL(server);
116
117    LOG.info("HTTP server started: "+ baseUrl);
118  }
119
120  @AfterClass
121  public static void stopServer() throws Exception {
122    try {
123      if (null != server) {
124        server.stop();
125      }
126    } catch (Exception e) {
127      LOG.info("Failed to stop info server", e);
128    }
129    try {
130      if (null != kdc) {
131        kdc.stop();
132      }
133    } catch (Exception e) {
134      LOG.info("Failed to stop mini KDC", e);
135    }
136  }
137
138  private static void setupUser(SimpleKdcServer kdc, File keytab, String principal)
139      throws KrbException {
140    kdc.createPrincipal(principal);
141    kdc.exportPrincipal(principal, keytab);
142  }
143
144  private static SimpleKdcServer buildMiniKdc() throws Exception {
145    SimpleKdcServer kdc = new SimpleKdcServer();
146
147    final File target = new File(System.getProperty("user.dir"), "target");
148    File kdcDir = new File(target, TestSpnegoHttpServer.class.getSimpleName());
149    if (kdcDir.exists()) {
150      deleteRecursively(kdcDir);
151    }
152    kdcDir.mkdirs();
153    kdc.setWorkDir(kdcDir);
154
155    kdc.setKdcHost(KDC_SERVER_HOST);
156    int kdcPort = getFreePort();
157    kdc.setAllowTcp(true);
158    kdc.setAllowUdp(false);
159    kdc.setKdcTcpPort(kdcPort);
160
161    LOG.info("Starting KDC server at " + KDC_SERVER_HOST + ":" + kdcPort);
162
163    kdc.init();
164
165    return kdc;
166  }
167
168  private static Configuration buildSpnegoConfiguration(String serverPrincipal, File
169      serverKeytab) {
170    Configuration conf = new Configuration();
171    KerberosName.setRules("DEFAULT");
172
173    conf.setInt(HttpServer.HTTP_MAX_THREADS, TestHttpServer.MAX_THREADS);
174
175    // Enable Kerberos (pre-req)
176    conf.set("hbase.security.authentication", "kerberos");
177    conf.set(HttpServer.HTTP_UI_AUTHENTICATION, "kerberos");
178    conf.set(HttpServer.HTTP_SPNEGO_AUTHENTICATION_PRINCIPAL_KEY, serverPrincipal);
179    conf.set(HttpServer.HTTP_SPNEGO_AUTHENTICATION_KEYTAB_KEY, serverKeytab.getAbsolutePath());
180
181    return conf;
182  }
183
184  @Test
185  public void testUnauthorizedClientsDisallowed() throws IOException {
186    URL url = new URL(getServerURL(server), "/echo?a=b");
187    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
188    assertEquals(HttpURLConnection.HTTP_UNAUTHORIZED, conn.getResponseCode());
189  }
190
191  @Test
192  public void testAllowedClient() throws Exception {
193    // Create the subject for the client
194    final Subject clientSubject = JaasKrbUtil.loginUsingKeytab(CLIENT_PRINCIPAL, clientKeytab);
195    final Set<Principal> clientPrincipals = clientSubject.getPrincipals();
196    // Make sure the subject has a principal
197    assertFalse(clientPrincipals.isEmpty());
198
199    // Get a TGT for the subject (might have many, different encryption types). The first should
200    // be the default encryption type.
201    Set<KerberosTicket> privateCredentials =
202            clientSubject.getPrivateCredentials(KerberosTicket.class);
203    assertFalse(privateCredentials.isEmpty());
204    KerberosTicket tgt = privateCredentials.iterator().next();
205    assertNotNull(tgt);
206
207    // The name of the principal
208    final String principalName = clientPrincipals.iterator().next().getName();
209
210    // Run this code, logged in as the subject (the client)
211    HttpResponse resp = Subject.doAs(clientSubject,
212        new PrivilegedExceptionAction<HttpResponse>() {
213      @Override
214      public HttpResponse run() throws Exception {
215        // Logs in with Kerberos via GSS
216        GSSManager gssManager = GSSManager.getInstance();
217        // jGSS Kerberos login constant
218        Oid oid = new Oid("1.2.840.113554.1.2.2");
219        GSSName gssClient = gssManager.createName(principalName, GSSName.NT_USER_NAME);
220        GSSCredential credential = gssManager.createCredential(gssClient,
221            GSSCredential.DEFAULT_LIFETIME, oid, GSSCredential.INITIATE_ONLY);
222
223        HttpClientContext context = HttpClientContext.create();
224        Lookup<AuthSchemeProvider> authRegistry = RegistryBuilder.<AuthSchemeProvider>create()
225            .register(AuthSchemes.SPNEGO, new SPNegoSchemeFactory(true, true))
226            .build();
227
228        HttpClient client = HttpClients.custom().setDefaultAuthSchemeRegistry(authRegistry).build();
229        BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
230        credentialsProvider.setCredentials(AuthScope.ANY, new KerberosCredentials(credential));
231
232        URL url = new URL(getServerURL(server), "/echo?a=b");
233        context.setTargetHost(new HttpHost(url.getHost(), url.getPort()));
234        context.setCredentialsProvider(credentialsProvider);
235        context.setAuthSchemeRegistry(authRegistry);
236
237        HttpGet get = new HttpGet(url.toURI());
238        return client.execute(get, context);
239      }
240    });
241
242    assertNotNull(resp);
243    assertEquals(HttpURLConnection.HTTP_OK, resp.getStatusLine().getStatusCode());
244    assertEquals("a:b", EntityUtils.toString(resp.getEntity()).trim());
245  }
246
247  @Test(expected = IllegalArgumentException.class)
248  public void testMissingConfigurationThrowsException() throws Exception {
249    Configuration conf = new Configuration();
250    conf.setInt(HttpServer.HTTP_MAX_THREADS, TestHttpServer.MAX_THREADS);
251    // Enable Kerberos (pre-req)
252    conf.set("hbase.security.authentication", "kerberos");
253    // Intentionally skip keytab and principal
254
255    HttpServer customServer = createTestServerWithSecurity(conf);
256    customServer.addServlet("echo", "/echo", EchoServlet.class);
257    customServer.addJerseyResourcePackage(JerseyResource.class.getPackage().getName(), "/jersey/*");
258    customServer.start();
259  }
260}