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]

Reply via email to