HBASE-26160: Configurable disallowlist for live editing of loglevels (#3549)

Signed-off-by: Wei-Chiu Chuang <weichiu@apache.org>
This commit is contained in:
Bryan Beaudreault 2021-08-04 21:45:47 -04:00 committed by GitHub
parent 83661c5636
commit da950b9be2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 75 additions and 12 deletions

View File

@ -22,8 +22,8 @@ import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Objects; import java.util.Objects;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -41,6 +41,7 @@ import org.apache.hadoop.hbase.logging.Log4jUtils;
import org.apache.hadoop.security.authentication.client.AuthenticatedURL; import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
import org.apache.hadoop.security.authentication.client.KerberosAuthenticator; import org.apache.hadoop.security.authentication.client.KerberosAuthenticator;
import org.apache.hadoop.security.ssl.SSLFactory; import org.apache.hadoop.security.ssl.SSLFactory;
import org.apache.hadoop.util.HttpExceptionUtils;
import org.apache.hadoop.util.ServletUtil; import org.apache.hadoop.util.ServletUtil;
import org.apache.hadoop.util.Tool; import org.apache.hadoop.util.Tool;
import org.apache.yetus.audience.InterfaceAudience; import org.apache.yetus.audience.InterfaceAudience;
@ -59,6 +60,8 @@ public final class LogLevel {
public static final String PROTOCOL_HTTP = "http"; public static final String PROTOCOL_HTTP = "http";
public static final String PROTOCOL_HTTPS = "https"; public static final String PROTOCOL_HTTPS = "https";
public static final String READONLY_LOGGERS_CONF_KEY = "hbase.ui.logLevels.readonly.loggers";
/** /**
* A command line implementation * A command line implementation
*/ */
@ -247,11 +250,11 @@ public final class LogLevel {
* @return a connected connection * @return a connected connection
* @throws Exception if it can not establish a connection. * @throws Exception if it can not establish a connection.
*/ */
private URLConnection connect(URL url) throws Exception { private HttpURLConnection connect(URL url) throws Exception {
AuthenticatedURL.Token token = new AuthenticatedURL.Token(); AuthenticatedURL.Token token = new AuthenticatedURL.Token();
AuthenticatedURL aUrl; AuthenticatedURL aUrl;
SSLFactory clientSslFactory; SSLFactory clientSslFactory;
URLConnection connection; HttpURLConnection connection;
// If https is chosen, configures SSL client. // If https is chosen, configures SSL client.
if (PROTOCOL_HTTPS.equals(url.getProtocol())) { if (PROTOCOL_HTTPS.equals(url.getProtocol())) {
clientSslFactory = new SSLFactory(SSLFactory.Mode.CLIENT, this.getConf()); clientSslFactory = new SSLFactory(SSLFactory.Mode.CLIENT, this.getConf());
@ -280,7 +283,9 @@ public final class LogLevel {
URL url = new URL(urlString); URL url = new URL(urlString);
System.out.println("Connecting to " + url); System.out.println("Connecting to " + url);
URLConnection connection = connect(url); HttpURLConnection connection = connect(url);
HttpExceptionUtils.validateResponse(connection, 200);
// read from the servlet // read from the servlet
@ -317,8 +322,10 @@ public final class LogLevel {
Configuration conf = (Configuration) getServletContext().getAttribute( Configuration conf = (Configuration) getServletContext().getAttribute(
HttpServer.CONF_CONTEXT_ATTRIBUTE); HttpServer.CONF_CONTEXT_ATTRIBUTE);
if (conf.getBoolean("hbase.master.ui.readonly", false)) { if (conf.getBoolean("hbase.master.ui.readonly", false)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Modification of HBase via" sendError(
+ " the UI is disallowed in configuration."); response,
HttpServletResponse.SC_FORBIDDEN,
"Modification of HBase via the UI is disallowed in configuration.");
return; return;
} }
response.setContentType("text/html"); response.setContentType("text/html");
@ -336,6 +343,8 @@ public final class LogLevel {
String logName = ServletUtil.getParameter(request, "log"); String logName = ServletUtil.getParameter(request, "log");
String level = ServletUtil.getParameter(request, "level"); String level = ServletUtil.getParameter(request, "level");
String[] readOnlyLogLevels = conf.getStrings(READONLY_LOGGERS_CONF_KEY);
if (logName != null) { if (logName != null) {
out.println("<p>Results:</p>"); out.println("<p>Results:</p>");
out.println(MARKER out.println(MARKER
@ -345,6 +354,14 @@ public final class LogLevel {
out.println(MARKER out.println(MARKER
+ "Log Class: <b>" + log.getClass().getName() +"</b><br />"); + "Log Class: <b>" + log.getClass().getName() +"</b><br />");
if (level != null) { if (level != null) {
if (!isLogLevelChangeAllowed(logName, readOnlyLogLevels)) {
sendError(
response,
HttpServletResponse.SC_PRECONDITION_FAILED,
"Modification of logger " + logName + " is disallowed in configuration.");
return;
}
out.println(MARKER + "Submitted Level: <b>" + level + "</b><br />"); out.println(MARKER + "Submitted Level: <b>" + level + "</b><br />");
} }
process(log, level, out); process(log, level, out);
@ -360,6 +377,24 @@ public final class LogLevel {
out.close(); out.close();
} }
private boolean isLogLevelChangeAllowed(String logger, String[] readOnlyLogLevels) {
if (readOnlyLogLevels == null) {
return true;
}
for (String readOnlyLogLevel : readOnlyLogLevels) {
if (logger.startsWith(readOnlyLogLevel)) {
return false;
}
}
return true;
}
private void sendError(HttpServletResponse response, int code, String message)
throws IOException {
response.setStatus(code, message);
response.sendError(code, message);
}
static final String FORMS = "<div class='container-fluid content'>\n" static final String FORMS = "<div class='container-fluid content'>\n"
+ "<div class='row inner_header'>\n" + "<div class='page-header'>\n" + "<div class='row inner_header'>\n" + "<div class='page-header'>\n"
+ "<h1>Get/Set Log Level</h1>\n" + "</div>\n" + "</div>\n" + "Actions:" + "<p>" + "<h1>Get/Set Log Level</h1>\n" + "</div>\n" + "</div>\n" + "Actions:" + "<p>"

View File

@ -24,12 +24,14 @@ import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.net.BindException; import java.net.BindException;
import java.net.SocketException; import java.net.SocketException;
import java.net.URI; import java.net.URI;
import java.security.PrivilegedExceptionAction; import java.security.PrivilegedExceptionAction;
import java.util.Properties; import java.util.Properties;
import javax.net.ssl.SSLException; import javax.net.ssl.SSLException;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.hadoop.HadoopIllegalArgumentException; import org.apache.hadoop.HadoopIllegalArgumentException;
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.conf.Configuration;
@ -73,6 +75,8 @@ public class TestLogLevel {
private static Configuration clientConf; private static Configuration clientConf;
private static Configuration sslConf; private static Configuration sslConf;
private static final String logName = TestLogLevel.class.getName(); private static final String logName = TestLogLevel.class.getName();
private static final String protectedPrefix = "protected";
private static final String protectedLogName = protectedPrefix + "." + logName;
private static final org.apache.logging.log4j.Logger log = private static final org.apache.logging.log4j.Logger log =
org.apache.logging.log4j.LogManager.getLogger(logName); org.apache.logging.log4j.LogManager.getLogger(logName);
private final static String PRINCIPAL = "loglevel.principal"; private final static String PRINCIPAL = "loglevel.principal";
@ -89,6 +93,7 @@ public class TestLogLevel {
@BeforeClass @BeforeClass
public static void setUp() throws Exception { public static void setUp() throws Exception {
serverConf = new Configuration(); serverConf = new Configuration();
serverConf.setStrings(LogLevel.READONLY_LOGGERS_CONF_KEY, protectedPrefix);
HTU = new HBaseCommonTestingUtil(serverConf); HTU = new HBaseCommonTestingUtil(serverConf);
File keystoreDir = new File(HTU.getDataTestDir("keystore").toString()); File keystoreDir = new File(HTU.getDataTestDir("keystore").toString());
@ -259,9 +264,17 @@ public class TestLogLevel {
private void testDynamicLogLevel(final String bindProtocol, final String connectProtocol, private void testDynamicLogLevel(final String bindProtocol, final String connectProtocol,
final boolean isSpnego) throws Exception { final boolean isSpnego) throws Exception {
testDynamicLogLevel(bindProtocol, connectProtocol, isSpnego, testDynamicLogLevel(bindProtocol, connectProtocol, isSpnego,
logName,
org.apache.logging.log4j.Level.DEBUG.toString()); org.apache.logging.log4j.Level.DEBUG.toString());
} }
private void testDynamicLogLevel(final String bindProtocol, final String connectProtocol,
final boolean isSpnego, final String newLevel) throws Exception {
testDynamicLogLevel(bindProtocol, connectProtocol, isSpnego,
logName,
newLevel);
}
/** /**
* Run both client and server using the given protocol. * Run both client and server using the given protocol.
* @param bindProtocol specify either http or https for server * @param bindProtocol specify either http or https for server
@ -270,13 +283,14 @@ public class TestLogLevel {
* @throws Exception if client can't accesss server. * @throws Exception if client can't accesss server.
*/ */
private void testDynamicLogLevel(final String bindProtocol, final String connectProtocol, private void testDynamicLogLevel(final String bindProtocol, final String connectProtocol,
final boolean isSpnego, final String newLevel) throws Exception { final boolean isSpnego, final String loggerName, final String newLevel) throws Exception {
if (!LogLevel.isValidProtocol(bindProtocol)) { if (!LogLevel.isValidProtocol(bindProtocol)) {
throw new Exception("Invalid server protocol " + bindProtocol); throw new Exception("Invalid server protocol " + bindProtocol);
} }
if (!LogLevel.isValidProtocol(connectProtocol)) { if (!LogLevel.isValidProtocol(connectProtocol)) {
throw new Exception("Invalid client protocol " + connectProtocol); throw new Exception("Invalid client protocol " + connectProtocol);
} }
org.apache.logging.log4j.Logger log = org.apache.logging.log4j.LogManager.getLogger(loggerName);
org.apache.logging.log4j.Level oldLevel = log.getLevel(); org.apache.logging.log4j.Level oldLevel = log.getLevel();
assertNotEquals("Get default Log Level which shouldn't be ERROR.", assertNotEquals("Get default Log Level which shouldn't be ERROR.",
org.apache.logging.log4j.Level.ERROR, oldLevel); org.apache.logging.log4j.Level.ERROR, oldLevel);
@ -305,8 +319,8 @@ public class TestLogLevel {
try { try {
clientUGI.doAs((PrivilegedExceptionAction<Void>) () -> { clientUGI.doAs((PrivilegedExceptionAction<Void>) () -> {
// client command line // client command line
getLevel(connectProtocol, authority); getLevel(connectProtocol, authority, loggerName);
setLevel(connectProtocol, authority, newLevel); setLevel(connectProtocol, authority, loggerName, newLevel);
return null; return null;
}); });
} finally { } finally {
@ -324,7 +338,7 @@ public class TestLogLevel {
* @param authority daemon's web UI address * @param authority daemon's web UI address
* @throws Exception if unable to connect * @throws Exception if unable to connect
*/ */
private void getLevel(String protocol, String authority) throws Exception { private void getLevel(String protocol, String authority, String logName) throws Exception {
String[] getLevelArgs = { "-getlevel", authority, logName, "-protocol", protocol }; String[] getLevelArgs = { "-getlevel", authority, logName, "-protocol", protocol };
CLI cli = new CLI(protocol.equalsIgnoreCase("https") ? sslConf : clientConf); CLI cli = new CLI(protocol.equalsIgnoreCase("https") ? sslConf : clientConf);
cli.run(getLevelArgs); cli.run(getLevelArgs);
@ -336,13 +350,27 @@ public class TestLogLevel {
* @param authority daemon's web UI address * @param authority daemon's web UI address
* @throws Exception if unable to run or log level does not change as expected * @throws Exception if unable to run or log level does not change as expected
*/ */
private void setLevel(String protocol, String authority, String newLevel) throws Exception { private void setLevel(String protocol, String authority, String logName, String newLevel) throws Exception {
String[] setLevelArgs = { "-setlevel", authority, logName, newLevel, "-protocol", protocol }; String[] setLevelArgs = { "-setlevel", authority, logName, newLevel, "-protocol", protocol };
CLI cli = new CLI(protocol.equalsIgnoreCase("https") ? sslConf : clientConf); CLI cli = new CLI(protocol.equalsIgnoreCase("https") ? sslConf : clientConf);
cli.run(setLevelArgs); cli.run(setLevelArgs);
org.apache.logging.log4j.Logger logger = org.apache.logging.log4j.LogManager.getLogger(logName);
assertEquals("new level not equal to expected: ", newLevel.toUpperCase(), assertEquals("new level not equal to expected: ", newLevel.toUpperCase(),
log.getLevel().toString()); logger.getLevel().toString());
}
@Test
public void testSettingProtectedLogLevel() throws Exception {
try {
testDynamicLogLevel(LogLevel.PROTOCOL_HTTP, LogLevel.PROTOCOL_HTTP, true, protectedLogName,
"DEBUG");
fail("Expected IO exception due to protected logger");
} catch (IOException e) {
assertTrue(e.getMessage().contains("" + HttpServletResponse.SC_PRECONDITION_FAILED));
assertTrue(e.getMessage().contains("Modification of logger " + protectedLogName + " is disallowed in configuration."));
}
} }
/** /**