This is an automated email from the ASF dual-hosted git repository.
pradeepagrawal8184 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ranger.git
The following commit(s) were added to refs/heads/master by this push:
new 18880728e RANGER-5823: Adding public api test cases of policy and repo
creation in docker based framework (#1287)
18880728e is described below
commit 18880728e7e8ed0630650658f774e28d684172d2
Author: Vishal Bhavsar <[email protected]>
AuthorDate: Wed Oct 7 18:20:48 2026 +0530
RANGER-5823: Adding public api test cases of policy and repo creation in
docker based framework (#1287)
---
functional-tests/.gitignore | 8 +
.../test_public_hive_services_and_policies_api.py | 395 +++++++++++++++++++++
functional-tests/pytest.ini | 10 +-
functional-tests/ranger_test_utils/utils.py | 358 +++++++++++++++++++
functional-tests/readme.md | 9 +-
functional-tests/run-tests.sh | 5 +-
6 files changed, 779 insertions(+), 6 deletions(-)
diff --git a/functional-tests/.gitignore b/functional-tests/.gitignore
new file mode 100644
index 000000000..606df5c16
--- /dev/null
+++ b/functional-tests/.gitignore
@@ -0,0 +1,8 @@
+# Local Python / pytest (do not commit)
+__pycache__/
+
+# Virtualenv created by run-tests.sh
+myenv/
+
+# Pytest cache
+.pytest_cache/
diff --git a/functional-tests/api/test_public_hive_services_and_policies_api.py
b/functional-tests/api/test_public_hive_services_and_policies_api.py
new file mode 100644
index 000000000..fd5ed008b
--- /dev/null
+++ b/functional-tests/api/test_public_hive_services_and_policies_api.py
@@ -0,0 +1,395 @@
+# 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.
+
+import pytest
+
+from ranger_test_utils.utils import (
+ POLICY_URL,
+ SERVICE_URL,
+ assert_http_ok,
+ build_hive_policy_payload,
+ build_hive_service_payload,
+ configure_test_logging,
+ create_ranger_admin_session,
+ ensure_argus_policy_principals,
+ find_policy_by_id,
+ find_service_by_id,
+ get_test_logger,
+ log_http_response,
+ policy_matches,
+ service_matches,
+ unique_suffix,
+)
+
+pytestmark = pytest.mark.api
+
+logger = get_test_logger(__name__)
+
+http = None
+run_id = None
+
+repo_id = None
+policy_id = None
+service_payload = None
+policy_payload = None
+
+repo_id1 = None
+repo_id2 = None
+service_payload1 = None
+service_payload2 = None
+policy_id1 = None
+policy_id2 = None
+
+
+def setup_module():
+ global http, run_id
+
+ configure_test_logging(module_name=__name__)
+ logger.info("=== setup_module: start ===")
+
+ http = create_ranger_admin_session()
+ run_id = unique_suffix()
+ logger.info("Test run id: %s", run_id)
+
+ logger.info("Creating policy users and groups in Ranger Admin")
+ ensure_argus_policy_principals(http)
+
+ logger.info("=== setup_module: done ===")
+
+
+def teardown_module():
+ if http is None:
+ return
+
+ logger.info("=== teardown_module: cleaning leftover policies and services
===")
+
+ if policy_id is not None:
+ logger.info("Deleting leftover policy id=%s", policy_id)
+ http.delete(POLICY_URL + str(policy_id))
+ if policy_id1 is not None:
+ logger.info("Deleting leftover policy id=%s", policy_id1)
+ http.delete(POLICY_URL + str(policy_id1))
+ if policy_id2 is not None:
+ logger.info("Deleting leftover policy id=%s", policy_id2)
+ http.delete(POLICY_URL + str(policy_id2))
+
+ if repo_id is not None:
+ logger.info("Deleting leftover service id=%s", repo_id)
+ http.delete(SERVICE_URL + str(repo_id))
+ if repo_id1 is not None:
+ logger.info("Deleting leftover service id=%s", repo_id1)
+ http.delete(SERVICE_URL + str(repo_id1))
+ if repo_id2 is not None:
+ logger.info("Deleting leftover service id=%s", repo_id2)
+ http.delete(SERVICE_URL + str(repo_id2))
+
+ logger.info("=== teardown_module: done ===")
+
+
+def test_create_repository_using_pub_api_02a():
+ global repo_id, service_payload
+
+ logger.info("--- test_create_repository_using_pub_api_02a ---")
+ service_name = "test_hive_repo_" + run_id
+ service_payload = build_hive_service_payload(
+ name=service_name,
+ description="hive repo using api",
+ )
+ logger.info("POST hive service name=%s", service_name)
+
+ response = http.post(SERVICE_URL, json=service_payload)
+ log_http_response(logger, "POST create service", response)
+ assert_http_ok(response, (200, 201), "POST create hive service",
SERVICE_URL)
+
+ body = response.json()
+ repo_id = body["id"]
+ logger.info("Created service id=%s", repo_id)
+ assert service_matches(service_payload, body)
+
+
+def test_get_repo_using_pub_api_02b():
+ logger.info("--- test_get_repo_using_pub_api_02b ---")
+ url = SERVICE_URL + str(repo_id)
+ logger.info("GET service id=%s", repo_id)
+
+ response = http.get(url)
+ log_http_response(logger, "GET service", response)
+ assert_http_ok(response, 200, "GET hive service by id", url)
+
+ body = response.json()
+ assert body["id"] == repo_id
+ assert service_matches(service_payload, body)
+ logger.info("GET service OK")
+
+
+def test_update_repo_using_pub_api_02d():
+ global repo_id, service_payload
+
+ logger.info("--- test_update_repo_using_pub_api_02d ---")
+ service_name = "test_hive_repo_updating_" + run_id
+ service_payload = build_hive_service_payload(
+ name=service_name,
+ description="hive repo using api updating",
+ )
+ url = SERVICE_URL + str(repo_id)
+ logger.info("PUT service id=%s new name=%s", repo_id, service_name)
+
+ response = http.put(url, json=service_payload)
+ log_http_response(logger, "PUT service", response)
+ assert_http_ok(response, 200, "PUT update hive service", url)
+
+ body = response.json()
+ repo_id = body["id"]
+ assert service_matches(service_payload, body)
+ logger.info("Updated service id=%s", repo_id)
+
+
+def test_create_hive_policy_using_pub_api_02e():
+ global policy_id, policy_payload
+
+ logger.info("--- test_create_hive_policy_using_pub_api_02e ---")
+ policy_name = "test_hive_api_" + run_id
+ policy_payload = build_hive_policy_payload(
+ service_name=service_payload["name"],
+ policy_name=policy_name,
+ database_list="default,xatest",
+ table_list="sample01,sample02",
+ column_list="*",
+ perm_map_list=[
+ {"userList": ["hrt_21"], "permList": ["select", "update"]},
+ {"userList": ["hrt_22"], "permList": ["select"]},
+ ],
+ )
+ logger.info("POST policy name=%s for service=%s", policy_name,
service_payload["name"])
+
+ response = http.post(POLICY_URL, json=policy_payload)
+ log_http_response(logger, "POST create policy", response)
+ assert_http_ok(response, (200, 201), "POST create hive policy", POLICY_URL)
+
+ body = response.json()
+ policy_id = body["id"]
+ logger.info("Created policy id=%s", policy_id)
+ assert policy_matches(policy_payload, body)
+
+
+def test_get_hive_policy_using_public_api_02f():
+ logger.info("--- test_get_hive_policy_using_public_api_02f ---")
+ url = POLICY_URL + str(policy_id)
+ logger.info("GET policy id=%s", policy_id)
+
+ response = http.get(url)
+ log_http_response(logger, "GET policy", response)
+ assert_http_ok(response, 200, "GET hive policy by id", url)
+
+ body = response.json()
+ assert body["id"] == policy_id
+ assert policy_matches(policy_payload, body)
+ logger.info("GET policy OK")
+
+
+def test_update_policy_using_pub_api_02h():
+ global policy_id, policy_payload
+
+ logger.info("--- test_update_policy_using_pub_api_02h ---")
+ policy_name = "test_hive_api_updated_" + run_id
+ policy_payload = build_hive_policy_payload(
+ service_name=service_payload["name"],
+ policy_name=policy_name,
+ database_list="default,xatest",
+ table_list="sample01,sample02",
+ column_list="*",
+ perm_map_list=[
+ {"groupList": ["audit"], "permList": ["select", "update"]},
+ {"userList": ["hrt_21"], "permList": ["select"]},
+ ],
+ policy_id=policy_id,
+ )
+ url = POLICY_URL + str(policy_id)
+ logger.info("PUT policy id=%s", policy_id)
+
+ response = http.put(url, json=policy_payload)
+ log_http_response(logger, "PUT policy", response)
+ assert_http_ok(response, 200, "PUT update hive policy", url)
+
+ body = response.json()
+ policy_id = body["id"]
+ assert policy_matches(policy_payload, body)
+ logger.info("Updated policy id=%s", policy_id)
+
+
+def test_delete_policy_using_pub_api_02i():
+ global policy_id
+
+ logger.info("--- test_delete_policy_using_pub_api_02i ---")
+ url = POLICY_URL + str(policy_id)
+ logger.info("DELETE policy id=%s", policy_id)
+
+ delete_response = http.delete(url)
+ log_http_response(logger, "DELETE policy", delete_response)
+ assert_http_ok(delete_response, (200, 204), "DELETE hive policy", url)
+
+ get_response = http.get(url)
+ log_http_response(logger, "GET policy after delete", get_response)
+ assert_http_ok(get_response, (400, 404), "GET policy after delete (expect
missing)", url)
+
+ policy_id = None
+ logger.info("Policy deleted")
+
+
+def test_delete_repo_using_pub_api_02j():
+ global repo_id
+
+ logger.info("--- test_delete_repo_using_pub_api_02j ---")
+ url = SERVICE_URL + str(repo_id)
+ logger.info("DELETE service id=%s", repo_id)
+
+ delete_response = http.delete(url)
+ log_http_response(logger, "DELETE service", delete_response)
+ assert_http_ok(delete_response, (200, 204), "DELETE hive service", url)
+
+ get_response = http.get(url)
+ log_http_response(logger, "GET service after delete", get_response)
+ assert_http_ok(get_response, (400, 404), "GET service after delete (expect
missing)", url)
+
+ repo_id = None
+ logger.info("Service deleted")
+
+
+def test_01_09_get_all_repos_using_pub_api_case1i():
+ global repo_id1, repo_id2, service_payload1, service_payload2
+
+ logger.info("--- test_01_09_get_all_repos_using_pub_api_case1i ---")
+ service_payload1 = build_hive_service_payload(
+ name="Test_Repo_Hive_API_06_01_" + run_id,
+ description="Testing Hive Repo",
+ username="hive",
+ password="hive",
+ )
+ service_payload2 = build_hive_service_payload(
+ name="Test_Repo_Hive_API_06_02_" + run_id,
+ description="Testing Hive Repo",
+ username="hive123",
+ password="hive123",
+ )
+
+ create1 = http.post(SERVICE_URL, json=service_payload1)
+ log_http_response(logger, "POST service 1", create1)
+ create2 = http.post(SERVICE_URL, json=service_payload2)
+ log_http_response(logger, "POST service 2", create2)
+ assert_http_ok(create1, (200, 201), "POST hive service 1", SERVICE_URL)
+ assert_http_ok(create2, (200, 201), "POST hive service 2", SERVICE_URL)
+
+ repo_id1 = create1.json()["id"]
+ repo_id2 = create2.json()["id"]
+ logger.info("Created services id=%s and id=%s", repo_id1, repo_id2)
+
+ list_response = http.get(SERVICE_URL)
+ log_http_response(logger, "GET all services", list_response)
+ assert_http_ok(list_response, 200, "GET all services", SERVICE_URL)
+
+ services = list_response.json()
+ found1 = find_service_by_id(services, repo_id1)
+ found2 = find_service_by_id(services, repo_id2)
+ assert found1 is not None
+ assert found2 is not None
+ assert service_matches(service_payload1, found1)
+ assert service_matches(service_payload2, found2)
+ logger.info("Both services found in list")
+
+
+def test_01_10_get_all_policies_case1j():
+ global policy_id1, policy_id2
+
+ logger.info("--- test_01_10_get_all_policies_case1j ---")
+ policy1 = build_hive_policy_payload(
+ service_name=service_payload1["name"],
+ policy_name="test_hive_api_01_" + run_id,
+ database_list="default01,xatest01",
+ table_list="test01,test02",
+ column_list="*",
+ perm_map_list=[
+ {"groupList": ["finance"], "permList": ["select", "update"]},
+ {"userList": ["hrt_21"], "permList": ["select"]},
+ {"userList": ["hrt_22"], "permList": ["update"]},
+ ],
+ )
+ policy2 = build_hive_policy_payload(
+ service_name=service_payload2["name"],
+ policy_name="test_hive_api_02_" + run_id,
+ database_list="xatestcw,iemployee",
+ table_list="sample01,sample02",
+ column_list="*",
+ perm_map_list=[
+ {"groupList": ["audit"], "permList": ["select", "update"]},
+ {"userList": ["hrt_21"], "permList": ["update"]},
+ ],
+ )
+
+ create1 = http.post(POLICY_URL, json=policy1)
+ log_http_response(logger, "POST policy 1", create1)
+ create2 = http.post(POLICY_URL, json=policy2)
+ log_http_response(logger, "POST policy 2", create2)
+ assert_http_ok(create1, (200, 201), "POST hive policy 1", POLICY_URL)
+ assert_http_ok(create2, (200, 201), "POST hive policy 2", POLICY_URL)
+
+ policy_id1 = create1.json()["id"]
+ policy_id2 = create2.json()["id"]
+ logger.info("Created policies id=%s and id=%s", policy_id1, policy_id2)
+
+ list_response = http.get(POLICY_URL)
+ log_http_response(logger, "GET all policies", list_response)
+ assert_http_ok(list_response, 200, "GET all policies", POLICY_URL)
+
+ policies = list_response.json()
+ found1 = find_policy_by_id(policies, policy_id1)
+ found2 = find_policy_by_id(policies, policy_id2)
+ assert found1 is not None
+ assert found2 is not None
+ assert policy_matches(policy1, found1)
+ assert policy_matches(policy2, found2)
+ logger.info("Both policies found in list")
+
+
+def test_01_11_delete_all_repo_case1k():
+ global policy_id1, policy_id2, repo_id1, repo_id2
+
+ logger.info("--- test_01_11_delete_all_repo_case1k ---")
+
+ if policy_id1 is not None:
+ logger.info("DELETE policy id=%s", policy_id1)
+ http.delete(POLICY_URL + str(policy_id1))
+ policy_id1 = None
+ if policy_id2 is not None:
+ logger.info("DELETE policy id=%s", policy_id2)
+ http.delete(POLICY_URL + str(policy_id2))
+ policy_id2 = None
+
+ if repo_id1 is not None:
+ url = SERVICE_URL + str(repo_id1)
+ logger.info("DELETE service id=%s", repo_id1)
+ response = http.delete(url)
+ log_http_response(logger, "DELETE service 1", response)
+ assert_http_ok(response, (200, 204), "DELETE hive service 1", url)
+ repo_id1 = None
+
+ if repo_id2 is not None:
+ url = SERVICE_URL + str(repo_id2)
+ logger.info("DELETE service id=%s", repo_id2)
+ response = http.delete(url)
+ log_http_response(logger, "DELETE service 2", response)
+ assert_http_ok(response, (200, 204), "DELETE hive service 2", url)
+ repo_id2 = None
+
+ logger.info("Cleanup finished")
diff --git a/functional-tests/pytest.ini b/functional-tests/pytest.ini
index a815e6cfe..87e6e65d6 100644
--- a/functional-tests/pytest.ini
+++ b/functional-tests/pytest.ini
@@ -26,4 +26,12 @@ markers =
xuserrest: xuserrest test cases
secure_endpoint: secure endpoint test cases
rolerest: rolerest test cases
-pythonpath = .
\ No newline at end of file
+ api: public API v2 test cases
+pythonpath = .
+# No console logging during pytest; logger.info -> logs/pytest.log (tail -f
logs/pytest.log).
+log_cli = false
+log_file = logs/pytest.log
+log_file_mode = w
+log_file_level = INFO
+log_file_format = %(asctime)s %(levelname)s [%(name)s] %(message)s
+log_file_date_format = %Y-%m-%d %H:%M:%S
\ No newline at end of file
diff --git a/functional-tests/ranger_test_utils/utils.py
b/functional-tests/ranger_test_utils/utils.py
new file mode 100644
index 000000000..8c1b6fd75
--- /dev/null
+++ b/functional-tests/ranger_test_utils/utils.py
@@ -0,0 +1,358 @@
+# 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.
+
+"""
+Shared helpers for Apache Ranger functional-tests (Docker pytest).
+
+"""
+
+import logging
+import uuid
+from datetime import datetime, timezone
+
+import requests
+
+# --- Ranger Admin defaults (local Docker) ---
+
+RANGER_ADMIN_BASE_URL = "http://localhost:6080/service"
+
+PUBLIC_V2_BASE = RANGER_ADMIN_BASE_URL + "/public/v2/api"
+SERVICE_URL = PUBLIC_V2_BASE + "/service/"
+POLICY_URL = PUBLIC_V2_BASE + "/policy/"
+XUSERS_BASE = RANGER_ADMIN_BASE_URL + "/xusers"
+
+DEFAULT_ADMIN_AUTH = ("admin", "rangerR0cks!")
+
+DEFAULT_JSON_HEADERS = {
+ "Accept": "application/json",
+ "Content-Type": "application/json",
+}
+
+# Users/groups used by migrated Argus public-API policy tests (created via
xusers if missing).
+ARGUS_POLICY_USERS = ["hrt_21", "hrt_22"]
+ARGUS_POLICY_GROUPS = ["finance", "audit"]
+
+HIVE_JDBC_URL = "jdbc:hive2://127.0.0.1:10000/default"
+HIVE_DRIVER = "org.apache.hive.jdbc.HiveDriver"
+
+# --- Logging ---
+
+
+def configure_test_logging(level=logging.INFO, module_name=None):
+ """
+ Call once from setup_module.
+
+ """
+ logging.getLogger().setLevel(level)
+ if module_name:
+ logging.getLogger(module_name).setLevel(level)
+
+
+def get_test_logger(module_name):
+ """
+ Module logger for tests.
+ """
+ log = logging.getLogger(module_name)
+ log.setLevel(logging.INFO)
+ return log
+
+
+def log_http_response(logger, step, response, max_body_length=400):
+ """Log HTTP status and a short response body for each REST call."""
+ logger.info("%s: HTTP %s", step, response.status_code)
+ if not response.text:
+ return
+ body = response.text
+ if len(body) > max_body_length:
+ body = body[:max_body_length] + "..."
+ logger.info("%s response body: %s", step, body)
+
+
+# --- HTTP assertions ---
+
+
+def assert_http_ok(response, ok_codes, step, url=""):
+ """
+ Check response.status_code. On failure, raise with step, URL, and body
text.
+ ok_codes can be 200 or (200, 201).
+ """
+ if isinstance(ok_codes, int):
+ expected = (ok_codes,)
+ else:
+ expected = ok_codes
+ if response.status_code in expected:
+ return
+ message = step + " failed: HTTP " + str(response.status_code)
+ message += " (expected " + str(expected) + ")"
+ if url:
+ message += "\nURL: " + url
+ message += "\nResponse body:\n" + response.text
+ raise AssertionError(message)
+
+
+def create_ranger_admin_session(auth=None, headers=None):
+ """
+ Build a requests.Session for Ranger Admin REST calls (Docker defaults).
+
+ Sets HTTP basic auth and JSON Accept/Content-Type headers on the session.
+ """
+ if auth is None:
+ auth = DEFAULT_ADMIN_AUTH
+ if headers is None:
+ headers = DEFAULT_JSON_HEADERS
+ session = requests.Session()
+ session.auth = auth
+ session.headers.update(headers)
+ return session
+
+
+# --- Test run IDs ---
+
+
+def unique_suffix():
+ """Unique string for service/policy names so parallel runs do not clash."""
+ time_part = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
+ random_part = uuid.uuid4().hex[:6]
+ return time_part + random_part
+
+
+# --- xusers: ensure principals exist ---
+
+
+def ensure_user_exists(session, username):
+ """Create a Ranger user via xusers REST if it is not already present."""
+ lookup_url = XUSERS_BASE + "/users/userName/" + username
+ lookup = session.get(lookup_url)
+ if lookup.status_code == 200:
+ return
+ payload = {
+ "name": username,
+ "firstName": username,
+ "lastName": "api_test",
+ "emailAddress": username + "@example.com",
+ "password": "Test@123",
+ "status": 1,
+ "isVisible": 1,
+ "userSource": 1,
+ "userRoleList": ["ROLE_USER"],
+ "groupIdList": [],
+ "groupNameList": [],
+ }
+ create_url = XUSERS_BASE + "/secure/users"
+ create = session.post(create_url, json=payload)
+ assert_http_ok(create, 200, "Create Ranger user " + username, create_url)
+
+
+def ensure_group_exists(session, group_name):
+ """Create a Ranger group via xusers REST if it is not already present."""
+ lookup_url = XUSERS_BASE + "/groups/groupName/" + group_name
+ lookup = session.get(lookup_url)
+ if lookup.status_code == 200:
+ return
+ payload = {"name": group_name, "groupSource": 0}
+ create_url = XUSERS_BASE + "/secure/groups"
+ create = session.post(create_url, json=payload)
+ assert_http_ok(create, 200, "Create Ranger group " + group_name,
create_url)
+
+
+def ensure_argus_policy_principals(session):
+ """Create users and groups referenced by migrated Argus policy tests."""
+ for username in ARGUS_POLICY_USERS:
+ ensure_user_exists(session, username)
+ for group_name in ARGUS_POLICY_GROUPS:
+ ensure_group_exists(session, group_name)
+
+
+# --- Hive public API v2 JSON builders ---
+
+
+def split_csv(value):
+ """Turn 'a, b ,c' into ['a', 'b', 'c']."""
+ return [part.strip() for part in value.split(",") if part.strip()]
+
+
+def build_hive_service_payload(
+ name,
+ description,
+ username="policymgr",
+ password="policymgr",
+ jdbc_url=HIVE_JDBC_URL,
+ is_enabled=True,
+):
+ """JSON body for POST/PUT on /public/v2/api/service/ (Hive type)."""
+ return {
+ "name": name,
+ "type": "hive",
+ "description": description,
+ "isEnabled": is_enabled,
+ "configs": {
+ "username": username,
+ "password": password,
+ "jdbc.driverClassName": HIVE_DRIVER,
+ "jdbc.url": jdbc_url,
+ "commonNameForCertificate": "",
+ },
+ }
+
+
+def resource_block(values):
+ """Standard Ranger policy resource block (database/table/column)."""
+ return {
+ "values": values,
+ "isExcludes": False,
+ "isRecursive": False,
+ }
+
+
+def perm_map_list_to_policy_items(perm_map_list):
+ """
+ Convert legacy test shape to Public API v2 policyItems.
+
+ Each entry: {"userList": [...], "groupList": [...], "permList": ["select",
...]}.
+ """
+ items = []
+ for entry in perm_map_list:
+ accesses = [
+ {"type": perm, "isAllowed": True}
+ for perm in entry.get("permList", [])
+ ]
+ items.append(
+ {
+ "users": entry.get("userList", []),
+ "groups": entry.get("groupList", []),
+ "accesses": accesses,
+ "delegateAdmin": False,
+ }
+ )
+ return items
+
+
+def build_hive_policy_payload(
+ service_name,
+ policy_name,
+ database_list,
+ table_list,
+ column_list,
+ perm_map_list,
+ description="",
+ is_enabled=True,
+ is_audit_enabled=True,
+ policy_id=None,
+):
+ """JSON body for POST/PUT on /public/v2/api/policy/."""
+ payload = {
+ "service": service_name,
+ "name": policy_name,
+ "description": description,
+ "isEnabled": is_enabled,
+ "isAuditEnabled": is_audit_enabled,
+ "resources": {
+ "database": resource_block(split_csv(database_list)),
+ "table": resource_block(split_csv(table_list)),
+ "column": resource_block(split_csv(column_list)),
+ },
+ "policyItems": perm_map_list_to_policy_items(perm_map_list),
+ }
+ if policy_id is not None:
+ payload["id"] = policy_id
+ return payload
+
+
+# --- Compare expected vs actual service/policy JSON ---
+
+
+def service_matches(expected, actual, attributes=None):
+ """True if selected top-level service fields match."""
+ if attributes is None:
+ attributes = ["name", "description", "type"]
+ return all(actual.get(key) == expected.get(key) for key in attributes)
+
+
+def get_allowed_access_types(policy_item):
+ """Sorted list of permission types marked isAllowed on a policy item."""
+ types = [
+ access.get("type")
+ for access in policy_item.get("accesses", [])
+ if access.get("isAllowed", True)
+ ]
+ return sorted(types)
+
+
+def single_policy_item_matches(expected_item, actual_item):
+ """True if users, groups, and allowed access types match one policy
item."""
+ if sorted(expected_item.get("users", [])) !=
sorted(actual_item.get("users", [])):
+ return False
+ if sorted(expected_item.get("groups", [])) != sorted(
+ actual_item.get("groups", [])
+ ):
+ return False
+ return get_allowed_access_types(expected_item) == get_allowed_access_types(
+ actual_item
+ )
+
+
+def policy_items_match(expected_items, actual_items):
+ """True if every expected policy item has a matching actual item (order
ignored)."""
+ if len(expected_items) != len(actual_items):
+ return False
+ for expected_item in expected_items:
+ if not any(
+ single_policy_item_matches(expected_item, actual_item)
+ for actual_item in actual_items
+ ):
+ return False
+ return True
+
+
+def resource_values_match(expected_resources, actual_resources, resource_name):
+ """True if database/table/column value lists match (order ignored)."""
+ exp_vals = sorted(
+ expected_resources.get(resource_name, {}).get("values", [])
+ )
+ act_vals = sorted(actual_resources.get(resource_name, {}).get("values",
[]))
+ return exp_vals == act_vals
+
+
+def policy_matches(expected, actual):
+ """True if name, service, hive resources, and policy items match."""
+ if expected.get("name") != actual.get("name"):
+ return False
+ if expected.get("service") != actual.get("service"):
+ return False
+ exp_res = expected.get("resources", {})
+ act_res = actual.get("resources", {})
+ for resource_name in ("database", "table", "column"):
+ if not resource_values_match(exp_res, act_res, resource_name):
+ return False
+ return policy_items_match(
+ expected.get("policyItems", []),
+ actual.get("policyItems", []),
+ )
+
+
+def find_service_by_id(services, service_id):
+ """Return the service dict with the given id, or None."""
+ for service in services:
+ if service.get("id") == service_id:
+ return service
+ return None
+
+
+def find_policy_by_id(policies, policy_id):
+ """Return the policy dict with the given id, or None."""
+ for policy in policies:
+ if policy.get("id") == policy_id:
+ return policy
+ return None
diff --git a/functional-tests/readme.md b/functional-tests/readme.md
index 4fd0ec485..2104d57f9 100644
--- a/functional-tests/readme.md
+++ b/functional-tests/readme.md
@@ -30,6 +30,7 @@ This test suite validates REST API endpoints for Apache
Ranger services — Admi
| **rolerest** | Test cases for Ranger Admin Role REST APIs |
| **servicerest** | Test cases for Ranger Admin Service REST APIs |
| **tagrest** | Test cases for Ranger Admin Tag REST APIs |
+| **api** | Public API v2 service and policy CRUD (admin Docker only) |
---
@@ -37,13 +38,15 @@ This test suite validates REST API endpoints for Apache
Ranger services — Admi
```text
functional-tests/
+├── ranger_test_utils/utils.py # Shared logging, HTTP asserts, Public API v2
helpers
├── hdfs/ # Tests on HDFS encryption cycle
├── kms/ # Tests on KMS REST API
├── xuserrest/ # Tests on Ranger User/Group/Role REST APIs
├── rolerest/ # Tests on Ranger Role REST APIs
├── servicerest/ # Tests on Ranger Service REST APIs
├── tagrest/ # Tests on Ranger Tag REST APIs
-│
+├── api/ # Public API v2 (service / policy) tests
+│
├── pytest.ini # Registers custom pytest markers
├── run-tests.sh # Script to automate setup and test execution
├── requirements.txt # Python dependencies
@@ -120,9 +123,10 @@ export AUDIT_INDEX_STORE=none
---
## Running Tests
-
The `run-tests.sh` script manages Docker container setup, dependency
installation, and test execution. It supports both interactive and
argument-based modes.
+1. Interactive Mode:
+
### 1. Interactive Mode
Run the script without arguments to be prompted for each input:
@@ -206,5 +210,4 @@ Regardless of which test suites you choose, these
containers always start:
---
## Test Reports
-
After execution, an HTML report is automatically generated for each suite in
the `functional-tests/` directory:
diff --git a/functional-tests/run-tests.sh b/functional-tests/run-tests.sh
index 758681911..8b8f2ebbd 100755
--- a/functional-tests/run-tests.sh
+++ b/functional-tests/run-tests.sh
@@ -16,13 +16,14 @@
#!/bin/bash
# All available test suites (pytest folders)
-ALL_TEST_SUITES=(rolerest xuserrest servicerest tagrest hdfs kms)
+
+ALL_TEST_SUITES=(rolerest xuserrest servicerest tagrest hdfs kms api)
# Suites that have actual docker-compose services
DOCKER_SERVICES=(hdfs kms)
# Test-only suites (no docker-compose file needed)
-TEST_ONLY_SUITES=(rolerest xuserrest servicerest tagrest)
+TEST_ONLY_SUITES=(rolerest xuserrest servicerest tagrest api)
#handle input
DB_TYPE="${1:-}"