Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion intg/src/main/java/org/apache/ranger/RangerClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -152,7 +153,7 @@ public class RangerClient {
public static final API GET_PLUGIN_INFO = new API(URI_PLUGIN_INFO, HttpMethod.GET, Response.Status.OK);
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>>() {};
Expand Down Expand Up @@ -483,6 +484,23 @@ 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 ClientResponse invokeREST(API api, Map<String, String> params, Object request) throws RangerServiceException {
final ClientResponse clientResponse;
try {
Expand Down
19 changes: 17 additions & 2 deletions intg/src/main/python/apache_ranger/client/ranger_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
}
}
}
147 changes: 147 additions & 0 deletions security-admin/src/main/java/org/apache/ranger/rest/AdminREST.java
Original file line number Diff line number Diff line change
@@ -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 + '\'' +
'}';
}
}
}