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 3bc00377d RANGER-5862 - Updated the tests and did functional test 
refactoring (#1342)
3bc00377d is described below

commit 3bc00377db985c696548dd55af5a70dfd5445ac3
Author: harshal1172 <[email protected]>
AuthorDate: Fri Oct 30 16:27:22 2026 +0530

    RANGER-5862 - Updated the tests and did functional test refactoring (#1342)
---
 functional-tests/conftest.py                       |  15 +
 functional-tests/hbase/test_hbase_01.py            |  97 ++---
 functional-tests/hive/test_hive_01.py              | 160 +++-----
 .../ranger_test_utils/access_audit_utils.py        | 161 ++++++++-
 functional-tests/ranger_test_utils/hbase_utils.py  | 248 ++++++++++---
 functional-tests/ranger_test_utils/hive_utils.py   | 246 ++++++++++---
 .../ranger_test_utils/plugin_conftest.py           | 146 ++++++++
 .../ranger_test_utils/plugin_enforcement_utils.py  |  61 ++++
 .../ranger_test_utils/plugin_test_users.py         | 401 +++++++++++++++++++++
 functional-tests/ranger_test_utils/utils.py        |  43 ++-
 functional-tests/readme.md                         |   1 +
 11 files changed, 1313 insertions(+), 266 deletions(-)

diff --git a/functional-tests/conftest.py b/functional-tests/conftest.py
index 1d7c35856..79e415b48 100644
--- a/functional-tests/conftest.py
+++ b/functional-tests/conftest.py
@@ -20,6 +20,7 @@
 
 import pytest
 
+from ranger_test_utils.plugin_conftest import 
ensure_plugin_test_users_for_session
 from ranger_test_utils.utils import LOGS_DIR, ensure_logs_directory
 
 LOG_FORMAT = "%(asctime)s %(levelname)s [%(name)s] %(message)s"
@@ -38,6 +39,20 @@ def pytest_configure(config):
         config.option.log_file = None
 
 
[email protected](scope="session", autouse=True)
+def _ensure_plugin_test_users_for_enforcement_suites(request):
+    """
+    Before plugin enforcement tests (markers: hive, hbase, kafka, hdfs_policy),
+    create shared Ranger xusers and Kerberos keytabs via plugin_conftest.
+    REST-only suites (api_test, kms, …) are unchanged.
+    """
+    if not request.session.items:
+        yield
+        return
+    ensure_plugin_test_users_for_session(request.session)
+    yield
+
+
 @pytest.fixture(scope="module", autouse=True)
 def _log_to_module_file(request):
     """Write logger.info (and above) to logs/<test_file_name>.log for each 
test module."""
diff --git a/functional-tests/hbase/test_hbase_01.py 
b/functional-tests/hbase/test_hbase_01.py
index a6739f1db..735cb642c 100644
--- a/functional-tests/hbase/test_hbase_01.py
+++ b/functional-tests/hbase/test_hbase_01.py
@@ -14,13 +14,10 @@
 # limitations under the License.
 
 """
-Authorization is asserted via HBase shell output only. Access-audit asserts 
are deferred until
-HBase plugin audits are reliably indexed in Docker — use 
ranger_test_utils.access_audit_utils
-when ready.
+HBase plugin enforcement via HBase shell and optional access-audit validation.
 
-AUDIT_FILTERS (future):
-  test_01_01: accessType=createTable, accessResult=0, aclEnforcer=ranger-acl,
-              resourceName=<table>, repoName=dev_hbase, requestUser=hrt_21
+Set ``AUDIT_INDEX_STORE`` to ``opensearch`` or ``solr`` when starting Docker
+(see functional-tests readme) so access audits are indexed and asserted.
 """
 
 import pytest
@@ -28,35 +25,30 @@
 from ranger_test_utils.hbase_utils import (
     HBASE_AUTHZ_DENIED_SUBSTR,
     HBASE_SERVICE_NAME,
-    HBASE_TEST_USER,
+    HBasePluginTest,
+    HBasePluginTestContext,
     cleanup_hbase_test_table,
     create_audit_only_policy,
-    ensure_kerberos_user,
     preflight_hbase_stack,
     prepare_hbase_test_table,
-    run_hbase_shell,
     wait_for_hbase_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,
+from ranger_test_utils.plugin_enforcement_utils import (
     configure_test_logging,
     create_ranger_admin_session,
-    ensure_user_exists,
-    get_test_logger,
-    log_testcase_begin_for_pytest,
-    unique_suffix,
+    logger,
 )
 
 pytestmark = pytest.mark.hbase
 
-logger = get_test_logger(__name__)
+TEST_CASE = None
+
+HBASE_TEST_TABLE = "iemployee"
 
 _admin_session = None
 _audit_policy_id = None
 _audit_suffix = None
-_run_suffix = None
-_test_table = None
 
 
 def setup_module():
@@ -64,24 +56,18 @@ def setup_module():
     logger.info("=== setup_module: start ===")
     preflight_hbase_stack()
 
-    global _admin_session, _audit_policy_id, _audit_suffix
-    global _run_suffix, _test_table
+    global TEST_CASE, _admin_session, _audit_policy_id, _audit_suffix
+    TEST_CASE = HBasePluginTestContext()
     _admin_session = create_ranger_admin_session()
-    _run_suffix = unique_suffix(4)
-    _test_table = "iemployee_" + _run_suffix
-    logger.info("Test table: %s", _test_table)
-
-    logger.info("Ensuring Ranger user and Kerberos principal for %s", 
HBASE_TEST_USER)
-    ensure_user_exists(
-        _admin_session,
-        HBASE_TEST_USER,
-        password=DOCKER_PLUGIN_TEST_USER_PASSWORD,
-        last_name="hive_test",
+    TEST_CASE.admin_session = _admin_session
+
+    logger.info(
+        "Plugin user %s (Ranger + Kerberos) ensured by 
functional-tests/conftest.py",
+        HBasePluginTestContext.USER_USER1,
     )
-    ensure_kerberos_user(HBASE_TEST_USER)
 
-    logger.info("HBase setup: drop table %s if present", _test_table)
-    prepare_hbase_test_table(_test_table)
+    logger.info("HBase setup: drop table %s if present", HBASE_TEST_TABLE)
+    prepare_hbase_test_table(HBASE_TEST_TABLE)
 
     logger.info(
         "Disabling all policies on %s and creating audit-only policy",
@@ -99,9 +85,8 @@ def teardown_module():
         return
 
     logger.info("=== teardown_module: start ===")
-    if _test_table is not None:
-        logger.info("HBase cleanup: drop table %s", _test_table)
-        cleanup_hbase_test_table(_test_table)
+    logger.info("HBase cleanup: drop table %s", HBASE_TEST_TABLE)
+    cleanup_hbase_test_table(HBASE_TEST_TABLE)
 
     if _audit_policy_id is not None:
         logger.info("Deleting audit policy id=%s", _audit_policy_id)
@@ -113,26 +98,20 @@ def teardown_module():
     logger.info("=== teardown_module: done ===")
 
 
-def test_01_01_create_hbase_table_case1a_deny(request):
-    log_testcase_begin_for_pytest(logger, request.node.name, suite="HBASE")
-    create_cmd = (
-        "create '"
-        + _test_table
-        + "','personal','payroll','medical'"
-    )
-    logger.info(
-        "Running HBase CREATE TABLE as %s (expect deny): %s",
-        HBASE_TEST_USER,
-        create_cmd,
-    )
-    exit_code, output = run_hbase_shell(
-        HBASE_TEST_USER,
-        [create_cmd],
-        expect_substring=HBASE_AUTHZ_DENIED_SUBSTR,
-    )
-    assert HBASE_AUTHZ_DENIED_SUBSTR in output, (
-        "Expected authorization denial in HBase shell output; exit_code="
-        + str(exit_code)
-        + " output="
-        + output
-    )
+def test_01_01_CreateHbaseTableCase1A():
+    HBasePluginTest(
+        "HBASE.test_01_01_CreateHbaseTableCase1A",
+        TEST_CASE,
+        hBaseCmds=["create 'iemployee','personal','payroll','medical'"],
+        reqUser=HBasePluginTestContext.USER_USER1,
+        verifyCriteria=HBasePluginTest.STDOUT_CONTAINS,
+        verifyStr=HBASE_AUTHZ_DENIED_SUBSTR,
+        checkAuditLog=False,
+        newAuditLogMustExist=True,
+        auditLogFilter={
+            "accessType": TEST_CASE.access_type["CREATE_TABLE"],
+            "accessResult": TEST_CASE.result_denied,
+            "accessEnforcer": "ranger-acl",
+            "resourceName": "iemployee",
+        },
+    ).execute()
diff --git a/functional-tests/hive/test_hive_01.py 
b/functional-tests/hive/test_hive_01.py
index 525133963..73b5debcb 100644
--- a/functional-tests/hive/test_hive_01.py
+++ b/functional-tests/hive/test_hive_01.py
@@ -14,15 +14,10 @@
 # limitations under the License.
 
 """
-Authorization is asserted via beeline output only. Access-audit asserts are 
deferred until
-Hive plugin audits are reliably indexed in Docker — use 
ranger_test_utils.access_audit_utils
-when ready.
-
-AUDIT_FILTERS (future):
-  test_01_01: accessType=CREATE, accessResult=0, aclEnforcer=ranger-acl,
-              resourcePath=<db>/<table>, repoName=dev_hive, requestUser=hrt_21
-  test_01_02: accessType=USE, accessResult=0, aclEnforcer=ranger-acl,
-              resourcePath=<db>, repoName=dev_hive, requestUser=hrt_21
+Hive plugin enforcement via beeline and optional access-audit validation.
+
+Set ``AUDIT_INDEX_STORE`` to ``opensearch`` or ``solr`` when starting Docker
+(see functional-tests readme) so access audits are indexed and asserted.
 """
 
 import pytest
@@ -30,37 +25,28 @@
 from ranger_test_utils.hive_utils import (
     HIVE_AUTHZ_DENIED_SUBSTR,
     HIVE_SERVICE_NAME,
-    HIVE_TEST_USER,
-    cleanup_hive_test_namespace,
+    HivePluginTest,
+    HivePluginTestContext,
     create_audit_only_policy,
     delete_policies_by_id,
-    ensure_kerberos_user,
     preflight_hive_stack,
-    prepare_hive_test_namespace,
     run_beeline,
     set_all_policies_enabled,
     wait_for_policy_propagation,
 )
-from ranger_test_utils.utils import (
-    DOCKER_PLUGIN_TEST_USER_PASSWORD,
+from ranger_test_utils.plugin_enforcement_utils import (
     configure_test_logging,
     create_ranger_admin_session,
-    ensure_user_exists,
-    get_test_logger,
-    log_testcase_begin_for_pytest,
-    unique_suffix,
+    logger,
 )
 
 pytestmark = pytest.mark.hive
 
-logger = get_test_logger(__name__)
+TEST_CASE = None
 
 _admin_session = None
 _audit_policy_id = None
 _audit_suffix = None
-_run_suffix = None
-_test_database = None
-_test_table = None
 
 
 def setup_module():
@@ -68,34 +54,18 @@ def setup_module():
     logger.info("=== setup_module: start ===")
     preflight_hive_stack()
 
-    global _admin_session, _audit_policy_id, _audit_suffix
-    global _run_suffix, _test_database, _test_table
+    global TEST_CASE, _admin_session, _audit_policy_id, _audit_suffix
+    TEST_CASE = HivePluginTestContext()
     _admin_session = create_ranger_admin_session()
-    _run_suffix = unique_suffix(4)
-    _test_database = "db_" + _run_suffix
-    _test_table = "employee_" + _run_suffix
-    logger.info(
-        "Test resources: database=%s table=%s",
-        _test_database,
-        _test_table,
-    )
-
-    logger.info("Ensuring Ranger user and Kerberos principal for %s", 
HIVE_TEST_USER)
-    ensure_user_exists(
-        _admin_session,
-        HIVE_TEST_USER,
-        password=DOCKER_PLUGIN_TEST_USER_PASSWORD,
-        last_name="hive_test",
-    )
-    ensure_kerberos_user(HIVE_TEST_USER)
+    TEST_CASE.admin_session = _admin_session
 
     logger.info(
-        "Hive service setup: create database %s and drop table %s.%s if 
present",
-        _test_database,
-        _test_database,
-        _test_table,
+        "Plugin user %s (Ranger + Kerberos) ensured by 
functional-tests/conftest.py",
+        HivePluginTestContext.USER_USER1,
     )
-    prepare_hive_test_namespace(_test_database, _test_table)
+
+    logger.info("Hive service setup: drop default.employee if present")
+    run_beeline(None, "drop table if exists default.employee;", 
service_user=True)
 
     logger.info("Disabling all policies on %s and creating audit-only policy", 
HIVE_SERVICE_NAME)
     set_all_policies_enabled(_admin_session, False, HIVE_SERVICE_NAME)
@@ -110,15 +80,6 @@ def teardown_module():
         return
 
     logger.info("=== teardown_module: start ===")
-    if _test_database is not None and _test_table is not None:
-        logger.info(
-            "Hive service cleanup: drop %s.%s and database %s",
-            _test_database,
-            _test_table,
-            _test_database,
-        )
-        cleanup_hive_test_namespace(_test_database, _test_table)
-
     if _audit_policy_id is not None:
         logger.info("Deleting audit policy id=%s", _audit_policy_id)
         delete_policies_by_id(_admin_session, [_audit_policy_id])
@@ -129,49 +90,44 @@ def teardown_module():
     logger.info("=== teardown_module: done ===")
 
 
-def test_01_01_table_create_employee_no_policy_as_user1_deny(request):
-    log_testcase_begin_for_pytest(logger, request.node.name)
-    sql = (
-        "create table "
-        + _test_database
-        + "."
-        + _test_table
-        + "(name String);"
-    )
-    logger.info(
-        "Running beeline CREATE TABLE as %s (expect deny): %s",
-        HIVE_TEST_USER,
-        sql,
-    )
-    exit_code, output = run_beeline(
-        HIVE_TEST_USER,
-        sql,
-        expect_substring=HIVE_AUTHZ_DENIED_SUBSTR,
-    )
-    assert HIVE_AUTHZ_DENIED_SUBSTR in output, (
-        "Expected authorization denial in beeline output; exit_code="
-        + str(exit_code)
-        + " output="
-        + output
-    )
-
-
-def test_01_02_database_use_default_no_policy_as_user1_deny(request):
-    log_testcase_begin_for_pytest(logger, request.node.name)
-    sql = "use " + _test_database + ";"
-    logger.info(
-        "Running beeline USE database as %s (expect deny): %s",
-        HIVE_TEST_USER,
-        sql,
-    )
-    exit_code, output = run_beeline(
-        HIVE_TEST_USER,
-        sql,
-        expect_substring=HIVE_AUTHZ_DENIED_SUBSTR,
-    )
-    assert HIVE_AUTHZ_DENIED_SUBSTR in output, (
-        "Expected authorization denial in beeline output; exit_code="
-        + str(exit_code)
-        + " output="
-        + output
-    )
[email protected](
+    ("validateImpala", "verifyStr"),
+    [(False, HIVE_AUTHZ_DENIED_SUBSTR)],
+)
+def test_01_01_TableCreateEmployeeNoPolicyAsUser1Deny(validateImpala, 
verifyStr):
+    HivePluginTest(
+        "HIVE.test_01_01_TableCreateEmployeeNoPolicyAsUser1Deny",
+        TEST_CASE,
+        "create table default.employee(name String);",
+        HivePluginTestContext.USER_USER1,
+        HivePluginTestContext.PASS_USER1,
+        verifyCriteria=HivePluginTest.STDERR_CONTAINS,
+        verifyStr=verifyStr,
+        newAuditLogMustExist=True,
+        validateImpala=validateImpala,
+        auditLogFilter={
+            "accessType": TEST_CASE.access_type["CREATE"],
+            "accessResult": TEST_CASE.result_denied,
+            "accessEnforcer": "ranger-acl",
+            "resourceName": "default/employee",
+        },
+    ).execute()
+
+
+def test_01_02_DatabaseUseDefaultNoPolicyAsUser1Deny():
+    HivePluginTest(
+        "HIVE.test_01_02_DatabaseUseDefaultNoPolicyAsUser1Deny",
+        TEST_CASE,
+        "use default;",
+        HivePluginTestContext.USER_USER1,
+        HivePluginTestContext.PASS_USER1,
+        verifyCriteria=HivePluginTest.STDERR_CONTAINS,
+        verifyStr=HIVE_AUTHZ_DENIED_SUBSTR,
+        newAuditLogMustExist=True,
+        auditLogFilter={
+            "accessType": TEST_CASE.access_type["USE"],
+            "accessResult": TEST_CASE.result_denied,
+            "accessEnforcer": "ranger-acl",
+            "resourceName": "default",
+        },
+    ).execute()
diff --git a/functional-tests/ranger_test_utils/access_audit_utils.py 
b/functional-tests/ranger_test_utils/access_audit_utils.py
index c37d3f765..7e249eca4 100644
--- a/functional-tests/ranger_test_utils/access_audit_utils.py
+++ b/functional-tests/ranger_test_utils/access_audit_utils.py
@@ -16,16 +16,16 @@
 """
 Ranger Admin access-audit query helpers (Docker functional-tests).
 
-Hive plugin tests do not call these yet — Hive access audits are not reliably
-indexed in the default Docker stack. Wire into hive tests once audit pipeline 
works.
+Audits are indexed in Solr or OpenSearch when ``AUDIT_INDEX_STORE`` is not
+``none``; tests poll the Admin ``/assets/accessAudit`` REST API.
 """
 
-import logging
+import os
 import time
 
-from ranger_test_utils.utils import RANGER_ADMIN_BASE_URL
+from ranger_test_utils.utils import RANGER_ADMIN_BASE_URL, get_test_logger
 
-logger = logging.getLogger(__name__)
+logger = get_test_logger(__name__)
 
 ACCESS_AUDIT_PATH = RANGER_ADMIN_BASE_URL + "/assets/accessAudit"
 
@@ -121,6 +121,155 @@ def wait_for_access_audits(
 
 
 def assert_access_audits_found(session, **kwargs):
-    """Convenience assert for future hive audit tests."""
+    """Assert at least one access audit row matches the given filters."""
     audits = wait_for_access_audits(session, **kwargs)
     assert audits, "Expected access audit events matching filters: " + 
str(kwargs)
+
+
+def access_audit_pipeline_enabled():
+    """True when Docker was started with Solr/OpenSearch audit indexing."""
+    return os.environ.get("AUDIT_INDEX_STORE", "opensearch").lower() != "none"
+
+
+def audit_filters_to_query_kwargs(audit_filter, username, repo_name):
+    """Map plugin test ``audit_filter`` dict keys to 
``wait_for_access_audits`` kwargs."""
+    if not audit_filter:
+        return {}
+    return {
+        "request_user": audit_filter.get("request_user", username),
+        "repo_name": audit_filter.get("repoName", 
audit_filter.get("repo_name", repo_name)),
+        "access_type": audit_filter.get("accessType", 
audit_filter.get("access_type")),
+        "resource_path": audit_filter.get(
+            "resourceName", audit_filter.get("resource_path")
+        ),
+        "access_result": audit_filter.get(
+            "accessResult", audit_filter.get("access_result")
+        ),
+        "acl_enforcer": audit_filter.get(
+            "aclEnforcer", audit_filter.get("acl_enforcer")
+        ),
+        "resource_type": audit_filter.get(
+            "resourceType", audit_filter.get("resource_type")
+        ),
+        "max_iterations": audit_filter.get("max_iterations", 12),
+        "sleep_time": audit_filter.get("sleep_time", 5),
+    }
+
+
+def verify_access_audits_after_operation(
+    session,
+    audit_filter,
+    username,
+    repo_name,
+    *,
+    after_event_time,
+    expect_audit_event=True,
+):
+    """
+    Poll access audits newer than ``after_event_time``; assert 
presence/absence.
+    """
+    query_kwargs = audit_filters_to_query_kwargs(audit_filter, username, 
repo_name)
+    query_kwargs["after_event_time"] = after_event_time
+    audits = wait_for_access_audits(session, **query_kwargs)
+    if expect_audit_event:
+        assert audits, (
+            "Expected access audit events matching filters: " + 
str(query_kwargs)
+        )
+    else:
+        assert not audits, (
+            "Unexpected access audit events matching filters: " + 
str(query_kwargs)
+        )
+    return audits
+
+
+class AccessAuditCapture:
+    """
+    Baseline Solr/OpenSearch-backed access audits before a plugin client 
action.
+
+    Use from any enforcement suite (Hive, HBase, Kafka, HDFS):
+
+        capture = begin_access_audit_capture(admin_session, repo_name, 
request_user)
+        ... run beeline / hbase shell / kafka producer / hdfs dfs ...
+        capture.verify(audit_filter)
+    """
+
+    def __init__(self, session, repo_name, request_user):
+        self.session = session
+        self.repo_name = repo_name
+        self.request_user = request_user
+        self.enabled = (
+            session is not None
+            and access_audit_pipeline_enabled()
+        )
+        self.after_event_time = None
+        if self.enabled:
+            self.after_event_time = fetch_last_access_audit_event_time(session)
+            logger.info(
+                "Access audit baseline eventTime=%s repo=%s requestUser=%s",
+                self.after_event_time,
+                repo_name,
+                request_user,
+            )
+        elif session is not None and not access_audit_pipeline_enabled():
+            logger.info(
+                "Skipping access audit checks (AUDIT_INDEX_STORE=none); "
+                "use opensearch or solr when starting Docker to enable"
+            )
+
+    def verify(
+        self,
+        audit_filter,
+        *,
+        expect_audit_event=True,
+        request_user=None,
+        repo_name=None,
+    ):
+        """Assert access audit rows in Solr/OpenSearch (via Admin REST) after 
the action."""
+        if not audit_filter:
+            return None
+        if not self.enabled:
+            return None
+        user = request_user if request_user is not None else self.request_user
+        repo = repo_name if repo_name is not None else self.repo_name
+        logger.info("Verifying access audits: %s", audit_filter)
+        return verify_access_audits_after_operation(
+            self.session,
+            audit_filter,
+            user,
+            repo,
+            after_event_time=self.after_event_time,
+            expect_audit_event=expect_audit_event,
+        )
+
+
+def begin_access_audit_capture(session, repo_name, request_user):
+    """
+    Record newest access audit timestamp before a plugin enforcement action.
+
+    Returns an :class:`AccessAuditCapture` for ``verify()`` after the client 
runs.
+    """
+    return AccessAuditCapture(session, repo_name, request_user)
+
+
+def verify_plugin_access_audit(
+    session,
+    audit_filter,
+    *,
+    request_user,
+    repo_name,
+    after_event_time,
+    expect_audit_event=True,
+):
+    """
+    One-shot access audit verify when the baseline timestamp is already known.
+
+    Prefer :func:`begin_access_audit_capture` + ``capture.verify()`` for new 
tests.
+    """
+    return verify_access_audits_after_operation(
+        session,
+        audit_filter,
+        request_user,
+        repo_name,
+        after_event_time=after_event_time,
+        expect_audit_event=expect_audit_event,
+    )
diff --git a/functional-tests/ranger_test_utils/hbase_utils.py 
b/functional-tests/ranger_test_utils/hbase_utils.py
index 511f188fe..ba3fbf91c 100644
--- a/functional-tests/ranger_test_utils/hbase_utils.py
+++ b/functional-tests/ranger_test_utils/hbase_utils.py
@@ -13,7 +13,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""HBase Docker helpers for Ranger functional-tests (HBase shell + plugin 
enforcement)."""
+"""HBase Docker helpers and declarative plugin scenarios (shell, policies, 
``HBasePluginTest``)."""
 
 import base64
 import socket
@@ -21,18 +21,23 @@
 
 import pytest
 
+from ranger_test_utils.plugin_enforcement_utils import (
+    logger as plugin_enforcement_logger,
+    normalize_audit_log_filter,
+)
 from ranger_test_utils.utils import (
     POLICY_URL,
     assert_http_ok,
     get_test_logger,
+    log_testcase_begin,
     unique_suffix,
 )
 from ranger_test_utils.hive_utils import (
-    HIVE_TEST_USER,
     KDC_CONTAINER,
     POLICY_PROPAGATION_SLEEP_SEC,
     get_docker_client,
 )
+from ranger_test_utils.plugin_test_users import PLUGIN_TEST_USER
 
 logger = get_test_logger(__name__)
 
@@ -47,7 +52,7 @@
 HBASE_SERVICE_PRINCIPAL = "hbase/ranger-hbase.rangernw@" + KERBEROS_REALM
 HBASE_SERVICE_KEYTAB = "/etc/keytabs/hbase.keytab"
 
-HBASE_TEST_USER = HIVE_TEST_USER
+HBASE_TEST_USER = PLUGIN_TEST_USER
 
 HBASE_AUTHZ_DENIED_SUBSTR = "AccessDeniedException"
 
@@ -120,53 +125,12 @@ def _log_multiline_block(header, text, 
max_chars=HBASE_SHELL_LOG_OUTPUT_MAX_CHAR
 
 def ensure_kerberos_user(username):
     """Create a user Kerberos principal and keytab on KDC, visible in 
ranger-hbase."""
-    kdc = get_docker_client().containers.get(KDC_CONTAINER)
-    principal = _user_principal(username)
-    keytab_in_kdc = "/etc/keytabs/ranger-hbase/" + username + ".keytab"
-    keytab_in_hbase = _user_keytab_in_hbase_container(username)
-
-    logger.info(
-        "Kerberos setup on %s: addprinc/ktadd for %s -> %s (visible in hbase 
as %s)",
-        KDC_CONTAINER,
-        principal,
-        keytab_in_kdc,
-        keytab_in_hbase,
+    from ranger_test_utils.plugin_test_users import (
+        PLUGIN_KERBEROS_STACK_HBASE,
+        ensure_kerberos_keytab_for_stack,
     )
 
-    add_cmd = 'kadmin.local -q "addprinc -randkey ' + principal + '"'
-    exit_add, out_add = kdc.exec_run(add_cmd, user="root", demux=True)
-    _log_multiline_block(
-        "kadmin addprinc exit_code=%s" % exit_add,
-        ((out_add[0] or b"") + (out_add[1] or b"")).decode(),
-        max_chars=2000,
-    )
-
-    kdc.exec_run("rm -f " + keytab_in_kdc, user="root")
-    ktadd_cmd = (
-        'kadmin.local -q "ktadd -k '
-        + keytab_in_kdc
-        + " "
-        + principal
-        + '"'
-    )
-    exit_kt, out_kt = kdc.exec_run(ktadd_cmd, user="root", demux=True)
-    kt_out = ((out_kt[0] or b"") + (out_kt[1] or b"")).decode()
-    _log_multiline_block(
-        "kadmin ktadd exit_code=%s" % exit_kt,
-        kt_out,
-        max_chars=2000,
-    )
-    if exit_kt != 0:
-        raise RuntimeError(
-            "ktadd failed for "
-            + principal
-            + " (keytab "
-            + keytab_in_hbase
-            + "): "
-            + kt_out
-        )
-    kdc.exec_run("chmod 444 " + keytab_in_kdc, user="root")
-    verify_kerberos_keytab_in_hbase(username)
+    ensure_kerberos_keytab_for_stack(username, PLUGIN_KERBEROS_STACK_HBASE)
 
 
 def verify_kerberos_keytab_in_hbase(username):
@@ -355,3 +319,191 @@ def create_audit_only_policy(session, suffix=None, 
service_name=HBASE_SERVICE_NA
 def wait_for_hbase_policy_propagation(seconds=POLICY_PROPAGATION_SLEEP_SEC):
     logger.info("Waiting %ss for HBase plugin policy propagation", seconds)
     time.sleep(seconds)
+
+
+def execute_hbase_test(
+    shell_lines,
+    username=HBASE_TEST_USER,
+    *,
+    admin_session=None,
+    verify_output_contains=None,
+    verify_output_not_contains=None,
+    audit_filter=None,
+    expect_audit_event=True,
+    repo_name=HBASE_SERVICE_NAME,
+    service_user=False,
+):
+    """
+    Run HBase shell as ``username``, assert on output, optionally validate 
access audits.
+
+    Uses 
:func:`ranger_test_utils.access_audit_utils.begin_access_audit_capture`
+    (Solr/OpenSearch via Admin REST when the audit pipeline is enabled).
+    """
+    from ranger_test_utils.access_audit_utils import begin_access_audit_capture
+
+    if isinstance(shell_lines, str):
+        shell_lines = [shell_lines]
+
+    audit_capture = None
+    if (
+        audit_filter is not None
+        and admin_session is not None
+        and not service_user
+    ):
+        audit_capture = begin_access_audit_capture(
+            admin_session,
+            repo_name,
+            username,
+        )
+
+    exit_code, output = run_hbase_shell(
+        username,
+        shell_lines,
+        service_user=service_user,
+        expect_substring=verify_output_contains,
+    )
+
+    if verify_output_contains is not None:
+        assert verify_output_contains in output, (
+            "Expected text not found in HBase shell output; exit_code="
+            + str(exit_code)
+            + " output="
+            + output
+        )
+    if verify_output_not_contains is not None:
+        assert verify_output_not_contains not in output, (
+            "Unexpected text in HBase shell output; exit_code="
+            + str(exit_code)
+            + " output="
+            + output
+        )
+
+    if audit_capture is not None:
+        audit_capture.verify(
+            audit_filter,
+            expect_audit_event=expect_audit_event,
+        )
+
+    return exit_code, output
+
+
+class HBasePluginTestContext:
+    """
+    Per-module context (assign ``admin_session`` in ``setup_module``).
+
+    Class attributes mirror common ranger-qe ``XAHBaseTestCase`` fields used 
in ports.
+    """
+
+    USER_USER1 = HBASE_TEST_USER
+    USER_USER2 = "hrt_22"
+
+    result_denied = "0"
+    result_allowed = "1"
+
+    access_type = {
+        "CREATE_TABLE": "createTable",
+        "PUT": "put",
+        "SCAN": "scannerOpen",
+        "GET": "get",
+        "DISABLE_TABLE": "disableTable",
+        "DELETE_TABLE": "deleteTable",
+        "GRANT": "grant",
+        "REVOKE": "revoke",
+    }
+
+    def __init__(self):
+        self.admin_session = None
+        self.repo_name = HBASE_SERVICE_NAME
+
+
+class HBasePluginTest:
+    """One HBase enforcement scenario: log id, shell cmds, verify output, 
optional audit."""
+
+    STDERR_CONTAINS = "stderr_contains"
+    STDOUT_CONTAINS = "stdout_contains"
+    STDERR_NOT_CONTAINS = "stderr_not_contains"
+    STDOUT_NOT_CONTAINS = "stdout_not_contains"
+
+    def __init__(
+        self,
+        test_id,
+        test_case,
+        shell_cmds=None,
+        req_user=None,
+        *,
+        verify_criteria=STDOUT_CONTAINS,
+        verify_str=None,
+        new_audit_log_must_exist=True,
+        audit_log_filter=None,
+        logoutput=True,
+        verifyCriteria=None,
+        verifyStr=None,
+        newAuditLogMustExist=None,
+        auditLogFilter=None,
+        hBaseCmds=None,
+        reqUser=None,
+        checkAuditLog=None,
+    ):
+        self.test_id = test_id
+        self.test_case = test_case
+        if hBaseCmds is not None:
+            self.shell_cmds = hBaseCmds
+        elif shell_cmds is not None:
+            self.shell_cmds = shell_cmds
+        else:
+            raise ValueError("Provide shell_cmds or hBaseCmds")
+        self.req_user = reqUser if reqUser is not None else req_user
+        if self.req_user is None:
+            raise ValueError("Provide req_user or reqUser")
+        self.logoutput = logoutput
+
+        self.verify_criteria = (
+            verifyCriteria if verifyCriteria is not None else verify_criteria
+        )
+        self.verify_str = (
+            verifyStr
+            if verifyStr is not None
+            else (verify_str if verify_str is not None else 
HBASE_AUTHZ_DENIED_SUBSTR)
+        )
+        self.new_audit_log_must_exist = (
+            newAuditLogMustExist
+            if newAuditLogMustExist is not None
+            else new_audit_log_must_exist
+        )
+        self.audit_log_filter = (
+            auditLogFilter if auditLogFilter is not None else audit_log_filter
+        )
+        _ = checkAuditLog
+
+    def execute(self):
+        log_testcase_begin(plugin_enforcement_logger, self.test_id)
+
+        verify_contains = None
+        verify_not_contains = None
+        if self.verify_criteria in (
+            self.STDERR_CONTAINS,
+            self.STDOUT_CONTAINS,
+        ):
+            verify_contains = self.verify_str
+        elif self.verify_criteria in (
+            self.STDERR_NOT_CONTAINS,
+            self.STDOUT_NOT_CONTAINS,
+        ):
+            verify_not_contains = self.verify_str
+        else:
+            raise ValueError("Unsupported verify_criteria: " + 
str(self.verify_criteria))
+
+        admin_session = getattr(self.test_case, "admin_session", None)
+        repo_name = getattr(self.test_case, "repo_name", HBASE_SERVICE_NAME)
+        audit_filter = normalize_audit_log_filter(self.audit_log_filter)
+
+        execute_hbase_test(
+            self.shell_cmds,
+            username=self.req_user,
+            admin_session=admin_session,
+            verify_output_contains=verify_contains,
+            verify_output_not_contains=verify_not_contains,
+            audit_filter=audit_filter,
+            expect_audit_event=self.new_audit_log_must_exist,
+            repo_name=repo_name,
+        )
diff --git a/functional-tests/ranger_test_utils/hive_utils.py 
b/functional-tests/ranger_test_utils/hive_utils.py
index a6e408625..b939d9b43 100644
--- a/functional-tests/ranger_test_utils/hive_utils.py
+++ b/functional-tests/ranger_test_utils/hive_utils.py
@@ -13,19 +13,25 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""Hive Docker helpers for Ranger functional-tests (HS2 + plugin 
enforcement)."""
+"""Hive Docker helpers and declarative plugin scenarios (beeline, policies, 
``HivePluginTest``)."""
 
 import socket
 import time
 
 import pytest
 
+from ranger_test_utils.plugin_test_users import PLUGIN_TEST_USER
+from ranger_test_utils.plugin_enforcement_utils import (
+    logger as plugin_enforcement_logger,
+    normalize_audit_log_filter,
+)
 from ranger_test_utils.utils import (
     DOCKER_PLUGIN_TEST_USER_PASSWORD,
     POLICY_URL,
     RANGER_ADMIN_BASE_URL,
     assert_http_ok,
     get_test_logger,
+    log_testcase_begin,
     unique_suffix,
 )
 
@@ -47,7 +53,7 @@
     "jdbc:hive2://localhost:10000/default;principal=" + HIVE_HS2_PRINCIPAL
 )
 
-HIVE_TEST_USER = "hrt_21"
+HIVE_TEST_USER = PLUGIN_TEST_USER
 HIVE_TEST_USER_PASSWORD = DOCKER_PLUGIN_TEST_USER_PASSWORD
 
 HIVE_AUTHZ_DENIED_SUBSTR = "Permission denied"
@@ -152,8 +158,8 @@ def _log_text_block(title, text, 
max_chars=BEELINE_COMPLETE_LOG_MAX_CHARS):
         logger.info("%s", line)
 
 
-def _log_beeline_qe_style(exit_code, stdout, stderr, combined, sql, 
beeline_cmd, expect_substring=None):
-    """beeline logging: query, command, exit code, stdout, stderr."""
+def _log_beeline_run_detail(exit_code, stdout, stderr, combined, sql, 
beeline_cmd, expect_substring=None):
+    """Log beeline query, command, exit code, stdout, and stderr."""
     logger.info("Exit Code: %s", exit_code)
     _log_text_block("Output of beeline command is:", stdout)
     _log_text_block("Error of beeline command is:", stderr)
@@ -190,53 +196,12 @@ def _log_beeline_qe_style(exit_code, stdout, stderr, 
combined, sql, beeline_cmd,
 
 def ensure_kerberos_user(username):
     """Create a user Kerberos principal and keytab on KDC, visible in 
ranger-hive."""
-    kdc = get_docker_client().containers.get(KDC_CONTAINER)
-    principal = _user_principal(username)
-    keytab_in_kdc = "/etc/keytabs/ranger-hive/" + username + ".keytab"
-    keytab_in_hive = _user_keytab_in_hive_container(username)
-
-    logger.info(
-        "Kerberos setup on %s: addprinc/ktadd for %s -> %s (visible in hive as 
%s)",
-        KDC_CONTAINER,
-        principal,
-        keytab_in_kdc,
-        keytab_in_hive,
-    )
-
-    add_cmd = 'kadmin.local -q "addprinc -randkey ' + principal + '"'
-    exit_add, out_add = kdc.exec_run(add_cmd, user="root", demux=True)
-    _log_multiline_block(
-        "kadmin addprinc exit_code=%s" % exit_add,
-        ((out_add[0] or b"") + (out_add[1] or b"")).decode(),
-        max_chars=2000,
+    from ranger_test_utils.plugin_test_users import (
+        PLUGIN_KERBEROS_STACK_HIVE,
+        ensure_kerberos_keytab_for_stack,
     )
 
-    kdc.exec_run("rm -f " + keytab_in_kdc, user="root")
-    ktadd_cmd = (
-        'kadmin.local -q "ktadd -k '
-        + keytab_in_kdc
-        + " "
-        + principal
-        + '"'
-    )
-    exit_kt, out_kt = kdc.exec_run(ktadd_cmd, user="root", demux=True)
-    kt_out = ((out_kt[0] or b"") + (out_kt[1] or b"")).decode()
-    _log_multiline_block(
-        "kadmin ktadd exit_code=%s" % exit_kt,
-        kt_out,
-        max_chars=2000,
-    )
-    if exit_kt != 0:
-        raise RuntimeError(
-            "ktadd failed for "
-            + principal
-            + " (keytab "
-            + keytab_in_hive
-            + "): "
-            + kt_out
-        )
-    kdc.exec_run("chmod 444 " + keytab_in_kdc, user="root")
-    verify_kerberos_keytab_in_hive(username)
+    ensure_kerberos_keytab_for_stack(username, PLUGIN_KERBEROS_STACK_HIVE)
 
 
 def verify_kerberos_keytab_in_hive(username):
@@ -356,7 +321,7 @@ def run_beeline(username, sql, service_user=False, 
expect_substring=None):
     stderr = (output[1] or b"").decode()
     combined = stdout + stderr
 
-    _log_beeline_qe_style(
+    _log_beeline_run_detail(
         exit_code,
         stdout,
         stderr,
@@ -467,3 +432,184 @@ def delete_policies_by_id(session, policy_ids):
 def wait_for_policy_propagation(seconds=POLICY_PROPAGATION_SLEEP_SEC):
     logger.info("Waiting %ss for Hive plugin policy propagation", seconds)
     time.sleep(seconds)
+
+
+def execute_hive_test(
+    sql,
+    username=HIVE_TEST_USER,
+    *,
+    admin_session=None,
+    verify_output_contains=None,
+    verify_output_not_contains=None,
+    audit_filter=None,
+    expect_audit_event=True,
+    repo_name=HIVE_SERVICE_NAME,
+):
+    """
+    Run beeline as ``username``, assert on CLI output, optionally validate 
access audits.
+
+    Uses 
:func:`ranger_test_utils.access_audit_utils.begin_access_audit_capture`
+    for Solr/OpenSearch-backed audit checks (shared with other plugin suites).
+    """
+    from ranger_test_utils.access_audit_utils import begin_access_audit_capture
+
+    audit_capture = None
+    if audit_filter is not None and admin_session is not None:
+        audit_capture = begin_access_audit_capture(
+            admin_session,
+            repo_name,
+            username,
+        )
+
+    exit_code, output = run_beeline(
+        username,
+        sql,
+        expect_substring=verify_output_contains,
+    )
+
+    if verify_output_contains is not None:
+        assert verify_output_contains in output, (
+            "Expected text not found in beeline output; exit_code="
+            + str(exit_code)
+            + " output="
+            + output
+        )
+    if verify_output_not_contains is not None:
+        assert verify_output_not_contains not in output, (
+            "Unexpected text in beeline output; exit_code="
+            + str(exit_code)
+            + " output="
+            + output
+        )
+
+    if audit_capture is not None:
+        audit_capture.verify(
+            audit_filter,
+            expect_audit_event=expect_audit_event,
+        )
+
+    return exit_code, output
+
+
+class HivePluginTestContext:
+    """
+    Per-module context (assign ``admin_session`` in ``setup_module``).
+
+    Class attributes mirror common ranger-qe ``XAHiveTestCase`` fields used in 
ports.
+    """
+
+    USER_USER1 = HIVE_TEST_USER
+    USER_USER2 = "hrt_22"
+    PASS_USER1 = None
+    PASS_USER2 = None
+
+    result_denied = "0"
+    result_allowed = "1"
+
+    access_type = {
+        "CREATE": "CREATE",
+        "USE": "USE",
+        "DROP": "DROP",
+        "SELECT": "SELECT",
+        "UPDATE": "UPDATE",
+        "ALTER": "ALTER",
+        "INSERT": "INSERT",
+        "DELETE": "DELETE",
+        "GRANT": "GRANT",
+        "REVOKE": "REVOKE",
+    }
+
+    def __init__(self):
+        self.admin_session = None
+        self.repo_name = HIVE_SERVICE_NAME
+
+
+class HivePluginTest:
+    """One Hive enforcement scenario: log id, beeline SQL, verify output, 
optional audit."""
+
+    STDERR_CONTAINS = "stderr_contains"
+    STDOUT_CONTAINS = "stdout_contains"
+    STDERR_NOT_CONTAINS = "stderr_not_contains"
+    STDOUT_NOT_CONTAINS = "stdout_not_contains"
+
+    def __init__(
+        self,
+        test_id,
+        test_case,
+        sql,
+        req_user,
+        req_user_pass=None,
+        *,
+        verify_criteria=STDERR_CONTAINS,
+        verify_str=None,
+        new_audit_log_must_exist=True,
+        audit_log_filter=None,
+        validate_impala=False,
+        verifyCriteria=None,
+        verifyStr=None,
+        newAuditLogMustExist=None,
+        auditLogFilter=None,
+        validateImpala=None,
+        hSql=None,
+    ):
+        self.test_id = test_id
+        self.test_case = test_case
+        self.sql = hSql if hSql is not None else sql
+        self.req_user = req_user
+        self.req_user_pass = req_user_pass
+
+        self.verify_criteria = (
+            verifyCriteria if verifyCriteria is not None else verify_criteria
+        )
+        self.verify_str = (
+            verifyStr
+            if verifyStr is not None
+            else (verify_str if verify_str is not None else 
HIVE_AUTHZ_DENIED_SUBSTR)
+        )
+        self.new_audit_log_must_exist = (
+            newAuditLogMustExist
+            if newAuditLogMustExist is not None
+            else new_audit_log_must_exist
+        )
+        self.audit_log_filter = (
+            auditLogFilter if auditLogFilter is not None else audit_log_filter
+        )
+        self.validate_impala = (
+            validateImpala if validateImpala is not None else validate_impala
+        )
+
+    def execute(self):
+        if self.validate_impala:
+            pytest.skip("Impala validation is not enabled in Docker 
functional-tests")
+
+        log_testcase_begin(plugin_enforcement_logger, self.test_id)
+
+        verify_contains = None
+        verify_not_contains = None
+        if self.verify_criteria in (
+            self.STDERR_CONTAINS,
+            self.STDOUT_CONTAINS,
+        ):
+            verify_contains = self.verify_str
+        elif self.verify_criteria in (
+            self.STDERR_NOT_CONTAINS,
+            self.STDOUT_NOT_CONTAINS,
+        ):
+            verify_not_contains = self.verify_str
+        else:
+            raise ValueError("Unsupported verify_criteria: " + 
str(self.verify_criteria))
+
+        admin_session = getattr(self.test_case, "admin_session", None)
+        repo_name = getattr(self.test_case, "repo_name", HIVE_SERVICE_NAME)
+        audit_filter = normalize_audit_log_filter(self.audit_log_filter)
+
+        execute_hive_test(
+            self.sql,
+            username=self.req_user,
+            admin_session=admin_session,
+            verify_output_contains=verify_contains,
+            verify_output_not_contains=verify_not_contains,
+            audit_filter=audit_filter,
+            expect_audit_event=self.new_audit_log_must_exist,
+            repo_name=repo_name,
+        )
diff --git a/functional-tests/ranger_test_utils/plugin_conftest.py 
b/functional-tests/ranger_test_utils/plugin_conftest.py
new file mode 100644
index 000000000..7cbcd7910
--- /dev/null
+++ b/functional-tests/ranger_test_utils/plugin_conftest.py
@@ -0,0 +1,146 @@
+# 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.
+
+"""
+Session setup for Docker plugin enforcement suites (Hive, HDFS, HBase, Kafka).
+
+Used by ``functional-tests/conftest.py`` when collected tests carry a 
registered
+plugin marker. Suites can also call ``ensure_plugin_test_users_for_markers`` 
from
+``setup_module`` if they need explicit control.
+"""
+
+import sys
+
+import pytest
+import requests
+
+from ranger_test_utils.plugin_test_users import (
+    PLUGIN_KERBEROS_STACK_HADOOP,
+    PLUGIN_KERBEROS_STACK_HBASE,
+    PLUGIN_KERBEROS_STACK_HIVE,
+    PLUGIN_KERBEROS_STACK_KAFKA,
+    ensure_plugin_test_users,
+)
+from ranger_test_utils.utils import (
+    create_ranger_admin_session,
+    get_test_logger,
+    preflight_ranger_admin_http,
+)
+
+logger = get_test_logger(__name__)
+
+
+def _plugin_setup_progress(message):
+    """Visible on console during session setup (log_cli is off until module 
tests run)."""
+    print("[plugin-setup] " + message, file=sys.stderr, flush=True)
+
+# pytest marker name -> Kerberos stacks to provision for that suite
+PLUGIN_MARKER_KERBEROS_STACKS = {
+    "hive": (PLUGIN_KERBEROS_STACK_HIVE,),
+    "hbase": (PLUGIN_KERBEROS_STACK_HBASE,),
+    "kafka": (PLUGIN_KERBEROS_STACK_KAFKA,),
+    "hdfs_policy": (PLUGIN_KERBEROS_STACK_HADOOP,),
+}
+
+PLUGIN_ENFORCEMENT_MARKERS = frozenset(PLUGIN_MARKER_KERBEROS_STACKS.keys())
+
+
+def _preflight_for_marker(marker_name):
+    if marker_name == "hive":
+        from ranger_test_utils.hive_utils import preflight_hive_stack
+
+        preflight_hive_stack()
+    elif marker_name == "hbase":
+        from ranger_test_utils.hbase_utils import preflight_hbase_stack
+
+        preflight_hbase_stack()
+    elif marker_name == "kafka":
+        from ranger_test_utils.kafka_utils import preflight_kafka_stack
+
+        preflight_kafka_stack()
+    elif marker_name == "hdfs_policy":
+        from ranger_test_utils.hdfs_utils import preflight_hdfs_plugin_stack
+
+        preflight_hdfs_plugin_stack()
+
+
+def markers_present_in_session(session):
+    """Return plugin enforcement marker names present on collected tests."""
+    found = set()
+    for item in session.items:
+        for marker_name in PLUGIN_ENFORCEMENT_MARKERS:
+            if item.get_closest_marker(marker_name) is not None:
+                found.add(marker_name)
+    return found
+
+
+def kerberos_stacks_for_markers(marker_names):
+    """Merge Kerberos stack tuples for the given marker names (stable 
order)."""
+    stacks = []
+    seen = set()
+    for marker_name in sorted(marker_names):
+        for stack in PLUGIN_MARKER_KERBEROS_STACKS.get(marker_name, ()):
+            if stack not in seen:
+                seen.add(stack)
+                stacks.append(stack)
+    return tuple(stacks)
+
+
+def ensure_plugin_test_users_for_markers(marker_names):
+    """
+    Run stack preflights, then Ranger xusers + Kerberos for merged stacks.
+
+    Safe to call from pytest session setup or from a suite ``setup_module``.
+    """
+    marker_names = set(marker_names)
+    if not marker_names:
+        return
+
+    _plugin_setup_progress(
+        "starting for markers: " + ", ".join(sorted(marker_names))
+    )
+
+    for marker_name in sorted(marker_names):
+        logger.info("Plugin preflight for marker: %s", marker_name)
+        _plugin_setup_progress("preflight: " + marker_name)
+        _preflight_for_marker(marker_name)
+
+    _plugin_setup_progress("checking Ranger Admin HTTP")
+    preflight_ranger_admin_http()
+
+    stacks = kerberos_stacks_for_markers(marker_names)
+    _plugin_setup_progress(
+        "Ranger xusers + Kerberos for stacks: " + ", ".join(stacks)
+    )
+    logger.info(
+        "Ensuring plugin test users (Ranger + Kerberos) for stacks: %s",
+        stacks,
+    )
+    try:
+        session = create_ranger_admin_session()
+        ensure_plugin_test_users(session, kerberos_stacks=stacks)
+    except (requests.RequestException, RuntimeError) as exc:
+        pytest.skip("Plugin user setup failed: " + str(exc))
+
+    _plugin_setup_progress("done")
+    logger.info("Plugin test users ready for markers: %s", 
sorted(marker_names))
+
+
+def ensure_plugin_test_users_for_session(pytest_session):
+    """If any plugin enforcement tests were collected, provision shared 
users."""
+    marker_names = markers_present_in_session(pytest_session)
+    if not marker_names:
+        return
+    ensure_plugin_test_users_for_markers(marker_names)
diff --git a/functional-tests/ranger_test_utils/plugin_enforcement_utils.py 
b/functional-tests/ranger_test_utils/plugin_enforcement_utils.py
new file mode 100644
index 000000000..5dc09d575
--- /dev/null
+++ b/functional-tests/ranger_test_utils/plugin_enforcement_utils.py
@@ -0,0 +1,61 @@
+# 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 logging, access-audit checks, and pytest helpers for plugin enforcement 
suites.
+
+Test modules should import ``logger`` from here (same pattern as 
``api_test_utils``
+for ``api_tests``).
+
+Access audits (Solr/OpenSearch via Admin REST) for any service::
+
+    from ranger_test_utils.plugin_enforcement_utils import (
+        begin_access_audit_capture,
+        logger,
+    )
+
+    capture = begin_access_audit_capture(admin_session, "dev_hbase", "hrt_21")
+    ... hbase shell / kafka producer / hdfs dfs ...
+    capture.verify({"accessType": "...", "accessResult": "0", ...})
+"""
+
+from ranger_test_utils.access_audit_utils import (
+    AccessAuditCapture,
+    access_audit_pipeline_enabled,
+    assert_access_audits_found,
+    begin_access_audit_capture,
+    fetch_last_access_audit_event_time,
+    verify_plugin_access_audit,
+    wait_for_access_audits,
+)
+from ranger_test_utils.utils import (
+    configure_test_logging,
+    create_ranger_admin_session,
+    get_test_logger,
+    log_testcase_begin_for_pytest,
+    unique_suffix,
+)
+
+logger = get_test_logger(__name__)
+
+
+def normalize_audit_log_filter(audit_log_filter):
+    """Map ranger-qe ``accessEnforcer`` to functional-tests ``aclEnforcer``."""
+    if not audit_log_filter:
+        return None
+    normalized = dict(audit_log_filter)
+    if "accessEnforcer" in normalized and "aclEnforcer" not in normalized:
+        normalized["aclEnforcer"] = normalized.pop("accessEnforcer")
+    return normalized
diff --git a/functional-tests/ranger_test_utils/plugin_test_users.py 
b/functional-tests/ranger_test_utils/plugin_test_users.py
new file mode 100644
index 000000000..7c40d5e77
--- /dev/null
+++ b/functional-tests/ranger_test_utils/plugin_test_users.py
@@ -0,0 +1,401 @@
+# 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 Ranger xusers and Kerberos principals for Docker plugin enforcement 
suites.
+
+Call ``ensure_plugin_test_users`` from suite ``conftest.py`` (session scope) or
+``setup_module`` before service-specific policy setup. Kerberos uses ``ktadd
+-norandkey`` on KDC volume paths so the same principal can receive keytabs on
+multiple stacks without invalidating existing keytabs.
+"""
+
+import subprocess
+import sys
+import tempfile
+
+from ranger_test_utils.utils import (
+    ARGUS_POLICY_USERS,
+    DOCKER_PLUGIN_TEST_USER_PASSWORD,
+    ensure_group_exists,
+    ensure_user_assigned_to_groups,
+    ensure_user_exists,
+    get_test_logger,
+)
+
+logger = get_test_logger(__name__)
+
+KDC_CONTAINER = "ranger-kdc"
+HADOOP_CONTAINER = "ranger-hadoop"
+KERBEROS_REALM = "EXAMPLE.COM"
+
+# Primary enforcement user (Hive/HBase tests); second user used by HDFS/Kafka 
deny cases.
+PLUGIN_TEST_USER = ARGUS_POLICY_USERS[0]
+PLUGIN_TEST_USER_ALT = ARGUS_POLICY_USERS[1]
+
+# Policy / multi-user Hive-HDFS scenarios in ranger-qe (hrt_1 .. hrt_10).
+PLUGIN_HRT_USERS_1_TO_10 = ["hrt_" + str(i) for i in range(1, 11)]
+
+# Ranger xusers provisioned on QE clusters (ansible create-test-users role 
test_users).
+# Source: ranger-qe/.qaas/ansible/roles/create-test-users/vars/main.yml
+PLUGIN_RANGER_CLUSTER_TEST_USERS = [
+    "hiveuser",
+    "hrt_21",
+    "hrt_22",
+    "hivetest",
+    "impala",
+    "hbaseuser",
+    "hrt_qa",
+]
+
+
+def _unique_usernames_in_order(names):
+    seen = set()
+    result = []
+    for name in names:
+        if name not in seen:
+            seen.add(name)
+            result.append(name)
+    return result
+
+
+# Default Ranger xusers for plugin session setup: hrt_1..10 plus QE cluster 
test_users.
+PLUGIN_RANGER_DEFAULT_TEST_USERS = _unique_usernames_in_order(
+    PLUGIN_HRT_USERS_1_TO_10 + PLUGIN_RANGER_CLUSTER_TEST_USERS
+)
+
+# Subset that need Kerberos keytabs for current Docker plugin enforcement 
suites.
+PLUGIN_KERBEROS_TEST_USERS = list(ARGUS_POLICY_USERS)
+
+# Ranger group membership aligned with QE ranger_test_users_groups (hrt_21 / 
hrt_22).
+PLUGIN_RANGER_USER_GROUP_MAP = {
+    "hrt_21": ["finance"],
+    "hrt_22": ["audit"],
+}
+
+# Kerberos stacks plugin suites can request (see ``KDC_VOLUME_KEYTAB_DIRS`` / 
hadoop).
+PLUGIN_KERBEROS_STACK_HIVE = "hive"
+PLUGIN_KERBEROS_STACK_HBASE = "hbase"
+PLUGIN_KERBEROS_STACK_KAFKA = "kafka"
+PLUGIN_KERBEROS_STACK_HADOOP = "hadoop"
+
+PLUGIN_KERBEROS_STACKS_ALL = (
+    PLUGIN_KERBEROS_STACK_HIVE,
+    PLUGIN_KERBEROS_STACK_HBASE,
+    PLUGIN_KERBEROS_STACK_KAFKA,
+    PLUGIN_KERBEROS_STACK_HADOOP,
+)
+
+# KDC paths are volume-mounted into service containers as 
/etc/keytabs/<user>.keytab
+KDC_VOLUME_KEYTAB_DIRS = {
+    PLUGIN_KERBEROS_STACK_HIVE: "/etc/keytabs/ranger-hive",
+    PLUGIN_KERBEROS_STACK_HBASE: "/etc/keytabs/ranger-hbase",
+    PLUGIN_KERBEROS_STACK_KAFKA: "/etc/keytabs/ranger-kafka",
+}
+
+_docker_client = None
+
+
+def get_docker_client():
+    global _docker_client
+    if _docker_client is None:
+        import docker
+
+        _docker_client = docker.from_env(timeout=10)
+        try:
+            _docker_client.ping()
+        except Exception as exc:
+            raise RuntimeError(
+                "Docker is not available (is the daemon running?): " + str(exc)
+            ) from exc
+    return _docker_client
+
+
+def user_kerberos_principal(username):
+    return username + "@" + KERBEROS_REALM
+
+
+def user_keytab_in_service_container(username):
+    return "/etc/keytabs/" + username + ".keytab"
+
+
+def ensure_plugin_ranger_groups(session):
+    """Create finance/audit groups and assign hrt_21 / hrt_22 like QE cluster 
seed."""
+    for group_name in ("finance", "audit"):
+        ensure_group_exists(session, group_name)
+    for username, group_names in PLUGIN_RANGER_USER_GROUP_MAP.items():
+        ensure_user_assigned_to_groups(session, username, group_names)
+
+
+def ensure_plugin_test_users(
+    session,
+    usernames=None,
+    kerberos_usernames=None,
+    kerberos_stacks=None,
+    ranger_password=DOCKER_PLUGIN_TEST_USER_PASSWORD,
+    verify_keytabs=True,
+    ensure_groups=True,
+):
+    """
+    Create Ranger xusers and Kerberos keytabs for plugin enforcement tests.
+
+    Parameters
+    ----------
+    session:
+        Ranger Admin ``requests.Session`` from ``create_ranger_admin_session``.
+    usernames:
+        Ranger user names to ensure (default: 
``PLUGIN_RANGER_DEFAULT_TEST_USERS``:
+        ``hrt_1``..``hrt_10`` plus ranger-qe ansible ``test_users``).
+    kerberos_usernames:
+        Users to receive keytabs (default: ``PLUGIN_KERBEROS_TEST_USERS`` / 
hrt_21, hrt_22).
+    kerberos_stacks:
+        Subset of ``PLUGIN_KERBEROS_STACKS_ALL``; default is Hive only.
+    verify_keytabs:
+        When True, kinit in the target container after each stack keytab is 
installed.
+    ensure_groups:
+        When True, ensure finance/audit groups and hrt_21/hrt_22 membership.
+    """
+    if usernames is None:
+        usernames = list(PLUGIN_RANGER_DEFAULT_TEST_USERS)
+    if kerberos_usernames is None:
+        kerberos_usernames = list(PLUGIN_KERBEROS_TEST_USERS)
+    if kerberos_stacks is None:
+        kerberos_stacks = (PLUGIN_KERBEROS_STACK_HIVE,)
+
+    logger.info(
+        "Ensuring plugin test users in Ranger: %s; Kerberos users: %s; stacks: 
%s",
+        usernames,
+        kerberos_usernames,
+        kerberos_stacks,
+    )
+    for username in usernames:
+        print(
+            "[plugin-setup] Ranger user: " + username,
+            file=sys.stderr,
+            flush=True,
+        )
+        ensure_user_exists(
+            session,
+            username,
+            password=ranger_password,
+            last_name="plugin_test",
+        )
+
+    if ensure_groups:
+        print("[plugin-setup] Ranger groups: finance, audit", file=sys.stderr, 
flush=True)
+        ensure_plugin_ranger_groups(session)
+
+    for username in kerberos_usernames:
+        for stack in kerberos_stacks:
+            print(
+                "[plugin-setup] Kerberos "
+                + username
+                + " @ stack "
+                + stack,
+                file=sys.stderr,
+                flush=True,
+            )
+            ensure_kerberos_keytab_for_stack(
+                username,
+                stack,
+                verify=verify_keytabs,
+            )
+
+
+def ensure_kerberos_keytab_for_stack(username, stack, verify=True):
+    """Idempotent principal + keytab for one plugin stack."""
+    ensure_kerberos_principal(username)
+    if stack in KDC_VOLUME_KEYTAB_DIRS:
+        _install_keytab_on_kdc_volume(username, KDC_VOLUME_KEYTAB_DIRS[stack])
+    elif stack == PLUGIN_KERBEROS_STACK_HADOOP:
+        _install_keytab_on_hadoop_container(username)
+    else:
+        raise ValueError("Unknown Kerberos stack: " + str(stack))
+
+    if not verify:
+        return
+
+    if stack == PLUGIN_KERBEROS_STACK_HIVE:
+        from ranger_test_utils.hive_utils import verify_kerberos_keytab_in_hive
+
+        verify_kerberos_keytab_in_hive(username)
+    elif stack == PLUGIN_KERBEROS_STACK_HBASE:
+        from ranger_test_utils.hbase_utils import 
verify_kerberos_keytab_in_hbase
+
+        verify_kerberos_keytab_in_hbase(username)
+    elif stack == PLUGIN_KERBEROS_STACK_KAFKA:
+        _verify_kerberos_keytab_in_kafka(username)
+    elif stack == PLUGIN_KERBEROS_STACK_HADOOP:
+        _verify_kerberos_keytab_in_hadoop(username)
+
+
+def ensure_kerberos_principal(username):
+    """Create user@REALM in KDC if it does not exist."""
+    kdc = get_docker_client().containers.get(KDC_CONTAINER)
+    principal = user_kerberos_principal(username)
+    exit_code, output = kdc.exec_run(
+        'kadmin.local -q "getprinc ' + principal + '"',
+        user="root",
+    )
+    combined = (output or b"").decode()
+    if exit_code == 0 and "does not exist" not in combined:
+        logger.info("Kerberos principal already exists: %s", principal)
+        return
+
+    exit_add, out_add = kdc.exec_run(
+        'kadmin.local -q "addprinc -randkey ' + principal + '"',
+        user="root",
+        demux=True,
+    )
+    add_out = ((out_add[0] or b"") + (out_add[1] or b"")).decode()
+    if exit_add != 0:
+        raise RuntimeError(
+            "addprinc failed for " + principal + ": " + add_out
+        )
+    logger.info("Created Kerberos principal: %s", principal)
+
+
+def _install_keytab_on_kdc_volume(username, kdc_keytab_dir):
+    kdc = get_docker_client().containers.get(KDC_CONTAINER)
+    principal = user_kerberos_principal(username)
+    keytab_path = kdc_keytab_dir + "/" + username + ".keytab"
+
+    logger.info(
+        "Kerberos keytab on KDC volume: principal=%s path=%s",
+        principal,
+        keytab_path,
+    )
+    kdc.exec_run("rm -f " + keytab_path, user="root")
+    exit_code, output = kdc.exec_run(
+        'kadmin.local -q "ktadd -norandkey -k '
+        + keytab_path
+        + " "
+        + principal
+        + '"',
+        user="root",
+        demux=True,
+    )
+    kt_out = ((output[0] or b"") + (output[1] or b"")).decode()
+    if exit_code != 0:
+        raise RuntimeError(
+            "ktadd failed for " + principal + " at " + keytab_path + ": " + 
kt_out
+        )
+    kdc.exec_run("chmod 444 " + keytab_path, user="root")
+
+
+def _install_keytab_on_hadoop_container(username):
+    kdc = get_docker_client().containers.get(KDC_CONTAINER)
+    hadoop = get_docker_client().containers.get(HADOOP_CONTAINER)
+    principal = user_kerberos_principal(username)
+    keytab_name = username + ".keytab"
+    kdc_tmp = "/tmp/" + keytab_name
+    dest_path = user_keytab_in_service_container(username)
+
+    ensure_kerberos_principal(username)
+
+    kdc.exec_run("rm -f " + kdc_tmp, user="root")
+    exit_code, output = kdc.exec_run(
+        'kadmin.local -q "xst -k ' + kdc_tmp + " " + principal + '"',
+        user="root",
+    )
+    if exit_code != 0:
+        raise RuntimeError(
+            "keytab export failed for "
+            + principal
+            + ": "
+            + (output 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 on %s: principal=%s path=%s",
+        HADOOP_CONTAINER,
+        principal,
+        dest_path,
+    )
+
+
+def _verify_kerberos_keytab_in_kafka(username):
+    from ranger_test_utils.kafka_utils import KAFKA_CONTAINER
+
+    container = get_docker_client().containers.get(KAFKA_CONTAINER)
+    principal = user_kerberos_principal(username)
+    keytab = user_keytab_in_service_container(username)
+    shell = (
+        "kdestroy -A 2>/dev/null || true; "
+        "kinit -kt "
+        + keytab
+        + " "
+        + principal
+        + "; klist"
+    )
+    exit_code, output = container.exec_run(
+        ["bash", "-c", shell], user="root", demux=True
+    )
+    combined = ((output[0] or b"") + (output[1] or b"")).decode()
+    if exit_code != 0:
+        raise RuntimeError(
+            "Kerberos kinit failed in "
+            + KAFKA_CONTAINER
+            + " for "
+            + principal
+            + ": "
+            + combined
+        )
+
+
+def _verify_kerberos_keytab_in_hadoop(username):
+    hadoop = get_docker_client().containers.get(HADOOP_CONTAINER)
+    principal = user_kerberos_principal(username)
+    keytab = user_keytab_in_service_container(username)
+    shell = (
+        "kdestroy -A 2>/dev/null || true; "
+        "kinit -kt "
+        + keytab
+        + " "
+        + principal
+        + "; klist"
+    )
+    exit_code, output = hadoop.exec_run(
+        ["bash", "-c", shell], user=username, demux=True
+    )
+    combined = ((output[0] or b"") + (output[1] or b"")).decode()
+    if exit_code != 0:
+        raise RuntimeError(
+            "Kerberos kinit failed in "
+            + HADOOP_CONTAINER
+            + " for "
+            + principal
+            + ": "
+            + combined
+        )
diff --git a/functional-tests/ranger_test_utils/utils.py 
b/functional-tests/ranger_test_utils/utils.py
index d751f5a62..a791aca8b 100644
--- a/functional-tests/ranger_test_utils/utils.py
+++ b/functional-tests/ranger_test_utils/utils.py
@@ -38,6 +38,9 @@
 
 DEFAULT_ADMIN_AUTH = ("admin", "rangerR0cks!")
 
+# Avoid indefinite hangs when Ranger Admin or Docker is not up (plugin session 
setup).
+RANGER_ADMIN_REQUEST_TIMEOUT_SEC = 30
+
 DEFAULT_JSON_HEADERS = {
     "Accept": "application/json",
     "Content-Type": "application/json",
@@ -121,6 +124,14 @@ def assert_http_ok(response, ok_codes, step, url=""):
     raise AssertionError(message)
 
 
+class _RangerAdminSession(requests.Session):
+    """requests.Session with a default timeout on every call."""
+
+    def request(self, method, url, **kwargs):
+        kwargs.setdefault("timeout", RANGER_ADMIN_REQUEST_TIMEOUT_SEC)
+        return super().request(method, url, **kwargs)
+
+
 def create_ranger_admin_session(auth=None, headers=None):
     """
     Build a requests.Session for Ranger Admin REST calls (Docker defaults).
@@ -131,12 +142,42 @@ def create_ranger_admin_session(auth=None, headers=None):
         auth = DEFAULT_ADMIN_AUTH
     if headers is None:
         headers = DEFAULT_JSON_HEADERS
-    session = requests.Session()
+    session = _RangerAdminSession()
     session.auth = auth
     session.headers.update(headers)
     return session
 
 
+def preflight_ranger_admin_http():
+    """Skip plugin tests when Ranger Admin is not reachable on Docker 
defaults."""
+    import pytest
+
+    url = RANGER_ADMIN_BASE_URL + "/plugins/definitions?page=0&pageSize=1"
+    try:
+        response = requests.get(
+            url,
+            auth=DEFAULT_ADMIN_AUTH,
+            headers=DEFAULT_JSON_HEADERS,
+            timeout=RANGER_ADMIN_REQUEST_TIMEOUT_SEC,
+        )
+    except requests.RequestException as exc:
+        pytest.skip(
+            "Ranger Admin is not reachable at "
+            + RANGER_ADMIN_BASE_URL
+            + " ("
+            + str(exc)
+            + "). Start the stack: cd functional-tests && ./run-tests.sh 
postgres hive"
+        )
+    if response.status_code not in (200, 401, 403):
+        pytest.skip(
+            "Ranger Admin returned HTTP "
+            + str(response.status_code)
+            + " at "
+            + url
+            + ". Wait for containers to finish starting, then retry."
+        )
+
+
 # --- Test run IDs ---
 
 
diff --git a/functional-tests/readme.md b/functional-tests/readme.md
index c9f9e25af..a4f296f4a 100644
--- a/functional-tests/readme.md
+++ b/functional-tests/readme.md
@@ -218,6 +218,7 @@ Brought up based on what you enter at the containers prompt 
(or `SELECTED_CONTAI
 - Tests assert on client output only (beeline, HBase shell, Kafka console 
producer). Access-audit checks are deferred; helpers are in 
`ranger_test_utils/access_audit_utils.py`.
 - `hive` and `hbase` start `ranger-hadoop` automatically. `kafka` starts 
`ranger-kafka`.
 - Example: `SELECTED_CONTAINERS="hive hbase kafka" ./run-tests.sh postgres 
hive hbase kafka`
+- **Shared plugin user/Kerberos setup** (session hook, markers, flow 
diagrams): [docs/plugin-enforcement-setup.md](docs/plugin-enforcement-setup.md)
 
 
 ### HDFS suite notes

Reply via email to