This is an automated email from the ASF dual-hosted git repository.

ramackri pushed a commit to branch ranger-2.10
in repository https://gitbox.apache.org/repos/asf/ranger.git


The following commit(s) were added to refs/heads/ranger-2.10 by this push:
     new 55beb757d RANGER-5482: Admin OpenSearch audit UI for ranger-2.10 
(PR-2) (#1262)
55beb757d is described below

commit 55beb757d5ec2015126156fc49b2eb1270ecae61
Author: Ramachandran Krishnan <[email protected]>
AuthorDate: Tue Oct 6 23:53:22 2026 +0530

    RANGER-5482: Admin OpenSearch audit UI for ranger-2.10 (PR-2) (#1262)
    
    * RANGER-5482: Admin OpenSearch audit UI for ranger-2.10 (PR-2)
    
    Add OpenSearch audit query/bootstrap on ranger-2.10 with minimal edits to 
existing files; avoid master-only reformatting 
(EmbeddedServer/XAuditMgr/ElasticSearchMgr churn).
    
    * RANGER-5482: Address PR review for Admin OpenSearch audit path
    
    Wire PDP-style audit-server config patterns into OpenSearch admin code:
    safe int config parsing, audit ingestor timezone property, UTF-8 schema
    load, simplify OpenSearchMgr locking, and standard imports.
    
    ---------
    
    Co-authored-by: ramk <[email protected]>
---
 .../ranger/server/tomcat/EmbeddedServer.java       |  12 +
 .../server/tomcat/OpenSearchIndexBootStrapper.java | 220 +++++++++++++
 .../conf/ranger_opensearch_schema.json             | 136 ++++++++
 security-admin/pom.xml                             |   5 +
 .../java/org/apache/ranger/biz/RangerBizUtil.java  |   1 +
 .../main/java/org/apache/ranger/biz/XAuditMgr.java |   8 +
 .../opensearch/OpenSearchAccessAuditsService.java  | 285 ++++++++++++++++
 .../apache/ranger/opensearch/OpenSearchMgr.java    | 193 +++++++++++
 .../apache/ranger/opensearch/OpenSearchUtil.java   | 362 +++++++++++++++++++++
 .../OpenSearchAccessAuditsServiceTest.java         | 185 +++++++++++
 .../ranger/opensearch/OpenSearchMgrTest.java       |  55 ++++
 .../ranger/opensearch/OpenSearchUtilTest.java      | 219 +++++++++++++
 12 files changed, 1681 insertions(+)

diff --git 
a/embeddedwebserver/src/main/java/org/apache/ranger/server/tomcat/EmbeddedServer.java
 
b/embeddedwebserver/src/main/java/org/apache/ranger/server/tomcat/EmbeddedServer.java
index c1b2cdb52..76427f532 100644
--- 
a/embeddedwebserver/src/main/java/org/apache/ranger/server/tomcat/EmbeddedServer.java
+++ 
b/embeddedwebserver/src/main/java/org/apache/ranger/server/tomcat/EmbeddedServer.java
@@ -68,8 +68,10 @@ public class EmbeddedServer {
        private static final String AUDIT_SOURCE_TYPE = 
"ranger.audit.source.type";
        private static final String AUDIT_SOURCE_SOLR = "solr";
        private static final String AUDIT_SOURCE_ES = "elasticsearch";
+       private static final String AUDIT_SOURCE_OPENSEARCH = "opensearch";
        private static final String SOLR_BOOTSTRAP_ENABLED = 
"ranger.audit.solr.bootstrap.enabled";
        private static final String ES_BOOTSTRAP_ENABLED = 
"ranger.audit.elasticsearch.bootstrap.enabled";
+       private static final String OS_BOOTSTRAP_ENABLED = 
"ranger.audit.opensearch.bootstrap.enabled";
        private static final String ADMIN_USER_KEYTAB = 
"ranger.admin.kerberos.keytab";
 
        private static final String ADMIN_NAME_RULES = 
"hadoop.security.auth_to_local";
@@ -351,6 +353,16 @@ private void startServer(final Tomcat server) {
                                                        LOG.severe("Error while 
setting elasticsearch " + e);
                                                }
                                        }
+                               } else if 
(AUDIT_SOURCE_OPENSEARCH.equalsIgnoreCase(auditSourceType)) {
+                                       boolean osBootstrapEnabled = 
Boolean.valueOf(EmbeddedServerUtil.getConfig(OS_BOOTSTRAP_ENABLED, "true"));
+                                       if (osBootstrapEnabled) {
+                                               try {
+                                                       
OpenSearchIndexBootStrapper osSchemaSetup = new OpenSearchIndexBootStrapper();
+                                                       osSchemaSetup.start();
+                                               } catch (Exception e) {
+                                                       LOG.severe("Error while 
setting opensearch " + e);
+                                               }
+                                       }
                                }
                        }
 
diff --git 
a/embeddedwebserver/src/main/java/org/apache/ranger/server/tomcat/OpenSearchIndexBootStrapper.java
 
b/embeddedwebserver/src/main/java/org/apache/ranger/server/tomcat/OpenSearchIndexBootStrapper.java
new file mode 100644
index 000000000..275fad119
--- /dev/null
+++ 
b/embeddedwebserver/src/main/java/org/apache/ranger/server/tomcat/OpenSearchIndexBootStrapper.java
@@ -0,0 +1,220 @@
+/*
+ * 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.server.tomcat;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.logging.Logger;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.HttpHost;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.entity.ContentType;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.nio.entity.NStringEntity;
+import org.apache.http.util.EntityUtils;
+import org.elasticsearch.client.Request;
+import org.elasticsearch.client.Response;
+import org.elasticsearch.client.RestClient;
+import org.elasticsearch.client.RestClientBuilder;
+
+public class OpenSearchIndexBootStrapper extends Thread {
+    private static final Logger LOG = 
Logger.getLogger(OpenSearchIndexBootStrapper.class.getName());
+
+    private static final String CONFIG_PREFIX     = "ranger.audit.opensearch";
+    private static final String CONFIG_URLS       = CONFIG_PREFIX + ".urls";
+    private static final String CONFIG_PORT       = CONFIG_PREFIX + ".port";
+    private static final String CONFIG_PROTOCOL   = CONFIG_PREFIX + 
".protocol";
+    private static final String CONFIG_USER       = CONFIG_PREFIX + ".user";
+    private static final String CONFIG_PASSWORD   = CONFIG_PREFIX + 
".password";
+    private static final String CONFIG_INDEX      = CONFIG_PREFIX + ".index";
+    private static final String CONFIG_INTERVAL   = CONFIG_PREFIX + 
".time.interval";
+    private static final String CONFIG_SHARDS     = CONFIG_PREFIX + 
".no.shards";
+    private static final String CONFIG_REPLICAS   = CONFIG_PREFIX + 
".no.replica";
+    private static final String CONFIG_MAX_RETRY  = CONFIG_PREFIX + 
".max.retry";
+    private static final String SCHEMA_FILE_NAME  = 
"ranger_opensearch_schema.json";
+
+    private volatile RestClient client;
+    private String  index;
+    private String  urls;
+    private String  protocol;
+    private String  user;
+    private String  password;
+    private int     port;
+    private int     noOfShards;
+    private int     noOfReplicas;
+    private int     maxRetry;
+    private long    timeInterval;
+
+    @Override
+    public void run() {
+        LOG.info("OpenSearchIndexBootStrapper: starting...");
+
+        readConfig();
+
+        if (StringUtils.isBlank(urls) || "NONE".equalsIgnoreCase(urls)) {
+            LOG.severe("OpenSearch URL is not configured. Aborting 
bootstrap.");
+
+            return;
+        }
+
+        int retryCounter = 0;
+
+        while (maxRetry == -1 || retryCounter < maxRetry) {
+            retryCounter++;
+
+            try {
+                LOG.info("Trying to acquire OpenSearch connection");
+
+                connect();
+
+                if (client == null) {
+                    logErrorAndWait(retryCounter, "Failed to create OpenSearch 
client");
+
+                    continue;
+                }
+
+                LOG.info("Connection to OpenSearch established successfully");
+
+                if (indexExists()) {
+                    LOG.info("Index '" + index + "' already exists. Bootstrap 
complete.");
+
+                    return;
+                }
+
+                createIndex();
+
+                LOG.info("Index '" + index + "' created successfully.");
+
+                return;
+            } catch (Exception e) {
+                logErrorAndWait(retryCounter, e.getMessage());
+            }
+        }
+
+        LOG.severe("OpenSearch index bootstrap failed after " + retryCounter + 
" attempts.");
+    }
+
+    private void readConfig() {
+        urls         = EmbeddedServerUtil.getConfig(CONFIG_URLS, "");
+        protocol     = EmbeddedServerUtil.getConfig(CONFIG_PROTOCOL, "http");
+        user         = EmbeddedServerUtil.getConfig(CONFIG_USER, "");
+        password     = EmbeddedServerUtil.getConfig(CONFIG_PASSWORD, "");
+        port         = EmbeddedServerUtil.getIntConfig(CONFIG_PORT, 9200);
+        index        = EmbeddedServerUtil.getConfig(CONFIG_INDEX, 
"ranger_audits");
+        noOfShards   = EmbeddedServerUtil.getIntConfig(CONFIG_SHARDS, 1);
+        noOfReplicas = EmbeddedServerUtil.getIntConfig(CONFIG_REPLICAS, 1);
+        maxRetry     = EmbeddedServerUtil.getIntConfig(CONFIG_MAX_RETRY, 30);
+        timeInterval = EmbeddedServerUtil.getLongConfig(CONFIG_INTERVAL, 
60000L);
+    }
+
+    private void connect() {
+        if (client != null) {
+            return;
+        }
+
+        HttpHost[] hosts = 
Arrays.stream(urls.split(",")).map(String::trim).filter(h -> 
!h.isEmpty()).map(h -> new HttpHost(h, port, 
protocol)).toArray(HttpHost[]::new);
+
+        RestClientBuilder builder = RestClient.builder(hosts);
+
+        if (StringUtils.isNotBlank(user) && StringUtils.isNotBlank(password) 
&& !"NONE".equalsIgnoreCase(user) && !"NONE".equalsIgnoreCase(password)) {
+            CredentialsProvider credentialsProvider = new 
BasicCredentialsProvider();
+            credentialsProvider.setCredentials(AuthScope.ANY, new 
UsernamePasswordCredentials(user, password));
+            builder.setHttpClientConfigCallback(httpClientBuilder -> 
httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider));
+        }
+
+        client = builder.build();
+    }
+
+    private boolean indexExists() throws IOException {
+        Request request = new Request("HEAD", "/" + index);
+        request.addParameter("ignore", "404");
+
+        Response response = client.performRequest(request);
+
+        return response.getStatusLine().getStatusCode() == 200;
+    }
+
+    private void createIndex() throws IOException {
+        String schemaJson = loadSchemaFile();
+        String body       = buildCreateIndexBody(schemaJson);
+
+        Request request = new Request("PUT", "/" + index);
+
+        request.setEntity(new NStringEntity(body, 
ContentType.APPLICATION_JSON));
+
+        Response response = client.performRequest(request);
+        String result = EntityUtils.toString(response.getEntity());
+
+        if (response.getStatusLine().getStatusCode() >= 400) {
+            throw new IOException("Failed to create index '" + index + "': " + 
result);
+        }
+
+        LOG.info("Create index response: " + result);
+    }
+
+    private String buildCreateIndexBody(String mappingsJson) {
+        return "{\"settings\":{\"number_of_shards\":" + noOfShards
+                + ",\"number_of_replicas\":" + noOfReplicas
+                + "},\"mappings\":" + mappingsJson + "}";
+    }
+
+    private String loadSchemaFile() throws IOException {
+        String jarLocation;
+
+        try {
+            jarLocation = 
this.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
+        } catch (Exception e) {
+            throw new IOException("Cannot determine Ranger home directory", e);
+        }
+
+        String rangerHomeDir = new 
File(jarLocation).getParentFile().getParentFile().getParentFile().getPath();
+        String schemaPath    = Paths.get(rangerHomeDir, "contrib", 
"opensearch_for_audit_setup", "conf", SCHEMA_FILE_NAME).toString();
+
+        File schemaFile = new File(schemaPath);
+
+        if (!schemaFile.exists()) {
+            throw new IOException("OpenSearch schema file not found: " + 
schemaPath);
+        }
+
+        return new String(Files.readAllBytes(Paths.get(schemaPath)), 
StandardCharsets.UTF_8);
+    }
+
+    private void logErrorAndWait(int retryCounter, String message) {
+        int attemptsLeft = maxRetry == -1 ? -1 : maxRetry - retryCounter;
+
+        LOG.severe("Error during OpenSearch bootstrap. [retrying after " + 
timeInterval
+                + " ms]. Attempts left: " + attemptsLeft + ". Error: " + 
message);
+
+        try {
+            Thread.sleep(timeInterval);
+        } catch (InterruptedException e) {
+            LOG.warning("OpenSearch bootstrap thread interrupted");
+
+            Thread.currentThread().interrupt();
+        }
+    }
+}
diff --git 
a/security-admin/contrib/opensearch_for_audit_setup/conf/ranger_opensearch_schema.json
 
b/security-admin/contrib/opensearch_for_audit_setup/conf/ranger_opensearch_schema.json
new file mode 100644
index 000000000..f8f3bbf2d
--- /dev/null
+++ 
b/security-admin/contrib/opensearch_for_audit_setup/conf/ranger_opensearch_schema.json
@@ -0,0 +1,136 @@
+{
+  "properties": {
+    "_expire_at_": {
+      "type": "date",
+      "store": true,
+      "doc_values": true
+    },
+    "_ttl_": {
+      "type": "text",
+      "store": true
+    },
+    "_version_": {
+      "type": "long",
+      "store": true,
+      "index": false
+    },
+    "access": {
+      "type": "keyword"
+    },
+    "action": {
+      "type": "keyword"
+    },
+    "agent": {
+      "type": "keyword"
+    },
+    "agentHost": {
+      "type": "keyword"
+    },
+    "cliIP": {
+      "type": "keyword"
+    },
+    "cliType": {
+      "type": "keyword"
+    },
+    "cluster": {
+      "type": "keyword"
+    },
+    "reqContext": {
+      "type": "keyword"
+    },
+    "enforcer": {
+      "type": "keyword"
+    },
+    "event_count": {
+      "type": "long",
+      "doc_values": true
+    },
+    "event_dur_ms": {
+      "type": "long",
+      "doc_values": true
+    },
+    "evtTime": {
+      "type": "date",
+      "doc_values": true
+    },
+    "id": {
+      "type": "keyword",
+      "store": true
+    },
+    "logType": {
+      "type": "keyword"
+    },
+    "policy": {
+      "type": "long",
+      "doc_values": true
+    },
+    "proxyUsers": {
+      "type": "keyword"
+    },
+    "reason": {
+      "type": "text"
+    },
+    "repo": {
+      "type": "keyword"
+    },
+    "repoType": {
+      "type": "integer",
+      "doc_values": true
+    },
+    "req_caller_id": {
+      "type": "keyword"
+    },
+    "req_self_id": {
+      "type": "keyword"
+    },
+    "reqData": {
+      "type": "text"
+    },
+    "reqUser": {
+      "type": "keyword"
+    },
+    "resType": {
+      "type": "keyword"
+    },
+    "resource": {
+      "type": "keyword"
+    },
+    "result": {
+      "type": "integer"
+    },
+    "seq_num": {
+      "type": "long",
+      "doc_values": true
+    },
+    "sess": {
+      "type": "keyword"
+    },
+    "tags": {
+      "type": "keyword"
+    },
+    "tags_str": {
+      "type": "text"
+    },
+    "datasets": {
+      "type": "keyword"
+    },
+    "projects": {
+      "type": "keyword"
+    },
+    "datasetIds": {
+      "type": "long"
+    },
+    "text": {
+      "type": "text"
+    },
+    "zoneName": {
+      "type": "keyword"
+    },
+    "policyVersion": {
+      "type": "long"
+    },
+    "additionalInfo": {
+      "type": "text"
+    }
+  }
+}
diff --git a/security-admin/pom.xml b/security-admin/pom.xml
index 3929423c5..8531426bf 100644
--- a/security-admin/pom.xml
+++ b/security-admin/pom.xml
@@ -522,6 +522,11 @@
             <artifactId>ranger-audit-dest-es</artifactId>
             <version>${project.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.ranger</groupId>
+            <artifactId>ranger-audit-dest-os</artifactId>
+            <version>${project.version}</version>
+        </dependency>
         <dependency>
             <groupId>org.apache.ranger</groupId>
             <artifactId>ranger-authn</artifactId>
diff --git 
a/security-admin/src/main/java/org/apache/ranger/biz/RangerBizUtil.java 
b/security-admin/src/main/java/org/apache/ranger/biz/RangerBizUtil.java
index cb096f0d3..1a0037eb9 100644
--- a/security-admin/src/main/java/org/apache/ranger/biz/RangerBizUtil.java
+++ b/security-admin/src/main/java/org/apache/ranger/biz/RangerBizUtil.java
@@ -118,6 +118,7 @@ public class RangerBizUtil {
        public static final String AUDIT_STORE_RDBMS = "DB";
        public static final String AUDIT_STORE_SOLR           = "solr";
        public static final String  AUDIT_STORE_ELASTIC_SEARCH = 
"elasticSearch";
+       public static final String  AUDIT_STORE_OPENSEARCH     = "opensearch";
        public static final String  AUDIT_STORE_CLOUD_WATCH = "cloudwatch";
        public static final boolean BATCH_CLEAR_ENABLED = 
PropertiesUtil.getBooleanProperty("ranger.jpa.jdbc.batch-clear.enable", true);
        public static final int POLICY_BATCH_SIZE  = 
PropertiesUtil.getIntProperty("ranger.jpa.jdbc.batch-clear.size", 10);
diff --git a/security-admin/src/main/java/org/apache/ranger/biz/XAuditMgr.java 
b/security-admin/src/main/java/org/apache/ranger/biz/XAuditMgr.java
index 038402fd0..e421b8afe 100644
--- a/security-admin/src/main/java/org/apache/ranger/biz/XAuditMgr.java
+++ b/security-admin/src/main/java/org/apache/ranger/biz/XAuditMgr.java
@@ -26,6 +26,7 @@
 import org.apache.ranger.common.SearchCriteria;
 import org.apache.ranger.common.UserSessionBase;
 import org.apache.ranger.elasticsearch.ElasticSearchAccessAuditsService;
+import org.apache.ranger.opensearch.OpenSearchAccessAuditsService;
 import org.apache.ranger.solr.SolrAccessAuditsService;
 import org.apache.ranger.view.VXAccessAudit;
 import org.apache.ranger.view.VXAccessAuditList;
@@ -45,6 +46,9 @@ public class XAuditMgr extends XAuditMgrBase {
        @Autowired
        ElasticSearchAccessAuditsService elasticSearchAccessAuditsService;
 
+       @Autowired
+       OpenSearchAccessAuditsService openSearchAccessAuditsService;
+
        @Autowired
        CloudWatchAccessAuditsService cloudWatchAccessAuditsService;
 
@@ -123,6 +127,8 @@ public VXAccessAuditList searchXAccessAudits(SearchCriteria 
searchCriteria) {
                        return 
solrAccessAuditsService.searchXAccessAudits(searchCriteria);
                } else if 
(RangerBizUtil.AUDIT_STORE_ELASTIC_SEARCH.equalsIgnoreCase(auditDBType)) {
                        return 
elasticSearchAccessAuditsService.searchXAccessAudits(searchCriteria);
+               } else if 
(RangerBizUtil.AUDIT_STORE_OPENSEARCH.equalsIgnoreCase(auditDBType)) {
+                       return 
openSearchAccessAuditsService.searchXAccessAudits(searchCriteria);
                } else if 
(RangerBizUtil.AUDIT_STORE_CLOUD_WATCH.equalsIgnoreCase(auditDBType)) {
                        return 
cloudWatchAccessAuditsService.searchXAccessAudits(searchCriteria);
                } else {
@@ -137,6 +143,8 @@ public VXLong getXAccessAuditSearchCount(SearchCriteria 
searchCriteria) {
                        return 
solrAccessAuditsService.getXAccessAuditSearchCount(searchCriteria);
                } else if 
(RangerBizUtil.AUDIT_STORE_ELASTIC_SEARCH.equalsIgnoreCase(auditDBType)) {
                        return 
elasticSearchAccessAuditsService.getXAccessAuditSearchCount(searchCriteria);
+               } else if 
(RangerBizUtil.AUDIT_STORE_OPENSEARCH.equalsIgnoreCase(auditDBType)) {
+                       return 
openSearchAccessAuditsService.getXAccessAuditSearchCount(searchCriteria);
                } else if 
(RangerBizUtil.AUDIT_STORE_CLOUD_WATCH.equalsIgnoreCase(auditDBType)) {
                        return 
cloudWatchAccessAuditsService.getXAccessAuditSearchCount(searchCriteria);
                } else {
diff --git 
a/security-admin/src/main/java/org/apache/ranger/opensearch/OpenSearchAccessAuditsService.java
 
b/security-admin/src/main/java/org/apache/ranger/opensearch/OpenSearchAccessAuditsService.java
new file mode 100644
index 000000000..831e4d339
--- /dev/null
+++ 
b/security-admin/src/main/java/org/apache/ranger/opensearch/OpenSearchAccessAuditsService.java
@@ -0,0 +1,285 @@
+/*
+ * 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.opensearch;
+
+import org.apache.ranger.AccessAuditsService;
+import org.apache.ranger.audit.provider.MiscUtil;
+import org.apache.ranger.common.MessageEnums;
+import org.apache.ranger.common.PropertiesUtil;
+import org.apache.ranger.common.RESTErrorUtil;
+import org.apache.ranger.common.SearchCriteria;
+import org.apache.ranger.db.XXServiceDefDao;
+import org.apache.ranger.entity.XXService;
+import org.apache.ranger.entity.XXServiceDef;
+import org.apache.ranger.opensearch.OpenSearchUtil.OpenSearchSearchResult;
+import org.apache.ranger.view.VXAccessAudit;
+import org.apache.ranger.view.VXAccessAuditList;
+import org.apache.ranger.view.VXLong;
+import org.elasticsearch.client.RestClient;
+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.Service;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@Service
+@Scope("singleton")
+public class OpenSearchAccessAuditsService extends AccessAuditsService {
+    private static final Logger LOG = 
LoggerFactory.getLogger(OpenSearchAccessAuditsService.class);
+
+    @Autowired
+    OpenSearchMgr openSearchMgr;
+
+    @Autowired
+    OpenSearchUtil openSearchUtil;
+
+    public VXAccessAuditList searchXAccessAudits(SearchCriteria 
searchCriteria) {
+        RestClient    client              = openSearchMgr.getClient();
+        final boolean hiveQueryVisibility = 
PropertiesUtil.getBooleanProperty("ranger.audit.hive.query.visibility", true);
+
+        if (client == null) {
+            LOG.warn("OpenSearch client is null, so not running the query.");
+
+            throw restErrorUtil.createRESTException("Error connecting to 
OpenSearch", MessageEnums.ERROR_SYSTEM);
+        }
+
+        Map<String, Object> paramList = searchCriteria.getParamList();
+
+        updateUserExclusion(paramList);
+
+        OpenSearchSearchResult result;
+
+        try {
+            result = openSearchUtil.searchResources(searchCriteria, 
searchFields, sortFields, client, openSearchMgr.getIndex());
+        } catch (IOException e) {
+            LOG.warn("OpenSearch query failed: {}", e.getMessage());
+
+            throw restErrorUtil.createRESTException("Error querying 
OpenSearch", MessageEnums.ERROR_SYSTEM);
+        }
+
+        List<VXAccessAudit> xAccessAuditList = new ArrayList<>();
+
+        for (Map<String, Object> source : result.getSources()) {
+            VXAccessAudit vXAccessAudit = populateViewBean(source);
+            String        serviceType   = vXAccessAudit.getServiceType();
+            boolean       isHive        = "hive".equalsIgnoreCase(serviceType);
+
+            if (!hiveQueryVisibility && isHive) {
+                vXAccessAudit.setRequestData(null);
+            } else if (isHive) {
+                String accessType = vXAccessAudit.getAccessType();
+
+                if ("grant".equalsIgnoreCase(accessType) || 
"revoke".equalsIgnoreCase(accessType)) {
+                    String requestData = vXAccessAudit.getRequestData();
+
+                    if (requestData != null) {
+                        try {
+                            
vXAccessAudit.setRequestData(java.net.URLDecoder.decode(requestData, "UTF-8"));
+                        } catch (UnsupportedEncodingException e) {
+                            LOG.warn("Error while encoding request data: {}", 
requestData, e);
+                        }
+                    }
+                }
+            }
+
+            xAccessAuditList.add(vXAccessAudit);
+        }
+
+        VXAccessAuditList returnList = new VXAccessAuditList();
+
+        returnList.setPageSize(searchCriteria.getMaxRows());
+        returnList.setResultSize(result.getSources().size());
+        returnList.setTotalCount(result.getTotalHits());
+        returnList.setStartIndex(searchCriteria.getStartIndex());
+        returnList.setVXAccessAudits(xAccessAuditList);
+
+        return returnList;
+    }
+
+    public void setRestErrorUtil(RESTErrorUtil restErrorUtil) {
+        this.restErrorUtil = restErrorUtil;
+    }
+
+    public VXLong getXAccessAuditSearchCount(SearchCriteria searchCriteria) {
+        long   count  = 100;
+        VXLong vXLong = new VXLong();
+
+        vXLong.setValue(count);
+
+        return vXLong;
+    }
+
+    private VXAccessAudit populateViewBean(Map<String, Object> source) {
+        VXAccessAudit accessAudit = new VXAccessAudit();
+        Object        value;
+
+        value = source.get("id");
+        if (value != null) {
+            accessAudit.setId((long) value.hashCode());
+        }
+
+        value = source.get("cluster");
+        if (value != null) {
+            accessAudit.setClusterName(value.toString());
+        }
+
+        value = source.get("zoneName");
+        if (value != null) {
+            accessAudit.setZoneName(value.toString());
+        }
+
+        value = source.get("agentHost");
+        if (value != null) {
+            accessAudit.setAgentHost(value.toString());
+        }
+
+        value = source.get("policyVersion");
+        if (value != null) {
+            accessAudit.setPolicyVersion(MiscUtil.toLong(value));
+        }
+
+        value = source.get("access");
+        if (value != null) {
+            accessAudit.setAccessType(value.toString());
+        }
+
+        value = source.get("enforcer");
+        if (value != null) {
+            accessAudit.setAclEnforcer(value.toString());
+        }
+
+        value = source.get("agent");
+        if (value != null) {
+            accessAudit.setAgentId(value.toString());
+        }
+
+        value = source.get("repo");
+        if (value != null) {
+            accessAudit.setRepoName(value.toString());
+
+            XXService xxService = 
daoManager.getXXService().findByName(accessAudit.getRepoName());
+
+            if (xxService != null) {
+                accessAudit.setRepoDisplayName(xxService.getDisplayName());
+            }
+        }
+
+        value = source.get("sess");
+        if (value != null) {
+            accessAudit.setSessionId(value.toString());
+        }
+
+        value = source.get("reqUser");
+        if (value != null) {
+            accessAudit.setRequestUser(value.toString());
+        }
+
+        value = source.get("reqData");
+        if (value != null) {
+            accessAudit.setRequestData(value.toString());
+        }
+
+        value = source.get("resource");
+        if (value != null) {
+            accessAudit.setResourcePath(value.toString());
+        }
+
+        value = source.get("cliIP");
+        if (value != null) {
+            accessAudit.setClientIP(value.toString());
+        }
+
+        value = source.get("result");
+        if (value != null) {
+            accessAudit.setAccessResult(MiscUtil.toInt(value));
+        }
+
+        value = source.get("policy");
+        if (value != null) {
+            accessAudit.setPolicyId(MiscUtil.toLong(value));
+        }
+
+        value = source.get("repoType");
+        if (value != null) {
+            accessAudit.setRepoType(MiscUtil.toInt(value));
+
+            if (null != daoManager) {
+                XXServiceDefDao xxServiceDef = daoManager.getXXServiceDef();
+
+                if (xxServiceDef != null) {
+                    XXServiceDef xServiceDef = xxServiceDef.getById((long) 
accessAudit.getRepoType());
+
+                    if (xServiceDef != null) {
+                        accessAudit.setServiceType(xServiceDef.getName());
+                        
accessAudit.setServiceTypeDisplayName(xServiceDef.getDisplayName());
+                    }
+                }
+            }
+        }
+
+        value = source.get("resType");
+        if (value != null) {
+            accessAudit.setResourceType(value.toString());
+        }
+
+        value = source.get("reason");
+        if (value != null) {
+            accessAudit.setResultReason(value.toString());
+        }
+
+        value = source.get("action");
+        if (value != null) {
+            accessAudit.setAction(value.toString());
+        }
+
+        value = source.get("evtTime");
+        if (value != null) {
+            accessAudit.setEventTime(MiscUtil.toLocalDate(value));
+        }
+
+        value = source.get("seq_num");
+        if (value != null) {
+            accessAudit.setSequenceNumber(MiscUtil.toLong(value));
+        }
+
+        value = source.get("event_count");
+        if (value != null) {
+            accessAudit.setEventCount(MiscUtil.toLong(value));
+        }
+
+        value = source.get("event_dur_ms");
+        if (value != null) {
+            accessAudit.setEventDuration(MiscUtil.toLong(value));
+        }
+
+        value = source.get("tags");
+        if (value != null) {
+            accessAudit.setTags(value.toString());
+        }
+
+        return accessAudit;
+    }
+}
diff --git 
a/security-admin/src/main/java/org/apache/ranger/opensearch/OpenSearchMgr.java 
b/security-admin/src/main/java/org/apache/ranger/opensearch/OpenSearchMgr.java
new file mode 100644
index 000000000..f98f3085e
--- /dev/null
+++ 
b/security-admin/src/main/java/org/apache/ranger/opensearch/OpenSearchMgr.java
@@ -0,0 +1,193 @@
+/*
+ * 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.opensearch;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.HttpHost;
+import org.apache.http.auth.AuthSchemeProvider;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.config.AuthSchemes;
+import org.apache.http.config.Lookup;
+import org.apache.http.config.RegistryBuilder;
+import org.apache.http.impl.auth.SPNegoSchemeFactory;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.ranger.audit.destination.OpenSearchAuditDestination;
+import org.apache.ranger.authorization.credutils.CredentialsProviderUtil;
+import 
org.apache.ranger.authorization.credutils.kerberos.KerberosCredentialsProvider;
+import org.apache.ranger.common.PropertiesUtil;
+import org.elasticsearch.client.RestClient;
+import org.elasticsearch.client.RestClientBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.security.auth.Subject;
+import javax.security.auth.kerberos.KerberosTicket;
+
+import java.security.PrivilegedActionException;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Locale;
+
+@Component
+public class OpenSearchMgr {
+    private static final Logger LOG = 
LoggerFactory.getLogger(OpenSearchMgr.class);
+
+    private static final String CONFIG_PREFIX              = 
OpenSearchAuditDestination.CONFIG_PREFIX;
+    private static final String CONFIG_URLS                = 
OpenSearchAuditDestination.CONFIG_URLS;
+    private static final String CONFIG_PORT                = 
OpenSearchAuditDestination.CONFIG_PORT;
+    private static final String CONFIG_PROTOCOL            = 
OpenSearchAuditDestination.CONFIG_PROTOCOL;
+    private static final String CONFIG_USER                = 
OpenSearchAuditDestination.CONFIG_USER;
+    private static final String CONFIG_PASSWORD            = 
OpenSearchAuditDestination.CONFIG_PASSWORD;
+    private static final String CONFIG_INDEX               = 
OpenSearchAuditDestination.CONFIG_INDEX;
+    private static final String CONFIG_AUTH_TYPE           = 
OpenSearchAuditDestination.CONFIG_AUTH_TYPE;
+    private static final String CONFIG_KERBEROS_PRINCIPAL  = 
OpenSearchAuditDestination.CONFIG_KERBEROS_PRINCIPAL;
+    private static final String CONFIG_KERBEROS_KEYTAB     = 
OpenSearchAuditDestination.CONFIG_KERBEROS_KEYTAB;
+
+    private volatile RestClient client;
+    private Subject             subject;
+    private String              user;
+    private String              password;
+    private String              index;
+    private String              loginPrincipal;
+    private String              loginKeytab;
+
+    public RestClient getClient() {
+        RestClient me = client;
+
+        if (me != null && subject != null) {
+            KerberosTicket ticket = CredentialsProviderUtil.getTGT(subject);
+
+            try {
+                if (new Date().getTime() > ticket.getEndTime().getTime()) {
+                    client                                     = null;
+                    CredentialsProviderUtil.ticketExpireTime80 = 0;
+
+                    me = connect();
+                } else if (CredentialsProviderUtil.ticketWillExpire(ticket)) {
+                    subject = CredentialsProviderUtil.login(loginPrincipal, 
loginKeytab);
+                }
+            } catch (PrivilegedActionException e) {
+                LOG.error("PrivilegedActionException:", e);
+
+                throw new RuntimeException(e);
+            }
+
+            return me;
+        } else {
+            me = connect();
+        }
+
+        return me;
+    }
+
+    String getIndex() {
+        return index;
+    }
+
+    void setIndex(String index) {
+        this.index = index;
+    }
+
+    synchronized RestClient connect() {
+        RestClient me = client;
+
+        if (me == null) {
+            String urls     = PropertiesUtil.getProperty(CONFIG_PREFIX + "." + 
CONFIG_URLS);
+            String protocol = PropertiesUtil.getProperty(CONFIG_PREFIX + "." + 
CONFIG_PROTOCOL, "http");
+
+            user     = PropertiesUtil.getProperty(CONFIG_PREFIX + "." + 
CONFIG_USER, "");
+            password = PropertiesUtil.getProperty(CONFIG_PREFIX + "." + 
CONFIG_PASSWORD, "");
+
+            String authType          = 
PropertiesUtil.getProperty(CONFIG_PREFIX + "." + CONFIG_AUTH_TYPE, "");
+            String kerberosPrincipal = 
PropertiesUtil.getProperty(CONFIG_PREFIX + "." + CONFIG_KERBEROS_PRINCIPAL, "");
+            String kerberosKeytab    = 
PropertiesUtil.getProperty(CONFIG_PREFIX + "." + CONFIG_KERBEROS_KEYTAB, "");
+
+            int port = PropertiesUtil.getIntProperty(CONFIG_PREFIX + "." + 
CONFIG_PORT, 9200);
+
+            this.index = PropertiesUtil.getProperty(CONFIG_PREFIX + "." + 
CONFIG_INDEX, "ranger_audits");
+
+            String parameterString = String.format(Locale.ROOT, "User:%s, 
%s://%s:%s/%s", user, protocol, urls, port, index);
+
+            LOG.info("Initializing OpenSearch connection: {}", 
parameterString);
+
+            if (urls != null) {
+                urls = urls.trim();
+            }
+
+            if (StringUtils.isBlank(urls) || "NONE".equalsIgnoreCase(urls)) {
+                LOG.warn("OpenSearch URLs not configured or set to NONE");
+
+                return null;
+            }
+
+            try {
+                if 
(OpenSearchAuditDestination.AUTH_TYPE_KERBEROS.equals(OpenSearchAuditDestination.resolveAuthType(authType,
 user, password))) {
+                    loginPrincipal = 
OpenSearchAuditDestination.isConfigured(kerberosPrincipal) ? kerberosPrincipal 
: user;
+                    loginKeytab    = 
OpenSearchAuditDestination.isConfigured(kerberosKeytab) ? kerberosKeytab : 
password;
+                    subject        = 
CredentialsProviderUtil.login(loginPrincipal, loginKeytab);
+                }
+
+                RestClientBuilder builder = buildRestClientBuilder(urls, 
protocol, user, password, port, authType, kerberosPrincipal, kerberosKeytab);
+
+                client = builder.build();
+                me     = client;
+            } catch (Throwable t) {
+                LOG.error("Cannot connect to OpenSearch: {}", parameterString, 
t);
+            }
+        }
+
+        return me;
+    }
+
+    public static RestClientBuilder buildRestClientBuilder(String urls, String 
protocol, String user, String password, int port) {
+        return buildRestClientBuilder(urls, protocol, user, password, port, 
"", "", "");
+    }
+
+    public static RestClientBuilder buildRestClientBuilder(String urls, String 
protocol, String user, String password, int port, String authType, String 
kerberosPrincipal, String kerberosKeytab) {
+        HttpHost[] hosts = 
Arrays.stream(urls.split(",")).map(String::trim).filter(h -> 
!h.isEmpty()).map(h -> new HttpHost(h, port, 
protocol)).toArray(HttpHost[]::new);
+
+        RestClientBuilder builder = RestClient.builder(hosts);
+
+        String resolvedAuthType = 
OpenSearchAuditDestination.resolveAuthType(authType, user, password);
+
+        if 
(OpenSearchAuditDestination.AUTH_TYPE_KERBEROS.equals(resolvedAuthType)) {
+            String principal = 
OpenSearchAuditDestination.isConfigured(kerberosPrincipal) ? kerberosPrincipal 
: user;
+            String keytab    = 
OpenSearchAuditDestination.isConfigured(kerberosKeytab) ? kerberosKeytab : 
password;
+
+            KerberosCredentialsProvider credentialsProvider = 
CredentialsProviderUtil.getKerberosCredentials(principal, keytab);
+            Lookup<AuthSchemeProvider> authRegistry = 
RegistryBuilder.<AuthSchemeProvider>create().register(AuthSchemes.SPNEGO, new 
SPNegoSchemeFactory()).build();
+
+            builder.setHttpClientConfigCallback(httpClientBuilder -> {
+                
httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
+                httpClientBuilder.setDefaultAuthSchemeRegistry(authRegistry);
+                return httpClientBuilder;
+            });
+        } else if 
(OpenSearchAuditDestination.AUTH_TYPE_BASIC.equals(resolvedAuthType)) {
+            CredentialsProvider credentialsProvider = new 
BasicCredentialsProvider();
+            credentialsProvider.setCredentials(AuthScope.ANY, new 
UsernamePasswordCredentials(user, password));
+            builder.setHttpClientConfigCallback(httpClientBuilder -> 
httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider));
+        }
+
+        return builder;
+    }
+}
diff --git 
a/security-admin/src/main/java/org/apache/ranger/opensearch/OpenSearchUtil.java 
b/security-admin/src/main/java/org/apache/ranger/opensearch/OpenSearchUtil.java
new file mode 100644
index 000000000..164483d74
--- /dev/null
+++ 
b/security-admin/src/main/java/org/apache/ranger/opensearch/OpenSearchUtil.java
@@ -0,0 +1,362 @@
+/*
+ * 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.opensearch;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.http.entity.ContentType;
+import org.apache.http.nio.entity.NStringEntity;
+import org.apache.http.util.EntityUtils;
+import org.apache.ranger.common.PropertiesUtil;
+import org.apache.ranger.common.SearchCriteria;
+import org.apache.ranger.common.SearchField;
+import org.apache.ranger.common.SortField;
+import org.apache.ranger.common.StringUtil;
+import org.elasticsearch.client.Request;
+import org.elasticsearch.client.Response;
+import org.elasticsearch.client.RestClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+import java.util.stream.Collectors;
+
+@Component
+public class OpenSearchUtil {
+    private static final Logger LOG = 
LoggerFactory.getLogger(OpenSearchUtil.class);
+
+    private static final ObjectMapper MAPPER               = new 
ObjectMapper();
+    private static final String       LUCENE_SPECIAL_CHARS = 
"+-=&|><!(){}[]^\"~*?:\\/";
+    private static final String       DATE_FORMAT_STR      = 
"yyyy-MM-dd'T'HH:mm:ss'Z'";
+
+    private static final ThreadLocal<DateFormat> DATE_FORMAT = 
ThreadLocal.withInitial(() -> {
+        SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT_STR);
+        String           timeZone   = 
PropertiesUtil.getProperty("ranger.audit.opensearch.timezone");
+
+        if (timeZone != null) {
+            LOG.info("Setting timezone to {}", timeZone);
+
+            try {
+                dateFormat.setTimeZone(TimeZone.getTimeZone(timeZone));
+            } catch (Throwable t) {
+                LOG.error("Error setting timezone. TimeZone = {}", timeZone);
+            }
+        }
+
+        return dateFormat;
+    });
+
+    @Autowired
+    StringUtil stringUtil;
+
+    public OpenSearchUtil() {
+    }
+
+    public OpenSearchSearchResult searchResources(SearchCriteria 
searchCriteria, List<SearchField> searchFields, List<SortField> sortFields, 
RestClient client, String index) throws IOException {
+        String body = buildSearchBody(searchCriteria, searchFields, 
sortFields);
+
+        LOG.debug("OpenSearch query on index [{}]: {}", index, body);
+
+        Request request = new Request("POST", "/" + index + "/_search");
+
+        request.setEntity(new NStringEntity(body, 
ContentType.APPLICATION_JSON));
+
+        Response                  response  = client.performRequest(request);
+        String                    json      = 
EntityUtils.toString(response.getEntity());
+        JsonNode                  root      = MAPPER.readTree(json);
+        long                      totalHits = 
root.at("/hits/total/value").asLong(0);
+        JsonNode                  hitsArray = root.at("/hits/hits");
+        List<Map<String, Object>> sources   = new ArrayList<>();
+
+        if (hitsArray.isArray()) {
+            for (JsonNode hit : hitsArray) {
+                JsonNode sourceNode = hit.get("_source");
+
+                if (sourceNode != null) {
+                    @SuppressWarnings("unchecked")
+                    Map<String, Object> source = 
MAPPER.convertValue(sourceNode, Map.class);
+
+                    sources.add(source);
+                }
+            }
+        }
+
+        return new OpenSearchSearchResult(totalHits, sources);
+    }
+
+    public List<Map<String, Object>> fetchByIds(RestClient client, String 
index, List<String> ids) throws IOException {
+        if (ids == null || ids.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        Map<String, Object> mgetBody = new HashMap<>();
+
+        mgetBody.put("ids", ids);
+
+        String   body   = MAPPER.writeValueAsString(mgetBody);
+        Request request = new Request("POST", "/" + index + "/_mget");
+
+        request.setEntity(new NStringEntity(body, 
ContentType.APPLICATION_JSON));
+
+        Response                  response = client.performRequest(request);
+        String                    json     = 
EntityUtils.toString(response.getEntity());
+        JsonNode                  root     = MAPPER.readTree(json);
+        JsonNode                  docsNode = root.get("docs");
+        List<Map<String, Object>> results  = new ArrayList<>();
+
+        if (docsNode != null && docsNode.isArray()) {
+            for (JsonNode doc : docsNode) {
+                if (doc.has("found") && doc.get("found").asBoolean()) {
+                    JsonNode sourceNode = doc.get("_source");
+
+                    if (sourceNode != null) {
+                        @SuppressWarnings("unchecked")
+                        Map<String, Object> source = 
MAPPER.convertValue(sourceNode, Map.class);
+
+                        results.add(source);
+                    }
+                }
+            }
+        }
+
+        return results;
+    }
+
+    String buildSearchBody(SearchCriteria searchCriteria, List<SearchField> 
searchFields, List<SortField> sortFields) {
+        List<Map<String, Object>> mustClauses  = new ArrayList<>();
+        Date                     fromDate      = null;
+        Date                     toDate        = null;
+        String                   dateFieldName = null;
+
+        if (searchCriteria.getParamList() != null) {
+            for (SearchField field : searchFields) {
+                String clientFieldName = field.getClientFieldName();
+                Object paramValue      = 
searchCriteria.getParamValue(clientFieldName);
+
+                if (paramValue == null || paramValue.toString().isEmpty()) {
+                    continue;
+                }
+
+                String                  fieldName  = field.getFieldName();
+                SearchField.DATA_TYPE   dataType   = field.getDataType();
+                SearchField.SEARCH_TYPE searchType = field.getSearchType();
+
+                if (dataType == SearchField.DATA_TYPE.DATE) {
+                    if (paramValue instanceof Date) {
+                        if (searchType == 
SearchField.SEARCH_TYPE.GREATER_EQUAL_THAN || searchType == 
SearchField.SEARCH_TYPE.GREATER_THAN) {
+                            fromDate      = (Date) paramValue;
+                            dateFieldName = fieldName;
+                        } else if (searchType == 
SearchField.SEARCH_TYPE.LESS_EQUAL_THAN || searchType == 
SearchField.SEARCH_TYPE.LESS_THAN) {
+                            toDate        = (Date) paramValue;
+                            dateFieldName = fieldName;
+                        }
+                    }
+
+                    continue;
+                }
+
+                Map<String, Object> clause = buildClause(fieldName, dataType, 
searchType, paramValue);
+
+                if (clause != null) {
+                    mustClauses.add(clause);
+                }
+            }
+
+            if (fromDate != null || toDate != null) {
+                mustClauses.add(buildDateRange(dateFieldName, fromDate, 
toDate));
+            }
+        }
+
+        Map<String, Object> query = new LinkedHashMap<>();
+        Map<String, Object> bool  = new HashMap<>();
+
+        if (mustClauses.isEmpty()) {
+            Map<String, Object> matchAll = new HashMap<>();
+            matchAll.put("match_all", new HashMap<>());
+            bool.put("must", Collections.singletonList(matchAll));
+        } else {
+            bool.put("must", mustClauses);
+        }
+        query.put("query", Collections.singletonMap("bool", bool));
+        query.put("from", searchCriteria.getStartIndex());
+        query.put("size", searchCriteria.getMaxRows());
+
+        String[] sortResolved = resolveSortField(searchCriteria, sortFields);
+
+        if (sortResolved != null) {
+            Map<String, Object> orderMap = new HashMap<>();
+            orderMap.put("order", sortResolved[1]);
+            Map<String, Object> sortEntry = new HashMap<>();
+            sortEntry.put(sortResolved[0], orderMap);
+            query.put("sort", Collections.singletonList(sortEntry));
+        }
+
+        try {
+            return MAPPER.writeValueAsString(query);
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to serialize OpenSearch query", 
e);
+        }
+    }
+
+    private Map<String, Object> buildClause(String fieldName, 
SearchField.DATA_TYPE dataType, SearchField.SEARCH_TYPE searchType, Object 
paramValue) {
+        if (fieldName.startsWith("-")) {
+            Map<String, Object> inner = buildClause(fieldName.substring(1), 
dataType, searchType, paramValue);
+
+            if (inner != null) {
+                Map<String, Object> mustNotBool = new HashMap<>();
+                mustNotBool.put("must_not", Collections.singletonList(inner));
+                Map<String, Object> boolClause = new HashMap<>();
+                boolClause.put("bool", mustNotBool);
+                return boolClause;
+            }
+        } else if (paramValue instanceof Collection) {
+            Collection<?> valueList = (Collection<?>) paramValue;
+
+            if (!valueList.isEmpty()) {
+                String queryString = valueList.stream().map(v -> "(" + 
escapeLucene(v.toString().trim().toLowerCase()) + 
")").collect(Collectors.joining(" OR "));
+
+                Map<String, Object> queryStringBody = new HashMap<>();
+                queryStringBody.put("query", queryString);
+                queryStringBody.put("default_field", fieldName);
+                return Collections.singletonMap("query_string", 
queryStringBody);
+            }
+        } else if (searchType == SearchField.SEARCH_TYPE.PARTIAL) {
+            String value = paramValue.toString().trim();
+
+            if (!value.isEmpty()) {
+                Map<String, Object> partialQuery = new HashMap<>();
+                partialQuery.put("query", "*" + 
escapeLucene(value.toLowerCase()) + "*");
+                partialQuery.put("default_field", fieldName);
+                return Collections.singletonMap("query_string", partialQuery);
+            }
+        } else {
+            String value = paramValue.toString().trim();
+
+            if (!value.isEmpty()) {
+                return Collections.singletonMap("match_phrase", 
Collections.singletonMap(fieldName, escapeLucene(value.toLowerCase())));
+            }
+        }
+
+        return null;
+    }
+
+    private Map<String, Object> buildDateRange(String fieldName, Date 
fromDate, Date toDate) {
+        Map<String, Object> rangeParams = new LinkedHashMap<>();
+
+        rangeParams.put("format", DATE_FORMAT_STR);
+
+        if (fromDate != null) {
+            rangeParams.put("gte", DATE_FORMAT.get().format(fromDate));
+        }
+
+        if (toDate != null) {
+            rangeParams.put("lte", DATE_FORMAT.get().format(toDate));
+        }
+
+        return Collections.singletonMap("range", 
Collections.singletonMap(fieldName, rangeParams));
+    }
+
+    private String[] resolveSortField(SearchCriteria searchCriteria, 
List<SortField> sortFields) {
+        String sortBy      = searchCriteria.getSortBy();
+        String querySortBy = null;
+
+        if (sortBy != null && !sortBy.trim().isEmpty()) {
+            sortBy = sortBy.trim();
+
+            for (SortField sortField : sortFields) {
+                if (sortBy.equalsIgnoreCase(sortField.getParamName())) {
+                    querySortBy = sortField.getFieldName();
+
+                    searchCriteria.setSortBy(sortField.getParamName());
+
+                    break;
+                }
+            }
+        }
+
+        if (querySortBy == null) {
+            for (SortField sortField : sortFields) {
+                if (sortField.isDefault()) {
+                    querySortBy = sortField.getFieldName();
+
+                    searchCriteria.setSortBy(sortField.getParamName());
+                    
searchCriteria.setSortType(sortField.getDefaultOrder().name());
+
+                    break;
+                }
+            }
+        }
+
+        if (querySortBy != null) {
+            String order = 
"desc".equalsIgnoreCase(searchCriteria.getSortType()) ? "desc" : "asc";
+
+            return new String[] {querySortBy, order};
+        }
+
+        return null;
+    }
+
+    static String escapeLucene(String value) {
+        StringBuilder sb = new StringBuilder();
+
+        for (int i = 0; i < value.length(); i++) {
+            char c = value.charAt(i);
+
+            if (LUCENE_SPECIAL_CHARS.indexOf(c) >= 0) {
+                sb.append('\\');
+            }
+
+            sb.append(c);
+        }
+
+        return sb.toString();
+    }
+
+    public static class OpenSearchSearchResult {
+        private final long                      totalHits;
+        private final List<Map<String, Object>> sources;
+
+        public OpenSearchSearchResult(long totalHits, List<Map<String, 
Object>> sources) {
+            this.totalHits = totalHits;
+            this.sources   = sources;
+        }
+
+        public long getTotalHits() {
+            return totalHits;
+        }
+
+        public List<Map<String, Object>> getSources() {
+            return sources;
+        }
+    }
+}
diff --git 
a/security-admin/src/test/java/org/apache/ranger/opensearch/OpenSearchAccessAuditsServiceTest.java
 
b/security-admin/src/test/java/org/apache/ranger/opensearch/OpenSearchAccessAuditsServiceTest.java
new file mode 100644
index 000000000..12975f390
--- /dev/null
+++ 
b/security-admin/src/test/java/org/apache/ranger/opensearch/OpenSearchAccessAuditsServiceTest.java
@@ -0,0 +1,185 @@
+/*
+ * 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.opensearch;
+
+import org.apache.ranger.common.MessageEnums;
+import org.apache.ranger.common.RESTErrorUtil;
+import org.apache.ranger.common.SearchCriteria;
+import org.apache.ranger.db.RangerDaoManager;
+import org.apache.ranger.db.XXServiceDao;
+import org.apache.ranger.db.XXServiceDefDao;
+import org.apache.ranger.opensearch.OpenSearchUtil.OpenSearchSearchResult;
+import org.apache.ranger.view.VXAccessAuditList;
+import org.elasticsearch.client.RestClient;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import javax.ws.rs.WebApplicationException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class OpenSearchAccessAuditsServiceTest {
+    @InjectMocks
+    OpenSearchAccessAuditsService service;
+
+    @Mock
+    OpenSearchMgr openSearchMgr;
+
+    @Mock
+    OpenSearchUtil openSearchUtil;
+
+    @Mock
+    RESTErrorUtil restErrorUtil;
+
+    @Mock
+    RangerDaoManager daoManager;
+
+    @Mock
+    XXServiceDao xxServiceDao;
+
+    @Mock
+    XXServiceDefDao xxServiceDefDao;
+
+    @Mock
+    RestClient restClient;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        service.setRestErrorUtil(restErrorUtil);
+
+        java.lang.reflect.Field daoField = 
org.apache.ranger.AccessAuditsService.class.getDeclaredField("daoManager");
+
+        daoField.setAccessible(true);
+        daoField.set(service, daoManager);
+    }
+
+    @Test
+    void searchXAccessAudits_clientNull_throwsException() {
+        when(openSearchMgr.getClient()).thenReturn(null);
+        when(restErrorUtil.createRESTException(anyString(), 
any(MessageEnums.class)))
+                .thenReturn(new WebApplicationException(500));
+
+        SearchCriteria criteria = new SearchCriteria();
+
+        assertThrows(WebApplicationException.class, () -> 
service.searchXAccessAudits(criteria));
+    }
+
+    @Test
+    void searchXAccessAudits_ioException_throwsException() throws Exception {
+        when(openSearchMgr.getClient()).thenReturn(restClient);
+        when(openSearchMgr.getIndex()).thenReturn("ranger_audits");
+        when(openSearchUtil.searchResources(any(), any(), any(), 
eq(restClient), eq("ranger_audits")))
+                .thenThrow(new IOException("Connection refused"));
+        when(restErrorUtil.createRESTException(anyString(), 
any(MessageEnums.class)))
+                .thenReturn(new WebApplicationException(500));
+
+        SearchCriteria criteria = new SearchCriteria();
+
+        assertThrows(WebApplicationException.class, () -> 
service.searchXAccessAudits(criteria));
+    }
+
+    @Test
+    void searchXAccessAudits_success() throws Exception {
+        when(openSearchMgr.getClient()).thenReturn(restClient);
+        when(openSearchMgr.getIndex()).thenReturn("ranger_audits");
+
+        Map<String, Object> doc1 = new HashMap<>();
+
+        doc1.put("id", "test-id-001");
+        doc1.put("reqUser", "testuser1");
+        doc1.put("resource", "/tmp/test");
+        doc1.put("access", "read");
+        doc1.put("result", 1);
+        doc1.put("repo", "dev_hdfs");
+        doc1.put("repoType", 1);
+        doc1.put("action", "read");
+
+        List<Map<String, Object>> sources = new ArrayList<>();
+
+        sources.add(doc1);
+
+        OpenSearchSearchResult mockResult = new OpenSearchSearchResult(1, 
sources);
+
+        when(openSearchUtil.searchResources(any(), any(), any(), 
eq(restClient), anyString()))
+                .thenReturn(mockResult);
+        when(daoManager.getXXService()).thenReturn(xxServiceDao);
+        when(daoManager.getXXServiceDef()).thenReturn(xxServiceDefDao);
+
+        SearchCriteria criteria = new SearchCriteria();
+
+        criteria.setMaxRows(25);
+        criteria.setStartIndex(0);
+
+        VXAccessAuditList result = service.searchXAccessAudits(criteria);
+
+        assertNotNull(result);
+        assertEquals(1, result.getTotalCount());
+        assertEquals(1, result.getResultSize());
+        assertNotNull(result.getVXAccessAudits());
+        assertEquals(1, result.getVXAccessAudits().size());
+        assertEquals("testuser1", 
result.getVXAccessAudits().get(0).getRequestUser());
+        assertEquals("/tmp/test", 
result.getVXAccessAudits().get(0).getResourcePath());
+        assertEquals("read", 
result.getVXAccessAudits().get(0).getAccessType());
+    }
+
+    @Test
+    void searchXAccessAudits_emptyResults() throws Exception {
+        when(openSearchMgr.getClient()).thenReturn(restClient);
+        when(openSearchMgr.getIndex()).thenReturn("ranger_audits");
+
+        OpenSearchSearchResult mockResult = new OpenSearchSearchResult(0, new 
ArrayList<>());
+
+        when(openSearchUtil.searchResources(any(), any(), any(), 
eq(restClient), anyString()))
+                .thenReturn(mockResult);
+
+        SearchCriteria criteria = new SearchCriteria();
+
+        criteria.setMaxRows(25);
+        criteria.setStartIndex(0);
+
+        VXAccessAuditList result = service.searchXAccessAudits(criteria);
+
+        assertNotNull(result);
+        assertEquals(0, result.getTotalCount());
+        assertEquals(0, result.getResultSize());
+        assertTrue(result.getVXAccessAudits().isEmpty());
+    }
+
+    private static void assertTrue(boolean condition) {
+        org.junit.jupiter.api.Assertions.assertTrue(condition);
+    }
+}
diff --git 
a/security-admin/src/test/java/org/apache/ranger/opensearch/OpenSearchMgrTest.java
 
b/security-admin/src/test/java/org/apache/ranger/opensearch/OpenSearchMgrTest.java
new file mode 100644
index 000000000..61731da28
--- /dev/null
+++ 
b/security-admin/src/test/java/org/apache/ranger/opensearch/OpenSearchMgrTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.opensearch;
+
+import org.elasticsearch.client.RestClientBuilder;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+class OpenSearchMgrTest {
+    @Test
+    void buildRestClientBuilder_basicAuth() {
+        RestClientBuilder builder = 
OpenSearchMgr.buildRestClientBuilder("localhost", "http", "admin", "password", 
9200);
+
+        assertNotNull(builder);
+    }
+
+    @Test
+    void buildRestClientBuilder_noAuth() {
+        RestClientBuilder builder = 
OpenSearchMgr.buildRestClientBuilder("localhost", "http", "", "", 9200);
+
+        assertNotNull(builder);
+    }
+
+    @Test
+    void buildRestClientBuilder_noneCredentials() {
+        RestClientBuilder builder = 
OpenSearchMgr.buildRestClientBuilder("localhost", "https", "NONE", "NONE", 
9200);
+
+        assertNotNull(builder);
+    }
+
+    @Test
+    void buildRestClientBuilder_multipleHosts() {
+        RestClientBuilder builder = 
OpenSearchMgr.buildRestClientBuilder("host1,host2,host3", "http", "user", 
"pass", 9200);
+
+        assertNotNull(builder);
+    }
+}
diff --git 
a/security-admin/src/test/java/org/apache/ranger/opensearch/OpenSearchUtilTest.java
 
b/security-admin/src/test/java/org/apache/ranger/opensearch/OpenSearchUtilTest.java
new file mode 100644
index 000000000..33ae7bfa5
--- /dev/null
+++ 
b/security-admin/src/test/java/org/apache/ranger/opensearch/OpenSearchUtilTest.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.opensearch;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.ranger.common.SearchCriteria;
+import org.apache.ranger.common.SearchField;
+import org.apache.ranger.common.SortField;
+import org.apache.ranger.common.StringUtil;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
+
+class OpenSearchUtilTest {
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    @InjectMocks
+    OpenSearchUtil openSearchUtil;
+
+    @Mock
+    StringUtil stringUtil;
+
+    @BeforeEach
+    void setUp() {
+        MockitoAnnotations.openMocks(this);
+
+        when(stringUtil.isEmpty(anyString())).thenAnswer(inv -> {
+            String s = inv.getArgument(0);
+            return s == null || s.trim().isEmpty();
+        });
+    }
+
+    @Test
+    void buildSearchBody_emptyParams() throws Exception {
+        SearchCriteria criteria = new SearchCriteria();
+
+        criteria.setMaxRows(25);
+        criteria.setStartIndex(0);
+
+        List<SearchField> searchFields = new ArrayList<>();
+        List<SortField>   sortFields   = Arrays.asList(new 
SortField("eventTime", "evtTime", true, SortField.SORT_ORDER.DESC));
+        String            body         = 
openSearchUtil.buildSearchBody(criteria, searchFields, sortFields);
+        JsonNode          root         = MAPPER.readTree(body);
+
+        assertNotNull(root.get("query"));
+        assertEquals(0, root.get("from").asInt());
+        assertEquals(25, root.get("size").asInt());
+    }
+
+    @Test
+    void buildSearchBody_partialStringSearch() throws Exception {
+        SearchCriteria criteria = new SearchCriteria();
+
+        criteria.setMaxRows(10);
+        criteria.setStartIndex(0);
+        criteria.addParam("requestUser", "testuser");
+
+        List<SearchField> searchFields = Arrays.asList(new 
SearchField("requestUser", "reqUser", SearchField.DATA_TYPE.STRING, 
SearchField.SEARCH_TYPE.PARTIAL));
+        List<SortField>   sortFields   = Arrays.asList(new 
SortField("eventTime", "evtTime", true, SortField.SORT_ORDER.DESC));
+        String            body         = 
openSearchUtil.buildSearchBody(criteria, searchFields, sortFields);
+        JsonNode          root         = MAPPER.readTree(body);
+        JsonNode          mustClauses  = root.at("/query/bool/must");
+        String            bodyStr      = body.toLowerCase();
+
+        assertTrue(mustClauses.isArray());
+        assertTrue(mustClauses.size() > 0);
+
+        assertTrue(bodyStr.contains("testuser"));
+        assertTrue(bodyStr.contains("query_string"));
+    }
+
+    @Test
+    void buildSearchBody_fullStringMatch() throws Exception {
+        SearchCriteria criteria = new SearchCriteria();
+
+        criteria.setMaxRows(10);
+        criteria.setStartIndex(0);
+        criteria.addParam("accessType", "read");
+
+        List<SearchField> searchFields = Arrays.asList(new 
SearchField("accessType", "access", SearchField.DATA_TYPE.STRING, 
SearchField.SEARCH_TYPE.FULL));
+        List<SortField>   sortFields   = Arrays.asList(new 
SortField("eventTime", "evtTime", true, SortField.SORT_ORDER.DESC));
+        String            body         = 
openSearchUtil.buildSearchBody(criteria, searchFields, sortFields);
+
+        assertTrue(body.contains("match_phrase"));
+        assertTrue(body.contains("access"));
+        assertTrue(body.contains("read"));
+    }
+
+    @Test
+    void buildSearchBody_dateRange() throws Exception {
+        SearchCriteria criteria = new SearchCriteria();
+
+        criteria.setMaxRows(10);
+        criteria.setStartIndex(0);
+        criteria.addParam("startDate", new Date(1700000000000L));
+        criteria.addParam("endDate", new Date(1700100000000L));
+
+        List<SearchField> searchFields = Arrays.asList(
+                new SearchField("startDate", "evtTime", 
SearchField.DATA_TYPE.DATE, SearchField.SEARCH_TYPE.GREATER_EQUAL_THAN),
+                new SearchField("endDate", "evtTime", 
SearchField.DATA_TYPE.DATE, SearchField.SEARCH_TYPE.LESS_EQUAL_THAN));
+        List<SortField> sortFields = Arrays.asList(new SortField("eventTime", 
"evtTime", true, SortField.SORT_ORDER.DESC));
+
+        String body = openSearchUtil.buildSearchBody(criteria, searchFields, 
sortFields);
+
+        assertTrue(body.contains("range"));
+        assertTrue(body.contains("evtTime"));
+        assertTrue(body.contains("gte"));
+        assertTrue(body.contains("lte"));
+    }
+
+    @Test
+    void buildSearchBody_collectionOrQuery() throws Exception {
+        SearchCriteria criteria = new SearchCriteria();
+
+        criteria.setMaxRows(10);
+        criteria.setStartIndex(0);
+        criteria.addParam("requestUser", Arrays.asList("user1", "user2", 
"user3"));
+
+        List<SearchField> searchFields = Arrays.asList(new 
SearchField("requestUser", "reqUser", SearchField.DATA_TYPE.STR_LIST, 
SearchField.SEARCH_TYPE.FULL));
+        List<SortField>   sortFields   = Arrays.asList(new 
SortField("eventTime", "evtTime", true, SortField.SORT_ORDER.DESC));
+        String            body         = 
openSearchUtil.buildSearchBody(criteria, searchFields, sortFields);
+
+        assertTrue(body.contains("query_string"));
+        assertTrue(body.contains("OR"));
+        assertTrue(body.contains("user1"));
+        assertTrue(body.contains("user2"));
+        assertTrue(body.contains("user3"));
+    }
+
+    @Test
+    void buildSearchBody_negation() throws Exception {
+        SearchCriteria criteria = new SearchCriteria();
+
+        criteria.setMaxRows(10);
+        criteria.setStartIndex(0);
+        criteria.addParam("excludeUser", "serviceuser");
+
+        List<SearchField> searchFields = Arrays.asList(new 
SearchField("excludeUser", "-reqUser", SearchField.DATA_TYPE.STRING, 
SearchField.SEARCH_TYPE.FULL));
+        List<SortField>   sortFields   = Arrays.asList(new 
SortField("eventTime", "evtTime", true, SortField.SORT_ORDER.DESC));
+        String            body         = 
openSearchUtil.buildSearchBody(criteria, searchFields, sortFields);
+
+        assertTrue(body.contains("must_not"));
+        assertTrue(body.contains("reqUser"));
+    }
+
+    @Test
+    void buildSearchBody_sorting() throws Exception {
+        SearchCriteria criteria = new SearchCriteria();
+
+        criteria.setMaxRows(10);
+        criteria.setStartIndex(5);
+        criteria.setSortBy("eventTime");
+        criteria.setSortType("asc");
+
+        List<SearchField> searchFields = new ArrayList<>();
+        List<SortField>   sortFields   = Arrays.asList(new 
SortField("eventTime", "evtTime", true, SortField.SORT_ORDER.DESC));
+        String            body         = 
openSearchUtil.buildSearchBody(criteria, searchFields, sortFields);
+        JsonNode          root         = MAPPER.readTree(body);
+        JsonNode          sortNode     = root.get("sort");
+
+        assertEquals(5, root.get("from").asInt());
+        assertEquals(10, root.get("size").asInt());
+
+        assertNotNull(sortNode);
+        assertTrue(sortNode.isArray());
+        assertTrue(sortNode.get(0).has("evtTime"));
+        assertEquals("asc", sortNode.get(0).at("/evtTime/order").asText());
+    }
+
+    @Test
+    void escapeLucene_specialCharacters() {
+        String input   = "test+value:with*special?chars";
+        String escaped = OpenSearchUtil.escapeLucene(input);
+
+        assertTrue(escaped.contains("\\+"));
+        assertTrue(escaped.contains("\\:"));
+        assertTrue(escaped.contains("\\*"));
+        assertTrue(escaped.contains("\\?"));
+    }
+
+    @Test
+    void escapeLucene_noSpecialChars() {
+        String input   = "simplevalue";
+        String escaped = OpenSearchUtil.escapeLucene(input);
+
+        assertEquals("simplevalue", escaped);
+    }
+}

Reply via email to