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

maheshbandal15 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 f0edc7821 RANGER-5825: Fix HDFS authz optimization on directory rename 
(#1310)
f0edc7821 is described below

commit f0edc78214a52914b8e0f55686ca4110ef19fe3c
Author: Mahesh Bandal <[email protected]>
AuthorDate: Sat Oct 17 00:36:49 2026 +0530

    RANGER-5825: Fix HDFS authz optimization on directory rename (#1310)
    
    * RANGER-5825: Fix HDFS authz optimization on directory rename
---
 .../authorization/hadoop/OperationOptimizer.java   |  40 +++---
 .../hadoop/RangerAccessControlEnforcer.java        |   1 +
 .../services/hdfs/RangerHdfsAuthorizerTest.java    | 142 ++++++++++++++++++++-
 3 files changed, 162 insertions(+), 21 deletions(-)

diff --git 
a/hdfs-agent/src/main/java/org/apache/ranger/authorization/hadoop/OperationOptimizer.java
 
b/hdfs-agent/src/main/java/org/apache/ranger/authorization/hadoop/OperationOptimizer.java
index 7f1522717..eb5915e6d 100644
--- 
a/hdfs-agent/src/main/java/org/apache/ranger/authorization/hadoop/OperationOptimizer.java
+++ 
b/hdfs-agent/src/main/java/org/apache/ranger/authorization/hadoop/OperationOptimizer.java
@@ -121,6 +121,30 @@ private OptimizedAuthzContext optimizeCreateOp() {
     }
 
     private OptimizedAuthzContext optimizeDeleteOp() {
+        return optimizeDirectoryTreeMutationOp();
+    }
+
+    private OptimizedAuthzContext optimizeRenameOp() {
+        if (inode != null && inode.isDirectory()) {
+            return optimizeDirectoryTreeMutationOp();
+        }
+
+        INode nodeToAuthorize = getINodeToAuthorize();
+
+        if (nodeToAuthorize == null) {
+            return OPT_BYPASS_AUTHZ;
+        }
+
+        if (!nodeToAuthorize.isDirectory()) {
+            LOG.debug("nodeToCheck is not a directory for a rename operation! 
Optimization skipped");
+
+            return null;
+        }
+
+        return getOrCreateOptimizedAuthzContext();
+    }
+
+    private OptimizedAuthzContext optimizeDirectoryTreeMutationOp() {
         int numOfRequestedAccesses = 0;
 
         if (ancestorAccess != null) {
@@ -148,22 +172,6 @@ private OptimizedAuthzContext optimizeDeleteOp() {
         }
     }
 
-    private OptimizedAuthzContext optimizeRenameOp() {
-        INode nodeToAuthorize = getINodeToAuthorize();
-
-        if (nodeToAuthorize == null) {
-            return OPT_BYPASS_AUTHZ;
-        }
-
-        if (!nodeToAuthorize.isDirectory()) {
-            LOG.debug("nodeToCheck is not a directory for a rename operation! 
Optimization skipped");
-
-            return null;
-        }
-
-        return getOrCreateOptimizedAuthzContext();
-    }
-
     private OptimizedAuthzContext optimizeMkdirsOp() {
         INode nodeToAuthorize = getINodeToAuthorize();
 
diff --git 
a/hdfs-agent/src/main/java/org/apache/ranger/authorization/hadoop/RangerAccessControlEnforcer.java
 
b/hdfs-agent/src/main/java/org/apache/ranger/authorization/hadoop/RangerAccessControlEnforcer.java
index 924c4fc86..8efe1c978 100644
--- 
a/hdfs-agent/src/main/java/org/apache/ranger/authorization/hadoop/RangerAccessControlEnforcer.java
+++ 
b/hdfs-agent/src/main/java/org/apache/ranger/authorization/hadoop/RangerAccessControlEnforcer.java
@@ -787,6 +787,7 @@ Skipping this authorization check may break semantic 
equivalence (according to H
                               Namenode calls this twice when renaming a file 
for source as well as target directories. For each directory, first invocation 
checks if the user has a EXECUTE access on the parent directory, and second 
invocation
                               checks if the user has a WRITE access to the 
parent (or ancestor when checking target directory) . The optimized code 
combines these checks into a WRITE_EXECUTE access for the first invocation,
                               and optimizes away the second call.
+                              When the source is an existing directory, the 
first traverse-only call is optimized away and the subsequent substantive check 
is evaluated as WRITE_EXECUTE.
                           5. OPERATION_NAME_LISTSTATUS, 
OPERATION_NAME_GETEZFORPATH:
                               Namenode calls this twice when listing a 
directory or getting the encryption zone for the directory. First invocation 
checks if the user has a EXECUTE access
                               on the directory, and second checks if the user 
has a READ_EXECUTE access on the directory. The optimized code combines these 
checks into a READ_EXECUTE access
diff --git 
a/hdfs-agent/src/test/java/org/apache/ranger/services/hdfs/RangerHdfsAuthorizerTest.java
 
b/hdfs-agent/src/test/java/org/apache/ranger/services/hdfs/RangerHdfsAuthorizerTest.java
index 2310988e7..4582ec38b 100644
--- 
a/hdfs-agent/src/test/java/org/apache/ranger/services/hdfs/RangerHdfsAuthorizerTest.java
+++ 
b/hdfs-agent/src/test/java/org/apache/ranger/services/hdfs/RangerHdfsAuthorizerTest.java
@@ -37,6 +37,7 @@
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.OutputStreamWriter;
+import java.lang.reflect.Field;
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 
@@ -46,11 +47,13 @@
  * Direct tests for the {@link RangerHdfsAuthorizer} without going through the 
HDFS layer.
  */
 public class RangerHdfsAuthorizerTest {
-    private static final int          SNAPSHOT_ID   = 
Snapshot.CURRENT_STATE_ID;
-    private static final String       FILE_OWNER    = "fileOwner";
-    private static final String       FILE_GROUP    = "superGroup";
-    private static final FsPermission READ_ONLY     = new 
FsPermission(FsAction.READ, FsAction.NONE, FsAction.NONE);
-    private static final FsPermission EXEC_BY_OWNER = new 
FsPermission(FsAction.EXECUTE, FsAction.NONE, FsAction.NONE);
+    private static final int          SNAPSHOT_ID           = 
Snapshot.CURRENT_STATE_ID;
+    private static final String       FILE_OWNER            = "fileOwner";
+    private static final String       FILE_GROUP            = "superGroup";
+    private static final String       OPERATION_NAME_DELETE = "delete";
+    private static final String       OPERATION_NAME_RENAME = "rename";
+    private static final FsPermission READ_ONLY             = new 
FsPermission(FsAction.READ, FsAction.NONE, FsAction.NONE);
+    private static final FsPermission EXEC_BY_OWNER         = new 
FsPermission(FsAction.EXECUTE, FsAction.NONE, FsAction.NONE);
 
     private static RangerHdfsAuthorizer  authorizer;
     private static AccessControlEnforcer rangerControlEnforcer;
@@ -192,6 +195,62 @@ public void hdfsBaseFileNameTokenReadTest() throws 
AccessControlException {
         readFailWithPath("/tmp/tmpdir5/t/data-file.txt");
     }
 
+    @Test
+    public void testOperationOptimizerRenameDirectorySourceEvaluated() throws 
Exception {
+        runWithAuthzOptimizationEnabled(true, () -> {
+            AccessControlEnforcer enforcer = 
authorizer.getExternalAccessControlEnforcer(null);
+            INode[]               inodes   = createPath("/tmp/srcdir", false, 
true);
+
+            checkPermission(enforcer, "bob", inodes, "/tmp/srcdir", null, 
null, null, null, false, OPERATION_NAME_RENAME);
+
+            Assertions.assertThrows(AccessControlException.class, () ->
+                    checkPermission(enforcer, "bob", inodes, "/tmp/srcdir", 
null, FsAction.WRITE, null, FsAction.ALL, true, OPERATION_NAME_RENAME));
+
+            // mv of a directory: no subAccess
+            Assertions.assertThrows(AccessControlException.class, () ->
+                    
checkPermission(authorizer.getExternalAccessControlEnforcer(null), "bob", 
inodes, "/tmp/srcdir", null, FsAction.WRITE, null, null, false, 
OPERATION_NAME_RENAME));
+
+            checkPermission(authorizer.getExternalAccessControlEnforcer(null), 
"bob", createPath("/tmp/tmpdir2/srcdir", false, true), "/tmp/tmpdir2/srcdir", 
null, FsAction.WRITE, null, null, false, OPERATION_NAME_RENAME);
+        });
+
+        runWithAuthzOptimizationEnabled(false, () -> {
+            Assertions.assertThrows(AccessControlException.class, () ->
+                    
checkPermission(authorizer.getExternalAccessControlEnforcer(null), "bob", 
createPath("/tmp/srcdir", false, true), "/tmp/srcdir", null, FsAction.WRITE, 
null, FsAction.ALL, true, OPERATION_NAME_RENAME));
+        });
+    }
+
+    @Test
+    public void testOperationOptimizerRenameFileUnchanged() throws Exception {
+        runWithAuthzOptimizationEnabled(true, () -> {
+            AccessControlEnforcer enforcer = 
authorizer.getExternalAccessControlEnforcer(null);
+
+            checkPermission(enforcer, "bob", 
createPath("/tmp/tmpdir2/data-file3", true, true), "/tmp/tmpdir2/data-file3", 
null, null, null, null, false, OPERATION_NAME_RENAME);
+
+            Assertions.assertThrows(AccessControlException.class, () ->
+                    
checkPermission(authorizer.getExternalAccessControlEnforcer(null), "bob", 
createPath("/tmp/data-file3", true, true), "/tmp/data-file3", null, null, null, 
null, false, OPERATION_NAME_RENAME));
+            Assertions.assertThrows(AccessControlException.class, () ->
+                    
checkPermission(authorizer.getExternalAccessControlEnforcer(null), "bob", 
createPath("/tmp/data-file3", true, true), "/tmp/data-file3", null, 
FsAction.WRITE, null, null, true, OPERATION_NAME_RENAME));
+        });
+
+        runWithAuthzOptimizationEnabled(false, () -> {
+            Assertions.assertThrows(AccessControlException.class, () ->
+                    
checkPermission(authorizer.getExternalAccessControlEnforcer(null), "bob", 
createPath("/tmp/data-file3", true, true), "/tmp/data-file3", null, 
FsAction.WRITE, null, null, true, OPERATION_NAME_RENAME));
+        });
+    }
+
+    @Test
+    public void testOperationOptimizerDeleteRegression() throws Exception {
+        runWithAuthzOptimizationEnabled(true, () -> {
+            AccessControlEnforcer enforcer = 
authorizer.getExternalAccessControlEnforcer(null);
+            INode[]               inodes   = createPath("/tmp/srcdir", false, 
true);
+
+            checkPermission(enforcer, "bob", inodes, "/tmp/srcdir", null, 
null, null, null, false, OPERATION_NAME_DELETE);
+
+            Assertions.assertThrows(AccessControlException.class, () ->
+                    checkPermission(enforcer, "bob", inodes, "/tmp/srcdir", 
null, FsAction.WRITE, null, FsAction.ALL, true, OPERATION_NAME_DELETE));
+        });
+    }
+
     private void readFailWithPath(String path) {
         final TestFileSystem fs = new TestFileSystem(path);
         fs.setFilePermission(READ_ONLY);
@@ -240,6 +299,79 @@ private static INode createNode(String[] pathSegments, int 
i, String owner, Stri
         return mock;
     }
 
+    private static INode[] createPath(String path, boolean file, boolean 
exists) {
+        String[] segments = path.split("/");
+        INode[]  ret      = new INode[segments.length];
+
+        for (int i = 0; i < segments.length; i++) {
+            boolean isLast = i == segments.length - 1;
+
+            ret[i] = isLast && !exists ? null : createNode(segments, i, 
FILE_OWNER, FILE_GROUP, isLast && file);
+        }
+
+        return ret;
+    }
+
+    private static void runWithAuthzOptimizationEnabled(boolean enabled, 
ThrowingRunnable runnable) throws Exception {
+        boolean previous = setAuthzOptimizationEnabled(enabled);
+
+        try {
+            runnable.run();
+        } finally {
+            setAuthzOptimizationEnabled(previous);
+        }
+    }
+
+    private static boolean setAuthzOptimizationEnabled(boolean enabled) throws 
ReflectiveOperationException {
+        Field   pluginField = 
RangerHdfsAuthorizer.class.getDeclaredField("rangerPlugin");
+        Object  plugin;
+        Field   flagField;
+        boolean previous;
+
+        pluginField.setAccessible(true);
+        plugin    = pluginField.get(authorizer);
+        flagField = 
plugin.getClass().getDeclaredField("authzOptimizationEnabled");
+        flagField.setAccessible(true);
+        previous = flagField.getBoolean(plugin);
+        flagField.setBoolean(plugin, enabled);
+
+        return previous;
+    }
+
+    private static void checkPermission(AccessControlEnforcer enforcer, String 
userName, INode[] inodes, String path, FsAction ancestorAccess, FsAction 
parentAccess, FsAction access, FsAction subAccess, boolean ignoreEmptyDir, 
String operationName) throws AccessControlException {
+        INodeAttributes[] attrs = new INodeAttributes[inodes.length];
+
+        for (int i = 0; i < inodes.length; i++) {
+            attrs[i] = inodes[i];
+        }
+
+        INodeAttributeProvider.AuthorizationContext.Builder builder =
+                new INodeAttributeProvider.AuthorizationContext.Builder()
+                        .fsOwner(FILE_OWNER)
+                        .supergroup(FILE_GROUP)
+                        
.callerUgi(UserGroupInformation.createUserForTesting(userName, new String[0]))
+                        .inodeAttrs(attrs)
+                        .inodes(inodes)
+                        .pathByNameArr(new byte[0][0])
+                        .snapshotId(SNAPSHOT_ID)
+                        .path(path)
+                        .ancestorIndex(inodes.length - 2)
+                        .doCheckOwner(false)
+                        .ancestorAccess(ancestorAccess)
+                        .parentAccess(parentAccess)
+                        .access(access)
+                        .subAccess(subAccess)
+                        .ignoreEmptyDir(ignoreEmptyDir)
+                        .operationName(operationName)
+                        .callerContext(null);
+
+        enforcer.checkPermissionWithContext(new 
INodeAttributeProvider.AuthorizationContext(builder));
+    }
+
+    private interface ThrowingRunnable {
+        void run() throws Exception;
+    }
+
     static class TestFileSystem {
         final String            path;
         final String[]          pathSegments;

Reply via email to