View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to you under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    * http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.hadoop.hbase.http;
18  
19  import java.io.BufferedReader;
20  import java.io.File;
21  import java.io.IOException;
22  import java.io.InputStreamReader;
23  import java.net.HttpURLConnection;
24  import java.net.URL;
25  import java.security.Principal;
26  import java.security.PrivilegedExceptionAction;
27  import java.util.Set;
28  
29  import javax.security.auth.Subject;
30  import javax.security.auth.kerberos.KerberosTicket;
31  
32  import org.apache.commons.logging.Log;
33  import org.apache.commons.logging.LogFactory;
34  import org.apache.hadoop.conf.Configuration;
35  import org.apache.hadoop.hbase.http.TestHttpServer.EchoServlet;
36  import org.apache.hadoop.hbase.http.resource.JerseyResource;
37  import org.apache.hadoop.hbase.testclassification.MiscTests;
38  import org.apache.hadoop.hbase.testclassification.SmallTests;
39  import org.apache.hadoop.security.authentication.util.KerberosName;
40  import org.apache.http.HttpHost;
41  import org.apache.http.HttpResponse;
42  import org.apache.http.auth.AuthSchemeProvider;
43  import org.apache.http.auth.AuthScope;
44  import org.apache.http.auth.KerberosCredentials;
45  import org.apache.http.client.HttpClient;
46  import org.apache.http.client.config.AuthSchemes;
47  import org.apache.http.client.methods.HttpGet;
48  import org.apache.http.client.protocol.HttpClientContext;
49  import org.apache.http.config.Lookup;
50  import org.apache.http.config.RegistryBuilder;
51  import org.apache.http.entity.ByteArrayEntity;
52  import org.apache.http.entity.ContentType;
53  import org.apache.http.impl.auth.SPNegoSchemeFactory;
54  import org.apache.http.impl.client.BasicCredentialsProvider;
55  import org.apache.http.impl.client.HttpClients;
56  import org.apache.http.util.EntityUtils;
57  import org.apache.kerby.kerberos.kerb.KrbException;
58  import org.apache.kerby.kerberos.kerb.client.JaasKrbUtil;
59  import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer;
60  import org.ietf.jgss.GSSCredential;
61  import org.ietf.jgss.GSSManager;
62  import org.ietf.jgss.GSSName;
63  import org.ietf.jgss.Oid;
64  import org.junit.AfterClass;
65  import org.junit.BeforeClass;
66  import org.junit.Test;
67  import org.junit.experimental.categories.Category;
68  
69  /**
70   * Test class for SPNEGO authentication on the HttpServer. Uses Kerby's MiniKDC and Apache
71   * HttpComponents to verify that a simple Servlet is reachable via SPNEGO and unreachable w/o.
72   */
73  @Category({MiscTests.class, SmallTests.class})
74  public class TestSpnegoHttpServer extends HttpServerFunctionalTest {
75    private static final Log LOG = LogFactory.getLog(TestSpnegoHttpServer.class);
76    private static final String KDC_SERVER_HOST = "localhost";
77    private static final String CLIENT_PRINCIPAL = "client";
78  
79    private static HttpServer server;
80    private static URL baseUrl;
81    private static SimpleKdcServer kdc;
82    private static File infoServerKeytab;
83    private static File clientKeytab;
84  
85    @BeforeClass
86    public static void setupServer() throws Exception {
87      final String serverPrincipal = "HTTP/" + KDC_SERVER_HOST;
88      final File target = new File(System.getProperty("user.dir"), "target");
89      assertTrue(target.exists());
90  
91      kdc = buildMiniKdc();
92      kdc.start();
93  
94      File keytabDir = new File(target, TestSpnegoHttpServer.class.getSimpleName()
95          + "_keytabs");
96      if (keytabDir.exists()) {
97        deleteRecursively(keytabDir);
98      }
99      keytabDir.mkdirs();
100 
101     infoServerKeytab = new File(keytabDir, serverPrincipal.replace('/', '_') + ".keytab");
102     clientKeytab = new File(keytabDir, CLIENT_PRINCIPAL + ".keytab");
103 
104     setupUser(kdc, clientKeytab, CLIENT_PRINCIPAL);
105     setupUser(kdc, infoServerKeytab, serverPrincipal);
106 
107     Configuration conf = buildSpnegoConfiguration(serverPrincipal, infoServerKeytab);
108 
109     server = createTestServerWithSecurity(conf);
110     server.addServlet("echo", "/echo", EchoServlet.class);
111     server.addJerseyResourcePackage(JerseyResource.class.getPackage().getName(), "/jersey/*");
112     server.start();
113     baseUrl = getServerURL(server);
114 
115     LOG.info("HTTP server started: "+ baseUrl);
116   }
117 
118   @AfterClass
119   public static void stopServer() throws Exception {
120     try {
121       if (null != server) {
122         server.stop();
123       }
124     } catch (Exception e) {
125       LOG.info("Failed to stop info server", e);
126     }
127     try {
128       if (null != kdc) {
129         kdc.stop();
130       }
131     } catch (Exception e) {
132       LOG.info("Failed to stop mini KDC", e);
133     }
134   }
135 
136   private static void setupUser(SimpleKdcServer kdc, File keytab, String principal)
137       throws KrbException {
138     kdc.createPrincipal(principal);
139     kdc.exportPrincipal(principal, keytab);
140   }
141 
142   private static SimpleKdcServer buildMiniKdc() throws Exception {
143     SimpleKdcServer kdc = new SimpleKdcServer();
144 
145     final File target = new File(System.getProperty("user.dir"), "target");
146     File kdcDir = new File(target, TestSpnegoHttpServer.class.getSimpleName());
147     if (kdcDir.exists()) {
148       deleteRecursively(kdcDir);
149     }
150     kdcDir.mkdirs();
151     kdc.setWorkDir(kdcDir);
152 
153     kdc.setKdcHost(KDC_SERVER_HOST);
154     int kdcPort = getFreePort();
155     kdc.setAllowTcp(true);
156     kdc.setAllowUdp(false);
157     kdc.setKdcTcpPort(kdcPort);
158 
159     LOG.info("Starting KDC server at " + KDC_SERVER_HOST + ":" + kdcPort);
160 
161     kdc.init();
162 
163     return kdc;
164   }
165 
166   private static Configuration buildSpnegoConfiguration(String serverPrincipal, File
167       serverKeytab) {
168     Configuration conf = new Configuration();
169     KerberosName.setRules("DEFAULT");
170 
171     conf.setInt(HttpServer.HTTP_MAX_THREADS, 10);
172 
173     // Enable Kerberos (pre-req)
174     conf.set("hbase.security.authentication", "kerberos");
175     conf.set(HttpServer.HTTP_UI_AUTHENTICATION, "kerberos");
176     conf.set(HttpServer.HTTP_SPNEGO_AUTHENTICATION_PRINCIPAL_KEY, serverPrincipal);
177     conf.set(HttpServer.HTTP_SPNEGO_AUTHENTICATION_KEYTAB_KEY, serverKeytab.getAbsolutePath());
178 
179     return conf;
180   }
181 
182   @Test
183   public void testUnauthorizedClientsDisallowed() throws IOException {
184     URL url = new URL(getServerURL(server), "/echo?a=b");
185     HttpURLConnection conn = (HttpURLConnection) url.openConnection();
186     assertEquals(HttpURLConnection.HTTP_UNAUTHORIZED, conn.getResponseCode());
187   }
188 
189   @Test
190   public void testAllowedClient() throws Exception {
191     // Create the subject for the client
192     final Subject clientSubject = JaasKrbUtil.loginUsingKeytab(CLIENT_PRINCIPAL, clientKeytab);
193     final Set<Principal> clientPrincipals = clientSubject.getPrincipals();
194     // Make sure the subject has a principal
195     assertFalse(clientPrincipals.isEmpty());
196 
197     // Get a TGT for the subject (might have many, different encryption types). The first should
198     // be the default encryption type.
199     Set<KerberosTicket> privateCredentials =
200             clientSubject.getPrivateCredentials(KerberosTicket.class);
201     assertFalse(privateCredentials.isEmpty());
202     KerberosTicket tgt = privateCredentials.iterator().next();
203     assertNotNull(tgt);
204 
205     // The name of the principal
206     final String principalName = clientPrincipals.iterator().next().getName();
207 
208     // Run this code, logged in as the subject (the client)
209     HttpResponse resp = Subject.doAs(clientSubject,
210         new PrivilegedExceptionAction<HttpResponse>() {
211       @Override
212       public HttpResponse run() throws Exception {
213         // Logs in with Kerberos via GSS
214         GSSManager gssManager = GSSManager.getInstance();
215         // jGSS Kerberos login constant
216         Oid oid = new Oid("1.2.840.113554.1.2.2");
217         GSSName gssClient = gssManager.createName(principalName, GSSName.NT_USER_NAME);
218         GSSCredential credential = gssManager.createCredential(gssClient,
219             GSSCredential.DEFAULT_LIFETIME, oid, GSSCredential.INITIATE_ONLY);
220 
221         HttpClientContext context = HttpClientContext.create();
222         Lookup<AuthSchemeProvider> authRegistry = RegistryBuilder.<AuthSchemeProvider>create()
223             .register(AuthSchemes.SPNEGO, new SPNegoSchemeFactory(true, true))
224             .build();
225 
226         HttpClient client = HttpClients.custom().setDefaultAuthSchemeRegistry(authRegistry).build();
227         BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
228         credentialsProvider.setCredentials(AuthScope.ANY, new KerberosCredentials(credential));
229 
230         URL url = new URL(getServerURL(server), "/echo?a=b");
231         context.setTargetHost(new HttpHost(url.getHost(), url.getPort()));
232         context.setCredentialsProvider(credentialsProvider);
233         context.setAuthSchemeRegistry(authRegistry);
234 
235         HttpGet get = new HttpGet(url.toURI());
236         return client.execute(get, context);
237       }
238     });
239 
240     assertNotNull(resp);
241     assertEquals(HttpURLConnection.HTTP_OK, resp.getStatusLine().getStatusCode());
242     assertEquals("a:b", EntityUtils.toString(resp.getEntity()).trim());
243   }
244 
245   @Test(expected = IllegalArgumentException.class)
246   public void testMissingConfigurationThrowsException() throws Exception {
247     Configuration conf = new Configuration();
248     conf.setInt(HttpServer.HTTP_MAX_THREADS, 10);
249     // Enable Kerberos (pre-req)
250     conf.set("hbase.security.authentication", "kerberos");
251     // Intentionally skip keytab and principal
252 
253     HttpServer customServer = createTestServerWithSecurity(conf);
254     customServer.addServlet("echo", "/echo", EchoServlet.class);
255     customServer.addJerseyResourcePackage(JerseyResource.class.getPackage().getName(), "/jersey/*");
256     customServer.start();
257   }
258 }