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.assertTrue; 023 024import java.io.File; 025import java.lang.management.ManagementFactory; 026import java.net.HttpURLConnection; 027import java.net.URL; 028import java.security.PrivilegedExceptionAction; 029import javax.management.ObjectName; 030import org.apache.hadoop.conf.Configuration; 031import org.apache.hadoop.fs.CommonConfigurationKeys; 032import org.apache.hadoop.fs.Path; 033import org.apache.hadoop.hbase.HBaseClassTestRule; 034import org.apache.hadoop.hbase.HBaseTestingUtility; 035import org.apache.hadoop.hbase.HConstants; 036import org.apache.hadoop.hbase.LocalHBaseCluster; 037import org.apache.hadoop.hbase.TableName; 038import org.apache.hadoop.hbase.Waiter; 039import org.apache.hadoop.hbase.coprocessor.CoprocessorHost; 040import org.apache.hadoop.hbase.security.HBaseKerberosUtils; 041import org.apache.hadoop.hbase.security.token.TokenProvider; 042import org.apache.hadoop.hbase.testclassification.MediumTests; 043import org.apache.hadoop.hbase.testclassification.MiscTests; 044import org.apache.hadoop.hbase.util.CommonFSUtils; 045import org.apache.hadoop.hbase.util.Pair; 046import org.apache.hadoop.minikdc.MiniKdc; 047import org.apache.hadoop.security.UserGroupInformation; 048import org.apache.http.auth.AuthSchemeProvider; 049import org.apache.http.auth.AuthScope; 050import org.apache.http.auth.KerberosCredentials; 051import org.apache.http.client.config.AuthSchemes; 052import org.apache.http.client.methods.CloseableHttpResponse; 053import org.apache.http.client.methods.HttpGet; 054import org.apache.http.config.Lookup; 055import org.apache.http.config.RegistryBuilder; 056import org.apache.http.impl.auth.SPNegoSchemeFactory; 057import org.apache.http.impl.client.BasicCredentialsProvider; 058import org.apache.http.impl.client.CloseableHttpClient; 059import org.apache.http.impl.client.HttpClients; 060import org.apache.http.util.EntityUtils; 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.Rule; 069import org.junit.Test; 070import org.junit.experimental.categories.Category; 071import org.junit.rules.TestName; 072import org.slf4j.Logger; 073import org.slf4j.LoggerFactory; 074 075/** 076 * Testing info servers for admin acl. 077 */ 078@Category({ MiscTests.class, MediumTests.class }) 079public class TestInfoServersACL { 080 081 @ClassRule 082 public static final HBaseClassTestRule CLASS_RULE = 083 HBaseClassTestRule.forClass(TestInfoServersACL.class); 084 085 private static final Logger LOG = LoggerFactory.getLogger(TestInfoServersACL.class); 086 private final static HBaseTestingUtility UTIL = new HBaseTestingUtility(); 087 private static Configuration conf; 088 089 protected static String USERNAME; 090 private static LocalHBaseCluster CLUSTER; 091 private static final File KEYTAB_FILE = new File(UTIL.getDataTestDir("keytab").toUri().getPath()); 092 private static MiniKdc KDC; 093 private static String HOST = "localhost"; 094 private static String PRINCIPAL; 095 private static String HTTP_PRINCIPAL; 096 097 @Rule 098 public TestName name = new TestName(); 099 100 // user/group present in hbase.admin.acl 101 private static final String USER_ADMIN_STR = "admin"; 102 103 // user with no permissions 104 private static final String USER_NONE_STR = "none"; 105 106 @BeforeClass 107 public static void beforeClass() throws Exception { 108 conf = UTIL.getConfiguration(); 109 KDC = UTIL.setupMiniKdc(KEYTAB_FILE); 110 USERNAME = UserGroupInformation.getLoginUser().getShortUserName(); 111 PRINCIPAL = USERNAME + "/" + HOST; 112 HTTP_PRINCIPAL = "HTTP/" + HOST; 113 // Create principals for services and the test users 114 KDC.createPrincipal(KEYTAB_FILE, PRINCIPAL, HTTP_PRINCIPAL, USER_ADMIN_STR, USER_NONE_STR); 115 UTIL.startMiniZKCluster(); 116 117 HBaseKerberosUtils.setSecuredConfiguration(conf, PRINCIPAL + "@" + KDC.getRealm(), 118 HTTP_PRINCIPAL + "@" + KDC.getRealm()); 119 HBaseKerberosUtils.setSSLConfiguration(UTIL, TestInfoServersACL.class); 120 121 conf.setStrings(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY, TokenProvider.class.getName()); 122 UTIL.startMiniDFSCluster(1); 123 Path rootdir = UTIL.getDataTestDirOnTestFS("TestInfoServersACL"); 124 CommonFSUtils.setRootDir(conf, rootdir); 125 126 // The info servers do not run in tests by default. 127 // Set them to ephemeral ports so they will start 128 // setup configuration 129 conf.setInt(HConstants.MASTER_INFO_PORT, 0); 130 conf.setInt(HConstants.REGIONSERVER_INFO_PORT, 0); 131 132 conf.set(HttpServer.HTTP_UI_AUTHENTICATION, "kerberos"); 133 conf.set(HttpServer.HTTP_SPNEGO_AUTHENTICATION_PRINCIPAL_KEY, HTTP_PRINCIPAL); 134 conf.set(HttpServer.HTTP_SPNEGO_AUTHENTICATION_KEYTAB_KEY, KEYTAB_FILE.getAbsolutePath()); 135 136 // ACL lists work only when "hadoop.security.authorization" is set to true 137 conf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_AUTHORIZATION, true); 138 // only user admin will have acl access 139 conf.set(HttpServer.HTTP_SPNEGO_AUTHENTICATION_ADMIN_USERS_KEY, USER_ADMIN_STR); 140 // conf.set(HttpServer.FILTER_INITIALIZERS_PROPERTY, ""); 141 142 CLUSTER = new LocalHBaseCluster(conf, 1); 143 CLUSTER.startup(); 144 CLUSTER.getActiveMaster().waitForMetaOnline(); 145 } 146 147 /** 148 * Helper method to shut down the cluster (if running) 149 */ 150 @AfterClass 151 public static void shutDownMiniCluster() throws Exception { 152 if (CLUSTER != null) { 153 CLUSTER.shutdown(); 154 CLUSTER.join(); 155 } 156 if (KDC != null) { 157 KDC.stop(); 158 } 159 UTIL.shutdownMiniCluster(); 160 } 161 162 @Test 163 public void testAuthorizedUser() throws Exception { 164 UserGroupInformation admin = UserGroupInformation 165 .loginUserFromKeytabAndReturnUGI(USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath()); 166 admin.doAs(new PrivilegedExceptionAction<Void>() { 167 @Override 168 public Void run() throws Exception { 169 // Check the expected content is present in the http response 170 String expectedContent = "Get Log Level"; 171 Pair<Integer, String> pair = getLogLevelPage(); 172 assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue()); 173 assertTrue("expected=" + expectedContent + ", content=" + pair.getSecond(), 174 pair.getSecond().contains(expectedContent)); 175 return null; 176 } 177 }); 178 } 179 180 @Test 181 public void testUnauthorizedUser() throws Exception { 182 UserGroupInformation nonAdmin = UserGroupInformation 183 .loginUserFromKeytabAndReturnUGI(USER_NONE_STR, KEYTAB_FILE.getAbsolutePath()); 184 nonAdmin.doAs(new PrivilegedExceptionAction<Void>() { 185 @Override 186 public Void run() throws Exception { 187 Pair<Integer, String> pair = getLogLevelPage(); 188 assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue()); 189 return null; 190 } 191 }); 192 } 193 194 @Test 195 public void testTableActionsAvailableForAdmins() throws Exception { 196 final String expectedAuthorizedContent = "Actions:"; 197 UserGroupInformation admin = UserGroupInformation 198 .loginUserFromKeytabAndReturnUGI(USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath()); 199 admin.doAs(new PrivilegedExceptionAction<Void>() { 200 @Override 201 public Void run() throws Exception { 202 // Check the expected content is present in the http response 203 Pair<Integer, String> pair = getTablePage(TableName.META_TABLE_NAME); 204 assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue()); 205 assertTrue("expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond(), 206 pair.getSecond().contains(expectedAuthorizedContent)); 207 return null; 208 } 209 }); 210 211 UserGroupInformation nonAdmin = UserGroupInformation 212 .loginUserFromKeytabAndReturnUGI(USER_NONE_STR, KEYTAB_FILE.getAbsolutePath()); 213 nonAdmin.doAs(new PrivilegedExceptionAction<Void>() { 214 @Override 215 public Void run() throws Exception { 216 Pair<Integer, String> pair = getTablePage(TableName.META_TABLE_NAME); 217 assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue()); 218 assertFalse( 219 "should not find=" + expectedAuthorizedContent + ", content=" + pair.getSecond(), 220 pair.getSecond().contains(expectedAuthorizedContent)); 221 return null; 222 } 223 }); 224 } 225 226 @Test 227 public void testLogsAvailableForAdmins() throws Exception { 228 final String expectedAuthorizedContent = "Directory: /logs/"; 229 UserGroupInformation admin = UserGroupInformation 230 .loginUserFromKeytabAndReturnUGI(USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath()); 231 admin.doAs(new PrivilegedExceptionAction<Void>() { 232 @Override 233 public Void run() throws Exception { 234 // Check the expected content is present in the http response 235 Pair<Integer, String> pair = getLogsPage(); 236 assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue()); 237 assertTrue("expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond(), 238 pair.getSecond().contains(expectedAuthorizedContent)); 239 return null; 240 } 241 }); 242 243 UserGroupInformation nonAdmin = UserGroupInformation 244 .loginUserFromKeytabAndReturnUGI(USER_NONE_STR, KEYTAB_FILE.getAbsolutePath()); 245 nonAdmin.doAs(new PrivilegedExceptionAction<Void>() { 246 @Override 247 public Void run() throws Exception { 248 Pair<Integer, String> pair = getLogsPage(); 249 assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue()); 250 return null; 251 } 252 }); 253 } 254 255 @Test 256 public void testDumpActionsAvailableForAdmins() throws Exception { 257 final String expectedAuthorizedContent = "Master status for"; 258 UserGroupInformation admin = UserGroupInformation 259 .loginUserFromKeytabAndReturnUGI(USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath()); 260 admin.doAs(new PrivilegedExceptionAction<Void>() { 261 @Override 262 public Void run() throws Exception { 263 // Check the expected content is present in the http response 264 Pair<Integer, String> pair = getMasterDumpPage(); 265 assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue()); 266 assertTrue("expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond(), 267 pair.getSecond().contains(expectedAuthorizedContent)); 268 return null; 269 } 270 }); 271 272 UserGroupInformation nonAdmin = UserGroupInformation 273 .loginUserFromKeytabAndReturnUGI(USER_NONE_STR, KEYTAB_FILE.getAbsolutePath()); 274 nonAdmin.doAs(new PrivilegedExceptionAction<Void>() { 275 @Override 276 public Void run() throws Exception { 277 Pair<Integer, String> pair = getMasterDumpPage(); 278 assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue()); 279 return null; 280 } 281 }); 282 } 283 284 @Test 285 public void testStackActionsAvailableForAdmins() throws Exception { 286 final String expectedAuthorizedContent = "Process Thread Dump"; 287 UserGroupInformation admin = UserGroupInformation 288 .loginUserFromKeytabAndReturnUGI(USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath()); 289 admin.doAs(new PrivilegedExceptionAction<Void>() { 290 @Override 291 public Void run() throws Exception { 292 // Check the expected content is present in the http response 293 Pair<Integer, String> pair = getStacksPage(); 294 assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue()); 295 assertTrue("expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond(), 296 pair.getSecond().contains(expectedAuthorizedContent)); 297 return null; 298 } 299 }); 300 301 UserGroupInformation nonAdmin = UserGroupInformation 302 .loginUserFromKeytabAndReturnUGI(USER_NONE_STR, KEYTAB_FILE.getAbsolutePath()); 303 nonAdmin.doAs(new PrivilegedExceptionAction<Void>() { 304 @Override 305 public Void run() throws Exception { 306 Pair<Integer, String> pair = getStacksPage(); 307 assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue()); 308 return null; 309 } 310 }); 311 } 312 313 @Test 314 public void testJmxAvailableForAdmins() throws Exception { 315 final String expectedAuthorizedContent = "Hadoop:service=HBase"; 316 UTIL.waitFor(30000, new Waiter.Predicate<Exception>() { 317 @Override 318 public boolean evaluate() throws Exception { 319 for (ObjectName name : ManagementFactory.getPlatformMBeanServer() 320 .queryNames(new ObjectName("*:*"), null)) { 321 if (name.toString().contains(expectedAuthorizedContent)) { 322 LOG.info("{}", name); 323 return true; 324 } 325 } 326 return false; 327 } 328 }); 329 UserGroupInformation admin = UserGroupInformation 330 .loginUserFromKeytabAndReturnUGI(USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath()); 331 admin.doAs(new PrivilegedExceptionAction<Void>() { 332 @Override 333 public Void run() throws Exception { 334 // Check the expected content is present in the http response 335 Pair<Integer, String> pair = getJmxPage(); 336 assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue()); 337 assertTrue("expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond(), 338 pair.getSecond().contains(expectedAuthorizedContent)); 339 return null; 340 } 341 }); 342 343 UserGroupInformation nonAdmin = UserGroupInformation 344 .loginUserFromKeytabAndReturnUGI(USER_NONE_STR, KEYTAB_FILE.getAbsolutePath()); 345 nonAdmin.doAs(new PrivilegedExceptionAction<Void>() { 346 @Override 347 public Void run() throws Exception { 348 Pair<Integer, String> pair = getJmxPage(); 349 assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue()); 350 return null; 351 } 352 }); 353 } 354 355 @Test 356 public void testMetricsAvailableForAdmins() throws Exception { 357 // Looks like there's nothing exported to this, but leave it since 358 // it's Hadoop2 only and will eventually be removed due to that. 359 final String expectedAuthorizedContent = ""; 360 UserGroupInformation admin = UserGroupInformation 361 .loginUserFromKeytabAndReturnUGI(USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath()); 362 admin.doAs(new PrivilegedExceptionAction<Void>() { 363 @Override 364 public Void run() throws Exception { 365 // Check the expected content is present in the http response 366 Pair<Integer, String> pair = getMetricsPage(); 367 if (HttpURLConnection.HTTP_NOT_FOUND == pair.getFirst()) { 368 // Not on hadoop 2 369 return null; 370 } 371 assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue()); 372 assertTrue("expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond(), 373 pair.getSecond().contains(expectedAuthorizedContent)); 374 return null; 375 } 376 }); 377 378 UserGroupInformation nonAdmin = UserGroupInformation 379 .loginUserFromKeytabAndReturnUGI(USER_NONE_STR, KEYTAB_FILE.getAbsolutePath()); 380 nonAdmin.doAs(new PrivilegedExceptionAction<Void>() { 381 @Override 382 public Void run() throws Exception { 383 Pair<Integer, String> pair = getMetricsPage(); 384 if (HttpURLConnection.HTTP_NOT_FOUND == pair.getFirst()) { 385 // Not on hadoop 2 386 return null; 387 } 388 assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue()); 389 return null; 390 } 391 }); 392 } 393 394 private String getInfoServerHostAndPort() { 395 return "http://localhost:" + CLUSTER.getActiveMaster().getInfoServer().getPort(); 396 } 397 398 private Pair<Integer, String> getLogLevelPage() throws Exception { 399 // Build the url which we want to connect to 400 URL url = new URL(getInfoServerHostAndPort() + "/logLevel"); 401 return getUrlContent(url); 402 } 403 404 private Pair<Integer, String> getTablePage(TableName tn) throws Exception { 405 URL url = new URL(getInfoServerHostAndPort() + "/table.jsp?name=" + tn.getNameAsString()); 406 return getUrlContent(url); 407 } 408 409 private Pair<Integer, String> getLogsPage() throws Exception { 410 URL url = new URL(getInfoServerHostAndPort() + "/logs/"); 411 return getUrlContent(url); 412 } 413 414 private Pair<Integer, String> getMasterDumpPage() throws Exception { 415 URL url = new URL(getInfoServerHostAndPort() + "/dump"); 416 return getUrlContent(url); 417 } 418 419 private Pair<Integer, String> getStacksPage() throws Exception { 420 URL url = new URL(getInfoServerHostAndPort() + "/stacks"); 421 return getUrlContent(url); 422 } 423 424 private Pair<Integer, String> getJmxPage() throws Exception { 425 URL url = new URL(getInfoServerHostAndPort() + "/jmx"); 426 return getUrlContent(url); 427 } 428 429 private Pair<Integer, String> getMetricsPage() throws Exception { 430 URL url = new URL(getInfoServerHostAndPort() + "/metrics"); 431 return getUrlContent(url); 432 } 433 434 /** 435 * Retrieves the content of the specified URL. The content will only be returned if the status 436 * code for the operation was HTTP 200/OK. 437 */ 438 private Pair<Integer, String> getUrlContent(URL url) throws Exception { 439 try (CloseableHttpClient client = 440 createHttpClient(UserGroupInformation.getCurrentUser().getUserName())) { 441 CloseableHttpResponse resp = client.execute(new HttpGet(url.toURI())); 442 int code = resp.getStatusLine().getStatusCode(); 443 if (code == HttpURLConnection.HTTP_OK) { 444 return new Pair<>(code, EntityUtils.toString(resp.getEntity())); 445 } 446 return new Pair<>(code, null); 447 } 448 } 449 450 private CloseableHttpClient createHttpClient(String clientPrincipal) throws Exception { 451 // Logs in with Kerberos via GSS 452 GSSManager gssManager = GSSManager.getInstance(); 453 // jGSS Kerberos login constant 454 Oid oid = new Oid("1.2.840.113554.1.2.2"); 455 GSSName gssClient = gssManager.createName(clientPrincipal, GSSName.NT_USER_NAME); 456 GSSCredential credential = gssManager.createCredential(gssClient, 457 GSSCredential.DEFAULT_LIFETIME, oid, GSSCredential.INITIATE_ONLY); 458 459 Lookup<AuthSchemeProvider> authRegistry = RegistryBuilder.<AuthSchemeProvider> create() 460 .register(AuthSchemes.SPNEGO, new SPNegoSchemeFactory(true, true)).build(); 461 462 BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); 463 credentialsProvider.setCredentials(AuthScope.ANY, new KerberosCredentials(credential)); 464 465 return HttpClients.custom().setDefaultAuthSchemeRegistry(authRegistry) 466 .setDefaultCredentialsProvider(credentialsProvider).build(); 467 } 468}