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 178b76d46 RANGER-5839: Add Docker system tests to verify HDFS
group-policy cases (#1312)
178b76d46 is described below
commit 178b76d46d712676ada4d0f058a480c7c68a82a2
Author: Vishal Bhavsar <[email protected]>
AuthorDate: Wed Oct 14 20:32:05 2026 +0530
RANGER-5839: Add Docker system tests to verify HDFS group-policy cases
(#1312)
---
functional-tests/hdfs/conftest.py | 57 +++-
functional-tests/hdfs/test_hdfs_group_policy_03.py | 271 +++++++++++++++++++
functional-tests/hdfs/utils.py | 10 +-
functional-tests/kms/utils.py | 49 +++-
functional-tests/pytest.ini | 1 +
functional-tests/ranger_test_utils/hdfs_utils.py | 301 +++++++++++++++++++++
functional-tests/ranger_test_utils/utils.py | 21 ++
functional-tests/readme.md | 25 +-
8 files changed, 711 insertions(+), 24 deletions(-)
diff --git a/functional-tests/hdfs/conftest.py
b/functional-tests/hdfs/conftest.py
index deb0ea4cc..f72b448f4 100644
--- a/functional-tests/hdfs/conftest.py
+++ b/functional-tests/hdfs/conftest.py
@@ -16,18 +16,36 @@
import docker
import pytest
import time
-from hdfs.test_config import CORE_SITE_XML_PATH, HADOOP_CONTAINER, HDFS_USER,
KMS_CONTAINER, KMS_PROPERTY, TEST_EZ_PATHS, TEST_KMS_KEYS
-from hdfs.utils import cleanup_test_artifacts, ensure_hadoop_user_keytab,
ensure_kms_hdfs_policy, ensure_kms_kerberos_rules
-from kms.utils import ensure_keyadmin_keytab, ensure_ticket
+from hdfs.test_config import (
+ CORE_SITE_XML_PATH,
+ HADOOP_CONTAINER,
+ HDFS_USER,
+ KMS_CONTAINER,
+ KMS_PROPERTY,
+ TEST_EZ_PATHS,
+ TEST_KMS_KEYS,
+)
# Setup Docker Client
client = docker.from_env()
+
+def _session_needs_encryption_setup(session):
+ """Encryption/KMS session setup — skip for hdfs_policy-only runs."""
+ if not session.items:
+ return False
+ for item in session.items:
+ if item.get_closest_marker("hdfs_policy") is None:
+ return True
+ return False
+
+
@pytest.fixture(scope="session")
def hadoop_container():
- container = client.containers.get(HADOOP_CONTAINER) #to get hadoop
container instance
+ container = client.containers.get(HADOOP_CONTAINER)
return container
+
def ensure_key_provider_and_simple_auth(container) -> bool:
"""
Ensures:
@@ -63,6 +81,7 @@ def ensure_key_provider_and_simple_auth(container) -> bool:
return changed
+
def ensure_user_exists(container, username: str) -> None:
exit_code, _ = container.exec_run(f"id -u {username}", user="root")
if exit_code == 0:
@@ -71,6 +90,7 @@ def ensure_user_exists(container, username: str) -> None:
container.exec_run(f"useradd -m -s /bin/bash {username}", user="root")
container.exec_run(f"usermod -aG hadoop {username}", user="root")
+
def ensure_kms_provider_configured(container) -> bool:
"""
Ensures KMS provider property exists in core-site.xml.
@@ -104,8 +124,23 @@ def ensure_kms_provider_configured(container) -> bool:
return changed
+
@pytest.fixture(scope="session", autouse=True)
-def setup_environment(hadoop_container):
+def setup_encryption_environment(request):
+ if not _session_needs_encryption_setup(request.session):
+ yield
+ return
+
+ hadoop_container = client.containers.get(HADOOP_CONTAINER)
+
+ from hdfs.utils import (
+ cleanup_test_artifacts,
+ ensure_hadoop_user_keytab,
+ ensure_kms_hdfs_policy,
+ ensure_kms_kerberos_rules,
+ )
+ from kms.utils import ensure_keyadmin_keytab, ensure_ticket
+
changed = ensure_kms_provider_configured(hadoop_container)
if changed:
hadoop_container.restart()
@@ -118,14 +153,17 @@ def setup_environment(hadoop_container):
# safemode leave with kerberos (hdfs principal already has keytab in
container)
hadoop_container.exec_run(
- ["bash", "-c",
- "kinit -kt /etc/keytabs/hdfs.keytab
hdfs/[email protected] 2>/dev/null;"
- "hdfs dfsadmin -safemode leave"],
+ [
+ "bash",
+ "-c",
+ "kinit -kt /etc/keytabs/hdfs.keytab
hdfs/[email protected] 2>/dev/null;"
+ "hdfs dfsadmin -safemode leave",
+ ],
user=HDFS_USER,
)
kms_container = client.containers.get(KMS_CONTAINER)
- ensure_kms_kerberos_rules(kms_container) # already sleeps 30s internally
when restart is needed
+ ensure_kms_kerberos_rules(kms_container)
ensure_keyadmin_keytab()
ensure_ticket()
@@ -134,4 +172,3 @@ def setup_environment(hadoop_container):
cleanup_test_artifacts(hadoop_container, TEST_KMS_KEYS, TEST_EZ_PATHS)
yield
cleanup_test_artifacts(hadoop_container, TEST_KMS_KEYS, TEST_EZ_PATHS)
-
diff --git a/functional-tests/hdfs/test_hdfs_group_policy_03.py
b/functional-tests/hdfs/test_hdfs_group_policy_03.py
new file mode 100644
index 000000000..ff518e50b
--- /dev/null
+++ b/functional-tests/hdfs/test_hdfs_group_policy_03.py
@@ -0,0 +1,271 @@
+# 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.
+
+"""
+Group write on /demo_<suffix>/data_<suffix> and read+execute on / and
/demo_<suffix>
+for group finance.
+"""
+
+import pytest
+
+from ranger_test_utils.hdfs_utils import (
+ GROUP_AUDIT,
+ GROUP_FINANCE,
+ HDFS_AUTHZ_DENIED_SUBSTR,
+ HDFS_SERVICE_NAME,
+ HDFS_SERVICE_USER,
+ USER_AUDIT,
+ USER_FINANCE,
+ build_hdfs_policy_payload,
+ cleanup_hdfs_paths,
+ create_hdfs_policy,
+ ensure_kerberos_user_on_hadoop,
+ ensure_unix_user_with_primary_group,
+ HADOOP_CONTAINER,
+ get_docker_client,
+ preflight_hdfs_plugin_stack,
+ run_hdfs_dfs,
+ wait_for_hdfs_policy_propagation,
+)
+from ranger_test_utils.hive_utils import delete_policies_by_id,
set_all_policies_enabled
+from ranger_test_utils.utils import (
+ DOCKER_PLUGIN_TEST_USER_PASSWORD,
+ configure_test_logging,
+ create_ranger_admin_session,
+ ensure_argus_policy_principals,
+ ensure_user_assigned_to_groups,
+ ensure_user_exists,
+ get_test_logger,
+ log_testcase_begin_for_pytest,
+ unique_suffix,
+)
+
+pytestmark = pytest.mark.hdfs_policy
+
+logger = get_test_logger(__name__)
+
+_admin_session = None
+_policy_ids = []
+_hdfs_paths_to_cleanup = []
+_run_suffix = None
+_demo_base = None
+_demo_data = None
+
+# Container-local files for copyFromLocal (case 3B/3D).
+_LOCAL_STAGING_ROOT = "/tmp/ranger-ft/demo/data"
+
+
+def _demo_paths_for_suffix(run_suffix):
+ """QE /demo/data layout with unique paths: /demo_<suffix>/data_<suffix>."""
+ demo_base = "/demo_" + run_suffix
+ return demo_base, demo_base + "/data_" + run_suffix
+
+
+def _prepare_demo_data_hdfs_path(hdfs_data_path):
+ """Recreate path with mode 000 — Ranger grants access, not POSIX."""
+ run_hdfs_dfs(HDFS_SERVICE_USER, "-rm -r -f " + hdfs_data_path,
service_user=True)
+ run_hdfs_dfs(
+ HDFS_SERVICE_USER,
+ "-mkdir -p " + hdfs_data_path,
+ service_user=True,
+ )
+ run_hdfs_dfs(
+ HDFS_SERVICE_USER,
+ "-chmod -R 000 " + hdfs_data_path,
+ service_user=True,
+ )
+
+
+def _prepare_local_copy_sources(container):
+ container.exec_run("rm -rf /tmp/ranger-ft", user="root")
+ container.exec_run("mkdir -p " + _LOCAL_STAGING_ROOT, user="root")
+ for filename in ("filefinance.txt", "fileaudit.txt"):
+ path = _LOCAL_STAGING_ROOT + "/" + filename
+ container.exec_run(
+ 'bash -c "echo test content > ' + path + '"',
+ user="root",
+ )
+ container.exec_run("chmod a+r " + path, user="root")
+
+
+def setup_module():
+ configure_test_logging(module_name=__name__)
+ logger.info("=== setup_module: start (HDFS group policy case 3) ===")
+ preflight_hdfs_plugin_stack()
+
+ global _admin_session, _policy_ids, _run_suffix, _demo_base, _demo_data
+ _run_suffix = unique_suffix(4)
+ _demo_base, _demo_data = _demo_paths_for_suffix(_run_suffix)
+ logger.info("Test HDFS paths: demo_base=%s demo_data=%s", _demo_base,
_demo_data)
+
+ _admin_session = create_ranger_admin_session()
+ hadoop = get_docker_client().containers.get(HADOOP_CONTAINER)
+
+ logger.info("Ensuring Ranger users, groups, and group membership")
+ ensure_argus_policy_principals(_admin_session)
+ ensure_user_exists(
+ _admin_session,
+ USER_FINANCE,
+ password=DOCKER_PLUGIN_TEST_USER_PASSWORD,
+ last_name="hdfs_test",
+ )
+ ensure_user_exists(
+ _admin_session,
+ USER_AUDIT,
+ password=DOCKER_PLUGIN_TEST_USER_PASSWORD,
+ last_name="hdfs_test",
+ )
+ ensure_user_assigned_to_groups(_admin_session, USER_FINANCE,
[GROUP_FINANCE])
+ ensure_user_assigned_to_groups(_admin_session, USER_AUDIT, [GROUP_AUDIT])
+
+ logger.info("Ensuring Unix users and Kerberos principals on ranger-hadoop")
+ ensure_unix_user_with_primary_group(hadoop, USER_FINANCE, GROUP_FINANCE)
+ ensure_unix_user_with_primary_group(hadoop, USER_AUDIT, GROUP_AUDIT)
+ ensure_kerberos_user_on_hadoop(USER_FINANCE)
+ ensure_kerberos_user_on_hadoop(USER_AUDIT)
+
+ _prepare_local_copy_sources(hadoop)
+
+ logger.info("Disabling existing policies on %s", HDFS_SERVICE_NAME)
+ set_all_policies_enabled(_admin_session, False, HDFS_SERVICE_NAME)
+
+ policy_data_write = build_hdfs_policy_payload(
+ policy_name="gp3-data-write-" + _run_suffix,
+ path_values=[_demo_data],
+ perm_map_list=[
+ {
+ "groupList": [GROUP_FINANCE],
+ "userList": [],
+ "permList": ["write"],
+ }
+ ],
+ description=_demo_data + " Write for Group:Finance",
+ is_recursive=False,
+ )
+ policy_root_demo_rx = build_hdfs_policy_payload(
+ policy_name="gp3-root-demo-rx-" + _run_suffix,
+ path_values=["/", _demo_base],
+ perm_map_list=[
+ {
+ "groupList": [GROUP_FINANCE],
+ "userList": [],
+ "permList": ["read", "execute"],
+ }
+ ],
+ description="/, " + _demo_base + " RX for Group:Finance",
+ is_recursive=False,
+ )
+ _policy_ids = [
+ create_hdfs_policy(_admin_session, policy_data_write),
+ create_hdfs_policy(_admin_session, policy_root_demo_rx),
+ ]
+ logger.info("Created HDFS policies: %s", _policy_ids)
+
+ _prepare_demo_data_hdfs_path(_demo_data)
+ wait_for_hdfs_policy_propagation()
+ logger.info("=== setup_module: done ===")
+
+
+def teardown_module():
+ if _admin_session is None:
+ return
+
+ logger.info("=== teardown_module: start ===")
+ if _hdfs_paths_to_cleanup:
+ cleanup_hdfs_paths(_hdfs_paths_to_cleanup)
+ if _demo_base is not None:
+ cleanup_hdfs_paths([_demo_base])
+
+ if _policy_ids:
+ delete_policies_by_id(_admin_session, _policy_ids)
+
+ logger.info("Re-enabling all policies on %s", HDFS_SERVICE_NAME)
+ set_all_policies_enabled(_admin_session, True, HDFS_SERVICE_NAME)
+ wait_for_hdfs_policy_propagation()
+ logger.info("=== teardown_module: done ===")
+
+
+def test_03_01_create_hdfs_directory_case3a(request):
+ log_testcase_begin_for_pytest(logger, request.node.name, suite="HDFS")
+ hdfsdir = _demo_data + "/findir_test_03_01"
+ _hdfs_paths_to_cleanup.append(hdfsdir)
+
+ exit_code, output = run_hdfs_dfs(
+ USER_FINANCE,
+ "-mkdir -p " + hdfsdir,
+ fail_on_error=False,
+ )
+ assert HDFS_AUTHZ_DENIED_SUBSTR not in output, (
+ "Expected mkdir to succeed for finance user; exit_code="
+ + str(exit_code)
+ + " output="
+ + output
+ )
+
+
+def test_03_02_create_hdfs_file_case3b(request):
+ log_testcase_begin_for_pytest(logger, request.node.name, suite="HDFS")
+ localfile = _LOCAL_STAGING_ROOT + "/filefinance.txt"
+ hdfsfile = _demo_data + "/filefinance_test_03_02.txt"
+ _hdfs_paths_to_cleanup.append(hdfsfile)
+
+ exit_code, output = run_hdfs_dfs(
+ USER_FINANCE,
+ "-copyFromLocal " + localfile + " " + hdfsfile,
+ fail_on_error=False,
+ )
+ assert HDFS_AUTHZ_DENIED_SUBSTR not in output, (
+ "Expected copyFromLocal to succeed for finance user; exit_code="
+ + str(exit_code)
+ + " output="
+ + output
+ )
+
+
+def test_03_03_create_hdfs_directory_case3c(request):
+ log_testcase_begin_for_pytest(logger, request.node.name, suite="HDFS")
+ hdfsdir = _demo_data + "/auditdir_test_03_03"
+ _hdfs_paths_to_cleanup.append(hdfsdir)
+
+ exit_code, output = run_hdfs_dfs(
+ USER_AUDIT,
+ "-mkdir -p " + hdfsdir,
+ fail_on_error=False,
+ )
+ assert HDFS_AUTHZ_DENIED_SUBSTR in output, (
+ "Expected mkdir to be denied for audit user; exit_code="
+ + str(exit_code)
+ + " output="
+ + output
+ )
+
+
+def test_03_04_create_hdfs_file_case3d(request):
+ log_testcase_begin_for_pytest(logger, request.node.name, suite="HDFS")
+ localfile = _LOCAL_STAGING_ROOT + "/fileaudit.txt"
+ hdfsfile = _demo_data + "/fileaudit_test_03_04.txt"
+ _hdfs_paths_to_cleanup.append(hdfsfile)
+
+ exit_code, output = run_hdfs_dfs(
+ USER_AUDIT,
+ "-copyFromLocal " + localfile + " " + hdfsfile,
+ fail_on_error=False,
+ )
+ assert HDFS_AUTHZ_DENIED_SUBSTR in output, (
+ "Expected copyFromLocal to be denied for audit user; exit_code="
+ + str(exit_code)
+ + " output="
+ + output
+ )
diff --git a/functional-tests/hdfs/utils.py b/functional-tests/hdfs/utils.py
index 3728fda9e..bffe31dce 100644
--- a/functional-tests/hdfs/utils.py
+++ b/functional-tests/hdfs/utils.py
@@ -18,7 +18,13 @@
import docker
import requests
from hdfs.test_config import (KMS_CONTAINER, HADOOP_NAMENODE_LOG_PATH,
KMS_LOG_PATH)
-from kms.utils import krb_requests, ensure_ticket, BASE_URL, PARAMS, container
as kms_container
+from kms.utils import (
+ BASE_URL,
+ PARAMS,
+ ensure_ticket,
+ get_kms_container,
+ krb_requests,
+)
from hdfs.test_config import HDFS_USER
import subprocess
import tempfile
@@ -318,7 +324,7 @@ def delete_kms_key(key_name: str) -> None:
# Delete KMS key if present. Ignores missing-key responses.
# Only refresh the keyadmin ticket when no valid ticket exists — avoids a
full
# kdestroy+kinit on every key deletion (e.g. 6 calls in
cleanup_test_artifacts).
- ec, _ = kms_container.exec_run("klist -s 2>/dev/null", user="root")
+ ec, _ = get_kms_container().exec_run("klist -s 2>/dev/null", user="root")
if ec != 0:
ensure_ticket()
resp = krb_requests.delete(f"{BASE_URL}/key/{key_name}", params=PARAMS)
diff --git a/functional-tests/kms/utils.py b/functional-tests/kms/utils.py
index 27560e597..9705a50c3 100644
--- a/functional-tests/kms/utils.py
+++ b/functional-tests/kms/utils.py
@@ -37,8 +37,31 @@
TESTUSER_PARAMS = {}
BASE_URL = "http://ranger-kms.rangernw:9292/kms/v1"
PARAMS = {"user.name": "keyadmin"}
-client = docker.from_env()
-container = client.containers.get(KMS_CONTAINER_NAME)
+
+_docker_client = None
+
+
+def get_docker_client():
+ global _docker_client
+ if _docker_client is None:
+ _docker_client = docker.from_env()
+ return _docker_client
+
+
+def get_kms_container():
+ """Return ranger-kms container (lazy — no Docker call at import time)."""
+ return get_docker_client().containers.get(KMS_CONTAINER_NAME)
+
+
+class _LazyKmsContainer:
+ """Backward-compatible alias; resolves ranger-kms on first use."""
+
+ def __getattr__(self, name):
+ return getattr(get_kms_container(), name)
+
+
+container = _LazyKmsContainer()
+
KEYADMIN_PRINCIPAL = "[email protected]"
KEYADMIN_KEYTAB = "/etc/keytabs/keyadmin.keytab"
KDC_CONTAINER = "ranger-kdc"
@@ -111,7 +134,9 @@ def _sh_quote(s):
# Output format: <status_code>\n---BODY---\n<body text>
shell_cmd = f'{curl_cmd}; echo "\\n---BODY---"; cat /tmp/curl_body.txt'
- exit_code, output = container.exec_run(["sh", "-c", shell_cmd],
user="root")
+ exit_code, output = get_kms_container().exec_run(
+ ["sh", "-c", shell_cmd], user="root"
+ )
raw = output.decode() if output else ""
separator = "\n---BODY---\n"
@@ -157,8 +182,8 @@ def __repr__(self):
def _ensure_kdc_principal_keytab(principal: str, dest_container_name: str,
dest_keytab_path: str, kdc_tmp_path: str, ) -> None:
# create principal in KDC, export keytab, copy to target container
- kdc = client.containers.get(KDC_CONTAINER)
- dest = client.containers.get(dest_container_name)
+ kdc = get_docker_client().containers.get(KDC_CONTAINER)
+ dest = get_docker_client().containers.get(dest_container_name)
exit_code, output = kdc.exec_run(f'kadmin.local -q "getprinc
{principal}"', user="root")
if exit_code != 0 or b"does not exist" in output:
@@ -184,13 +209,14 @@ def ensure_keyadmin_keytab():
def _kinit_with_retry(principal: str, keytab: str, ensure_keytab_fn,
max_attempts: int = 5, wait: int = 10) -> None:
"""Ensure the keytab exists then kinit, retrying to absorb KDC warm-up
delays."""
- exit_code, _ = container.exec_run(f"test -f {keytab}", user="root")
+ kms = get_kms_container()
+ exit_code, _ = kms.exec_run(f"test -f {keytab}", user="root")
if exit_code != 0:
ensure_keytab_fn()
- container.exec_run("kdestroy -A 2>/dev/null || true", user="root")
+ kms.exec_run("kdestroy -A 2>/dev/null || true", user="root")
for _ in range(max_attempts):
- exit_code, output = container.exec_run(
+ exit_code, output = kms.exec_run(
f"kinit -kt {keytab} {principal}", user="root"
)
if exit_code == 0:
@@ -226,7 +252,7 @@ def modify_blacklist_property(operation, users,
action="add"):
ensure_ticket()
- result = container.exec_run(f"cat {dbks_site_path}", user="root")
+ result = get_kms_container().exec_run(f"cat {dbks_site_path}", user="root")
if result.exit_code != 0:
raise RuntimeError(f"Cannot read dbks-site.xml:
{result.output.decode()}")
@@ -266,7 +292,10 @@ def modify_blacklist_property(operation, users,
action="add"):
tar.addfile(info, io.BytesIO(data))
tarstream.seek(0)
-
container.put_archive(path="/opt/ranger/ranger-3.0.0-SNAPSHOT-kms/ews/webapp/WEB-INF/classes/conf/",
data=tarstream)
+ get_kms_container().put_archive(
+
path="/opt/ranger/ranger-3.0.0-SNAPSHOT-kms/ews/webapp/WEB-INF/classes/conf/",
+ data=tarstream,
+ )
print(f"Successfully {'added' if action == 'add' else 'removed'} {users}
in {prop_name}")
diff --git a/functional-tests/pytest.ini b/functional-tests/pytest.ini
index 2777e38c3..132c00b2c 100644
--- a/functional-tests/pytest.ini
+++ b/functional-tests/pytest.ini
@@ -29,6 +29,7 @@ markers =
api: public API v2 test cases
hive: Hive plugin enforcement (Docker HS2)
hbase: HBase plugin enforcement (Docker HBase shell)
+ hdfs_policy: HDFS Ranger plugin path policy enforcement (Docker dfs)
pythonpath = .
# No console logging during pytest; logger.info -> logs/pytest.log (tail -f
logs/pytest.log).
log_cli = false
diff --git a/functional-tests/ranger_test_utils/hdfs_utils.py
b/functional-tests/ranger_test_utils/hdfs_utils.py
new file mode 100644
index 000000000..b9bef7e5e
--- /dev/null
+++ b/functional-tests/ranger_test_utils/hdfs_utils.py
@@ -0,0 +1,301 @@
+# 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.
+
+"""HDFS Docker helpers for Ranger functional-tests (dfs + plugin
enforcement)."""
+
+import socket
+import subprocess
+import tempfile
+import time
+
+import pytest
+
+from ranger_test_utils.utils import (
+ POLICY_URL,
+ assert_http_ok,
+ get_test_logger,
+ perm_map_list_to_policy_items,
+)
+
+logger = get_test_logger(__name__)
+
+HDFS_SERVICE_NAME = "dev_hdfs"
+HADOOP_CONTAINER = "ranger-hadoop"
+KDC_CONTAINER = "ranger-kdc"
+ADMIN_CONTAINER = "ranger"
+
+KERBEROS_REALM = "EXAMPLE.COM"
+HDFS_SERVICE_USER = "hdfs"
+HDFS_SERVICE_PRINCIPAL = "hdfs/ranger-hadoop.rangernw@" + KERBEROS_REALM
+HDFS_SERVICE_KEYTAB = "/etc/keytabs/hdfs.keytab"
+
+USER_FINANCE = "hrt_21"
+USER_AUDIT = "hrt_22"
+GROUP_FINANCE = "finance"
+GROUP_AUDIT = "audit"
+
+HDFS_AUTHZ_DENIED_SUBSTR = "Permission denied"
+
+POLICY_PROPAGATION_SLEEP_SEC = 15
+
+_docker_client = None
+
+
+def get_docker_client():
+ global _docker_client
+ if _docker_client is None:
+ import docker
+
+ _docker_client = docker.from_env()
+ return _docker_client
+
+
+def _container_running(name):
+ client = get_docker_client()
+ try:
+ container = client.containers.get(name)
+ except Exception:
+ return False
+ container.reload()
+ return container.status == "running"
+
+
+def _namenode_rpc_port_open(host="127.0.0.1", port=9000, timeout=2.0):
+ """NameNode RPC — published by docker-compose.ranger-hadoop.yml
(9000:9000)."""
+ try:
+ with socket.create_connection((host, port), timeout=timeout):
+ return True
+ except OSError:
+ return False
+
+
+def preflight_hdfs_plugin_stack():
+ """Skip tests when Ranger Admin, KDC, or ranger-hadoop are unavailable."""
+ required = [ADMIN_CONTAINER, KDC_CONTAINER, HADOOP_CONTAINER]
+ missing = [name for name in required if not _container_running(name)]
+ if missing:
+ pytest.skip(
+ "HDFS plugin functional-tests need running containers: "
+ + ", ".join(missing)
+ + ". Run ./run-tests.sh postgres hdfs with
SELECTED_CONTAINERS=hadoop."
+ )
+
+ if not _namenode_rpc_port_open():
+ pytest.skip(
+ "HDFS NameNode RPC is not accepting connections on localhost:9000 "
+ "(is ranger-hadoop up and healthy? export
SELECTED_CONTAINERS=hadoop)."
+ )
+
+
+def _user_principal(username):
+ return username + "@" + KERBEROS_REALM
+
+
+def _user_keytab_in_hadoop_container(username):
+ return "/etc/keytabs/" + username + ".keytab"
+
+
+def ensure_unix_user_with_primary_group(container, username, primary_group):
+ """Create Linux user/group on ranger-hadoop for UGI group resolution."""
+ container.exec_run("groupadd -f " + primary_group, user="root")
+ exit_code, _ = container.exec_run("id -u " + username, user="root")
+ if exit_code == 0:
+ container.exec_run(
+ "usermod -g " + primary_group + " " + username,
+ user="root",
+ )
+ container.exec_run("usermod -aG hadoop " + username, user="root")
+ return
+
+ container.exec_run(
+ "useradd -m -s /bin/bash -g "
+ + primary_group
+ + " -G hadoop "
+ + username,
+ user="root",
+ )
+
+
+def ensure_kerberos_user_on_hadoop(username):
+ """Create user@REALM principal and install keytab on ranger-hadoop."""
+ kdc = get_docker_client().containers.get(KDC_CONTAINER)
+ hadoop = get_docker_client().containers.get(HADOOP_CONTAINER)
+ principal = _user_principal(username)
+ keytab_name = username + ".keytab"
+ kdc_tmp = "/tmp/" + keytab_name
+ dest_path = _user_keytab_in_hadoop_container(username)
+
+ exit_code, output = kdc.exec_run(
+ 'kadmin.local -q "getprinc ' + principal + '"',
+ user="root",
+ )
+ combined = (output or b"").decode()
+ if exit_code != 0 or "does not exist" in combined:
+ ec, out = kdc.exec_run(
+ 'kadmin.local -q "addprinc -randkey ' + principal + '"',
+ user="root",
+ )
+ if ec != 0:
+ raise RuntimeError(
+ "addprinc failed for " + principal + ": " + (out or
b"").decode()
+ )
+
+ kdc.exec_run("rm -f " + kdc_tmp, user="root")
+ ec, out = kdc.exec_run(
+ 'kadmin.local -q "xst -k ' + kdc_tmp + " " + principal + '"',
+ user="root",
+ )
+ if ec != 0:
+ raise RuntimeError(
+ "keytab export failed for " + principal + ": " + (out or
b"").decode()
+ )
+
+ local = tempfile.NamedTemporaryFile(delete=False, suffix=".keytab")
+ local.close()
+ try:
+ subprocess.check_call(
+ ["docker", "cp", KDC_CONTAINER + ":" + kdc_tmp, local.name]
+ )
+ subprocess.check_call(
+ ["docker", "cp", local.name, HADOOP_CONTAINER + ":" + dest_path]
+ )
+ finally:
+ import os
+
+ os.remove(local.name)
+
+ hadoop.exec_run("chmod 444 " + dest_path, user="root")
+ hadoop.exec_run("chown " + username + ":" + username + " " + dest_path,
user="root")
+ logger.info("Kerberos keytab installed for %s at %s", principal, dest_path)
+
+
+def _log_hdfs_output(exit_code, stdout, stderr, cmd):
+ logger.info("HDFS command exit_code=%s cmd=%s", exit_code, cmd)
+ for label, text in (("stdout", stdout), ("stderr", stderr)):
+ body = text if text else "(empty)"
+ logger.info("HDFS %s:", label)
+ for line in body.splitlines():
+ logger.info(" %s", line)
+
+
+def run_hdfs_dfs(
+ username,
+ dfs_subcommand,
+ service_user=False,
+ fail_on_error=True,
+):
+ """
+ Run `hdfs dfs <dfs_subcommand>` inside ranger-hadoop.
+
+ Returns (exit_code, combined_stdout_stderr).
+ """
+ hadoop = get_docker_client().containers.get(HADOOP_CONTAINER)
+ if service_user:
+ principal = HDFS_SERVICE_PRINCIPAL
+ keytab = HDFS_SERVICE_KEYTAB
+ exec_user = HDFS_SERVICE_USER
+ ranger_user_label = HDFS_SERVICE_USER
+ else:
+ principal = _user_principal(username)
+ keytab = _user_keytab_in_hadoop_container(username)
+ exec_user = username
+ ranger_user_label = username
+
+ cmd = "hdfs dfs " + dfs_subcommand
+ shell = (
+ "kdestroy -A 2>/dev/null || true; "
+ "kinit -kt "
+ + keytab
+ + " "
+ + principal
+ + "; "
+ + 'kinit_rc=$?; if [ "$kinit_rc" -ne 0 ]; then exit "$kinit_rc"; fi; '
+ + 'HADOOP_OPTS="-Dhadoop.security.authentication=kerberos" '
+ + cmd
+ )
+ logger.info(
+ "HDFS context: container=%s exec_user=%s ranger_user=%s",
+ HADOOP_CONTAINER,
+ exec_user,
+ ranger_user_label,
+ )
+ exit_code, output = hadoop.exec_run(["bash", "-c", shell], user=exec_user,
demux=True)
+ stdout = (output[0] or b"").decode()
+ stderr = (output[1] or b"").decode()
+ combined = stdout + stderr
+ _log_hdfs_output(exit_code, stdout, stderr, cmd)
+
+ if fail_on_error and exit_code != 0:
+ raise AssertionError(
+ "HDFS command failed (exit "
+ + str(exit_code)
+ + "): "
+ + cmd
+ + "\n"
+ + combined
+ )
+ return exit_code, combined
+
+
+def cleanup_hdfs_paths(paths):
+ """Remove HDFS paths if present (as hdfs service user)."""
+ for path in paths:
+ run_hdfs_dfs(
+ HDFS_SERVICE_USER,
+ "-rm -r -f " + path,
+ service_user=True,
+ fail_on_error=False,
+ )
+
+
+def build_hdfs_policy_payload(
+ policy_name,
+ path_values,
+ perm_map_list,
+ description="",
+ is_recursive=False,
+ is_audit_enabled=True,
+ service_name=HDFS_SERVICE_NAME,
+):
+ """JSON body for POST/PUT on /public/v2/api/policy/ (HDFS path
resource)."""
+ return {
+ "service": service_name,
+ "name": policy_name,
+ "description": description,
+ "isEnabled": True,
+ "isAuditEnabled": is_audit_enabled,
+ "resources": {
+ "path": {
+ "values": path_values,
+ "isExcludes": False,
+ "isRecursive": is_recursive,
+ }
+ },
+ "policyItems": perm_map_list_to_policy_items(perm_map_list),
+ }
+
+
+def create_hdfs_policy(session, payload):
+ response = session.post(POLICY_URL, json=payload)
+ assert_http_ok(response, (200, 201), "Create HDFS policy", POLICY_URL)
+ body = response.json()
+ policy_id = body.get("id")
+ assert policy_id is not None, "Create policy response missing id: " +
response.text
+ return policy_id
+
+
+def wait_for_hdfs_policy_propagation(seconds=POLICY_PROPAGATION_SLEEP_SEC):
+ logger.info("Waiting %ss for HDFS plugin policy propagation", seconds)
+ time.sleep(seconds)
diff --git a/functional-tests/ranger_test_utils/utils.py
b/functional-tests/ranger_test_utils/utils.py
index eb0931dbb..534c8bfac 100644
--- a/functional-tests/ranger_test_utils/utils.py
+++ b/functional-tests/ranger_test_utils/utils.py
@@ -246,6 +246,27 @@ def ensure_argus_policy_principals(session):
ensure_group_exists(session, group_name)
+def ensure_user_assigned_to_groups(session, username, group_names):
+ """Assign Ranger xuser group membership (idempotent for
functional-tests)."""
+ if not group_names:
+ return
+ url = XUSERS_BASE + "/users/userinfo"
+ payload = {
+ "xuserInfo": {"name": username},
+ "xgroupInfo": [{"name": group_name} for group_name in group_names],
+ }
+ response = session.post(url, json=payload)
+ assert_http_ok(
+ response,
+ 200,
+ "Assign groups "
+ + ",".join(group_names)
+ + " to user "
+ + username,
+ url,
+ )
+
+
# --- Hive public API v2 JSON builders ---
diff --git a/functional-tests/readme.md b/functional-tests/readme.md
index f9f1e3d22..c46bee8ec 100644
--- a/functional-tests/readme.md
+++ b/functional-tests/readme.md
@@ -24,7 +24,7 @@ A Docker-based test execution framework that validates Apache
Ranger service int
| Suite | Description |
|---|---|
-| **hdfs** | Test cases for HDFS encryption lifecycle using KMS |
+| **hdfs** | HDFS encryption lifecycle (KMS) and Ranger HDFS plugin path
policies (`hdfs_policy` marker) |
| **kms** | Test cases for KMS REST API functionality |
| **xuserrest** | Test cases for Ranger Admin User/Group REST APIs |
| **rolerest** | Test cases for Ranger Admin Role REST APIs |
@@ -52,6 +52,7 @@ functional-tests/
├── hbase/ # HBase plugin enforcement tests
├── ranger_test_utils/hive_utils.py
├── ranger_test_utils/hbase_utils.py
+├── ranger_test_utils/hdfs_utils.py
├── ranger_test_utils/access_audit_utils.py # For future access-audit asserts
│
├── ranger_test_utils/utils.py # Shared logging, HTTP asserts, Public API v2
helpers
@@ -127,6 +128,11 @@ The containers prompt always appears unless
`SELECTED_CONTAINERS` is exported.
**Examples:**
```text
+# Run hdfs suite (encryption + hdfs_policy tests) — requires ranger-hadoop
+export AUDIT_INDEX_STORE=none
+export SELECTED_CONTAINERS=hadoop
+./run-tests.sh postgres hdfs
+
# Run hdfs and kms tests — postgres db, no audit pipeline, hadoop container
only
export AUDIT_INDEX_STORE=none
export SELECTED_CONTAINERS=hadoop
@@ -153,7 +159,7 @@ All variables have sensible defaults and can be exported
before running or passe
| `CLEAN_CONTAINERS` | `0` | Set to `1` to wipe all containers and force a
full rebuild from scratch |
| `RUN_TESTS` | `1` | Set to `0` to bring up infrastructure only without
running any tests |
| `AUDIT_INDEX_STORE` | `opensearch` | Audit backend: `opensearch`, `solr`, or
`none` to skip the audit pipeline entirely |
-| `SELECTED_CONTAINERS` | _(prompted)_ | Space-separated optional containers
to bring up, e.g. `"hadoop hive"`. Set to `"all"` to bring up every optional
container. |
+| `SELECTED_CONTAINERS` | _(prompted)_ | Space-separated optional containers
to bring up, e.g. `"hadoop hive"`. Set to `"all"` to bring up every optional
container. **Required for the hdfs suite:** export `hadoop` or `all` (starts
`ranger-hadoop`). |
### `CLEAN_CONTAINERS`
@@ -228,6 +234,21 @@ Brought up based on what you enter at the containers
prompt (or `SELECTED_CONTAI
- Tests currently assert **HBase shell authorization output only**.
Access-audit helpers live in `ranger_test_utils/access_audit_utils.py` for a
follow-up once auditing works.
- Example: `./run-tests.sh postgres hbase`
+### HDFS suite notes (encryption + `hdfs_policy`)
+
+- Service repo for plugin tests is `dev_hdfs` (seeded by ranger-docker).
+- **`./run-tests.sh postgres hdfs` does not start `ranger-hadoop` by itself.**
Export `SELECTED_CONTAINERS=hadoop` or `SELECTED_CONTAINERS=all` before running
(or choose `hadoop` / `all` at the containers prompt).
+- `./run-tests.sh` runs the whole `hdfs/` folder (no marker filter). Use `-m
hdfs_policy` only for selective local runs.
+- Example:
+ ```text
+ export AUDIT_INDEX_STORE=none
+ export SELECTED_CONTAINERS=hadoop
+ ./run-tests.sh postgres hdfs
+ ```
+- Plugin tests only (skips encryption/KMS session setup in `hdfs/conftest.py`):
+ `pytest hdfs/test_hdfs_group_policy_03.py -m hdfs_policy`
+ Still requires `ranger-hadoop` (and Ranger Admin) running.
+
## Test Reports
After execution, an HTML report is automatically generated for each suite in
the `functional-tests/` directory: