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]
