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

smengcl pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new a6332fb5987 HDDS-16724. Add support for missing S3 apis to STS (#11413)
a6332fb5987 is described below

commit a6332fb59879b80615b9233e953d6813d03ae7a8
Author: fmorg-git <[email protected]>
AuthorDate: Thu Oct 8 01:12:24 2026 -0700

    HDDS-16724. Add support for missing S3 apis to STS (#11413)
    
    Co-authored-by: Fabian Morgan <[email protected]>
---
 .../security/acl/iam/IamSessionPolicyResolver.java |  10 ++
 .../acl/iam/TestIamSessionPolicyResolver.java      | 140 +++++++++++----
 .../main/smoketest/security/ozone-secure-sts.robot | 200 ++++++++++++++++++++-
 .../apache/hadoop/ozone/om/OmMetadataReader.java   |  34 +++-
 .../hadoop/ozone/om/TestOMMetadataReader.java      |  73 ++++++++
 .../hadoop/ozone/s3/endpoint/EndpointBase.java     |   1 +
 .../ozone/s3/endpoint/ObjectAttributesHandler.java |   2 +
 .../hadoop/ozone/s3/util/S3GActionIamMapper.java   |  13 ++
 .../TestS3ActionOverrideForOwnerVerification.java  | 143 ++++++++++++++-
 .../ozone/s3/util/TestS3GActionIamMapper.java      |   7 +
 10 files changed, 578 insertions(+), 45 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 6dcf468f6b7..50ea6f3107a 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
@@ -1140,6 +1140,14 @@ enum S3Action {
         EnumSet.of(READ, LIST), EnumSet.noneOf(ACLType.class)),
     PUT_BUCKET_ACL("s3:PutBucketAcl", ActionKind.BUCKET, EnumSet.of(READ), 
EnumSet.of(READ, READ_ACL, WRITE_ACL),
         EnumSet.noneOf(ACLType.class)),
+    GET_BUCKET_TAGGING("s3:GetBucketTagging", ActionKind.BUCKET, 
EnumSet.of(READ), EnumSet.of(READ),
+        EnumSet.noneOf(ACLType.class)),
+    PUT_BUCKET_TAGGING("s3:PutBucketTagging", ActionKind.BUCKET, 
EnumSet.of(READ), EnumSet.of(READ, WRITE),
+        EnumSet.noneOf(ACLType.class)),
+    GET_BUCKET_LIFECYCLE("s3:GetLifecycleConfiguration", ActionKind.BUCKET, 
EnumSet.of(READ), EnumSet.of(READ),
+        EnumSet.noneOf(ACLType.class)),
+    PUT_BUCKET_LIFECYCLE("s3:PutLifecycleConfiguration", ActionKind.BUCKET, 
EnumSet.of(READ), EnumSet.of(READ, WRITE),
+        EnumSet.noneOf(ACLType.class)),
 
     // Object-scope
     ABORT_MULTIPART_UPLOAD("s3:AbortMultipartUpload", ActionKind.OBJECT, 
EnumSet.of(READ), EnumSet.of(READ),
@@ -1151,6 +1159,8 @@ enum S3Action {
         EnumSet.of(WRITE)),
     // 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_ATTRIBUTES("s3:GetObjectAttributes", 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),
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 54e3be080ea..4b32cab1d73 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
@@ -37,13 +37,18 @@
 import static 
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.DELETE_OBJECT;
 import static 
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.DELETE_OBJECT_TAGGING;
 import static 
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.GET_BUCKET_ACL;
+import static 
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.GET_BUCKET_LIFECYCLE;
+import static 
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.GET_BUCKET_TAGGING;
 import static 
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.GET_OBJECT;
+import static 
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.GET_OBJECT_ATTRIBUTES;
 import static 
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.GET_OBJECT_TAGGING;
 import static 
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.LIST_ALL_MY_BUCKETS;
 import static 
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.LIST_BUCKET;
 import static 
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.LIST_BUCKET_MULTIPART_UPLOADS;
 import static 
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.LIST_MULTIPART_UPLOAD_PARTS;
 import static 
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.PUT_BUCKET_ACL;
+import static 
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.PUT_BUCKET_LIFECYCLE;
+import static 
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.PUT_BUCKET_TAGGING;
 import static 
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.PUT_OBJECT;
 import static 
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.PUT_OBJECT_TAGGING;
 import static 
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3ResourceType;
@@ -77,11 +82,12 @@ public class TestIamSessionPolicyResolver {
 
   private static final String VOLUME = "s3v";
   private static final Set<String> ALL_OBJECT_ACTIONS = strSet(
-      "AbortMultipartUpload", "DeleteObject", "DeleteObjectTagging", 
"GetObject", "GetObjectTagging",
-      "ListMultipartUploadParts", "PutObject", "PutObjectTagging");
+      "AbortMultipartUpload", "DeleteObject", "DeleteObjectTagging", 
"GetObject", "GetObjectAttributes",
+      "GetObjectTagging", "ListMultipartUploadParts", "PutObject", 
"PutObjectTagging");
 
   private static final Set<String> ALL_BUCKET_ACTIONS = strSet(
-      "CreateBucket", "DeleteBucket", "GetBucketAcl", "ListBucket", 
"ListBucketMultipartUploads", "PutBucketAcl");
+      "CreateBucket", "DeleteBucket", "GetBucketAcl", "GetBucketTagging", 
"GetLifecycleConfiguration", "ListBucket",
+      "ListBucketMultipartUploads", "PutBucketAcl", "PutBucketTagging", 
"PutLifecycleConfiguration");
 
   private static final Set<String> ALL_BUCKET_ACTIONS_WITH_LIST_ALL_MY_BUCKETS;
 
@@ -797,11 +803,14 @@ public void testBuildCaseInsensitiveS3ActionMap() {
 
     // Verify s3:Get* contains Get actions
     final Set<S3Action> getActions = caseInsensitiveS3ActionMap.get("s3:get*");
-    assertThat(getActions).containsOnly(GET_OBJECT, GET_BUCKET_ACL, 
GET_OBJECT_TAGGING);
+    assertThat(getActions).containsOnly(
+        GET_OBJECT, GET_OBJECT_ATTRIBUTES, GET_BUCKET_ACL, GET_OBJECT_TAGGING, 
GET_BUCKET_TAGGING,
+        GET_BUCKET_LIFECYCLE);
 
     // Verify s3:Put* contains Put actions
     final Set<S3Action> putActions = caseInsensitiveS3ActionMap.get("s3:put*");
-    assertThat(putActions).containsOnly(PUT_OBJECT, PUT_OBJECT_TAGGING, 
PUT_BUCKET_ACL);
+    assertThat(putActions).containsOnly(
+        PUT_OBJECT, PUT_OBJECT_TAGGING, PUT_BUCKET_ACL, PUT_BUCKET_TAGGING, 
PUT_BUCKET_LIFECYCLE);
 
     // Verify s3:List* contains List actions
     final Set<S3Action> listActions = 
caseInsensitiveS3ActionMap.get("s3:list*");
@@ -867,20 +876,21 @@ public void 
testMapPolicyActionsToS3ActionsWithMultipleActionsMapAllCorrectly()
   @Test
   public void testMapPolicyActionsToS3ActionsWithWildcardExpansion() {
     final Set<S3Action> result = 
mapPolicyActionsToS3Actions(Collections.singleton("s3:Get*"));
-    assertThat(result).containsOnly(GET_OBJECT, GET_BUCKET_ACL, 
GET_OBJECT_TAGGING);
+    assertThat(result).containsOnly(
+        GET_OBJECT, GET_OBJECT_ATTRIBUTES, GET_BUCKET_ACL, GET_OBJECT_TAGGING, 
GET_BUCKET_TAGGING,
+        GET_BUCKET_LIFECYCLE);
 
     // Ensure it is case-insensitive
     final Set<S3Action> resultCi = 
mapPolicyActionsToS3Actions(Collections.singleton("s3:gET*"));
-    assertThat(resultCi).containsOnly(GET_OBJECT, GET_BUCKET_ACL, 
GET_OBJECT_TAGGING);
+    assertThat(resultCi).containsOnly(
+        GET_OBJECT, GET_OBJECT_ATTRIBUTES, GET_BUCKET_ACL, GET_OBJECT_TAGGING, 
GET_BUCKET_TAGGING,
+        GET_BUCKET_LIFECYCLE);
   }
 
   @Test
   public void testMapPolicyActionsToS3ActionsWithS3StarReturnsAll() {
     final Set<S3Action> result = 
mapPolicyActionsToS3Actions(Collections.singleton("s3:*"));
-    assertThat(result).containsOnly(
-        LIST_ALL_MY_BUCKETS, CREATE_BUCKET, DELETE_BUCKET, GET_BUCKET_ACL, 
LIST_BUCKET, LIST_BUCKET_MULTIPART_UPLOADS,
-        PUT_BUCKET_ACL, ABORT_MULTIPART_UPLOAD, DELETE_OBJECT, 
DELETE_OBJECT_TAGGING, GET_OBJECT, GET_OBJECT_TAGGING,
-        LIST_MULTIPART_UPLOAD_PARTS, PUT_OBJECT, PUT_OBJECT_TAGGING);
+    assertThat(result).containsOnly(S3Action.values());
 
     final Set<S3Action> resultCi = 
mapPolicyActionsToS3Actions(Collections.singleton("S3:*"));
     assertThat(resultCi).isEqualTo(result);
@@ -903,24 +913,25 @@ public void 
testMapPolicyActionsToS3ActionsWithOnlyUnsupportedActionsReturnsEmpt
   @Test
   public void testMapPolicyActionsToS3ActionsDeduplicatesResults() {
     final Set<S3Action> result = mapPolicyActionsToS3Actions(strSet("s3:Get*", 
"s3:GetObject", "s3:GetBucketAcl"));
-    assertThat(result).containsOnly(GET_OBJECT, GET_BUCKET_ACL, 
GET_OBJECT_TAGGING);
+    assertThat(result).containsOnly(
+        GET_OBJECT, GET_OBJECT_ATTRIBUTES, GET_BUCKET_ACL, GET_OBJECT_TAGGING, 
GET_BUCKET_TAGGING,
+        GET_BUCKET_LIFECYCLE);
   }
 
   @Test
   public void testMapPolicyActionsToS3ActionsHandlesMultipleWildcards() {
     final Set<S3Action> result = mapPolicyActionsToS3Actions(strSet("s3:Get*", 
"s3:Put*"));
     assertThat(result).containsOnly(
-        GET_OBJECT, GET_BUCKET_ACL, GET_OBJECT_TAGGING, PUT_OBJECT, 
PUT_OBJECT_TAGGING, PUT_BUCKET_ACL);
+        GET_OBJECT, GET_OBJECT_ATTRIBUTES, GET_BUCKET_ACL, GET_OBJECT_TAGGING, 
GET_BUCKET_TAGGING,
+        GET_BUCKET_LIFECYCLE, PUT_OBJECT, PUT_OBJECT_TAGGING, PUT_BUCKET_ACL, 
PUT_BUCKET_TAGGING,
+        PUT_BUCKET_LIFECYCLE);
   }
 
   @Test
   public void testMapPolicyActionsToS3ActionsWithS3StarIgnoresOtherActions() {
     final Set<S3Action> result = mapPolicyActionsToS3Actions(strSet("s3:*", 
"s3:GetObject", "s3:PutObject"));
     // When s3:* is present, it should return all supported concrete actions
-    assertThat(result).containsOnly(
-        LIST_ALL_MY_BUCKETS, CREATE_BUCKET, DELETE_BUCKET, GET_BUCKET_ACL, 
LIST_BUCKET, LIST_BUCKET_MULTIPART_UPLOADS,
-        PUT_BUCKET_ACL, ABORT_MULTIPART_UPLOAD, DELETE_OBJECT, 
DELETE_OBJECT_TAGGING, GET_OBJECT, GET_OBJECT_TAGGING,
-        LIST_MULTIPART_UPLOAD_PARTS, PUT_OBJECT, PUT_OBJECT_TAGGING);
+    assertThat(result).containsOnly(S3Action.values());
   }
 
   @Test
@@ -1620,7 +1631,8 @@ public void 
testCreatePathsAndPermissionsWithAllConcreteS3Actions() {
         new OzoneGrant(objSet(bucket("bucket1")), acls(READ), 
ALL_OBJECT_ACTIONS),
         new OzoneGrant(objSet(volume()), acls(READ), 
ALL_BUCKET_AND_OBJECT_ACTIONS),
         new OzoneGrant(
-            objSet(bucket("bucket2")), acls(READ, LIST, CREATE, DELETE, 
READ_ACL, WRITE_ACL), ALL_BUCKET_ACTIONS),
+            objSet(bucket("bucket2")), acls(READ, LIST, CREATE, DELETE, 
READ_ACL, WRITE_ACL, WRITE),
+            ALL_BUCKET_ACTIONS),
         new OzoneGrant(objSet(prefix("bucket2", "")), acls(READ), 
strSet("ListBucket")));
 
     final Map<IOzoneObj, Set<ACLType>> objToAclsMapRanger = new 
LinkedHashMap<>();
@@ -1632,7 +1644,8 @@ public void 
testCreatePathsAndPermissionsWithAllConcreteS3Actions() {
         new OzoneGrant(objSet(bucket("bucket1")), acls(READ), 
ALL_OBJECT_ACTIONS),
         new OzoneGrant(objSet(volume()), acls(READ), 
ALL_BUCKET_AND_OBJECT_ACTIONS),
         new OzoneGrant(
-            objSet(bucket("bucket2")), acls(READ, LIST, CREATE, DELETE, 
READ_ACL, WRITE_ACL), ALL_BUCKET_ACTIONS),
+            objSet(bucket("bucket2")), acls(READ, LIST, CREATE, DELETE, 
READ_ACL, WRITE_ACL, WRITE),
+            ALL_BUCKET_ACTIONS),
         new OzoneGrant(objSet(key("bucket2", "*")), acls(READ), 
strSet("ListBucket")));
 
   }
@@ -1823,7 +1836,7 @@ public void 
testDeduplicatesAcrossMultipleStatementsWhenAllActionPresent() throw
     final Set<OzoneGrant> expectedResolvedNative = new LinkedHashSet<>();
     // Expected for native: bucket union of supported bucket ACLs; volume 
READ; prefix "" READ (from ListBucket)
     final Set<IOzoneObj> bucketSet = objSet(bucket("my-bucket"));
-    final Set<ACLType> bucketAcls = acls(READ, LIST, CREATE, DELETE, READ_ACL, 
WRITE_ACL);
+    final Set<ACLType> bucketAcls = acls(READ, LIST, CREATE, DELETE, READ_ACL, 
WRITE_ACL, WRITE);
     expectedResolvedNative.add(new OzoneGrant(bucketSet, bucketAcls, 
ALL_BUCKET_ACTIONS));
     expectedResolvedNative.add(new OzoneGrant(objSet(prefix("my-bucket", "")), 
acls(READ), strSet("ListBucket")));
     expectedResolvedNative.add(new OzoneGrant(objSet(volume()), acls(READ), 
ALL_BUCKET_ACTIONS));
@@ -1936,7 +1949,7 @@ public void testAllActionsForBucket() throws OMException {
     final Set<OzoneGrant> expectedResolvedNative = new LinkedHashSet<>();
     // Expected for native: union of supported bucket ACLs for bucket; volume 
READ; prefix "" READ (from ListBucket)
     final Set<IOzoneObj> bucketSet = objSet(bucket("my-bucket"));
-    final Set<ACLType> allBucketAcls = acls(READ, LIST, CREATE, DELETE, 
READ_ACL, WRITE_ACL);
+    final Set<ACLType> allBucketAcls = acls(READ, LIST, CREATE, DELETE, 
READ_ACL, WRITE_ACL, WRITE);
     expectedResolvedNative.add(new OzoneGrant(bucketSet, allBucketAcls, 
ALL_BUCKET_ACTIONS));
     expectedResolvedNative.add(new OzoneGrant(objSet(prefix("my-bucket", "")), 
acls(READ), strSet("ListBucket")));
     expectedResolvedNative.add(new OzoneGrant(objSet(volume()), acls(READ), 
ALL_BUCKET_ACTIONS));
@@ -2070,7 +2083,7 @@ public void testMultipleResourcesInOneStatement() throws 
OMException {
     final Set<IOzoneObj> bucketSet = objSet(bucket("my-bucket"));
     expectedResolvedNative.add(
         new OzoneGrant(
-            bucketSet, acls(READ, LIST, CREATE, DELETE, READ_ACL, WRITE_ACL), 
ALL_BUCKET_AND_OBJECT_ACTIONS));
+            bucketSet, acls(READ, LIST, CREATE, DELETE, READ_ACL, WRITE_ACL, 
WRITE), ALL_BUCKET_AND_OBJECT_ACTIONS));
     expectedResolvedNative.add(
         new OzoneGrant(
             objSet(prefix("my-bucket", "")), acls(READ, CREATE, WRITE, 
DELETE), ALL_OBJECT_ACTIONS_WITH_LIST_BUCKET));
@@ -2081,7 +2094,7 @@ public void testMultipleResourcesInOneStatement() throws 
OMException {
     // Expected for Ranger: bucket union of supported bucket ACLs; key "*" 
union of supported object ACLs; volume READ
     expectedResolvedRanger.add(
         new OzoneGrant(
-            bucketSet, acls(READ, LIST, CREATE, DELETE, READ_ACL, WRITE_ACL), 
ALL_BUCKET_AND_OBJECT_ACTIONS));
+            bucketSet, acls(READ, LIST, CREATE, DELETE, READ_ACL, WRITE_ACL, 
WRITE), ALL_BUCKET_AND_OBJECT_ACTIONS));
     expectedResolvedRanger.add(
         new OzoneGrant(
             objSet(key("my-bucket", "*")), acls(READ, CREATE, WRITE, DELETE), 
ALL_OBJECT_ACTIONS_WITH_LIST_BUCKET));
@@ -2506,7 +2519,8 @@ public void testAllActionsOnAllResources() throws 
OMException {
     expectedResolvedRanger.add(new OzoneGrant(objSet(volume()), acls(READ, 
LIST), emptySet()));
     expectedResolvedRanger.add(
         new OzoneGrant(
-            objSet(bucket("*")), acls(READ, LIST, CREATE, DELETE, READ_ACL, 
WRITE_ACL), ALL_BUCKET_AND_OBJECT_ACTIONS));
+            objSet(bucket("*")), acls(READ, LIST, CREATE, DELETE, READ_ACL, 
WRITE_ACL, WRITE),
+            ALL_BUCKET_AND_OBJECT_ACTIONS));
     expectedResolvedRanger.add(
         new OzoneGrant(objSet(key("*", "*")), acls(READ, CREATE, WRITE, 
DELETE), ALL_OBJECT_ACTIONS_WITH_LIST_BUCKET));
     assertThat(resolvedFromRangerAuthorizer).isEqualTo(expectedResolvedRanger);
@@ -2530,7 +2544,7 @@ public void testAllActionsOnAllBucketResources() throws 
OMException {
     final Set<OzoneGrant> expectedResolvedRanger = new LinkedHashSet<>();
     // Expected for Ranger: union of supported bucket ACLs on wildcard bucket; 
volume READ, LIST; key "*" READ
     final Set<IOzoneObj> bucketSet = objSet(bucket("*"));
-    final Set<ACLType> bucketAcls = acls(READ, LIST, CREATE, DELETE, READ_ACL, 
WRITE_ACL);
+    final Set<ACLType> bucketAcls = acls(READ, LIST, CREATE, DELETE, READ_ACL, 
WRITE_ACL, WRITE);
     expectedResolvedRanger.add(new OzoneGrant(bucketSet, bucketAcls, 
ALL_BUCKET_ACTIONS));
     expectedResolvedRanger.add(new OzoneGrant(objSet(key("*", "*")), 
acls(READ), strSet("ListBucket")));
     expectedResolvedRanger.add(
@@ -2584,23 +2598,38 @@ public void testWildcardActionGroupGetStar() throws 
OMException {
     final Set<IOzoneObj> bucketSet = objSet(bucket("my-bucket"));
     final Set<ACLType> bucketAcls = acls(READ, READ_ACL);
     expectedResolvedNative.add(
-        new OzoneGrant(bucketSet, bucketAcls, strSet("GetBucketAcl", 
"GetObject", "GetObjectTagging")));
+        new OzoneGrant(
+            bucketSet, bucketAcls, strSet(
+                "GetBucketAcl", "GetBucketTagging", 
"GetLifecycleConfiguration", "GetObject", "GetObjectAttributes",
+                "GetObjectTagging")));
     // Expected for native: READ acl on prefix "" under bucket; volume READ
     expectedResolvedNative.add(
-        new OzoneGrant(objSet(prefix("my-bucket", "")), acls(READ), 
strSet("GetObject", "GetObjectTagging")));
+        new OzoneGrant(
+            objSet(prefix("my-bucket", "")), acls(READ), strSet(
+                "GetObject", "GetObjectAttributes", "GetObjectTagging")));
     expectedResolvedNative.add(
-        new OzoneGrant(objSet(volume()), acls(READ), strSet("GetBucketAcl", 
"GetObject", "GetObjectTagging")));
+        new OzoneGrant(
+            objSet(volume()), acls(READ), strSet(
+                "GetBucketAcl", "GetBucketTagging", 
"GetLifecycleConfiguration", "GetObject", "GetObjectAttributes",
+                "GetObjectTagging")));
     assertThat(resolvedFromNativeAuthorizer).isEqualTo(expectedResolvedNative);
 
     final Set<OzoneGrant> expectedResolvedRanger = new LinkedHashSet<>();
     // Expected for Ranger: bucket READ, READ_ACL acls
     expectedResolvedRanger.add(
-        new OzoneGrant(bucketSet, bucketAcls, strSet("GetBucketAcl", 
"GetObject", "GetObjectTagging")));
+        new OzoneGrant(
+            bucketSet, bucketAcls, strSet(
+                "GetBucketAcl", "GetBucketTagging", 
"GetLifecycleConfiguration", "GetObject", "GetObjectAttributes",
+                "GetObjectTagging")));
     // Expected for Ranger: READ key acl for resource type KEY with key name 
"*"; volume READ
     expectedResolvedRanger.add(
-        new OzoneGrant(objSet(key("my-bucket", "*")), acls(READ), 
strSet("GetObject", "GetObjectTagging")));
+        new OzoneGrant(
+            objSet(key("my-bucket", "*")), acls(READ), strSet("GetObject", 
"GetObjectAttributes", "GetObjectTagging")));
     expectedResolvedRanger.add(
-        new OzoneGrant(objSet(volume()), acls(READ), strSet("GetBucketAcl", 
"GetObject", "GetObjectTagging")));
+        new OzoneGrant(
+            objSet(volume()), acls(READ), strSet(
+                "GetBucketAcl", "GetBucketTagging", 
"GetLifecycleConfiguration", "GetObject", "GetObjectAttributes",
+                "GetObjectTagging")));
     assertThat(resolvedFromRangerAuthorizer).isEqualTo(expectedResolvedRanger);
   }
 
@@ -2670,33 +2699,68 @@ public void testWildcardActionGroupPutStar() throws 
OMException {
 
     // Ensure what we got is what we expected
     final Set<OzoneGrant> expectedResolvedNative = new LinkedHashSet<>();
-    // Expected for native: bucket READ, READ_ACL, WRITE_ACL acl
+    // Expected for native: bucket READ, READ_ACL, WRITE_ACL, WRITE acl
     final Set<IOzoneObj> bucketSet = objSet(bucket("my-bucket"));
-    final Set<ACLType> bucketAcl = acls(READ, READ_ACL, WRITE_ACL);
+    final Set<ACLType> bucketAcl = acls(READ, READ_ACL, WRITE_ACL, WRITE);
     expectedResolvedNative.add(
-        new OzoneGrant(bucketSet, bucketAcl, strSet("PutBucketAcl", 
"PutObject", "PutObjectTagging")));
+        new OzoneGrant(
+            bucketSet, bucketAcl, strSet(
+                "PutBucketAcl", "PutObject", "PutObjectTagging", 
"PutLifecycleConfiguration", "PutBucketTagging")));
     // Expected for native: CREATE, WRITE acls on prefix "" under bucket
     final Set<IOzoneObj> keyPrefixSet = objSet(prefix("my-bucket", ""));
     final Set<ACLType> keyAcls = acls(CREATE, WRITE);
     expectedResolvedNative.add(new OzoneGrant(keyPrefixSet, keyAcls, 
strSet("PutObject", "PutObjectTagging")));
     // Expected for native: volume READ
     expectedResolvedNative.add(
-        new OzoneGrant(objSet(volume()), acls(READ), strSet("PutBucketAcl", 
"PutObject", "PutObjectTagging")));
+        new OzoneGrant(
+            objSet(volume()), acls(READ), strSet(
+                "PutBucketAcl", "PutObject", "PutObjectTagging", 
"PutLifecycleConfiguration", "PutBucketTagging")));
     assertThat(resolvedFromNativeAuthorizer).isEqualTo(expectedResolvedNative);
 
     final Set<OzoneGrant> expectedResolvedRanger = new LinkedHashSet<>();
-    // Expected for Ranger: bucket READ, READ_ACL, WRITE_ACL acl
+    // Expected for Ranger: bucket READ, READ_ACL, WRITE_ACL, WRITE acl
     expectedResolvedRanger.add(
-        new OzoneGrant(bucketSet, bucketAcl, strSet("PutBucketAcl", 
"PutObject", "PutObjectTagging")));
+        new OzoneGrant(
+            bucketSet, bucketAcl, strSet(
+                "PutBucketAcl", "PutObject", "PutObjectTagging", 
"PutLifecycleConfiguration", "PutBucketTagging")));
     // Expected for Ranger: CREATE, WRITE key acls for resource type KEY with 
key name "*"
     final Set<IOzoneObj> rangerKeySet = objSet(key("my-bucket", "*"));
     expectedResolvedRanger.add(new OzoneGrant(rangerKeySet, keyAcls, 
strSet("PutObject", "PutObjectTagging")));
     // Expected for Ranger: volume READ
     expectedResolvedRanger.add(
-        new OzoneGrant(objSet(volume()), acls(READ), strSet("PutBucketAcl", 
"PutObject", "PutObjectTagging")));
+        new OzoneGrant(
+            objSet(volume()), acls(READ), strSet(
+                "PutBucketAcl", "PutObject", "PutObjectTagging", 
"PutLifecycleConfiguration", "PutBucketTagging")));
     assertThat(resolvedFromRangerAuthorizer).isEqualTo(expectedResolvedRanger);
   }
 
+  @Test
+  public void 
testPutBucketTaggingAndPutLifecycleConfigurationRequireBucketReadAndWrite() 
throws OMException {
+    final String json = "{\n" +
+        "  \"Statement\": [{\n" +
+        "    \"Effect\": \"Allow\",\n" +
+        "    \"Action\": [\n" +
+        "      \"s3:PutBucketTagging\",\n" +
+        "      \"s3:PutLifecycleConfiguration\"\n" +
+        "    ],\n" +
+        "    \"Resource\": \"arn:aws:s3:::my-bucket\"\n" +
+        "  }]\n" +
+        "}";
+
+    final Set<OzoneGrant> resolvedFromNativeAuthorizer = resolve(json, VOLUME, 
NATIVE);
+    final Set<OzoneGrant> resolvedFromRangerAuthorizer = resolve(json, VOLUME, 
RANGER);
+
+    // Ensure what we got is what we expected
+    final Set<OzoneGrant> expected = new LinkedHashSet<>();
+    final Set<IOzoneObj> bucketSet = objSet(bucket("my-bucket"));
+    final Set<ACLType> bucketAcl = acls(READ, WRITE);
+    expected.add(new OzoneGrant(bucketSet, bucketAcl, 
strSet("PutBucketTagging", "PutLifecycleConfiguration")));
+    expected.add(new OzoneGrant(objSet(volume()), acls(READ), 
strSet("PutBucketTagging", "PutLifecycleConfiguration")));
+
+    assertThat(resolvedFromNativeAuthorizer).isEqualTo(expected);
+    assertThat(resolvedFromRangerAuthorizer).isEqualTo(expected);
+  }
+
   @Test
   public void testWildcardActionGroupDeleteStar() throws OMException {
     final String json = "{\n" +
diff --git 
a/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot 
b/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot
index 407cf60f105..81c784382c8 100644
--- a/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot
+++ b/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot
@@ -45,6 +45,7 @@ ${PARTIAL_LIST_ALL_BUCKETS_VOL_LIST_ROLE}       
partial-list-all-buckets-vol-lis
 ${PARTIAL_BUCKET_READ_ROLE}                     partial-bucket-read
 ${PARTIAL_BUCKET_READ_UPLOAD_PREFIX_ROLE}       
partial-bucket-read-upload-prefix
 ${PARTIAL_BUCKET_LIST_ROLE}                     partial-bucket-list
+${PARTIAL_BUCKET_WRITE_ROLE}                    partial-bucket-write
 ${PARTIAL_BUCKET_READ_ACL_ROLE}                 partial-bucket-read-acl
 ${PARTIAL_PUT_OBJECT_KEY_CREATE_ROLE}           partial-put-object-key-create
 ${PARTIAL_PUT_OBJECT_KEY_WRITE_ROLE}            partial-put-object-key-write
@@ -58,6 +59,7 @@ ${PARTIAL_LIST_ALL_BUCKETS_VOL_LIST_ROLE_ARN}   
arn:aws:iam::123456789012:role/$
 ${PARTIAL_BUCKET_READ_ROLE_ARN}                 
arn:aws:iam::123456789012:role/${PARTIAL_BUCKET_READ_ROLE}
 ${PARTIAL_BUCKET_READ_UPLOAD_PREFIX_ROLE_ARN}   
arn:aws:iam::123456789012:role/${PARTIAL_BUCKET_READ_UPLOAD_PREFIX_ROLE}
 ${PARTIAL_BUCKET_LIST_ROLE_ARN}                 
arn:aws:iam::123456789012:role/${PARTIAL_BUCKET_LIST_ROLE}
+${PARTIAL_BUCKET_WRITE_ROLE_ARN}                
arn:aws:iam::123456789012:role/${PARTIAL_BUCKET_WRITE_ROLE}
 ${PARTIAL_BUCKET_READ_ACL_ROLE_ARN}             
arn:aws:iam::123456789012:role/${PARTIAL_BUCKET_READ_ACL_ROLE}
 ${PARTIAL_PUT_OBJECT_KEY_CREATE_ROLE_ARN}       
arn:aws:iam::123456789012:role/${PARTIAL_PUT_OBJECT_KEY_CREATE_ROLE}
 ${PARTIAL_PUT_OBJECT_KEY_WRITE_ROLE_ARN}        
arn:aws:iam::123456789012:role/${PARTIAL_PUT_OBJECT_KEY_WRITE_ROLE}
@@ -86,6 +88,8 @@ ${TEST_USER_NON_ADMIN}                  testuser2
 @{ICEBERG_PREFIX_USERA_STAR_FSO}        userA/    userA/userA.txt
 @{ICEBERG_PREFIX_USER_OBS}              userA/userA.txt    userB/userB.txt    
userAfile.txt
 @{ICEBERG_PREFIX_USER_FSO}              userA/    userA/userA.txt    userB/    
userB/userB.txt    userAfile.txt
+${STS_BUCKET_TAGGING_JSON}              
{"TagSet":[{"Key":"sts-tag","Value":"sts-value"}]}
+${STS_LIFECYCLE_JSON}                   
{"Rules":[{"ID":"sts-lifecycle-rule","Prefix":"sts-lifecycle-unused/","Status":"Enabled","Expiration":{"Days":1}}]}
 
 *** Keywords ***
 Populate Iceberg Bucket
@@ -393,24 +397,24 @@ Create STS Link Bucket Access Policies
     Refresh Ranger Policy Cache
 
 Create Partial Access Roles in Ranger
-    FOR    ${role}    IN    ${PARTIAL_LIST_ALL_BUCKETS_VOL_READ_ROLE}    
${PARTIAL_LIST_ALL_BUCKETS_VOL_LIST_ROLE}    ${PARTIAL_BUCKET_READ_ROLE}    
${PARTIAL_BUCKET_READ_UPLOAD_PREFIX_ROLE}    ${PARTIAL_BUCKET_LIST_ROLE}    
${PARTIAL_BUCKET_READ_ACL_ROLE}    ${PARTIAL_PUT_OBJECT_KEY_CREATE_ROLE}    
${PARTIAL_PUT_OBJECT_KEY_WRITE_ROLE}
+    FOR    ${role}    IN    ${PARTIAL_LIST_ALL_BUCKETS_VOL_READ_ROLE}    
${PARTIAL_LIST_ALL_BUCKETS_VOL_LIST_ROLE}    ${PARTIAL_BUCKET_READ_ROLE}    
${PARTIAL_BUCKET_READ_UPLOAD_PREFIX_ROLE}    ${PARTIAL_BUCKET_LIST_ROLE}    
${PARTIAL_BUCKET_WRITE_ROLE}    ${PARTIAL_BUCKET_READ_ACL_ROLE}    
${PARTIAL_PUT_OBJECT_KEY_CREATE_ROLE}    ${PARTIAL_PUT_OBJECT_KEY_WRITE_ROLE}
         ${role_json} =            Set Variable                  { "name": 
"${role}", "description": "Partial access role" }
         Create Ranger Role        ${role_json}
     END
 
 Create Partial Access Assume Role Policies
-    FOR    ${role}    IN    ${PARTIAL_LIST_ALL_BUCKETS_VOL_READ_ROLE}    
${PARTIAL_LIST_ALL_BUCKETS_VOL_LIST_ROLE}    ${PARTIAL_BUCKET_READ_ROLE}    
${PARTIAL_BUCKET_READ_UPLOAD_PREFIX_ROLE}    ${PARTIAL_BUCKET_LIST_ROLE}    
${PARTIAL_BUCKET_READ_ACL_ROLE}    ${PARTIAL_PUT_OBJECT_KEY_CREATE_ROLE}    
${PARTIAL_PUT_OBJECT_KEY_WRITE_ROLE}
+    FOR    ${role}    IN    ${PARTIAL_LIST_ALL_BUCKETS_VOL_READ_ROLE}    
${PARTIAL_LIST_ALL_BUCKETS_VOL_LIST_ROLE}    ${PARTIAL_BUCKET_READ_ROLE}    
${PARTIAL_BUCKET_READ_UPLOAD_PREFIX_ROLE}    ${PARTIAL_BUCKET_LIST_ROLE}    
${PARTIAL_BUCKET_WRITE_ROLE}    ${PARTIAL_BUCKET_READ_ACL_ROLE}    
${PARTIAL_PUT_OBJECT_KEY_CREATE_ROLE}    ${PARTIAL_PUT_OBJECT_KEY_WRITE_ROLE}
         Create Ranger Assume Role Policy  ${role}              
${ICEBERG_SVC_CATALOG_USER}
     END
 
 Create Partial Access Volume Policies
     # Append partial-role items to existing "iceberg volume access" policy to 
avoid duplicate resourceSignature conflicts.
-    ${policy_items} =             Set Variable                  [ { 
"accesses": [ { "type": "read", "isAllowed": true } ], "roles": [ 
"${PARTIAL_LIST_ALL_BUCKETS_VOL_READ_ROLE}" ], "delegateAdmin": false }, { 
"accesses": [ { "type": "list", "isAllowed": true } ], "roles": [ 
"${PARTIAL_LIST_ALL_BUCKETS_VOL_LIST_ROLE}" ], "delegateAdmin": false }, { 
"accesses": [ { "type": "read", "isAllowed": true } ], "roles": [ 
"${PARTIAL_BUCKET_READ_ROLE}", "${PARTIAL_BUCKET_READ_UPLOAD_PREFIX_ROLE}",  
[...]
+    ${policy_items} =             Set Variable                  [ { 
"accesses": [ { "type": "read", "isAllowed": true } ], "roles": [ 
"${PARTIAL_LIST_ALL_BUCKETS_VOL_READ_ROLE}" ], "delegateAdmin": false }, { 
"accesses": [ { "type": "list", "isAllowed": true } ], "roles": [ 
"${PARTIAL_LIST_ALL_BUCKETS_VOL_LIST_ROLE}" ], "delegateAdmin": false }, { 
"accesses": [ { "type": "read", "isAllowed": true } ], "roles": [ 
"${PARTIAL_BUCKET_READ_ROLE}", "${PARTIAL_BUCKET_READ_UPLOAD_PREFIX_ROLE}",  
[...]
     Update Ranger Policy Items    iceberg volume access         ${policy_items}
 
 Create Partial Access Bucket Policies
     # Append partial-role items to existing "iceberg ${ICEBERG_BUCKET_OBS} 
bucket access" policy.
-    ${policy_items} =             Set Variable                  [ { 
"accesses": [ { "type": "read", "isAllowed": true } ], "roles": [ 
"${PARTIAL_BUCKET_READ_ROLE}" ], "delegateAdmin": false }, { "accesses": [ { 
"type": "read", "isAllowed": true } ], "roles": [ 
"${PARTIAL_BUCKET_READ_UPLOAD_PREFIX_ROLE}" ], "delegateAdmin": false }, { 
"accesses": [ { "type": "list", "isAllowed": true } ], "roles": [ 
"${PARTIAL_BUCKET_LIST_ROLE}" ], "delegateAdmin": false }, { "accesses": [ { 
"type": "read [...]
+    ${policy_items} =             Set Variable                  [ { 
"accesses": [ { "type": "read", "isAllowed": true } ], "roles": [ 
"${PARTIAL_BUCKET_READ_ROLE}" ], "delegateAdmin": false }, { "accesses": [ { 
"type": "read", "isAllowed": true } ], "roles": [ 
"${PARTIAL_BUCKET_READ_UPLOAD_PREFIX_ROLE}" ], "delegateAdmin": false }, { 
"accesses": [ { "type": "list", "isAllowed": true } ], "roles": [ 
"${PARTIAL_BUCKET_LIST_ROLE}" ], "delegateAdmin": false }, { "accesses": [ { 
"type": "writ [...]
     Update Ranger Policy Items    iceberg ${ICEBERG_BUCKET_OBS} bucket access  
${policy_items}
 
 Create Partial Access Table Policies
@@ -950,6 +954,154 @@ STS session policy PutBucketAcl must require bucket 
WRITE_ACL
     ${output} =                   Execute And Ignore Error      aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} put-bucket-acl --bucket 
${ICEBERG_BUCKET_OBS} --grant-read "" --profile sts
     Should Contain                ${output}                     AccessDenied
 
+STS session policy GetBucketTagging must require bucket READ
+    ${put_tag_policy} =           Set Variable                  
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutBucketTagging","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+    ${get_tag_policy} =           Set Variable                  
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetBucketTagging","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+    ${delete_tag_policy} =        Set Variable                  
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutBucketTagging","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+
+    Assume Role And Configure STS Profile                       
policy_json=${put_tag_policy}  perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+    ${output} =                   Execute                       aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} put-bucket-tagging --bucket 
${ICEBERG_BUCKET_OBS} --tagging '${STS_BUCKET_TAGGING_JSON}' --profile sts
+    Should Not Contain            ${output}                     AccessDenied
+
+    # Positive control
+    Assume Role And Configure STS Profile                       
policy_json=${get_tag_policy}  perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+    ${output} =                   Execute                       aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} get-bucket-tagging --bucket 
${ICEBERG_BUCKET_OBS} --profile sts
+    Should Contain                ${output}                     sts-tag
+
+    # Negative: missing READ
+    Assume Role And Configure STS Profile                       
policy_json=${get_tag_policy}  perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${PARTIAL_BUCKET_LIST_ROLE_ARN}
+    ${output} =                   Execute And Ignore Error      aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} get-bucket-tagging --bucket 
${ICEBERG_BUCKET_OBS} --profile sts
+    Should Contain                ${output}                     AccessDenied
+
+    Assume Role And Configure STS Profile                       
policy_json=${delete_tag_policy}  perm_access_key_id=${PERMANENT_ACCESS_KEY_ID} 
 perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+    ${output} =                   Execute                       aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} delete-bucket-tagging --bucket 
${ICEBERG_BUCKET_OBS} --profile sts
+    Should Not Contain            ${output}                     AccessDenied
+
+STS session policy PutBucketTagging must require bucket READ and WRITE
+    ${put_tag_policy} =           Set Variable                  
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutBucketTagging","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+    ${delete_tag_policy} =        Set Variable                  
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutBucketTagging","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+
+    # Positive control
+    Assume Role And Configure STS Profile                       
policy_json=${put_tag_policy}  perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+    ${output} =                   Execute                       aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} put-bucket-tagging --bucket 
${ICEBERG_BUCKET_OBS} --tagging '${STS_BUCKET_TAGGING_JSON}' --profile sts
+    Should Not Contain            ${output}                     AccessDenied
+
+    # Negative A: missing WRITE
+    Assume Role And Configure STS Profile                       
policy_json=${put_tag_policy}  perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${PARTIAL_BUCKET_READ_ROLE_ARN}
+    ${output} =                   Execute And Ignore Error      aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} put-bucket-tagging --bucket 
${ICEBERG_BUCKET_OBS} --tagging '${STS_BUCKET_TAGGING_JSON}' --profile sts
+    Should Contain                ${output}                     AccessDenied
+
+    # Negative B: missing READ
+    Assume Role And Configure STS Profile                       
policy_json=${put_tag_policy}  perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${PARTIAL_BUCKET_WRITE_ROLE_ARN}
+    ${output} =                   Execute And Ignore Error      aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} put-bucket-tagging --bucket 
${ICEBERG_BUCKET_OBS} --tagging '${STS_BUCKET_TAGGING_JSON}' --profile sts
+    Should Contain                ${output}                     AccessDenied
+
+    Assume Role And Configure STS Profile                       
policy_json=${delete_tag_policy}  perm_access_key_id=${PERMANENT_ACCESS_KEY_ID} 
 perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+    ${output} =                   Execute                       aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} delete-bucket-tagging --bucket 
${ICEBERG_BUCKET_OBS} --profile sts
+    Should Not Contain            ${output}                     AccessDenied
+
+STS session policy PutBucketTagging must authorize DeleteBucketTagging with 
bucket READ and WRITE
+    ${put_tag_policy} =           Set Variable                  
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutBucketTagging","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+    ${delete_tag_policy} =        Set Variable                  
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutBucketTagging","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+    ${get_tag_policy} =           Set Variable                  
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetBucketTagging","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+
+    Assume Role And Configure STS Profile                       
policy_json=${put_tag_policy}  perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+    ${output} =                   Execute                       aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} put-bucket-tagging --bucket 
${ICEBERG_BUCKET_OBS} --tagging '${STS_BUCKET_TAGGING_JSON}' --profile sts
+    Should Not Contain            ${output}                     AccessDenied
+
+    # Negative A: missing WRITE
+    Assume Role And Configure STS Profile                       
policy_json=${delete_tag_policy}  perm_access_key_id=${PERMANENT_ACCESS_KEY_ID} 
 perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${PARTIAL_BUCKET_READ_ROLE_ARN}
+    ${output} =                   Execute And Ignore Error      aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} delete-bucket-tagging --bucket 
${ICEBERG_BUCKET_OBS} --profile sts
+    Should Contain                ${output}                     AccessDenied
+
+    # Negative B: missing READ
+    Assume Role And Configure STS Profile                       
policy_json=${delete_tag_policy}  perm_access_key_id=${PERMANENT_ACCESS_KEY_ID} 
 perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${PARTIAL_BUCKET_WRITE_ROLE_ARN}
+    ${output} =                   Execute And Ignore Error      aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} delete-bucket-tagging --bucket 
${ICEBERG_BUCKET_OBS} --profile sts
+    Should Contain                ${output}                     AccessDenied
+
+    # Positive control
+    Assume Role And Configure STS Profile                       
policy_json=${delete_tag_policy}  perm_access_key_id=${PERMANENT_ACCESS_KEY_ID} 
 perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+    ${output} =                   Execute                       aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} delete-bucket-tagging --bucket 
${ICEBERG_BUCKET_OBS} --profile sts
+    Should Not Contain            ${output}                     AccessDenied
+
+    Assume Role And Configure STS Profile                       
policy_json=${get_tag_policy}  perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+    ${output} =                   Execute And Ignore Error      aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} get-bucket-tagging --bucket 
${ICEBERG_BUCKET_OBS} --profile sts
+    Should Contain                ${output}                     NoSuchTagSet
+
+STS session policy GetLifecycleConfiguration must require bucket READ
+    ${put_lifecycle_policy} =     Set Variable                  
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutLifecycleConfiguration","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+    ${get_lifecycle_policy} =     Set Variable                  
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetLifecycleConfiguration","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+    ${delete_lifecycle_policy} =  Set Variable                  
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutLifecycleConfiguration","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+
+    Assume Role And Configure STS Profile                       
policy_json=${put_lifecycle_policy}  
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+    ${output} =                   Execute                       aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} put-bucket-lifecycle-configuration --bucket 
${ICEBERG_BUCKET_OBS} --lifecycle-configuration '${STS_LIFECYCLE_JSON}' 
--profile sts
+    Should Not Contain            ${output}                     AccessDenied
+
+    # Positive control
+    Assume Role And Configure STS Profile                       
policy_json=${get_lifecycle_policy}  
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+    ${output} =                   Execute                       aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} get-bucket-lifecycle-configuration --bucket 
${ICEBERG_BUCKET_OBS} --profile sts
+    Should Contain                ${output}                     
sts-lifecycle-rule
+
+    # Negative: missing READ
+    Assume Role And Configure STS Profile                       
policy_json=${get_lifecycle_policy}  
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${PARTIAL_BUCKET_LIST_ROLE_ARN}
+    ${output} =                   Execute And Ignore Error      aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} get-bucket-lifecycle-configuration --bucket 
${ICEBERG_BUCKET_OBS} --profile sts
+    Should Contain                ${output}                     AccessDenied
+
+    Assume Role And Configure STS Profile                       
policy_json=${delete_lifecycle_policy}  
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+    ${output} =                   Execute                       aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} delete-bucket-lifecycle --bucket 
${ICEBERG_BUCKET_OBS} --profile sts
+    Should Not Contain            ${output}                     AccessDenied
+
+STS session policy PutLifecycleConfiguration must require bucket READ and WRITE
+    ${put_lifecycle_policy} =     Set Variable                  
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutLifecycleConfiguration","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+    ${delete_lifecycle_policy} =  Set Variable                  
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutLifecycleConfiguration","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+
+    # Positive control
+    Assume Role And Configure STS Profile                       
policy_json=${put_lifecycle_policy}  
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+    ${output} =                   Execute                       aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} put-bucket-lifecycle-configuration --bucket 
${ICEBERG_BUCKET_OBS} --lifecycle-configuration '${STS_LIFECYCLE_JSON}' 
--profile sts
+    Should Not Contain            ${output}                     AccessDenied
+
+    # Negative A: missing WRITE
+    Assume Role And Configure STS Profile                       
policy_json=${put_lifecycle_policy}  
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${PARTIAL_BUCKET_READ_ROLE_ARN}
+    ${output} =                   Execute And Ignore Error      aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} put-bucket-lifecycle-configuration --bucket 
${ICEBERG_BUCKET_OBS} --lifecycle-configuration '${STS_LIFECYCLE_JSON}' 
--profile sts
+    Should Contain                ${output}                     AccessDenied
+
+    # Negative B: missing READ
+    Assume Role And Configure STS Profile                       
policy_json=${put_lifecycle_policy}  
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${PARTIAL_BUCKET_WRITE_ROLE_ARN}
+    ${output} =                   Execute And Ignore Error      aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} put-bucket-lifecycle-configuration --bucket 
${ICEBERG_BUCKET_OBS} --lifecycle-configuration '${STS_LIFECYCLE_JSON}' 
--profile sts
+    Should Contain                ${output}                     AccessDenied
+
+    Assume Role And Configure STS Profile                       
policy_json=${delete_lifecycle_policy}  
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+    ${output} =                   Execute                       aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} delete-bucket-lifecycle --bucket 
${ICEBERG_BUCKET_OBS} --profile sts
+    Should Not Contain            ${output}                     AccessDenied
+
+STS session policy PutLifecycleConfiguration must authorize 
DeleteBucketLifecycle with bucket READ and WRITE
+    ${put_lifecycle_policy} =     Set Variable                  
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutLifecycleConfiguration","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+    ${delete_lifecycle_policy} =  Set Variable                  
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutLifecycleConfiguration","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+    ${get_lifecycle_policy} =     Set Variable                  
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetLifecycleConfiguration","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+
+    Assume Role And Configure STS Profile                       
policy_json=${put_lifecycle_policy}  
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+    ${output} =                   Execute                       aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} put-bucket-lifecycle-configuration --bucket 
${ICEBERG_BUCKET_OBS} --lifecycle-configuration '${STS_LIFECYCLE_JSON}' 
--profile sts
+    Should Not Contain            ${output}                     AccessDenied
+
+    # Negative A: missing WRITE
+    Assume Role And Configure STS Profile                       
policy_json=${delete_lifecycle_policy}  
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${PARTIAL_BUCKET_READ_ROLE_ARN}
+    ${output} =                   Execute And Ignore Error      aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} delete-bucket-lifecycle --bucket 
${ICEBERG_BUCKET_OBS} --profile sts
+    Should Contain                ${output}                     AccessDenied
+
+    # Negative B: missing READ
+    Assume Role And Configure STS Profile                       
policy_json=${delete_lifecycle_policy}  
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${PARTIAL_BUCKET_WRITE_ROLE_ARN}
+    ${output} =                   Execute And Ignore Error      aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} delete-bucket-lifecycle --bucket 
${ICEBERG_BUCKET_OBS} --profile sts
+    Should Contain                ${output}                     AccessDenied
+
+    # Positive control
+    Assume Role And Configure STS Profile                       
policy_json=${delete_lifecycle_policy}  
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+    ${output} =                   Execute                       aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} delete-bucket-lifecycle --bucket 
${ICEBERG_BUCKET_OBS} --profile sts
+    Should Not Contain            ${output}                     AccessDenied
+
+    Assume Role And Configure STS Profile                       
policy_json=${get_lifecycle_policy}  
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+    ${output} =                   Execute And Ignore Error      aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} get-bucket-lifecycle-configuration --bucket 
${ICEBERG_BUCKET_OBS} --profile sts
+    Should Contain                ${output}                     
NoSuchLifecycleConfiguration
+
 STS session policy GetObjectTagging must require key READ
     ${key_suffix} =               Generate Random String        8   [LOWER]
     ${key} =                      Set Variable                  
sts-object-${key_suffix}.txt
@@ -1109,6 +1261,39 @@ STS session policy AbortMultipartUpload must require key 
WRITE
     ${output} =                   Execute                       aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} abort-multipart-upload --bucket 
${ICEBERG_BUCKET_OBS} --key ${key} --upload-id ${upload_id} --profile sts
     Should Not Contain            ${output}                     AccessDenied
 
+STS session policy GetObjectAttributes must require GetObject and 
GetObjectAttributes with key READ
+    ${key_suffix} =               Generate Random String        8   [LOWER]
+    ${key} =                      Set Variable                  
sts-object-${key_suffix}.txt
+    ${local_path} =               Set Variable                  
${TEMP_DIR}/${key}
+    Create File                   ${local_path}                 
get-object-attributes content
+    ${put_policy} =               Set Variable                  
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutObject","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}/${key}"}]}
+    ${get_attrs_policy} =         Set Variable                  
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:GetObject","s3:GetObjectAttributes"],"Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}/${key}"}]}
+    ${get_object_only_policy} =   Set Variable                  
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}/${key}"}]}
+    ${get_attrs_only_policy} =    Set Variable                  
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObjectAttributes","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}/${key}"}]}
+
+    Assume Role And Configure STS Profile                       
policy_json=${put_policy}  perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+    Put Object Should Succeed     ${ICEBERG_BUCKET_OBS}  ${key}  ${local_path}
+
+    # Positive control
+    Assume Role And Configure STS Profile                       
policy_json=${get_attrs_policy}  perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+    ${output} =                   Execute                       aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} get-object-attributes --bucket 
${ICEBERG_BUCKET_OBS} --key ${key} --object-attributes ETag --profile sts
+    Should Contain                ${output}                     ETag
+
+    # Negative A: session policy with only GetObjectAttributes
+    Assume Role And Configure STS Profile                       
policy_json=${get_attrs_only_policy}  
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+    ${output} =                   Execute And Ignore Error      aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} get-object-attributes --bucket 
${ICEBERG_BUCKET_OBS} --key ${key} --object-attributes ETag --profile sts
+    Should Contain                ${output}                     AccessDenied
+
+    # Negative B: session policy with only GetObject
+    Assume Role And Configure STS Profile                       
policy_json=${get_object_only_policy}  
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+    ${output} =                   Execute And Ignore Error      aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} get-object-attributes --bucket 
${ICEBERG_BUCKET_OBS} --key ${key} --object-attributes ETag --profile sts
+    Should Contain                ${output}                     AccessDenied
+
+    # Negative C: missing key READ
+    Assume Role And Configure STS Profile                       
policy_json=${get_attrs_policy}  perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${PARTIAL_BUCKET_READ_ROLE_ARN}
+    ${output} =                   Execute And Ignore Error      aws s3api 
--endpoint-url ${S3G_ENDPOINT_URL} get-object-attributes --bucket 
${ICEBERG_BUCKET_OBS} --key ${key} --object-attributes ETag --profile sts
+    Should Contain                ${output}                     AccessDenied
+
 STS session policy containing only GetObject must deny GetObjectTagging
     ${session_policy} =           Set Variable                  
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}/${ICEBERG_BUCKET_TESTFILE}"}]}
     Assume Role And Configure STS Profile                       
policy_json=${session_policy}  perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
@@ -1446,6 +1631,13 @@ Expired STS temporary credentials must return 
ExpiredToken on S3 APIs
     Execute S3api Expect Expired Token                          
get-object-tagging --bucket ${ICEBERG_BUCKET_OBS} --key 
${ICEBERG_BUCKET_TESTFILE}
     Execute S3api Expect Expired Token                          
put-object-tagging --bucket ${ICEBERG_BUCKET_OBS} --key 
${ICEBERG_BUCKET_TESTFILE} --tagging 
'{"TagSet":[{"Key":"tag-key-expired-sts-token","Value":"tag-value-expired-sts-token"}]}'
     Execute S3api Expect Expired Token                          
delete-object-tagging --bucket ${ICEBERG_BUCKET_OBS} --key 
${ICEBERG_BUCKET_TESTFILE}
+    Execute S3api Expect Expired Token                          
get-bucket-tagging --bucket ${ICEBERG_BUCKET_OBS}
+    Execute S3api Expect Expired Token                          
put-bucket-tagging --bucket ${ICEBERG_BUCKET_OBS} --tagging 
'${STS_BUCKET_TAGGING_JSON}'
+    Execute S3api Expect Expired Token                          
delete-bucket-tagging --bucket ${ICEBERG_BUCKET_OBS}
+    Execute S3api Expect Expired Token                          
get-bucket-lifecycle-configuration --bucket ${ICEBERG_BUCKET_OBS}
+    Execute S3api Expect Expired Token                          
put-bucket-lifecycle-configuration --bucket ${ICEBERG_BUCKET_OBS} 
--lifecycle-configuration '${STS_LIFECYCLE_JSON}'
+    Execute S3api Expect Expired Token                          
delete-bucket-lifecycle --bucket ${ICEBERG_BUCKET_OBS}
+    Execute S3api Expect Expired Token                          
get-object-attributes --bucket ${ICEBERG_BUCKET_OBS} --key 
${ICEBERG_BUCKET_TESTFILE} --object-attributes ETag
 
 Revoking Permanent User Must Revoke Existing Session Token
     # Create session tokens for both buckets, verify they work, then revoke 
permanent user secret and verify both fail.
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
index c3121599566..e93219ae64a 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
@@ -29,6 +29,7 @@
 
 import java.io.IOException;
 import java.net.InetAddress;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -83,6 +84,13 @@
  * from a rocksDb instance, for both the OM and OM snapshots.
  */
 public class OmMetadataReader implements IOmMetadataReader, Auditor {
+  /**
+   * S3 actions that additionally require other S3 actions to be authorized 
within the same request.
+   * AWS requires both s3:GetObject and s3:GetObjectAttributes for 
GetObjectAttributes.
+   */
+  private static final Map<String, List<String>> DEPENDENT_S3_ACTIONS =
+      Collections.singletonMap("GetObjectAttributes", 
Collections.singletonList("GetObject"));
+
   private final KeyManager keyManager;
   private final PrefixManager prefixManager;
   private final VolumeManager volumeManager;
@@ -717,7 +725,7 @@ public boolean checkAcls(OzoneObj obj, 
RequestContext.Builder contextBuilder,
     final RequestContext context = contextBuilder.build();
 
     if (!captureLatencyNs(perfMetrics::setCheckAccessLatencyNs,
-        () -> accessAuthorizer.checkAccess(obj, context))) {
+        () -> checkAccessIncludingDependentS3Actions(obj, context))) {
       if (throwIfPermissionDenied) {
         String volumeName = obj.getVolumeName() != null ?
                 "Volume:" + obj.getVolumeName() + " " : "";
@@ -754,6 +762,30 @@ public boolean checkAcls(OzoneObj obj, 
RequestContext.Builder contextBuilder,
     }
   }
 
+  /**
+   * Checks access for the S3 action in the context and then for each S3 
action it depends on (see
+   * {@link #DEPENDENT_S3_ACTIONS}), so a single request does not need a 
separate RPC per action.
+   * Each {@link IAccessAuthorizer#checkAccess} call carries exactly one S3 
action.
+   *
+   * @return true only if access is granted for the S3 action and all of its 
dependent S3 actions.
+   */
+  private boolean checkAccessIncludingDependentS3Actions(OzoneObj obj, 
RequestContext context)
+      throws OMException {
+    if (!accessAuthorizer.checkAccess(obj, context)) {
+      return false;
+    }
+    final String s3Action = context.getS3Action();
+    if (s3Action == null) {
+      return true;
+    }
+    for (String dependentAction : DEPENDENT_S3_ACTIONS.getOrDefault(s3Action, 
Collections.emptyList())) {
+      if (!accessAuthorizer.checkAccess(obj, 
context.toBuilder().setS3Action(dependentAction).build())) {
+        return false;
+      }
+    }
+    return true;
+  }
+
   /**
    * Enriches the given {@link RequestContext.Builder} with per-request fields 
from the Ozone Manager
    * thread locals: the session policy from {@link STSTokenIdentifier} (set on 
STS requests) and the
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOMMetadataReader.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOMMetadataReader.java
index 99b4c861c19..5defb87760f 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOMMetadataReader.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOMMetadataReader.java
@@ -23,6 +23,7 @@
 import static org.apache.hadoop.ozone.security.acl.OzoneObj.ResourceType.KEY;
 import static 
org.apache.hadoop.ozone.security.acl.OzoneObj.ResourceType.VOLUME;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -166,6 +167,78 @@ public void testCheckAclsAttachesS3ActionFromThreadLocal() 
throws Exception {
     verifyS3ActionPassedToAuthorizer(accessAuthorizer, obj, "GetObject");
   }
 
+  @Test
+  public void testCheckAclsAlsoChecksGetObjectForGetObjectAttributes() throws 
Exception {
+    OzoneManager.setS3Auth(S3Authentication.newBuilder()
+        .setAccessId(ACCESS_KEY_ID)
+        .setS3Action("GetObjectAttributes")
+        .build());
+
+    final IAccessAuthorizer accessAuthorizer = 
createMockIAccessAuthorizerReturningTrue();
+    final OmMetadataReader omMetadataReader = 
createMetadataReader(accessAuthorizer);
+    final OzoneObj obj = createTestOzoneObj();
+
+    assertTrue(omMetadataReader.checkAcls(obj, 
createTestRequestContextBuilder(), true));
+
+    final ArgumentCaptor<RequestContext> captor = 
ArgumentCaptor.forClass(RequestContext.class);
+    verify(accessAuthorizer, times(2)).checkAccess(eq(obj), captor.capture());
+    assertEquals("GetObjectAttributes", 
captor.getAllValues().get(0).getS3Action());
+    assertEquals("GetObject", captor.getAllValues().get(1).getS3Action());
+  }
+
+  @Test
+  public void testCheckAclsDeniedWhenDependentGetObjectActionDenied() throws 
Exception {
+    OzoneManager.setS3Auth(S3Authentication.newBuilder()
+        .setAccessId(ACCESS_KEY_ID)
+        .setS3Action("GetObjectAttributes")
+        .build());
+
+    final IAccessAuthorizer accessAuthorizer = 
createMockIAccessAuthorizerReturningTrue();
+    final OzoneObj obj = createTestOzoneObj();
+    when(accessAuthorizer.checkAccess(eq(obj), 
any(RequestContext.class))).thenAnswer(
+        invocation -> 
!"GetObject".equals(invocation.<RequestContext>getArgument(1).getS3Action()));
+    final OmMetadataReader omMetadataReader = 
createMetadataReader(accessAuthorizer);
+
+    final OMException ex = assertThrows(
+        OMException.class, () -> omMetadataReader.checkAcls(obj, 
createTestRequestContextBuilder(), true));
+
+    assertEquals(ResultCodes.PERMISSION_DENIED, ex.getResult());
+    verify(accessAuthorizer, times(2)).checkAccess(eq(obj), 
any(RequestContext.class));
+  }
+
+  @Test
+  public void testCheckAclsSkipsDependentActionWhenGetObjectAttributesDenied() 
throws Exception {
+    OzoneManager.setS3Auth(S3Authentication.newBuilder()
+        .setAccessId(ACCESS_KEY_ID)
+        .setS3Action("GetObjectAttributes")
+        .build());
+
+    final IAccessAuthorizer accessAuthorizer = 
createMockIAccessAuthorizerReturningTrue();
+    final OzoneObj obj = createTestOzoneObj();
+    when(accessAuthorizer.checkAccess(eq(obj), 
any(RequestContext.class))).thenReturn(false);
+    final OmMetadataReader omMetadataReader = 
createMetadataReader(accessAuthorizer);
+
+    assertFalse(omMetadataReader.checkAcls(obj, 
createTestRequestContextBuilder(), false));
+
+    verify(accessAuthorizer).checkAccess(eq(obj), any(RequestContext.class));
+  }
+
+  @Test
+  public void testCheckAclsDoesNotCheckDependentActionWhenStsFeatureDisabled() 
throws Exception {
+    OzoneManager.setS3Auth(S3Authentication.newBuilder()
+        .setAccessId(ACCESS_KEY_ID)
+        .setS3Action("GetObjectAttributes")
+        .build());
+
+    final IAccessAuthorizer accessAuthorizer = 
createMockIAccessAuthorizerReturningTrue();
+    final OmMetadataReader omMetadataReader = 
createMetadataReader(accessAuthorizer, mock(KeyManager.class), false);
+    final OzoneObj obj = createTestOzoneObj();
+
+    assertTrue(omMetadataReader.checkAcls(obj, 
createTestRequestContextBuilder(), true));
+
+    verifyS3ActionPassedToAuthorizer(accessAuthorizer, obj, null);
+  }
+
   @Test
   public void testCheckAclsDoesNotAttachS3ActionWhenStsFeatureDisabled() 
throws Exception {
     OzoneManager.setS3Auth(S3Authentication.newBuilder()
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
index 349d3765ad2..b7b02b5fc53 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
@@ -716,6 +716,7 @@ protected void copyDependenciesTo(EndpointBase target) {
     }
     target.queryParams = queryParams;
     target.s3Auth = s3Auth;
+    target.s3StsEnabled = s3StsEnabled;
     target.setClient(this.client);
     target.setOzoneConfiguration(this.ozoneConfiguration);
     target.setContext(this.context);
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectAttributesHandler.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectAttributesHandler.java
index 45755f22de4..011005223e4 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectAttributesHandler.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectAttributesHandler.java
@@ -134,6 +134,8 @@ Response handleGetRequest(ObjectRequestContext context, 
String keyPath)
       NavigableMap<Integer, Long> completedPartSizes = null;
       boolean directoryBucketLayout = false;
       try {
+        // AWS requires the credentials to support both s3:GetObject and 
s3:GetObjectAttributes actions. OM checks
+        // both while authorizing this single request, which is sent with the 
GetObjectAttributes action.
         if (requestedAttributes.contains(ATTR_OBJECT_PARTS)) {
           S3HeadObjectAttributes headAttributes =
               getClientProtocol().headS3ObjectAttributes(bucketName, keyPath);
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3GActionIamMapper.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3GActionIamMapper.java
index 7615828d422..259325eb8be 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3GActionIamMapper.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3GActionIamMapper.java
@@ -49,6 +49,16 @@ private S3GActionIamMapper() {
       return "GetBucketAcl";
     case PUT_ACL:
       return "PutBucketAcl";
+    case GET_BUCKET_TAGGING:
+      return "GetBucketTagging";
+    case PUT_BUCKET_TAGGING:
+    case DELETE_BUCKET_TAGGING:
+      return "PutBucketTagging";
+    case GET_BUCKET_LIFECYCLE:
+      return "GetLifecycleConfiguration";
+    case PUT_BUCKET_LIFECYCLE:
+    case DELETE_BUCKET_LIFECYCLE:
+      return "PutLifecycleConfiguration";
     case LIST_MULTIPART_UPLOAD:
       return "ListBucketMultipartUploads";
     case MULTI_DELETE:
@@ -67,6 +77,9 @@ private S3GActionIamMapper() {
     case GET_KEY:
     case HEAD_KEY:
       return "GetObject";
+    case GET_OBJECT_ATTRIBUTES:
+      // OM also enforces the dependent GetObject action when authorizing this 
request.
+      return "GetObjectAttributes";
     case ABORT_MULTIPART_UPLOAD:
       return "AbortMultipartUpload";
     case GET_OBJECT_TAGGING:
diff --git 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3ActionOverrideForOwnerVerification.java
 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3ActionOverrideForOwnerVerification.java
index c8bf7570cdc..d4f4befbe73 100644
--- 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3ActionOverrideForOwnerVerification.java
+++ 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3ActionOverrideForOwnerVerification.java
@@ -17,6 +17,7 @@
 
 package org.apache.hadoop.ozone.s3.endpoint;
 
+import static 
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.getObjectAttributes;
 import static org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.put;
 import static org.apache.hadoop.ozone.s3.util.S3Consts.COPY_SOURCE_HEADER;
 import static 
org.apache.hadoop.ozone.s3.util.S3Consts.EXPECTED_BUCKET_OWNER_HEADER;
@@ -33,26 +34,40 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.atomic.AtomicReference;
 import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 import org.apache.hadoop.ozone.OzoneConfigKeys;
 import org.apache.hadoop.ozone.client.ObjectStore;
 import org.apache.hadoop.ozone.client.OzoneBucket;
 import org.apache.hadoop.ozone.client.OzoneClient;
+import org.apache.hadoop.ozone.client.OzoneKey;
 import org.apache.hadoop.ozone.client.OzoneVolume;
+import org.apache.hadoop.ozone.client.S3HeadObjectAttributes;
 import org.apache.hadoop.ozone.client.protocol.ClientProtocol;
 import org.apache.hadoop.ozone.om.exceptions.OMException;
 import org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes;
+import org.apache.hadoop.ozone.om.helpers.BucketLayout;
 import org.apache.hadoop.ozone.om.protocol.S3Auth;
+import org.apache.hadoop.ozone.s3.exception.OS3Exception;
+import org.apache.hadoop.ozone.s3.metrics.S3GatewayMetrics;
 import org.apache.hadoop.ozone.s3.signature.SignatureInfo;
+import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 /**
- * Verifies bucket-owner-condition verification (source bucket owner lookup) 
runs under the correct IAM S3 action
- * string for copy-style operations.
+ * Verifies temporary IAM S3 action overrides used by composite operations.
  */
 public class TestS3ActionOverrideForOwnerVerification {
 
@@ -63,6 +78,11 @@ public class TestS3ActionOverrideForOwnerVerification {
   private static final String SOURCE_OWNER = "source-owner";
   private static final String DEST_OWNER = "dest-owner";
 
+  @BeforeAll
+  public static void setUp() {
+    S3GatewayMetrics.create(new OzoneConfiguration());
+  }
+
   @Test
   public void 
testUploadPartCopyUsesGetObjectActionForSourceBucketOwnerLookup() throws 
Exception {
     final AtomicReference<String> actionAtSourceBucketOwnerLookup = new 
AtomicReference<>();
@@ -109,6 +129,60 @@ public void 
testCopyObjectDoesNotSetActionWhenStsDisabled() throws Exception {
     assertNull(actionAtSourceBucketOwnerLookup.get());
   }
 
+  @ParameterizedTest
+  @ValueSource(strings = {"ETag", "ObjectParts"})
+  public void 
testGetObjectAttributesSendsSingleOmRequestWithGetObjectAttributesAction(String 
attribute)
+      throws Exception {
+    final List<String> actions = new ArrayList<>();
+    final ClientProtocol clientProtocol = mock(ClientProtocol.class);
+    final ObjectEndpoint endpoint = newGetObjectAttributesEndpoint(actions, 
true, clientProtocol, false);
+
+    final Response response = getObjectAttributes(endpoint, DEST_BUCKET, 
DEST_KEY, attribute);
+
+    assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+    // OM enforces the dependent GetObject action itself, so S3 Gateway makes 
exactly one OM request.
+    assertEquals(Collections.singletonList("GetObjectAttributes"), actions);
+    verifySingleMetadataLookup(clientProtocol, attribute);
+  }
+
+  @ParameterizedTest
+  @ValueSource(strings = {"ETag", "ObjectParts"})
+  public void 
testGetObjectAttributesSkipsAdditionalCheckWhenStsDisabled(String attribute) 
throws Exception {
+    final List<String> actions = new ArrayList<>();
+    final ObjectEndpoint endpoint = newGetObjectAttributesEndpoint(actions, 
false);
+
+    final Response response = getObjectAttributes(endpoint, DEST_BUCKET, 
DEST_KEY, attribute);
+
+    assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+    // With STS disabled, no S3 action is sent to OM.
+    assertEquals(Collections.singletonList(null), actions);
+  }
+
+  @ParameterizedTest
+  @ValueSource(strings = {"ETag", "ObjectParts"})
+  public void 
testGetObjectAttributesPermissionDeniedReturnsAccessDenied(String attribute) 
throws Exception {
+    final List<String> actions = new ArrayList<>();
+    final ClientProtocol clientProtocol = mock(ClientProtocol.class);
+    final ObjectEndpoint endpoint = newGetObjectAttributesEndpoint(actions, 
true, clientProtocol, true);
+
+    final OS3Exception ex = assertThrows(
+        OS3Exception.class, () -> getObjectAttributes(endpoint, DEST_BUCKET, 
DEST_KEY, attribute));
+
+    assertEquals("AccessDenied", ex.getCode());
+    assertEquals(Collections.singletonList("GetObjectAttributes"), actions);
+    verifySingleMetadataLookup(clientProtocol, attribute);
+  }
+
+  private static void verifySingleMetadataLookup(ClientProtocol 
clientProtocol, String attribute) throws Exception {
+    if ("ObjectParts".equals(attribute)) {
+      verify(clientProtocol).headS3ObjectAttributes(DEST_BUCKET, DEST_KEY);
+      verify(clientProtocol, never()).headS3Object(DEST_BUCKET, DEST_KEY);
+    } else {
+      verify(clientProtocol).headS3Object(DEST_BUCKET, DEST_KEY);
+      verify(clientProtocol, never()).headS3ObjectAttributes(DEST_BUCKET, 
DEST_KEY);
+    }
+  }
+
   private static ObjectEndpoint newEndpoint(AtomicReference<String> 
actionAtSourceBucketOwnerLookup,
       boolean isStsEnabled) throws Exception {
     final HttpHeaders headers = mock(HttpHeaders.class);
@@ -170,5 +244,70 @@ private static ObjectEndpoint 
newEndpoint(AtomicReference<String> actionAtSource
         .setSignatureInfo(signatureInfo)
         .build();
   }
+
+  private static ObjectEndpoint newGetObjectAttributesEndpoint(List<String> 
actions, boolean isStsEnabled)
+      throws Exception {
+    return newGetObjectAttributesEndpoint(actions, isStsEnabled, 
mock(ClientProtocol.class), false);
+  }
+
+  private static ObjectEndpoint newGetObjectAttributesEndpoint(List<String> 
actions, boolean isStsEnabled,
+      ClientProtocol clientProtocol, boolean denyAccess) throws Exception {
+    final HttpHeaders headers = mock(HttpHeaders.class);
+    final SignatureInfo signatureInfo = mock(SignatureInfo.class);
+    when(signatureInfo.isSignPayload()).thenReturn(true);
+    when(signatureInfo.getStringToSign()).thenReturn("string-to-sign");
+    when(signatureInfo.getSignature()).thenReturn("signature");
+    when(signatureInfo.getAwsAccessId()).thenReturn("access-id");
+
+    final OzoneClient client = mock(OzoneClient.class);
+    final ObjectStore objectStore = mock(ObjectStore.class);
+    final AtomicReference<S3Auth> s3AuthRef = new AtomicReference<>();
+    doAnswer(invocationOnMock -> {
+      s3AuthRef.set(invocationOnMock.getArgument(0));
+      return null;
+    }).when(clientProtocol).setThreadLocalS3Auth(any(S3Auth.class));
+
+    when(client.getObjectStore()).thenReturn(objectStore);
+    when(client.getProxy()).thenReturn(clientProtocol);
+    when(objectStore.getClientProxy()).thenReturn(clientProtocol);
+
+    final OzoneKey key = mock(OzoneKey.class);
+    when(key.isFile()).thenReturn(true);
+    when(key.getModificationTime()).thenReturn(Instant.now());
+    final S3HeadObjectAttributes headAttributes = 
mock(S3HeadObjectAttributes.class);
+    when(headAttributes.getKey()).thenReturn(key);
+    
when(headAttributes.getBucketLayout()).thenReturn(BucketLayout.OBJECT_STORE);
+
+    when(clientProtocol.headS3Object(DEST_BUCKET, 
DEST_KEY)).thenAnswer(invocationOnMock -> {
+      recordS3Action(s3AuthRef, actions);
+      return returnOrDeny(key, denyAccess);
+    });
+    when(clientProtocol.headS3ObjectAttributes(DEST_BUCKET, 
DEST_KEY)).thenAnswer(invocationOnMock -> {
+      recordS3Action(s3AuthRef, actions);
+      return returnOrDeny(headAttributes, denyAccess);
+    });
+
+    final OzoneConfiguration conf = new OzoneConfiguration();
+    conf.setBoolean(OzoneConfigKeys.OZONE_S3G_STS_HTTP_ENABLED_KEY, 
isStsEnabled);
+    return EndpointBuilder.newObjectEndpointBuilder()
+        .setClient(client)
+        .setConfig(conf)
+        .setHeaders(headers)
+        .setSignatureInfo(signatureInfo)
+        .build();
+  }
+
+  private static <T> T returnOrDeny(T result, boolean denyAccess) throws 
OMException {
+    if (denyAccess) {
+      throw new OMException("Permission denied", 
ResultCodes.PERMISSION_DENIED);
+    }
+    return result;
+  }
+
+  private static void recordS3Action(AtomicReference<S3Auth> s3AuthRef, 
List<String> actions) {
+    final S3Auth s3Auth = s3AuthRef.get();
+    assertNotNull(s3Auth, "S3Auth must be initialized before metadata lookup");
+    actions.add(s3Auth.getS3Action());
+  }
 }
 
diff --git 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestS3GActionIamMapper.java
 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestS3GActionIamMapper.java
index b2d1625a185..338dcabfe11 100644
--- 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestS3GActionIamMapper.java
+++ 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestS3GActionIamMapper.java
@@ -52,6 +52,13 @@ public void mapsCoreObjectActions() {
     assertEquals("DeleteObjectTagging", 
S3GActionIamMapper.toS3ActionString(S3GAction.DELETE_OBJECT_TAGGING));
     assertEquals("GetObjectAcl", 
S3GActionIamMapper.toS3ActionString(S3GAction.GET_OBJECT_ACL));
     assertEquals("PutObjectAcl", 
S3GActionIamMapper.toS3ActionString(S3GAction.PUT_OBJECT_ACL));
+    assertEquals("GetObjectAttributes", 
S3GActionIamMapper.toS3ActionString(S3GAction.GET_OBJECT_ATTRIBUTES));
+    assertEquals("GetBucketTagging", 
S3GActionIamMapper.toS3ActionString(S3GAction.GET_BUCKET_TAGGING));
+    assertEquals("PutBucketTagging", 
S3GActionIamMapper.toS3ActionString(S3GAction.PUT_BUCKET_TAGGING));
+    assertEquals("PutBucketTagging", 
S3GActionIamMapper.toS3ActionString(S3GAction.DELETE_BUCKET_TAGGING));
+    assertEquals("GetLifecycleConfiguration", 
S3GActionIamMapper.toS3ActionString(S3GAction.GET_BUCKET_LIFECYCLE));
+    assertEquals("PutLifecycleConfiguration", 
S3GActionIamMapper.toS3ActionString(S3GAction.PUT_BUCKET_LIFECYCLE));
+    assertEquals("PutLifecycleConfiguration", 
S3GActionIamMapper.toS3ActionString(S3GAction.DELETE_BUCKET_LIFECYCLE));
   }
 
   @Test


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

Reply via email to