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