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