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

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

commit ff6f7e0c5ec6249a7cfade22a651ca66c6e0b0bd
Merge: 2b5240bf92f a46d391d915
Author: Fabian Morgan <[email protected]>
AuthorDate: Tue Aug 18 18:54:44 2026 -0700

    Merge remote-tracking branch 'upstream/master' into HDDS-13323-sts
    
     Conflicts:
            
hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java

 .../hdds/scm/storage/StreamBlockInputStream.java   |  35 +++-
 .../scm/storage/TestStreamBlockInputStream.java    | 114 ++++++++++++
 .../ozone/container/keyvalue/KeyValueHandler.java  |  11 ++
 .../container/keyvalue/TestKeyValueHandler.java    | 176 ++++++++++++++++++
 .../apache/hadoop/ozone/client/OzoneBucket.java    |  73 ++++++++
 .../ozone/client/io/OzoneDataStreamOutput.java     |   8 +
 .../hadoop/ozone/client/io/OzoneOutputStream.java  |  10 +
 .../ozone/client/protocol/ClientProtocol.java      |  76 ++++++++
 .../apache/hadoop/ozone/client/rpc/RpcClient.java  | 112 ++++++++++-
 hadoop-ozone/common/pom.xml                        |   4 +
 .../hadoop/ozone/om}/AWSV4AuthValidator.java       |  35 +---
 .../apache/hadoop/ozone/om/helpers/OmKeyArgs.java  |  13 ++
 .../hadoop/ozone/om/helpers/OpenKeySession.java    |  10 +
 ...OzoneManagerProtocolClientSideTranslatorPB.java |   7 +-
 hadoop-ozone/dist/src/main/license/bin/LICENSE.txt |   1 +
 hadoop-ozone/dist/src/main/license/jar-report.txt  |   1 +
 .../ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java   |   2 +-
 .../ozone/om/TestKeyLookupAclNormalization.java    | 206 +++++++++++++++++++++
 .../src/main/proto/OmClientProtocol.proto          |   2 +
 hadoop-ozone/ozone-manager/pom.xml                 |   4 -
 .../apache/hadoop/ozone/om/OmMetadataReader.java   | 102 ++++++----
 .../org/apache/hadoop/ozone/om/OzoneManager.java   |   5 +
 .../ozone/om/request/key/OMKeyCreateRequest.java   |  40 +++-
 .../om/request/key/OMKeyCreateRequestWithFSO.java  |   8 +-
 .../OzoneDelegationTokenSecretManager.java         |   1 +
 .../hadoop/ozone/security/S3SecurityUtil.java      |   3 +-
 .../hadoop/ozone/om/TestOMMetadataReader.java      |  42 +++++
 .../om/request/key/TestOMKeyCreateRequest.java     | 105 +++++++++++
 .../ozone/security/TestAWSV4AuthValidator.java     |   1 +
 .../hadoop/ozone/security/TestS3SecurityUtil.java  |   1 +
 hadoop-ozone/s3gateway/pom.xml                     |   9 +
 .../java/org/apache/hadoop/ozone/s3/Gateway.java   |   8 +
 .../org/apache/hadoop/ozone/s3/TestGateway.java    |  81 ++++++++
 33 files changed, 1216 insertions(+), 90 deletions(-)

diff --cc 
hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmKeyArgs.java
index 3d29637d973,5c55d7b9b2e..03ff4bd8f61
--- 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmKeyArgs.java
+++ 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/OmKeyArgs.java
@@@ -61,10 -61,8 +61,11 @@@ public final class OmKeyArgs extends Wi
    // This allows a key to be created an committed atomically if the original 
has not
    // been modified.
    private Long expectedDataGeneration = null;
 +  // Original S3/list prefix when keyName is empty (root listing). Used for 
STS
 +  // auth to check LIST on this prefix instead of "*".
 +  private final String listPrefix;
    private final String expectedETag;
+   private final boolean derivedKeyPiggyBacking;
  
    private OmKeyArgs(Builder b) {
      super(b);
@@@ -86,8 -84,8 +87,9 @@@
      this.ownerName = b.ownerName;
      this.tags = b.tags.build();
      this.expectedDataGeneration = b.expectedDataGeneration;
 +    this.listPrefix = b.listPrefix;
      this.expectedETag = b.expectedETag;
+     this.derivedKeyPiggyBacking = b.derivedKeyPiggyBacking;
    }
  
    public boolean getIsMultipartKey() {
@@@ -255,8 -249,8 +261,9 @@@
      private boolean forceUpdateContainerCacheFromSCM;
      private final MapBuilder<String, String> tags;
      private Long expectedDataGeneration = null;
 +    private String listPrefix = null;
      private String expectedETag;
+     private boolean derivedKeyPiggyBacking;
  
      public Builder() {
        this(AclListBuilder.empty());
@@@ -303,9 -297,9 +310,10 @@@
            obj.forceUpdateContainerCacheFromSCM;
        this.expectedDataGeneration = obj.expectedDataGeneration;
        this.expectedETag = obj.expectedETag;
+       this.derivedKeyPiggyBacking = obj.derivedKeyPiggyBacking;
        this.tags = MapBuilder.of(obj.tags);
        this.acls = AclListBuilder.of(obj.acls);
 +      this.listPrefix = obj.listPrefix;
      }
  
      public Builder setVolumeName(String volume) {
diff --cc 
hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
index a37994e299d,020fb1a3922..434d05132bf
--- 
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
@@@ -57,8 -56,7 +57,9 @@@ import org.apache.hadoop.ozone.om.helpe
  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.om.request.OMClientRequest;
 +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;
  import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLType;
@@@ -238,32 -237,8 +240,7 @@@ public class OmMetadataReader implement
  
      try {
        if (isAclEnabled) {
-         if (isStsS3Request()) {
-           // 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();
-           final String aclKey;
-           if (StringUtils.isNotBlank(listPrefix)) {
-             if (StringUtils.isBlank(keyName)) {
-               aclKey = listPrefix;
-             } else if (isStsListPathUnderRequestPrefix(keyName, listPrefix)) {
-               aclKey = listPrefix;
-             } else {
-               throw new OMException(
-                   "STS listStatus: key path: " + keyName + " does not match 
authorized list prefix: " + listPrefix,
-                   ResultCodes.PERMISSION_DENIED);
-             }
-           } else if (keyName != null && !keyName.isEmpty()) {
-             aclKey = keyName;
-           } else {
-             aclKey = "*";
-           }
-           checkAcls(ResourceType.KEY, StoreType.OZONE, ACLType.READ, 
bucket.realVolume(), bucket.realBucket(), aclKey);
-         } else {
-           checkAcls(getResourceType(args), StoreType.OZONE, ACLType.READ,
-               bucket, args.getKeyName());
-         }
 -        checkAcls(getResourceType(args), StoreType.OZONE, ACLType.READ,
 -            bucket, args.getKeyName());
++        checkListStatusAcls(args, bucket);
        }
        metrics.incNumListStatus();
        return keyManager.listStatus(args, recursive, startKey,
@@@ -299,8 -274,8 +276,7 @@@
  
      try {
        if (isAclEnabled) {
--        checkAcls(getResourceType(resolvedArgs), StoreType.OZONE, 
ACLType.READ,
--            bucket, resolvedArgs.getKeyName());
++        checkListStatusAcls(resolvedArgs, bucket);
        }
        metrics.incNumListStatus();
        List<OzoneFileStatus> ozoneFileStatuses = keyManager.listStatus(
@@@ -811,29 -756,6 +813,59 @@@
      return accessAuthorizer.isNative();
    }
  
 +  private boolean isStsS3Request() {
 +    return getS3Auth() != null && OzoneManager.getStsTokenIdentifier() != 
null;
 +  }
 +
++  private void checkListStatusAcls(OmKeyArgs args, ResolvedBucket bucket) 
throws IOException {
++    if (isStsS3Request()) {
++      checkAcls(
++          ResourceType.KEY, StoreType.OZONE, ACLType.READ, 
bucket.realVolume(), bucket.realBucket(),
++          getStsListStatusAclKey(args));
++      return;
++    }
++
++    checkAcls(getResourceType(args), StoreType.OZONE, ACLType.READ, bucket, 
args.getKeyName());
++  }
++
++  private static String getStsListStatusAclKey(OmKeyArgs args) throws 
OMException {
++    // 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();
++    if (StringUtils.isNotBlank(listPrefix)) {
++      if (StringUtils.isBlank(keyName) || 
isStsListPathUnderRequestPrefix(keyName, listPrefix)) {
++        return listPrefix;
++      }
++      throw new OMException(
++          "STS listStatus: key path: " + keyName + " does not match 
authorized list prefix: " + listPrefix,
++          ResultCodes.PERMISSION_DENIED);
++    }
++    if (StringUtils.isNotEmpty(keyName)) {
++      return keyName;
++    }
++    return "*";
++  }
++
 +  /**
 +   * For STS, {@code listPrefix} is the original S3 ListObjects prefix. 
Internal FSO listing may call
 +   * {@code listStatus} with {@code keyName} set to a subdirectory (e.g. 
userA) while the session policy
 +   * still authorizes only the request prefix (e.g. user). This returns true 
when {@code keyName} is the
 +   * prefix itself, a key path under that prefix, or an ancestor directory on 
the way to a deeper prefix.
 +   */
 +  private static boolean isStsListPathUnderRequestPrefix(String keyName, 
String listPrefix) {
 +    if (StringUtils.isBlank(listPrefix) || StringUtils.isBlank(keyName)) {
 +      return false;
 +    }
 +    if (keyName.equals(listPrefix)) {
 +      return true;
 +    }
 +    if (keyName.startsWith(listPrefix)) {
 +      return true;
 +    }
 +    return listPrefix.startsWith(keyName + "/");
 +  }
 +
    private ResourceType getResourceType(OmKeyArgs args) {
      if (args.getKeyName() == null || args.getKeyName().isEmpty()) {
        return ResourceType.BUCKET;
diff --cc 
hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/S3SecurityUtil.java
index 923bf9d9a78,860c69242b2..08ac1f2bee1
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/S3SecurityUtil.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/S3SecurityUtil.java
@@@ -23,14 -21,9 +23,15 @@@ import static org.apache.hadoop.ozone.o
  import static 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto.Type.S3AUTHINFO;
  
  import com.google.protobuf.ServiceException;
 +import java.io.IOException;
 +import java.time.Clock;
 +import java.time.ZoneOffset;
  import org.apache.hadoop.hdds.annotation.InterfaceAudience;
  import org.apache.hadoop.hdds.annotation.InterfaceStability;
 +import org.apache.hadoop.hdds.utils.db.Table;
  import org.apache.hadoop.io.Text;
++import org.apache.hadoop.ozone.om.AWSV4AuthValidator;
 +import org.apache.hadoop.ozone.om.OMMetadataManager;
  import org.apache.hadoop.ozone.om.OzoneManager;
  import org.apache.hadoop.ozone.om.exceptions.OMException;
  import org.apache.hadoop.ozone.om.exceptions.OMLeaderNotReadyException;
diff --cc 
hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOMMetadataReader.java
index cf94366d567,9b7d4a552f5..e5d55b5617a
--- 
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
@@@ -112,553 -69,4 +112,595 @@@ public class TestOMMetadataReader 
      }
    }
  
 +  @Test
 +  public void testCheckAclsAttachesSessionPolicyFromThreadLocal() throws 
Exception {
 +    final String sessionPolicy = "session-policy-from-thread-local";
 +    setupStsTokenIdentifier();
 +
 +    final IAccessAuthorizer accessAuthorizer = 
createMockIAccessAuthorizerReturningTrue();
 +    final OmMetadataReader omMetadataReader = 
createMetadataReader(accessAuthorizer);
 +
 +    final RequestContext.Builder contextWithoutSessionPolicyBuilder = 
createTestRequestContextBuilder();
 +    final OzoneObj obj = createTestOzoneObj();
 +
 +    assertTrue(omMetadataReader.checkAcls(obj, 
contextWithoutSessionPolicyBuilder, true));
 +
 +    verifySessionPolicyPassedToAuthorizer(accessAuthorizer, obj, 
sessionPolicy);
 +  }
 +
 +  @Test
 +  public void testNoSessionPolicyWhenThreadLocalIsNull() throws Exception {
 +    // No STS token identifier in thread local
 +    OzoneManager.setStsTokenIdentifier(null);
 +
 +    final IAccessAuthorizer accessAuthorizer = 
createMockIAccessAuthorizerReturningTrue();
 +    final OmMetadataReader omMetadataReader = 
createMetadataReader(accessAuthorizer);
 +
 +    final RequestContext.Builder contextWithoutSessionPolicyBuilder = 
createTestRequestContextBuilder();
 +    final OzoneObj obj = createTestOzoneObj();
 +
 +    assertTrue(omMetadataReader.checkAcls(obj, 
contextWithoutSessionPolicyBuilder, true));
 +
 +    verifySessionPolicyPassedToAuthorizer(accessAuthorizer, obj, null);
 +  }
 +
 +  @Test
 +  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 testCheckAclsDoesNotAttachS3ActionWhenStsFeatureDisabled() 
throws Exception {
 +    OzoneManager.setS3Auth(S3Authentication.newBuilder()
 +        .setAccessId(ACCESS_KEY_ID)
 +        .setS3Action("GetObject")
 +        .build());
 +
 +    final IAccessAuthorizer accessAuthorizer = 
createMockIAccessAuthorizerReturningTrue();
 +    final OmMetadataReader omMetadataReader = 
createMetadataReader(accessAuthorizer, mock(KeyManager.class), false);
 +
 +    final RequestContext.Builder contextWithoutS3ActionBuilder = 
createTestRequestContextBuilder();
 +    final OzoneObj obj = createTestOzoneObj();
 +
 +    assertTrue(omMetadataReader.checkAcls(obj, contextWithoutS3ActionBuilder, 
true));
 +
 +    verifyS3ActionPassedToAuthorizer(accessAuthorizer, obj, null);
 +  }
 +
 +  @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();
 +    final KeyManager keyManager = createListStatusKeyManagerReturningEmpty();
 +
 +    final OmMetadataReader omMetadataReader = 
createMetadataReader(accessAuthorizer, keyManager);
 +    final OmKeyArgs args = createOmKeyArgs();
 +
 +    final List<OzoneFileStatus> statuses = omMetadataReader.listStatus(args, 
false, "", MAX_KEYS, false);
 +    assertTrue(statuses.isEmpty());
 +
 +    final List<AclCheck> checks = captureAclChecks(accessAuthorizer, 2);
 +
 +    // For STS S3 requests, listStatus() performs these checks:
 +    // 1. Volume READ (for volume access)
 +    // 2) Key READ (for the specific prefix being listed)
 +    assertContainsVolumeReadCheck(checks);
 +    assertContainsKeyReadCheckWithName(checks, KEY_PREFIX);
 +  }
 +
 +  @Test
 +  public void testListStatusUsesReadAclForNonStsRequest() throws Exception {
 +    setupNonStsS3Request();
 +
 +    final IAccessAuthorizer accessAuthorizer = 
createMockIAccessAuthorizerReturningTrue();
 +    final KeyManager keyManager = createListStatusKeyManagerReturningEmpty();
 +
 +    final OmMetadataReader omMetadataReader = 
createMetadataReader(accessAuthorizer, keyManager);
 +    final OmKeyArgs args = createOmKeyArgs();
 +
 +    final List<OzoneFileStatus> statuses = omMetadataReader.listStatus(args, 
false, "", MAX_KEYS, false);
 +    assertTrue(statuses.isEmpty());
 +
 +    final List<AclCheck> checks = captureAclChecks(accessAuthorizer, 2);
 +    assertTrue(checks.stream().allMatch(check -> 
check.getContext().getAclRights() == READ));
 +
 +    assertContainsVolumeReadCheck(checks);
 +    // We want to ensure the current behavior for non-STS requests remains 
the same
 +    assertContainsKeyReadCheckWithName(checks);
 +  }
 +
 +  @Test
 +  public void 
testListStatusUsesListPrefixForAclWhenKeyNameEmptyAndListPrefixSet() throws 
Exception {
 +    setupStsS3Request();
 +
 +    final IAccessAuthorizer accessAuthorizer = 
createMockIAccessAuthorizerReturningTrue();
 +    final KeyManager keyManager = createListStatusKeyManagerReturningEmpty();
 +
 +    final OmMetadataReader omMetadataReader = 
createMetadataReader(accessAuthorizer, keyManager);
 +    final OmKeyArgs args = new OmKeyArgs.Builder()
 +        .setVolumeName(VOLUME_NAME)
 +        .setBucketName(BUCKET_NAME)
 +        .setKeyName("")
 +        .setListPrefix("userA/")
 +        .build();
 +
 +    final List<OzoneFileStatus> statuses = omMetadataReader.listStatus(args, 
false, "", MAX_KEYS, false);
 +    assertTrue(statuses.isEmpty());
 +
 +    final List<AclCheck> checks = captureAclChecks(accessAuthorizer, 2);
 +    assertContainsVolumeReadCheck(checks);
 +    assertContainsKeyReadCheckWithName(checks, "userA/");
 +  }
 +
 +  @Test
 +  public void testListStatusUsesWildcardForAclWhenKeyNameAndListPrefixEmpty() 
throws Exception {
 +    setupStsS3Request();
 +
 +    final IAccessAuthorizer accessAuthorizer = 
createMockIAccessAuthorizerReturningTrue();
 +    final KeyManager keyManager = createListStatusKeyManagerReturningEmpty();
 +
 +    final OmMetadataReader omMetadataReader = 
createMetadataReader(accessAuthorizer, keyManager);
 +    final OmKeyArgs args = new OmKeyArgs.Builder()
 +        .setVolumeName(VOLUME_NAME)
 +        .setBucketName(BUCKET_NAME)
 +        .setKeyName("")
 +        .build();
 +
 +    final List<OzoneFileStatus> statuses = omMetadataReader.listStatus(args, 
false, "", MAX_KEYS, false);
 +    assertTrue(statuses.isEmpty());
 +
 +    final List<AclCheck> checks = captureAclChecks(accessAuthorizer, 2);
 +    assertContainsVolumeReadCheck(checks);
 +    assertContainsKeyReadCheckWithName(checks, "*");
 +  }
 +
 +  @Test
 +  public void 
testListStatusUsesListPrefixForAclWhenKeyNameIsDescendantOfListPrefix() throws 
Exception {
 +    setupStsS3Request();
 +
 +    final IAccessAuthorizer accessAuthorizer = 
createMockIAccessAuthorizerReturningTrue();
 +    final KeyManager keyManager = createListStatusKeyManagerReturningEmpty();
 +
 +    final OmMetadataReader omMetadataReader = 
createMetadataReader(accessAuthorizer, keyManager);
 +    final OmKeyArgs args = new OmKeyArgs.Builder()
 +        .setVolumeName(VOLUME_NAME)
 +        .setBucketName(BUCKET_NAME)
 +        .setKeyName("userA")
 +        .setListPrefix("user")
 +        .build();
 +
 +    final List<OzoneFileStatus> statuses = omMetadataReader.listStatus(args, 
false, "", MAX_KEYS, false);
 +    assertTrue(statuses.isEmpty());
 +
 +    final List<AclCheck> checks = captureAclChecks(accessAuthorizer, 2);
 +    assertContainsVolumeReadCheck(checks);
 +    assertContainsKeyReadCheckWithName(checks, "user");
 +  }
 +
 +  @Test
 +  public void 
testListStatusUsesListPrefixForAclWhenKeyNameIsAncestorOfListPrefix() throws 
Exception {
 +    setupStsS3Request();
 +
 +    final IAccessAuthorizer accessAuthorizer = 
createMockIAccessAuthorizerReturningTrue();
 +    final KeyManager keyManager = createListStatusKeyManagerReturningEmpty();
 +
 +    final OmMetadataReader omMetadataReader = 
createMetadataReader(accessAuthorizer, keyManager);
 +    final OmKeyArgs args = new OmKeyArgs.Builder()
 +        .setVolumeName(VOLUME_NAME)
 +        .setBucketName(BUCKET_NAME)
 +        .setKeyName("user")
 +        .setListPrefix("user/foo")
 +        .build();
 +
 +    final List<OzoneFileStatus> statuses = omMetadataReader.listStatus(args, 
false, "", MAX_KEYS, false);
 +    assertTrue(statuses.isEmpty());
 +
 +    final List<AclCheck> checks = captureAclChecks(accessAuthorizer, 2);
 +    assertContainsVolumeReadCheck(checks);
 +    assertContainsKeyReadCheckWithName(checks, "user/foo");
 +  }
 +
 +  @Test
 +  public void testListStatusThrowsWhenStsKeyNameNotUnderListPrefix() throws 
Exception {
 +    setupStsS3Request();
 +
 +    final IAccessAuthorizer accessAuthorizer = 
createMockIAccessAuthorizerReturningTrue();
 +    final KeyManager keyManager = createListStatusKeyManagerReturningEmpty();
 +
 +    final OmMetadataReader omMetadataReader = 
createMetadataReader(accessAuthorizer, keyManager);
 +    final OmKeyArgs args = new OmKeyArgs.Builder()
 +        .setVolumeName(VOLUME_NAME)
 +        .setBucketName(BUCKET_NAME)
 +        .setKeyName(KEY_PREFIX)
 +        .setListPrefix("other/")
 +        .build();
 +
 +    final OMException ex = assertThrows(
 +        OMException.class, () -> omMetadataReader.listStatus(args, false, "", 
MAX_KEYS, false));
 +    assertEquals(ResultCodes.PERMISSION_DENIED, ex.getResult());
 +  }
 +
++  @Test
++  public void 
testListStatusLightUsesListPrefixForAclWhenKeyNameIsAncestorOfListPrefix() 
throws Exception {
++    setupStsS3Request();
++
++    final IAccessAuthorizer accessAuthorizer = 
createMockIAccessAuthorizerReturningTrue();
++    final KeyManager keyManager = createListStatusKeyManagerReturningEmpty();
++
++    final OmMetadataReader omMetadataReader = 
createMetadataReader(accessAuthorizer, keyManager);
++    final OmKeyArgs args = new OmKeyArgs.Builder()
++        .setVolumeName(VOLUME_NAME)
++        .setBucketName(BUCKET_NAME)
++        .setKeyName("user")
++        .setListPrefix("user/foo")
++        .build();
++
++    omMetadataReader.listStatusLight(args, false, "", MAX_KEYS, false);
++
++    final List<AclCheck> checks = captureAclChecks(accessAuthorizer, 2);
++    assertContainsVolumeReadCheck(checks);
++    assertContainsKeyReadCheckWithName(checks, "user/foo");
++  }
++
++  @Test
++  public void testListStatusLightThrowsWhenStsKeyNameNotUnderListPrefix() 
throws Exception {
++    setupStsS3Request();
++
++    final IAccessAuthorizer accessAuthorizer = 
createMockIAccessAuthorizerReturningTrue();
++    final KeyManager keyManager = createListStatusKeyManagerReturningEmpty();
++
++    final OmMetadataReader omMetadataReader = 
createMetadataReader(accessAuthorizer, keyManager);
++    final OmKeyArgs args = new OmKeyArgs.Builder()
++        .setVolumeName(VOLUME_NAME)
++        .setBucketName(BUCKET_NAME)
++        .setKeyName(KEY_PREFIX)
++        .setListPrefix("other/")
++        .build();
++
++    final OMException ex = assertThrows(
++        OMException.class, () -> omMetadataReader.listStatusLight(args, 
false, "", MAX_KEYS, false));
++    assertEquals(ResultCodes.PERMISSION_DENIED, ex.getResult());
++  }
++
 +  @Test
 +  public void testGetFileStatusUsesReadAclForStsS3Request() throws Exception {
 +    setupStsS3Request();
 +
 +    final IAccessAuthorizer accessAuthorizer = 
createMockIAccessAuthorizerReturningTrue();
 +    final KeyManager keyManager = 
createGetFileStatusKeyManagerReturningStatus();
 +
 +    final OmMetadataReader omMetadataReader = 
createMetadataReader(accessAuthorizer, keyManager);
 +    final OmKeyArgs args = createOmKeyArgs();
 +
 +    omMetadataReader.getFileStatus(args);
 +
 +    final List<AclCheck> checks = captureAclChecks(accessAuthorizer, 2);
 +    assertContainsVolumeReadCheck(checks);
 +    assertContainsKeyReadCheckWithName(checks, KEY_PREFIX);
 +  }
 +
 +  @Test
 +  public void testGetFileStatusUsesReadAclForNonStsS3Request() throws 
Exception {
 +    setupNonStsS3Request();
 +
 +    final IAccessAuthorizer accessAuthorizer = 
createMockIAccessAuthorizerReturningTrue();
 +    final KeyManager keyManager = 
createGetFileStatusKeyManagerReturningStatus();
 +
 +    final OmMetadataReader omMetadataReader = 
createMetadataReader(accessAuthorizer, keyManager);
 +    final OmKeyArgs args = createOmKeyArgs();
 +
 +    omMetadataReader.getFileStatus(args);
 +
 +    final List<AclCheck> checks = captureAclChecks(accessAuthorizer, 2);
 +    assertContainsVolumeReadCheck(checks);
 +    assertContainsKeyReadCheckWithName(checks);
 +  }
 +
 +  @Test
 +  public void testListKeysUsesPrefixCheckForStsS3Request() throws Exception {
 +    setupStsS3Request();
 +
 +    final IAccessAuthorizer accessAuthorizer = 
createMockIAccessAuthorizerReturningTrue();
 +    final KeyManager keyManager = createListKeysKeyManagerReturningEmpty();
 +
 +    final OmMetadataReader omMetadataReader = 
createMetadataReader(accessAuthorizer, keyManager);
 +
 +    // Case 1: List with prefix "userA/"
 +    omMetadataReader.listKeys(VOLUME_NAME, BUCKET_NAME, "", "userA/", (int) 
MAX_KEYS);
 +
 +    List<AclCheck> checks = captureAclChecks(accessAuthorizer, 4);
 +    assertContainsBucketListCheck(checks);
 +    assertContainsKeyReadCheckWithName(checks, "userA/");
 +
 +    // Reset to make case 2 assertions independent of case 1 captures.
 +    reset(accessAuthorizer);
 +    reenableAllowAllAccessChecks(accessAuthorizer);
 +
 +    // Case 2: List with empty prefix (should check "*")
 +    omMetadataReader.listKeys(VOLUME_NAME, BUCKET_NAME, "", "", (int) 
MAX_KEYS);
 +
 +    checks = captureAclChecks(accessAuthorizer, 4);
 +    assertContainsBucketListCheck(checks);
 +    assertContainsKeyReadCheckWithName(checks, "*");
 +  }
 +
 +  private OmMetadataReader createMetadataReader(IAccessAuthorizer 
accessAuthorizer) throws IOException {
 +    return createMetadataReader(accessAuthorizer, mock(KeyManager.class));
 +  }
 +
 +  private OmMetadataReader createMetadataReader(IAccessAuthorizer 
accessAuthorizer, KeyManager keyManager)
 +      throws IOException {
 +    return createMetadataReader(accessAuthorizer, keyManager, true);
 +  }
 +
 +  private OmMetadataReader createMetadataReader(IAccessAuthorizer 
accessAuthorizer, KeyManager keyManager,
 +      boolean isS3StsEnabled) throws IOException {
 +    final OzoneManager ozoneManager = mock(OzoneManager.class);
 +    
when(ozoneManager.getBucketManager()).thenReturn(mock(BucketManager.class));
 +    
when(ozoneManager.getVolumeManager()).thenReturn(mock(VolumeManager.class));
 +    when(ozoneManager.getConfiguration()).thenReturn(new 
OzoneConfiguration());
 +    when(ozoneManager.getAclsEnabled()).thenReturn(true);
 +    when(ozoneManager.isS3STSEnabled()).thenReturn(isS3StsEnabled);
 +    final OMPerformanceMetrics perfMetrics = mock(OMPerformanceMetrics.class);
 +    // OmMetadataReader uses these MutableRate metrics via 
MetricUtil.captureLatencyNs(...).
 +    
when(perfMetrics.getListKeysResolveBucketLatencyNs()).thenReturn(mock(MutableRate.class));
 +    
when(perfMetrics.getListKeysAclCheckLatencyNs()).thenReturn(mock(MutableRate.class));
 +    when(ozoneManager.getPerfMetrics()).thenReturn(perfMetrics);
 +    when(ozoneManager.getVolumeOwner(any(), any(), 
any())).thenReturn("volume-owner");
 +    when(ozoneManager.getBucketOwner(any(), any(), any(), 
any())).thenReturn("bucket-owner");
 +    when(ozoneManager.getOmRpcServerAddr()).thenReturn(new 
InetSocketAddress("127.0.0.1", 9874));
 +    when(ozoneManager.resolveBucketLink(any(Pair.class)))
 +        .thenReturn(
 +            new ResolvedBucket(
 +                VOLUME_NAME, BUCKET_NAME, VOLUME_NAME, BUCKET_NAME, 
"bucket-owner", FILE_SYSTEM_OPTIMIZED));
 +    when(ozoneManager.resolveBucketLink(any(OmKeyArgs.class)))
 +        .thenReturn(
 +            new ResolvedBucket(
 +                VOLUME_NAME, BUCKET_NAME, VOLUME_NAME, BUCKET_NAME, 
"bucket-owner", FILE_SYSTEM_OPTIMIZED));
 +
 +    return new OmMetadataReader(
 +        keyManager, mock(PrefixManager.class), ozoneManager, 
mock(Logger.class), mock(AuditLogger.class),
 +        mock(OmMetadataReaderMetrics.class), accessAuthorizer);
 +  }
 +
 +  /**
 +   * Creates and sets a mock STSTokenIdentifier with a session policy in the 
thread-local.
 +   */
 +  private void setupStsTokenIdentifier() {
 +    final STSTokenIdentifier stsTokenIdentifier = 
mock(STSTokenIdentifier.class);
 +    
when(stsTokenIdentifier.getSessionPolicy()).thenReturn("session-policy-from-thread-local");
 +    OzoneManager.setStsTokenIdentifier(stsTokenIdentifier);
 +  }
 +
 +  /**
 +   * Creates a mock IAccessAuthorizer that returns the specified result for 
checkAccess.
 +   * @return the mocked IAccessAuthorizer
 +   */
 +  private IAccessAuthorizer createMockIAccessAuthorizerReturningTrue() throws 
OMException {
 +    final IAccessAuthorizer accessAuthorizer = mock(IAccessAuthorizer.class);
 +    when(accessAuthorizer.checkAccess(any(OzoneObj.class), 
any(RequestContext.class)))
 +        .thenReturn(true);
 +    return accessAuthorizer;
 +  }
 +
 +  /**
 +   * Creates a test RequestContext.Builder.
 +   *
 +   * @return the constructed RequestContext.Builder
 +   */
 +  private RequestContext.Builder createTestRequestContextBuilder() {
 +    return RequestContext.newBuilder()
 +        .setClientUgi(UserGroupInformation.createRemoteUser("testUser"))
 +        .setIp(InetAddress.getLoopbackAddress())
 +        .setHost("localhost")
 +        .setAclType(IAccessAuthorizer.ACLIdentityType.USER)
 +        .setAclRights(READ)
 +        .setOwnerName("owner");
 +  }
 +
 +  /**
 +   * Creates a test OzoneObj representing a key.
 +   * @return the constructed OzoneObj
 +   */
 +  private OzoneObj createTestOzoneObj() {
 +    return OzoneObjInfo.Builder.newBuilder()
 +        .setResType(KEY)
 +        .setStoreType(OzoneObj.StoreType.OZONE)
 +        .setVolumeName(VOLUME_NAME)
 +        .setBucketName(BUCKET_NAME)
 +        .setKeyName("key")
 +        .build();
 +  }
 +
 +  private void setupStsS3Request() {
 +    OzoneManager.setStsTokenIdentifier(mock(STSTokenIdentifier.class));
 +    
OzoneManager.setS3Auth(S3Authentication.newBuilder().setAccessId(TestOMMetadataReader.ACCESS_KEY_ID).build());
 +  }
 +
 +  private void setupNonStsS3Request() {
 +    OzoneManager.setStsTokenIdentifier(null);
 +    OzoneManager.setS3Auth(null);
 +  }
 +
 +  private OmKeyArgs createOmKeyArgs() {
 +    return new OmKeyArgs.Builder()
 +        .setVolumeName(VOLUME_NAME)
 +        .setBucketName(BUCKET_NAME)
 +        .setKeyName(TestOMMetadataReader.KEY_PREFIX)
 +        .build();
 +  }
 +
 +  private KeyManager createListStatusKeyManagerReturningEmpty() throws 
IOException {
 +    final KeyManager keyManager = mock(KeyManager.class);
 +    when(keyManager.listStatus(any(OmKeyArgs.class), eq(false), eq(""), 
eq(MAX_KEYS), any(), eq(false)))
 +        .thenReturn(Collections.emptyList());
 +    return keyManager;
 +  }
 +
 +  private KeyManager createGetFileStatusKeyManagerReturningStatus() throws 
IOException {
 +    final KeyManager keyManager = mock(KeyManager.class);
 +    when(keyManager.getFileStatus(any(OmKeyArgs.class), any()))
 +        .thenReturn(mock(OzoneFileStatus.class));
 +    return keyManager;
 +  }
 +
 +  private KeyManager createListKeysKeyManagerReturningEmpty() throws 
IOException {
 +    final KeyManager keyManager = mock(KeyManager.class);
 +    when(keyManager.listKeys(any(), any(), any(), any(), eq(100)))
 +        .thenReturn(new ListKeysResult(Collections.emptyList(), false));
 +    return keyManager;
 +  }
 +
 +  private void reenableAllowAllAccessChecks(IAccessAuthorizer 
accessAuthorizer) throws OMException {
 +    when(accessAuthorizer.checkAccess(any(OzoneObj.class), 
any(RequestContext.class)))
 +        .thenReturn(true);
 +  }
 +
 +  /**
 +   * Verifies that the accessAuthorizer received a call to checkAccess with 
the expected session policy.
 +   * @param accessAuthorizer the mock authorizer to verify
 +   * @param expectedObj the expected OzoneObj
 +   * @param expectedSessionPolicy the expected session policy (could be null)
 +   */
 +  private void verifySessionPolicyPassedToAuthorizer(IAccessAuthorizer 
accessAuthorizer, OzoneObj expectedObj,
 +      String expectedSessionPolicy) throws OMException {
 +    final ArgumentCaptor<RequestContext> captor = 
ArgumentCaptor.forClass(RequestContext.class);
 +    verify(accessAuthorizer).checkAccess(eq(expectedObj), captor.capture());
 +    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);
 +    final ArgumentCaptor<RequestContext> ctxCaptor = 
ArgumentCaptor.forClass(RequestContext.class);
 +    verify(accessAuthorizer, 
times(expectedCheckCount)).checkAccess(objCaptor.capture(), 
ctxCaptor.capture());
 +    return toAclChecks(objCaptor.getAllValues(), ctxCaptor.getAllValues());
 +  }
 +
 +  private List<AclCheck> toAclChecks(List<OzoneObj> objs, 
List<RequestContext> contexts) {
 +    assertEquals(objs.size(), contexts.size(), "Captured ACL objects and 
contexts should align");
 +    final List<AclCheck> checks = new ArrayList<>();
 +    for (int i = 0; i < objs.size(); i++) {
 +      checks.add(new AclCheck(objs.get(i), contexts.get(i)));
 +    }
 +    return checks;
 +  }
 +
 +  private void assertContainsVolumeReadCheck(List<AclCheck> checks) {
 +    assertTrue(checks.stream().anyMatch(this::isVolumeReadCheck), "Expected a 
VOLUME READ ACL check");
 +  }
 +
 +  private boolean isVolumeReadCheck(AclCheck check) {
 +    return check.getObj().getResourceType() == VOLUME && 
check.getContext().getAclRights() == READ;
 +  }
 +
 +  private void assertContainsBucketListCheck(List<AclCheck> checks) {
 +    assertTrue(
 +        checks.stream().anyMatch(
 +            check -> check.getObj().getResourceType() == 
OzoneObj.ResourceType.BUCKET &&
 +                check.getContext().getAclRights() == LIST),
 +        "Expected a BUCKET LIST ACL check");
 +  }
 +
 +  private void assertContainsKeyReadCheckWithName(List<AclCheck> checks, 
String keyName) {
 +    assertTrue(
 +        checks.stream().anyMatch(
 +            check -> check.getObj().getResourceType() == KEY && 
check.getContext().getAclRights() == READ &&
 +                keyName.equals(check.getObj().getKeyName())),
 +        "Expected a KEY READ ACL check for key '" + keyName + "'");
 +  }
 +
 +  private void assertContainsKeyReadCheckWithName(List<AclCheck> checks) {
 +    assertTrue(
 +        checks.stream().anyMatch(
 +            check -> check.getObj().getResourceType() == KEY && 
check.getContext().getAclRights() == READ &&
 +                
TestOMMetadataReader.KEY_PREFIX.equals(check.getObj().getKeyName())),
 +        "Expected a KEY READ ACL check for key '" + 
TestOMMetadataReader.KEY_PREFIX + "'");
 +  }
 +
 +  private static final class AclCheck {
 +    private final OzoneObj obj;
 +    private final RequestContext context;
 +
 +    private AclCheck(OzoneObj obj, RequestContext context) {
 +      this.obj = obj;
 +      this.context = context;
 +    }
 +
 +    private OzoneObj getObj() {
 +      return obj;
 +    }
 +
 +    private RequestContext getContext() {
 +      return context;
 +    }
 +  }
  }
diff --cc 
hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestS3SecurityUtil.java
index 76b126ae603,00000000000..99c358b929d
mode 100644,000000..100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestS3SecurityUtil.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestS3SecurityUtil.java
@@@ -1,318 -1,0 +1,319 @@@
 +/*
 + * 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.security;
 +
 +import static 
org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INTERNAL_ERROR;
 +import static 
org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_TOKEN;
 +import static 
org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.REVOKED_TOKEN;
 +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 +import static org.junit.jupiter.api.Assertions.assertEquals;
 +import static org.junit.jupiter.api.Assertions.assertThrows;
 +import static org.junit.jupiter.api.Assertions.assertTrue;
 +import static org.mockito.ArgumentMatchers.any;
 +import static org.mockito.ArgumentMatchers.anyString;
 +import static org.mockito.ArgumentMatchers.eq;
 +import static org.mockito.Mockito.CALLS_REAL_METHODS;
 +import static org.mockito.Mockito.doThrow;
 +import static org.mockito.Mockito.mock;
 +import static org.mockito.Mockito.mockStatic;
 +import static org.mockito.Mockito.spy;
 +import static org.mockito.Mockito.when;
 +
 +import java.io.IOException;
 +import java.time.Clock;
 +import java.util.UUID;
 +import java.util.concurrent.ThreadLocalRandom;
 +import org.apache.hadoop.hdds.security.symmetric.SecretKeyClient;
 +import org.apache.hadoop.hdds.utils.db.InMemoryTestTable;
 +import org.apache.hadoop.hdds.utils.db.Table;
++import org.apache.hadoop.ozone.om.AWSV4AuthValidator;
 +import org.apache.hadoop.ozone.om.OMMetadataManager;
 +import org.apache.hadoop.ozone.om.OzoneManager;
 +import org.apache.hadoop.ozone.om.S3SecretManager;
 +import org.apache.hadoop.ozone.om.exceptions.OMException;
 +import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
 +import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.S3Authentication;
 +import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
 +import org.apache.ozone.test.MockClock;
 +import org.junit.jupiter.api.Test;
 +import org.mockito.MockedStatic;
 +
 +/**
 + * Tests for STS revocation handling in {@link S3SecurityUtil}.
 + */
 +public class TestS3SecurityUtil {
 +  private static final byte[] ENCRYPTION_KEY = new byte[5];
 +  private static final MockClock CLOCK = MockClock.newInstance();
 +  private static final String TEMP_ACCESS_KEY_ID = "temp-access-key-id";
 +
 +  {
 +    ThreadLocalRandom.current().nextBytes(ENCRYPTION_KEY);
 +  }
 +
 +  @Test
 +  public void testValidateS3CredentialFailsWhenTokenRevoked() throws 
Exception {
 +    // If the revoked STS token table contains an entry for the session 
token, the request should be rejected with
 +    // REVOKED_TOKEN
 +    validateS3CredentialHelper(
 +        new TestConfig()
 +            .setTokenRevoked(true)
 +            .setExpectedResult(REVOKED_TOKEN)
 +            .setExpectedMessage("STS token has been revoked"));
 +  }
 +
 +  @Test
 +  public void testValidateS3CredentialWhenMetadataUnavailable() throws 
Exception {
 +    // If the metadata manager is not available, throws INTERNAL_ERROR
 +    validateS3CredentialHelper(
 +        new TestConfig()
 +            .setMetadataManager(null)
 +            .setExpectedResult(INTERNAL_ERROR)
 +            .setExpectedMessage("Could not determine STS revocation: 
metadataManager is null"));
 +  }
 +
 +  @Test
 +  public void testValidateS3CredentialSuccessWhenNotRevoked() throws 
Exception {
 +    // Normal case: token is NOT revoked and request is accepted
 +    validateS3CredentialHelper(new TestConfig());
 +  }
 +
 +  @Test
 +  public void 
testValidateS3CredentialWhenMetadataManagerAvailableButRevokedTableNull() 
throws Exception {
 +    // If the revoked STS token table is not available, throws INTERNAL_ERROR
 +    validateS3CredentialHelper(
 +        new TestConfig()
 +            .setRevokedSTSTokenTable(null)
 +            .setExpectedResult(INTERNAL_ERROR)
 +            .setExpectedMessage("Could not determine STS revocation: 
revokedStsTokenTable is null"));
 +  }
 +
 +  @Test
 +  public void testValidateS3CredentialWhenTableThrowsException() throws 
Exception {
 +    // If the revoked STS token table lookup throws, throws INTERNAL_ERROR 
(wrapped)
 +    final Table<String, Long> revokedSTSTokenTable = spy(new 
InMemoryTestTable<>());
 +    doThrow(new RuntimeException("lookup 
failed")).when(revokedSTSTokenTable).getIfExist(anyString());
 +
 +    validateS3CredentialHelper(
 +        new TestConfig()
 +            .setRevokedSTSTokenTable(revokedSTSTokenTable)
 +            .setExpectedResult(INTERNAL_ERROR)
 +            .setExpectedMessage("Could not determine STS revocation because 
of Exception: lookup failed"));
 +  }
 +
 +  @Test
 +  public void 
testValidateS3CredentialFailsWhenOriginalAccessKeyIdPrincipalRevoked() throws 
Exception {
 +    // If the originalAccessKeyId principal is revoked, throws REVOKED_TOKEN
 +    validateS3CredentialHelper(
 +        new TestConfig()
 +            .setOriginalAccessKeyIdRevoked(true)
 +            .setExpectedResult(REVOKED_TOKEN)
 +            .setExpectedMessage("STS token no longer valid: 
OriginalAccessKeyId principal revoked"));
 +  }
 +
 +  @Test
 +  public void 
testValidateS3CredentialFailsWhenOriginalAccessKeyIdCheckThrows() throws 
Exception {
 +    // If checking originalAccessKeyId principal revocation fails, throws 
INTERNAL_ERROR
 +    validateS3CredentialHelper(
 +        new TestConfig()
 +            .setShouldOriginalAccessKeyIdCheckThrowError(true)
 +            .setExpectedResult(INTERNAL_ERROR)
 +            .setExpectedMessage("Could not determine if original principal is 
revoked"));
 +  }
 +
 +  @Test
 +  public void 
testValidateS3CredentialFailsWhenRequestAccessIdDoesNotMatchTokenOwner() throws 
Exception {
 +    validateS3CredentialHelper(
 +        new TestConfig()
 +            .setRequestAccessId("some-other-access-id")
 +            .setExpectedResult(INVALID_TOKEN)
 +            .setExpectedMessage("STS token validation failed - accessKeyId is 
invalid for session token"));
 +  }
 +
 +  @Test
 +  public void testValidateS3CredentialFailsWhenRequestAccessIdMissing() 
throws Exception {
 +    validateS3CredentialHelper(
 +        new TestConfig()
 +            .setIncludeAccessId(false)
 +            .setExpectedResult(INVALID_TOKEN)
 +            .setExpectedMessage("STS token validation failed - accessKeyId is 
invalid for session token"));
 +  }
 +
 +  @Test
 +  public void testValidateS3CredentialFailsWhenRequestAccessIdEmpty() throws 
Exception {
 +    validateS3CredentialHelper(
 +        new TestConfig()
 +            .setRequestAccessId("")
 +            .setExpectedResult(INVALID_TOKEN)
 +            .setExpectedMessage("STS token validation failed - accessKeyId is 
invalid for session token"));
 +  }
 +
 +  private void validateS3CredentialHelper(TestConfig config) throws Exception 
{
 +    try (OzoneManager ozoneManager = mock(OzoneManager.class)) {
 +      when(ozoneManager.isSecurityEnabled()).thenReturn(true);
 +      
when(ozoneManager.getSecretKeyClient()).thenReturn(mock(SecretKeyClient.class));
 +
 +      final OMMetadataManager metadataManager = config.metadataManager;
 +      when(ozoneManager.getMetadataManager()).thenReturn(metadataManager);
 +      if (metadataManager != null) {
 +        
when(metadataManager.getS3RevokedStsTokenTable()).thenReturn(config.revokedSTSTokenTable);
 +      }
 +
 +      // Mock S3SecretManager to handle originalAccessKeyId checks
 +      final S3SecretManager s3SecretManager = mock(S3SecretManager.class);
 +      when(ozoneManager.getS3SecretManager()).thenReturn(s3SecretManager);
 +      if (config.shouldOriginalAccessKeyIdCheckThrowError) {
 +        when(s3SecretManager.hasS3Secret(anyString())).thenThrow(
 +            new IOException("An error occurred while checking if s3Secret 
exists"));
 +      } else if (config.isOriginalAccessKeyIdRevoked) {
 +        // Returning false means secret does NOT exist -> principal is revoked
 +        when(s3SecretManager.hasS3Secret(anyString())).thenReturn(false);
 +      } else {
 +        // Returning true means secret exists -> principal is valid
 +        when(s3SecretManager.hasS3Secret(anyString())).thenReturn(true);
 +      }
 +
 +      final String sessionToken = "session-token";
 +      if (config.isTokenRevoked && config.revokedSTSTokenTable != null) {
 +        final long insertionTimeMillis = CLOCK.millis();
 +        config.revokedSTSTokenTable.put(sessionToken, insertionTimeMillis);
 +      }
 +
 +      final STSTokenIdentifier stsTokenIdentifier = 
createSTSTokenIdentifier();
 +
 +      try (MockedStatic<STSSecurityUtil> stsSecurityUtilMock = 
mockStatic(STSSecurityUtil.class, CALLS_REAL_METHODS);
 +           MockedStatic<AWSV4AuthValidator> awsV4AuthValidatorMock = 
mockStatic(
 +               AWSV4AuthValidator.class, CALLS_REAL_METHODS)) {
 +
 +        stsSecurityUtilMock.when(
 +            () -> STSSecurityUtil.constructValidateAndDecryptSTSToken(
 +                eq(sessionToken), any(SecretKeyClient.class), 
any(Clock.class)))
 +            .thenReturn(stsTokenIdentifier);
 +
 +        // Mock AWS V4 signature validation
 +        awsV4AuthValidatorMock.when(() -> 
AWSV4AuthValidator.validateRequest(anyString(), anyString(), anyString()))
 +            .thenReturn(true);
 +
 +        final OMRequest omRequest = createRequestWithSessionToken(
 +            config.requestAccessId, config.includeAccessId);
 +
 +        if (config.expectedResult != null) {
 +          final OMException omException = assertThrows(
 +              OMException.class, () -> 
S3SecurityUtil.validateS3Credential(omRequest, ozoneManager));
 +          assertEquals(config.expectedResult, omException.getResult());
 +          if (config.expectedMessage != null) {
 +            assertTrue(
 +                omException.getMessage().contains(config.expectedMessage),
 +                "Expected exception message to contain: '" + 
config.expectedMessage + "' but was: '" +
 +                    omException.getMessage() + "'");
 +          }
 +        } else {
 +          assertDoesNotThrow(() -> 
S3SecurityUtil.validateS3Credential(omRequest, ozoneManager));
 +        }
 +      }
 +    }
 +  }
 +
 +  private STSTokenIdentifier createSTSTokenIdentifier() {
 +    return new STSTokenIdentifier(
 +        TEMP_ACCESS_KEY_ID, "original-access-key-id", 
"arn:aws:iam::123456789012:role/test-role",
 +        CLOCK.instant().plusSeconds(3600), "secret-access-key", 
"session-policy",
 +        ENCRYPTION_KEY);
 +  }
 +
 +  private static OMRequest createRequestWithSessionToken(String accessId, 
boolean includeAccessId) {
 +    final S3Authentication.Builder s3AuthenticationBuilder = 
S3Authentication.newBuilder()
 +        .setStringToSign("string-to-sign")
 +        .setSignature("signature")
 +        .setSessionToken("session-token");
 +    if (includeAccessId) {
 +      s3AuthenticationBuilder.setAccessId(accessId);
 +    }
 +    final S3Authentication s3Authentication = s3AuthenticationBuilder.build();
 +
 +    return OMRequest.newBuilder()
 +        .setClientId(UUID.randomUUID().toString())
 +        .setCmdType(Type.CreateVolume)
 +        .setS3Authentication(s3Authentication)
 +        .build();
 +  }
 +
 +  /**
 +   * Helper class to create various scenarios for testing.
 +   */
 +  private static final class TestConfig {
 +    private OMMetadataManager metadataManager = mock(OMMetadataManager.class);
 +    private Table<String, Long> revokedSTSTokenTable = new 
InMemoryTestTable<>();
 +    private boolean isTokenRevoked = false;
 +    private boolean isOriginalAccessKeyIdRevoked = false;
 +    private boolean shouldOriginalAccessKeyIdCheckThrowError = false;
 +    private String requestAccessId = TEMP_ACCESS_KEY_ID;
 +    private boolean includeAccessId = true;
 +    private OMException.ResultCodes expectedResult = null;
 +    private String expectedMessage = null;
 +
 +    @SuppressWarnings("SameParameterValue")
 +    TestConfig setMetadataManager(OMMetadataManager metadataManager) {
 +      this.metadataManager = metadataManager;
 +      return this;
 +    }
 +
 +    TestConfig setRevokedSTSTokenTable(Table<String, Long> table) {
 +      this.revokedSTSTokenTable = table;
 +      return this;
 +    }
 +
 +    @SuppressWarnings("SameParameterValue")
 +    TestConfig setTokenRevoked(boolean isRevoked) {
 +      this.isTokenRevoked = isRevoked;
 +      return this;
 +    }
 +
 +    @SuppressWarnings("SameParameterValue")
 +    TestConfig setOriginalAccessKeyIdRevoked(boolean isRevoked) {
 +      this.isOriginalAccessKeyIdRevoked = isRevoked;
 +      return this;
 +    }
 +
 +    @SuppressWarnings("SameParameterValue")
 +    TestConfig setShouldOriginalAccessKeyIdCheckThrowError(boolean isError) {
 +      this.shouldOriginalAccessKeyIdCheckThrowError = isError;
 +      return this;
 +    }
 +
 +    TestConfig setRequestAccessId(String requestAccessId) {
 +      this.requestAccessId = requestAccessId;
 +      return this;
 +    }
 +
 +    @SuppressWarnings("SameParameterValue")
 +    TestConfig setIncludeAccessId(boolean includeAccessId) {
 +      this.includeAccessId = includeAccessId;
 +      return this;
 +    }
 +
 +    TestConfig setExpectedResult(OMException.ResultCodes result) {
 +      this.expectedResult = result;
 +      return this;
 +    }
 +
 +    TestConfig setExpectedMessage(String message) {
 +      this.expectedMessage = message;
 +      return this;
 +    }
 +  }
 +}


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

Reply via email to