Skip to content

Commit a24956d

Browse files
authored
HBASE-26160: Configurable disallowlist for live editing of loglevels (#3559)
Signed-off-by: Wei-Chiu Chuang <[email protected]>
1 parent effde97 commit a24956d

File tree

2 files changed

+74
-13
lines changed

2 files changed

+74
-13
lines changed

hbase-http/src/main/java/org/apache/hadoop/hbase/http/log/LogLevel.java

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
import java.io.IOException;
2323
import java.io.InputStreamReader;
2424
import java.io.PrintWriter;
25+
import java.net.HttpURLConnection;
2526
import java.net.URL;
26-
import java.net.URLConnection;
2727
import java.nio.charset.StandardCharsets;
2828
import java.util.Objects;
2929
import java.util.regex.Pattern;
@@ -41,6 +41,7 @@
4141
import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
4242
import org.apache.hadoop.security.authentication.client.KerberosAuthenticator;
4343
import org.apache.hadoop.security.ssl.SSLFactory;
44+
import org.apache.hadoop.util.HttpExceptionUtils;
4445
import org.apache.hadoop.util.ServletUtil;
4546
import org.apache.hadoop.util.Tool;
4647
import org.apache.yetus.audience.InterfaceAudience;
@@ -60,6 +61,8 @@ public final class LogLevel {
6061
public static final String PROTOCOL_HTTP = "http";
6162
public static final String PROTOCOL_HTTPS = "https";
6263

64+
public static final String READONLY_LOGGERS_CONF_KEY = "hbase.ui.logLevels.readonly.loggers";
65+
6366
/**
6467
* A command line implementation
6568
*/
@@ -248,11 +251,11 @@ private void doSetLevel() throws Exception {
248251
* @return a connected connection
249252
* @throws Exception if it can not establish a connection.
250253
*/
251-
private URLConnection connect(URL url) throws Exception {
254+
private HttpURLConnection connect(URL url) throws Exception {
252255
AuthenticatedURL.Token token = new AuthenticatedURL.Token();
253256
AuthenticatedURL aUrl;
254257
SSLFactory clientSslFactory;
255-
URLConnection connection;
258+
HttpURLConnection connection;
256259
// If https is chosen, configures SSL client.
257260
if (PROTOCOL_HTTPS.equals(url.getProtocol())) {
258261
clientSslFactory = new SSLFactory(SSLFactory.Mode.CLIENT, this.getConf());
@@ -281,7 +284,9 @@ private void process(String urlString) throws Exception {
281284
URL url = new URL(urlString);
282285
System.out.println("Connecting to " + url);
283286

284-
URLConnection connection = connect(url);
287+
HttpURLConnection connection = connect(url);
288+
289+
HttpExceptionUtils.validateResponse(connection, 200);
285290

286291
// read from the servlet
287292

@@ -319,8 +324,10 @@ public void doGet(HttpServletRequest request, HttpServletResponse response)
319324
Configuration conf = (Configuration) getServletContext().getAttribute(
320325
HttpServer.CONF_CONTEXT_ATTRIBUTE);
321326
if (conf.getBoolean("hbase.master.ui.readonly", false)) {
322-
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Modification of HBase via"
323-
+ " the UI is disallowed in configuration.");
327+
sendError(
328+
response,
329+
HttpServletResponse.SC_FORBIDDEN,
330+
"Modification of HBase via the UI is disallowed in configuration.");
324331
return;
325332
}
326333
response.setContentType("text/html");
@@ -338,6 +345,8 @@ public void doGet(HttpServletRequest request, HttpServletResponse response)
338345
String logName = ServletUtil.getParameter(request, "log");
339346
String level = ServletUtil.getParameter(request, "level");
340347

348+
String[] readOnlyLogLevels = conf.getStrings(READONLY_LOGGERS_CONF_KEY);
349+
341350
if (logName != null) {
342351
out.println("<p>Results:</p>");
343352
out.println(MARKER
@@ -347,6 +356,14 @@ public void doGet(HttpServletRequest request, HttpServletResponse response)
347356
out.println(MARKER
348357
+ "Log Class: <b>" + log.getClass().getName() +"</b><br />");
349358
if (level != null) {
359+
if (!isLogLevelChangeAllowed(logName, readOnlyLogLevels)) {
360+
sendError(
361+
response,
362+
HttpServletResponse.SC_PRECONDITION_FAILED,
363+
"Modification of logger " + logName + " is disallowed in configuration.");
364+
return;
365+
}
366+
350367
out.println(MARKER + "Submitted Level: <b>" + level + "</b><br />");
351368
}
352369
process(log, level, out);
@@ -362,6 +379,24 @@ public void doGet(HttpServletRequest request, HttpServletResponse response)
362379
out.close();
363380
}
364381

382+
private boolean isLogLevelChangeAllowed(String logger, String[] readOnlyLogLevels) {
383+
if (readOnlyLogLevels == null) {
384+
return true;
385+
}
386+
for (String readOnlyLogLevel : readOnlyLogLevels) {
387+
if (logger.startsWith(readOnlyLogLevel)) {
388+
return false;
389+
}
390+
}
391+
return true;
392+
}
393+
394+
private void sendError(HttpServletResponse response, int code, String message)
395+
throws IOException {
396+
response.setStatus(code, message);
397+
response.sendError(code, message);
398+
}
399+
365400
static final String FORMS = "<div class='container-fluid content'>\n"
366401
+ "<div class='row inner_header'>\n" + "<div class='page-header'>\n"
367402
+ "<h1>Get/Set Log Level</h1>\n" + "</div>\n" + "</div>\n" + "Actions:" + "<p>"

hbase-http/src/test/java/org/apache/hadoop/hbase/http/log/TestLogLevel.java

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@
2323
import static org.junit.Assert.assertTrue;
2424
import static org.junit.Assert.fail;
2525
import java.io.File;
26+
import java.io.IOException;
2627
import java.net.BindException;
2728
import java.net.SocketException;
2829
import java.net.URI;
2930
import java.security.PrivilegedExceptionAction;
3031
import java.util.Properties;
3132
import javax.net.ssl.SSLException;
33+
import javax.servlet.http.HttpServletResponse;
3234
import org.apache.commons.io.FileUtils;
3335
import org.apache.hadoop.HadoopIllegalArgumentException;
3436
import org.apache.hadoop.conf.Configuration;
@@ -75,6 +77,8 @@ public class TestLogLevel {
7577
private static Configuration clientConf;
7678
private static Configuration sslConf;
7779
private static final String logName = TestLogLevel.class.getName();
80+
private static final String protectedPrefix = "protected";
81+
private static final String protectedLogName = protectedPrefix + "." + logName;
7882
private static final Logger log = LogManager.getLogger(logName);
7983
private final static String PRINCIPAL = "loglevel.principal";
8084
private final static String KEYTAB = "loglevel.keytab";
@@ -90,6 +94,7 @@ public class TestLogLevel {
9094
@BeforeClass
9195
public static void setUp() throws Exception {
9296
serverConf = new Configuration();
97+
serverConf.setStrings(LogLevel.READONLY_LOGGERS_CONF_KEY, protectedPrefix);
9398
HTU = new HBaseCommonTestingUtility(serverConf);
9499

95100
File keystoreDir = new File(HTU.getDataTestDir("keystore").toString());
@@ -276,7 +281,13 @@ private HttpServer createServer(String protocol, boolean isSpnego)
276281
private void testDynamicLogLevel(final String bindProtocol, final String connectProtocol,
277282
final boolean isSpnego)
278283
throws Exception {
279-
testDynamicLogLevel(bindProtocol, connectProtocol, isSpnego, Level.DEBUG.toString());
284+
testDynamicLogLevel(bindProtocol, connectProtocol, isSpnego, logName, Level.DEBUG.toString());
285+
}
286+
287+
private void testDynamicLogLevel(final String bindProtocol, final String connectProtocol,
288+
final boolean isSpnego, final String newLevel)
289+
throws Exception {
290+
testDynamicLogLevel(bindProtocol, connectProtocol, isSpnego, logName, newLevel);
280291
}
281292

282293
/**
@@ -288,15 +299,16 @@ private void testDynamicLogLevel(final String bindProtocol, final String connect
288299
* @throws Exception if client can't accesss server.
289300
*/
290301
private void testDynamicLogLevel(final String bindProtocol, final String connectProtocol,
291-
final boolean isSpnego, final String newLevel)
302+
final boolean isSpnego, final String loggerName, final String newLevel)
292303
throws Exception {
293304
if (!LogLevel.isValidProtocol(bindProtocol)) {
294305
throw new Exception("Invalid server protocol " + bindProtocol);
295306
}
296307
if (!LogLevel.isValidProtocol(connectProtocol)) {
297308
throw new Exception("Invalid client protocol " + connectProtocol);
298309
}
299-
Level oldLevel = log.getEffectiveLevel();
310+
Logger log = LogManager.getLogger(loggerName);
311+
Level oldLevel = log.getLevel();
300312
assertNotEquals("Get default Log Level which shouldn't be ERROR.",
301313
Level.ERROR, oldLevel);
302314

@@ -324,8 +336,8 @@ private void testDynamicLogLevel(final String bindProtocol, final String connect
324336
try {
325337
clientUGI.doAs((PrivilegedExceptionAction<Void>) () -> {
326338
// client command line
327-
getLevel(connectProtocol, authority);
328-
setLevel(connectProtocol, authority, newLevel);
339+
getLevel(connectProtocol, authority, loggerName);
340+
setLevel(connectProtocol, authority, loggerName, newLevel);
329341
return null;
330342
});
331343
} finally {
@@ -345,7 +357,7 @@ private void testDynamicLogLevel(final String bindProtocol, final String connect
345357
* @param authority daemon's web UI address
346358
* @throws Exception if unable to connect
347359
*/
348-
private void getLevel(String protocol, String authority) throws Exception {
360+
private void getLevel(String protocol, String authority, String logName) throws Exception {
349361
String[] getLevelArgs = {"-getlevel", authority, logName, "-protocol", protocol};
350362
CLI cli = new CLI(protocol.equalsIgnoreCase("https") ? sslConf : clientConf);
351363
cli.run(getLevelArgs);
@@ -359,16 +371,30 @@ private void getLevel(String protocol, String authority) throws Exception {
359371
* @param authority daemon's web UI address
360372
* @throws Exception if unable to run or log level does not change as expected
361373
*/
362-
private void setLevel(String protocol, String authority, String newLevel)
374+
private void setLevel(String protocol, String authority, String logName, String newLevel)
363375
throws Exception {
364376
String[] setLevelArgs = {"-setlevel", authority, logName, newLevel, "-protocol", protocol};
365377
CLI cli = new CLI(protocol.equalsIgnoreCase("https") ? sslConf : clientConf);
366378
cli.run(setLevelArgs);
367379

380+
Logger log = LogManager.getLogger(logName);
381+
368382
assertEquals("new level not equal to expected: ", newLevel.toUpperCase(),
369383
log.getEffectiveLevel().toString());
370384
}
371385

386+
@Test
387+
public void testSettingProtectedLogLevel() throws Exception {
388+
try {
389+
testDynamicLogLevel(LogLevel.PROTOCOL_HTTP, LogLevel.PROTOCOL_HTTP, true, protectedLogName,
390+
"DEBUG");
391+
fail("Expected IO exception due to protected logger");
392+
} catch (IOException e) {
393+
assertTrue(e.getMessage().contains("" + HttpServletResponse.SC_PRECONDITION_FAILED));
394+
assertTrue(e.getMessage().contains("Modification of logger " + protectedLogName + " is disallowed in configuration."));
395+
}
396+
}
397+
372398
/**
373399
* Test setting log level to "Info".
374400
*

0 commit comments

Comments
 (0)