blob: 4016e7bd47c56d02f6ecdbcc2151d34399ed287f [file] [log] [blame]
/*
* 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();
}
}