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

sammichen pushed a commit to branch HDDS-13323-sts
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/HDDS-13323-sts by this push:
     new b4ff104a446 HDDS-13925. [STS] Part 2 - Create utility to convert IAM 
policy to groupings of OzoneObj and Acls (#9292)
b4ff104a446 is described below

commit b4ff104a446d2acee9f9e7b6e252962cab3b5843
Author: fmorg-git <[email protected]>
AuthorDate: Mon Nov 24 22:15:44 2025 -0800

    HDDS-13925. [STS] Part 2 - Create utility to convert IAM policy to 
groupings of OzoneObj and Acls (#9292)
---
 .../security/acl/iam/IamSessionPolicyResolver.java | 143 +++++++++++++++++-
 .../acl/iam/TestIamSessionPolicyResolver.java      | 165 ++++++++++++++++++++-
 2 files changed, 297 insertions(+), 11 deletions(-)

diff --git 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/security/acl/iam/IamSessionPolicyResolver.java
 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/security/acl/iam/IamSessionPolicyResolver.java
index 23fbf063c87..dbf3dc9e076 100644
--- 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/security/acl/iam/IamSessionPolicyResolver.java
+++ 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/security/acl/iam/IamSessionPolicyResolver.java
@@ -17,19 +17,31 @@
 
 package org.apache.hadoop.ozone.security.acl.iam;
 
+import static java.util.Collections.singleton;
 import static 
org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_REQUEST;
 import static 
org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.NOT_SUPPORTED_OPERATION;
+import static 
org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLType.CREATE;
+import static 
org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLType.DELETE;
+import static 
org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLType.LIST;
+import static 
org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLType.READ;
+import static 
org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLType.READ_ACL;
+import static 
org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLType.WRITE_ACL;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.annotations.VisibleForTesting;
 import java.util.Collections;
+import java.util.EnumSet;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.hadoop.ozone.om.exceptions.OMException;
 import org.apache.hadoop.ozone.security.acl.AssumeRoleRequest;
+import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLType;
 
 /**
  * Resolves a limited subset of AWS IAM session policies into Ozone ACL grants,
@@ -74,6 +86,12 @@ public final class IamSessionPolicyResolver {
   // under Policy section.
   private static final int MAX_JSON_LENGTH = 2048;
 
+  // Used to group actions into s3:Get*, s3:Put*, s3:List*, s3:Delete*, 
s3:Create*
+  private static final String[] S3_ACTION_PREFIXES = {"s3:Get", "s3:Put", 
"s3:List", "s3:Delete", "s3:Create"};
+
+  @VisibleForTesting
+  static final Map<String, Set<S3Action>> S3_ACTION_MAP_CI = 
buildCaseInsensitiveS3ActionMap();
+
   private IamSessionPolicyResolver() {
   }
 
@@ -275,13 +293,54 @@ private static Set<String> 
parsePrefixesFromConditions(JsonNode stmt) throws OME
     return prefixes;
   }
 
+  /**
+   * Builds a case-insensitive S3Action map by lowercasing keys.  This map is 
used for mapping policy actions to
+   * S3Action enum values.  This map is built once and cached statically.
+   */
+  @VisibleForTesting
+  static Map<String, Set<S3Action>> buildCaseInsensitiveS3ActionMap() {
+    final Map<String, Set<S3Action>> ciMap = new LinkedHashMap<>();
+    for (S3Action sa : S3Action.values()) {
+      // Exact action mapping
+      ciMap.put(sa.name.toLowerCase(), singleton(sa));
+
+      // Group into s3:Get*, s3:Put*, s3:List*, s3:Delete*, s3:Create* based 
on action name prefix
+      for (String prefix : S3_ACTION_PREFIXES) {
+        if (sa.name.startsWith(prefix)) {
+          final String wildcardKey = (prefix + "*").toLowerCase();
+          ciMap.computeIfAbsent(wildcardKey, k -> new 
LinkedHashSet<>()).add(sa);
+          break;
+        }
+      }
+    }
+    return Collections.unmodifiableMap(ciMap);
+  }
+
   /**
    * Maps actions from JSON IAM policy to S3Action enum in order to determine 
what the
    * permissions should be.
    */
-  private static Set<S3Action> mapPolicyActionsToS3Actions(Set<String> 
actions) {
-    // TODO implement in future PR
-    return Collections.emptySet();
+  @VisibleForTesting
+  static Set<S3Action> mapPolicyActionsToS3Actions(Set<String> actions) {
+    if (actions == null || actions.isEmpty()) {
+      return Collections.emptySet();
+    }
+
+    // Map the actions from the IAM policy to S3Action
+    final Set<S3Action> mappedActions = new LinkedHashSet<>();
+    for (String action : actions) {
+      if ("s3:*".equalsIgnoreCase(action)) {
+        return EnumSet.of(S3Action.ALL_S3);
+      }
+
+      // Unsupported actions are silently ignored
+      final Set<S3Action> s3Actions = 
S3_ACTION_MAP_CI.get(action.toLowerCase());
+      if (s3Actions != null) {
+        mappedActions.addAll(s3Actions);
+      }
+    }
+
+    return mappedActions;
   }
 
   /**
@@ -321,16 +380,84 @@ public enum AuthorizerType {
   }
 
   /**
-   * Utility to help categorize IAM policy resources, whether for bucket, key, 
wildcards, etc.
+   * The type of resource the S3 action applies to.
    */
-  private static final class ResourceSpec {
-    // TODO implement in future PR
+  private enum ActionKind {
+    VOLUME,
+    BUCKET,
+    OBJECT,
+    ALL
   }
 
   /**
-   * Represents S3 actions and requisite permissions required and at what 
level.
+   * Utility to help categorize IAM policy resources, whether for bucket, key, 
wildcards, etc.
    */
-  private enum S3Action {
+  private static final class ResourceSpec {
     // TODO implement in future PR
   }
+
+  @VisibleForTesting
+  enum S3Action {
+    // Volume-scope
+    // Used for ListBuckets api
+    LIST_ALL_MY_BUCKETS("s3:ListAllMyBuckets", ActionKind.VOLUME, 
EnumSet.of(READ, LIST),
+        EnumSet.noneOf(ACLType.class), EnumSet.noneOf(ACLType.class)),
+
+    // Bucket-scope
+    CREATE_BUCKET("s3:CreateBucket", ActionKind.BUCKET, EnumSet.of(READ), 
EnumSet.of(CREATE),
+        EnumSet.noneOf(ACLType.class)),
+    DELETE_BUCKET("s3:DeleteBucket", ActionKind.BUCKET, EnumSet.of(READ), 
EnumSet.of(DELETE),
+        EnumSet.noneOf(ACLType.class)),
+    GET_BUCKET_ACL("s3:GetBucketAcl", ActionKind.BUCKET, EnumSet.of(READ), 
EnumSet.of(READ, READ_ACL),
+        EnumSet.noneOf(ACLType.class)),
+    GET_BUCKET_LOCATION("s3:GetBucketLocation", ActionKind.BUCKET, 
EnumSet.of(READ), EnumSet.of(READ),
+        EnumSet.noneOf(ACLType.class)),
+    // Used for HeadBucket, ListObjects and ListObjectsV2 apis
+    LIST_BUCKET("s3:ListBucket", ActionKind.BUCKET, EnumSet.of(READ), 
EnumSet.of(READ, LIST),
+        EnumSet.noneOf(ACLType.class)),
+    // Used for ListMultipartUploads API
+    LIST_BUCKET_MULTIPART_UPLOADS("s3:ListBucketMultipartUploads", 
ActionKind.BUCKET, EnumSet.of(READ),
+        EnumSet.of(READ, LIST), EnumSet.noneOf(ACLType.class)),
+    PUT_BUCKET_ACL("s3:PutBucketAcl", ActionKind.BUCKET, EnumSet.of(READ), 
EnumSet.of(WRITE_ACL),
+        EnumSet.noneOf(ACLType.class)),
+
+    // Object-scope
+    ABORT_MULTIPART_UPLOAD("s3:AbortMultipartUpload", ActionKind.OBJECT, 
EnumSet.of(READ), EnumSet.of(READ),
+        EnumSet.of(DELETE)),
+    // Used for DeleteObject (when versionId parameter is not supplied),
+    // DeleteObjects (when versionId parameter is not supplied) APIs
+    DELETE_OBJECT("s3:DeleteObject", ActionKind.OBJECT, EnumSet.of(READ), 
EnumSet.of(READ), EnumSet.of(DELETE)),
+    DELETE_OBJECT_TAGGING("s3:DeleteObjectTagging", ActionKind.OBJECT, 
EnumSet.of(READ), EnumSet.of(READ),
+        EnumSet.of(DELETE)),
+    // Used for HeadObject, CopyObject (for source bucket), GetObject (without 
versionId parameter) APIs
+    GET_OBJECT("s3:GetObject", ActionKind.OBJECT, EnumSet.of(READ), 
EnumSet.of(READ), EnumSet.of(READ)),
+    GET_OBJECT_TAGGING("s3:GetObjectTagging", ActionKind.OBJECT, 
EnumSet.of(READ), EnumSet.of(READ), EnumSet.of(READ)),
+    // Used for ListParts API
+    LIST_MULTIPART_UPLOAD_PARTS("s3:ListMultipartUploadParts", 
ActionKind.OBJECT, EnumSet.of(READ), EnumSet.of(READ),
+        EnumSet.of(READ)),
+    // Used for CreateMultipartUpload, UploadPart, CompleteMultipartUpload,
+    // CopyObject (for destination bucket), PutObject APIs
+    PUT_OBJECT("s3:PutObject", ActionKind.OBJECT, EnumSet.of(READ), 
EnumSet.of(READ),
+        EnumSet.of(CREATE, ACLType.WRITE)),
+    PUT_OBJECT_TAGGING("s3:PutObjectTagging", ActionKind.OBJECT, 
EnumSet.of(READ), EnumSet.of(READ),
+        EnumSet.of(ACLType.WRITE)),
+
+    // Wildcard all
+    ALL_S3("s3:*", ActionKind.ALL, EnumSet.of(ACLType.ALL), 
EnumSet.of(ACLType.ALL), EnumSet.of(ACLType.ALL));
+
+    private final String name;
+    private final ActionKind kind;
+    private final Set<ACLType> volumePerms;
+    private final Set<ACLType> bucketPerms;
+    private final Set<ACLType> objectPerms;
+
+    S3Action(String name, ActionKind kind, Set<ACLType> volumePerms, 
Set<ACLType> bucketPerms,
+        Set<ACLType> objectPerms) {
+      this.name = name;
+      this.kind = kind;
+      this.volumePerms = volumePerms;
+      this.bucketPerms = bucketPerms;
+      this.objectPerms = objectPerms;
+    }
+  }
 }
diff --git 
a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/security/acl/iam/TestIamSessionPolicyResolver.java
 
b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/security/acl/iam/TestIamSessionPolicyResolver.java
index d37f441a51e..5721901b19c 100644
--- 
a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/security/acl/iam/TestIamSessionPolicyResolver.java
+++ 
b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/security/acl/iam/TestIamSessionPolicyResolver.java
@@ -21,9 +21,16 @@
 import static 
org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.NOT_SUPPORTED_OPERATION;
 import static 
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.AuthorizerType.NATIVE;
 import static 
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.AuthorizerType.RANGER;
+import static 
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.buildCaseInsensitiveS3ActionMap;
+import static 
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.mapPolicyActionsToS3Actions;
 import static org.assertj.core.api.Assertions.assertThat;
 
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
 import org.apache.hadoop.ozone.om.exceptions.OMException;
+import 
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action;
 import org.junit.jupiter.api.Test;
 
 /**
@@ -257,6 +264,158 @@ public void testEffectMustBeCaseSensitive() {
         json, "Unsupported Effect - aLLOw", NOT_SUPPORTED_OPERATION);
   }
 
+  @Test
+  public void testBuildCaseInsensitiveS3ActionMapMatchesConstant() {
+    
assertThat(buildCaseInsensitiveS3ActionMap()).isEqualTo(IamSessionPolicyResolver.S3_ACTION_MAP_CI);
+  }
+
+  @Test
+  public void testBuildCaseInsensitiveS3ActionMap() {
+    final Map<String, Set<S3Action>> caseInsensitiveS3ActionMap = 
buildCaseInsensitiveS3ActionMap();
+    
+    // Verify that individual S3 actions are present
+    assertThat(caseInsensitiveS3ActionMap).containsKeys(
+        "s3:listbucket", "s3:getobject", "s3:putobject", "s3:deleteobject", 
"s3:createbucket", "s3:listallmybuckets");
+
+    // Verify that wildcard actions are present
+    assertThat(caseInsensitiveS3ActionMap).containsKeys(
+        "s3:*", "s3:get*", "s3:put*", "s3:list*", "s3:delete*", "s3:create*");
+
+    // Verify s3:Get* contains Get actions
+    final Set<S3Action> getActions = caseInsensitiveS3ActionMap.get("s3:get*");
+    assertThat(getActions).containsOnly(
+        S3Action.GET_OBJECT, S3Action.GET_BUCKET_ACL, 
S3Action.GET_BUCKET_LOCATION, S3Action.GET_OBJECT_TAGGING);
+
+    // Verify s3:Put* contains Put actions
+    final Set<S3Action> putActions = caseInsensitiveS3ActionMap.get("s3:put*");
+    assertThat(putActions).containsOnly(
+        S3Action.PUT_OBJECT, S3Action.PUT_OBJECT_TAGGING, 
S3Action.PUT_BUCKET_ACL);
+
+    // Verify s3:List* contains List actions
+    final Set<S3Action> listActions = 
caseInsensitiveS3ActionMap.get("s3:list*");
+    assertThat(listActions).containsOnly(
+        S3Action.LIST_BUCKET, S3Action.LIST_ALL_MY_BUCKETS, 
S3Action.LIST_BUCKET_MULTIPART_UPLOADS,
+        S3Action.LIST_MULTIPART_UPLOAD_PARTS);
+
+    // Verify s3:Delete* contains Delete actions
+    final Set<S3Action> deleteActions = 
caseInsensitiveS3ActionMap.get("s3:delete*");
+    assertThat(deleteActions).containsOnly(
+        S3Action.DELETE_OBJECT, S3Action.DELETE_BUCKET, 
S3Action.DELETE_OBJECT_TAGGING);
+
+    // Verify s3:Create* contains Create actions
+    final Set<S3Action> createActions = 
caseInsensitiveS3ActionMap.get("s3:create*");
+    assertThat(createActions).containsOnly(S3Action.CREATE_BUCKET);
+  }
+
+  @Test
+  public void 
testBuildCaseInsensitiveS3ActionMapIndividualActionsContainSingleEntry() {
+    final Map<String, Set<S3Action>> actionMap = 
buildCaseInsensitiveS3ActionMap();
+    
+    // Individual actions should map to a set with exactly one entry
+    final Set<S3Action> listBucketAction = actionMap.get("s3:listbucket");
+    assertThat(listBucketAction).hasSize(1);
+    
+    final Set<S3Action> getObjectAction = actionMap.get("s3:getobject");
+    assertThat(getObjectAction).hasSize(1);
+  }
+
+  @Test
+  public void testMapPolicyActionsToS3ActionsWithNullReturnsEmpty() {
+    final Set<S3Action> result = mapPolicyActionsToS3Actions(null);
+    assertThat(result).isEmpty();
+  }
+
+  @Test
+  public void testMapPolicyActionsToS3ActionsWithEmptyListReturnsEmpty() {
+    final Set<S3Action> result = 
mapPolicyActionsToS3Actions(Collections.emptySet());
+    assertThat(result).isEmpty();
+  }
+
+  @Test
+  public void testMapPolicyActionsToS3ActionsWithSingleActionMapsCorrectly() {
+    final Set<S3Action> listBucket = 
mapPolicyActionsToS3Actions(Collections.singleton("s3:ListBucket"));
+    assertThat(listBucket).containsOnly(S3Action.LIST_BUCKET);
+
+    // Ensure case-insensitive action works
+    final Set<S3Action> listBucketCi = 
mapPolicyActionsToS3Actions(Collections.singleton("S3:ListBuCKet"));
+    assertThat(listBucketCi).containsOnly(S3Action.LIST_BUCKET);
+
+    final Set<S3Action> deleteObject = 
mapPolicyActionsToS3Actions(Collections.singleton("s3:DeleteObject"));
+    assertThat(deleteObject).containsOnly(S3Action.DELETE_OBJECT);
+
+    // Ensure case-insensitive action works
+    final Set<S3Action> deleteObjectCi = 
mapPolicyActionsToS3Actions(Collections.singleton("S3:DeLETeObjeCT"));
+    assertThat(deleteObjectCi).containsOnly(S3Action.DELETE_OBJECT);
+  }
+
+  @Test
+  public void 
testMapPolicyActionsToS3ActionsWithMultipleActionsMapAllCorrectly() {
+    final Set<S3Action> result = 
mapPolicyActionsToS3Actions(strSet("s3:ListBucket", "s3:GetObject", 
"s3:PutObject"));
+    assertThat(result).containsOnly(S3Action.LIST_BUCKET, S3Action.GET_OBJECT, 
S3Action.PUT_OBJECT);
+  }
+
+  @Test
+  public void testMapPolicyActionsToS3ActionsWithWildcardExpansion() {
+    final Set<S3Action> result = 
mapPolicyActionsToS3Actions(Collections.singleton("s3:Get*"));
+    assertThat(result).containsOnly(S3Action.GET_OBJECT, 
S3Action.GET_BUCKET_ACL, S3Action.GET_BUCKET_LOCATION,
+        S3Action.GET_OBJECT_TAGGING);
+
+    // Ensure it is case-insensitive
+    final Set<S3Action> resultCi = 
mapPolicyActionsToS3Actions(Collections.singleton("s3:gET*"));
+    assertThat(resultCi).containsOnly(S3Action.GET_OBJECT, 
S3Action.GET_BUCKET_ACL, S3Action.GET_BUCKET_LOCATION,
+        S3Action.GET_OBJECT_TAGGING);
+  }
+
+  @Test
+  public void testMapPolicyActionsToS3ActionsWithS3StarReturnsAll() {
+    final Set<S3Action> result = 
mapPolicyActionsToS3Actions(Collections.singleton("s3:*"));
+    assertThat(result).containsOnly(S3Action.ALL_S3);
+
+    final Set<S3Action> resultCi = 
mapPolicyActionsToS3Actions(Collections.singleton("S3:*"));
+    assertThat(resultCi).containsOnly(S3Action.ALL_S3);
+  }
+
+  @Test
+  public void testMapPolicyActionsToS3ActionsIgnoresUnsupportedActions() {
+    final Set<S3Action> result = 
mapPolicyActionsToS3Actions(strSet("s3:GetAccelerateConfiguration", 
"s3:GetObject"));
+    // Unsupported action should be silently ignored
+    assertThat(result).containsOnly(S3Action.GET_OBJECT);
+  }
+
+  @Test
+  public void 
testMapPolicyActionsToS3ActionsWithOnlyUnsupportedActionsReturnsEmpty() {
+    final Set<S3Action> result = mapPolicyActionsToS3Actions(
+        strSet("s3:GetAccelerateConfiguration", "s3:PutBucketVersioning"));
+    assertThat(result).isEmpty();
+  }
+
+  @Test
+  public void testMapPolicyActionsToS3ActionsDeduplicatesResults() {
+    final Set<S3Action> result = mapPolicyActionsToS3Actions(strSet("s3:Get*", 
"s3:GetObject", "s3:GetBucketAcl"));
+    assertThat(result).containsOnly(S3Action.GET_OBJECT, 
S3Action.GET_BUCKET_ACL, S3Action.GET_BUCKET_LOCATION,
+        S3Action.GET_OBJECT_TAGGING);
+  }
+
+  @Test
+  public void testMapPolicyActionsToS3ActionsHandlesMultipleWildcards() {
+    final Set<S3Action> result = mapPolicyActionsToS3Actions(strSet("s3:Get*", 
"s3:Put*"));
+    assertThat(result).containsOnly(S3Action.GET_OBJECT, 
S3Action.GET_BUCKET_ACL, S3Action.GET_BUCKET_LOCATION,
+        S3Action.GET_OBJECT_TAGGING, S3Action.PUT_OBJECT, 
S3Action.PUT_OBJECT_TAGGING, S3Action.PUT_BUCKET_ACL);
+  }
+
+  @Test
+  public void testMapPolicyActionsToS3ActionsWithS3StarIgnoresOtherActions() {
+    final Set<S3Action> result = mapPolicyActionsToS3Actions(strSet("s3:*", 
"s3:GetObject", "s3:PutObject"));
+    // When s3:* is present, it should return only the ALL_S3 action
+    assertThat(result).containsOnly(S3Action.ALL_S3);
+  }
+
+  private static Set<String> strSet(String... strs) {
+    final Set<String> s = new LinkedHashSet<>();
+    Collections.addAll(s, strs);
+    return s;
+  }
+
   private static void expectResolveThrows(String json,
       IamSessionPolicyResolver.AuthorizerType authorizerType, String 
expectedMessage,
       OMException.ResultCodes expectedCode) {
@@ -268,9 +427,9 @@ private static void expectResolveThrows(String json,
       assertThat(ex.getResult()).isEqualTo(expectedCode);
     }
   }
-  
-  private static void expectResolveThrowsForBothAuthorizers(String json,
-      String expectedMessage, OMException.ResultCodes expectedCode) {
+
+  private static void expectResolveThrowsForBothAuthorizers(String json, 
String expectedMessage,
+      OMException.ResultCodes expectedCode) {
     expectResolveThrows(json, NATIVE, expectedMessage, expectedCode);
     expectResolveThrows(json, RANGER, expectedMessage, expectedCode);
   }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to