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.Assert.assertEquals;
021import static org.junit.Assert.assertFalse;
022import static org.junit.Assert.assertNotNull;
023import static org.junit.Assert.assertTrue;
024
025import java.io.File;
026import java.net.HttpURLConnection;
027import java.net.URL;
028import java.security.Principal;
029import java.security.PrivilegedExceptionAction;
030import java.util.Set;
031import javax.security.auth.Subject;
032import javax.security.auth.kerberos.KerberosTicket;
033import org.apache.hadoop.conf.Configuration;
034import org.apache.hadoop.hbase.HBaseClassTestRule;
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.hadoop.security.authorize.AccessControlList;
043import org.apache.http.HttpHost;
044import org.apache.http.HttpResponse;
045import org.apache.http.auth.AuthSchemeProvider;
046import org.apache.http.auth.AuthScope;
047import org.apache.http.auth.KerberosCredentials;
048import org.apache.http.client.HttpClient;
049import org.apache.http.client.config.AuthSchemes;
050import org.apache.http.client.methods.HttpGet;
051import org.apache.http.client.protocol.HttpClientContext;
052import org.apache.http.config.Lookup;
053import org.apache.http.config.RegistryBuilder;
054import org.apache.http.impl.auth.SPNegoSchemeFactory;
055import org.apache.http.impl.client.BasicCredentialsProvider;
056import org.apache.http.impl.client.HttpClients;
057import org.apache.http.util.EntityUtils;
058import org.apache.kerby.kerberos.kerb.KrbException;
059import org.apache.kerby.kerberos.kerb.client.JaasKrbUtil;
060import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer;
061import org.ietf.jgss.GSSCredential;
062import org.ietf.jgss.GSSManager;
063import org.ietf.jgss.GSSName;
064import org.ietf.jgss.Oid;
065import org.junit.AfterClass;
066import org.junit.BeforeClass;
067import org.junit.ClassRule;
068import org.junit.Test;
069import org.junit.experimental.categories.Category;
070import org.slf4j.Logger;
071import org.slf4j.LoggerFactory;
072
073/**
074 * Test class for SPNEGO Proxyuser authentication on the HttpServer. Uses Kerby's MiniKDC and Apache
075 * HttpComponents to verify that the doas= mechanicsm works, and that the proxyuser settings are
076 * observed.
077 */
078@Category({ MiscTests.class, SmallTests.class })
079public class TestProxyUserSpnegoHttpServer extends HttpServerFunctionalTest {
080  @ClassRule
081  public static final HBaseClassTestRule CLASS_RULE =
082    HBaseClassTestRule.forClass(TestProxyUserSpnegoHttpServer.class);
083
084  private static final Logger LOG = LoggerFactory.getLogger(TestProxyUserSpnegoHttpServer.class);
085  private static final String KDC_SERVER_HOST = "localhost";
086  private static final String WHEEL_PRINCIPAL = "wheel";
087  private static final String UNPRIVILEGED_PRINCIPAL = "unprivileged";
088  private static final String PRIVILEGED_PRINCIPAL = "privileged";
089  private static final String PRIVILEGED2_PRINCIPAL = "privileged2";
090
091  private static HttpServer server;
092  private static URL baseUrl;
093  private static SimpleKdcServer kdc;
094  private static File infoServerKeytab;
095  private static File wheelKeytab;
096  private static File unprivilegedKeytab;
097  private static File privilegedKeytab;
098  private static File privileged2Keytab;
099
100  @BeforeClass
101  public static void setupServer() throws Exception {
102    Configuration conf = new Configuration();
103    HBaseCommonTestingUtil htu = new HBaseCommonTestingUtil(conf);
104
105    final String serverPrincipal = "HTTP/" + KDC_SERVER_HOST;
106
107    kdc = SimpleKdcServerUtil.getRunningSimpleKdcServer(new File(htu.getDataTestDir().toString()),
108      HBaseCommonTestingUtil::randomFreePort);
109    File keytabDir = new File(htu.getDataTestDir("keytabs").toString());
110    if (keytabDir.exists()) {
111      deleteRecursively(keytabDir);
112    }
113    keytabDir.mkdirs();
114
115    infoServerKeytab = new File(keytabDir, serverPrincipal.replace('/', '_') + ".keytab");
116    wheelKeytab = new File(keytabDir, WHEEL_PRINCIPAL + ".keytab");
117    unprivilegedKeytab = new File(keytabDir, UNPRIVILEGED_PRINCIPAL + ".keytab");
118    privilegedKeytab = new File(keytabDir, PRIVILEGED_PRINCIPAL + ".keytab");
119    privileged2Keytab = new File(keytabDir, PRIVILEGED2_PRINCIPAL + ".keytab");
120
121    setupUser(kdc, wheelKeytab, WHEEL_PRINCIPAL);
122    setupUser(kdc, unprivilegedKeytab, UNPRIVILEGED_PRINCIPAL);
123    setupUser(kdc, privilegedKeytab, PRIVILEGED_PRINCIPAL);
124    setupUser(kdc, privileged2Keytab, PRIVILEGED2_PRINCIPAL);
125
126    setupUser(kdc, infoServerKeytab, serverPrincipal);
127
128    buildSpnegoConfiguration(conf, serverPrincipal, infoServerKeytab);
129    AccessControlList acl = buildAdminAcl(conf);
130
131    server = createTestServerWithSecurityAndAcl(conf, acl);
132    server.addPrivilegedServlet("echo", "/echo", EchoServlet.class);
133    server.addJerseyResourcePackage(JerseyResource.class.getPackage().getName(), "/jersey/*");
134    server.start();
135    baseUrl = getServerURL(server);
136
137    LOG.info("HTTP server started: " + baseUrl);
138  }
139
140  @AfterClass
141  public static void stopServer() throws Exception {
142    try {
143      if (null != server) {
144        server.stop();
145      }
146    } catch (Exception e) {
147      LOG.info("Failed to stop info server", e);
148    }
149    try {
150      if (null != kdc) {
151        kdc.stop();
152      }
153    } catch (Exception e) {
154      LOG.info("Failed to stop mini KDC", e);
155    }
156  }
157
158  private static void setupUser(SimpleKdcServer kdc, File keytab, String principal)
159    throws KrbException {
160    kdc.createPrincipal(principal);
161    kdc.exportPrincipal(principal, keytab);
162  }
163
164  protected static Configuration buildSpnegoConfiguration(Configuration conf,
165    String serverPrincipal, File serverKeytab) {
166    KerberosName.setRules("DEFAULT");
167
168    conf.setInt(HttpServer.HTTP_MAX_THREADS, TestHttpServer.MAX_THREADS);
169
170    // Enable Kerberos (pre-req)
171    conf.set("hbase.security.authentication", "kerberos");
172    conf.set(HttpServer.HTTP_UI_AUTHENTICATION, "kerberos");
173    conf.set(HttpServer.HTTP_SPNEGO_AUTHENTICATION_PRINCIPAL_KEY, serverPrincipal);
174    conf.set(HttpServer.HTTP_SPNEGO_AUTHENTICATION_KEYTAB_KEY, serverKeytab.getAbsolutePath());
175
176    conf.set(HttpServer.HTTP_SPNEGO_AUTHENTICATION_ADMIN_USERS_KEY, PRIVILEGED_PRINCIPAL);
177    conf.set(HttpServer.HTTP_SPNEGO_AUTHENTICATION_PROXYUSER_ENABLE_KEY, "true");
178    conf.set("hadoop.security.authorization", "true");
179
180    conf.set("hadoop.proxyuser.wheel.hosts", "*");
181    conf.set("hadoop.proxyuser.wheel.users", PRIVILEGED_PRINCIPAL + "," + UNPRIVILEGED_PRINCIPAL);
182    return conf;
183  }
184
185  /**
186   * Builds an ACL that will restrict the users who can issue commands to endpoints on the UI which
187   * are meant only for administrators.
188   */
189  public static AccessControlList buildAdminAcl(Configuration conf) {
190    final String userGroups = conf.get(HttpServer.HTTP_SPNEGO_AUTHENTICATION_ADMIN_USERS_KEY, null);
191    final String adminGroups =
192      conf.get(HttpServer.HTTP_SPNEGO_AUTHENTICATION_ADMIN_GROUPS_KEY, null);
193    if (userGroups == null && adminGroups == null) {
194      // Backwards compatibility - if the user doesn't have anything set, allow all users in.
195      return new AccessControlList("*", null);
196    }
197    return new AccessControlList(userGroups, adminGroups);
198  }
199
200  @Test
201  public void testProxyAllowed() throws Exception {
202    testProxy(WHEEL_PRINCIPAL, PRIVILEGED_PRINCIPAL, HttpURLConnection.HTTP_OK, null);
203  }
204
205  @Test
206  public void testProxyDisallowedForUnprivileged() throws Exception {
207    testProxy(WHEEL_PRINCIPAL, UNPRIVILEGED_PRINCIPAL, HttpURLConnection.HTTP_FORBIDDEN,
208      "403 User unprivileged is unauthorized to access this page.");
209  }
210
211  @Test
212  public void testProxyDisallowedForNotSudoAble() throws Exception {
213    testProxy(WHEEL_PRINCIPAL, PRIVILEGED2_PRINCIPAL, HttpURLConnection.HTTP_FORBIDDEN,
214      "403 Forbidden");
215  }
216
217  public void testProxy(String clientPrincipal, String doAs, int responseCode, String statusLine)
218    throws Exception {
219    // Create the subject for the client
220    final Subject clientSubject = JaasKrbUtil.loginUsingKeytab(WHEEL_PRINCIPAL, wheelKeytab);
221    final Set<Principal> clientPrincipals = clientSubject.getPrincipals();
222    // Make sure the subject has a principal
223    assertFalse(clientPrincipals.isEmpty());
224
225    // Get a TGT for the subject (might have many, different encryption types). The first should
226    // be the default encryption type.
227    Set<KerberosTicket> privateCredentials =
228      clientSubject.getPrivateCredentials(KerberosTicket.class);
229    assertFalse(privateCredentials.isEmpty());
230    KerberosTicket tgt = privateCredentials.iterator().next();
231    assertNotNull(tgt);
232
233    // The name of the principal
234    final String principalName = clientPrincipals.iterator().next().getName();
235
236    // Run this code, logged in as the subject (the client)
237    HttpResponse resp = Subject.doAs(clientSubject, new PrivilegedExceptionAction<HttpResponse>() {
238      @Override
239      public HttpResponse run() throws Exception {
240        // Logs in with Kerberos via GSS
241        GSSManager gssManager = GSSManager.getInstance();
242        // jGSS Kerberos login constant
243        Oid oid = new Oid("1.2.840.113554.1.2.2");
244        GSSName gssClient = gssManager.createName(principalName, GSSName.NT_USER_NAME);
245        GSSCredential credential = gssManager.createCredential(gssClient,
246          GSSCredential.DEFAULT_LIFETIME, oid, GSSCredential.INITIATE_ONLY);
247
248        HttpClientContext context = HttpClientContext.create();
249        Lookup<AuthSchemeProvider> authRegistry = RegistryBuilder.<AuthSchemeProvider> create()
250          .register(AuthSchemes.SPNEGO, new SPNegoSchemeFactory(true, true)).build();
251
252        HttpClient client = HttpClients.custom().setDefaultAuthSchemeRegistry(authRegistry).build();
253        BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
254        credentialsProvider.setCredentials(AuthScope.ANY, new KerberosCredentials(credential));
255
256        URL url = new URL(getServerURL(server), "/echo?doAs=" + doAs + "&a=b");
257        context.setTargetHost(new HttpHost(url.getHost(), url.getPort()));
258        context.setCredentialsProvider(credentialsProvider);
259        context.setAuthSchemeRegistry(authRegistry);
260
261        HttpGet get = new HttpGet(url.toURI());
262        return client.execute(get, context);
263      }
264    });
265
266    assertNotNull(resp);
267    assertEquals(responseCode, resp.getStatusLine().getStatusCode());
268    if (responseCode == HttpURLConnection.HTTP_OK) {
269      assertTrue(EntityUtils.toString(resp.getEntity()).trim().contains("a:b"));
270    } else {
271      assertTrue(resp.getStatusLine().toString().contains(statusLine)
272        || EntityUtils.toString(resp.getEntity()).contains(statusLine));
273    }
274  }
275
276}