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.rest;
019
020import static org.junit.Assert.assertEquals;
021import static org.junit.Assert.assertTrue;
022
023import com.fasterxml.jackson.databind.ObjectMapper;
024import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
025
026import java.io.File;
027import java.net.HttpURLConnection;
028import java.net.URL;
029import java.security.Principal;
030import java.security.PrivilegedExceptionAction;
031
032import javax.ws.rs.core.MediaType;
033
034import org.apache.commons.io.FileUtils;
035import org.apache.hadoop.conf.Configuration;
036import org.apache.hadoop.fs.CommonConfigurationKeys;
037import org.apache.hadoop.hbase.HBaseClassTestRule;
038import org.apache.hadoop.hbase.HBaseTestingUtility;
039import org.apache.hadoop.hbase.MiniHBaseCluster;
040import org.apache.hadoop.hbase.TableName;
041import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
042import org.apache.hadoop.hbase.client.Connection;
043import org.apache.hadoop.hbase.client.ConnectionFactory;
044import org.apache.hadoop.hbase.client.Put;
045import org.apache.hadoop.hbase.client.Table;
046import org.apache.hadoop.hbase.client.TableDescriptor;
047import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
048import org.apache.hadoop.hbase.coprocessor.CoprocessorHost;
049import org.apache.hadoop.hbase.http.ssl.KeyStoreTestUtil;
050import org.apache.hadoop.hbase.rest.model.CellModel;
051import org.apache.hadoop.hbase.rest.model.CellSetModel;
052import org.apache.hadoop.hbase.rest.model.RowModel;
053import org.apache.hadoop.hbase.security.HBaseKerberosUtils;
054import org.apache.hadoop.hbase.security.access.AccessControlClient;
055import org.apache.hadoop.hbase.security.access.AccessControlConstants;
056import org.apache.hadoop.hbase.security.access.AccessController;
057import org.apache.hadoop.hbase.security.access.Permission.Action;
058import org.apache.hadoop.hbase.security.token.TokenProvider;
059import org.apache.hadoop.hbase.testclassification.MiscTests;
060import org.apache.hadoop.hbase.testclassification.SmallTests;
061import org.apache.hadoop.hbase.util.Bytes;
062import org.apache.hadoop.hbase.util.Pair;
063import org.apache.hadoop.hdfs.DFSConfigKeys;
064import org.apache.hadoop.http.HttpConfig;
065import org.apache.hadoop.minikdc.MiniKdc;
066import org.apache.hadoop.security.UserGroupInformation;
067import org.apache.hadoop.security.authentication.util.KerberosName;
068import org.apache.http.HttpEntity;
069import org.apache.http.HttpHost;
070import org.apache.http.auth.AuthSchemeProvider;
071import org.apache.http.auth.AuthScope;
072import org.apache.http.auth.Credentials;
073import org.apache.http.client.AuthCache;
074import org.apache.http.client.CredentialsProvider;
075import org.apache.http.client.config.AuthSchemes;
076import org.apache.http.client.methods.CloseableHttpResponse;
077import org.apache.http.client.methods.HttpGet;
078import org.apache.http.client.methods.HttpPut;
079import org.apache.http.client.protocol.HttpClientContext;
080import org.apache.http.config.Registry;
081import org.apache.http.config.RegistryBuilder;
082import org.apache.http.conn.HttpClientConnectionManager;
083import org.apache.http.entity.ContentType;
084import org.apache.http.entity.StringEntity;
085import org.apache.http.impl.auth.SPNegoSchemeFactory;
086import org.apache.http.impl.client.BasicAuthCache;
087import org.apache.http.impl.client.BasicCredentialsProvider;
088import org.apache.http.impl.client.CloseableHttpClient;
089import org.apache.http.impl.client.HttpClients;
090import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
091import org.apache.http.util.EntityUtils;
092import org.junit.AfterClass;
093import org.junit.BeforeClass;
094import org.junit.ClassRule;
095import org.junit.Test;
096import org.junit.experimental.categories.Category;
097import org.slf4j.Logger;
098import org.slf4j.LoggerFactory;
099
100/**
101 * Test class for SPNEGO authentication on the HttpServer. Uses Kerby's MiniKDC and Apache
102 * HttpComponents to verify that a simple Servlet is reachable via SPNEGO and unreachable w/o.
103 */
104@Category({MiscTests.class, SmallTests.class})
105public class TestSecureRESTServer {
106
107  @ClassRule
108  public static final HBaseClassTestRule CLASS_RULE =
109      HBaseClassTestRule.forClass(TestSecureRESTServer.class);
110
111  private static final Logger LOG = LoggerFactory.getLogger(TestSecureRESTServer.class);
112  private static final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
113  private static final HBaseRESTTestingUtility REST_TEST = new HBaseRESTTestingUtility();
114  private static MiniHBaseCluster CLUSTER;
115
116  private static final String HOSTNAME = "localhost";
117  private static final String CLIENT_PRINCIPAL = "client";
118  // The principal for accepting SPNEGO authn'ed requests (*must* be HTTP/fqdn)
119  private static final String SPNEGO_SERVICE_PRINCIPAL = "HTTP/" + HOSTNAME;
120  // The principal we use to connect to HBase
121  private static final String REST_SERVER_PRINCIPAL = "rest";
122  private static final String SERVICE_PRINCIPAL = "hbase/" + HOSTNAME;
123
124  private static URL baseUrl;
125  private static MiniKdc KDC;
126  private static RESTServer server;
127  private static File restServerKeytab;
128  private static File clientKeytab;
129  private static File serviceKeytab;
130
131  private static void setSecuredHadoopConfiguration(Configuration conf,
132      String servicePrincipal, String serviceKeytab, String spnegoServerPrincipal) {
133    // if we drop support for hadoop-2.4.0 and hadoop-2.4.1,
134    // the following key should be changed.
135    // 1) DFS_NAMENODE_USER_NAME_KEY -> DFS_NAMENODE_KERBEROS_PRINCIPAL_KEY
136    // 2) DFS_DATANODE_USER_NAME_KEY -> DFS_DATANODE_KERBEROS_PRINCIPAL_KEY
137    // HDFS
138    conf.set(DFSConfigKeys.DFS_NAMENODE_USER_NAME_KEY, servicePrincipal);
139    conf.set(DFSConfigKeys.DFS_NAMENODE_KEYTAB_FILE_KEY, serviceKeytab);
140    conf.set(DFSConfigKeys.DFS_DATANODE_USER_NAME_KEY, servicePrincipal);
141    conf.set(DFSConfigKeys.DFS_DATANODE_KEYTAB_FILE_KEY, serviceKeytab);
142    conf.setBoolean(DFSConfigKeys.DFS_BLOCK_ACCESS_TOKEN_ENABLE_KEY, true);
143
144    if (spnegoServerPrincipal != null) {
145      conf.set(DFSConfigKeys.DFS_WEB_AUTHENTICATION_KERBEROS_PRINCIPAL_KEY,
146          spnegoServerPrincipal);
147    }
148
149    conf.setBoolean("ignore.secure.ports.for.testing", true);
150
151    UserGroupInformation.setConfiguration(conf);
152  }
153
154  @BeforeClass
155  public static void setupServer() throws Exception {
156    final File target = new File(System.getProperty("user.dir"), "target");
157    assertTrue(target.exists());
158
159    /*
160     * Keytabs
161     */
162    File keytabDir = new File(target, TestSecureRESTServer.class.getSimpleName()
163        + "_keytabs");
164    if (keytabDir.exists()) {
165      FileUtils.deleteDirectory(keytabDir);
166    }
167    keytabDir.mkdirs();
168    // Keytab for HBase services (RS, Master)
169    serviceKeytab = new File(keytabDir, "hbase.service.keytab");
170    // The keytab for the REST server
171    restServerKeytab = new File(keytabDir, "spnego.keytab");
172    // Keytab for the client
173    clientKeytab = new File(keytabDir, CLIENT_PRINCIPAL + ".keytab");
174
175    /*
176     * Update UGI
177     */
178    Configuration conf = TEST_UTIL.getConfiguration();
179
180    /*
181     * Start KDC
182     */
183    KDC = TEST_UTIL.setupMiniKdc(serviceKeytab);
184    KDC.createPrincipal(clientKeytab, CLIENT_PRINCIPAL);
185    KDC.createPrincipal(serviceKeytab, SERVICE_PRINCIPAL);
186    // REST server's keytab contains keys for both principals REST uses
187    KDC.createPrincipal(restServerKeytab, SPNEGO_SERVICE_PRINCIPAL, REST_SERVER_PRINCIPAL);
188
189    // Set configuration for HBase
190    HBaseKerberosUtils.setPrincipalForTesting(SERVICE_PRINCIPAL + "@" + KDC.getRealm());
191    HBaseKerberosUtils.setKeytabFileForTesting(serviceKeytab.getAbsolutePath());
192    // Why doesn't `setKeytabFileForTesting` do this?
193    conf.set("hbase.master.keytab.file", serviceKeytab.getAbsolutePath());
194    conf.set("hbase.regionserver.hostname", "localhost");
195    conf.set("hbase.master.hostname", "localhost");
196    // Work around HBASE-20950 missing in branch-2.0 and branch-2.1
197    HBaseKerberosUtils.setPrincipalForTesting(SERVICE_PRINCIPAL+ "@" + KDC.getRealm());
198    HBaseKerberosUtils.setSecuredConfiguration(conf);
199    setSecuredHadoopConfiguration(conf,
200        SERVICE_PRINCIPAL+ "@" + KDC.getRealm(), serviceKeytab.getAbsolutePath(),
201        SPNEGO_SERVICE_PRINCIPAL+ "@" + KDC.getRealm());
202    setHdfsSecuredConfiguration(conf);
203    conf.setStrings(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY,
204        TokenProvider.class.getName(), AccessController.class.getName());
205    conf.setStrings(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY,
206        AccessController.class.getName());
207    conf.setStrings(CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY,
208        AccessController.class.getName());
209    // Enable EXEC permission checking
210    conf.setBoolean(AccessControlConstants.EXEC_PERMISSION_CHECKS_KEY, true);
211    conf.set("hbase.superuser", "hbase");
212    conf.set("hadoop.proxyuser.rest.hosts", "*");
213    conf.set("hadoop.proxyuser.rest.users", "*");
214    UserGroupInformation.setConfiguration(conf);
215
216    updateKerberosConfiguration(conf, REST_SERVER_PRINCIPAL, SPNEGO_SERVICE_PRINCIPAL,
217        restServerKeytab);
218
219    // Start HDFS
220    TEST_UTIL.startMiniCluster(1, 1);
221
222    // Start REST
223    UserGroupInformation restUser = UserGroupInformation.loginUserFromKeytabAndReturnUGI(
224        REST_SERVER_PRINCIPAL, restServerKeytab.getAbsolutePath());
225    restUser.doAs(new PrivilegedExceptionAction<Void>() {
226      @Override
227      public Void run() throws Exception {
228        REST_TEST.startServletContainer(conf);
229        return null;
230      }
231    });
232    baseUrl = new URL("http://localhost:" + REST_TEST.getServletPort());
233
234    LOG.info("HTTP server started: "+ baseUrl);
235    TEST_UTIL.waitTableAvailable(TableName.valueOf("hbase:acl"));
236
237    // Let the REST server create, read, and write globally
238    UserGroupInformation superuser = UserGroupInformation.loginUserFromKeytabAndReturnUGI(
239        SERVICE_PRINCIPAL, serviceKeytab.getAbsolutePath());
240    superuser.doAs(new PrivilegedExceptionAction<Void>() {
241      @Override
242      public Void run() throws Exception {
243        try (Connection conn = ConnectionFactory.createConnection(TEST_UTIL.getConfiguration())) {
244          AccessControlClient.grant(
245              conn, REST_SERVER_PRINCIPAL, Action.CREATE, Action.READ, Action.WRITE);
246        } catch (Throwable t) {
247          if (t instanceof Exception) {
248            throw (Exception) t;
249          } else {
250            throw new Exception(t);
251          }
252        }
253        return null;
254      }
255    });
256  }
257
258  @AfterClass
259  public static void stopServer() throws Exception {
260    try {
261      if (null != server) {
262        server.stop();
263      }
264    } catch (Exception e) {
265      LOG.info("Failed to stop info server", e);
266    }
267    try {
268      if (CLUSTER != null) {
269        CLUSTER.shutdown();
270      }
271    } catch (Exception e) {
272      LOG.info("Failed to stop HBase cluster", e);
273    }
274    try {
275      if (null != KDC) {
276        KDC.stop();
277      }
278    } catch (Exception e) {
279      LOG.info("Failed to stop mini KDC", e);
280    }
281  }
282
283  private static void setHdfsSecuredConfiguration(Configuration conf) throws Exception {
284    // Set principal+keytab configuration for HDFS
285    conf.set(DFSConfigKeys.DFS_NAMENODE_KERBEROS_PRINCIPAL_KEY,
286        SERVICE_PRINCIPAL + "@" + KDC.getRealm());
287    conf.set(DFSConfigKeys.DFS_NAMENODE_KEYTAB_FILE_KEY, serviceKeytab.getAbsolutePath());
288    conf.set(DFSConfigKeys.DFS_DATANODE_KERBEROS_PRINCIPAL_KEY,
289        SERVICE_PRINCIPAL + "@" + KDC.getRealm());
290    conf.set(DFSConfigKeys.DFS_DATANODE_KEYTAB_FILE_KEY, serviceKeytab.getAbsolutePath());
291    conf.set(DFSConfigKeys.DFS_WEB_AUTHENTICATION_KERBEROS_PRINCIPAL_KEY,
292        SPNEGO_SERVICE_PRINCIPAL + "@" + KDC.getRealm());
293    // Enable token access for HDFS blocks
294    conf.setBoolean(DFSConfigKeys.DFS_BLOCK_ACCESS_TOKEN_ENABLE_KEY, true);
295    // Only use HTTPS (required because we aren't using "secure" ports)
296    conf.set(DFSConfigKeys.DFS_HTTP_POLICY_KEY, HttpConfig.Policy.HTTPS_ONLY.name());
297    // Bind on localhost for spnego to have a chance at working
298    conf.set(DFSConfigKeys.DFS_NAMENODE_HTTPS_ADDRESS_KEY, "localhost:0");
299    conf.set(DFSConfigKeys.DFS_DATANODE_HTTPS_ADDRESS_KEY, "localhost:0");
300
301    // Generate SSL certs
302    File keystoresDir = new File(TEST_UTIL.getDataTestDir("keystore").toUri().getPath());
303    keystoresDir.mkdirs();
304    String sslConfDir = KeyStoreTestUtil.getClasspathDir(TestSecureRESTServer.class);
305    KeyStoreTestUtil.setupSSLConfig(keystoresDir.getAbsolutePath(), sslConfDir, conf, false);
306
307    // Magic flag to tell hdfs to not fail on using ports above 1024
308    conf.setBoolean("ignore.secure.ports.for.testing", true);
309  }
310
311  private static void updateKerberosConfiguration(Configuration conf,
312      String serverPrincipal, String spnegoPrincipal, File serverKeytab) {
313    KerberosName.setRules("DEFAULT");
314
315    // Enable Kerberos (pre-req)
316    conf.set("hbase.security.authentication", "kerberos");
317    conf.set(RESTServer.REST_AUTHENTICATION_TYPE, "kerberos");
318    // User to talk to HBase as
319    conf.set(RESTServer.REST_KERBEROS_PRINCIPAL, serverPrincipal);
320    // User to accept SPNEGO-auth'd http calls as
321    conf.set("hbase.rest.authentication.kerberos.principal", spnegoPrincipal);
322    // Keytab for both principals above
323    conf.set(RESTServer.REST_KEYTAB_FILE, serverKeytab.getAbsolutePath());
324    conf.set("hbase.rest.authentication.kerberos.keytab", serverKeytab.getAbsolutePath());
325  }
326
327  @Test
328  public void testPositiveAuthorization() throws Exception {
329    // Create a table, write a row to it, grant read perms to the client
330    UserGroupInformation superuser = UserGroupInformation.loginUserFromKeytabAndReturnUGI(
331        SERVICE_PRINCIPAL, serviceKeytab.getAbsolutePath());
332    final TableName table = TableName.valueOf("publicTable");
333    superuser.doAs(new PrivilegedExceptionAction<Void>() {
334      @Override
335      public Void run() throws Exception {
336        try (Connection conn = ConnectionFactory.createConnection(TEST_UTIL.getConfiguration())) {
337          TableDescriptor desc = TableDescriptorBuilder.newBuilder(table)
338              .setColumnFamily(ColumnFamilyDescriptorBuilder.of("f1"))
339              .build();
340          conn.getAdmin().createTable(desc);
341          try (Table t = conn.getTable(table)) {
342            Put p = new Put(Bytes.toBytes("a"));
343            p.addColumn(Bytes.toBytes("f1"), new byte[0], Bytes.toBytes("1"));
344            t.put(p);
345          }
346          AccessControlClient.grant(conn, CLIENT_PRINCIPAL, Action.READ);
347        } catch (Throwable e) {
348          if (e instanceof Exception) {
349            throw (Exception) e;
350          } else {
351            throw new Exception(e);
352          }
353        }
354        return null;
355      }
356    });
357
358    // Read that row as the client
359    Pair<CloseableHttpClient,HttpClientContext> pair = getClient();
360    CloseableHttpClient client = pair.getFirst();
361    HttpClientContext context = pair.getSecond();
362
363    HttpGet get = new HttpGet(new URL("http://localhost:"+ REST_TEST.getServletPort()).toURI()
364        + "/" + table + "/a");
365    get.addHeader("Accept", "application/json");
366    UserGroupInformation user = UserGroupInformation.loginUserFromKeytabAndReturnUGI(
367        CLIENT_PRINCIPAL, clientKeytab.getAbsolutePath());
368    String jsonResponse = user.doAs(new PrivilegedExceptionAction<String>() {
369      @Override
370      public String run() throws Exception {
371        try (CloseableHttpResponse response = client.execute(get, context)) {
372          final int statusCode = response.getStatusLine().getStatusCode();
373          assertEquals(response.getStatusLine().toString(), HttpURLConnection.HTTP_OK, statusCode);
374          HttpEntity entity = response.getEntity();
375          return EntityUtils.toString(entity);
376        }
377      }
378    });
379    ObjectMapper mapper = new JacksonJaxbJsonProvider()
380        .locateMapper(CellSetModel.class, MediaType.APPLICATION_JSON_TYPE);
381    CellSetModel model = mapper.readValue(jsonResponse, CellSetModel.class);
382    assertEquals(1, model.getRows().size());
383    RowModel row = model.getRows().get(0);
384    assertEquals("a", Bytes.toString(row.getKey()));
385    assertEquals(1, row.getCells().size());
386    CellModel cell = row.getCells().get(0);
387    assertEquals("1", Bytes.toString(cell.getValue()));
388  }
389
390  @Test
391  public void testNegativeAuthorization() throws Exception {
392    Pair<CloseableHttpClient,HttpClientContext> pair = getClient();
393    CloseableHttpClient client = pair.getFirst();
394    HttpClientContext context = pair.getSecond();
395
396    StringEntity entity = new StringEntity(
397        "{\"name\":\"test\", \"ColumnSchema\":[{\"name\":\"f\"}]}", ContentType.APPLICATION_JSON);
398    HttpPut put = new HttpPut("http://localhost:"+ REST_TEST.getServletPort() + "/test/schema");
399    put.setEntity(entity);
400
401
402    UserGroupInformation unprivileged = UserGroupInformation.loginUserFromKeytabAndReturnUGI(
403        CLIENT_PRINCIPAL, clientKeytab.getAbsolutePath());
404    unprivileged.doAs(new PrivilegedExceptionAction<Void>() {
405      @Override
406      public Void run() throws Exception {
407        try (CloseableHttpResponse response = client.execute(put, context)) {
408          final int statusCode = response.getStatusLine().getStatusCode();
409          HttpEntity entity = response.getEntity();
410          assertEquals("Got response: "+ EntityUtils.toString(entity),
411              HttpURLConnection.HTTP_FORBIDDEN, statusCode);
412        }
413        return null;
414      }
415    });
416  }
417
418  private Pair<CloseableHttpClient,HttpClientContext> getClient() {
419    HttpClientConnectionManager pool = new PoolingHttpClientConnectionManager();
420    HttpHost host = new HttpHost("localhost", REST_TEST.getServletPort());
421    Registry<AuthSchemeProvider> authRegistry =
422        RegistryBuilder.<AuthSchemeProvider>create().register(AuthSchemes.SPNEGO,
423            new SPNegoSchemeFactory(true, true)).build();
424    CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
425    credentialsProvider.setCredentials(AuthScope.ANY, EmptyCredentials.INSTANCE);
426    AuthCache authCache = new BasicAuthCache();
427
428    CloseableHttpClient client = HttpClients.custom()
429        .setDefaultAuthSchemeRegistry(authRegistry)
430        .setConnectionManager(pool).build();
431
432    HttpClientContext context = HttpClientContext.create();
433    context.setTargetHost(host);
434    context.setCredentialsProvider(credentialsProvider);
435    context.setAuthSchemeRegistry(authRegistry);
436    context.setAuthCache(authCache);
437
438    return new Pair<>(client, context);
439  }
440
441  private static class EmptyCredentials implements Credentials {
442    public static final EmptyCredentials INSTANCE = new EmptyCredentials();
443
444    @Override public String getPassword() {
445      return null;
446    }
447    @Override public Principal getUserPrincipal() {
448      return null;
449    }
450  }
451}