This is an automated email from the ASF dual-hosted git repository.

weichiu pushed a commit to branch branch-2.4
in repository https://gitbox.apache.org/repos/asf/hbase.git


The following commit(s) were added to refs/heads/branch-2.4 by this push:
     new 27b0c41  HBASE-26160: Configurable disallowlist for live editing of 
loglevels (#3558)
27b0c41 is described below

commit 27b0c41cb2c3c825922bb2e779beb0690fc57a43
Author: Bryan Beaudreault <[email protected]>
AuthorDate: Wed Aug 4 23:12:10 2021 -0400

    HBASE-26160: Configurable disallowlist for live editing of loglevels (#3558)
    
    Signed-off-by: Wei-Chiu Chuang <[email protected]>
---
 .../org/apache/hadoop/hbase/http/log/LogLevel.java | 47 +++++++++++++++++++---
 .../apache/hadoop/hbase/http/log/TestLogLevel.java | 40 ++++++++++++++----
 2 files changed, 74 insertions(+), 13 deletions(-)

diff --git 
a/hbase-http/src/main/java/org/apache/hadoop/hbase/http/log/LogLevel.java 
b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/log/LogLevel.java
index 1fcfa13..8195817 100644
--- a/hbase-http/src/main/java/org/apache/hadoop/hbase/http/log/LogLevel.java
+++ b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/log/LogLevel.java
@@ -22,8 +22,8 @@ import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.PrintWriter;
+import java.net.HttpURLConnection;
 import java.net.URL;
-import java.net.URLConnection;
 import java.nio.charset.StandardCharsets;
 import java.util.Objects;
 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.KerberosAuthenticator;
 import org.apache.hadoop.security.ssl.SSLFactory;
+import org.apache.hadoop.util.HttpExceptionUtils;
 import org.apache.hadoop.util.ServletUtil;
 import org.apache.hadoop.util.Tool;
 import org.apache.yetus.audience.InterfaceAudience;
@@ -60,6 +61,8 @@ public final class LogLevel {
   public static final String PROTOCOL_HTTP = "http";
   public static final String PROTOCOL_HTTPS = "https";
 
+  public static final String READONLY_LOGGERS_CONF_KEY = 
"hbase.ui.logLevels.readonly.loggers";
+
   /**
    * A command line implementation
    */
@@ -248,11 +251,11 @@ public final class LogLevel {
      * @return a connected 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 aUrl;
       SSLFactory clientSslFactory;
-      URLConnection connection;
+      HttpURLConnection connection;
       // If https is chosen, configures SSL client.
       if (PROTOCOL_HTTPS.equals(url.getProtocol())) {
         clientSslFactory = new SSLFactory(SSLFactory.Mode.CLIENT, 
this.getConf());
@@ -281,7 +284,9 @@ public final class LogLevel {
       URL url = new URL(urlString);
       System.out.println("Connecting to " + url);
 
-      URLConnection connection = connect(url);
+      HttpURLConnection connection = connect(url);
+
+      HttpExceptionUtils.validateResponse(connection, 200);
 
       // read from the servlet
 
@@ -319,8 +324,10 @@ public final class LogLevel {
       Configuration conf = (Configuration) getServletContext().getAttribute(
           HttpServer.CONF_CONTEXT_ATTRIBUTE);
       if (conf.getBoolean("hbase.master.ui.readonly", false)) {
-        response.sendError(HttpServletResponse.SC_FORBIDDEN, "Modification of 
HBase via"
-            + " the UI is disallowed in configuration.");
+        sendError(
+          response,
+          HttpServletResponse.SC_FORBIDDEN,
+          "Modification of HBase via the UI is disallowed in configuration.");
         return;
       }
       response.setContentType("text/html");
@@ -338,6 +345,8 @@ public final class LogLevel {
       String logName = ServletUtil.getParameter(request, "log");
       String level = ServletUtil.getParameter(request, "level");
 
+      String[] readOnlyLogLevels = conf.getStrings(READONLY_LOGGERS_CONF_KEY);
+
       if (logName != null) {
         out.println("<p>Results:</p>");
         out.println(MARKER
@@ -347,6 +356,14 @@ public final class LogLevel {
         out.println(MARKER
             + "Log Class: <b>" + log.getClass().getName() +"</b><br />");
         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 />");
         }
         process(log, level, out);
@@ -362,6 +379,24 @@ public final class LogLevel {
       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"
         + "<div class='row inner_header'>\n" + "<div class='page-header'>\n"
         + "<h1>Get/Set Log Level</h1>\n" + "</div>\n" + "</div>\n" + 
"Actions:" + "<p>"
diff --git 
a/hbase-http/src/test/java/org/apache/hadoop/hbase/http/log/TestLogLevel.java 
b/hbase-http/src/test/java/org/apache/hadoop/hbase/http/log/TestLogLevel.java
index 2c5d0c4..b52129c 100644
--- 
a/hbase-http/src/test/java/org/apache/hadoop/hbase/http/log/TestLogLevel.java
+++ 
b/hbase-http/src/test/java/org/apache/hadoop/hbase/http/log/TestLogLevel.java
@@ -23,12 +23,14 @@ import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import java.io.File;
+import java.io.IOException;
 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 javax.servlet.http.HttpServletResponse;
 import org.apache.commons.io.FileUtils;
 import org.apache.hadoop.HadoopIllegalArgumentException;
 import org.apache.hadoop.conf.Configuration;
@@ -75,6 +77,8 @@ public class TestLogLevel {
   private static Configuration clientConf;
   private static Configuration sslConf;
   private static final String logName = TestLogLevel.class.getName();
+  private static final String protectedPrefix = "protected";
+  private static final String protectedLogName = protectedPrefix + "." + 
logName;
   private static final Logger log = LogManager.getLogger(logName);
   private final static String PRINCIPAL = "loglevel.principal";
   private final static String KEYTAB  = "loglevel.keytab";
@@ -90,6 +94,7 @@ public class TestLogLevel {
   @BeforeClass
   public static void setUp() throws Exception {
     serverConf = new Configuration();
+    serverConf.setStrings(LogLevel.READONLY_LOGGERS_CONF_KEY, protectedPrefix);
     HTU = new HBaseCommonTestingUtility(serverConf);
 
     File keystoreDir = new File(HTU.getDataTestDir("keystore").toString());
@@ -276,7 +281,13 @@ public class TestLogLevel {
   private void testDynamicLogLevel(final String bindProtocol, final String 
connectProtocol,
       final boolean isSpnego)
       throws Exception {
-    testDynamicLogLevel(bindProtocol, connectProtocol, isSpnego, 
Level.DEBUG.toString());
+    testDynamicLogLevel(bindProtocol, connectProtocol, isSpnego, logName, 
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);
   }
 
   /**
@@ -288,7 +299,7 @@ public class TestLogLevel {
    * @throws Exception if client can't accesss server.
    */
   private void testDynamicLogLevel(final String bindProtocol, final String 
connectProtocol,
-      final boolean isSpnego, final String newLevel)
+      final boolean isSpnego, final String loggerName, final String newLevel)
       throws Exception {
     if (!LogLevel.isValidProtocol(bindProtocol)) {
       throw new Exception("Invalid server protocol " + bindProtocol);
@@ -296,7 +307,8 @@ public class TestLogLevel {
     if (!LogLevel.isValidProtocol(connectProtocol)) {
       throw new Exception("Invalid client protocol " + connectProtocol);
     }
-    Level oldLevel = log.getEffectiveLevel();
+    Logger log = LogManager.getLogger(loggerName);
+    Level oldLevel = log.getLevel();
     assertNotEquals("Get default Log Level which shouldn't be ERROR.",
         Level.ERROR, oldLevel);
 
@@ -324,8 +336,8 @@ public class TestLogLevel {
     try {
       clientUGI.doAs((PrivilegedExceptionAction<Void>) () -> {
         // client command line
-        getLevel(connectProtocol, authority);
-        setLevel(connectProtocol, authority, newLevel);
+        getLevel(connectProtocol, authority, loggerName);
+        setLevel(connectProtocol, authority, loggerName, newLevel);
         return null;
       });
     } finally {
@@ -345,7 +357,7 @@ public class TestLogLevel {
    * @param authority daemon's web UI address
    * @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};
     CLI cli = new CLI(protocol.equalsIgnoreCase("https") ? sslConf : 
clientConf);
     cli.run(getLevelArgs);
@@ -359,16 +371,30 @@ public class TestLogLevel {
    * @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 protocol, String authority, String newLevel)
+  private void setLevel(String protocol, String authority, String logName, 
String newLevel)
       throws Exception {
     String[] setLevelArgs = {"-setlevel", authority, logName, newLevel, 
"-protocol", protocol};
     CLI cli = new CLI(protocol.equalsIgnoreCase("https") ? sslConf : 
clientConf);
     cli.run(setLevelArgs);
 
+    Logger log = LogManager.getLogger(logName);
+
     assertEquals("new level not equal to expected: ", newLevel.toUpperCase(),
         log.getEffectiveLevel().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."));
+    }
+  }
+
   /**
    * Test setting log level to "Info".
    *

Reply via email to