This is an automated email from the ASF dual-hosted git repository. madhan pushed a commit to branch ranger-2.8 in repository https://gitbox.apache.org/repos/asf/ranger.git
commit 9491343680664bfaa74f2e1e77c78331fb82cc59 Author: Fateh Singh <fateh...@gmail.com> AuthorDate: Sat Sep 20 10:05:09 2025 -0700 RANGER-5266: [Generated by Gemini AI]: (security-admin) REST endpoints to set log level while service is running (#620) (cherry picked from commit 346fe34a41446cf29abefb627b61ab01376da4f3) --- .../main/java/org/apache/ranger/RangerClient.java | 41 ++++-- .../python/apache_ranger/client/ranger_client.py | 19 ++- .../apache/ranger/biz/RangerLogLevelService.java | 115 ++++++++++++++++ .../java/org/apache/ranger/rest/AdminREST.java | 147 +++++++++++++++++++++ 4 files changed, 309 insertions(+), 13 deletions(-) diff --git a/intg/src/main/java/org/apache/ranger/RangerClient.java b/intg/src/main/java/org/apache/ranger/RangerClient.java index a61c13fd2..eb38b4ac0 100644 --- a/intg/src/main/java/org/apache/ranger/RangerClient.java +++ b/intg/src/main/java/org/apache/ranger/RangerClient.java @@ -92,6 +92,7 @@ public class RangerClient { private static final String URI_PLUGIN_INFO = URI_BASE + "/plugins/info"; private static final String URI_POLICY_DELTAS = URI_BASE + "/server/policydeltas"; private static final String URI_PURGE_RECORDS = URI_BASE + "/server/purge/records"; + private static final String URI_LOGGERS_SET_LEVEL = "/service/admin/set-logger-level"; // APIs @@ -153,6 +154,7 @@ public class RangerClient { public static final API DELETE_POLICY_DELTAS = new API(URI_POLICY_DELTAS, HttpMethod.DELETE, Response.Status.NO_CONTENT); public static final API PURGE_RECORDS = new API(URI_PURGE_RECORDS, HttpMethod.DELETE, Response.Status.OK); + public static final API SET_LOG_LEVEL = new API(URI_LOGGERS_SET_LEVEL, HttpMethod.POST, Response.Status.OK); private static final TypeReference<Void> TYPE_VOID = new TypeReference<Void>() {}; private static final TypeReference<Set<String>> TYPE_SET_STRING = new TypeReference<Set<String>>() {}; @@ -170,17 +172,6 @@ public class RangerClient { private final RangerRESTClient restClient; private boolean isSecureMode = false; - private void authInit(String authType, String username, String password) { - if (AUTH_KERBEROS.equalsIgnoreCase(authType)) { - isSecureMode = true; - MiscUtil.loginWithKeyTab(password, username, null); - UserGroupInformation ugi = MiscUtil.getUGILoginUser(); - LOG.info("RangerClient.authInit() UGI user: " + ugi.getUserName() + " principal: " + username); - } else { - restClient.setBasicAuthInfo(username, password); - } - } - public RangerClient(String hostName, String authType, String username, String password, String configFile) { restClient = new RangerRESTClient(hostName, configFile, new Configuration()); authInit(authType, username, password); @@ -483,6 +474,34 @@ public List<RangerPurgeResult> purgeRecords(String recordType, int retentionDays return callAPI(PURGE_RECORDS, queryParams, null, TYPE_LIST_PURGE_RESULT); } + /** + * Sets the log level for a specific class or package. + * This operation requires ROLE_SYS_ADMIN role. + * + * @param loggerName The name of the logger (class or package name) + * @param logLevel The log level to set (TRACE, DEBUG, INFO, WARN, ERROR, OFF) + * @return A message indicating the result of the operation + * @throws RangerServiceException if the operation fails + */ + public String setLogLevel(String loggerName, String logLevel) throws RangerServiceException { + Map<String, Object> requestData = new HashMap<>(); + requestData.put("loggerName", loggerName); + requestData.put("logLevel", logLevel); + + return callAPI(SET_LOG_LEVEL, null, requestData, new TypeReference<String>() {}); + } + + private void authInit(String authType, String username, String password) { + if (AUTH_KERBEROS.equalsIgnoreCase(authType)) { + isSecureMode = true; + MiscUtil.loginWithKeyTab(password, username, null); + UserGroupInformation ugi = MiscUtil.getUGILoginUser(); + LOG.info("RangerClient.authInit() UGI user: {} principal: {}", ugi.getUserName(), username); + } else { + restClient.setBasicAuthInfo(username, password); + } + } + private ClientResponse invokeREST(API api, Map<String, String> params, Object request) throws RangerServiceException { final ClientResponse clientResponse; try { diff --git a/intg/src/main/python/apache_ranger/client/ranger_client.py b/intg/src/main/python/apache_ranger/client/ranger_client.py index f6e865107..405e80141 100644 --- a/intg/src/main/python/apache_ranger/client/ranger_client.py +++ b/intg/src/main/python/apache_ranger/client/ranger_client.py @@ -355,8 +355,21 @@ def delete_policy_deltas(self, days, reloadServicePoliciesCache): def purge_records(self, record_type, retention_days): return self.client_http.call_api(RangerClient.PURGE_RECORDS, { 'type': record_type, 'retentionDays': retention_days}) - - + def set_log_level(self, logger_name, log_level): + """ + Sets the log level for a specific class or package. + This operation requires ROLE_SYS_ADMIN role. + + :param logger_name: The name of the logger (class or package name) + :param log_level: The log level to set (TRACE, DEBUG, INFO, WARN, ERROR, OFF) + :return: A message indicating the result of the operation + :raises: RangerServiceException if the operation fails + """ + request_data = { + 'loggerName': logger_name, + 'logLevel': log_level + } + return self.client_http.call_api(RangerClient.SET_LOG_LEVEL, request_data=request_data) # URIs @@ -401,6 +414,7 @@ def purge_records(self, record_type, retention_days): URI_PLUGIN_INFO = URI_BASE + "/plugins/info" URI_POLICY_DELTAS = URI_BASE + "/server/policydeltas" URI_PURGE_RECORDS = URI_BASE + "/server/purge/records" + URI_LOGGERS_SET_LEVEL = "service/admin/set-logger-level" # APIs CREATE_SERVICEDEF = API(URI_SERVICEDEF, HttpMethod.POST, HTTPStatus.OK) @@ -469,6 +483,7 @@ def purge_records(self, record_type, retention_days): DELETE_POLICY_DELTAS = API(URI_POLICY_DELTAS, HttpMethod.DELETE, HTTPStatus.NO_CONTENT) PURGE_RECORDS = API(URI_PURGE_RECORDS, HttpMethod.DELETE, HTTPStatus.OK) + SET_LOG_LEVEL = API(URI_LOGGERS_SET_LEVEL, HttpMethod.POST, HTTPStatus.OK) class HadoopSimpleAuth(AuthBase): diff --git a/security-admin/src/main/java/org/apache/ranger/biz/RangerLogLevelService.java b/security-admin/src/main/java/org/apache/ranger/biz/RangerLogLevelService.java new file mode 100644 index 000000000..b428755e6 --- /dev/null +++ b/security-admin/src/main/java/org/apache/ranger/biz/RangerLogLevelService.java @@ -0,0 +1,115 @@ +/* + * 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.ranger.biz; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import org.apache.commons.lang.StringUtils; +import org.slf4j.ILoggerFactory; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +/** + * Service class to handle log level management operations. + * This class only supports Logback as the logging mechanism. + */ +@Component +public class RangerLogLevelService { + private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(RangerLogLevelService.class); + + private static final String LOGBACK_CLASSIC_PREFIX = "ch.qos.logback.classic"; + + /** + * Sets the log level for a specific class or package. + * @param loggerName The name of the logger (class or package name) + * @param logLevel The log level to set (TRACE, DEBUG, INFO, WARN, ERROR, OFF) + * @return A message indicating the result of the operation + * @throws IllegalArgumentException if the log level is invalid + * @throws UnsupportedOperationException if Logback is not the active logging framework + */ + public String setLogLevel(String loggerName, String logLevel) { + ILoggerFactory iLoggerFactory = LoggerFactory.getILoggerFactory(); + String loggerFactoryClassName = iLoggerFactory.getClass().getName(); + + if (loggerFactoryClassName.startsWith(LOGBACK_CLASSIC_PREFIX)) { + LOG.info("Setting log level for logger '{}' to '{}'", loggerName, logLevel); + + return setLogbackLogLevel(loggerName, logLevel); + } else { + String message = "Logback is the only supported logging mechanism. Detected unsupported SLF4J binding: " + loggerFactoryClassName; + + LOG.error(message); + + throw new UnsupportedOperationException(message); + } + } + + /** + * Sets the Logback log level for a specific logger. + */ + private String setLogbackLogLevel(String loggerName, String logLevel) { + try { + Level level = validateAndParseLogLevel(logLevel); + Logger logger = getLogger(loggerName); + + logger.setLevel(level); + + LOG.info("Successfully set log level for logger '{}' to '{}'", loggerName, level); + + return String.format("Log level for logger '%s' has been set to '%s'", loggerName, level); + } catch (Exception e) { + LOG.error("Failed to set log level for logger '{}' to '{}'", loggerName, logLevel, e); + + throw new RuntimeException("Failed to set log level for logger '" + loggerName + "' to '" + logLevel + "'", e); + } + } + + private static Logger getLogger(String loggerName) { + ILoggerFactory iLoggerFactory = LoggerFactory.getILoggerFactory(); + + if (!(iLoggerFactory instanceof LoggerContext)) { + throw new IllegalStateException("Expected ILoggerFactory to be an instance of LoggerContext, but found " + iLoggerFactory.getClass().getName() + ". Is Logback configured as the SLF4J backend?"); + } + + LoggerContext context = (LoggerContext) iLoggerFactory; + + // Get or create the logger + return context.getLogger(loggerName); + } + + /** + * Validates and parses the log level string. + * @param logLevel The log level string to validate + * @return The corresponding Logback Level object + * @throws IllegalArgumentException if the log level is invalid + */ + private Level validateAndParseLogLevel(String logLevel) { + if (StringUtils.isBlank(logLevel)) { + throw new IllegalArgumentException("Log level cannot be null or empty"); + } + + try { + return Level.valueOf(logLevel.trim().toUpperCase()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid log level: '" + logLevel + "'. " + "Valid levels are: TRACE, DEBUG, INFO, WARN, ERROR, OFF"); + } + } +} diff --git a/security-admin/src/main/java/org/apache/ranger/rest/AdminREST.java b/security-admin/src/main/java/org/apache/ranger/rest/AdminREST.java new file mode 100644 index 000000000..78ab34626 --- /dev/null +++ b/security-admin/src/main/java/org/apache/ranger/rest/AdminREST.java @@ -0,0 +1,147 @@ +/* + * 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.ranger.rest; + +import org.apache.commons.lang.StringUtils; +import org.apache.ranger.biz.RangerLogLevelService; +import org.apache.ranger.common.MessageEnums; +import org.apache.ranger.common.RESTErrorUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Component; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +@Path("admin") +@Component +@Scope("singleton") +public class AdminREST { + private static final Logger LOG = LoggerFactory.getLogger(AdminREST.class); + + @Inject + RangerLogLevelService logLevelService; + + @Inject + RESTErrorUtil restErrorUtil; + + /** + * An endpoint to set the log level for a specific class or package. + * This operation requires ROLE_SYS_ADMIN role as it affects system logging behavior + * and can impact performance and security monitoring. + * + * @param request The request containing loggerName and logLevel + * @return An HTTP response indicating success or failure. + */ + @POST + @Path("/set-logger-level") + @Consumes("application/json") + @Produces("application/json") + @PreAuthorize("hasRole('ROLE_SYS_ADMIN')") + public Response setLogLevel(LogLevelRequest request) { + try { + // Validate input parameters + if (request == null) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("Request body is required") + .build(); + } + + if (StringUtils.isBlank(request.getLoggerName())) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("loggerName is required") + .build(); + } + + if (StringUtils.isBlank(request.getLogLevel())) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("logLevel is required") + .build(); + } + + LOG.info("Setting log level for logger '{}' to '{}'", request.getLoggerName(), request.getLogLevel()); + + String result = logLevelService.setLogLevel(request.getLoggerName().trim(), request.getLogLevel().trim()); + + return Response.ok(result).build(); + } catch (IllegalArgumentException e) { + LOG.error("Invalid parameters for setting log level:", e); + + return Response.status(Response.Status.BAD_REQUEST) + .entity("Invalid parameters: " + e.getMessage()) + .build(); + } catch (UnsupportedOperationException e) { + LOG.error("Unsupported operation for setting log level:", e); + + return Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity("Service not available: " + e.getMessage()) + .build(); + } catch (Exception e) { + LOG.error("Error setting log level for request: {}", request, e); + + throw restErrorUtil.createRESTException(e.getMessage(), MessageEnums.ERROR_SYSTEM); + } + } + + /** + * Request class for JSON payload. + */ + public static class LogLevelRequest { + private String loggerName; + private String logLevel; + + public LogLevelRequest() { + // Default constructor for JSON deserialization + } + + public LogLevelRequest(String loggerName, String logLevel) { + this.loggerName = loggerName; + this.logLevel = logLevel; + } + + public String getLoggerName() { + return loggerName; + } + + public void setLoggerName(String loggerName) { + this.loggerName = loggerName; + } + + public String getLogLevel() { + return logLevel; + } + + public void setLogLevel(String logLevel) { + this.logLevel = logLevel; + } + + @Override + public String toString() { + return "LogLevelRequest{" + + "loggerName='" + loggerName + '\'' + + ", logLevel='" + logLevel + '\'' + + '}'; + } + } +}