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)

Reply via email to