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; 023import java.io.File; 024import java.lang.management.ManagementFactory; 025import java.net.HttpURLConnection; 026import java.net.URL; 027import java.security.PrivilegedExceptionAction; 028import javax.management.ObjectName; 029import org.apache.hadoop.conf.Configuration; 030import org.apache.hadoop.fs.CommonConfigurationKeys; 031import org.apache.hadoop.fs.Path; 032import org.apache.hadoop.hbase.HBaseClassTestRule; 033import org.apache.hadoop.hbase.HBaseTestingUtility; 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.AfterClass; 065import org.junit.BeforeClass; 066import org.junit.ClassRule; 067import org.junit.Rule; 068import org.junit.Test; 069import org.junit.experimental.categories.Category; 070import org.junit.rules.TestName; 071import org.slf4j.Logger; 072import org.slf4j.LoggerFactory; 073 074/** 075 * Testing info servers for admin acl. 076 */ 077@Category({ MiscTests.class, MediumTests.class }) 078public class TestInfoServersACL { 079 080 @ClassRule 081 public static final HBaseClassTestRule CLASS_RULE = 082 HBaseClassTestRule.forClass(TestInfoServersACL.class); 083 084 private static final Logger LOG = LoggerFactory.getLogger(TestInfoServersACL.class); 085 private final static HBaseTestingUtility UTIL = new HBaseTestingUtility(); 086 private static Configuration conf; 087 088 protected static String USERNAME; 089 private static LocalHBaseCluster CLUSTER; 090 private static final File KEYTAB_FILE = new File(UTIL.getDataTestDir("keytab").toUri().getPath()); 091 private static MiniKdc KDC; 092 private static String HOST = "localhost"; 093 private static String PRINCIPAL; 094 private static String HTTP_PRINCIPAL; 095 096 @Rule 097 public TestName name = new TestName(); 098 099 // user/group present in hbase.admin.acl 100 private static final String USER_ADMIN_STR = "admin"; 101 102 // user with no permissions 103 private static final String USER_NONE_STR = "none"; 104 105 @BeforeClass 106 public static void beforeClass() throws Exception { 107 conf = UTIL.getConfiguration(); 108 KDC = UTIL.setupMiniKdc(KEYTAB_FILE); 109 USERNAME = UserGroupInformation.getLoginUser().getShortUserName(); 110 PRINCIPAL = USERNAME + "/" + HOST; 111 HTTP_PRINCIPAL = "HTTP/" + HOST; 112 // Create principals for services and the test users 113 KDC.createPrincipal(KEYTAB_FILE, PRINCIPAL, HTTP_PRINCIPAL, USER_ADMIN_STR, USER_NONE_STR); 114 UTIL.startMiniZKCluster(); 115 116 HBaseKerberosUtils.setSecuredConfiguration(conf, 117 PRINCIPAL + "@" + KDC.getRealm(), HTTP_PRINCIPAL + "@" + KDC.getRealm()); 118 HBaseKerberosUtils.setSSLConfiguration(UTIL, TestInfoServersACL.class); 119 120 conf.setStrings(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY, 121 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.loginUserFromKeytabAndReturnUGI( 165 USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath()); 166 admin.doAs(new PrivilegedExceptionAction<Void>() { 167 @Override public Void run() throws Exception { 168 // Check the expected content is present in the http response 169 String expectedContent = "Get Log Level"; 170 Pair<Integer,String> pair = getLogLevelPage(); 171 assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue()); 172 assertTrue("expected=" + expectedContent + ", content=" + pair.getSecond(), 173 pair.getSecond().contains(expectedContent)); 174 return null; 175 } 176 }); 177 } 178 179 @Test 180 public void testUnauthorizedUser() throws Exception { 181 UserGroupInformation nonAdmin = UserGroupInformation.loginUserFromKeytabAndReturnUGI( 182 USER_NONE_STR, KEYTAB_FILE.getAbsolutePath()); 183 nonAdmin.doAs(new PrivilegedExceptionAction<Void>() { 184 @Override public Void run() throws Exception { 185 Pair<Integer,String> pair = getLogLevelPage(); 186 assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue()); 187 return null; 188 } 189 }); 190 } 191 192 @Test 193 public void testTableActionsAvailableForAdmins() throws Exception { 194 final String expectedAuthorizedContent = "Actions:"; 195 UserGroupInformation admin = UserGroupInformation.loginUserFromKeytabAndReturnUGI( 196 USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath()); 197 admin.doAs(new PrivilegedExceptionAction<Void>() { 198 @Override public Void run() throws Exception { 199 // Check the expected content is present in the http response 200 Pair<Integer,String> pair = getTablePage(TableName.META_TABLE_NAME); 201 assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue()); 202 assertTrue("expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond(), 203 pair.getSecond().contains(expectedAuthorizedContent)); 204 return null; 205 } 206 }); 207 208 UserGroupInformation nonAdmin = UserGroupInformation.loginUserFromKeytabAndReturnUGI( 209 USER_NONE_STR, KEYTAB_FILE.getAbsolutePath()); 210 nonAdmin.doAs(new PrivilegedExceptionAction<Void>() { 211 @Override public Void run() throws Exception { 212 Pair<Integer,String> pair = getTablePage(TableName.META_TABLE_NAME); 213 assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue()); 214 assertFalse("should not find=" + expectedAuthorizedContent + ", content=" + 215 pair.getSecond(), pair.getSecond().contains(expectedAuthorizedContent)); 216 return null; 217 } 218 }); 219 } 220 221 @Test 222 public void testLogsAvailableForAdmins() throws Exception { 223 final String expectedAuthorizedContent = "Directory: /logs/"; 224 UserGroupInformation admin = UserGroupInformation.loginUserFromKeytabAndReturnUGI( 225 USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath()); 226 admin.doAs(new PrivilegedExceptionAction<Void>() { 227 @Override public Void run() throws Exception { 228 // Check the expected content is present in the http response 229 Pair<Integer,String> pair = getLogsPage(); 230 assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue()); 231 assertTrue("expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond(), 232 pair.getSecond().contains(expectedAuthorizedContent)); 233 return null; 234 } 235 }); 236 237 UserGroupInformation nonAdmin = UserGroupInformation.loginUserFromKeytabAndReturnUGI( 238 USER_NONE_STR, KEYTAB_FILE.getAbsolutePath()); 239 nonAdmin.doAs(new PrivilegedExceptionAction<Void>() { 240 @Override public Void run() throws Exception { 241 Pair<Integer,String> pair = getLogsPage(); 242 assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue()); 243 return null; 244 } 245 }); 246 } 247 248 @Test 249 public void testDumpActionsAvailableForAdmins() throws Exception { 250 final String expectedAuthorizedContent = "Master status for"; 251 UserGroupInformation admin = UserGroupInformation.loginUserFromKeytabAndReturnUGI( 252 USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath()); 253 admin.doAs(new PrivilegedExceptionAction<Void>() { 254 @Override public Void run() throws Exception { 255 // Check the expected content is present in the http response 256 Pair<Integer,String> pair = getMasterDumpPage(); 257 assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue()); 258 assertTrue("expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond(), 259 pair.getSecond().contains(expectedAuthorizedContent)); 260 return null; 261 } 262 }); 263 264 UserGroupInformation nonAdmin = UserGroupInformation.loginUserFromKeytabAndReturnUGI( 265 USER_NONE_STR, KEYTAB_FILE.getAbsolutePath()); 266 nonAdmin.doAs(new PrivilegedExceptionAction<Void>() { 267 @Override public Void run() throws Exception { 268 Pair<Integer,String> pair = getMasterDumpPage(); 269 assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue()); 270 return null; 271 } 272 }); 273 } 274 275 @Test 276 public void testStackActionsAvailableForAdmins() throws Exception { 277 final String expectedAuthorizedContent = "Process Thread Dump"; 278 UserGroupInformation admin = UserGroupInformation.loginUserFromKeytabAndReturnUGI( 279 USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath()); 280 admin.doAs(new PrivilegedExceptionAction<Void>() { 281 @Override public Void run() throws Exception { 282 // Check the expected content is present in the http response 283 Pair<Integer,String> pair = getStacksPage(); 284 assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue()); 285 assertTrue("expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond(), 286 pair.getSecond().contains(expectedAuthorizedContent)); 287 return null; 288 } 289 }); 290 291 UserGroupInformation nonAdmin = UserGroupInformation.loginUserFromKeytabAndReturnUGI( 292 USER_NONE_STR, KEYTAB_FILE.getAbsolutePath()); 293 nonAdmin.doAs(new PrivilegedExceptionAction<Void>() { 294 @Override 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.loginUserFromKeytabAndReturnUGI( 319 USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath()); 320 admin.doAs(new PrivilegedExceptionAction<Void>() { 321 @Override public Void run() throws Exception { 322 // Check the expected content is present in the http response 323 Pair<Integer,String> pair = getJmxPage(); 324 assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue()); 325 assertTrue("expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond(), 326 pair.getSecond().contains(expectedAuthorizedContent)); 327 return null; 328 } 329 }); 330 331 UserGroupInformation nonAdmin = UserGroupInformation.loginUserFromKeytabAndReturnUGI( 332 USER_NONE_STR, KEYTAB_FILE.getAbsolutePath()); 333 nonAdmin.doAs(new PrivilegedExceptionAction<Void>() { 334 @Override public Void run() throws Exception { 335 Pair<Integer,String> pair = getJmxPage(); 336 assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue()); 337 return null; 338 } 339 }); 340 } 341 342 @Test 343 public void testMetricsAvailableForAdmins() throws Exception { 344 // Looks like there's nothing exported to this, but leave it since 345 // it's Hadoop2 only and will eventually be removed due to that. 346 final String expectedAuthorizedContent = ""; 347 UserGroupInformation admin = UserGroupInformation.loginUserFromKeytabAndReturnUGI( 348 USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath()); 349 admin.doAs(new PrivilegedExceptionAction<Void>() { 350 @Override public Void run() throws Exception { 351 // Check the expected content is present in the http response 352 Pair<Integer,String> pair = getMetricsPage(); 353 if (HttpURLConnection.HTTP_NOT_FOUND == pair.getFirst()) { 354 // Not on hadoop 2 355 return null; 356 } 357 assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue()); 358 assertTrue("expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond(), 359 pair.getSecond().contains(expectedAuthorizedContent)); 360 return null; 361 } 362 }); 363 364 UserGroupInformation nonAdmin = UserGroupInformation.loginUserFromKeytabAndReturnUGI( 365 USER_NONE_STR, KEYTAB_FILE.getAbsolutePath()); 366 nonAdmin.doAs(new PrivilegedExceptionAction<Void>() { 367 @Override public Void run() throws Exception { 368 Pair<Integer,String> pair = getMetricsPage(); 369 if (HttpURLConnection.HTTP_NOT_FOUND == pair.getFirst()) { 370 // Not on hadoop 2 371 return null; 372 } 373 assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue()); 374 return null; 375 } 376 }); 377 } 378 379 private String getInfoServerHostAndPort() { 380 return "http://localhost:" + CLUSTER.getActiveMaster().getInfoServer().getPort(); 381 } 382 383 private Pair<Integer,String> getLogLevelPage() throws Exception { 384 // Build the url which we want to connect to 385 URL url = new URL(getInfoServerHostAndPort() + "/logLevel"); 386 return getUrlContent(url); 387 } 388 389 private Pair<Integer,String> getTablePage(TableName tn) throws Exception { 390 URL url = new URL(getInfoServerHostAndPort() + "/table.jsp?name=" + tn.getNameAsString()); 391 return getUrlContent(url); 392 } 393 394 private Pair<Integer,String> getLogsPage() throws Exception { 395 URL url = new URL(getInfoServerHostAndPort() + "/logs/"); 396 return getUrlContent(url); 397 } 398 399 private Pair<Integer,String> getMasterDumpPage() throws Exception { 400 URL url = new URL(getInfoServerHostAndPort() + "/dump"); 401 return getUrlContent(url); 402 } 403 404 private Pair<Integer,String> getStacksPage() throws Exception { 405 URL url = new URL(getInfoServerHostAndPort() + "/stacks"); 406 return getUrlContent(url); 407 } 408 409 private Pair<Integer,String> getJmxPage() throws Exception { 410 URL url = new URL(getInfoServerHostAndPort() + "/jmx"); 411 return getUrlContent(url); 412 } 413 414 private Pair<Integer,String> getMetricsPage() throws Exception { 415 URL url = new URL(getInfoServerHostAndPort() + "/metrics"); 416 return getUrlContent(url); 417 } 418 419 /** 420 * Retrieves the content of the specified URL. The content will only be returned if the status 421 * code for the operation was HTTP 200/OK. 422 */ 423 private Pair<Integer,String> getUrlContent(URL url) throws Exception { 424 try (CloseableHttpClient client = createHttpClient( 425 UserGroupInformation.getCurrentUser().getUserName())) { 426 CloseableHttpResponse resp = client.execute(new HttpGet(url.toURI())); 427 int code = resp.getStatusLine().getStatusCode(); 428 if (code == HttpURLConnection.HTTP_OK) { 429 return new Pair<>(code, EntityUtils.toString(resp.getEntity())); 430 } 431 return new Pair<>(code, null); 432 } 433 } 434 435 private CloseableHttpClient createHttpClient(String clientPrincipal) throws Exception { 436 // Logs in with Kerberos via GSS 437 GSSManager gssManager = GSSManager.getInstance(); 438 // jGSS Kerberos login constant 439 Oid oid = new Oid("1.2.840.113554.1.2.2"); 440 GSSName gssClient = gssManager.createName(clientPrincipal, GSSName.NT_USER_NAME); 441 GSSCredential credential = gssManager.createCredential( 442 gssClient, GSSCredential.DEFAULT_LIFETIME, oid, GSSCredential.INITIATE_ONLY); 443 444 Lookup<AuthSchemeProvider> authRegistry = RegistryBuilder.<AuthSchemeProvider>create() 445 .register(AuthSchemes.SPNEGO, new SPNegoSchemeFactory(true, true)).build(); 446 447 BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); 448 credentialsProvider.setCredentials(AuthScope.ANY, new KerberosCredentials(credential)); 449 450 return HttpClients.custom().setDefaultAuthSchemeRegistry(authRegistry) 451 .setDefaultCredentialsProvider(credentialsProvider).build(); 452 } 453}