This is an automated email from the ASF dual-hosted git repository.
reidchan pushed a commit to branch branch-1.4
in repository https://gitbox.apache.org/repos/asf/hbase.git
The following commit(s) were added to refs/heads/branch-1.4 by this push:
new 7e09a7f HBASE-22184 [security] Support get|set LogLevel in HTTPS mode
7e09a7f is described below
commit 7e09a7f6840b9e6da385823210c1acd37a93bcb7
Author: Reid Chan <[email protected]>
AuthorDate: Fri May 17 19:01:53 2019 +0800
HBASE-22184 [security] Support get|set LogLevel in HTTPS mode
---
.../org/apache/hadoop/hbase/http/log/LogLevel.java | 61 ++++++++-
.../apache/hadoop/hbase/http/log/TestLogLevel.java | 150 ++++++++++++++++++---
2 files changed, 185 insertions(+), 26 deletions(-)
diff --git
a/hbase-server/src/main/java/org/apache/hadoop/hbase/http/log/LogLevel.java
b/hbase-server/src/main/java/org/apache/hadoop/hbase/http/log/LogLevel.java
index 328e1b1..88ac391 100644
--- a/hbase-server/src/main/java/org/apache/hadoop/hbase/http/log/LogLevel.java
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/http/log/LogLevel.java
@@ -28,6 +28,8 @@ import java.net.URL;
import java.net.URLConnection;
import java.util.regex.Pattern;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLSocketFactory;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
@@ -45,6 +47,7 @@ import
org.apache.hadoop.hbase.classification.InterfaceStability;
import org.apache.hadoop.hbase.http.HttpServer;
import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
import org.apache.hadoop.security.authentication.client.KerberosAuthenticator;
+import org.apache.hadoop.security.ssl.SSLFactory;
import org.apache.hadoop.util.ServletUtil;
import org.apache.hadoop.util.Tool;
@@ -54,10 +57,12 @@ import org.apache.hadoop.util.Tool;
@InterfaceStability.Evolving
public class LogLevel {
private static final String USAGES = "\nUsage: General options are:\n"
- + "\t[-getlevel <host:port> <classname>\n"
- + "\t[-setlevel <host:port> <classname> <level> ";
+ + "\t[-getlevel <host:port> <classname> [-protocol (http|https)]\n"
+ + "\t[-setlevel <host:port> <classname> <level> [-protocol
(http|https)]";
public static final String PROTOCOL_HTTP = "http";
+ public static final String PROTOCOL_HTTPS = "https";
+
/**
* A command line implementation
*/
@@ -80,9 +85,14 @@ public class LogLevel {
System.exit(-1);
}
+ public static boolean isValidProtocol(String protocol) {
+ return ((protocol.equals(PROTOCOL_HTTP) ||
protocol.equals(PROTOCOL_HTTPS)));
+ }
+
@VisibleForTesting
static class CLI extends Configured implements Tool {
private Operations operation = Operations.UNKNOWN;
+ private String protocol;
private String hostName;
private String className;
private String level;
@@ -136,6 +146,9 @@ public class LogLevel {
case "-setlevel":
nextArgIndex = parseSetLevelArgs(args, nextArgIndex);
break;
+ case "-protocol":
+ nextArgIndex = parseProtocolArgs(args, nextArgIndex);
+ break;
default:
throw new HadoopIllegalArgumentException(
"Unexpected argument " + args[nextArgIndex]);
@@ -147,6 +160,11 @@ public class LogLevel {
throw new HadoopIllegalArgumentException(
"Must specify either -getlevel or -setlevel");
}
+
+ // if protocol is unspecified, set it as http.
+ if (protocol == null) {
+ protocol = PROTOCOL_HTTP;
+ }
}
private int parseGetLevelArgs(String[] args, int index) throws
@@ -182,6 +200,24 @@ public class LogLevel {
return index + 4;
}
+ private int parseProtocolArgs(String[] args, int index) throws
+ HadoopIllegalArgumentException {
+ // make sure only -protocol is specified
+ if (protocol != null) {
+ throw new HadoopIllegalArgumentException("Redundant -protocol
command");
+ }
+ // check number of arguments is sufficient
+ if (index + 1 >= args.length) {
+ throw new HadoopIllegalArgumentException("-protocol needs one
parameter");
+ }
+ // check protocol is valid
+ protocol = args[index + 1];
+ if (!isValidProtocol(protocol)) {
+ throw new HadoopIllegalArgumentException("Invalid protocol: " +
protocol);
+ }
+ return index + 2;
+ }
+
/**
* Send HTTP request to get log level.
*
@@ -189,7 +225,7 @@ public class LogLevel {
* @throws Exception if unable to connect
*/
private void doGetLevel() throws Exception {
- process(PROTOCOL_HTTP + "://" + hostName + "/logLevel?log=" + className);
+ process(protocol + "://" + hostName + "/logLevel?log=" + className);
}
/**
@@ -199,7 +235,7 @@ public class LogLevel {
* @throws Exception if unable to connect
*/
private void doSetLevel() throws Exception {
- process(PROTOCOL_HTTP + "://" + hostName + "/logLevel?log=" + className
+ process(protocol + "://" + hostName + "/logLevel?log=" + className
+ "&level=" + level);
}
@@ -215,10 +251,23 @@ public class LogLevel {
private URLConnection connect(URL url) throws Exception {
AuthenticatedURL.Token token = new AuthenticatedURL.Token();
AuthenticatedURL aUrl;
+ SSLFactory clientSslFactory;
URLConnection connection;
- aUrl = new AuthenticatedURL(new KerberosAuthenticator());
- connection = aUrl.openConnection(url, token);
+ // If https is chosen, configures SSL client.
+ if (PROTOCOL_HTTPS.equals(url.getProtocol())) {
+ clientSslFactory = new SSLFactory(SSLFactory.Mode.CLIENT,
this.getConf());
+ clientSslFactory.init();
+ SSLSocketFactory sslSocketF =
clientSslFactory.createSSLSocketFactory();
+
+ aUrl = new AuthenticatedURL(new KerberosAuthenticator(),
clientSslFactory);
+ connection = aUrl.openConnection(url, token);
+ HttpsURLConnection httpsConn = (HttpsURLConnection) connection;
+ httpsConn.setSSLSocketFactory(sslSocketF);
+ } else {
+ aUrl = new AuthenticatedURL(new KerberosAuthenticator());
+ connection = aUrl.openConnection(url, token);
+ }
connection.connect();
return connection;
}
diff --git
a/hbase-server/src/test/java/org/apache/hadoop/hbase/http/log/TestLogLevel.java
b/hbase-server/src/test/java/org/apache/hadoop/hbase/http/log/TestLogLevel.java
index c00d13d..ae3c98f 100644
---
a/hbase-server/src/test/java/org/apache/hadoop/hbase/http/log/TestLogLevel.java
+++
b/hbase-server/src/test/java/org/apache/hadoop/hbase/http/log/TestLogLevel.java
@@ -17,17 +17,19 @@
*/
package org.apache.hadoop.hbase.http.log;
-import static org.apache.hadoop.hbase.http.log.LogLevel.PROTOCOL_HTTP;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import java.io.File;
import java.net.BindException;
+import java.net.SocketException;
import java.net.URI;
import java.security.PrivilegedExceptionAction;
import java.util.Properties;
+import javax.net.ssl.SSLException;
import org.apache.commons.io.FileUtils;
import org.apache.hadoop.HadoopIllegalArgumentException;
@@ -36,14 +38,18 @@ import org.apache.hadoop.fs.CommonConfigurationKeys;
import org.apache.hadoop.fs.CommonConfigurationKeysPublic;
import org.apache.hadoop.fs.FileUtil;
import org.apache.hadoop.hbase.HBaseCommonTestingUtility;
+import org.apache.hadoop.hbase.http.HttpConfig;
import org.apache.hadoop.hbase.http.HttpServer;
import org.apache.hadoop.hbase.http.log.LogLevel.CLI;
+import org.apache.hadoop.hbase.http.ssl.KeyStoreTestUtil;
import org.apache.hadoop.hbase.testclassification.MiscTests;
import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.apache.hadoop.hdfs.DFSConfigKeys;
import org.apache.hadoop.minikdc.MiniKdc;
import org.apache.hadoop.net.NetUtils;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.authorize.AccessControlList;
+import org.apache.hadoop.security.ssl.SSLFactory;
import org.apache.hadoop.test.GenericTestUtils;
import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
@@ -60,8 +66,11 @@ import org.junit.experimental.categories.Category;
@Category({MiscTests.class, SmallTests.class})
public class TestLogLevel {
private static File BASEDIR;
+ private static String keystoresDir;
+ private static String sslConfDir;
private static Configuration serverConf;
private static Configuration clientConf;
+ private static Configuration sslConf;
private static final String logName = TestLogLevel.class.getName();
private static final Logger log = LogManager.getLogger(logName);
private final static String PRINCIPAL = "loglevel.principal";
@@ -88,6 +97,8 @@ public class TestLogLevel {
serverConf = new Configuration();
clientConf = new Configuration();
+ setupSSL(BASEDIR);
+
kdc = setupMiniKdc();
// Create two principles: a client and a HTTP principal
kdc.createPrincipal(KEYTAB_FILE, clientPrincipal, HTTP_PRINCIPAL);
@@ -126,6 +137,35 @@ public class TestLogLevel {
return kdc;
}
+ static private void setupSSL(File base) throws Exception {
+ Configuration conf = new Configuration();
+ conf.set(DFSConfigKeys.DFS_HTTP_POLICY_KEY,
HttpConfig.Policy.HTTPS_ONLY.name());
+ conf.set(DFSConfigKeys.DFS_NAMENODE_HTTPS_ADDRESS_KEY, "localhost:0");
+ conf.set(DFSConfigKeys.DFS_DATANODE_HTTPS_ADDRESS_KEY, "localhost:0");
+
+ keystoresDir = base.getAbsolutePath();
+ sslConfDir = KeyStoreTestUtil.getClasspathDir(TestLogLevel.class);
+ KeyStoreTestUtil.setupSSLConfig(keystoresDir, sslConfDir, conf, false);
+
+ sslConf = getSslConfig();
+ }
+
+ /**
+ * Get the SSL configuration.
+ * This method is copied from KeyStoreTestUtil#getSslConfig() in Hadoop.
+ * @return {@link Configuration} instance with ssl configs loaded.
+ */
+ private static Configuration getSslConfig() {
+ Configuration sslConf = new Configuration(false);
+ String sslServerConfFile = "ssl-server.xml";
+ String sslClientConfFile = "ssl-client.xml";
+ sslConf.addResource(sslServerConfFile);
+ sslConf.addResource(sslClientConfFile);
+ sslConf.set(SSLFactory.SSL_SERVER_CONF_KEY, sslServerConfFile);
+ sslConf.set(SSLFactory.SSL_CLIENT_CONF_KEY, sslClientConfFile);
+ return sslConf;
+ }
+
@AfterClass
public static void tearDown() {
if (kdc != null) {
@@ -193,15 +233,16 @@ public class TestLogLevel {
/**
* Creates and starts a Jetty server binding at an ephemeral port to run
* LogLevel servlet.
+ * @param protocol "http" or "https"
* @param isSpnego true if SPNEGO is enabled
* @return a created HttpServer object
* @throws Exception if unable to create or start a Jetty server
*/
- private HttpServer createServer(boolean isSpnego)
+ private HttpServer createServer(String protocol, boolean isSpnego)
throws Exception {
HttpServer.Builder builder = new HttpServer.Builder()
.setName("..")
- .addEndpoint(new URI(PROTOCOL_HTTP + "://localhost:0"))
+ .addEndpoint(new URI(protocol + "://localhost:0"))
.setFindPort(true)
.setConf(serverConf);
if (isSpnego) {
@@ -214,24 +255,43 @@ public class TestLogLevel {
.setACL(new AccessControlList("client"));
}
+ // if using HTTPS, configure keystore/truststore properties.
+ if (protocol.equals(LogLevel.PROTOCOL_HTTPS)) {
+ builder =
builder.keyPassword(sslConf.get("ssl.server.keystore.keypassword"))
+ .keyStore(sslConf.get("ssl.server.keystore.location"),
+ sslConf.get("ssl.server.keystore.password"),
+ sslConf.get("ssl.server.keystore.type", "jks"))
+
.trustStore(sslConf.get("ssl.server.truststore.location"),
+ sslConf.get("ssl.server.truststore.password"),
+ sslConf.get("ssl.server.truststore.type", "jks"));
+ }
+
HttpServer server = builder.build();
server.start();
return server;
}
- private void testDynamicLogLevel(final boolean isSpnego)
- throws Exception {
- testDynamicLogLevel(isSpnego, Level.DEBUG.toString());
+ private void testDynamicLogLevel(final String bindProtocol, final String
connectProtocol,
+ final boolean isSpnego) throws Exception {
+ testDynamicLogLevel(bindProtocol, connectProtocol, isSpnego,
Level.DEBUG.toString());
}
/**
* Run both client and server using the given protocol.
*
+ * @param bindProtocol specify either http or https for server
+ * @param connectProtocol specify either http or https for client
* @param isSpnego true if SPNEGO is enabled
* @throws Exception if client can't accesss server.
*/
- private void testDynamicLogLevel(final boolean isSpnego, final String
newLevel)
- throws Exception {
+ private void testDynamicLogLevel(final String bindProtocol, final String
connectProtocol,
+ final boolean isSpnego, final String newLevel) throws Exception {
+ if (!LogLevel.isValidProtocol(bindProtocol)) {
+ throw new Exception("Invalid server protocol " + bindProtocol);
+ }
+ if (!LogLevel.isValidProtocol(connectProtocol)) {
+ throw new Exception("Invalid client protocol " + connectProtocol);
+ }
Level oldLevel = log.getEffectiveLevel();
assertNotEquals("Get default Log Level which shouldn't be ERROR.",
Level.ERROR, oldLevel);
@@ -249,7 +309,7 @@ public class TestLogLevel {
UserGroupInformation.setConfiguration(serverConf);
}
- final HttpServer server = createServer(isSpnego);
+ final HttpServer server = createServer(bindProtocol, isSpnego);
// get server port
final String authority =
NetUtils.getHostPortString(server.getConnectorAddress(0));
@@ -261,8 +321,8 @@ public class TestLogLevel {
clientUGI.doAs(new PrivilegedExceptionAction<Void>() {
@Override public Void run() throws Exception {
// client command line
- TestLogLevel.this.getLevel(authority);
- TestLogLevel.this.setLevel(authority, newLevel);
+ getLevel(connectProtocol, authority);
+ setLevel(connectProtocol, authority, newLevel);
return null;
}
});
@@ -278,11 +338,12 @@ public class TestLogLevel {
* Run LogLevel command line to start a client to get log level of this test
* class.
*
+ * @param protocol specify either http or https
* @param authority daemon's web UI address
* @throws Exception if unable to connect
*/
- private void getLevel(String authority) throws Exception {
- String[] getLevelArgs = {"-getlevel", authority, logName};
+ private void getLevel(String protocol, String authority) throws Exception {
+ String[] getLevelArgs = {"-getlevel", authority, logName, "-protocol",
protocol};
CLI cli = new CLI(clientConf);
cli.run(getLevelArgs);
}
@@ -291,12 +352,13 @@ public class TestLogLevel {
* Run LogLevel command line to start a client to set log level of this test
* class to debug.
*
+ * @param protocol specify either http or https
* @param authority daemon's web UI address
* @throws Exception if unable to run or log level does not change as
expected
*/
- private void setLevel(String authority, String newLevel)
+ private void setLevel(String protocol, String authority, String newLevel)
throws Exception {
- String[] setLevelArgs = {"-setlevel", authority, logName, newLevel};
+ String[] setLevelArgs = {"-setlevel", authority, logName, newLevel,
"-protocol", protocol};
CLI cli = new CLI(clientConf);
cli.run(setLevelArgs);
@@ -311,7 +373,7 @@ public class TestLogLevel {
*/
@Test(timeout=60000)
public void testInfoLogLevel() throws Exception {
- testDynamicLogLevel(true, "INFO");
+ testDynamicLogLevel(LogLevel.PROTOCOL_HTTP, LogLevel.PROTOCOL_HTTP, true,
"INFO");
}
/**
@@ -321,26 +383,74 @@ public class TestLogLevel {
*/
@Test(timeout=60000)
public void testErrorLogLevel() throws Exception {
- testDynamicLogLevel(true, "ERROR");
+ testDynamicLogLevel(LogLevel.PROTOCOL_HTTP, LogLevel.PROTOCOL_HTTP, true,
"ERROR");
}
/**
* Server runs HTTP, no SPNEGO.
*
* @throws Exception if http client can't access http server.
+ * or http client can access https server.
*/
@Test(timeout=60000)
public void testLogLevelByHttp() throws Exception {
- testDynamicLogLevel(false);
+ testDynamicLogLevel(LogLevel.PROTOCOL_HTTP, LogLevel.PROTOCOL_HTTP, false);
+ try {
+ testDynamicLogLevel(LogLevel.PROTOCOL_HTTP, LogLevel.PROTOCOL_HTTPS,
false);
+ fail("A HTTPS Client should not have succeeded in connecting to a HTTP
server");
+ } catch (SSLException e) {
+ GenericTestUtils.assertExceptionContains("Unrecognized SSL message", e);
+ }
}
/**
* Server runs HTTP + SPNEGO.
*
- * @throws Exception if http client can't access http server.
+ * @throws Exception if http client can't access http server,
+ * or http client can access https server.
*/
@Test(timeout=60000)
public void testLogLevelByHttpWithSpnego() throws Exception {
- testDynamicLogLevel(true);
+ testDynamicLogLevel(LogLevel.PROTOCOL_HTTP, LogLevel.PROTOCOL_HTTP, true);
+ try {
+ testDynamicLogLevel(LogLevel.PROTOCOL_HTTP, LogLevel.PROTOCOL_HTTPS,
true);
+ fail("A HTTPS Client should not have succeeded in connecting to a HTTP
server");
+ } catch (SSLException e) {
+ GenericTestUtils.assertExceptionContains("Unrecognized SSL message", e);
+ }
+ }
+
+ /**
+ * Server runs HTTPS, no SPNEGO.
+ *
+ * @throws Exception if https client can't access https server,
+ * or https client can access http server.
+ */
+ @Test(timeout=60000)
+ public void testLogLevelByHttps() throws Exception {
+ testDynamicLogLevel(LogLevel.PROTOCOL_HTTPS, LogLevel.PROTOCOL_HTTPS,
false);
+ try {
+ testDynamicLogLevel(LogLevel.PROTOCOL_HTTPS, LogLevel.PROTOCOL_HTTP,
false);
+ fail("A HTTP Client should not have succeeded in connecting to a HTTPS
server");
+ } catch (SocketException e) {
+ GenericTestUtils.assertExceptionContains("Unexpected end of file from
server", e);
+ }
+ }
+
+ /**
+ * Server runs HTTPS + SPNEGO.
+ *
+ * @throws Exception if https client can't access https server,
+ * or https client can access http server.
+ */
+ @Test(timeout=60000)
+ public void testLogLevelByHttpsWithSpnego() throws Exception {
+ testDynamicLogLevel(LogLevel.PROTOCOL_HTTPS, LogLevel.PROTOCOL_HTTPS,
true);
+ try {
+ testDynamicLogLevel(LogLevel.PROTOCOL_HTTPS, LogLevel.PROTOCOL_HTTP,
true);
+ fail("A HTTP Client should not have succeeded in connecting to a HTTPS
server");
+ } catch (SocketException e) {
+ GenericTestUtils.assertExceptionContains("Unexpected end of file from
server", e);
+ }
}
}