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 static org.junit.jupiter.api.Assertions.assertEquals;
021import static org.junit.jupiter.api.Assertions.assertFalse;
022import static org.junit.jupiter.api.Assertions.assertNotNull;
023import static org.junit.jupiter.api.Assertions.assertThrows;
024
025import java.io.File;
026import java.io.IOException;
027import java.net.HttpURLConnection;
028import java.net.URL;
029import java.security.Principal;
030import java.security.PrivilegedExceptionAction;
031import java.util.Set;
032import javax.security.auth.Subject;
033import javax.security.auth.kerberos.KerberosTicket;
034import org.apache.hadoop.conf.Configuration;
035import org.apache.hadoop.hbase.HBaseCommonTestingUtil;
036import org.apache.hadoop.hbase.http.TestHttpServer.EchoServlet;
037import org.apache.hadoop.hbase.http.resource.JerseyResource;
038import org.apache.hadoop.hbase.testclassification.MiscTests;
039import org.apache.hadoop.hbase.testclassification.SmallTests;
040import org.apache.hadoop.hbase.util.SimpleKdcServerUtil;
041import org.apache.hadoop.security.authentication.util.KerberosName;
042import org.apache.http.HttpHost;
043import org.apache.http.HttpResponse;
044import org.apache.http.auth.AuthSchemeProvider;
045import org.apache.http.auth.AuthScope;
046import org.apache.http.auth.KerberosCredentials;
047import org.apache.http.client.HttpClient;
048import org.apache.http.client.config.AuthSchemes;
049import org.apache.http.client.methods.HttpGet;
050import org.apache.http.client.protocol.HttpClientContext;
051import org.apache.http.config.Lookup;
052import org.apache.http.config.RegistryBuilder;
053import org.apache.http.impl.auth.SPNegoSchemeFactory;
054import org.apache.http.impl.client.BasicCredentialsProvider;
055import org.apache.http.impl.client.HttpClients;
056import org.apache.http.util.EntityUtils;
057import org.apache.kerby.kerberos.kerb.KrbException;
058import org.apache.kerby.kerberos.kerb.client.JaasKrbUtil;
059import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer;
060import org.ietf.jgss.GSSCredential;
061import org.ietf.jgss.GSSManager;
062import org.ietf.jgss.GSSName;
063import org.ietf.jgss.Oid;
064import org.junit.jupiter.api.AfterAll;
065import org.junit.jupiter.api.BeforeAll;
066import org.junit.jupiter.api.Tag;
067import org.junit.jupiter.api.Test;
068import org.slf4j.Logger;
069import org.slf4j.LoggerFactory;
070
071/**
072 * Test class for SPNEGO authentication on the HttpServer. Uses Kerby's MiniKDC and Apache
073 * HttpComponents to verify that a simple Servlet is reachable via SPNEGO and unreachable w/o.
074 */
075@Tag(MiscTests.TAG)
076@Tag(SmallTests.TAG)
077public class TestSpnegoHttpServer extends HttpServerFunctionalTest {
078
079  private static final Logger LOG = LoggerFactory.getLogger(TestSpnegoHttpServer.class);
080  private static final String KDC_SERVER_HOST = "localhost";
081  private static final String CLIENT_PRINCIPAL = "client";
082
083  private static HttpServer server;
084  private static URL baseUrl;
085  private static SimpleKdcServer kdc;
086  private static File infoServerKeytab;
087  private static File clientKeytab;
088
089  @BeforeAll
090  public static void setupServer() throws Exception {
091    Configuration conf = new Configuration();
092    HBaseCommonTestingUtil htu = new HBaseCommonTestingUtil(conf);
093
094    final String serverPrincipal = "HTTP/" + KDC_SERVER_HOST;
095
096    kdc = SimpleKdcServerUtil.getRunningSimpleKdcServer(new File(htu.getDataTestDir().toString()),
097      HBaseCommonTestingUtil::randomFreePort);
098    File keytabDir = new File(htu.getDataTestDir("keytabs").toString());
099    if (keytabDir.exists()) {
100      deleteRecursively(keytabDir);
101    }
102    keytabDir.mkdirs();
103
104    infoServerKeytab = new File(keytabDir, serverPrincipal.replace('/', '_') + ".keytab");
105    clientKeytab = new File(keytabDir, CLIENT_PRINCIPAL + ".keytab");
106
107    setupUser(kdc, clientKeytab, CLIENT_PRINCIPAL);
108    setupUser(kdc, infoServerKeytab, serverPrincipal);
109
110    buildSpnegoConfiguration(conf, serverPrincipal, infoServerKeytab);
111
112    server = createTestServerWithSecurity(conf);
113    server.addUnprivilegedServlet("echo", "/echo", EchoServlet.class);
114    server.addJerseyResourcePackage(JerseyResource.class.getPackage().getName(), "/jersey/*");
115    server.start();
116    baseUrl = getServerURL(server);
117
118    LOG.info("HTTP server started: " + baseUrl);
119  }
120
121  @AfterAll
122  public static void stopServer() throws Exception {
123    try {
124      if (null != server) {
125        server.stop();
126      }
127    } catch (Exception e) {
128      LOG.info("Failed to stop info server", e);
129    }
130    try {
131      if (null != kdc) {
132        kdc.stop();
133      }
134    } catch (Exception e) {
135      LOG.info("Failed to stop mini KDC", e);
136    }
137  }
138
139  private static void setupUser(SimpleKdcServer kdc, File keytab, String principal)
140    throws KrbException {
141    kdc.createPrincipal(principal);
142    kdc.exportPrincipal(principal, keytab);
143  }
144
145  private static Configuration buildSpnegoConfiguration(Configuration conf, String serverPrincipal,
146    File serverKeytab) {
147    KerberosName.setRules("DEFAULT");
148
149    conf.setInt(HttpServer.HTTP_MAX_THREADS, TestHttpServer.MAX_THREADS);
150
151    // Enable Kerberos (pre-req)
152    conf.set("hbase.security.authentication", "kerberos");
153    conf.set(HttpServer.HTTP_UI_AUTHENTICATION, "kerberos");
154    conf.set(HttpServer.HTTP_SPNEGO_AUTHENTICATION_PRINCIPAL_KEY, serverPrincipal);
155    conf.set(HttpServer.HTTP_SPNEGO_AUTHENTICATION_KEYTAB_KEY, serverKeytab.getAbsolutePath());
156
157    return conf;
158  }
159
160  @Test
161  public void testUnauthorizedClientsDisallowed() throws IOException {
162    URL url = new URL(getServerURL(server), "/echo?a=b");
163    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
164    assertEquals(HttpURLConnection.HTTP_UNAUTHORIZED, conn.getResponseCode());
165  }
166
167  @Test
168  public void testAllowedClient() throws Exception {
169    // Create the subject for the client
170    final Subject clientSubject = JaasKrbUtil.loginUsingKeytab(CLIENT_PRINCIPAL, clientKeytab);
171    final Set<Principal> clientPrincipals = clientSubject.getPrincipals();
172    // Make sure the subject has a principal
173    assertFalse(clientPrincipals.isEmpty());
174
175    // Get a TGT for the subject (might have many, different encryption types). The first should
176    // be the default encryption type.
177    Set<KerberosTicket> privateCredentials =
178      clientSubject.getPrivateCredentials(KerberosTicket.class);
179    assertFalse(privateCredentials.isEmpty());
180    KerberosTicket tgt = privateCredentials.iterator().next();
181    assertNotNull(tgt);
182
183    // The name of the principal
184    final String principalName = clientPrincipals.iterator().next().getName();
185
186    // Run this code, logged in as the subject (the client)
187    HttpResponse resp = Subject.doAs(clientSubject, new PrivilegedExceptionAction<HttpResponse>() {
188      @Override
189      public HttpResponse run() throws Exception {
190        // Logs in with Kerberos via GSS
191        GSSManager gssManager = GSSManager.getInstance();
192        // jGSS Kerberos login constant
193        Oid oid = new Oid("1.2.840.113554.1.2.2");
194        GSSName gssClient = gssManager.createName(principalName, GSSName.NT_USER_NAME);
195        GSSCredential credential = gssManager.createCredential(gssClient,
196          GSSCredential.DEFAULT_LIFETIME, oid, GSSCredential.INITIATE_ONLY);
197
198        HttpClientContext context = HttpClientContext.create();
199        Lookup<AuthSchemeProvider> authRegistry = RegistryBuilder.<AuthSchemeProvider> create()
200          .register(AuthSchemes.SPNEGO, new SPNegoSchemeFactory(true, true)).build();
201
202        HttpClient client = HttpClients.custom().setDefaultAuthSchemeRegistry(authRegistry).build();
203        BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
204        credentialsProvider.setCredentials(AuthScope.ANY, new KerberosCredentials(credential));
205
206        URL url = new URL(getServerURL(server), "/echo?a=b");
207        context.setTargetHost(new HttpHost(url.getHost(), url.getPort()));
208        context.setCredentialsProvider(credentialsProvider);
209        context.setAuthSchemeRegistry(authRegistry);
210
211        HttpGet get = new HttpGet(url.toURI());
212        return client.execute(get, context);
213      }
214    });
215
216    assertNotNull(resp);
217    assertEquals(HttpURLConnection.HTTP_OK, resp.getStatusLine().getStatusCode());
218    assertEquals("a:b", EntityUtils.toString(resp.getEntity()).trim());
219  }
220
221  @Test
222  public void testMissingConfigurationThrowsException() throws Exception {
223    Configuration conf = new Configuration();
224    conf.setInt(HttpServer.HTTP_MAX_THREADS, TestHttpServer.MAX_THREADS);
225    // Enable Kerberos (pre-req)
226    conf.set("hbase.security.authentication", "kerberos");
227    // Intentionally skip keytab and principal
228
229    assertThrows(IllegalArgumentException.class, () -> {
230      HttpServer customServer = createTestServerWithSecurity(conf);
231      customServer.addUnprivilegedServlet("echo", "/echo", EchoServlet.class);
232      customServer.addJerseyResourcePackage(JerseyResource.class.getPackage().getName(),
233        "/jersey/*");
234      customServer.start();
235    });
236  }
237}