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}