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

ChenSammi 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 fd0ae5c7f7e HDDS-15137. [STS] Ensure each S3 API has an associated S3 
Action (#10197)
fd0ae5c7f7e is described below

commit fd0ae5c7f7ec626c326a20be1b972a5e8e784771
Author: fmorg-git <[email protected]>
AuthorDate: Fri Jun 12 01:10:31 2026 -0700

    HDDS-15137. [STS] Ensure each S3 API has an associated S3 Action (#10197)
---
 .../apache/hadoop/ozone/om/protocol/S3Auth.java    |  10 ++
 ...OzoneManagerProtocolClientSideTranslatorPB.java |   8 +-
 .../src/main/proto/OmClientProtocol.proto          |   3 +
 .../apache/hadoop/ozone/om/OmMetadataReader.java   |  59 ++++-----
 .../org/apache/hadoop/ozone/om/OzoneManager.java   |   7 +-
 .../ozone/om/ratis/OzoneManagerStateMachine.java   |   8 ++
 .../hadoop/ozone/om/request/OMClientRequest.java   |   2 +-
 .../hadoop/ozone/om/TestOMMetadataReader.java      | 140 +++++++++++++-------
 .../hadoop/ozone/s3/endpoint/BucketEndpoint.java   |  17 ++-
 .../hadoop/ozone/s3/endpoint/EndpointBase.java     |  36 ++++++
 .../hadoop/ozone/s3/endpoint/ObjectEndpoint.java   | 144 +++++++++++----------
 .../hadoop/ozone/s3/endpoint/RootEndpoint.java     |   8 +-
 .../hadoop/ozone/s3/endpoint/S3RequestContext.java |   2 +
 .../hadoop/ozone/s3/util/S3GActionIamMapper.java   |  92 +++++++++++++
 .../ozone/s3/endpoint/TestCopyActionsAudit.java    | 133 +++++++++++++++++++
 .../ozone/s3/util/TestS3GActionIamMapper.java      |  68 ++++++++++
 .../apache/hadoop/ozone/s3/util/package-info.java  |  21 +++
 17 files changed, 593 insertions(+), 165 deletions(-)

diff --git 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/S3Auth.java
 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/S3Auth.java
index fa023dfc811..577339c96ac 100644
--- 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/S3Auth.java
+++ 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/S3Auth.java
@@ -29,6 +29,8 @@ public class S3Auth {
   private String userPrincipal;
   // Optional STS session token when using temporary credentials
   private String sessionToken;
+  // S3 action without s3: prefix (e.g. PutObject), set by S3 Gateway for use 
in finer-grained STS permissions.
+  private String s3Action;
 
   public S3Auth(final String stringToSign,
                 final String signature,
@@ -67,4 +69,12 @@ public String getSessionToken() {
   public void setSessionToken(String sessionToken) {
     this.sessionToken = sessionToken;
   }
+
+  public String getS3Action() {
+    return s3Action;
+  }
+
+  public void setS3Action(String s3Action) {
+    this.s3Action = s3Action;
+  }
 }
diff --git 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
index 3030052d75d..8fdf9712c08 100644
--- 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
+++ 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
@@ -332,6 +332,9 @@ private OMResponse submitRequest(OMRequest omRequest)
       if (threadLocalS3Auth.get().getSessionToken() != null) {
         
s3AuthBuilder.setSessionToken(threadLocalS3Auth.get().getSessionToken());
       }
+      if (threadLocalS3Auth.get().getS3Action() != null) {
+        s3AuthBuilder.setS3Action(threadLocalS3Auth.get().getS3Action());
+      }
 
       builder.setS3Authentication(s3AuthBuilder.build());
     }
@@ -1788,10 +1791,7 @@ public OmMultipartCommitUploadPartInfo 
commitMultipartUploadPart(
         handleError(submitRequest(omRequest))
         .getCommitMultiPartUploadResponse();
 
-    OmMultipartCommitUploadPartInfo info = new
-        OmMultipartCommitUploadPartInfo(response.getPartName(),
-          response.getETag());
-    return info;
+    return new OmMultipartCommitUploadPartInfo(response.getPartName(), 
response.getETag());
   }
 
   @Override
diff --git 
a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto 
b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
index 5cf503275a2..7674d85ca92 100644
--- a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
+++ b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
@@ -2346,6 +2346,9 @@ message S3Authentication {
     optional string resolvedStsOriginalAccessKeyId = 7;
     optional string resolvedStsTempAccessKeyId = 8;
     optional string resolvedStsSecretKeyId = 9;
+    // S3 action without the s3: prefix for this request (e.g. GetObject), set 
by S3 Gateway for use
+    // in finer-grained STS permissions.
+    optional string s3Action = 10;
 }
 
 message RecoverLeaseRequest {
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 46573917c88..40caed20554 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
@@ -55,6 +55,7 @@
 import org.apache.hadoop.ozone.om.helpers.OzoneFileStatusLight;
 import org.apache.hadoop.ozone.om.helpers.S3VolumeContext;
 import org.apache.hadoop.ozone.om.protocolPB.grpc.GrpcClientConstants;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.S3Authentication;
 import org.apache.hadoop.ozone.security.STSTokenIdentifier;
 import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer;
 import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLIdentityType;
@@ -236,9 +237,7 @@ public List<OzoneFileStatus> listStatus(OmKeyArgs args, 
boolean recursive,
     try {
       if (isAclEnabled) {
         if (isStsS3Request()) {
-          // We need to be able to tell the difference between being able to 
download a file and merely seeing the file
-          // name in a list.  Use READ for download ability and LIST (here) 
for listing.
-          // When listPrefix is set (original S3 ListObjects prefix), 
authorize LIST on that prefix for the whole
+          // When listPrefix is set (original S3 ListObjects prefix), 
authorize READ on that prefix for the whole
           // listing, including FSO traversal where keyName is an internal 
directory (e.g. userA) under prefix user.
           final String listPrefix = args.getListPrefix();
           final String keyName = args.getKeyName();
@@ -258,7 +257,7 @@ public List<OzoneFileStatus> listStatus(OmKeyArgs args, 
boolean recursive,
           } else {
             aclKey = "*";
           }
-          checkAcls(ResourceType.KEY, StoreType.OZONE, ACLType.LIST, 
bucket.realVolume(), bucket.realBucket(), aclKey);
+          checkAcls(ResourceType.KEY, StoreType.OZONE, ACLType.READ, 
bucket.realVolume(), bucket.realBucket(), aclKey);
         } else {
           checkAcls(getResourceType(args), StoreType.OZONE, ACLType.READ,
               bucket, args.getKeyName());
@@ -304,12 +303,7 @@ public OzoneFileStatus getFileStatus(OmKeyArgs args) 
throws IOException {
 
     try {
       if (isAclEnabled) {
-        if (isStsS3Request()) {
-          checkAcls(getResourceType(args), StoreType.OZONE, ACLType.LIST, 
bucket, args.getKeyName());
-        } else {
-          checkAcls(getResourceType(args), StoreType.OZONE, ACLType.READ,
-              bucket, args.getKeyName());
-        }
+        checkAcls(getResourceType(args), StoreType.OZONE, ACLType.READ, 
bucket, args.getKeyName());
       }
       metrics.incNumGetFileStatus();
       return keyManager.getFileStatus(args, getClientAddress());
@@ -384,7 +378,7 @@ public ListKeysResult listKeys(String volumeName, String 
bucketName,
           final String aclKey = (keyPrefix == null || keyPrefix.isEmpty()) ? 
"*" : keyPrefix;
           captureLatencyNs(
               perfMetrics.getListKeysAclCheckLatencyNs(), () -> checkAcls(
-                  ResourceType.KEY, StoreType.OZONE, ACLType.LIST, 
bucket.realVolume(), bucket.realBucket(), aclKey));
+                  ResourceType.KEY, StoreType.OZONE, ACLType.READ, 
bucket.realVolume(), bucket.realBucket(), aclKey));
         } else {
           captureLatencyNs(perfMetrics.getListKeysAclCheckLatencyNs(), () ->
               checkAcls(ResourceType.BUCKET, StoreType.OZONE, ACLType.LIST,
@@ -612,16 +606,15 @@ public boolean checkAcls(ResourceType resType, StoreType 
storeType,
         .setVolumeName(vol)
         .setBucketName(bucket)
         .setKeyName(key).build();
-    RequestContext context = RequestContext.newBuilder()
+    RequestContext.Builder contextBuilder = RequestContext.newBuilder()
         .setClientUgi(ugi)
         .setIp(remoteAddress)
         .setHost(hostName)
         .setAclType(ACLIdentityType.USER)
         .setAclRights(aclType)
-        .setOwnerName(owner)
-        .build();
+        .setOwnerName(owner);
 
-    return checkAcls(obj, context, throwIfPermissionDenied);
+    return checkAcls(obj, contextBuilder, throwIfPermissionDenied);
   }
 
   /**
@@ -631,13 +624,14 @@ public boolean checkAcls(ResourceType resType, StoreType 
storeType,
    * @throws OMException ResultCodes.PERMISSION_DENIED if permission denied
    *                     and throwOnPermissionDenied set to true.
    */
-  public boolean checkAcls(OzoneObj obj, RequestContext context,
+  public boolean checkAcls(OzoneObj obj, RequestContext.Builder contextBuilder,
       boolean throwIfPermissionDenied) throws OMException {
 
-    final RequestContext normalizedRequestContext = 
maybeAttachSessionPolicyFromThreadLocal(context);
+    maybeAddToContextFromThreadLocal(contextBuilder);
+    final RequestContext context = contextBuilder.build();
 
     if (!captureLatencyNs(perfMetrics::setCheckAccessLatencyNs,
-        () -> accessAuthorizer.checkAccess(obj, normalizedRequestContext))) {
+        () -> accessAuthorizer.checkAccess(obj, context))) {
       if (throwIfPermissionDenied) {
         String volumeName = obj.getVolumeName() != null ?
                 "Volume:" + obj.getVolumeName() + " " : "";
@@ -647,7 +641,7 @@ public boolean checkAcls(OzoneObj obj, RequestContext 
context,
                 "Key:" + obj.getKeyName() : "";
         // For STS tokens, make clear that the user is using an assumed role, 
otherwise the access denied
         // message could be confusing
-        String user = 
normalizedRequestContext.getClientUgi().getShortUserName();
+        String user = context.getClientUgi().getShortUserName();
         final STSTokenIdentifier stsTokenIdentifier = 
OzoneManager.getStsTokenIdentifier();
         if (stsTokenIdentifier != null) {
           final StringBuilder builder = new StringBuilder(user)
@@ -660,11 +654,11 @@ public boolean checkAcls(OzoneObj obj, RequestContext 
context,
         }
         log.warn("User {} doesn't have {} permission to access {} {}{}{}",
             user,
-            normalizedRequestContext.getAclRights(),
+            context.getAclRights(),
             obj.getResourceType(), volumeName, bucketName, keyName);
         throw new OMException(
             "User " + user +
-            " doesn't have " + normalizedRequestContext.getAclRights() +
+            " doesn't have " + context.getAclRights() +
             " permission to access " + obj.getResourceType() + " " +
             volumeName  + bucketName + keyName, ResultCodes.PERMISSION_DENIED);
       }
@@ -675,21 +669,22 @@ public boolean checkAcls(OzoneObj obj, RequestContext 
context,
   }
 
   /**
-   * Attaches session policy to RequestContext if an STSTokenIdentifier is 
found in the Ozone Manager thread local
-   * (meaning this is an STS request), and the STSTokenIdentifier has a 
session policy.  Otherwise, returns the
-   * RequestContext as it was before.
-   * @param context the original RequestContext
-   * @return RequestContext as before or with sessionPolicy embedded
+   * 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
+   * S3 action from {@link S3Authentication} (set on S3 requests). Either or 
both may be absent, in
+   * which case the corresponding field is left untouched on the builder.
+   * @param contextBuilder the builder to enrich in-place
    */
-  private RequestContext 
maybeAttachSessionPolicyFromThreadLocal(RequestContext context) {
+  public static void maybeAddToContextFromThreadLocal(RequestContext.Builder 
contextBuilder) {
     final STSTokenIdentifier stsTokenIdentifier = 
OzoneManager.getStsTokenIdentifier();
-    if (stsTokenIdentifier == null) {
-      return context;
+    if (stsTokenIdentifier != null) {
+      contextBuilder.setSessionPolicy(stsTokenIdentifier.getSessionPolicy());
     }
 
-    return context.toBuilder()
-        .setSessionPolicy(stsTokenIdentifier.getSessionPolicy())
-        .build();
+    final S3Authentication s3Authentication = OzoneManager.getS3Auth();
+    if (s3Authentication != null && s3Authentication.hasS3Action() && 
!s3Authentication.getS3Action().isEmpty()) {
+      contextBuilder.setS3Action(s3Authentication.getS3Action());
+    }
   }
 
   static String getClientAddress() {
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
index 1986b34acb3..012f7c25dda 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
@@ -2891,16 +2891,15 @@ public boolean checkAcls(ResourceType resType, 
StoreType storeType,
         .setVolumeName(vol)
         .setBucketName(bucket)
         .setKeyName(key).build();
-    RequestContext context = RequestContext.newBuilder()
+    RequestContext.Builder contextBuilder = RequestContext.newBuilder()
         .setClientUgi(ugi)
         .setIp(remoteAddress)
         .setHost(hostName)
         .setAclType(ACLIdentityType.USER)
         .setAclRights(aclType)
-        .setOwnerName(owner)
-        .build();
+        .setOwnerName(owner);
 
-    return omMetadataReader.checkAcls(obj, context, throwIfPermissionDenied);
+    return omMetadataReader.checkAcls(obj, contextBuilder, 
throwIfPermissionDenied);
   }
 
   /**
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/OzoneManagerStateMachine.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/OzoneManagerStateMachine.java
index 34c5a485d61..2603cd78949 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/OzoneManagerStateMachine.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/OzoneManagerStateMachine.java
@@ -664,6 +664,7 @@ public void close() {
    */
   @VisibleForTesting
   OMResponse runCommand(OMRequest request, TermIndex termIndex) {
+    boolean isS3AuthThreadLocalSet = false;
     boolean isStsThreadLocalSet = false;
     try {
       if (ozoneManager.isSecurityEnabled() && request.hasS3Authentication()) {
@@ -672,6 +673,10 @@ OMResponse runCommand(OMRequest request, TermIndex 
termIndex) {
         STSSecurityUtil.ensureResolvedStsFieldsInvariants(request);
 
         final OzoneManagerProtocolProtos.S3Authentication s3Auth = 
request.getS3Authentication();
+        // ThreadLocal carries S3 action for OmMetadataReader.
+        OzoneManager.setS3Auth(s3Auth);
+        isS3AuthThreadLocalSet = true;
+
         if (s3Auth.hasSessionToken() && !s3Auth.getSessionToken().isEmpty()) {
           // ThreadLocal carries session policy for OmMetadataReader
           final STSTokenIdentifier rehydratedTokenIdentifier = new 
STSTokenIdentifier(
@@ -706,6 +711,9 @@ OMResponse runCommand(OMRequest request, TermIndex 
termIndex) {
       String errorMessage = "Request " + request + " failed with exception";
       ExitUtils.terminate(1, errorMessage, e, LOG);
     } finally {
+      if (isS3AuthThreadLocalSet) {
+        OzoneManager.setS3Auth(null);
+      }
       if (isStsThreadLocalSet) {
         OzoneManager.setStsTokenIdentifier(null);
       }
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java
index 81d6925442b..6b9c6698cf9 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java
@@ -385,7 +385,7 @@ protected void checkACLsWithFSO(OzoneManager ozoneManager, 
String volumeName,
         OmMetadataReader omMetadataReader =
             (OmMetadataReader) rcMetadataReader.get();
 
-        omMetadataReader.checkAcls(obj, contextBuilder.build(), true);
+        omMetadataReader.checkAcls(obj, contextBuilder, true);
       }
     }
   }
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 8403d2203e0..e35a3952193 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,7 +23,6 @@
 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;
@@ -78,8 +77,9 @@ public class TestOMMetadataReader {
   private static final long MAX_KEYS = 100L;
 
   @AfterEach
-  public void clearStsThreadLocal() {
+  public void clearOmThreadLocals() {
     OzoneManager.setStsTokenIdentifier(null);
+    OzoneManager.setS3Auth(null);
   }
 
   @Test
@@ -120,10 +120,10 @@ public void 
testCheckAclsAttachesSessionPolicyFromThreadLocal() throws Exception
     final IAccessAuthorizer accessAuthorizer = 
createMockIAccessAuthorizerReturningTrue();
     final OmMetadataReader omMetadataReader = 
createMetadataReader(accessAuthorizer);
 
-    final RequestContext contextWithoutSessionPolicy = 
createTestRequestContext();
+    final RequestContext.Builder contextWithoutSessionPolicyBuilder = 
createTestRequestContextBuilder();
     final OzoneObj obj = createTestOzoneObj();
 
-    assertTrue(omMetadataReader.checkAcls(obj, contextWithoutSessionPolicy, 
true));
+    assertTrue(omMetadataReader.checkAcls(obj, 
contextWithoutSessionPolicyBuilder, true));
 
     verifySessionPolicyPassedToAuthorizer(accessAuthorizer, obj, 
sessionPolicy);
   }
@@ -136,16 +136,67 @@ public void testNoSessionPolicyWhenThreadLocalIsNull() 
throws Exception {
     final IAccessAuthorizer accessAuthorizer = 
createMockIAccessAuthorizerReturningTrue();
     final OmMetadataReader omMetadataReader = 
createMetadataReader(accessAuthorizer);
 
-    final RequestContext contextWithoutSessionPolicy = 
createTestRequestContext();
+    final RequestContext.Builder contextWithoutSessionPolicyBuilder = 
createTestRequestContextBuilder();
     final OzoneObj obj = createTestOzoneObj();
 
-    assertTrue(omMetadataReader.checkAcls(obj, contextWithoutSessionPolicy, 
true));
+    assertTrue(omMetadataReader.checkAcls(obj, 
contextWithoutSessionPolicyBuilder, true));
 
     verifySessionPolicyPassedToAuthorizer(accessAuthorizer, obj, null);
   }
 
   @Test
-  public void testListStatusUsesListAclForStsS3Request() throws Exception {
+  public void testCheckAclsAttachesS3ActionFromThreadLocal() throws Exception {
+    OzoneManager.setS3Auth(S3Authentication.newBuilder()
+        .setAccessId(ACCESS_KEY_ID)
+        .setS3Action("GetObject")
+        .build());
+
+    final IAccessAuthorizer accessAuthorizer = 
createMockIAccessAuthorizerReturningTrue();
+    final OmMetadataReader omMetadataReader = 
createMetadataReader(accessAuthorizer);
+
+    final RequestContext.Builder contextWithoutS3ActionBuilder = 
createTestRequestContextBuilder();
+    final OzoneObj obj = createTestOzoneObj();
+
+    assertTrue(omMetadataReader.checkAcls(obj, contextWithoutS3ActionBuilder, 
true));
+
+    verifyS3ActionPassedToAuthorizer(accessAuthorizer, obj, "GetObject");
+  }
+
+  @Test
+  public void testCheckAclsLeavesS3ActionUnsetWhenS3AuthThreadLocalNull() 
throws Exception {
+    final IAccessAuthorizer accessAuthorizer = 
createMockIAccessAuthorizerReturningTrue();
+    final OmMetadataReader omMetadataReader = 
createMetadataReader(accessAuthorizer);
+
+    final RequestContext.Builder contextWithoutS3ActionBuilder = 
createTestRequestContextBuilder();
+    final OzoneObj obj = createTestOzoneObj();
+
+    assertTrue(omMetadataReader.checkAcls(obj, contextWithoutS3ActionBuilder, 
true));
+
+    verifyS3ActionPassedToAuthorizer(accessAuthorizer, obj, null);
+  }
+
+  @Test
+  public void testCheckAclsAttachesSessionPolicyAndS3ActionFromThreadLocals() 
throws Exception {
+    setupStsTokenIdentifier();
+
+    OzoneManager.setS3Auth(S3Authentication.newBuilder()
+        .setAccessId(ACCESS_KEY_ID)
+        .setS3Action("PutObject")
+        .build());
+
+    final IAccessAuthorizer accessAuthorizer = 
createMockIAccessAuthorizerReturningTrue();
+    final OmMetadataReader omMetadataReader = 
createMetadataReader(accessAuthorizer);
+
+    final RequestContext.Builder baseContextBuilder = 
createTestRequestContextBuilder();
+    final OzoneObj obj = createTestOzoneObj();
+
+    assertTrue(omMetadataReader.checkAcls(obj, baseContextBuilder, true));
+
+    verifySessionPolicyAndS3ActionPassedToAuthorizer(accessAuthorizer, obj);
+  }
+
+  @Test
+  public void testListStatusUsesReadAclForStsS3Request() throws Exception {
     setupStsS3Request();
 
     final IAccessAuthorizer accessAuthorizer = 
createMockIAccessAuthorizerReturningTrue();
@@ -161,10 +212,9 @@ public void testListStatusUsesListAclForStsS3Request() 
throws Exception {
 
     // For STS S3 requests, listStatus() performs these checks:
     // 1. Volume READ (for volume access)
-    // 2) Key LIST (for the specific prefix being listed) - we need LIST 
permission for STS in order to tell whether the
-    //    file should be listed only or downloadable (downloadable would be 
READ)
+    // 2) Key READ (for the specific prefix being listed)
     assertContainsVolumeReadCheck(checks);
-    assertContainsKeyListCheckWithName(checks, KEY_PREFIX);
+    assertContainsKeyReadCheckWithName(checks, KEY_PREFIX);
   }
 
   @Test
@@ -186,7 +236,6 @@ public void testListStatusUsesReadAclForNonStsRequest() 
throws Exception {
     assertContainsVolumeReadCheck(checks);
     // We want to ensure the current behavior for non-STS requests remains the 
same
     assertContainsKeyReadCheckWithName(checks);
-    assertDoesNotContainKeyListCheck(checks);
   }
 
   @Test
@@ -209,7 +258,7 @@ public void 
testListStatusUsesListPrefixForAclWhenKeyNameEmptyAndListPrefixSet()
 
     final List<AclCheck> checks = captureAclChecks(accessAuthorizer, 2);
     assertContainsVolumeReadCheck(checks);
-    assertContainsKeyListCheckWithName(checks, "userA/");
+    assertContainsKeyReadCheckWithName(checks, "userA/");
   }
 
   @Test
@@ -231,7 +280,7 @@ public void 
testListStatusUsesWildcardForAclWhenKeyNameAndListPrefixEmpty() thro
 
     final List<AclCheck> checks = captureAclChecks(accessAuthorizer, 2);
     assertContainsVolumeReadCheck(checks);
-    assertContainsKeyListCheckWithName(checks, "*");
+    assertContainsKeyReadCheckWithName(checks, "*");
   }
 
   @Test
@@ -254,7 +303,7 @@ public void 
testListStatusUsesListPrefixForAclWhenKeyNameIsDescendantOfListPrefi
 
     final List<AclCheck> checks = captureAclChecks(accessAuthorizer, 2);
     assertContainsVolumeReadCheck(checks);
-    assertContainsKeyListCheckWithName(checks, "user");
+    assertContainsKeyReadCheckWithName(checks, "user");
   }
 
   @Test
@@ -277,7 +326,7 @@ public void 
testListStatusUsesListPrefixForAclWhenKeyNameIsAncestorOfListPrefix(
 
     final List<AclCheck> checks = captureAclChecks(accessAuthorizer, 2);
     assertContainsVolumeReadCheck(checks);
-    assertContainsKeyListCheckWithName(checks, "user/foo");
+    assertContainsKeyReadCheckWithName(checks, "user/foo");
   }
 
   @Test
@@ -301,7 +350,7 @@ public void 
testListStatusThrowsWhenStsKeyNameNotUnderListPrefix() throws Except
   }
 
   @Test
-  public void testGetFileStatusUsesListAclForStsS3Request() throws Exception {
+  public void testGetFileStatusUsesReadAclForStsS3Request() throws Exception {
     setupStsS3Request();
 
     final IAccessAuthorizer accessAuthorizer = 
createMockIAccessAuthorizerReturningTrue();
@@ -314,8 +363,7 @@ public void testGetFileStatusUsesListAclForStsS3Request() 
throws Exception {
 
     final List<AclCheck> checks = captureAclChecks(accessAuthorizer, 2);
     assertContainsVolumeReadCheck(checks);
-    assertContainsKeyListCheckWithName(checks, KEY_PREFIX);
-    assertDoesNotContainKeyReadCheck(checks);
+    assertContainsKeyReadCheckWithName(checks, KEY_PREFIX);
   }
 
   @Test
@@ -333,7 +381,6 @@ public void 
testGetFileStatusUsesReadAclForNonStsS3Request() throws Exception {
     final List<AclCheck> checks = captureAclChecks(accessAuthorizer, 2);
     assertContainsVolumeReadCheck(checks);
     assertContainsKeyReadCheckWithName(checks);
-    assertDoesNotContainKeyListCheck(checks);
   }
 
   @Test
@@ -350,7 +397,7 @@ public void testListKeysUsesPrefixCheckForStsS3Request() 
throws Exception {
 
     List<AclCheck> checks = captureAclChecks(accessAuthorizer, 4);
     assertContainsBucketListCheck(checks);
-    assertContainsKeyListCheckWithName(checks, "userA/");
+    assertContainsKeyReadCheckWithName(checks, "userA/");
 
     // Reset to make case 2 assertions independent of case 1 captures.
     reset(accessAuthorizer);
@@ -361,7 +408,7 @@ public void testListKeysUsesPrefixCheckForStsS3Request() 
throws Exception {
 
     checks = captureAclChecks(accessAuthorizer, 4);
     assertContainsBucketListCheck(checks);
-    assertContainsKeyListCheckWithName(checks, "*");
+    assertContainsKeyReadCheckWithName(checks, "*");
   }
 
   private OmMetadataReader createMetadataReader(IAccessAuthorizer 
accessAuthorizer) throws IOException {
@@ -418,20 +465,18 @@ private IAccessAuthorizer 
createMockIAccessAuthorizerReturningTrue() throws OMEx
   }
 
   /**
-   * Creates a test RequestContext.
+   * Creates a test RequestContext.Builder.
    *
-   * @return the constructed RequestContext
+   * @return the constructed RequestContext.Builder
    */
-  private RequestContext createTestRequestContext() {
-    RequestContext.Builder builder = RequestContext.newBuilder()
+  private RequestContext.Builder createTestRequestContextBuilder() {
+    return RequestContext.newBuilder()
         .setClientUgi(UserGroupInformation.createRemoteUser("testUser"))
         .setIp(InetAddress.getLoopbackAddress())
         .setHost("localhost")
         .setAclType(IAccessAuthorizer.ACLIdentityType.USER)
         .setAclRights(READ)
         .setOwnerName("owner");
-
-    return builder.build();
   }
 
   /**
@@ -505,6 +550,27 @@ private void 
verifySessionPolicyPassedToAuthorizer(IAccessAuthorizer accessAutho
     assertEquals(expectedSessionPolicy, captor.getValue().getSessionPolicy());
   }
 
+  /**
+   * Verifies that the accessAuthorizer received a call to checkAccess with 
the expected s3 action.
+   * @param accessAuthorizer the mock authorizer to verify
+   * @param expectedObj the expected OzoneObj
+   * @param expectedS3Action the expected s3 action (could be null)
+   */
+  private void verifyS3ActionPassedToAuthorizer(IAccessAuthorizer 
accessAuthorizer, OzoneObj expectedObj,
+      String expectedS3Action) throws OMException {
+    final ArgumentCaptor<RequestContext> captor = 
ArgumentCaptor.forClass(RequestContext.class);
+    verify(accessAuthorizer).checkAccess(eq(expectedObj), captor.capture());
+    assertEquals(expectedS3Action, captor.getValue().getS3Action());
+  }
+
+  private void 
verifySessionPolicyAndS3ActionPassedToAuthorizer(IAccessAuthorizer 
accessAuthorizer,
+      OzoneObj expectedObj) throws OMException {
+    final ArgumentCaptor<RequestContext> captor = 
ArgumentCaptor.forClass(RequestContext.class);
+    verify(accessAuthorizer).checkAccess(eq(expectedObj), captor.capture());
+    assertEquals("session-policy-from-thread-local", 
captor.getValue().getSessionPolicy());
+    assertEquals("PutObject", captor.getValue().getS3Action());
+  }
+
   private List<AclCheck> captureAclChecks(IAccessAuthorizer accessAuthorizer, 
int expectedCheckCount)
       throws OMException {
     final ArgumentCaptor<OzoneObj> objCaptor = 
ArgumentCaptor.forClass(OzoneObj.class);
@@ -538,12 +604,12 @@ private void assertContainsBucketListCheck(List<AclCheck> 
checks) {
         "Expected a BUCKET LIST ACL check");
   }
 
-  private void assertContainsKeyListCheckWithName(List<AclCheck> checks, 
String keyName) {
+  private void assertContainsKeyReadCheckWithName(List<AclCheck> checks, 
String keyName) {
     assertTrue(
         checks.stream().anyMatch(
-            check -> check.getObj().getResourceType() == KEY && 
check.getContext().getAclRights() == LIST &&
+            check -> check.getObj().getResourceType() == KEY && 
check.getContext().getAclRights() == READ &&
                 keyName.equals(check.getObj().getKeyName())),
-        "Expected a KEY LIST ACL check for key '" + keyName + "'");
+        "Expected a KEY READ ACL check for key '" + keyName + "'");
   }
 
   private void assertContainsKeyReadCheckWithName(List<AclCheck> checks) {
@@ -554,20 +620,6 @@ private void 
assertContainsKeyReadCheckWithName(List<AclCheck> checks) {
         "Expected a KEY READ ACL check for key '" + 
TestOMMetadataReader.KEY_PREFIX + "'");
   }
 
-  private void assertDoesNotContainKeyReadCheck(List<AclCheck> checks) {
-    assertFalse(
-        checks.stream().anyMatch(
-            check -> check.getObj().getResourceType() == KEY && 
check.getContext().getAclRights() == READ),
-        "Did not expect a KEY READ ACL check");
-  }
-
-  private void assertDoesNotContainKeyListCheck(List<AclCheck> checks) {
-    assertFalse(
-        checks.stream().anyMatch(
-            check -> check.getObj().getResourceType() == KEY && 
check.getContext().getAclRights() == LIST),
-        "Did not expect a KEY LIST ACL check");
-  }
-
   private static final class AclCheck {
     private final OzoneObj obj;
     private final RequestContext context;
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
index 1e78813e971..66806be8e38 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
@@ -64,7 +64,6 @@
 import org.apache.hadoop.ozone.s3.util.ContinueToken;
 import org.apache.hadoop.ozone.s3.util.S3Consts.QueryParams;
 import org.apache.hadoop.ozone.s3.util.S3StorageType;
-import org.apache.hadoop.util.Time;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -286,19 +285,19 @@ Response handlePutRequest(S3RequestContext context, 
String bucketName, InputStre
   @HEAD
   public Response head(@PathParam(BUCKET) String bucketName)
       throws OS3Exception, IOException {
-    long startNanos = Time.monotonicNowNanos();
-    S3GAction s3GAction = S3GAction.HEAD_BUCKET;
+    S3RequestContext context = new S3RequestContext(this, 
S3GAction.HEAD_BUCKET);
+    long startNanos = context.getStartNanos();
     try {
       OzoneBucket bucket = getVolume().getBucket(bucketName);
       S3Owner.verifyBucketOwnerCondition(getHeaders(), bucketName, 
bucket.getOwner());
-      auditReadSuccess(s3GAction);
+      auditReadSuccess(context.getAction());
       getMetrics().updateHeadBucketSuccessStats(startNanos);
       return Response.ok().build();
     } catch (OMException e) {
-      auditReadFailure(s3GAction, e);
+      auditReadFailure(context.getAction(), e);
       throw newError(bucketName, e);
     } catch (Exception e) {
-      auditReadFailure(s3GAction, e);
+      auditReadFailure(context.getAction(), e);
       throw e;
     }
   }
@@ -338,7 +337,7 @@ public MultiDeleteResponse multiDelete(
       @QueryParam(QueryParams.DELETE) String delete,
       MultiDeleteRequest request
   ) throws OS3Exception, IOException {
-    S3GAction s3GAction = S3GAction.MULTI_DELETE;
+    S3RequestContext context = new S3RequestContext(this, 
S3GAction.MULTI_DELETE);
 
     OzoneBucket bucket = getVolume().getBucket(bucketName);
     MultiDeleteResponse result = new MultiDeleteResponse();
@@ -349,7 +348,7 @@ public MultiDeleteResponse multiDelete(
       for (DeleteObject keyToDelete : request.getObjects()) {
         deleteKeys.add(keyToDelete.getKey());
       }
-      long startNanos = Time.monotonicNowNanos();
+      long startNanos = context.getStartNanos();
       try {
         S3Owner.verifyBucketOwnerCondition(getHeaders(), bucketName, 
bucket.getOwner());
         undeletedKeyResultMap = bucket.deleteKeys(deleteKeys, true);
@@ -377,7 +376,7 @@ public MultiDeleteResponse multiDelete(
       }
     }
 
-    AuditMessage.Builder message = auditMessageFor(s3GAction);
+    AuditMessage.Builder message = auditMessageFor(context.getAction());
     message.getParams().put("failedDeletes", deleteKeys.toString());
 
     if (!result.getErrors().isEmpty()) {
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 0d6fbe1657b..fd6ef9c3c0b 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
@@ -85,6 +85,7 @@
 import org.apache.hadoop.ozone.audit.AuditLogger.PerformanceStringBuilder;
 import org.apache.hadoop.ozone.audit.AuditLoggerType;
 import org.apache.hadoop.ozone.audit.AuditMessage;
+import org.apache.hadoop.ozone.audit.S3GAction;
 import org.apache.hadoop.ozone.client.OzoneBucket;
 import org.apache.hadoop.ozone.client.OzoneClient;
 import org.apache.hadoop.ozone.client.OzoneClientUtils;
@@ -104,9 +105,11 @@
 import org.apache.hadoop.ozone.s3.metrics.S3GatewayMetrics;
 import org.apache.hadoop.ozone.s3.signature.SignatureInfo;
 import org.apache.hadoop.ozone.s3.util.AuditUtils;
+import org.apache.hadoop.ozone.s3.util.S3GActionIamMapper;
 import org.apache.hadoop.ozone.s3.util.S3Utils;
 import org.apache.http.NameValuePair;
 import org.apache.http.client.utils.URLEncodedUtils;
+import org.apache.ratis.util.function.CheckedSupplier;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -225,6 +228,39 @@ protected void init() {
     // hook method
   }
 
+  /**
+   * Sets the IAM S3 action on thread-local {@link S3Auth} for fine-grained 
STS authorization.
+   * Called when the handler resolves the {@link S3GAction}.
+   */
+  protected void applyS3Action(S3GAction action) {
+    if (s3Auth != null) {
+      s3Auth.setS3Action(S3GActionIamMapper.toS3ActionString(action));
+    }
+  }
+
+  /**
+   * Temporarily override the S3 action string set on {@link S3Auth} for 
authorization.
+   * <p>
+   * This does not change S3G auditing (which is based on {@link S3GAction}).
+   * The action string is the IAM-style S3 action name without the {@code s3:} 
prefix (for example
+   * {@code GetObject}, {@code PutObject}, {@code GetObjectTagging}).
+   * This is used for special case APIs like CopyObject that don't have a 1-1 
s3 action mapping, but
+   * requires GetObject on the source file and PutObject on the destination 
file.
+   */
+  protected <T, E extends Exception> T runWithS3ActionString(String s3Action, 
CheckedSupplier<T, E> checkedSupplier)
+      throws E {
+    if (s3Auth == null) {
+      return checkedSupplier.get();
+    }
+    final String originalS3Action = s3Auth.getS3Action();
+    s3Auth.setS3Action(s3Action);
+    try {
+      return checkedSupplier.get();
+    } finally {
+      s3Auth.setS3Action(originalS3Action);
+    }
+  }
+
   protected OzoneVolume getVolume() throws IOException {
     return client.getObjectStore().getS3Volume();
   }
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java
index dd09bff5a41..bfa3c3d5c79 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java
@@ -535,8 +535,8 @@ static void addEntityTagHeader(ResponseBuilder 
responseBuilder, OzoneKey key) {
   public Response head(
       @PathParam(BUCKET) String bucketName,
       @PathParam(PATH) String keyPath) throws IOException, OS3Exception {
-    long startNanos = Time.monotonicNowNanos();
-    S3GAction s3GAction = S3GAction.HEAD_KEY;
+    ObjectRequestContext context = new 
ObjectRequestContext(S3GAction.HEAD_KEY, bucketName);
+    long startNanos = context.getStartNanos();
 
     OzoneKey key;
     try {
@@ -551,12 +551,12 @@ public Response head(
           getHeaders(), keyPath, key, 
S3ConditionalRequest.PreconditionContext.READ);
       if (conditionalResponse != null) {
         getMetrics().updateHeadKeySuccessStats(startNanos);
-        auditReadSuccess(s3GAction);
+        auditReadSuccess(context.getAction());
         return conditionalResponse;
       }
       // TODO: return the specified range bytes of this object.
     } catch (OMException ex) {
-      auditReadFailure(s3GAction, ex);
+      auditReadFailure(context.getAction(), ex);
       getMetrics().updateHeadKeyFailureStats(startNanos);
       if (ex.getResult() == ResultCodes.KEY_NOT_FOUND) {
         // Just return 404 with no content
@@ -571,7 +571,7 @@ public Response head(
         throw newError(bucketName, keyPath, ex);
       }
     } catch (Exception ex) {
-      auditReadFailure(s3GAction, ex);
+      auditReadFailure(context.getAction(), ex);
       throw ex;
     }
 
@@ -588,7 +588,7 @@ public Response head(
     addLastModifiedDate(response, key);
     addCustomMetadataHeaders(response, key);
     getMetrics().updateHeadKeySuccessStats(startNanos);
-    auditReadSuccess(s3GAction);
+    auditReadSuccess(context.getAction());
     return response.build();
   }
 
@@ -674,8 +674,8 @@ public Response initializeMultipartUpload(
       @PathParam(BUCKET) String bucket,
       @PathParam(PATH) String key
   ) throws IOException, OS3Exception {
-    long startNanos = Time.monotonicNowNanos();
-    S3GAction s3GAction = S3GAction.INIT_MULTIPART_UPLOAD;
+    ObjectRequestContext context = new 
ObjectRequestContext(S3GAction.INIT_MULTIPART_UPLOAD, bucket);
+    long startNanos = context.getStartNanos();
 
     try {
       OzoneBucket ozoneBucket = getVolume().getBucket(bucket);
@@ -698,16 +698,16 @@ public Response initializeMultipartUpload(
       multipartUploadInitiateResponse.setKey(key);
       multipartUploadInitiateResponse.setUploadID(multipartInfo.getUploadID());
 
-      auditWriteSuccess(s3GAction);
+      auditWriteSuccess(context.getAction());
       getMetrics().updateInitMultipartUploadSuccessStats(startNanos);
       return Response.status(Status.OK).entity(
           multipartUploadInitiateResponse).build();
     } catch (OMException ex) {
-      auditWriteFailure(s3GAction, ex);
+      auditWriteFailure(context.getAction(), ex);
       getMetrics().updateInitMultipartUploadFailureStats(startNanos);
       throw newError(bucket, key, ex);
     } catch (Exception ex) {
-      auditWriteFailure(s3GAction, ex);
+      auditWriteFailure(context.getAction(), ex);
       getMetrics().updateInitMultipartUploadFailureStats(startNanos);
       throw ex;
     }
@@ -723,9 +723,9 @@ public Response completeMultipartUpload(
       @PathParam(PATH) String key,
       CompleteMultipartUploadRequest multipartUploadRequest
   ) throws IOException, OS3Exception {
+    ObjectRequestContext context = new 
ObjectRequestContext(S3GAction.COMPLETE_MULTIPART_UPLOAD, bucket);
     final String uploadID = queryParams().get(QueryParams.UPLOAD_ID, "");
-    long startNanos = Time.monotonicNowNanos();
-    S3GAction s3GAction = S3GAction.COMPLETE_MULTIPART_UPLOAD;
+    long startNanos = context.getStartNanos();
     // Using LinkedHashMap to preserve ordering of parts list.
     Map<Integer, String> partsMap = new LinkedHashMap<>();
     List<CompleteMultipartUploadRequest.Part> partList =
@@ -765,12 +765,12 @@ public Response completeMultipartUpload(
           wrapInQuotes(omMultipartUploadCompleteInfo.getHash()));
       // Location also setting as bucket name.
       completeMultipartUploadResponse.setLocation(bucket);
-      auditWriteSuccess(s3GAction);
+      auditWriteSuccess(context.getAction());
       getMetrics().updateCompleteMultipartUploadSuccessStats(startNanos);
       return Response.status(Status.OK).entity(completeMultipartUploadResponse)
           .build();
     } catch (OMException ex) {
-      auditWriteFailure(s3GAction, ex);
+      auditWriteFailure(context.getAction(), ex);
       getMetrics().updateCompleteMultipartUploadFailureStats(startNanos);
       if (ex.getResult() == ResultCodes.NO_SUCH_MULTIPART_UPLOAD_ERROR) {
         throw newError(NO_SUCH_UPLOAD, uploadID, ex);
@@ -798,7 +798,7 @@ public Response completeMultipartUpload(
       }
       throw newError(bucket, key, ex);
     } catch (Exception ex) {
-      auditWriteFailure(s3GAction, ex);
+      auditWriteFailure(context.getAction(), ex);
       getMetrics().updateCompleteMultipartUploadFailureStats(startNanos);
       throw ex;
     }
@@ -839,8 +839,8 @@ private Response createMultipartKey(OzoneVolume volume, 
OzoneBucket ozoneBucket,
                 uploadID, getChunkSize(), multiDigestInputStream, perf, 
getHeaders());
       }
       // OmMultipartCommitUploadPartInfo can only be gotten after the
-      // OzoneOutputStream is closed, so we need to save the OzoneOutputStream
-      final OzoneOutputStream outputStream;
+      // OzoneOutputStream is closed, so we need to get and save the commit 
info.
+      final OmMultipartCommitUploadPartInfo omMultipartCommitUploadPartInfo;
       long metadataLatencyNs;
       if (copyHeader != null) {
         Pair<String, String> result = parseSourceHeader(copyHeader);
@@ -852,8 +852,8 @@ private Response createMultipartKey(OzoneVolume volume, 
OzoneBucket ozoneBucket,
               ozoneBucket.getOwner());
         }
 
-        OzoneKeyDetails sourceKeyDetails = getClientProtocol().getKeyDetails(
-            volume.getName(), sourceBucket, sourceKey);
+        final OzoneKeyDetails sourceKeyDetails = runWithS3ActionString(
+            "GetObject", () -> 
getClientProtocol().getKeyDetails(volume.getName(), sourceBucket, sourceKey));
         String range =
             getHeaders().getHeaderString(COPY_SOURCE_HEADER_RANGE);
         RangeHeader rangeHeader = null;
@@ -879,7 +879,8 @@ private Response createMultipartKey(OzoneVolume volume, 
OzoneBucket ozoneBucket,
         }
 
         try (OzoneInputStream sourceObject = sourceKeyDetails.getContent()) {
-          long copyLength;
+          final long[] copyLengthHolder = new long[1];
+          final long[] metadataLatencyHolder = new long[1];
           if (range != null) {
             final long skipped =
                 sourceObject.skip(rangeHeader.getStartOffset());
@@ -889,52 +890,60 @@ private Response createMultipartKey(OzoneVolume volume, 
OzoneBucket ozoneBucket,
                       + rangeHeader.getStartOffset() + " actual: " + skipped);
             }
           }
-          try (OzoneOutputStream ozoneOutputStream = getClientProtocol()
-              .createMultipartKey(volume.getName(), bucketName, key, length,
-                  partNumber, uploadID)) {
-            metadataLatencyNs =
-                getMetrics().updateCopyKeyMetadataStats(startNanos);
-            copyLength = IOUtils.copyLarge(sourceObject, ozoneOutputStream, 0, 
length,
-                new byte[getIOBufferSize(length)]);
-            ozoneOutputStream.getMetadata()
-                .putAll(sourceKeyDetails.getMetadata());
-            String raw = ozoneOutputStream.getMetadata().get(OzoneConsts.ETAG);
-            if (raw != null) {
-              ozoneOutputStream.getMetadata().put(OzoneConsts.ETAG, 
stripQuotes(raw));
+          final long finalLength = length;
+          final long bytesToCopy = length;
+          omMultipartCommitUploadPartInfo = runWithS3ActionString("PutObject", 
() -> {
+            final OzoneOutputStream ozoneOutputStream = 
getClientProtocol().createMultipartKey(
+                volume.getName(), bucketName, key, finalLength, partNumber, 
uploadID);
+            try (OzoneOutputStream ignored = ozoneOutputStream) {
+              metadataLatencyHolder[0] = 
getMetrics().updateCopyKeyMetadataStats(startNanos);
+              copyLengthHolder[0] = IOUtils.copyLarge(
+                  sourceObject, ozoneOutputStream, 0, bytesToCopy, new 
byte[getIOBufferSize(bytesToCopy)]);
+              ozoneOutputStream.getMetadata()
+                  .putAll(sourceKeyDetails.getMetadata());
+              final String raw = 
ozoneOutputStream.getMetadata().get(OzoneConsts.ETAG);
+              if (raw != null) {
+                ozoneOutputStream.getMetadata().put(OzoneConsts.ETAG, 
stripQuotes(raw));
+              }
             }
-            outputStream = ozoneOutputStream;
-          }
-          getMetrics().incCopyObjectSuccessLength(copyLength);
-          perf.appendSizeBytes(copyLength);
+            return ozoneOutputStream.getCommitUploadPartInfo();
+          });
+          metadataLatencyNs = metadataLatencyHolder[0];
+          getMetrics().incCopyObjectSuccessLength(copyLengthHolder[0]);
+          perf.appendSizeBytes(copyLengthHolder[0]);
         }
       } else {
-        long putLength;
-        try (OzoneOutputStream ozoneOutputStream = getClientProtocol()
+        final long putLength;
+        // We don't need runWithS3ActionString("PutObject"...) here because 
the action in this else branch is
+        // S3GAction.CREATE_MULTIPART_KEY and this has a mapping in 
S3GActionIamMapper to "PutObject", so it's covered.
+        // In the if branch of the code, the request action is set to 
S3GAction.CREATE_MULTIPART_KEY_BY_COPY which is
+        // mapped to null in S3GActionIamMapper (by design, since it needs 
"GetObject" on the source and "PutObject"
+        // on the destination).
+        final OzoneOutputStream ozoneOutputStream = getClientProtocol()
             .createMultipartKey(volume.getName(), bucketName, key, length,
-                partNumber, uploadID)) {
+                partNumber, uploadID);
+        try (OzoneOutputStream ignored = ozoneOutputStream) {
           metadataLatencyNs =
               getMetrics().updatePutKeyMetadataStats(startNanos);
           putLength = IOUtils.copyLarge(multiDigestInputStream, 
ozoneOutputStream, 0, length,
               new byte[getIOBufferSize(length)]);
-          byte[] digest = 
multiDigestInputStream.getMessageDigest(OzoneConsts.MD5_HASH).digest();
-          String md5Hash = 
DatatypeConverter.printHexBinary(digest).toLowerCase();
-          String clientContentMD5 = 
getHeaders().getHeaderString(S3Consts.CHECKSUM_HEADER);
+          final byte[] digest = 
multiDigestInputStream.getMessageDigest(OzoneConsts.MD5_HASH).digest();
+          final String md5Hash = 
DatatypeConverter.printHexBinary(digest).toLowerCase();
+          final String clientContentMD5 = 
getHeaders().getHeaderString(S3Consts.CHECKSUM_HEADER);
           if (clientContentMD5 != null) {
-            CheckedRunnable<IOException> checkContentMD5Hook = () -> {
+            final CheckedRunnable<IOException> checkContentMD5Hook = () -> {
               S3Utils.validateContentMD5(clientContentMD5, md5Hash, key);
             };
             
ozoneOutputStream.getKeyOutputStream().setPreCommits(Collections.singletonList(checkContentMD5Hook));
           }
           ozoneOutputStream.getMetadata().put(OzoneConsts.ETAG, md5Hash);
-          outputStream = ozoneOutputStream;
         }
+        omMultipartCommitUploadPartInfo = 
ozoneOutputStream.getCommitUploadPartInfo();
         getMetrics().incPutKeySuccessLength(putLength);
         perf.appendSizeBytes(putLength);
       }
       perf.appendMetaLatencyNanos(metadataLatencyNs);
 
-      OmMultipartCommitUploadPartInfo omMultipartCommitUploadPartInfo =
-          outputStream.getCommitUploadPartInfo();
       String eTag = omMultipartCommitUploadPartInfo.getETag();
       // If the OmMultipartCommitUploadPartInfo does not contain eTag,
       // fall back to MPU part name for compatibility in case the (old) OM
@@ -1003,7 +1012,7 @@ srcKeyLen > getDatastreamMinLength()) {
             getMetrics().updateCopyKeyMetadataStats(startNanos);
         perf.appendMetaLatencyNanos(metadataLatencyNs);
         copyLength = IOUtils.copyLarge(src, dest, 0, srcKeyLen, new 
byte[getIOBufferSize(srcKeyLen)]);
-        String md5Hash = 
DatatypeConverter.printHexBinary(src.getMessageDigest().digest()).toLowerCase();
+        final String md5Hash = 
DatatypeConverter.printHexBinary(src.getMessageDigest().digest()).toLowerCase();
         dest.getMetadata().put(OzoneConsts.ETAG, md5Hash);
       }
     }
@@ -1024,7 +1033,7 @@ private CopyObjectResponse copyObject(OzoneVolume volume,
 
     String sourceBucket = result.getLeft();
     String sourceKey = result.getRight();
-    DigestInputStream sourceDigestInputStream = null;
+    final MessageDigest md5Digest = getMD5DigestInstance();
 
     if (S3Owner.hasBucketOwnershipVerificationConditions(getHeaders())) {
       String sourceBucketOwner = volume.getBucket(sourceBucket).getOwner();
@@ -1032,8 +1041,8 @@ private CopyObjectResponse copyObject(OzoneVolume volume,
       S3Owner.verifyBucketOwnerConditionOnCopyOperation(getHeaders(), 
sourceBucket, sourceBucketOwner, null, null);
     }
     try {
-      OzoneKeyDetails sourceKeyDetails = getClientProtocol().getKeyDetails(
-          volume.getName(), sourceBucket, sourceKey);
+      final OzoneKeyDetails sourceKeyDetails = runWithS3ActionString(
+          "GetObject", () -> 
getClientProtocol().getKeyDetails(volume.getName(), sourceBucket, sourceKey));
       // Checking whether we trying to copying to it self.
       if (sourceBucket.equals(destBucket) && sourceKey
           .equals(destkey)) {
@@ -1103,22 +1112,25 @@ private CopyObjectResponse copyObject(OzoneVolume 
volume,
         throw ex;
       }
 
-      try (OzoneInputStream src = getClientProtocol().getKey(volume.getName(),
-          sourceBucket, sourceKey)) {
+      try (OzoneInputStream src = runWithS3ActionString(
+              "GetObject", () -> getClientProtocol().getKey(volume.getName(), 
sourceBucket, sourceKey));
+           DigestInputStream sourceDigestInputStream = new 
DigestInputStream(src, md5Digest)) {
         getMetrics().updateCopyKeyMetadataStats(startNanos);
-        sourceDigestInputStream = new DigestInputStream(src, 
getMD5DigestInstance());
-        copy(volume, sourceDigestInputStream, sourceKeyLen, destkey, 
destBucket, replicationConfig,
-                customMetadata, perf, startNanos, tags, writeConditions);
-      }
+        runWithS3ActionString("PutObject", () -> {
+          copy(volume, sourceDigestInputStream, sourceKeyLen, destkey, 
destBucket,
+              replicationConfig, customMetadata, perf, startNanos, tags, 
writeConditions);
+          return null;
+        });
 
-      final OzoneKeyDetails destKeyDetails = getClientProtocol().getKeyDetails(
-          volume.getName(), destBucket, destkey);
+        final OzoneKeyDetails destKeyDetails = 
getClientProtocol().getKeyDetails(
+            volume.getName(), destBucket, destkey);
 
-      getMetrics().updateCopyObjectSuccessStats(startNanos);
-      CopyObjectResponse copyObjectResponse = new CopyObjectResponse();
-      
copyObjectResponse.setETag(wrapInQuotes(destKeyDetails.getMetadata().get(OzoneConsts.ETAG)));
-      copyObjectResponse.setLastModified(destKeyDetails.getModificationTime());
-      return copyObjectResponse;
+        getMetrics().updateCopyObjectSuccessStats(startNanos);
+        CopyObjectResponse copyObjectResponse = new CopyObjectResponse();
+        
copyObjectResponse.setETag(wrapInQuotes(destKeyDetails.getMetadata().get(OzoneConsts.ETAG)));
+        
copyObjectResponse.setLastModified(destKeyDetails.getModificationTime());
+        return copyObjectResponse;
+      }
     } catch (OMException ex) {
       if (ex.getResult() == ResultCodes.KEY_NOT_FOUND) {
         if (getHeaders().getHeaderString(S3Consts.IF_MATCH_HEADER) != null) {
@@ -1137,9 +1149,7 @@ private CopyObjectResponse copyObject(OzoneVolume volume,
     } finally {
       // Reset the thread-local message digest instance in case of exception
       // and MessageDigest#digest is never called
-      if (sourceDigestInputStream != null) {
-        sourceDigestInputStream.getMessageDigest().reset();
-      }
+      md5Digest.reset();
     }
   }
 
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/RootEndpoint.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/RootEndpoint.java
index 9e638a112a7..8fdb80d9a48 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/RootEndpoint.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/RootEndpoint.java
@@ -26,7 +26,6 @@
 import org.apache.hadoop.ozone.client.OzoneBucket;
 import org.apache.hadoop.ozone.s3.commontypes.BucketMetadata;
 import org.apache.hadoop.ozone.s3.exception.OS3Exception;
-import org.apache.hadoop.util.Time;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -48,7 +47,8 @@ public class RootEndpoint extends EndpointBase {
   @GET
   public Response get()
       throws OS3Exception, IOException {
-    long startNanos = Time.monotonicNowNanos();
+    S3RequestContext context = new S3RequestContext(this, 
S3GAction.LIST_S3_BUCKETS);
+    long startNanos = context.getStartNanos();
     boolean auditSuccess = true;
     try {
       ListBucketResponse response = new ListBucketResponse();
@@ -73,11 +73,11 @@ public Response get()
       return Response.ok(response).build();
     } catch (Exception ex) {
       auditSuccess = false;
-      auditReadFailure(S3GAction.LIST_S3_BUCKETS, ex);
+      auditReadFailure(context.getAction(), ex);
       throw ex;
     } finally {
       if (auditSuccess) {
-        auditReadSuccess(S3GAction.LIST_S3_BUCKETS);
+        auditReadSuccess(context.getAction());
       }
     }
   }
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3RequestContext.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3RequestContext.java
index 4130feaf6fd..ebcb773cef4 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3RequestContext.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3RequestContext.java
@@ -36,6 +36,7 @@ class S3RequestContext {
     this.startNanos = Time.monotonicNowNanos();
     this.perf = new PerformanceStringBuilder();
     this.action = action;
+    endpoint.applyS3Action(action);
   }
 
   long getStartNanos() {
@@ -59,6 +60,7 @@ S3GAction getAction() {
 
   void setAction(S3GAction action) {
     this.action = action;
+    endpoint.applyS3Action(action);
   }
 
   /**
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
new file mode 100644
index 00000000000..9953ebe2020
--- /dev/null
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3GActionIamMapper.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.s3.util;
+
+import jakarta.annotation.Nullable;
+import org.apache.hadoop.ozone.audit.S3GAction;
+
+/**
+ * Maps S3 Gateway operations to AWS IAM S3 action names. Values align with
+ * {@code org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver} 
so STS session
+ * policies and Ranger policy conditions use the same vocabulary.
+ */
+public final class S3GActionIamMapper {
+
+  private S3GActionIamMapper() {
+  }
+
+  /**
+   * @return S3 action string, or null if not applicable to IAM S3
+   */
+  public static @Nullable String toS3ActionString(@Nullable S3GAction action) {
+    if (action == null) {
+      return null;
+    }
+    switch (action) {
+    case GET_BUCKET:
+    case HEAD_BUCKET:
+      return "ListBucket";
+    case CREATE_BUCKET:
+      return "CreateBucket";
+    case DELETE_BUCKET:
+      return "DeleteBucket";
+    case GET_ACL:
+      return "GetBucketAcl";
+    case PUT_ACL:
+      return "PutBucketAcl";
+    case LIST_MULTIPART_UPLOAD:
+      return "ListBucketMultipartUploads";
+    case MULTI_DELETE:
+    case DELETE_KEY:
+      return "DeleteObject";
+    case LIST_S3_BUCKETS:
+      return "ListAllMyBuckets";
+    case CREATE_MULTIPART_KEY:
+    case CREATE_KEY:
+    case INIT_MULTIPART_UPLOAD:
+    case COMPLETE_MULTIPART_UPLOAD:
+    case CREATE_DIRECTORY:
+      return "PutObject";
+    case LIST_PARTS:
+      return "ListMultipartUploadParts";
+    case GET_KEY:
+    case HEAD_KEY:
+      return "GetObject";
+    case ABORT_MULTIPART_UPLOAD:
+      return "AbortMultipartUpload";
+    case GET_OBJECT_TAGGING:
+      return "GetObjectTagging";
+    case PUT_OBJECT_TAGGING:
+      return "PutObjectTagging";
+    case DELETE_OBJECT_TAGGING:
+      return "DeleteObjectTagging";
+    case PUT_OBJECT_ACL:
+      return "PutObjectAcl";
+    case COPY_OBJECT:
+    case CREATE_MULTIPART_KEY_BY_COPY:
+      // CopyObject / UploadPartCopy require distinct source (GetObject) and 
destination (PutObject)
+      // authorization. The endpoint code explicitly sets the IAM action 
string for each phase.
+      return null;
+    case GENERATE_SECRET:
+    case REVOKE_SECRET:
+    case ASSUME_ROLE:
+    default:
+      return null;
+    }
+  }
+}
diff --git 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestCopyActionsAudit.java
 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestCopyActionsAudit.java
new file mode 100644
index 00000000000..380a7780e9e
--- /dev/null
+++ 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestCopyActionsAudit.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.s3.endpoint;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.hadoop.ozone.s3.util.S3Consts.COPY_SOURCE_HEADER;
+import static org.apache.hadoop.ozone.s3.util.S3Consts.STORAGE_CLASS_HEADER;
+import static org.apache.hadoop.ozone.s3.util.S3Consts.X_AMZ_CONTENT_SHA256;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayInputStream;
+import java.io.OutputStream;
+import java.util.HashMap;
+import javax.ws.rs.core.HttpHeaders;
+import org.apache.hadoop.hdds.client.ReplicationConfig;
+import org.apache.hadoop.hdds.client.ReplicationFactor;
+import org.apache.hadoop.hdds.client.ReplicationType;
+import org.apache.hadoop.ozone.OzoneConsts;
+import org.apache.hadoop.ozone.audit.AuditLogger.PerformanceStringBuilder;
+import org.apache.hadoop.ozone.audit.S3GAction;
+import org.apache.hadoop.ozone.client.OzoneBucket;
+import org.apache.hadoop.ozone.client.OzoneClient;
+import org.apache.hadoop.ozone.client.OzoneClientStub;
+import org.apache.hadoop.ozone.s3.endpoint.ObjectEndpoint.ObjectRequestContext;
+import org.apache.hadoop.ozone.s3.util.S3Consts;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Verifies audit logging action for copy operations even if S3 action 
authorization strings are overridden internally.
+ * For example, S3G.COPY_OBJECT must use S3G.COPY_OBJECT as the audit action, 
even though internally the S3 actions
+ * checked are GetObject and PutObject.
+ */
+public class TestCopyActionsAudit {
+
+  @Test
+  public void testCopyObjectAuditActionRemainsCopyObject() throws Exception {
+    final String bucketName = OzoneConsts.S3_BUCKET;
+    final String srcKey = "src.txt";
+    final String destKey = "dest.txt";
+
+    final OzoneClient client = new OzoneClientStub();
+    client.getObjectStore().createS3Bucket(bucketName);
+    final OzoneBucket bucket = client.getObjectStore().getS3Bucket(bucketName);
+
+    try (OutputStream out = bucket.createKey(
+        srcKey, 3, ReplicationConfig.fromTypeAndFactor(ReplicationType.RATIS, 
ReplicationFactor.ONE),
+        new HashMap<>())) {
+      out.write("src".getBytes(UTF_8));
+    }
+
+    final HttpHeaders headers = mock(HttpHeaders.class);
+    when(headers.getHeaderString(STORAGE_CLASS_HEADER)).thenReturn("STANDARD");
+    
when(headers.getHeaderString(X_AMZ_CONTENT_SHA256)).thenReturn("mockSignature");
+    when(headers.getHeaderString(COPY_SOURCE_HEADER)).thenReturn(bucketName + 
"/" + srcKey);
+    when(headers.getHeaderString(HttpHeaders.CONTENT_LENGTH)).thenReturn("0");
+
+    final ObjectEndpoint endpoint = newEndpoint(client, headers);
+    final AuditingObjectOperationHandler auditing = spy(new 
AuditingObjectOperationHandler(endpoint));
+
+    final ObjectRequestContext requestContext = endpoint.new 
ObjectRequestContext(S3GAction.CREATE_KEY, bucketName);
+
+    auditing.handlePutRequest(requestContext, destKey, new 
ByteArrayInputStream(new byte[0]));
+
+    verify(auditing).auditWriteSuccess(eq(S3GAction.COPY_OBJECT), 
any(PerformanceStringBuilder.class));
+  }
+
+  @Test
+  public void testUploadPartCopyAuditActionRemainsCreateMultipartKeyByCopy() 
throws Exception {
+    final String bucketName = OzoneConsts.S3_BUCKET;
+    final String srcKey = "src-part.txt";
+    final String destKey = "dest-mpu.txt";
+
+    final OzoneClient client = new OzoneClientStub();
+    client.getObjectStore().createS3Bucket(bucketName);
+    final OzoneBucket bucket = client.getObjectStore().getS3Bucket(bucketName);
+
+    try (OutputStream out = bucket.createKey(
+        srcKey, 4, ReplicationConfig.fromTypeAndFactor(ReplicationType.RATIS, 
ReplicationFactor.ONE),
+        new HashMap<>())) {
+      out.write("part".getBytes(UTF_8));
+    }
+
+    final HttpHeaders headers = mock(HttpHeaders.class);
+    when(headers.getHeaderString(STORAGE_CLASS_HEADER)).thenReturn("STANDARD");
+    
when(headers.getHeaderString(X_AMZ_CONTENT_SHA256)).thenReturn("mockSignature");
+    when(headers.getHeaderString(COPY_SOURCE_HEADER)).thenReturn(bucketName + 
"/" + srcKey);
+    when(headers.getHeaderString(HttpHeaders.CONTENT_LENGTH)).thenReturn("0");
+
+    final ObjectEndpoint endpoint = newEndpoint(client, headers);
+
+    final String uploadId = 
EndpointTestUtils.initiateMultipartUpload(endpoint, bucketName, destKey);
+    assertNotNull(uploadId);
+
+    endpoint.queryParamsForTest().set(S3Consts.QueryParams.UPLOAD_ID, 
uploadId);
+    endpoint.queryParamsForTest().setInt(S3Consts.QueryParams.PART_NUMBER, 1);
+
+    final AuditingObjectOperationHandler auditing = spy(new 
AuditingObjectOperationHandler(endpoint));
+    final ObjectRequestContext requestContext = endpoint.new 
ObjectRequestContext(S3GAction.CREATE_KEY, bucketName);
+
+    auditing.handlePutRequest(requestContext, destKey, new 
ByteArrayInputStream(new byte[0]));
+
+    
verify(auditing).auditWriteSuccess(eq(S3GAction.CREATE_MULTIPART_KEY_BY_COPY), 
any(PerformanceStringBuilder.class));
+  }
+
+  private static ObjectEndpoint newEndpoint(OzoneClient client, HttpHeaders 
headers) {
+    return EndpointBuilder.newObjectEndpointBuilder()
+        .setClient(client)
+        .setHeaders(headers)
+        .build();
+  }
+}
+
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
new file mode 100644
index 00000000000..c7ae9e4e924
--- /dev/null
+++ 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestS3GActionIamMapper.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.s3.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.apache.hadoop.ozone.audit.S3GAction;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for {@link S3GActionIamMapper}. */
+public class TestS3GActionIamMapper {
+
+  @Test
+  public void mapsCoreObjectActions() {
+    assertEquals("ListBucket", 
S3GActionIamMapper.toS3ActionString(S3GAction.GET_BUCKET));
+    assertEquals("ListBucket", 
S3GActionIamMapper.toS3ActionString(S3GAction.HEAD_BUCKET));
+    assertEquals("CreateBucket", 
S3GActionIamMapper.toS3ActionString(S3GAction.CREATE_BUCKET));
+    assertEquals("DeleteBucket", 
S3GActionIamMapper.toS3ActionString(S3GAction.DELETE_BUCKET));
+    assertEquals("GetBucketAcl", 
S3GActionIamMapper.toS3ActionString(S3GAction.GET_ACL));
+    assertEquals("PutBucketAcl", 
S3GActionIamMapper.toS3ActionString(S3GAction.PUT_ACL));
+    assertEquals("ListBucketMultipartUploads", 
S3GActionIamMapper.toS3ActionString(S3GAction.LIST_MULTIPART_UPLOAD));
+    assertEquals("DeleteObject", 
S3GActionIamMapper.toS3ActionString(S3GAction.MULTI_DELETE));
+    assertEquals("DeleteObject", 
S3GActionIamMapper.toS3ActionString(S3GAction.DELETE_KEY));
+    assertEquals("ListAllMyBuckets", 
S3GActionIamMapper.toS3ActionString(S3GAction.LIST_S3_BUCKETS));
+    assertEquals("PutObject", 
S3GActionIamMapper.toS3ActionString(S3GAction.CREATE_MULTIPART_KEY));
+    assertEquals("PutObject", 
S3GActionIamMapper.toS3ActionString(S3GAction.CREATE_KEY));
+    assertEquals("PutObject", 
S3GActionIamMapper.toS3ActionString(S3GAction.INIT_MULTIPART_UPLOAD));
+    assertEquals("PutObject", 
S3GActionIamMapper.toS3ActionString(S3GAction.COMPLETE_MULTIPART_UPLOAD));
+    assertEquals("PutObject", 
S3GActionIamMapper.toS3ActionString(S3GAction.CREATE_DIRECTORY));
+    assertEquals("ListMultipartUploadParts", 
S3GActionIamMapper.toS3ActionString(S3GAction.LIST_PARTS));
+    assertEquals("GetObject", 
S3GActionIamMapper.toS3ActionString(S3GAction.GET_KEY));
+    assertEquals("GetObject", 
S3GActionIamMapper.toS3ActionString(S3GAction.HEAD_KEY));
+    assertEquals("AbortMultipartUpload", 
S3GActionIamMapper.toS3ActionString(S3GAction.ABORT_MULTIPART_UPLOAD));
+    assertEquals("GetObjectTagging", 
S3GActionIamMapper.toS3ActionString(S3GAction.GET_OBJECT_TAGGING));
+    assertEquals("PutObjectTagging", 
S3GActionIamMapper.toS3ActionString(S3GAction.PUT_OBJECT_TAGGING));
+    assertEquals("DeleteObjectTagging", 
S3GActionIamMapper.toS3ActionString(S3GAction.DELETE_OBJECT_TAGGING));
+    assertEquals("PutObjectAcl", 
S3GActionIamMapper.toS3ActionString(S3GAction.PUT_OBJECT_ACL));
+  }
+
+  @Test
+  public void copyActionsReturnNull() {
+    assertNull(S3GActionIamMapper.toS3ActionString(S3GAction.COPY_OBJECT));
+    
assertNull(S3GActionIamMapper.toS3ActionString(S3GAction.CREATE_MULTIPART_KEY_BY_COPY));
+  }
+
+  @Test
+  public void nonIamActionsReturnNull() {
+    assertNull(S3GActionIamMapper.toS3ActionString(S3GAction.ASSUME_ROLE));
+    assertNull(S3GActionIamMapper.toS3ActionString(S3GAction.GENERATE_SECRET));
+    assertNull(S3GActionIamMapper.toS3ActionString(S3GAction.REVOKE_SECRET));
+  }
+}
diff --git 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/package-info.java
 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/package-info.java
new file mode 100644
index 00000000000..4b7b37a574a
--- /dev/null
+++ 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Unit tests for s3 utilities.
+ */
+package org.apache.hadoop.ozone.s3.util;


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

Reply via email to