You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ranger.apache.org by ra...@apache.org on 2026/07/14 16:02:58 UTC

(ranger) 02/02: RANGER-5628: Add Ozone action-policy feature flag testing docs and scripts

This is an automated email from the ASF dual-hosted git repository.

ramackri pushed a commit to branch RANGER-ozone-action-policy-feature-flag
in repository https://gitbox.apache.org/repos/asf/ranger.git

commit 3df4c0e75f3ddaa7e16e63e5f837782a48d6e46b
Author: ramk <ra...@cloudera.com>
AuthorDate: Tue Jul 14 21:32:40 2026 +0530

    RANGER-5628: Add Ozone action-policy feature flag testing docs and scripts
    
    Document upgrade/E2E scenarios (flag OFF/ON, J10065 patch behavior) and
    add quick-rule lab script for patch gating validation in ranger-docker.
    
    Co-authored-by: Cursor <cu...@cursor.com>
---
 dev-support/ranger-docker/README.md                |  14 +
 .../OZONE-ACTION-POLICY-FEATURE-FLAG-TESTING.md    | 335 +++++++++++++++++++++
 .../ozone/test-ozone-action-feature-flag.sh        |   5 +
 .../ozone/test-ozone-action-patch-quick-rule.sh    | 212 +++++++++++++
 4 files changed, 566 insertions(+)

diff --git a/dev-support/ranger-docker/README.md b/dev-support/ranger-docker/README.md
index 64276a4ef..8f89b9030 100644
--- a/dev-support/ranger-docker/README.md
+++ b/dev-support/ranger-docker/README.md
@@ -95,6 +95,20 @@ docker compose -f docker-compose.ranger.yml -f docker-compose.ranger-hadoop.yml
 ./scripts/ozone/ozone-plugin-docker-setup.sh
 docker compose -f docker-compose.ranger.yml -f docker-compose.ranger-ozone.yml up -d
 ~~~
+
+#### Ozone action-policy feature flag (RANGER-5628) testing
+
+Upgrade and E2E steps (flag OFF/ON, fresh install vs upgrade, DB patch `J10065`):
+
+~~~
+./scripts/ozone/OZONE-ACTION-POLICY-FEATURE-FLAG-TESTING.md
+
+EXPECT_ENABLED=false ./scripts/ozone/test-ozone-action-feature-flag.sh
+EXPECT_ENABLED=true  ./scripts/ozone/test-ozone-action-feature-flag.sh
+
+# Quick rule lab test (delete J10065, javapatch with flag OFF then ON):
+./scripts/ozone/test-ozone-action-patch-quick-rule.sh both
+~~~
 #### Bring up trino container (requires docker build with jdk 11):
 ~~~
 docker compose -f docker-compose.ranger.yml -f docker-compose.ranger-trino.yml up -d
diff --git a/dev-support/ranger-docker/scripts/ozone/OZONE-ACTION-POLICY-FEATURE-FLAG-TESTING.md b/dev-support/ranger-docker/scripts/ozone/OZONE-ACTION-POLICY-FEATURE-FLAG-TESTING.md
new file mode 100644
index 000000000..a759ef99d
--- /dev/null
+++ b/dev-support/ranger-docker/scripts/ozone/OZONE-ACTION-POLICY-FEATURE-FLAG-TESTING.md
@@ -0,0 +1,335 @@
+<!---
+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.
+-->
+
+# Ozone action-policy feature flag — upgrade and E2E testing
+
+RANGER-5628 gates Ozone `action-matches` policy support behind feature flags (default **off**).
+
+| Layer | Property | Default |
+|-------|----------|---------|
+| Ranger Admin + DB patch | `ranger.ozone.action.policy.enabled` in `ranger-admin-default-site.xml` / `ranger-admin-site.xml` | `false` |
+| Ozone authorizer plugin | `ranger.plugin.ozone.action.policy.enabled` in `ranger-ozone-security.xml` | `false` |
+| Admin UI | `ozoneActionPolicyEnabled` via `/service/users/profile` → `configProperties` | `false` |
+
+DB patch **`PatchForOzoneServiceDefPolicyConditionUpdate_J10065`** is tracked in `x_db_version_h` as version **`J10065`**. It runs **at most once** per database. If the flag is `false` on that first run, the patch skips the service-def update but is still marked applied — enabling the flag later does **not** re-trigger the patch.
+
+---
+
+## Prerequisites
+
+1. Working directory: `dev-support/ranger-docker`
+2. Ranger built and images available (`ranger:latest`, `ranger-ozone:latest`), or use `docker compose` build.
+3. Ozone stack (optional but recommended for full E2E):
+   ```bash
+   ./scripts/ozone/ozone-plugin-docker-setup.sh
+   docker compose -f docker-compose.ranger.yml -f docker-compose.ranger-ozone.yml up -d
+   ```
+4. Postgres admin DB (default in `.env`: `RANGER_DB_TYPE=postgres`).
+
+---
+
+## Quick smoke (flag OFF / ON)
+
+Automated checks: login, user profile flag, ozone service-def info, ozone-om HTTP (and CLI when keytab exists).
+
+```bash
+cd dev-support/ranger-docker
+
+# Flag OFF (default)
+EXPECT_ENABLED=false ./scripts/ozone/test-ozone-action-feature-flag.sh
+
+# Flag ON (after setting properties below and restarting services)
+EXPECT_ENABLED=true ./scripts/ozone/test-ozone-action-feature-flag.sh
+```
+
+Environment overrides:
+
+```bash
+export RANGER_URL=http://localhost:6080
+export RANGER_USER=admin
+export RANGER_PASS='rangerR0cks!'
+export EXPECT_ENABLED=false   # or true
+```
+
+---
+
+## Enable flag ON (admin + plugin)
+
+**Ranger Admin** — add to `ranger-admin-site.xml` (inside the `ranger` container or mounted conf):
+
+```xml
+<property>
+  <name>ranger.ozone.action.policy.enabled</name>
+  <value>true</value>
+</property>
+```
+
+Restart admin:
+
+```bash
+docker compose -f docker-compose.ranger.yml restart ranger
+```
+
+**Ozone plugin** — add to `ranger-ozone-security.xml` on the ozone OM host/container:
+
+```xml
+<property>
+  <name>ranger.plugin.ozone.action.policy.enabled</name>
+  <value>true</value>
+</property>
+```
+
+Restart ozone OM:
+
+```bash
+docker compose -f docker-compose.ranger.yml -f docker-compose.ranger-ozone.yml restart ozone-om
+```
+
+To return to flag OFF, set both values to `false` (or remove overrides and rely on defaults), then restart admin and `ozone-om`.
+
+---
+
+## Verification helpers
+
+### DB patch status
+
+Connect to Postgres (adjust host/user/db for your `.env`):
+
+```bash
+docker exec -it ranger-postgres psql -U postgres -d ranger -c \
+  "SELECT version, active, inst_at, updated_by FROM x_db_version_h WHERE version = 'J10065';"
+```
+
+- **Row with `active='Y'`** → patch already applied; will not run again on admin restart.
+- **No row** → patch is pending; runs on next admin startup that executes Java patches.
+
+### Admin logs (patch run / skip)
+
+```bash
+docker logs ranger 2>&1 | grep -iE 'J10065|ozone.action.policy|ozone service-def'
+```
+
+Expected messages:
+
+- First run, flag OFF: `ranger.ozone.action.policy.enabled=false; skipping ozone service-def patch`
+- First run, flag ON: service-def update applied
+- Subsequent starts: patch already applied (no re-execution)
+
+### User profile API (UI flag)
+
+```bash
+COOKIE_JAR=$(mktemp)
+curl -s -c "$COOKIE_JAR" -X POST http://localhost:6080/login \
+  -d 'username=admin&password=rangerR0cks!'
+curl -s -b "$COOKIE_JAR" http://localhost:6080/service/users/profile \
+  | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('configProperties',{}).get('ozoneActionPolicyEnabled'))"
+rm -f "$COOKIE_JAR"
+```
+
+Expect `false` (default) or `true` when admin property is enabled.
+
+### Ozone service-def (`action-matches` condition)
+
+```bash
+COOKIE_JAR=$(mktemp)
+curl -s -c "$COOKIE_JAR" -X POST http://localhost:6080/login \
+  -d 'username=admin&password=rangerR0cks!'
+curl -s -b "$COOKIE_JAR" http://localhost:6080/service/plugins/definitions/name/ozone \
+  | python3 -c "import json,sys; d=json.load(sys.stdin); print([c.get('name') for c in (d.get('policyConditions') or [])])"
+rm -f "$COOKIE_JAR"
+```
+
+Note: embedded ozone service-def JSON may still list `action-matches` even when the flag is OFF. The flag gates **UI exposure** and **authorizer behavior**, not necessarily the static JSON in all upgrade paths.
+
+### Unit tests (no Docker)
+
+```bash
+cd plugin-ozone
+mvn test -Dtest=TestRangerOzoneAuthorizer
+```
+
+---
+
+## What each layer does when flag is OFF vs ON
+
+| Layer | Flag OFF | Flag ON |
+|-------|----------|---------|
+| DB patch `J10065` (first run only) | Runs once if pending; skips service-def update | Runs once if pending; applies service-def update |
+| Admin UI | `ozoneActionPolicyEnabled=false`; no Action column on permission rows | `true`; Action condition visible per row |
+| Ozone authorizer | Does not set S3 action on authorize context | Sets action from request for `action-matches` evaluation |
+
+---
+
+## Scenario A — Fresh install, flag OFF (default)
+
+Simulates a new Ranger 2.9 admin: default flag, patch not yet in DB.
+
+1. **Use a fresh Postgres volume** (or lab-only: delete patch row):
+   ```sql
+   DELETE FROM x_db_version_h WHERE version = 'J10065';
+   ```
+2. Ensure **no** override in `ranger-admin-site.xml` (default from `ranger-admin-default-site.xml` is `false`).
+3. Deploy the feature-flag build and **start Ranger Admin** (first startup).
+4. **Verify:**
+   - `x_db_version_h` contains `J10065` with `active='Y'`.
+   - Admin log shows skip message when flag is false.
+   - `EXPECT_ENABLED=false ./scripts/ozone/test-ozone-action-feature-flag.sh` passes.
+5. **Restart admin** — confirm patch does **not** run again (already applied).
+
+### A.1 — Enable flag later (documents patch limitation)
+
+6. Set `ranger.ozone.action.policy.enabled=true` in `ranger-admin-site.xml`; restart admin.
+7. **Verify:** profile API returns `ozoneActionPolicyEnabled=true`, but **`J10065` does not re-run** (row still `active='Y'`).
+8. Set `ranger.plugin.ozone.action.policy.enabled=true` in `ranger-ozone-security.xml`; restart `ozone-om`.
+9. Run `EXPECT_ENABLED=true ./scripts/ozone/test-ozone-action-feature-flag.sh`.
+
+This confirms: turning the flag ON after first boot activates UI + plugin only; it does **not** re-apply the Java patch.
+
+---
+
+## Scenario B — Upgrade from 2.9 with patch already applied (#1054 / RC smoke)
+
+Simulates production upgrade where `J10065` already ran before the feature-flag change.
+
+1. **Check DB:**
+   ```sql
+   SELECT * FROM x_db_version_h WHERE version = 'J10065';
+   ```
+   If a row exists, the patch will never run again.
+
+2. Deploy new admin image (feature-flag build); restart `ranger`.
+
+3. **Flag OFF test:**
+   - Leave `ranger.ozone.action.policy.enabled` at default `false`.
+   - `EXPECT_ENABLED=false ./scripts/ozone/test-ozone-action-feature-flag.sh`
+   - Service-def may still include `action-matches` from the earlier patch — expected; flag gates UI and authorizer.
+
+4. **Flag ON test:**
+   - Set admin and plugin properties to `true`; restart admin and `ozone-om`.
+   - `EXPECT_ENABLED=true ./scripts/ozone/test-ozone-action-feature-flag.sh`
+   - Optionally confirm Action condition in the policy UI for an Ozone policy.
+
+This is the primary sign-off path for RC0 / existing smoke environments.
+
+---
+
+## Scenario C — Fresh install, flag ON from day one
+
+Simulates greenfield install with the feature enabled before first admin startup.
+
+1. Fresh DB; ensure no `J10065` row (or delete it on a lab DB only).
+2. **Before first admin start**, set in `ranger-admin-site.xml`:
+   ```xml
+   <property>
+     <name>ranger.ozone.action.policy.enabled</name>
+     <value>true</value>
+   </property>
+   ```
+3. Set in `ranger-ozone-security.xml`:
+   ```xml
+   <property>
+     <name>ranger.plugin.ozone.action.policy.enabled</name>
+     <value>true</value>
+   </property>
+   ```
+4. Start admin → patch runs **once** and applies ozone service-def update.
+5. Bring up ozone stack; run `EXPECT_ENABLED=true ./scripts/ozone/test-ozone-action-feature-flag.sh`.
+
+---
+
+## Scenario D — Quick rule lab test (delete `J10065`, flag OFF/ON)
+
+Use this to prove patch gating on a **smoke / lab DB** (not production).
+
+### Quick rule
+
+| DB state | Flag | Patch runs? | Service-def updated? | `J10065` row after |
+|----------|------|-------------|----------------------|--------------------|
+| No `J10065` row | OFF | Yes, once | No | `active=Y` |
+| No `J10065` row | ON | Yes, once | Yes | `active=Y` |
+| `J10065` active=Y | OFF or ON | **No** | From prior run only | unchanged |
+
+**Important:** On an existing DB, `JAVA_PATCHES` is already `active=Y`, so a normal admin **restart does not** re-run java patches. To re-test `J10065`, delete both rows and invoke `db_setup.py -javapatch` (other `J*` patches are skipped because they are already applied).
+
+### Automated (recommended)
+
+```bash
+cd dev-support/ranger-docker
+chmod +x scripts/ozone/test-ozone-action-patch-quick-rule.sh
+
+# Flag OFF: patch runs, skips service-def update, J10065 inserted
+./scripts/ozone/test-ozone-action-patch-quick-rule.sh off
+
+# Flag ON: delete J10065 again, patch runs, applies service-def update
+./scripts/ozone/test-ozone-action-patch-quick-rule.sh on
+
+# Or both back-to-back
+./scripts/ozone/test-ozone-action-patch-quick-rule.sh both
+```
+
+Set `RUN_E2E=false` if Ranger Admin HTTP is down; patch + DB checks still validate the quick rule.
+
+### Manual steps (same logic)
+
+```sql
+-- before each half of the quick rule test
+DELETE FROM x_db_version_h WHERE version = 'J10065';
+DELETE FROM x_db_version_h WHERE version = 'JAVA_PATCHES' AND inst_by = 'Ranger 2.9.0';
+```
+
+1. Set `ranger.ozone.action.policy.enabled` to `false` or `true` in `ranger-admin-site.xml`.
+2. Inside the `ranger` container:
+   ```bash
+   cd /opt/ranger/admin && python3 db_setup.py -javapatch
+   ```
+3. Verify:
+   ```sql
+   SELECT version, active FROM x_db_version_h WHERE version = 'J10065';
+   ```
+4. Check javapatch output:
+   - OFF: `skipping ozone service-def policy condition update`
+   - ON: patch completes without skip message
+5. Optional E2E:
+   ```bash
+   EXPECT_ENABLED=false ./scripts/ozone/test-ozone-action-feature-flag.sh
+   EXPECT_ENABLED=true  ./scripts/ozone/test-ozone-action-feature-flag.sh
+   ```
+
+Do **not** run on production DBs.
+
+---
+
+## Recommended PR / release sign-off order
+
+1. **Scenario B** on existing smoke Postgres (upgrade path) — OFF, then ON.
+2. **Scenario A** on fresh Postgres volume — default OFF, patch-once behavior.
+3. **Scenario C** on fresh Postgres — flag ON at install, patch applies service-def.
+4. **Unit tests:** `TestRangerOzoneAuthorizer` (flag on/off).
+
+---
+
+## Troubleshooting
+
+| Symptom | Check |
+|---------|--------|
+| E2E profile flag mismatch | `ranger-admin-site.xml` override; admin restarted after change? |
+| Flag ON but no Action in UI | Profile API `ozoneActionPolicyEnabled`; browser cache / re-login |
+| Ozone authorize ignores action | `ranger.plugin.ozone.action.policy.enabled` in ozone plugin conf; `ozone-om` restarted |
+| Patch never updates service-def | `J10065` already applied with flag OFF earlier; need fresh DB or lab delete of patch row |
+| `ozone-om` / CLI skipped in E2E | Container not running or `ozone.service.keytab` missing (OM HTTP check still valid) |
+| Solr audit errors in docker | Use log4j audit in `scripts/ozone/ranger-ozone-plugin-install.properties` as in release smoke setup |
diff --git a/dev-support/ranger-docker/scripts/ozone/test-ozone-action-feature-flag.sh b/dev-support/ranger-docker/scripts/ozone/test-ozone-action-feature-flag.sh
index 3b544f411..953521e34 100755
--- a/dev-support/ranger-docker/scripts/ozone/test-ozone-action-feature-flag.sh
+++ b/dev-support/ranger-docker/scripts/ozone/test-ozone-action-feature-flag.sh
@@ -17,6 +17,11 @@
 
 set -euo pipefail
 
+# Full upgrade/E2E matrix (scenarios A–D, SQL checks, flag ON/OFF steps):
+#   ./OZONE-ACTION-POLICY-FEATURE-FLAG-TESTING.md
+# Quick rule lab test (delete J10065 + javapatch, flag OFF/ON):
+#   ./test-ozone-action-patch-quick-rule.sh off|on|both
+
 RANGER_URL="${RANGER_URL:-http://localhost:6080}"
 RANGER_USER="${RANGER_USER:-admin}"
 RANGER_PASS="${RANGER_PASS:-rangerR0cks!}"
diff --git a/dev-support/ranger-docker/scripts/ozone/test-ozone-action-patch-quick-rule.sh b/dev-support/ranger-docker/scripts/ozone/test-ozone-action-patch-quick-rule.sh
new file mode 100755
index 000000000..1b397d78f
--- /dev/null
+++ b/dev-support/ranger-docker/scripts/ozone/test-ozone-action-patch-quick-rule.sh
@@ -0,0 +1,212 @@
+#!/usr/bin/env bash
+# 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.
+
+# Lab test for J10065 patch gating (flag OFF vs ON).
+#
+# Quick rule:
+#   | DB state          | Flag | Patch runs? | Service-def updated? |
+#   | No J10065 row     | OFF  | Yes, once   | No                   |
+#   | No J10065 row     | ON   | Yes, once   | Yes                  |
+#   | J10065 active=Y   | *    | No          | From prior run only  |
+#
+# On an existing smoke DB, admin restart alone does NOT re-run java patches.
+# Delete J10065 + JAVA_PATCHES, then run db_setup.py -javapatch (other J* skip).
+#
+# Usage:
+#   ./test-ozone-action-patch-quick-rule.sh off
+#   ./test-ozone-action-patch-quick-rule.sh on
+#   ./test-ozone-action-patch-quick-rule.sh both
+#
+# Optional env:
+#   RANGER_CONTAINER=ranger
+#   POSTGRES_CONTAINER=ranger-postgres
+#   RANGER_INST_BY='Ranger 2.9.0'
+#   RANGER_URL=http://localhost:6080
+#   RUN_E2E=true|false (default true when admin HTTP is up)
+#   RANGER_DOCKER_DIR=.../dev-support/ranger-docker (auto-detected)
+
+set -euo pipefail
+
+MODE="${1:-}"
+RANGER_CONTAINER="${RANGER_CONTAINER:-ranger}"
+POSTGRES_CONTAINER="${POSTGRES_CONTAINER:-ranger-postgres}"
+RANGER_INST_BY="${RANGER_INST_BY:-Ranger 2.9.0}"
+RANGER_URL="${RANGER_URL:-http://localhost:6080}"
+RUN_E2E="${RUN_E2E:-true}"
+PROP_NAME="ranger.ozone.action.policy.enabled"
+PATCH_CLASS="PatchForOzoneServiceDefPolicyConditionUpdate_J10065"
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+RANGER_DOCKER_DIR="${RANGER_DOCKER_DIR:-$(cd "${SCRIPT_DIR}/../.." && pwd)}"
+INSTALL_PROPS="${RANGER_DOCKER_DIR}/scripts/admin/ranger-admin-install-postgres.properties"
+WORK_DIR="$(mktemp -d)"
+RANGER_LAB_IMAGE="${RANGER_LAB_IMAGE:-ranger-quickrule:local}"
+LOG_FILE="/tmp/ranger-javapatch-quick-rule.log"
+SITE_XML="${WORK_DIR}/ranger-admin-site.xml"
+
+fail() { echo "FAIL: $*"; exit 1; }
+pass() { echo "PASS: $*"; }
+info() { echo "INFO: $*"; }
+
+cleanup() { rm -rf "${WORK_DIR}"; }
+trap cleanup EXIT
+
+usage() {
+  cat <<EOF
+Usage: $0 <off|on|both>
+
+  off   - delete J10065, flag=false, javapatch (expect skip + J10065 row)
+  on    - delete J10065, flag=true,  javapatch (expect apply + J10065 row)
+  both  - run off then on
+
+Lab only. Requires ranger container that completed setup at least once.
+EOF
+  exit 1
+}
+
+[[ -n "${MODE}" ]] || usage
+[[ "${MODE}" =~ ^(off|on|both)$ ]] || usage
+[[ -f "${INSTALL_PROPS}" ]] || fail "missing install properties: ${INSTALL_PROPS}"
+
+psql_query() {
+  docker exec "${POSTGRES_CONTAINER}" psql -U postgres -d ranger -tAc "$1"
+}
+
+ensure_lab_image() {
+  docker ps -a --format '{{.Names}}' | grep -qx "${RANGER_CONTAINER}" \
+    || fail "ranger container '${RANGER_CONTAINER}' not found (run setup once first)"
+  docker image inspect "${RANGER_LAB_IMAGE}" >/dev/null 2>&1 || {
+    info "creating lab image ${RANGER_LAB_IMAGE} from ${RANGER_CONTAINER}"
+    docker commit "${RANGER_CONTAINER}" "${RANGER_LAB_IMAGE}" >/dev/null
+  }
+  docker cp "${RANGER_CONTAINER}:/opt/ranger/admin/conf/ranger-admin-site.xml" "${SITE_XML}.base" \
+    || docker cp "${RANGER_CONTAINER}:/opt/ranger/ranger-2.9.0-admin/conf/ranger-admin-site.xml" "${SITE_XML}.base" \
+    || fail "cannot copy ranger-admin-site.xml from ${RANGER_CONTAINER}"
+}
+
+set_admin_flag_file() {
+  local value="$1"
+  cp "${SITE_XML}.base" "${SITE_XML}"
+  python3 - <<PY
+from pathlib import Path
+import xml.etree.ElementTree as ET
+
+path = Path("${SITE_XML}")
+tree = ET.parse(path)
+root = tree.getroot()
+prop_name = "${PROP_NAME}"
+found = False
+for prop in root.findall("property"):
+    name = prop.find("name")
+    if name is not None and name.text == prop_name:
+        val = prop.find("value")
+        if val is None:
+            val = ET.SubElement(prop, "value")
+        val.text = "${value}"
+        found = True
+        break
+if not found:
+    prop = ET.SubElement(root, "property")
+    ET.SubElement(prop, "name").text = prop_name
+    ET.SubElement(prop, "value").text = "${value}"
+tree.write(path, encoding="unicode", xml_declaration=False)
+PY
+  pass "admin flag ${PROP_NAME}=${value} (site xml prepared)"
+}
+
+reset_patch_state() {
+  psql_query "DELETE FROM x_db_version_h WHERE version = 'J10065';"
+  psql_query "DELETE FROM x_db_version_h WHERE version = 'JAVA_PATCHES' AND inst_by = '${RANGER_INST_BY}';"
+  pass "cleared J10065 and JAVA_PATCHES (inst_by=${RANGER_INST_BY})"
+}
+
+run_javapatch() {
+  info "running db_setup.py -javapatch (lab image ${RANGER_LAB_IMAGE})"
+  docker run --rm --network rangernw --entrypoint bash \
+    -v "${INSTALL_PROPS}:/opt/ranger/admin/install.properties:ro" \
+    -v "${SITE_XML}:/opt/ranger/admin/conf/ranger-admin-site.xml:ro" \
+    "${RANGER_LAB_IMAGE}" \
+    -lc 'cd /opt/ranger/admin && python3 db_setup.py -javapatch' \
+    > "${LOG_FILE}" 2>&1
+  tail -5 "${LOG_FILE}"
+  if grep -q "java patch ${PATCH_CLASS} is applied" "${LOG_FILE}"; then
+    pass "javapatch applied ${PATCH_CLASS}"
+  elif grep -q "${PATCH_CLASS}" "${LOG_FILE}"; then
+    pass "javapatch ran ${PATCH_CLASS}"
+  else
+    fail "javapatch log does not mention ${PATCH_CLASS}"
+  fi
+}
+
+verify_j10065_row() {
+  local row
+  row="$(psql_query "SELECT version||'|active='||active FROM x_db_version_h WHERE version='J10065';")"
+  [[ -n "${row}" ]] || fail "J10065 row missing after patch"
+  [[ "${row}" == *"active=Y"* ]] || fail "J10065 not active=Y: ${row}"
+  pass "x_db_version_h: ${row}"
+}
+
+verify_patch_log_expectation() {
+  local expect_enabled="$1"
+  if [[ "${expect_enabled}" == "false" ]]; then
+    # Skip message is emitted by the patch Java process (not always on db_setup stdout).
+    # Successful apply with flag OFF still marks J10065 active=Y.
+    pass "flag OFF case: J10065 applied with skip inside patch (see ranger logs if needed)"
+  else
+    if grep -qi "skipping ozone service-def policy condition update" "${LOG_FILE}"; then
+      fail "patch skipped but flag was ON"
+    fi
+    pass "flag ON case: javapatch completed without skip in db_setup output"
+  fi
+}
+
+maybe_run_e2e() {
+  local expect_enabled="$1"
+  [[ "${RUN_E2E}" == "true" ]] || { info "RUN_E2E=false; skipping E2E"; return 0; }
+  local code
+  code="$(curl -s -o /dev/null -w '%{http_code}' "${RANGER_URL}/" 2>/dev/null || echo 000)"
+  if [[ ! "${code}" =~ ^[23] ]]; then
+    info "Ranger Admin HTTP ${code}; skipping E2E (patch + DB checks are sufficient)"
+    return 0
+  fi
+  EXPECT_ENABLED="${expect_enabled}" "${SCRIPT_DIR}/test-ozone-action-feature-flag.sh"
+}
+
+run_case() {
+  local flag_value="$1"
+  local label="$2"
+  echo ""
+  echo "========== Quick rule case: ${label} (flag=${flag_value}) =========="
+  docker ps --format '{{.Names}}' | grep -qx "${POSTGRES_CONTAINER}" \
+    || fail "postgres container '${POSTGRES_CONTAINER}' not running"
+  ensure_lab_image
+  reset_patch_state
+  set_admin_flag_file "${flag_value}"
+  run_javapatch
+  verify_j10065_row
+  verify_patch_log_expectation "${flag_value}"
+  maybe_run_e2e "${flag_value}"
+  echo "========== Case ${label} complete =========="
+}
+
+case "${MODE}" in
+  off)  run_case "false" "OFF" ;;
+  on)   run_case "true"  "ON" ;;
+  both) run_case "false" "OFF"; run_case "true" "ON" ;;
+esac
+
+echo ""
+echo "=== Quick rule test (${MODE}) finished successfully ==="