/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.hadoop.hbase.http; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.io.File; import java.lang.management.ManagementFactory; import java.net.HttpURLConnection; import java.net.URL; import java.security.PrivilegedExceptionAction; import javax.management.ObjectName; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.CommonConfigurationKeys; import org.apache.hadoop.fs.Path; import org.apache.hadoop.hbase.HBaseClassTestRule; import org.apache.hadoop.hbase.HBaseTestingUtility; import org.apache.hadoop.hbase.HConstants; import org.apache.hadoop.hbase.LocalHBaseCluster; import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.Waiter; import org.apache.hadoop.hbase.coprocessor.CoprocessorHost; import org.apache.hadoop.hbase.security.HBaseKerberosUtils; import org.apache.hadoop.hbase.security.token.TokenProvider; import org.apache.hadoop.hbase.testclassification.MediumTests; import org.apache.hadoop.hbase.testclassification.MiscTests; import org.apache.hadoop.hbase.util.CommonFSUtils; import org.apache.hadoop.hbase.util.Pair; import org.apache.hadoop.minikdc.MiniKdc; import org.apache.hadoop.security.UserGroupInformation; import org.apache.http.auth.AuthSchemeProvider; import org.apache.http.auth.AuthScope; import org.apache.http.auth.KerberosCredentials; import org.apache.http.client.config.AuthSchemes; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.config.Lookup; import org.apache.http.config.RegistryBuilder; import org.apache.http.impl.auth.SPNegoSchemeFactory; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import org.ietf.jgss.GSSCredential; import org.ietf.jgss.GSSManager; import org.ietf.jgss.GSSName; import org.ietf.jgss.Oid; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.rules.TestName; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Testing info servers for admin acl. */ @Category({ MiscTests.class, MediumTests.class }) public class TestInfoServersACL { @ClassRule public static final HBaseClassTestRule CLASS_RULE = HBaseClassTestRule.forClass(TestInfoServersACL.class); private static final Logger LOG = LoggerFactory.getLogger(TestInfoServersACL.class); private final static HBaseTestingUtility UTIL = new HBaseTestingUtility(); private static Configuration conf; protected static String USERNAME; private static LocalHBaseCluster CLUSTER; private static final File KEYTAB_FILE = new File(UTIL.getDataTestDir("keytab").toUri().getPath()); private static MiniKdc KDC; private static String HOST = "localhost"; private static String PRINCIPAL; private static String HTTP_PRINCIPAL; @Rule public TestName name = new TestName(); // user/group present in hbase.admin.acl private static final String USER_ADMIN_STR = "admin"; // user with no permissions private static final String USER_NONE_STR = "none"; @BeforeClass public static void beforeClass() throws Exception { conf = UTIL.getConfiguration(); KDC = UTIL.setupMiniKdc(KEYTAB_FILE); USERNAME = UserGroupInformation.getLoginUser().getShortUserName(); PRINCIPAL = USERNAME + "/" + HOST; HTTP_PRINCIPAL = "HTTP/" + HOST; // Create principals for services and the test users KDC.createPrincipal(KEYTAB_FILE, PRINCIPAL, HTTP_PRINCIPAL, USER_ADMIN_STR, USER_NONE_STR); UTIL.startMiniZKCluster(); HBaseKerberosUtils.setSecuredConfiguration(conf, PRINCIPAL + "@" + KDC.getRealm(), HTTP_PRINCIPAL + "@" + KDC.getRealm()); HBaseKerberosUtils.setSSLConfiguration(UTIL, TestInfoServersACL.class); conf.setStrings(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY, TokenProvider.class.getName()); UTIL.startMiniDFSCluster(1); Path rootdir = UTIL.getDataTestDirOnTestFS("TestInfoServersACL"); CommonFSUtils.setRootDir(conf, rootdir); // The info servers do not run in tests by default. // Set them to ephemeral ports so they will start // setup configuration conf.setInt(HConstants.MASTER_INFO_PORT, 0); conf.setInt(HConstants.REGIONSERVER_INFO_PORT, 0); conf.set(HttpServer.HTTP_UI_AUTHENTICATION, "kerberos"); conf.set(HttpServer.HTTP_SPNEGO_AUTHENTICATION_PRINCIPAL_KEY, HTTP_PRINCIPAL); conf.set(HttpServer.HTTP_SPNEGO_AUTHENTICATION_KEYTAB_KEY, KEYTAB_FILE.getAbsolutePath()); // ACL lists work only when "hadoop.security.authorization" is set to true conf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_AUTHORIZATION, true); // only user admin will have acl access conf.set(HttpServer.HTTP_SPNEGO_AUTHENTICATION_ADMIN_USERS_KEY, USER_ADMIN_STR); //conf.set(HttpServer.FILTER_INITIALIZERS_PROPERTY, ""); CLUSTER = new LocalHBaseCluster(conf, 1); CLUSTER.startup(); CLUSTER.getActiveMaster().waitForMetaOnline(); } /** * Helper method to shut down the cluster (if running) */ @AfterClass public static void shutDownMiniCluster() throws Exception { if (CLUSTER != null) { CLUSTER.shutdown(); CLUSTER.join(); } if (KDC != null) { KDC.stop(); } UTIL.shutdownMiniCluster(); } @Test public void testAuthorizedUser() throws Exception { UserGroupInformation admin = UserGroupInformation.loginUserFromKeytabAndReturnUGI( USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath()); admin.doAs(new PrivilegedExceptionAction<Void>() { @Override public Void run() throws Exception { // Check the expected content is present in the http response String expectedContent = "Get Log Level"; Pair<Integer,String> pair = getLogLevelPage(); assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue()); assertTrue("expected=" + expectedContent + ", content=" + pair.getSecond(), pair.getSecond().contains(expectedContent)); return null; } }); } @Test public void testUnauthorizedUser() throws Exception { UserGroupInformation nonAdmin = UserGroupInformation.loginUserFromKeytabAndReturnUGI( USER_NONE_STR, KEYTAB_FILE.getAbsolutePath()); nonAdmin.doAs(new PrivilegedExceptionAction<Void>() { @Override public Void run() throws Exception { Pair<Integer,String> pair = getLogLevelPage(); assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue()); return null; } }); } @Test public void testTableActionsAvailableForAdmins() throws Exception { final String expectedAuthorizedContent = "Actions:"; UserGroupInformation admin = UserGroupInformation.loginUserFromKeytabAndReturnUGI( USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath()); admin.doAs(new PrivilegedExceptionAction<Void>() { @Override public Void run() throws Exception { // Check the expected content is present in the http response Pair<Integer,String> pair = getTablePage(TableName.META_TABLE_NAME); assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue()); assertTrue("expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond(), pair.getSecond().contains(expectedAuthorizedContent)); return null; } }); UserGroupInformation nonAdmin = UserGroupInformation.loginUserFromKeytabAndReturnUGI( USER_NONE_STR, KEYTAB_FILE.getAbsolutePath()); nonAdmin.doAs(new PrivilegedExceptionAction<Void>() { @Override public Void run() throws Exception { Pair<Integer,String> pair = getTablePage(TableName.META_TABLE_NAME); assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue()); assertFalse("should not find=" + expectedAuthorizedContent + ", content=" + pair.getSecond(), pair.getSecond().contains(expectedAuthorizedContent)); return null; } }); } @Test public void testLogsAvailableForAdmins() throws Exception { final String expectedAuthorizedContent = "Directory: /logs/"; UserGroupInformation admin = UserGroupInformation.loginUserFromKeytabAndReturnUGI( USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath()); admin.doAs(new PrivilegedExceptionAction<Void>() { @Override public Void run() throws Exception { // Check the expected content is present in the http response Pair<Integer,String> pair = getLogsPage(); assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue()); assertTrue("expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond(), pair.getSecond().contains(expectedAuthorizedContent)); return null; } }); UserGroupInformation nonAdmin = UserGroupInformation.loginUserFromKeytabAndReturnUGI( USER_NONE_STR, KEYTAB_FILE.getAbsolutePath()); nonAdmin.doAs(new PrivilegedExceptionAction<Void>() { @Override public Void run() throws Exception { Pair<Integer,String> pair = getLogsPage(); assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue()); return null; } }); } @Test public void testDumpActionsAvailableForAdmins() throws Exception { final String expectedAuthorizedContent = "Master status for"; UserGroupInformation admin = UserGroupInformation.loginUserFromKeytabAndReturnUGI( USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath()); admin.doAs(new PrivilegedExceptionAction<Void>() { @Override public Void run() throws Exception { // Check the expected content is present in the http response Pair<Integer,String> pair = getMasterDumpPage(); assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue()); assertTrue("expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond(), pair.getSecond().contains(expectedAuthorizedContent)); return null; } }); UserGroupInformation nonAdmin = UserGroupInformation.loginUserFromKeytabAndReturnUGI( USER_NONE_STR, KEYTAB_FILE.getAbsolutePath()); nonAdmin.doAs(new PrivilegedExceptionAction<Void>() { @Override public Void run() throws Exception { Pair<Integer,String> pair = getMasterDumpPage(); assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue()); return null; } }); } @Test public void testStackActionsAvailableForAdmins() throws Exception { final String expectedAuthorizedContent = "Process Thread Dump"; UserGroupInformation admin = UserGroupInformation.loginUserFromKeytabAndReturnUGI( USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath()); admin.doAs(new PrivilegedExceptionAction<Void>() { @Override public Void run() throws Exception { // Check the expected content is present in the http response Pair<Integer,String> pair = getStacksPage(); assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue()); assertTrue("expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond(), pair.getSecond().contains(expectedAuthorizedContent)); return null; } }); UserGroupInformation nonAdmin = UserGroupInformation.loginUserFromKeytabAndReturnUGI( USER_NONE_STR, KEYTAB_FILE.getAbsolutePath()); nonAdmin.doAs(new PrivilegedExceptionAction<Void>() { @Override public Void run() throws Exception { Pair<Integer,String> pair = getStacksPage(); assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue()); return null; } }); } @Test public void testJmxAvailableForAdmins() throws Exception { final String expectedAuthorizedContent = "Hadoop:service=HBase"; UTIL.waitFor(30000, new Waiter.Predicate<Exception>() { @Override public boolean evaluate() throws Exception { for (ObjectName name: ManagementFactory.getPlatformMBeanServer(). queryNames(new ObjectName("*:*"), null)) { if (name.toString().contains(expectedAuthorizedContent)) { LOG.info("{}", name); return true; } } return false; } }); UserGroupInformation admin = UserGroupInformation.loginUserFromKeytabAndReturnUGI( USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath()); admin.doAs(new PrivilegedExceptionAction<Void>() { @Override public Void run() throws Exception { // Check the expected content is present in the http response Pair<Integer,String> pair = getJmxPage(); assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue()); assertTrue("expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond(), pair.getSecond().contains(expectedAuthorizedContent)); return null; } }); UserGroupInformation nonAdmin = UserGroupInformation.loginUserFromKeytabAndReturnUGI( USER_NONE_STR, KEYTAB_FILE.getAbsolutePath()); nonAdmin.doAs(new PrivilegedExceptionAction<Void>() { @Override public Void run() throws Exception { Pair<Integer,String> pair = getJmxPage(); assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue()); return null; } }); } @Test public void testMetricsAvailableForAdmins() throws Exception { // Looks like there's nothing exported to this, but leave it since // it's Hadoop2 only and will eventually be removed due to that. final String expectedAuthorizedContent = ""; UserGroupInformation admin = UserGroupInformation.loginUserFromKeytabAndReturnUGI( USER_ADMIN_STR, KEYTAB_FILE.getAbsolutePath()); admin.doAs(new PrivilegedExceptionAction<Void>() { @Override public Void run() throws Exception { // Check the expected content is present in the http response Pair<Integer,String> pair = getMetricsPage(); if (HttpURLConnection.HTTP_NOT_FOUND == pair.getFirst()) { // Not on hadoop 2 return null; } assertEquals(HttpURLConnection.HTTP_OK, pair.getFirst().intValue()); assertTrue("expected=" + expectedAuthorizedContent + ", content=" + pair.getSecond(), pair.getSecond().contains(expectedAuthorizedContent)); return null; } }); UserGroupInformation nonAdmin = UserGroupInformation.loginUserFromKeytabAndReturnUGI( USER_NONE_STR, KEYTAB_FILE.getAbsolutePath()); nonAdmin.doAs(new PrivilegedExceptionAction<Void>() { @Override public Void run() throws Exception { Pair<Integer,String> pair = getMetricsPage(); if (HttpURLConnection.HTTP_NOT_FOUND == pair.getFirst()) { // Not on hadoop 2 return null; } assertEquals(HttpURLConnection.HTTP_FORBIDDEN, pair.getFirst().intValue()); return null; } }); } private String getInfoServerHostAndPort() { return "http://localhost:" + CLUSTER.getActiveMaster().getInfoServer().getPort(); } private Pair<Integer,String> getLogLevelPage() throws Exception { // Build the url which we want to connect to URL url = new URL(getInfoServerHostAndPort() + "/logLevel"); return getUrlContent(url); } private Pair<Integer,String> getTablePage(TableName tn) throws Exception { URL url = new URL(getInfoServerHostAndPort() + "/table.jsp?name=" + tn.getNameAsString()); return getUrlContent(url); } private Pair<Integer,String> getLogsPage() throws Exception { URL url = new URL(getInfoServerHostAndPort() + "/logs/"); return getUrlContent(url); } private Pair<Integer,String> getMasterDumpPage() throws Exception { URL url = new URL(getInfoServerHostAndPort() + "/dump"); return getUrlContent(url); } private Pair<Integer,String> getStacksPage() throws Exception { URL url = new URL(getInfoServerHostAndPort() + "/stacks"); return getUrlContent(url); } private Pair<Integer,String> getJmxPage() throws Exception { URL url = new URL(getInfoServerHostAndPort() + "/jmx"); return getUrlContent(url); } private Pair<Integer,String> getMetricsPage() throws Exception { URL url = new URL(getInfoServerHostAndPort() + "/metrics"); return getUrlContent(url); } /** * Retrieves the content of the specified URL. The content will only be returned if the status * code for the operation was HTTP 200/OK. */ private Pair<Integer,String> getUrlContent(URL url) throws Exception { try (CloseableHttpClient client = createHttpClient( UserGroupInformation.getCurrentUser().getUserName())) { CloseableHttpResponse resp = client.execute(new HttpGet(url.toURI())); int code = resp.getStatusLine().getStatusCode(); if (code == HttpURLConnection.HTTP_OK) { return new Pair<>(code, EntityUtils.toString(resp.getEntity())); } return new Pair<>(code, null); } } private CloseableHttpClient createHttpClient(String clientPrincipal) throws Exception { // Logs in with Kerberos via GSS GSSManager gssManager = GSSManager.getInstance(); // jGSS Kerberos login constant Oid oid = new Oid("1.2.840.113554.1.2.2"); GSSName gssClient = gssManager.createName(clientPrincipal, GSSName.NT_USER_NAME); GSSCredential credential = gssManager.createCredential( gssClient, GSSCredential.DEFAULT_LIFETIME, oid, GSSCredential.INITIATE_ONLY); Lookup<AuthSchemeProvider> authRegistry = RegistryBuilder.<AuthSchemeProvider>create() .register(AuthSchemes.SPNEGO, new SPNegoSchemeFactory(true, true)).build(); BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials(AuthScope.ANY, new KerberosCredentials(credential)); return HttpClients.custom().setDefaultAuthSchemeRegistry(authRegistry) .setDefaultCredentialsProvider(credentialsProvider).build(); } }