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 6c34ac95dc379e3406af18bc7aa76b45e72722ec Author: ramk <[email protected]> AuthorDate: Tue Jul 14 19:53:36 2026 +0530 RANGER-5628: Gate Ozone action-matches policy behind feature flags Add ranger.ozone.action.policy.enabled (admin UI + DB patch) and ranger.plugin.ozone.action.policy.enabled (authorizer), default false. Includes E2E validation script and docker ozone audit workaround for tests. --- dev-support/ranger-docker/.env | 1 + .../ozone/ranger-ozone-plugin-install.properties | 6 +- .../ozone/test-ozone-action-feature-flag.sh | 81 ++++++++++++++++++++++ plugin-ozone/conf/ranger-ozone-security.xml | 9 +++ .../ozone/authorizer/RangerOzoneAuthorizer.java | 15 +++- .../authorizer/TestRangerOzoneAuthorizer.java | 57 +++++++++++++-- ...zoneServiceDefPolicyConditionUpdate_J10065.java | 8 +++ .../main/java/org/apache/ranger/rest/UserREST.java | 2 + .../conf.dist/ranger-admin-default-site.xml | 8 +++ .../react-webapp/src/utils/policyConditionUtils.js | 24 ++++++- .../views/PolicyListing/PolicyPermissionItem.jsx | 6 +- 11 files changed, 206 insertions(+), 11 deletions(-) diff --git a/dev-support/ranger-docker/.env b/dev-support/ranger-docker/.env index 1e9ea2966..d841cf510 100644 --- a/dev-support/ranger-docker/.env +++ b/dev-support/ranger-docker/.env @@ -1,5 +1,6 @@ BUILD_HOST_SRC=true SKIPTESTS=true +RANGER_DB_TYPE=postgres GIT_URL=https://github.com/apache/ranger.git BRANCH=ranger-2.9 PROFILE= diff --git a/dev-support/ranger-docker/scripts/ozone/ranger-ozone-plugin-install.properties b/dev-support/ranger-docker/scripts/ozone/ranger-ozone-plugin-install.properties index 99c31d2a3..d802def86 100644 --- a/dev-support/ranger-docker/scripts/ozone/ranger-ozone-plugin-install.properties +++ b/dev-support/ranger-docker/scripts/ozone/ranger-ozone-plugin-install.properties @@ -20,7 +20,7 @@ COMPONENT_INSTALL_DIR_NAME=ranger-ozone-plugin CUSTOM_USER=ozone CUSTOM_GROUP=hadoop -XAAUDIT.SOLR.IS_ENABLED=true +XAAUDIT.SOLR.IS_ENABLED=false XAAUDIT.SOLR.MAX_QUEUE_SIZE=1 XAAUDIT.SOLR.MAX_FLUSH_INTERVAL_MS=1000 XAAUDIT.SOLR.SOLR_URL=http://ranger-solr:8983/solr/ranger_audits @@ -40,7 +40,7 @@ XAAUDIT.HDFS.LOCAL_ARCHIVE_MAX_FILE_COUNT=10 XAAUDIT.SUMMARY.ENABLE=true -XAAUDIT.SOLR.ENABLE=true +XAAUDIT.SOLR.ENABLE=false XAAUDIT.SOLR.URL=http://ranger-solr:8983/solr/ranger_audits XAAUDIT.SOLR.USER=NONE XAAUDIT.SOLR.PASSWORD=NONE @@ -74,7 +74,7 @@ XAAUDIT.HDFS.AZURE_ACCOUNTKEY=__REPLACE_AZURE_ACCOUNT_KEY XAAUDIT.HDFS.AZURE_SHELL_KEY_PROVIDER=__REPLACE_AZURE_SHELL_KEY_PROVIDER XAAUDIT.HDFS.AZURE_ACCOUNTKEY_PROVIDER=__REPLACE_AZURE_ACCOUNT_KEY_PROVIDER -XAAUDIT.LOG4J.ENABLE=false +XAAUDIT.LOG4J.ENABLE=true XAAUDIT.LOG4J.IS_ASYNC=false XAAUDIT.LOG4J.ASYNC.MAX.QUEUE.SIZE=10240 XAAUDIT.LOG4J.ASYNC.MAX.FLUSH.INTERVAL.MS=30000 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 new file mode 100755 index 000000000..3b544f411 --- /dev/null +++ b/dev-support/ranger-docker/scripts/ozone/test-ozone-action-feature-flag.sh @@ -0,0 +1,81 @@ +#!/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. + +set -euo pipefail + +RANGER_URL="${RANGER_URL:-http://localhost:6080}" +RANGER_USER="${RANGER_USER:-admin}" +RANGER_PASS="${RANGER_PASS:-rangerR0cks!}" +EXPECT_ENABLED="${EXPECT_ENABLED:-false}" + +fail() { echo "FAIL: $*"; exit 1; } +pass() { echo "PASS: $*"; } + +echo "=== Ozone action-policy feature flag E2E (expect enabled=${EXPECT_ENABLED}) ===" +echo "Ranger URL: ${RANGER_URL}" + +COOKIE_JAR="$(mktemp)" +trap 'rm -f "${COOKIE_JAR}"' EXIT + +# Login +LOGIN_HTTP=$(curl -s -o /dev/null -w '%{http_code}' -c "${COOKIE_JAR}" \ + -X POST "${RANGER_URL}/login" \ + -d "username=${RANGER_USER}&password=${RANGER_PASS}") +[[ "${LOGIN_HTTP}" == "200" ]] || fail "login HTTP ${LOGIN_HTTP}" + +PROFILE_JSON=$(curl -s -b "${COOKIE_JAR}" "${RANGER_URL}/service/users/profile") +FLAG_VALUE=$(echo "${PROFILE_JSON}" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('configProperties',{}).get('ozoneActionPolicyEnabled','missing'))") + +if [[ "${EXPECT_ENABLED}" == "true" ]]; then + [[ "${FLAG_VALUE}" == "true" ]] || fail "profile ozoneActionPolicyEnabled expected true, got ${FLAG_VALUE}" +else + [[ "${FLAG_VALUE}" == "false" ]] || fail "profile ozoneActionPolicyEnabled expected false, got ${FLAG_VALUE}" +fi +pass "user profile ozoneActionPolicyEnabled=${FLAG_VALUE}" + +# Service-def may still list action-matches from embedded JSON; the flag gates UI + authorizer. +SERVICEDEF_JSON=$(curl -s -b "${COOKIE_JAR}" "${RANGER_URL}/service/plugins/definitions/name/ozone") +HAS_ACTION_MATCHES=$(echo "${SERVICEDEF_JSON}" | python3 -c " +import json,sys +d=json.load(sys.stdin) +conds=d.get('policyConditions') or [] +print('yes' if any(c.get('name')=='action-matches' for c in conds) else 'no') +") +echo "INFO: ozone service-def action-matches present=${HAS_ACTION_MATCHES} (embedded def; flag gates UI/plugin only)" + +# Ozone OM basic health (container stack) +if docker ps --format '{{.Names}}' | grep -qx 'ozone-om'; then + OM_HTTP=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:9874/ || true) + [[ "${OM_HTTP}" =~ ^[23] ]] || fail "ozone-om HTTP ${OM_HTTP}" + pass "ozone-om reachable (HTTP ${OM_HTTP})" + + OZONE_REALM="${OZONE_KERBEROS_REALM:-EXAMPLE.COM}" + if docker exec ozone-om test -f /etc/security/keytabs/ozone.service.keytab; then + docker exec ozone-om bash -lc "kinit -kt /etc/security/keytabs/ozone.service.keytab ozone/om.rangernw@${OZONE_REALM} 2>/dev/null || true" + if docker exec ozone-om timeout 20 /opt/hadoop/bin/ozone sh volume list 2>&1 | head -5; then + pass "ozone CLI volume list succeeded" + else + fail "ozone volume list failed" + fi + else + echo "WARN: ozone.service.keytab missing; skipping CLI volume list (API/OM checks are sufficient for feature-flag E2E)" + fi +else + echo "SKIP: ozone-om container not running (admin/API checks only)" +fi + +echo "=== All checks passed for EXPECT_ENABLED=${EXPECT_ENABLED} ===" diff --git a/plugin-ozone/conf/ranger-ozone-security.xml b/plugin-ozone/conf/ranger-ozone-security.xml index 40ebdb68c..bf4c77415 100644 --- a/plugin-ozone/conf/ranger-ozone-security.xml +++ b/plugin-ozone/conf/ranger-ozone-security.xml @@ -80,4 +80,13 @@ RangerRestClient read Timeout in Milli Seconds </description> </property> + + <property> + <name>ranger.plugin.ozone.action.policy.enabled</name> + <value>false</value> + <description> + When true, the Ozone authorizer supplies S3 actions on access requests + for action-matches policy condition evaluation. + </description> + </property> </configuration> diff --git a/plugin-ozone/src/main/java/org/apache/ranger/authorization/ozone/authorizer/RangerOzoneAuthorizer.java b/plugin-ozone/src/main/java/org/apache/ranger/authorization/ozone/authorizer/RangerOzoneAuthorizer.java index 44159ce92..c42ae69b9 100644 --- a/plugin-ozone/src/main/java/org/apache/ranger/authorization/ozone/authorizer/RangerOzoneAuthorizer.java +++ b/plugin-ozone/src/main/java/org/apache/ranger/authorization/ozone/authorizer/RangerOzoneAuthorizer.java @@ -71,6 +71,8 @@ public class RangerOzoneAuthorizer implements IAccessAuthorizer { private static final String S3_VOLUME_NAME = "s3Vol"; + static final String PROP_ACTION_POLICY_ENABLED = "action.policy.enabled"; + private static final Logger PERF_OZONEAUTH_REQUEST_LOG = RangerPerfTracer.getPerfLogger("ozoneauth.request"); private static final Logger LOG = LoggerFactory.getLogger(RangerOzoneAuthorizer.class); @@ -164,7 +166,9 @@ public boolean checkAccess(IOzoneObj ozoneObject, RequestContext context) { rangerRequest.setResource(rangerResource); rangerRequest.setAccessType(accessType); - rangerRequest.setAction(context.getS3Action()); + if (isActionPolicyEnabled(plugin)) { + rangerRequest.setAction(context.getS3Action()); + } rangerRequest.setRequestData(resource); rangerRequest.setClusterName(clusterName); @@ -374,5 +378,14 @@ private static String toRangerPermission(ACLType acl) { return null; } + + static boolean isActionPolicyEnabled(RangerBasePlugin plugin) { + if (plugin == null || plugin.getConfig() == null) { + return false; + } + + return plugin.getConfig().getBoolean( + plugin.getConfig().getPropertyPrefix() + "." + PROP_ACTION_POLICY_ENABLED, false); + } } diff --git a/plugin-ozone/src/test/java/org/apache/ranger/authorization/ozone/authorizer/TestRangerOzoneAuthorizer.java b/plugin-ozone/src/test/java/org/apache/ranger/authorization/ozone/authorizer/TestRangerOzoneAuthorizer.java index a715e9c6f..6cde84022 100644 --- a/plugin-ozone/src/test/java/org/apache/ranger/authorization/ozone/authorizer/TestRangerOzoneAuthorizer.java +++ b/plugin-ozone/src/test/java/org/apache/ranger/authorization/ozone/authorizer/TestRangerOzoneAuthorizer.java @@ -91,6 +91,23 @@ public static void setUpBeforeClass() { assertNotNull(ozoneAuthorizer); } + @Test + public void testIsActionPolicyEnabledDefaultFalse() { + RangerPluginConfig pluginConfig = new RangerPluginConfig(RANGER_SERVICE_TYPE, null, RANGER_APP_ID, null, null, null); + RangerBasePlugin plugin = new RangerBasePlugin(pluginConfig); + + assertFalse(RangerOzoneAuthorizer.isActionPolicyEnabled(plugin)); + } + + @Test + public void testIsActionPolicyEnabledWhenConfigured() { + RangerPluginConfig pluginConfig = new RangerPluginConfig(RANGER_SERVICE_TYPE, null, RANGER_APP_ID, null, null, null); + pluginConfig.set("ranger.plugin.ozone.action.policy.enabled", "true"); + RangerBasePlugin plugin = new RangerBasePlugin(pluginConfig); + + assertTrue(RangerOzoneAuthorizer.isActionPolicyEnabled(plugin)); + } + @Test public void testAssumeRoleDeny() { // user2 should not be allowed to assume role1 - no Ranger policy grants this permission @@ -228,15 +245,27 @@ public void testAssumeRoleWithS3ActionsInGrant() throws Exception { assertTrue(grant.getResources().contains("key:vol1/buck1/key1")); } + private RangerOzoneAuthorizer createAuthorizer(boolean actionPolicyEnabled) { + RangerPluginConfig pluginConfig = new RangerPluginConfig(RANGER_SERVICE_TYPE, null, RANGER_APP_ID, null, null, null); + if (actionPolicyEnabled) { + pluginConfig.set("ranger.plugin.ozone.action.policy.enabled", "true"); + } + RangerBasePlugin plugin = new RangerBasePlugin(pluginConfig); + plugin.init(); + return new RangerOzoneAuthorizer(plugin); + } + @Test public void testCheckAccessWithS3Action() throws Exception { // Verify that checkAccess passes the S3 action from RequestContext to the Ranger request, // allowing action-based inline policy evaluation // Note - these tests use policy 103 in om_dev_ozone.json + RangerOzoneAuthorizer authorizerWithActionPolicy = createAuthorizer(true); + Set<OzoneGrant> grants = Collections.singleton(grantWriteWithS3Actions); AssumeRoleRequest request = new AssumeRoleRequest(hostname, ipAddress, user1, role1, grants); - String sessionPolicy = ozoneAuthorizer.generateAssumeRoleSessionPolicy(request); + String sessionPolicy = authorizerWithActionPolicy.generateAssumeRoleSessionPolicy(request); assertNotNull(sessionPolicy); @@ -248,7 +277,7 @@ public void testCheckAccessWithS3Action() throws Exception { .setS3Action("AbortMultipartUpload") .build(); - assertTrue(ozoneAuthorizer.checkAccess(key1, ctxWriteAbortMultipartUpload), + assertTrue(authorizerWithActionPolicy.checkAccess(key1, ctxWriteAbortMultipartUpload), "session-policy should allow write on key1 when S3 action is AbortMultipartUpload"); // GetObject should be denied - it doesn't match the grant's S3 actions [AbortMultipartUpload, PutObjectTagging] @@ -259,7 +288,7 @@ public void testCheckAccessWithS3Action() throws Exception { .setS3Action("GetObject") .build(); - assertFalse(ozoneAuthorizer.checkAccess(key1, ctxWriteGetObject), + assertFalse(authorizerWithActionPolicy.checkAccess(key1, ctxWriteGetObject), "session-policy should deny write on key1 when S3 action is GetObject (not in grant)"); // No S3 action specified should be denied when the grant has S3-action restrictions @@ -270,7 +299,27 @@ public void testCheckAccessWithS3Action() throws Exception { .setS3Action(null) .build(); - assertFalse(ozoneAuthorizer.checkAccess(key1, ctxWriteNoAction), + assertFalse(authorizerWithActionPolicy.checkAccess(key1, ctxWriteNoAction), "session-policy should deny write on key1 when no S3 action is specified and grant has S3 actions"); } + + @Test + public void testCheckAccessIgnoresS3ActionWhenDisabled() throws Exception { + RangerOzoneAuthorizer authorizerWithoutActionPolicy = createAuthorizer(false); + + Set<OzoneGrant> grants = Collections.singleton(grantWriteWithS3Actions); + AssumeRoleRequest request = new AssumeRoleRequest(hostname, ipAddress, user1, role1, grants); + + String sessionPolicy = authorizerWithoutActionPolicy.generateAssumeRoleSessionPolicy(request); + + RequestContext ctxWriteAbortMultipartUpload = reqCtxBuilder + .setAclRights(IAccessAuthorizer.ACLType.WRITE) + .setRecursiveAccessCheck(false) + .setSessionPolicy(sessionPolicy) + .setS3Action("AbortMultipartUpload") + .build(); + + assertFalse(authorizerWithoutActionPolicy.checkAccess(key1, ctxWriteAbortMultipartUpload), + "action policy disabled: S3 action should not be evaluated"); + } } diff --git a/security-admin/src/main/java/org/apache/ranger/patch/PatchForOzoneServiceDefPolicyConditionUpdate_J10065.java b/security-admin/src/main/java/org/apache/ranger/patch/PatchForOzoneServiceDefPolicyConditionUpdate_J10065.java index 62e9846a0..3c57a2427 100644 --- a/security-admin/src/main/java/org/apache/ranger/patch/PatchForOzoneServiceDefPolicyConditionUpdate_J10065.java +++ b/security-admin/src/main/java/org/apache/ranger/patch/PatchForOzoneServiceDefPolicyConditionUpdate_J10065.java @@ -20,6 +20,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.ranger.biz.ServiceDBStore; import org.apache.ranger.common.JSONUtil; +import org.apache.ranger.common.PropertiesUtil; import org.apache.ranger.common.RangerValidatorFactory; import org.apache.ranger.db.RangerDaoManager; import org.apache.ranger.entity.XXServiceDef; @@ -40,6 +41,8 @@ public class PatchForOzoneServiceDefPolicyConditionUpdate_J10065 extends BaseLoader { private static final Logger logger = LoggerFactory.getLogger(PatchForOzoneServiceDefPolicyConditionUpdate_J10065.class); + static final String PROP_OZONE_ACTION_POLICY_ENABLED = "ranger.ozone.action.policy.enabled"; + @Autowired RangerDaoManager daoMgr; @@ -85,6 +88,11 @@ public void printStats() { public void execLoad() { logger.info("==> PatchForOzoneServiceDefPolicyConditionUpdate_J10065.execLoad()"); try { + if (!PropertiesUtil.getBooleanProperty(PROP_OZONE_ACTION_POLICY_ENABLED, false)) { + logger.info("{}=false; skipping ozone service-def policy condition update", + PROP_OZONE_ACTION_POLICY_ENABLED); + return; + } updateOzoneServiceDef(); } catch (Exception e) { logger.error("Error while applying PatchForOzoneServiceDefPolicyConditionUpdate_J10065", e); diff --git a/security-admin/src/main/java/org/apache/ranger/rest/UserREST.java b/security-admin/src/main/java/org/apache/ranger/rest/UserREST.java index 4708b8638..634e2a92c 100644 --- a/security-admin/src/main/java/org/apache/ranger/rest/UserREST.java +++ b/security-admin/src/main/java/org/apache/ranger/rest/UserREST.java @@ -285,6 +285,8 @@ public VXPortalUser getUserProfile(@Context HttpServletRequest request) { Map<String, String> configProperties = new HashMap<>(); Long inactivityTimeout = PropertiesUtil.getLongProperty("ranger.service.inactivity.timeout", 15*60); configProperties.put("inactivityTimeout", Long.toString(inactivityTimeout)); + configProperties.put("ozoneActionPolicyEnabled", Boolean.toString( + PropertiesUtil.getBooleanProperty("ranger.ozone.action.policy.enabled", false))); VXPortalUser userProfile = userManager.getUserProfileByLoginId(); userProfile.setConfigProperties(configProperties); return userProfile; diff --git a/security-admin/src/main/resources/conf.dist/ranger-admin-default-site.xml b/security-admin/src/main/resources/conf.dist/ranger-admin-default-site.xml index 1af339010..16abbafd9 100644 --- a/security-admin/src/main/resources/conf.dist/ranger-admin-default-site.xml +++ b/security-admin/src/main/resources/conf.dist/ranger-admin-default-site.xml @@ -152,6 +152,14 @@ <name>ranger.resource.accessControl.enabled</name> <value>true</value> </property> + <property> + <name>ranger.ozone.action.policy.enabled</name> + <value>false</value> + <description> + When true, enables Ozone action-matches policy conditions in the Admin UI + and applies S3 action-based evaluation in the Ozone authorizer. + </description> + </property> <property> <name>ranger.xuser.createdByUserId</name> <value>1</value> diff --git a/security-admin/src/main/webapp/react-webapp/src/utils/policyConditionUtils.js b/security-admin/src/main/webapp/react-webapp/src/utils/policyConditionUtils.js index e8c1c3c00..52c09e842 100644 --- a/security-admin/src/main/webapp/react-webapp/src/utils/policyConditionUtils.js +++ b/security-admin/src/main/webapp/react-webapp/src/utils/policyConditionUtils.js @@ -40,6 +40,27 @@ */ import { actionRequirementsRegistry } from "./actionRequirements/registry"; +import { getUserProfile } from "Utils/appState"; + +/** + * True when Ozone action-matches policy conditions are enabled in Ranger Admin. + * Controlled by ranger.ozone.action.policy.enabled (exposed via user profile configProperties). + */ +export const isOzoneActionPolicyEnabled = () => + getUserProfile()?.configProperties?.ozoneActionPolicyEnabled === "true"; + +/** + * Filters policy condition definitions for the current feature-flag state. + */ +export const filterPolicyConditionsForFeatureFlags = (policyConditions) => { + if (!Array.isArray(policyConditions)) { + return policyConditions; + } + if (isOzoneActionPolicyEnabled()) { + return policyConditions; + } + return policyConditions.filter((c) => c?.name !== "action-matches"); +}; /** * Sorts action options for the dropdown, placing wildcard patterns first @@ -182,7 +203,8 @@ export const getSelectedAccessTypesForRow = (formValues, attrName, index) => { * and to scope prune/apply logic. ip-range and _expression still render in the * per-row Editable popover (CustomCondition) and are evaluated on the policy item. */ -export const isPerRowCondition = (name) => name === "action-matches"; +export const isPerRowCondition = (name) => + name === "action-matches" && isOzoneActionPolicyEnabled(); /** * Safely parses a condition definition uiHint JSON string. diff --git a/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/PolicyPermissionItem.jsx b/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/PolicyPermissionItem.jsx index 342034f6f..3f84d011b 100644 --- a/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/PolicyPermissionItem.jsx +++ b/security-admin/src/main/webapp/react-webapp/src/views/PolicyListing/PolicyPermissionItem.jsx @@ -45,7 +45,7 @@ import { policyConditionUpdatedJSON } from "Utils/XAUtils"; import { selectInputCustomStyles } from "Components/CommonComponents"; -import { getSelectedLeafResourceTypes, getSelectedAccessTypesForRow, buildActionReqsMapFromConditionDef, getCleanConditions } from "Utils/policyConditionUtils"; +import { getSelectedLeafResourceTypes, getSelectedAccessTypesForRow, buildActionReqsMapFromConditionDef, getCleanConditions, filterPolicyConditionsForFeatureFlags } from "Utils/policyConditionUtils"; import { usePruneStaleConditions } from "../../hooks/usePruneStaleConditions"; const noneOptions = { @@ -145,7 +145,9 @@ export default function PolicyPermissionItem(props) { }, [serviceCompDetails, resourceSelectionSignature]); const conditionDefVal = useMemo( - () => policyConditionUpdatedJSON(serviceCompDetails.policyConditions), + () => policyConditionUpdatedJSON( + filterPolicyConditionsForFeatureFlags(serviceCompDetails.policyConditions) + ), [serviceCompDetails.policyConditions] );
