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);
+ }
+}