mneethiraj commented on code in PR #847: URL: https://github.com/apache/ranger/pull/847#discussion_r2831749643
########## agents-audit/dest-auditserver/src/main/java/org/apache/ranger/audit/destination/RangerAuditServerDestination.java: ########## @@ -0,0 +1,749 @@ +/* + * 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.audit.destination; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hadoop.security.UserGroupInformation; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.auth.AuthSchemeProvider; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.KerberosCredentials; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.config.AuthSchemes; +import org.apache.http.client.config.CookieSpecs; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.config.Lookup; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.auth.SPNegoSchemeFactory; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.client.StandardHttpRequestRetryHandler; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.protocol.HttpContext; +import org.apache.ranger.audit.model.AuditEventBase; +import org.apache.ranger.audit.provider.MiscUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivilegedExceptionAction; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +public class RangerAuditServerDestination extends AuditDestination { + public static final String PROP_AUDITSERVER_URL = "xasecure.audit.destination.auditserver.url"; + public static final String PROP_AUDITSERVER_USER_NAME = "xasecure.audit.destination.auditserver.username"; + public static final String PROP_AUDITSERVER_USER_PASSWORD = "xasecure.audit.destination.auditserver.password"; + public static final String PROP_AUDITSERVER_AUTH_TYPE = "xasecure.audit.destination.auditserver.authentication.type"; + public static final String PROP_AUDITSERVER_JWT_TOKEN = "xasecure.audit.destination.auditserver.jwt.token"; + public static final String PROP_AUDITSERVER_JWT_TOKEN_FILE = "xasecure.audit.destination.auditserver.jwt.token.file"; + public static final String PROP_AUDITSERVER_CLIENT_CONN_TIMEOUT_MS = "xasecure.audit.destination.auditserver.connection.timeout.ms"; + public static final String PROP_AUDITSERVER_CLIENT_READ_TIMEOUT_MS = "xasecure.audit.destination.auditserver.read.timeout.ms"; + public static final String PROP_AUDITSERVER_MAX_CONNECTION = "xasecure.audit.destination.auditserver.max.connections"; + public static final String PROP_AUDITSERVER_MAX_CONNECTION_PER_HOST = "xasecure.audit.destination.auditserver.max.connections.per.host"; + public static final String PROP_AUDITSERVER_VALIDATE_INACTIVE_MS = "xasecure.audit.destination.auditserver.validate.inactivity.ms"; + public static final String PROP_AUDITSERVER_POOL_RETRY_COUNT = "xasecure.audit.destination.auditserver.pool.retry.count"; + public static final String GSON_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; + public static final String REST_ACCEPTED_MIME_TYPE_JSON = "application/json"; + public static final String REST_CONTENT_TYPE_MIME_TYPE_JSON = "application/json"; + public static final String REST_HEADER_ACCEPT = "Accept"; + public static final String REST_HEADER_CONTENT_TYPE = "Content-type"; + public static final String REST_HEADER_AUTHORIZATION = "Authorization"; + public static final String REST_RELATIVE_PATH_POST = "/api/audit/post"; + + // Authentication types + public static final String AUTH_TYPE_KERBEROS = "kerberos"; + public static final String AUTH_TYPE_BASIC = "basic"; + public static final String AUTH_TYPE_JWT = "jwt"; + + private static final Logger LOG = LoggerFactory.getLogger(RangerAuditServerDestination.class); + private static final String PROTOCOL_HTTPS = "https"; + private volatile CloseableHttpClient httpClient; + private volatile Gson gsonBuilder; Review Comment: Instead of serializing using `Gson` here, I suggest using `MiscUtil.stringify(auditEvent)` - similar to other destination implementations like `HDFSAuditDestination` and `Log4JAuditDestination`. ########## agents-audit/dest-auditserver/src/main/java/org/apache/ranger/audit/destination/RangerAuditServerDestination.java: ########## @@ -0,0 +1,749 @@ +/* + * 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.audit.destination; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hadoop.security.UserGroupInformation; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.auth.AuthSchemeProvider; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.KerberosCredentials; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.config.AuthSchemes; +import org.apache.http.client.config.CookieSpecs; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.config.Lookup; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.auth.SPNegoSchemeFactory; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.client.StandardHttpRequestRetryHandler; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.protocol.HttpContext; +import org.apache.ranger.audit.model.AuditEventBase; +import org.apache.ranger.audit.provider.MiscUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivilegedExceptionAction; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +public class RangerAuditServerDestination extends AuditDestination { + public static final String PROP_AUDITSERVER_URL = "xasecure.audit.destination.auditserver.url"; + public static final String PROP_AUDITSERVER_USER_NAME = "xasecure.audit.destination.auditserver.username"; + public static final String PROP_AUDITSERVER_USER_PASSWORD = "xasecure.audit.destination.auditserver.password"; + public static final String PROP_AUDITSERVER_AUTH_TYPE = "xasecure.audit.destination.auditserver.authentication.type"; + public static final String PROP_AUDITSERVER_JWT_TOKEN = "xasecure.audit.destination.auditserver.jwt.token"; + public static final String PROP_AUDITSERVER_JWT_TOKEN_FILE = "xasecure.audit.destination.auditserver.jwt.token.file"; + public static final String PROP_AUDITSERVER_CLIENT_CONN_TIMEOUT_MS = "xasecure.audit.destination.auditserver.connection.timeout.ms"; + public static final String PROP_AUDITSERVER_CLIENT_READ_TIMEOUT_MS = "xasecure.audit.destination.auditserver.read.timeout.ms"; + public static final String PROP_AUDITSERVER_MAX_CONNECTION = "xasecure.audit.destination.auditserver.max.connections"; + public static final String PROP_AUDITSERVER_MAX_CONNECTION_PER_HOST = "xasecure.audit.destination.auditserver.max.connections.per.host"; + public static final String PROP_AUDITSERVER_VALIDATE_INACTIVE_MS = "xasecure.audit.destination.auditserver.validate.inactivity.ms"; + public static final String PROP_AUDITSERVER_POOL_RETRY_COUNT = "xasecure.audit.destination.auditserver.pool.retry.count"; + public static final String GSON_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; + public static final String REST_ACCEPTED_MIME_TYPE_JSON = "application/json"; + public static final String REST_CONTENT_TYPE_MIME_TYPE_JSON = "application/json"; + public static final String REST_HEADER_ACCEPT = "Accept"; + public static final String REST_HEADER_CONTENT_TYPE = "Content-type"; + public static final String REST_HEADER_AUTHORIZATION = "Authorization"; + public static final String REST_RELATIVE_PATH_POST = "/api/audit/post"; + + // Authentication types + public static final String AUTH_TYPE_KERBEROS = "kerberos"; + public static final String AUTH_TYPE_BASIC = "basic"; + public static final String AUTH_TYPE_JWT = "jwt"; + + private static final Logger LOG = LoggerFactory.getLogger(RangerAuditServerDestination.class); + private static final String PROTOCOL_HTTPS = "https"; + private volatile CloseableHttpClient httpClient; + private volatile Gson gsonBuilder; + private String httpURL; + private String authType; + private String jwtToken; + + @Override + public void init(Properties props, String propPrefix) { + LOG.info("==> RangerAuditServerDestination:init()"); + super.init(props, propPrefix); + + this.authType = MiscUtil.getStringProperty(props, PROP_AUDITSERVER_AUTH_TYPE); + if (AUTH_TYPE_JWT.equalsIgnoreCase(authType)) { + LOG.info("JWT authentication configured...."); + initJwtToken(); + } else if (StringUtils.isEmpty(authType)) { + // Authentication priority: JWT → Kerberos → Basic + try { + if (StringUtils.isNotEmpty(MiscUtil.getStringProperty(props, PROP_AUDITSERVER_JWT_TOKEN)) || + StringUtils.isNotEmpty(MiscUtil.getStringProperty(props, PROP_AUDITSERVER_JWT_TOKEN_FILE))) { + this.authType = AUTH_TYPE_JWT; + initJwtToken(); + } else if (isKerberosAuthenticated()) { + this.authType = AUTH_TYPE_KERBEROS; + } else if (StringUtils.isNotEmpty(MiscUtil.getStringProperty(props, PROP_AUDITSERVER_USER_NAME))) { + this.authType = AUTH_TYPE_BASIC; + } + } catch (Exception e) { + LOG.warn("Failed to auto-detect authentication type", e); + } + } + + LOG.info("Audit destination authentication type: {}", authType); + + if (AUTH_TYPE_KERBEROS.equalsIgnoreCase(authType)) { + preAuthenticateKerberos(); + } + + this.httpClient = buildHTTPClient(); + this.gsonBuilder = new GsonBuilder().setDateFormat(GSON_DATE_FORMAT).create(); + LOG.info("<== RangerAuditServerDestination:init()"); + } + + private void initJwtToken() { + LOG.info("==> RangerAuditServerDestination:initJwtToken()"); + + this.jwtToken = MiscUtil.getStringProperty(props, PROP_AUDITSERVER_JWT_TOKEN); + if (StringUtils.isEmpty(jwtToken)) { + String jwtTokenFile = MiscUtil.getStringProperty(props, PROP_AUDITSERVER_JWT_TOKEN_FILE); + if (StringUtils.isNotEmpty(jwtTokenFile)) { + try { + this.jwtToken = readJwtTokenFromFile(jwtTokenFile); + LOG.info("JWT token loaded from file: {}", jwtTokenFile); + } catch (Exception e) { + LOG.error("Failed to read JWT token from file: {}", jwtTokenFile, e); + } + } + } + + if (StringUtils.isEmpty(jwtToken)) { + LOG.warn("JWT authentication configured but no token found. Configure {} or {}", PROP_AUDITSERVER_JWT_TOKEN, PROP_AUDITSERVER_JWT_TOKEN_FILE); + } else { + LOG.info("JWT authentication initialized successfully"); + } + + LOG.info("<== RangerAuditServerDestination:initJwtToken()"); + } + + private String readJwtTokenFromFile(String tokenFile) throws IOException { + InputStream in = null; + try { + in = getFileInputStream(tokenFile); + if (in != null) { + String token = IOUtils.toString(in, Charset.defaultCharset()).trim(); + return token; + } else { + throw new IOException("Unable to read JWT token file: " + tokenFile); + } + } finally { + close(in, tokenFile); + } + } + + /** + * This method proactively obtains the TGT and service ticket for the audit server + * during initialization, so they are cached and ready when the first audit event arrives. + */ + private void preAuthenticateKerberos() { + LOG.info("==> RangerAuditServerDestination:preAuthenticateKerberos()"); + + try { + UserGroupInformation ugi = UserGroupInformation.getLoginUser(); + + if (ugi == null) { + LOG.warn("No UserGroupInformation available for Kerberos pre-authentication"); + return; + } + + if (!ugi.hasKerberosCredentials()) { + LOG.warn("User {} does not have Kerberos credentials for pre-authentication", ugi.getUserName()); + return; + } + + LOG.info("Pre-authenticating Kerberos for user: {}, authMethod: {}", ugi.getUserName(), ugi.getAuthenticationMethod()); + + ugi.checkTGTAndReloginFromKeytab(); + LOG.debug("TGT verified and refreshed if needed for user: {}", ugi.getUserName()); + + // Get the audit server URL to determine the target hostname for service ticket + String auditServerUrl = MiscUtil.getStringProperty(props, PROP_AUDITSERVER_URL); + + if (StringUtils.isNotEmpty(auditServerUrl)) { + try { + URI uri = new URI(auditServerUrl); + String hostname = uri.getHost(); + + LOG.info("Pre-fetching Kerberos service ticket for HTTP/{}", hostname); + + ugi.doAs((PrivilegedExceptionAction<Void>) () -> { + LOG.debug("Kerberos security context initialized for audit server: {}", hostname); + return null; + }); + + LOG.info("Kerberos pre-authentication completed successfully. Service ticket cached for HTTP/{}", hostname); + } catch (URISyntaxException e) { + LOG.warn("Invalid audit server URL format: {}. Skipping service ticket pre-fetch", auditServerUrl, e); + } catch (Exception e) { + LOG.warn("Failed to pre-fetch service ticket for audit server: {}. First request may need to obtain ticket", auditServerUrl, e); + } + } else { + LOG.warn("Audit server URL not configured. Cannot pre-fetch service ticket"); + } + } catch (Exception e) { + LOG.warn("Kerberos pre-authentication failed. First request will retry authentication", e); + } + + LOG.info("<== RangerAuditServerDestination:preAuthenticateKerberos()"); + } + + @Override + public void stop() { + LOG.info("==> RangerAuditServerDestination.stop() called.."); + logStatus(); + if (httpClient != null) { + try { + httpClient.close(); + } catch (IOException ioe) { + LOG.error("Error while closing httpclient in RangerAuditServerDestination!", ioe); + } finally { + httpClient = null; + } + } + } + + /* + * (non-Javadoc) + * + * @see org.apache.ranger.audit.provider.AuditProvider#flush() + */ + @Override + public void flush() { + } + + @Override + public boolean log(Collection<AuditEventBase> events) { + boolean ret = false; + try { + logStatusIfRequired(); + addTotalCount(events.size()); + + if (httpClient == null) { + httpClient = buildHTTPClient(); + if (httpClient == null) { + // HTTP Server is still not initialized. So need return error + addDeferredCount(events.size()); + return ret; + } + } + ret = logAsBatch(events); + } catch (Throwable t) { + addDeferredCount(events.size()); + logError("Error sending audit to HTTP Server", t); + } + return ret; + } + + public boolean isAsync() { + return true; + } + + private boolean logAsBatch(Collection<AuditEventBase> events) { + boolean batchSuccess = false; + int totalEvents = events.size(); + + LOG.debug("==> logAsBatch() Sending batch of {} events to Audit Server....", totalEvents); + + batchSuccess = sendBatch(events); + if (batchSuccess) { + addSuccessCount(totalEvents); + } else { + LOG.error("Failed to send batch of {} events", totalEvents); + addFailedCount(totalEvents); + } + + LOG.debug("<== logAsBatch() Batch processing complete: {}/{} events sent successfully", batchSuccess ? totalEvents : 0, totalEvents); + + return batchSuccess; + } + + private boolean sendBatch(Collection<AuditEventBase> events) { + boolean ret = false; + Map<String, String> queryParams = new HashMap<>(); + try { + UserGroupInformation ugi = UserGroupInformation.getCurrentUser(); + LOG.debug("Sending audit batch of {} events as user: {}", events.size(), ugi.getUserName()); + + PrivilegedExceptionAction<Map<?, ?>> action = + () -> executeHttpBatchRequest(REST_RELATIVE_PATH_POST, queryParams, events, Map.class); + Map<?, ?> response = executeAction(action, ugi); + + if (response != null) { + LOG.info("Audit batch sent successfully. {} events delivered. Response: {}", events.size(), response); + ret = true; + } else { + LOG.error("Received null response from audit server for batch of {} events", events.size()); + ret = false; + } + } catch (Exception e) { + LOG.error("Failed to send audit batch of {} events to {}. Error: {}", events.size(), httpURL, e.getMessage(), e); + + // Log additional context for authentication errors + if (e.getMessage() != null && e.getMessage().contains("401")) { + LOG.error("Authentication failure detected. Verify Kerberos credentials are valid and audit server is reachable."); + } + ret = false; + } + + return ret; + } + + public <T> T executeHttpBatchRequest(String relativeUrl, Map<String, String> params, Review Comment: Consider eliminating `relativeUrl` parameter as it will always be `REST_RELATIVE_PATH_POST` - in `executeHttpBatchRequest()` and `executeHttpRequestPOST()`. ########## agents-audit/dest-auditserver/src/main/java/org/apache/ranger/audit/destination/RangerAuditServerDestination.java: ########## @@ -0,0 +1,749 @@ +/* + * 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.audit.destination; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hadoop.security.UserGroupInformation; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.auth.AuthSchemeProvider; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.KerberosCredentials; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.config.AuthSchemes; +import org.apache.http.client.config.CookieSpecs; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.config.Lookup; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.auth.SPNegoSchemeFactory; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.client.StandardHttpRequestRetryHandler; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.protocol.HttpContext; +import org.apache.ranger.audit.model.AuditEventBase; +import org.apache.ranger.audit.provider.MiscUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivilegedExceptionAction; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +public class RangerAuditServerDestination extends AuditDestination { + public static final String PROP_AUDITSERVER_URL = "xasecure.audit.destination.auditserver.url"; + public static final String PROP_AUDITSERVER_USER_NAME = "xasecure.audit.destination.auditserver.username"; + public static final String PROP_AUDITSERVER_USER_PASSWORD = "xasecure.audit.destination.auditserver.password"; + public static final String PROP_AUDITSERVER_AUTH_TYPE = "xasecure.audit.destination.auditserver.authentication.type"; + public static final String PROP_AUDITSERVER_JWT_TOKEN = "xasecure.audit.destination.auditserver.jwt.token"; + public static final String PROP_AUDITSERVER_JWT_TOKEN_FILE = "xasecure.audit.destination.auditserver.jwt.token.file"; + public static final String PROP_AUDITSERVER_CLIENT_CONN_TIMEOUT_MS = "xasecure.audit.destination.auditserver.connection.timeout.ms"; + public static final String PROP_AUDITSERVER_CLIENT_READ_TIMEOUT_MS = "xasecure.audit.destination.auditserver.read.timeout.ms"; + public static final String PROP_AUDITSERVER_MAX_CONNECTION = "xasecure.audit.destination.auditserver.max.connections"; + public static final String PROP_AUDITSERVER_MAX_CONNECTION_PER_HOST = "xasecure.audit.destination.auditserver.max.connections.per.host"; + public static final String PROP_AUDITSERVER_VALIDATE_INACTIVE_MS = "xasecure.audit.destination.auditserver.validate.inactivity.ms"; + public static final String PROP_AUDITSERVER_POOL_RETRY_COUNT = "xasecure.audit.destination.auditserver.pool.retry.count"; + public static final String GSON_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; + public static final String REST_ACCEPTED_MIME_TYPE_JSON = "application/json"; + public static final String REST_CONTENT_TYPE_MIME_TYPE_JSON = "application/json"; + public static final String REST_HEADER_ACCEPT = "Accept"; + public static final String REST_HEADER_CONTENT_TYPE = "Content-type"; + public static final String REST_HEADER_AUTHORIZATION = "Authorization"; + public static final String REST_RELATIVE_PATH_POST = "/api/audit/post"; + + // Authentication types + public static final String AUTH_TYPE_KERBEROS = "kerberos"; + public static final String AUTH_TYPE_BASIC = "basic"; + public static final String AUTH_TYPE_JWT = "jwt"; + + private static final Logger LOG = LoggerFactory.getLogger(RangerAuditServerDestination.class); + private static final String PROTOCOL_HTTPS = "https"; + private volatile CloseableHttpClient httpClient; Review Comment: Have you considered using `RangerRESTClient`? Handling of multiple URLs and retries are built into this class. Minor refactoring might be needed to parameterize property names for keystore/truststore/basic-auth/jwt. ########## agents-audit/core/src/main/java/org/apache/ranger/audit/provider/BaseAuditHandler.java: ########## @@ -74,16 +74,16 @@ public abstract class BaseAuditHandler implements AuditHandler { int errorLogIntervalMS = 30 * 1000; // Every 30 seconds long lastErrorLogMS; - long totalCount; - long totalSuccessCount; - long totalFailedCount; - long totalStashedCount; - long totalDeferredCount; - long lastIntervalCount; - long lastIntervalSuccessCount; - long lastIntervalFailedCount; - long lastStashedCount; - long lastDeferredCount; + AtomicLong totalCount = new AtomicLong(0); + AtomicLong totalSuccessCount = new AtomicLong(0); + AtomicLong totalFailedCount = new AtomicLong(0); + AtomicLong totalStashedCount = new AtomicLong(0); + AtomicLong totalDeferredCount = new AtomicLong(0); + AtomicLong lastIntervalCount = new AtomicLong(0); Review Comment: `last*Count` variables are updated only in `logStatus()`. Is use of `AtomicLong` necessary for these members? If not, I suggest retaining their type as `long`. ########## agents-audit/dest-auditserver/src/main/java/org/apache/ranger/audit/destination/RangerAuditServerDestination.java: ########## @@ -0,0 +1,749 @@ +/* + * 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.audit.destination; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hadoop.security.UserGroupInformation; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.auth.AuthSchemeProvider; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.KerberosCredentials; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.config.AuthSchemes; +import org.apache.http.client.config.CookieSpecs; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.config.Lookup; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.auth.SPNegoSchemeFactory; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.client.StandardHttpRequestRetryHandler; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.protocol.HttpContext; +import org.apache.ranger.audit.model.AuditEventBase; +import org.apache.ranger.audit.provider.MiscUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivilegedExceptionAction; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +public class RangerAuditServerDestination extends AuditDestination { + public static final String PROP_AUDITSERVER_URL = "xasecure.audit.destination.auditserver.url"; + public static final String PROP_AUDITSERVER_USER_NAME = "xasecure.audit.destination.auditserver.username"; + public static final String PROP_AUDITSERVER_USER_PASSWORD = "xasecure.audit.destination.auditserver.password"; + public static final String PROP_AUDITSERVER_AUTH_TYPE = "xasecure.audit.destination.auditserver.authentication.type"; + public static final String PROP_AUDITSERVER_JWT_TOKEN = "xasecure.audit.destination.auditserver.jwt.token"; + public static final String PROP_AUDITSERVER_JWT_TOKEN_FILE = "xasecure.audit.destination.auditserver.jwt.token.file"; + public static final String PROP_AUDITSERVER_CLIENT_CONN_TIMEOUT_MS = "xasecure.audit.destination.auditserver.connection.timeout.ms"; + public static final String PROP_AUDITSERVER_CLIENT_READ_TIMEOUT_MS = "xasecure.audit.destination.auditserver.read.timeout.ms"; + public static final String PROP_AUDITSERVER_MAX_CONNECTION = "xasecure.audit.destination.auditserver.max.connections"; + public static final String PROP_AUDITSERVER_MAX_CONNECTION_PER_HOST = "xasecure.audit.destination.auditserver.max.connections.per.host"; + public static final String PROP_AUDITSERVER_VALIDATE_INACTIVE_MS = "xasecure.audit.destination.auditserver.validate.inactivity.ms"; + public static final String PROP_AUDITSERVER_POOL_RETRY_COUNT = "xasecure.audit.destination.auditserver.pool.retry.count"; + public static final String GSON_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; + public static final String REST_ACCEPTED_MIME_TYPE_JSON = "application/json"; + public static final String REST_CONTENT_TYPE_MIME_TYPE_JSON = "application/json"; + public static final String REST_HEADER_ACCEPT = "Accept"; + public static final String REST_HEADER_CONTENT_TYPE = "Content-type"; + public static final String REST_HEADER_AUTHORIZATION = "Authorization"; + public static final String REST_RELATIVE_PATH_POST = "/api/audit/post"; + + // Authentication types + public static final String AUTH_TYPE_KERBEROS = "kerberos"; + public static final String AUTH_TYPE_BASIC = "basic"; + public static final String AUTH_TYPE_JWT = "jwt"; + + private static final Logger LOG = LoggerFactory.getLogger(RangerAuditServerDestination.class); + private static final String PROTOCOL_HTTPS = "https"; + private volatile CloseableHttpClient httpClient; + private volatile Gson gsonBuilder; + private String httpURL; + private String authType; + private String jwtToken; + + @Override + public void init(Properties props, String propPrefix) { + LOG.info("==> RangerAuditServerDestination:init()"); + super.init(props, propPrefix); + + this.authType = MiscUtil.getStringProperty(props, PROP_AUDITSERVER_AUTH_TYPE); + if (AUTH_TYPE_JWT.equalsIgnoreCase(authType)) { + LOG.info("JWT authentication configured...."); + initJwtToken(); + } else if (StringUtils.isEmpty(authType)) { + // Authentication priority: JWT → Kerberos → Basic + try { + if (StringUtils.isNotEmpty(MiscUtil.getStringProperty(props, PROP_AUDITSERVER_JWT_TOKEN)) || + StringUtils.isNotEmpty(MiscUtil.getStringProperty(props, PROP_AUDITSERVER_JWT_TOKEN_FILE))) { + this.authType = AUTH_TYPE_JWT; + initJwtToken(); + } else if (isKerberosAuthenticated()) { + this.authType = AUTH_TYPE_KERBEROS; + } else if (StringUtils.isNotEmpty(MiscUtil.getStringProperty(props, PROP_AUDITSERVER_USER_NAME))) { + this.authType = AUTH_TYPE_BASIC; + } + } catch (Exception e) { + LOG.warn("Failed to auto-detect authentication type", e); + } + } + + LOG.info("Audit destination authentication type: {}", authType); + + if (AUTH_TYPE_KERBEROS.equalsIgnoreCase(authType)) { + preAuthenticateKerberos(); + } + + this.httpClient = buildHTTPClient(); + this.gsonBuilder = new GsonBuilder().setDateFormat(GSON_DATE_FORMAT).create(); + LOG.info("<== RangerAuditServerDestination:init()"); + } + + private void initJwtToken() { + LOG.info("==> RangerAuditServerDestination:initJwtToken()"); + + this.jwtToken = MiscUtil.getStringProperty(props, PROP_AUDITSERVER_JWT_TOKEN); + if (StringUtils.isEmpty(jwtToken)) { + String jwtTokenFile = MiscUtil.getStringProperty(props, PROP_AUDITSERVER_JWT_TOKEN_FILE); + if (StringUtils.isNotEmpty(jwtTokenFile)) { + try { + this.jwtToken = readJwtTokenFromFile(jwtTokenFile); + LOG.info("JWT token loaded from file: {}", jwtTokenFile); + } catch (Exception e) { + LOG.error("Failed to read JWT token from file: {}", jwtTokenFile, e); + } + } + } + + if (StringUtils.isEmpty(jwtToken)) { + LOG.warn("JWT authentication configured but no token found. Configure {} or {}", PROP_AUDITSERVER_JWT_TOKEN, PROP_AUDITSERVER_JWT_TOKEN_FILE); + } else { + LOG.info("JWT authentication initialized successfully"); + } + + LOG.info("<== RangerAuditServerDestination:initJwtToken()"); + } + + private String readJwtTokenFromFile(String tokenFile) throws IOException { + InputStream in = null; + try { + in = getFileInputStream(tokenFile); + if (in != null) { + String token = IOUtils.toString(in, Charset.defaultCharset()).trim(); + return token; + } else { + throw new IOException("Unable to read JWT token file: " + tokenFile); + } + } finally { + close(in, tokenFile); + } + } + + /** + * This method proactively obtains the TGT and service ticket for the audit server + * during initialization, so they are cached and ready when the first audit event arrives. + */ + private void preAuthenticateKerberos() { + LOG.info("==> RangerAuditServerDestination:preAuthenticateKerberos()"); + + try { + UserGroupInformation ugi = UserGroupInformation.getLoginUser(); + + if (ugi == null) { + LOG.warn("No UserGroupInformation available for Kerberos pre-authentication"); + return; + } + + if (!ugi.hasKerberosCredentials()) { + LOG.warn("User {} does not have Kerberos credentials for pre-authentication", ugi.getUserName()); + return; + } + + LOG.info("Pre-authenticating Kerberos for user: {}, authMethod: {}", ugi.getUserName(), ugi.getAuthenticationMethod()); + + ugi.checkTGTAndReloginFromKeytab(); + LOG.debug("TGT verified and refreshed if needed for user: {}", ugi.getUserName()); + + // Get the audit server URL to determine the target hostname for service ticket + String auditServerUrl = MiscUtil.getStringProperty(props, PROP_AUDITSERVER_URL); + + if (StringUtils.isNotEmpty(auditServerUrl)) { + try { + URI uri = new URI(auditServerUrl); + String hostname = uri.getHost(); + + LOG.info("Pre-fetching Kerberos service ticket for HTTP/{}", hostname); + + ugi.doAs((PrivilegedExceptionAction<Void>) () -> { + LOG.debug("Kerberos security context initialized for audit server: {}", hostname); + return null; + }); + + LOG.info("Kerberos pre-authentication completed successfully. Service ticket cached for HTTP/{}", hostname); + } catch (URISyntaxException e) { + LOG.warn("Invalid audit server URL format: {}. Skipping service ticket pre-fetch", auditServerUrl, e); + } catch (Exception e) { + LOG.warn("Failed to pre-fetch service ticket for audit server: {}. First request may need to obtain ticket", auditServerUrl, e); + } + } else { + LOG.warn("Audit server URL not configured. Cannot pre-fetch service ticket"); + } + } catch (Exception e) { + LOG.warn("Kerberos pre-authentication failed. First request will retry authentication", e); + } + + LOG.info("<== RangerAuditServerDestination:preAuthenticateKerberos()"); + } + + @Override + public void stop() { + LOG.info("==> RangerAuditServerDestination.stop() called.."); + logStatus(); + if (httpClient != null) { + try { + httpClient.close(); + } catch (IOException ioe) { + LOG.error("Error while closing httpclient in RangerAuditServerDestination!", ioe); + } finally { + httpClient = null; + } + } + } + + /* + * (non-Javadoc) + * + * @see org.apache.ranger.audit.provider.AuditProvider#flush() + */ + @Override + public void flush() { + } + + @Override + public boolean log(Collection<AuditEventBase> events) { + boolean ret = false; + try { + logStatusIfRequired(); + addTotalCount(events.size()); + + if (httpClient == null) { + httpClient = buildHTTPClient(); + if (httpClient == null) { + // HTTP Server is still not initialized. So need return error + addDeferredCount(events.size()); + return ret; + } + } + ret = logAsBatch(events); + } catch (Throwable t) { + addDeferredCount(events.size()); + logError("Error sending audit to HTTP Server", t); + } + return ret; + } + + public boolean isAsync() { + return true; + } + + private boolean logAsBatch(Collection<AuditEventBase> events) { + boolean batchSuccess = false; + int totalEvents = events.size(); + + LOG.debug("==> logAsBatch() Sending batch of {} events to Audit Server....", totalEvents); + + batchSuccess = sendBatch(events); + if (batchSuccess) { + addSuccessCount(totalEvents); + } else { + LOG.error("Failed to send batch of {} events", totalEvents); + addFailedCount(totalEvents); + } + + LOG.debug("<== logAsBatch() Batch processing complete: {}/{} events sent successfully", batchSuccess ? totalEvents : 0, totalEvents); + + return batchSuccess; + } + + private boolean sendBatch(Collection<AuditEventBase> events) { + boolean ret = false; + Map<String, String> queryParams = new HashMap<>(); + try { + UserGroupInformation ugi = UserGroupInformation.getCurrentUser(); + LOG.debug("Sending audit batch of {} events as user: {}", events.size(), ugi.getUserName()); + + PrivilegedExceptionAction<Map<?, ?>> action = + () -> executeHttpBatchRequest(REST_RELATIVE_PATH_POST, queryParams, events, Map.class); + Map<?, ?> response = executeAction(action, ugi); + + if (response != null) { + LOG.info("Audit batch sent successfully. {} events delivered. Response: {}", events.size(), response); + ret = true; + } else { + LOG.error("Received null response from audit server for batch of {} events", events.size()); + ret = false; + } + } catch (Exception e) { + LOG.error("Failed to send audit batch of {} events to {}. Error: {}", events.size(), httpURL, e.getMessage(), e); + + // Log additional context for authentication errors + if (e.getMessage() != null && e.getMessage().contains("401")) { + LOG.error("Authentication failure detected. Verify Kerberos credentials are valid and audit server is reachable."); + } + ret = false; + } + + return ret; + } + + public <T> T executeHttpBatchRequest(String relativeUrl, Map<String, String> params, + Collection<AuditEventBase> events, Class<T> clazz) throws Exception { + T finalResponse = postAndParse(httpURL + relativeUrl, params, events, clazz); + return finalResponse; + } + + public <T> T executeHttpRequestPOST(String relativeUrl, Map<String, String> params, Object obj, Class<T> clazz) throws Exception { Review Comment: If `executeHttpBatchRequest()` is not accessed outside of this class, I suggest keeping its visibility to `private`. Please review all other methods as well. ########## ranger-audit-server/ranger-audit-server-service/src/main/java/org/apache/ranger/audit/rest/AuditREST.java: ########## @@ -0,0 +1,219 @@ +/* + * 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.audit.rest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; +import org.apache.ranger.audit.model.AuthzAuditEvent; +import org.apache.ranger.audit.producer.AuditDestinationMgr; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Path("/audit") +@Component +@Scope("request") +public class AuditREST { + private static final Logger LOG = LoggerFactory.getLogger(AuditREST.class); + private Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").create(); + @Autowired + AuditDestinationMgr auditDestinationMgr; + + /** + * Health check endpoint + */ + @GET + @Path("/health") + @Produces("application/json") + public Response healthCheck() { + LOG.debug("==> AuditREST.healthCheck()"); + Response ret; + String jsonString; + + try { + // Check if audit destination manager is available and healthy + if (auditDestinationMgr != null) { + Map<String, Object> resp = new HashMap<>(); + resp.put("status", "UP"); + resp.put("service", "ranger-audit-server"); + jsonString = buildResponse(resp); + ret = Response.ok() + .entity(jsonString) + .build(); + } else { + Map<String, Object> resp = new HashMap<>(); + resp.put("status", "DOWN"); + resp.put("service", "ranger-audit-server"); + resp.put("reason", "AuditDestinationMgr not available"); + jsonString = buildResponse(resp); + ret = Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity(jsonString) + .build(); + } + } catch (Exception e) { + LOG.error("Health check failed", e); + Map<String, Object> resp = new HashMap<>(); + resp.put("status", "DOWN"); + resp.put("service", "ranger-audit-server"); + resp.put("reason", e.getMessage()); + jsonString = buildResponse(resp); + ret = Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity(jsonString) + .build(); + } + + LOG.debug("<== AuditREST.healthCheck(): {}", ret); + + return ret; + } + + /** + * Status endpoint for monitoring + */ + @GET + @Path("/status") + @Produces("application/json") + public Response getStatus() { + LOG.debug("==> AuditREST.getStatus()"); + + Response ret; + String jsonString; + + try { + String status = (auditDestinationMgr != null) ? "READY" : "NOT_READY"; + + Map<String, Object> resp = new HashMap<>(); + resp.put("status", status); + resp.put("timestamp", System.currentTimeMillis()); + resp.put("service", "ranger-audit-server"); + jsonString = buildResponse(resp); + ret = Response.status(Response.Status.OK) + .entity(jsonString) + .build(); + } catch (Exception e) { + LOG.error("Status check failed", e); + ret = Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("{\"status\":\"ERROR\",\"error\":\"" + e.getMessage() + "\"}") + .build(); + } + + LOG.debug("<== AuditREST.getStatus(): {}", ret); + + return ret; + } + + /** + * Access Audits producer endpoint. + */ + @POST + @Path("/post") Review Comment: `/post' => '/access` ########## ranger-audit-server/ranger-audit-server-service/src/main/java/org/apache/ranger/audit/rest/AuditREST.java: ########## @@ -0,0 +1,219 @@ +/* + * 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.audit.rest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; +import org.apache.ranger.audit.model.AuthzAuditEvent; +import org.apache.ranger.audit.producer.AuditDestinationMgr; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Path("/audit") +@Component +@Scope("request") +public class AuditREST { + private static final Logger LOG = LoggerFactory.getLogger(AuditREST.class); + private Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").create(); + @Autowired + AuditDestinationMgr auditDestinationMgr; + + /** + * Health check endpoint + */ + @GET + @Path("/health") + @Produces("application/json") + public Response healthCheck() { + LOG.debug("==> AuditREST.healthCheck()"); + Response ret; + String jsonString; + + try { + // Check if audit destination manager is available and healthy + if (auditDestinationMgr != null) { + Map<String, Object> resp = new HashMap<>(); + resp.put("status", "UP"); + resp.put("service", "ranger-audit-server"); + jsonString = buildResponse(resp); + ret = Response.ok() + .entity(jsonString) + .build(); + } else { + Map<String, Object> resp = new HashMap<>(); + resp.put("status", "DOWN"); + resp.put("service", "ranger-audit-server"); + resp.put("reason", "AuditDestinationMgr not available"); + jsonString = buildResponse(resp); + ret = Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity(jsonString) + .build(); + } + } catch (Exception e) { + LOG.error("Health check failed", e); + Map<String, Object> resp = new HashMap<>(); + resp.put("status", "DOWN"); + resp.put("service", "ranger-audit-server"); + resp.put("reason", e.getMessage()); + jsonString = buildResponse(resp); + ret = Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity(jsonString) + .build(); + } + + LOG.debug("<== AuditREST.healthCheck(): {}", ret); + + return ret; + } + + /** + * Status endpoint for monitoring + */ + @GET + @Path("/status") + @Produces("application/json") + public Response getStatus() { + LOG.debug("==> AuditREST.getStatus()"); + + Response ret; + String jsonString; + + try { + String status = (auditDestinationMgr != null) ? "READY" : "NOT_READY"; + + Map<String, Object> resp = new HashMap<>(); + resp.put("status", status); + resp.put("timestamp", System.currentTimeMillis()); + resp.put("service", "ranger-audit-server"); + jsonString = buildResponse(resp); + ret = Response.status(Response.Status.OK) + .entity(jsonString) + .build(); + } catch (Exception e) { + LOG.error("Status check failed", e); + ret = Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("{\"status\":\"ERROR\",\"error\":\"" + e.getMessage() + "\"}") + .build(); + } + + LOG.debug("<== AuditREST.getStatus(): {}", ret); + + return ret; + } + + /** + * Access Audits producer endpoint. + */ + @POST + @Path("/post") + @Consumes("application/json") + @Produces("application/json") + public Response postAudit(String accessAudits) { + LOG.debug("==> AuditREST.postAudit(): {}", accessAudits); + + Response ret; + if (accessAudits == null || accessAudits.trim().isEmpty()) { + LOG.warn("Empty or null audit events batch received"); + ret = Response.status(Response.Status.BAD_REQUEST) + .entity(buildErrorResponse("Audit events cannot be empty")) + .build(); + } else if (auditDestinationMgr == null) { + LOG.error("AuditDestinationMgr not initialized"); + ret = Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity(buildErrorResponse("Audit service not available")) + .build(); + } else { + try { + String jsonString; + List<AuthzAuditEvent> events = gson.fromJson(accessAudits, new TypeToken<List<AuthzAuditEvent>>() {}.getType()); Review Comment: Consider eliminating a new instance in every call by using a static final member initialized with `new TypeToken<List<AuthzAuditEvent>>() {}.getType()`. ########## ranger-audit-server/ranger-audit-server-service/src/main/java/org/apache/ranger/audit/rest/AuditREST.java: ########## @@ -0,0 +1,219 @@ +/* + * 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.audit.rest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; +import org.apache.ranger.audit.model.AuthzAuditEvent; +import org.apache.ranger.audit.producer.AuditDestinationMgr; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Path("/audit") +@Component +@Scope("request") +public class AuditREST { + private static final Logger LOG = LoggerFactory.getLogger(AuditREST.class); + private Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").create(); + @Autowired + AuditDestinationMgr auditDestinationMgr; + + /** + * Health check endpoint + */ + @GET + @Path("/health") + @Produces("application/json") + public Response healthCheck() { + LOG.debug("==> AuditREST.healthCheck()"); + Response ret; + String jsonString; + + try { + // Check if audit destination manager is available and healthy + if (auditDestinationMgr != null) { + Map<String, Object> resp = new HashMap<>(); + resp.put("status", "UP"); + resp.put("service", "ranger-audit-server"); + jsonString = buildResponse(resp); + ret = Response.ok() + .entity(jsonString) + .build(); + } else { + Map<String, Object> resp = new HashMap<>(); + resp.put("status", "DOWN"); + resp.put("service", "ranger-audit-server"); + resp.put("reason", "AuditDestinationMgr not available"); + jsonString = buildResponse(resp); + ret = Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity(jsonString) + .build(); + } + } catch (Exception e) { + LOG.error("Health check failed", e); + Map<String, Object> resp = new HashMap<>(); + resp.put("status", "DOWN"); + resp.put("service", "ranger-audit-server"); + resp.put("reason", e.getMessage()); + jsonString = buildResponse(resp); + ret = Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity(jsonString) + .build(); + } + + LOG.debug("<== AuditREST.healthCheck(): {}", ret); + + return ret; + } + + /** + * Status endpoint for monitoring + */ + @GET + @Path("/status") + @Produces("application/json") + public Response getStatus() { + LOG.debug("==> AuditREST.getStatus()"); + + Response ret; + String jsonString; + + try { + String status = (auditDestinationMgr != null) ? "READY" : "NOT_READY"; + + Map<String, Object> resp = new HashMap<>(); + resp.put("status", status); + resp.put("timestamp", System.currentTimeMillis()); + resp.put("service", "ranger-audit-server"); + jsonString = buildResponse(resp); + ret = Response.status(Response.Status.OK) + .entity(jsonString) + .build(); + } catch (Exception e) { + LOG.error("Status check failed", e); + ret = Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("{\"status\":\"ERROR\",\"error\":\"" + e.getMessage() + "\"}") + .build(); + } + + LOG.debug("<== AuditREST.getStatus(): {}", ret); + + return ret; + } + + /** + * Access Audits producer endpoint. + */ + @POST + @Path("/post") + @Consumes("application/json") + @Produces("application/json") + public Response postAudit(String accessAudits) { Review Comment: Consider replacing the parameter type from `String` to `List<AuthzAuditEvent>` and eliminate the deserialization in line 162. ########## agents-common/src/main/java/org/apache/ranger/plugin/policyengine/RangerAccessResult.java: ########## @@ -259,6 +259,16 @@ public int getServiceType() { return ret; } + public String getServiceTypeName() { + String ret = null; Review Comment: Consider simplifying this with: ``` return serviceDef != null ? serviceDef.getName() : null; ``` ########## ranger-audit-server/ranger-audit-server-service/src/main/java/org/apache/ranger/audit/rest/AuditREST.java: ########## @@ -0,0 +1,219 @@ +/* + * 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.audit.rest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; +import org.apache.ranger.audit.model.AuthzAuditEvent; +import org.apache.ranger.audit.producer.AuditDestinationMgr; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Path("/audit") +@Component +@Scope("request") +public class AuditREST { + private static final Logger LOG = LoggerFactory.getLogger(AuditREST.class); + private Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").create(); + @Autowired + AuditDestinationMgr auditDestinationMgr; + + /** + * Health check endpoint + */ + @GET + @Path("/health") + @Produces("application/json") + public Response healthCheck() { + LOG.debug("==> AuditREST.healthCheck()"); + Response ret; + String jsonString; + + try { + // Check if audit destination manager is available and healthy + if (auditDestinationMgr != null) { + Map<String, Object> resp = new HashMap<>(); + resp.put("status", "UP"); + resp.put("service", "ranger-audit-server"); + jsonString = buildResponse(resp); + ret = Response.ok() + .entity(jsonString) + .build(); + } else { + Map<String, Object> resp = new HashMap<>(); + resp.put("status", "DOWN"); + resp.put("service", "ranger-audit-server"); + resp.put("reason", "AuditDestinationMgr not available"); + jsonString = buildResponse(resp); + ret = Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity(jsonString) + .build(); + } + } catch (Exception e) { + LOG.error("Health check failed", e); + Map<String, Object> resp = new HashMap<>(); + resp.put("status", "DOWN"); + resp.put("service", "ranger-audit-server"); + resp.put("reason", e.getMessage()); + jsonString = buildResponse(resp); + ret = Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity(jsonString) + .build(); + } + + LOG.debug("<== AuditREST.healthCheck(): {}", ret); + + return ret; + } + + /** + * Status endpoint for monitoring + */ + @GET + @Path("/status") + @Produces("application/json") + public Response getStatus() { + LOG.debug("==> AuditREST.getStatus()"); + + Response ret; + String jsonString; + + try { + String status = (auditDestinationMgr != null) ? "READY" : "NOT_READY"; + + Map<String, Object> resp = new HashMap<>(); + resp.put("status", status); + resp.put("timestamp", System.currentTimeMillis()); + resp.put("service", "ranger-audit-server"); + jsonString = buildResponse(resp); + ret = Response.status(Response.Status.OK) + .entity(jsonString) + .build(); + } catch (Exception e) { + LOG.error("Status check failed", e); + ret = Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("{\"status\":\"ERROR\",\"error\":\"" + e.getMessage() + "\"}") + .build(); + } + + LOG.debug("<== AuditREST.getStatus(): {}", ret); + + return ret; + } + + /** + * Access Audits producer endpoint. + */ + @POST + @Path("/post") + @Consumes("application/json") + @Produces("application/json") + public Response postAudit(String accessAudits) { Review Comment: `postAudit()` => `logAccessAudit()` ########## ranger-audit-server/ranger-audit-server-service/src/main/java/org/apache/ranger/audit/rest/AuditREST.java: ########## @@ -0,0 +1,219 @@ +/* + * 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.audit.rest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; +import org.apache.ranger.audit.model.AuthzAuditEvent; +import org.apache.ranger.audit.producer.AuditDestinationMgr; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Path("/audit") +@Component +@Scope("request") +public class AuditREST { + private static final Logger LOG = LoggerFactory.getLogger(AuditREST.class); + private Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").create(); + @Autowired + AuditDestinationMgr auditDestinationMgr; + + /** + * Health check endpoint + */ + @GET + @Path("/health") + @Produces("application/json") + public Response healthCheck() { + LOG.debug("==> AuditREST.healthCheck()"); + Response ret; + String jsonString; + + try { + // Check if audit destination manager is available and healthy + if (auditDestinationMgr != null) { + Map<String, Object> resp = new HashMap<>(); + resp.put("status", "UP"); + resp.put("service", "ranger-audit-server"); + jsonString = buildResponse(resp); + ret = Response.ok() + .entity(jsonString) + .build(); + } else { + Map<String, Object> resp = new HashMap<>(); + resp.put("status", "DOWN"); + resp.put("service", "ranger-audit-server"); + resp.put("reason", "AuditDestinationMgr not available"); + jsonString = buildResponse(resp); + ret = Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity(jsonString) + .build(); + } + } catch (Exception e) { + LOG.error("Health check failed", e); + Map<String, Object> resp = new HashMap<>(); + resp.put("status", "DOWN"); + resp.put("service", "ranger-audit-server"); + resp.put("reason", e.getMessage()); + jsonString = buildResponse(resp); + ret = Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity(jsonString) + .build(); + } + + LOG.debug("<== AuditREST.healthCheck(): {}", ret); + + return ret; + } + + /** + * Status endpoint for monitoring + */ + @GET + @Path("/status") + @Produces("application/json") + public Response getStatus() { + LOG.debug("==> AuditREST.getStatus()"); + + Response ret; + String jsonString; + + try { + String status = (auditDestinationMgr != null) ? "READY" : "NOT_READY"; + + Map<String, Object> resp = new HashMap<>(); + resp.put("status", status); + resp.put("timestamp", System.currentTimeMillis()); + resp.put("service", "ranger-audit-server"); + jsonString = buildResponse(resp); + ret = Response.status(Response.Status.OK) + .entity(jsonString) + .build(); + } catch (Exception e) { + LOG.error("Status check failed", e); + ret = Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("{\"status\":\"ERROR\",\"error\":\"" + e.getMessage() + "\"}") + .build(); + } + + LOG.debug("<== AuditREST.getStatus(): {}", ret); + + return ret; + } + + /** + * Access Audits producer endpoint. + */ + @POST + @Path("/post") + @Consumes("application/json") + @Produces("application/json") + public Response postAudit(String accessAudits) { + LOG.debug("==> AuditREST.postAudit(): {}", accessAudits); + + Response ret; + if (accessAudits == null || accessAudits.trim().isEmpty()) { + LOG.warn("Empty or null audit events batch received"); + ret = Response.status(Response.Status.BAD_REQUEST) + .entity(buildErrorResponse("Audit events cannot be empty")) + .build(); + } else if (auditDestinationMgr == null) { + LOG.error("AuditDestinationMgr not initialized"); + ret = Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity(buildErrorResponse("Audit service not available")) + .build(); + } else { + try { + String jsonString; + List<AuthzAuditEvent> events = gson.fromJson(accessAudits, new TypeToken<List<AuthzAuditEvent>>() {}.getType()); + + for (AuthzAuditEvent event : events) { Review Comment: Only aithorized users, by default service admin users, should be allowed to log access audit logs. Please add this validation. Consider receiving the serviceName as a url parameter - `/access/{serviceName}` - this will make it easier to perform validation, especially since multiple events are being processed in this call. It will be necessary to enforce that all events are for the same service. ########## ranger-audit-server/ranger-audit-server-service/src/main/java/org/apache/ranger/audit/rest/AuditREST.java: ########## @@ -0,0 +1,219 @@ +/* + * 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.audit.rest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; +import org.apache.ranger.audit.model.AuthzAuditEvent; +import org.apache.ranger.audit.producer.AuditDestinationMgr; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Path("/audit") +@Component +@Scope("request") +public class AuditREST { + private static final Logger LOG = LoggerFactory.getLogger(AuditREST.class); + private Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").create(); + @Autowired + AuditDestinationMgr auditDestinationMgr; + + /** + * Health check endpoint + */ + @GET + @Path("/health") + @Produces("application/json") + public Response healthCheck() { + LOG.debug("==> AuditREST.healthCheck()"); + Response ret; + String jsonString; + + try { + // Check if audit destination manager is available and healthy + if (auditDestinationMgr != null) { + Map<String, Object> resp = new HashMap<>(); + resp.put("status", "UP"); + resp.put("service", "ranger-audit-server"); + jsonString = buildResponse(resp); + ret = Response.ok() + .entity(jsonString) + .build(); + } else { + Map<String, Object> resp = new HashMap<>(); + resp.put("status", "DOWN"); + resp.put("service", "ranger-audit-server"); + resp.put("reason", "AuditDestinationMgr not available"); + jsonString = buildResponse(resp); + ret = Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity(jsonString) + .build(); + } + } catch (Exception e) { + LOG.error("Health check failed", e); + Map<String, Object> resp = new HashMap<>(); + resp.put("status", "DOWN"); + resp.put("service", "ranger-audit-server"); + resp.put("reason", e.getMessage()); + jsonString = buildResponse(resp); + ret = Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity(jsonString) + .build(); + } + + LOG.debug("<== AuditREST.healthCheck(): {}", ret); + + return ret; + } + + /** + * Status endpoint for monitoring + */ + @GET + @Path("/status") + @Produces("application/json") + public Response getStatus() { + LOG.debug("==> AuditREST.getStatus()"); + + Response ret; + String jsonString; + + try { + String status = (auditDestinationMgr != null) ? "READY" : "NOT_READY"; + + Map<String, Object> resp = new HashMap<>(); + resp.put("status", status); + resp.put("timestamp", System.currentTimeMillis()); + resp.put("service", "ranger-audit-server"); + jsonString = buildResponse(resp); + ret = Response.status(Response.Status.OK) + .entity(jsonString) + .build(); + } catch (Exception e) { + LOG.error("Status check failed", e); + ret = Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("{\"status\":\"ERROR\",\"error\":\"" + e.getMessage() + "\"}") + .build(); + } + + LOG.debug("<== AuditREST.getStatus(): {}", ret); + + return ret; + } + + /** + * Access Audits producer endpoint. + */ + @POST + @Path("/post") + @Consumes("application/json") + @Produces("application/json") + public Response postAudit(String accessAudits) { + LOG.debug("==> AuditREST.postAudit(): {}", accessAudits); + + Response ret; + if (accessAudits == null || accessAudits.trim().isEmpty()) { + LOG.warn("Empty or null audit events batch received"); + ret = Response.status(Response.Status.BAD_REQUEST) + .entity(buildErrorResponse("Audit events cannot be empty")) + .build(); + } else if (auditDestinationMgr == null) { + LOG.error("AuditDestinationMgr not initialized"); + ret = Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity(buildErrorResponse("Audit service not available")) + .build(); + } else { + try { + String jsonString; + List<AuthzAuditEvent> events = gson.fromJson(accessAudits, new TypeToken<List<AuthzAuditEvent>>() {}.getType()); + + for (AuthzAuditEvent event : events) { + auditDestinationMgr.log(event); + } + + Map<String, Object> response = new HashMap<>(); + response.put("total", events.size()); + response.put("timestamp", System.currentTimeMillis()); + jsonString = buildResponse(response); + ret = Response.status(Response.Status.OK) + .entity(jsonString) + .build(); + } catch (JsonSyntaxException e) { + LOG.error("Invalid Access audit JSON string...", e); + ret = Response.status(Response.Status.BAD_REQUEST) + .entity(buildErrorResponse("Invalid Access audit JSON string..." + e.getMessage())) + .build(); + } catch (Exception e) { + LOG.error("Error processing access audits batch...", e); + ret = Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(buildErrorResponse("Invalid Access audit JSON string..." + e.getMessage())) + .build(); + } + } + + LOG.debug("<== AuditREST.postAudit(): HttpStatus {}", ret.getStatus()); + + return ret; + } + + private String buildResponse(Map<String, Object> respMap) { + String ret; + + try { + ObjectMapper objectMapper = new ObjectMapper(); Review Comment: `ObjectMapper` could be an expensive object to create. I suggest retrieving the instance with a call to `MiscUtil.getMapper()`. Please review other such occurances as well. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
