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 49a657b9d RANGER-5835: Add Docker system tests for KMS DECRYPT_EEK on
HDFS EZ (#1306)
49a657b9d is described below
commit 49a657b9df090b74e167f24ab2adaedfd8f68f98
Author: Chinmay Hegde <[email protected]>
AuthorDate: Mon Oct 12 17:57:02 2026 +0530
RANGER-5835: Add Docker system tests for KMS DECRYPT_EEK on HDFS EZ (#1306)
* RANGER-5835: Add Docker system tests for kms decrypt_eek on hdfs
encryption zones
* RANGER-5835: Optimised test execution time and few nitpicks
---
functional-tests/hdfs/test_config.py | 2 +
functional-tests/hdfs/test_encryption02.py | 3 +-
functional-tests/hdfs/test_encryption04.py | 238 +++++++++++++++++++++++++++++
functional-tests/hdfs/utils.py | 41 +++--
functional-tests/kms/test_keyOps_policy.py | 18 ++-
5 files changed, 285 insertions(+), 17 deletions(-)
diff --git a/functional-tests/hdfs/test_config.py
b/functional-tests/hdfs/test_config.py
index c0024afaa..4df10d711 100644
--- a/functional-tests/hdfs/test_config.py
+++ b/functional-tests/hdfs/test_config.py
@@ -93,10 +93,12 @@
"hdfs-key",
"test-key1", "test-key2", "test-key3",
"cross-key", "cross-key2",
+ "decrypt-eek-key", # test_encryption04
]
# All HDFS encryption zone paths
TEST_EZ_PATHS = [
"/secure_zone",
"/secure_zone1",
"/secure_zone2",
+ "/decrypt_eek_zone", # test_encryption04
]
diff --git a/functional-tests/hdfs/test_encryption02.py
b/functional-tests/hdfs/test_encryption02.py
index 70f0c787c..75bd09c97 100644
--- a/functional-tests/hdfs/test_encryption02.py
+++ b/functional-tests/hdfs/test_encryption02.py
@@ -199,7 +199,8 @@ def test_Readfile_after_keyDeletion(hadoop_container):
# Use return_exit_code=True so stderr (where HDFS error messages live)
is included
# in output — required after demux=True split in run_command.
output, exit_code = run_command(hadoop_container, cmd, HIVE_USER,
fail_on_error=False, return_exit_code=True)
- print(f"Command Output:\n{output}")
+ error_lines = [line for line in output.splitlines() if
line.startswith("-cat:")]
+ print("Command Output:\n" + "\n".join(error_lines))
if exit_code != 0:
failure_detected = True
diff --git a/functional-tests/hdfs/test_encryption04.py
b/functional-tests/hdfs/test_encryption04.py
new file mode 100644
index 000000000..b140545cb
--- /dev/null
+++ b/functional-tests/hdfs/test_encryption04.py
@@ -0,0 +1,238 @@
+# 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.
+
+
+# Tests DECRYPT_EEK KMS policy enforcement on HDFS encryption zones.
+#
+# Policy under test (KMS policy scoped to the test key):
+# - hdfs is DENIED DECRYPT_EEK
+# - hrt21 is GRANTED DECRYPT_EEK
+#
+# Both writing into an EZ and reading from it need DECRYPT_EEK (the client
+# decrypts the file's EDEK), so hdfs can neither write nor read; hrt21 can do
+# both. The file inside the EZ is therefore written by hrt21.
+
+import time
+
+import pytest
+import requests
+
+from hdfs.utils import (
+ run_command,
+ run_kerberos_command,
+ ensure_kms_key,
+ create_encryption_zone,
+ cleanup_encryption_zone,
+ delete_kms_key,
+ ensure_hadoop_user_keytab,
+ ensure_ranger_user,
+ RANGER_AUTH,
+ RANGER_POLICY_URL,
+)
+from hdfs.test_config import HDFS_USER
+
+# ── Constants
──────────────────────────────────────────────────────────────────
+
+KMS_SERVICE_NAME = "dev_kms"
+POLICY_NAME = "pytest-decrypt-eek-policy"
+
+HRT21_USER = "hrt21"
+HRT21_PRINCIPAL = "hrt21/[email protected]"
+HRT21_KEYTAB = "/etc/keytabs/hrt21.keytab"
+
+EZ_KEY = "decrypt-eek-key"
+EZ_NAME = "decrypt_eek_zone"
+TEST_FILE = "decrypt_eek_testfile.txt"
+PLAIN_TEXT = "KMS DECRYPT_EEK policy enforcement test content"
+
+PLAIN_LOCAL_FILE = f"/tmp/{TEST_FILE}"
+PLAIN_HDFS_FILE = f"/tmp/{TEST_FILE}"
+DENIED_MSG = "not allowed to do 'DECRYPT_EEK'"
+
+# KMS plugin polls Ranger every 30s (ranger.plugin.kms.policy.pollIntervalMs)
+KMS_SYNC_TIMEOUT = 120
+KMS_SYNC_INTERVAL = 5
+
+
+# ── Helpers
────────────────────────────────────────────────────────────────────
+
+def _ensure_os_user(container, username):
+ exit_code, _ = container.exec_run(f"id -u {username}", user="root")
+ if exit_code != 0:
+ container.exec_run(f"useradd -m -s /bin/bash {username}", user="root")
+ container.exec_run(f"usermod -aG hadoop {username}", user="root")
+
+
+def _delete_test_policy():
+ resp = requests.get(
+ RANGER_POLICY_URL,
+ params={"serviceName": KMS_SERVICE_NAME, "policyName": POLICY_NAME},
+ auth=RANGER_AUTH,
+ timeout=30,
+ )
+ if resp.status_code != 200:
+ return
+ for policy in resp.json():
+ requests.delete(f"{RANGER_POLICY_URL}/{policy['id']}",
auth=RANGER_AUTH, timeout=30)
+
+
+def _create_test_policy():
+ """deny hdfs / allow hrt21 DECRYPT_EEK on EZ_KEY only (other keys
unaffected)."""
+ _delete_test_policy()
+ policy = {
+ "service": KMS_SERVICE_NAME,
+ "name": POLICY_NAME,
+ "isEnabled": True,
+ "isAuditEnabled": True,
+ "resources": {
+ "keyname": {"values": [EZ_KEY], "isExcludes": False,
"isRecursive": False}
+ },
+ "policyItems": [{
+ "accesses": [{"type": "decrypteek", "isAllowed": True}],
+ "users": [HRT21_USER],
+ "delegateAdmin": False,
+ }],
+ "denyPolicyItems": [{
+ "accesses": [{"type": "decrypteek", "isAllowed": True}],
+ "users": [HDFS_USER],
+ "delegateAdmin": False,
+ }],
+ }
+ resp = requests.post(RANGER_POLICY_URL, auth=RANGER_AUTH, json=policy,
timeout=30)
+ assert resp.status_code in (200, 201), \
+ f"Failed to create KMS policy: {resp.status_code} {resp.text}"
+
+
+def _wait_for_deny_enforced(container):
+ """Probe-copy into the EZ as hdfs until the KMS plugin enforces the
deny."""
+ probe = f"/{EZ_NAME}/_probe_{TEST_FILE}"
+ deadline = time.time() + KMS_SYNC_TIMEOUT
+ while True:
+ _, rc = run_command(
+ container,
+ f'hdfs dfs -cp "{PLAIN_HDFS_FILE}" "{probe}"',
+ HDFS_USER,
+ fail_on_error=False,
+ return_exit_code=True,
+ )
+ if rc != 0:
+ return
+ run_command(
+ container, f"hdfs dfs -rm -f -skipTrash {probe}", HDFS_USER,
fail_on_error=False
+ )
+ if time.time() > deadline:
+ pytest.fail(
+ f"KMS plugin did not enforce the DECRYPT_EEK deny for hdfs
within {KMS_SYNC_TIMEOUT}s"
+ )
+ time.sleep(KMS_SYNC_INTERVAL)
+
+
+# ── Module fixture
─────────────────────────────────────────────────────────────
+
[email protected](scope="module", autouse=True)
+def setup_decrypt_eek_env(hadoop_container):
+ # Provision hrt21: OS user -> Kerberos principal/keytab -> Ranger user
+ _ensure_os_user(hadoop_container, HRT21_USER)
+ ensure_hadoop_user_keytab(HRT21_USER)
+ ensure_ranger_user(HRT21_USER)
+
+ # Plain source file in HDFS (outside the EZ) to be copied into the EZ
+ run_command(hadoop_container, f'echo "{PLAIN_TEXT}" > {PLAIN_LOCAL_FILE}',
HDFS_USER)
+ run_command(
+ hadoop_container,
+ f'hdfs dfs -put -f "{PLAIN_LOCAL_FILE}" "{PLAIN_HDFS_FILE}"',
+ HDFS_USER,
+ )
+
+ # Key + EZ, writable by hrt21
+ ensure_kms_key(EZ_KEY)
+ create_encryption_zone(hadoop_container, EZ_NAME, EZ_KEY)
+ run_command(hadoop_container, f"hdfs dfs -chmod 777 /{EZ_NAME}", HDFS_USER)
+
+ _create_test_policy()
+ _wait_for_deny_enforced(hadoop_container)
+
+ yield
+
+ _delete_test_policy()
+ run_command(
+ hadoop_container, f"hdfs dfs -rm -f -skipTrash {PLAIN_HDFS_FILE}",
HDFS_USER,
+ fail_on_error=False,
+ )
+ cleanup_encryption_zone(hadoop_container, f"/{EZ_NAME}")
+ delete_kms_key(EZ_KEY)
+
+
+# ── Tests
───────────────────────────────────────────────────────────────────────
+
+# ****** ****************** Test Case 01
******************************************
+# ***** Encrypt a file when placed under encrypted zone; validate DECRYPT_EEK
policy.
+# *****
+# ***** Covers:
+# ***** - HDFS admin (denied DECRYPT_EEK) cannot copy into / read from the EZ
+# ***** - hrt21 (granted DECRYPT_EEK) can copy into / read from the EZ and
+# ***** sees the original plain text content
+#
*********************************************************************************
+
+def test_01_EncryptFile_case1A(hadoop_container):
+ encrypted_file = f"/{EZ_NAME}/{TEST_FILE}"
+
+ # Copy file to encrypted directory as admin -> denied
+ output, rc = run_command(
+ hadoop_container,
+ f'hdfs dfs -cp "{PLAIN_HDFS_FILE}" "{encrypted_file}"',
+ HDFS_USER,
+ fail_on_error=False,
+ return_exit_code=True,
+ )
+ assert rc != 0, "Admin should not be able to cp into EZ (denied
DECRYPT_EEK) but succeeded"
+ assert DENIED_MSG in output, f"Expected '{DENIED_MSG}' in output but got:
{output}"
+
+ # Copy file to encrypted directory as hrt21 -> allowed
+ output, rc = run_kerberos_command(
+ hadoop_container,
+ f'hdfs dfs -cp "{PLAIN_HDFS_FILE}" "{encrypted_file}"',
+ HRT21_USER,
+ HRT21_PRINCIPAL,
+ HRT21_KEYTAB,
+ fail_on_error=False,
+ return_exit_code=True,
+ )
+ assert DENIED_MSG not in output, f"hrt21 unexpectedly denied DECRYPT_EEK:
{output}"
+ assert rc == 0, f"hrt21 should be able to cp into EZ (granted DECRYPT_EEK)
but failed: {output}"
+
+ # Read encrypted file as admin -> denied
+ output, rc = run_command(
+ hadoop_container,
+ f'hdfs dfs -cat "{encrypted_file}"',
+ HDFS_USER,
+ fail_on_error=False,
+ return_exit_code=True,
+ )
+ assert rc != 0, "Admin should not be able to cat from EZ (denied
DECRYPT_EEK) but succeeded"
+ assert DENIED_MSG in output, f"Expected '{DENIED_MSG}' in output but got:
{output}"
+
+ # Read encrypted file as hrt21 -> plain text
+ output, rc = run_kerberos_command(
+ hadoop_container,
+ f'hdfs dfs -cat "{encrypted_file}"',
+ HRT21_USER,
+ HRT21_PRINCIPAL,
+ HRT21_KEYTAB,
+ fail_on_error=False,
+ return_exit_code=True,
+ )
+ assert rc == 0, f"hrt21 should be able to cat from EZ (granted
DECRYPT_EEK) but failed: {output}"
+ assert PLAIN_TEXT in output, f"Expected plain text '{PLAIN_TEXT}' in cat
output but got: {output}"
diff --git a/functional-tests/hdfs/utils.py b/functional-tests/hdfs/utils.py
index 3d22817d1..3728fda9e 100644
--- a/functional-tests/hdfs/utils.py
+++ b/functional-tests/hdfs/utils.py
@@ -39,7 +39,8 @@
"CREATE", "DELETE", "ROLLOVER", "GET", "GETKEYS", "GETMETADATA",
"GENERATEEEK", "DECRYPTEEK", "SETKEYMATERIAL",
]
-KMS_SITE_PATH =
"/opt/ranger/ranger-kms/ews/webapp/WEB-INF/classes/conf/kms-site.xml"
+KMS_HOME = "/opt/ranger/kms"
+KMS_SITE_PATH = f"{KMS_HOME}/ews/webapp/WEB-INF/classes/conf/kms-site.xml"
HADOOP_CONTAINER = "ranger-hadoop"
@@ -203,8 +204,28 @@ def ensure_ranger_user(username: str) -> None:
print(f"WARN: create Ranger user {username}: {resp.status_code}
{resp.text}")
+def _wait_for_kms_up(kms_container, timeout=180) -> None:
+ # Block until the KMS port accepts connections inside the container.
+ deadline = time.time() + timeout
+ while time.time() < deadline:
+ kms_container.reload()
+ if kms_container.status == "running":
+ ec, _ = kms_container.exec_run(
+ ["bash", "-c", "exec 3<>/dev/tcp/localhost/9292"], user="root"
+ )
+ if ec == 0:
+ return
+ time.sleep(3)
+ pytest.fail(f"ranger-kms did not come up within {timeout}s")
+
+
def ensure_kms_kerberos_rules(kms_container) -> bool:
# Patch KMS kerberos name rules at runtime. Returns True if KMS was
restarted.
+ kms_container.reload()
+ if kms_container.status != "running":
+ kms_container.start()
+ _wait_for_kms_up(kms_container)
+
exit_code, _ = kms_container.exec_run(
f"grep -q 'hive/ranger-hadoop' {KMS_SITE_PATH}", user="root"
)
@@ -219,11 +240,9 @@ def ensure_kms_kerberos_rules(kms_container) -> bool:
for rule in rules:
kms_container.exec_run(f"sed -i '/DEFAULT/i {rule}' {KMS_SITE_PATH}",
user="root")
- kms_container.exec_run(
- "/opt/ranger/ranger-kms/ranger-kms-services.sh restart", user="root"
- )
- print("Patched KMS kerberos rules, restarted ranger-kms.")
- time.sleep(30)
+ kms_container.restart()
+ _wait_for_kms_up(kms_container)
+ print("Patched KMS kerberos rules, restarted ranger-kms container.")
return True
@@ -260,17 +279,19 @@ def ensure_kms_hdfs_policy() -> None:
existing_users = set(policy_items[0].get("users") or [])
needed_users = set(HDFS_KMS_USERS)
- existing_access = {a["type"] for a in policy_items[0].get("accesses", [])}
+ # Ranger returns access types in lowercase; compare case-insensitively
+ existing_access = {a["type"].lower() for a in
policy_items[0].get("accesses", [])}
+ needed_access = {a.lower() for a in HDFS_KMS_ACCESSES}
- if needed_users.issubset(existing_users) and
set(HDFS_KMS_ACCESSES).issubset(existing_access):
+ if needed_users.issubset(existing_users) and
needed_access.issubset(existing_access):
print("KMS policy already grants required HDFS users and accesses.")
return
policy_items[0]["users"] = sorted(existing_users | needed_users)
for access_type in HDFS_KMS_ACCESSES:
- if access_type not in existing_access:
+ if access_type.lower() not in existing_access:
policy_items[0].setdefault("accesses", []).append(
- {"type": access_type, "isAllowed": True}
+ {"type": access_type.lower(), "isAllowed": True}
)
target["policyItems"] = policy_items
diff --git a/functional-tests/kms/test_keyOps_policy.py
b/functional-tests/kms/test_keyOps_policy.py
index 845049f11..e4964dca1 100644
--- a/functional-tests/kms/test_keyOps_policy.py
+++ b/functional-tests/kms/test_keyOps_policy.py
@@ -44,8 +44,15 @@ def setup_keytabs():
# create base policy
------------------------------------------------------------------
[email protected](scope="function", autouse=True)
[email protected](scope="module", autouse=True)
def create_initial_kms_policy():
+ """Create the pytest-* policy once for the entire module.
+
+ All test functions share the same policy_id and call update_kms_policy()
+ at the start of their body, which resets policyItems to [] before writing
+ the accesses needed for that specific test. This means there is no
+ cross-test state leakage even though the policy object is reused.
+ """
policy_name = "pytest-policy"
keyname_pattern = "pytest-*"
@@ -68,19 +75,18 @@ def create_initial_kms_policy():
"policyItems": []
}
- # Create policy
+ # Create policy once — shared across all tests in this module
response = requests.post(BASE_URL_RANGER, auth=RANGER_KMS_AUTH,
json=policy_data)
- #time.sleep(30)
time.sleep(5)
- if response.status_code != 200 and response.status_code != 201:
+ if response.status_code not in (200, 201):
raise Exception(f"Failed to create initial policy: {response.text}")
- created_policy = response.json()
- policy_id = created_policy["id"]
+ policy_id = response.json()["id"]
try:
yield policy_id
finally:
+ # Delete once at module teardown, not after every test
requests.delete(f"{BASE_URL_RANGER}/{policy_id}", auth=RANGER_KMS_AUTH)
@pytest.fixture(autouse=True)