From b77702669a02ae86733b01c3489f54f8a7e667ad Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Sun, 16 Aug 2026 16:23:34 -0700
Subject: [PATCH 01/20] redesign: update design doc
---
hadoop-hdds/docs/content/design/ozone-sts.md | 20 ++++++++++++++------
1 file changed, 14 insertions(+), 6 deletions(-)
diff --git a/hadoop-hdds/docs/content/design/ozone-sts.md b/hadoop-hdds/docs/content/design/ozone-sts.md
index 8c7d5cd3b44..3acbafdcf9a 100644
--- a/hadoop-hdds/docs/content/design/ozone-sts.md
+++ b/hadoop-hdds/docs/content/design/ozone-sts.md
@@ -139,17 +139,24 @@ was included with the AssumeRole request, the String return value will also incl
would further limit the scope of the permissions, resources and actions granted by the role in Ranger, such that the temporary
credential will have the permissions and actions comprising the intersection of the role permissions and actions and the sessionPolicy permissions and actions.
- HMAC-SHA256 signature - used to ensure the sessionToken was created by Ozone and was not altered since it was created.
+- creation time of the token (via `OMTokenProto#issueDate`, exposed as `STSTokenIdentifier#getCreationTime()`)
- expiration time of the token (via `ShortLivedTokenIdentifier#getExpiry()`)
- UUID of the OzoneManager secret key used to sign the sessionToken and encrypt the secretAccessKey (via `ShortLivedTokenIdentifier#getSecretKeyId()`)
## 3.5 STS Token Revocation
In the rare event temporary credentials need to be revoked (ex. for security reasons), a table in the OzoneManager RocksDB will be created
-to store revoked tokens, and a command-line utility will be created to add tokens to the table. A background cleaner service
-will be created to run every 3 hours to delete revoked tokens that have been in the table for more than 12 hours. The
-input parameter for the command-line utility will be the sessionToken - this value is returned in plain text as a result
-of the AssumeRole call (mentioned above). In this way, specific STS tokens can be revoked as opposed to all tokens. Furthermore,
-AWS doesn't have a standard API to revoke tokens therefore we are creating our own system.
+to store revocation cutoffs per originalAccessKeyId, and a command-line utility will be created to add entries to the table.
+A background cleaner service will be created to run every 3 hours to delete revocation entries whose cutoff is more than 12 hours old.
+
+The command-line utility accepts only `originalAccessKeyId`. The OM stores revocations by keying the table on
+`originalAccessKeyId` and storing the revocation cutoff time in milliseconds as the value. When the command is issued,
+all STS tokens created by that `originalAccessKeyId` whose signed `creationTime` is strictly before the cutoff are
+revoked. Tokens created at or after the cutoff remain valid.
+
+Before writing a revocation entry, the OM verifies that `originalAccessKeyId` corresponds to a real Kerberos identity by
+checking that an S3 secret exists for it. This prevents bogus entries from filling the table. Non-admins may only
+revoke their own `originalAccessKeyId`; S3 and tenant admins may revoke other principals.
Additionally, if the Kerberos identity of the user that created the STS token is revoked via the `ozone s3 revokesecret`
command, then all the existing and unexpired STS tokens that user created will be revoked.
@@ -221,7 +228,8 @@ created in Ranger as per the Prerequisites above.
originalAccessKeyId in the session token and perform the following checks:
- Ensure that if the accessKeyId starts with "ASIA", that a sessionToken was included in the `x-amz-security-token` header
- Ensure the sessionToken is not expired
- - Ensure the sessionToken is not revoked via a `keyMayExist` check in OzoneManager RocksDB
+ - Ensure the STS credentials are not revoked by looking up the revocation cutoff for the token's originalAccessKeyId
+ and comparing it against the token's signed creationTime
- Validate the HMAC-SHA256 signature in the sessionToken
- Decrypt the secretAccessKey from the sessionToken and validate the AWS signature
- Authorize the call with either RangerOzoneAuthorizer or OzoneNativeAuthorizer
From 7b47b09222e583f92290d21d16da41affc821be9 Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Sun, 16 Aug 2026 16:29:09 -0700
Subject: [PATCH 02/20] redesign: update proto and related classes
---
.../apache/hadoop/ozone/client/ObjectStore.java | 8 ++++----
.../ozone/client/protocol/ClientProtocol.java | 6 +++---
.../hadoop/ozone/client/rpc/RpcClient.java | 4 ++--
.../ozone/om/protocol/OzoneManagerProtocol.java | 6 +++---
...oneManagerProtocolClientSideTranslatorPB.java | 4 ++--
.../src/main/proto/OmClientProtocol.proto | 16 +++++++++++++---
.../hadoop/ozone/client/ClientProtocolStub.java | 2 +-
7 files changed, 28 insertions(+), 18 deletions(-)
diff --git a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/ObjectStore.java b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/ObjectStore.java
index bd045ef04e0..ce0f780b72d 100644
--- a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/ObjectStore.java
+++ b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/ObjectStore.java
@@ -813,12 +813,12 @@ public AssumeRoleResponseInfo assumeRole(String roleArn, String roleSessionName,
}
/**
- * Revokes an STS token.
- * @param sessionToken The STS sessionToken
+ * Revokes STS tokens for the given original access key ID.
+ * @param originalAccessKeyId The original long-lived access key ID whose STS tokens to revoke
* @throws IOException if an error occurs while revoking the STS token
*/
- public void revokeSTSToken(String sessionToken) throws IOException {
- proxy.revokeSTSToken(sessionToken);
+ public void revokeSTSToken(String originalAccessKeyId) throws IOException {
+ proxy.revokeSTSToken(originalAccessKeyId);
}
/**
diff --git a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
index 807cd2757cf..b5f7baa0ef2 100644
--- a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
+++ b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
@@ -1648,11 +1648,11 @@ AssumeRoleResponseInfo assumeRole(String roleArn, String roleSessionName, int du
String awsIamSessionPolicy, String requestId) throws IOException;
/**
- * Revokes an STS token.
- * @param sessionToken The STS sessionToken
+ * Revokes STS tokens for the given original access key ID.
+ * @param originalAccessKeyId The original long-lived access key ID whose STS tokens to revoke
* @throws IOException if an error occurs while revoking the STS token
*/
- void revokeSTSToken(String sessionToken) throws IOException;
+ void revokeSTSToken(String originalAccessKeyId) throws IOException;
/**
* Gets the lifecycle configuration information.
diff --git a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
index 733c915dcd0..9ca47013462 100644
--- a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
+++ b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
@@ -3022,8 +3022,8 @@ public AssumeRoleResponseInfo assumeRole(String roleArn, String roleSessionName,
}
@Override
- public void revokeSTSToken(String sessionToken) throws IOException {
- ozoneManagerClient.revokeSTSToken(sessionToken);
+ public void revokeSTSToken(String originalAccessKeyId) throws IOException {
+ ozoneManagerClient.revokeSTSToken(originalAccessKeyId);
}
@Override
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
index 604669487c6..46254e3d6f6 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
@@ -1336,11 +1336,11 @@ default AssumeRoleResponseInfo assumeRole(String roleArn, String roleSessionName
}
/**
- * Revokes an STS token.
- * @param sessionToken The STS sessionToken
+ * Revokes STS tokens for the given original access key ID.
+ * @param originalAccessKeyId The original long-lived access key ID whose STS tokens to revoke
* @throws IOException if an error occurs while revoking the STS token
*/
- default void revokeSTSToken(String sessionToken) throws IOException {
+ default void revokeSTSToken(String originalAccessKeyId) throws IOException {
throw new UnsupportedOperationException("OzoneManager does not require this to be implemented");
}
}
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
index c60bc60db70..7077fd1b02c 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
@@ -2981,10 +2981,10 @@ public AssumeRoleResponseInfo assumeRole(String roleArn, String roleSessionName,
}
@Override
- public void revokeSTSToken(String sessionToken) throws IOException {
+ public void revokeSTSToken(String originalAccessKeyId) throws IOException {
final OzoneManagerProtocolProtos.RevokeSTSTokenRequest request =
OzoneManagerProtocolProtos.RevokeSTSTokenRequest.newBuilder()
- .setSessionToken(sessionToken)
+ .setOriginalAccessKeyId(originalAccessKeyId)
.build();
final OMRequest omRequest = createOMRequest(Type.RevokeSTSToken)
diff --git a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
index c06d54209a0..029a78a335d 100644
--- a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
+++ b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
@@ -333,6 +333,7 @@ message OMRequest {
optional RevokeSTSTokenRequest revokeSTSTokenRequest = 155;
optional DeleteRevokedSTSTokensRequest deleteRevokedSTSTokensRequest = 156;
optional UpdateAssumeRoleRequest updateAssumeRoleRequest = 157;
+ optional UpdateRevokeSTSTokenRequest updateRevokeSTSTokenRequest = 158;
}
message OMResponse {
@@ -2534,18 +2535,27 @@ message UpdateAssumeRoleRequest {
}
message RevokeSTSTokenRequest {
- required string sessionToken = 1;
+ required string originalAccessKeyId = 1;
+}
+
+/**
+ This request will be used internally by OM to replicate the revocation cutoff captured by the leader
+ across the OMs in HA mode.
+*/
+message UpdateRevokeSTSTokenRequest {
+ required string originalAccessKeyId = 1;
+ required uint64 revocationTimeMillis = 2;
}
message RevokeSTSTokenResponse {
}
/**
- This will contain a list of revoked STS session tokens whose entries should be removed from
+ This will contain a list of originalAccessKeyIds whose revocation entries should be removed from
the s3RevokedStsTokenTable.
*/
message DeleteRevokedSTSTokensRequest {
- repeated string sessionToken = 1;
+ repeated string originalAccessKeyId = 1;
}
message DeleteRevokedSTSTokensResponse {
diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
index b79984fa93e..abd80cbc1fc 100644
--- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
+++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
@@ -899,7 +899,7 @@ public AssumeRoleResponseInfo assumeRole(String roleArn, String roleSessionName,
}
@Override
- public void revokeSTSToken(String sessionToken) throws IOException {
+ public void revokeSTSToken(String originalAccessKeyId) throws IOException {
}
@Override
From 12ec2502e5535063fb3c24455914bfe34b75c922 Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Sun, 16 Aug 2026 16:30:39 -0700
Subject: [PATCH 03/20] redesign: update rocksDb related classes
---
.../ozone/om/OmMetadataManagerImpl.java | 2 +-
.../hadoop/ozone/om/codec/OMDBDefinition.java | 7 +++--
.../ozone/om/TestOmMetadataManager.java | 28 +++++++++----------
3 files changed, 20 insertions(+), 17 deletions(-)
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataManagerImpl.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataManagerImpl.java
index 97f6cf92036..f1a7a51d3e2 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataManagerImpl.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataManagerImpl.java
@@ -540,7 +540,7 @@ protected void initializeOmTables(CacheType cacheType,
compactionLogTable = initializer.get(OMDBDefinition.COMPACTION_LOG_TABLE_DEF);
- // sessionToken -> insertionTimeMillis
+ // originalAccessKeyId -> revocationTimeMillis
// FULL_CACHE keeps revocations in memory as there are not expected to be many
s3RevokedStsTokenTable = initializer.get(
OMDBDefinition.S3_REVOKED_STS_TOKEN_TABLE_DEF, cacheType);
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/codec/OMDBDefinition.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/codec/OMDBDefinition.java
index 2e99871e17c..08600bb9950 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/codec/OMDBDefinition.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/codec/OMDBDefinition.java
@@ -60,7 +60,7 @@
* | userTable | /user :- UserVolumeInfo |
* | dTokenTable | OzoneTokenID :- renew_time |
* | s3SecretTable | s3g_access_key_id :- s3Secret |
- * | s3RevokedStsTokenTable | sts_session_token :- insertionTimeMillis |
+ * | s3RevokedStsTokenTable | originalAccessKeyId :- revocationTimeMillis |
* |------------------------------------------------------------------------|
* }
*
@@ -169,7 +169,10 @@ public final class OMDBDefinition extends DBDefinition.WithMap {
S3SecretValue.getCodec());
public static final String S3_REVOKED_STS_TOKEN_TABLE = "s3RevokedStsTokenTable";
- /** s3RevokedStsTokenTable: sts_session_token :- insertionTimeMillis.*/
+ /**
+ * s3RevokedStsTokenTable: originalAccessKeyId :- revocationTimeMillis.
+ * The value is the revocation cutoff in milliseconds.
+ */
public static final DBColumnFamilyDefinition S3_REVOKED_STS_TOKEN_TABLE_DEF
= new DBColumnFamilyDefinition<>(S3_REVOKED_STS_TOKEN_TABLE,
StringCodec.get(),
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOmMetadataManager.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOmMetadataManager.java
index d0d11c5ba94..c284b460dd6 100644
--- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOmMetadataManager.java
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOmMetadataManager.java
@@ -1535,9 +1535,9 @@ public void testS3RevokedStsTokenTablePutAndGet() throws Exception {
assertNotNull(omMetadataManager.getS3RevokedStsTokenTable(), "s3RevokedStsTokenTable should be initialized");
final MockClock clock = MockClock.newInstance();
- final String sessionToken1 = "test-session-token-1";
+ final String originalAccessKeyId1 = "orig-1";
final long insertionTime1 = clock.millis();
- final String sessionToken2 = "test-session-token-2";
+ final String originalAccessKeyId2 = "orig-2";
final long insertionTime2 = insertionTime1 + 1234L;
// This table is configured as FULL_CACHE in OmMetadataManagerImpl.
@@ -1546,25 +1546,25 @@ public void testS3RevokedStsTokenTablePutAndGet() throws Exception {
final TypedTable revokedTable =
(TypedTable) omMetadataManager.getS3RevokedStsTokenTable();
- revokedTable.put(sessionToken1, insertionTime1);
- revokedTable.put(sessionToken2, insertionTime2);
+ revokedTable.put(originalAccessKeyId1, insertionTime1);
+ revokedTable.put(originalAccessKeyId2, insertionTime2);
// Verify the values are persisted in RocksDB.
- assertEquals(insertionTime1, revokedTable.getSkipCache(sessionToken1));
- assertEquals(insertionTime2, revokedTable.getSkipCache(sessionToken2));
+ assertEquals(insertionTime1, revokedTable.getSkipCache(originalAccessKeyId1));
+ assertEquals(insertionTime2, revokedTable.getSkipCache(originalAccessKeyId2));
// Update cache to make get/getIfExist reflect the write for FULL_CACHE tables.
- revokedTable.addCacheEntry(sessionToken1, insertionTime1, 1L);
- revokedTable.addCacheEntry(sessionToken2, insertionTime2, 1L);
+ revokedTable.addCacheEntry(originalAccessKeyId1, insertionTime1, 1L);
+ revokedTable.addCacheEntry(originalAccessKeyId2, insertionTime2, 1L);
// Verify get and getIfExist return the stored value
- assertEquals(insertionTime1, revokedTable.get(sessionToken1));
- assertEquals(insertionTime1, revokedTable.getIfExist(sessionToken1));
- assertEquals(insertionTime2, revokedTable.get(sessionToken2));
- assertEquals(insertionTime2, revokedTable.getIfExist(sessionToken2));
+ assertEquals(insertionTime1, revokedTable.get(originalAccessKeyId1));
+ assertEquals(insertionTime1, revokedTable.getIfExist(originalAccessKeyId1));
+ assertEquals(insertionTime2, revokedTable.get(originalAccessKeyId2));
+ assertEquals(insertionTime2, revokedTable.getIfExist(originalAccessKeyId2));
- // Invalid sessionToken should return null for getIfExist
- assertNull(revokedTable.getIfExist("INVALID_SESSION_TOKEN"));
+ // Invalid originalAccessKeyId should return null for getIfExist.
+ assertNull(revokedTable.getIfExist("INVALID_ORIGINAL_ACCESS_KEY_ID"));
}
@Test
From 8b29e9f5fde33d7b93c8e85554d0d736721b6ba1 Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Sun, 16 Aug 2026 16:31:05 -0700
Subject: [PATCH 04/20] redesign: update ozone cli
---
.../ozone/shell/s3/RevokeSTSTokenHandler.java | 22 +++++++++----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/hadoop-ozone/cli-shell/src/main/java/org/apache/hadoop/ozone/shell/s3/RevokeSTSTokenHandler.java b/hadoop-ozone/cli-shell/src/main/java/org/apache/hadoop/ozone/shell/s3/RevokeSTSTokenHandler.java
index 274304217f8..9cd715d581a 100644
--- a/hadoop-ozone/cli-shell/src/main/java/org/apache/hadoop/ozone/shell/s3/RevokeSTSTokenHandler.java
+++ b/hadoop-ozone/cli-shell/src/main/java/org/apache/hadoop/ozone/shell/s3/RevokeSTSTokenHandler.java
@@ -29,18 +29,18 @@
/**
* Executes revocation of STS tokens.
*
- * This command marks the specified STS token as revoked by adding it to the OM's revoked STS token table.
- * Subsequent S3 requests using the same session token will be rejected once the revocation
- * state has propagated.
+ * This command records a revocation cutoff for the given original access key ID in the OM's
+ * revoked STS token table. Subsequent S3 requests using STS tokens created before that cutoff
+ * will be rejected once the revocation state has propagated.
*/
@Command(name = "revokeststoken",
- description = "Revoke S3 STS token for the given session token")
+ description = "Revoke S3 STS tokens for the given original access key ID")
public class RevokeSTSTokenHandler extends S3Handler {
- @Option(names = "-t",
+ @Option(names = {"-o", "--original-access-key-id"},
required = true,
- description = "STS session token")
- private String sessionToken;
+ description = "Original long-lived access key ID whose STS tokens should be revoked")
+ private String originalAccessKeyId;
@Option(names = "-y",
description = "Continue without interactive user confirmation")
@@ -56,8 +56,8 @@ protected void execute(OzoneClient client, OzoneAddress address)
throws IOException {
if (!yes) {
- out().print("Enter 'y' to confirm STS token revocation for sessionToken '" +
- sessionToken + "': ");
+ out().print(
+ "Enter 'y' to confirm STS token revocation for originalAccessKeyId '" + originalAccessKeyId + "': ");
out().flush();
final Scanner scanner = new Scanner(new InputStreamReader(System.in, StandardCharsets.UTF_8));
final String confirmation = scanner.next().trim().toLowerCase();
@@ -67,7 +67,7 @@ protected void execute(OzoneClient client, OzoneAddress address)
}
}
- client.getObjectStore().revokeSTSToken(sessionToken);
- out().println("STS token revoked for sessionToken '" + sessionToken + "'.");
+ client.getObjectStore().revokeSTSToken(originalAccessKeyId);
+ out().println("STS tokens revoked for originalAccessKeyId '" + originalAccessKeyId + "'.");
}
}
From cff74bce9e0e0f8cc4928e30fb53cbcbe23fd47a Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Sun, 16 Aug 2026 16:37:22 -0700
Subject: [PATCH 05/20] redesign: update main revocation logic
---
.../org/apache/hadoop/ozone/OzoneConsts.java | 2 +
.../src/main/resources/ozone-default.xml | 7 +-
.../hadoop/ozone/om/helpers/S3STSUtils.java | 6 +
.../om/ratis/OzoneManagerStateMachine.java | 24 +-
.../ozone/om/request/OMClientRequest.java | 4 +-
.../s3/security/S3AssumeRoleRequest.java | 19 +-
.../S3DeleteRevokedSTSTokensRequest.java | 5 +-
.../s3/security/S3RevokeSTSTokenRequest.java | 109 ++++--
.../S3DeleteRevokedSTSTokensResponse.java | 12 +-
.../s3/security/S3RevokeSTSTokenResponse.java | 19 +-
.../RevokedSTSTokenCleanupService.java | 50 +--
.../hadoop/ozone/security/S3SecurityUtil.java | 15 +-
.../ozone/security/STSSecurityUtil.java | 5 +-
.../ozone/security/STSTokenIdentifier.java | 161 +++++++-
.../ozone/security/STSTokenSecretManager.java | 15 +-
.../security/TestS3RevokeSTSTokenRequest.java | 329 +++++++++-------
.../TestRevokedSTSTokenCleanupService.java | 65 ++--
.../ozone/security/TestS3SecurityUtil.java | 54 ++-
.../ozone/security/TestSTSSecurityUtil.java | 50 ++-
.../security/TestSTSTokenEncryption.java | 18 +-
.../security/TestSTSTokenIdentifier.java | 356 +++++++++++++-----
.../security/TestSTSTokenSecretManager.java | 1 +
.../ozone/s3/endpoint/EndpointBase.java | 8 +
.../hadoop/ozone/s3/util/AuditUtils.java | 23 ++
.../ozone/s3/endpoint/TestEndpointBase.java | 57 +++
.../hadoop/ozone/s3/util/TestAuditUtils.java | 62 +++
26 files changed, 1064 insertions(+), 412 deletions(-)
create mode 100644 hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestAuditUtils.java
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneConsts.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneConsts.java
index e7e78b81206..d2ebc831e4b 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneConsts.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneConsts.java
@@ -314,6 +314,8 @@ public final class OzoneConsts {
public static final String S3_SETSECRET_USER = "S3SetSecretUser";
public static final String S3_REVOKESECRET_USER = "S3RevokeSecretUser";
public static final String S3_REVOKESTSTOKEN_USER = "S3RevokeSTSTokenUser";
+ public static final String S3_STS_ORIGINAL_ACCESS_KEY_ID = "originalAccessKeyId";
+ public static final String S3_STS_TEMP_ACCESS_KEY_ID = "tempAccessKeyId";
public static final String RENAMED_KEYS_MAP = "renamedKeysMap";
public static final String UNRENAMED_KEYS_MAP = "unRenamedKeysMap";
public static final String MULTIPART_UPLOAD_PART_NUMBER = "partNumber";
diff --git a/hadoop-hdds/common/src/main/resources/ozone-default.xml b/hadoop-hdds/common/src/main/resources/ozone-default.xml
index c72c351402e..db79aa505f0 100644
--- a/hadoop-hdds/common/src/main/resources/ozone-default.xml
+++ b/hadoop-hdds/common/src/main/resources/ozone-default.xml
@@ -5254,9 +5254,10 @@
3h
OZONE, OM, PERFORMANCE, SECURITY
- A background job that periodically checks revoked STS token entries and
- deletes ones that have existed for 12 hours. This entry controls the interval of this
- cleanup check. Unit could be defined with postfix (ns,ms,s,m,h,d).
+ A background service that periodically scans the s3RevokedStsTokenTable and deletes
+ revocation entries whose cutoff is older than the maximum STS token lifetime (12 hours).
+ This property controls how often the cleanup service runs. Unit could be defined with
+ postfix (ns,ms,s,m,h,d).
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/S3STSUtils.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/S3STSUtils.java
index 763c8fe9bfa..64221089655 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/S3STSUtils.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/S3STSUtils.java
@@ -40,6 +40,12 @@ public final class S3STSUtils {
// AWS limit for session policy is 2048 characters
public static final int MAX_SESSION_POLICY_LENGTH = 2048;
+ public static final String STS_TOKEN_PREFIX = "ASIA";
+ public static final String STS_ACCESS_KEY_ID_ALLOWED_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ public static final int STS_ACCESS_KEY_ID_ALLOWED_CHARS_LENGTH = STS_ACCESS_KEY_ID_ALLOWED_CHARS.length();
+ public static final int STS_ACCESS_KEY_ID_RANDOM_LENGTH = 20;
+ public static final int STS_ACCESS_KEY_ID_LENGTH = STS_TOKEN_PREFIX.length() + STS_ACCESS_KEY_ID_RANDOM_LENGTH;
+
private S3STSUtils() {
}
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/OzoneManagerStateMachine.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/OzoneManagerStateMachine.java
index 3db85f50805..a5e4154b43d 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/OzoneManagerStateMachine.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/OzoneManagerStateMachine.java
@@ -25,6 +25,7 @@
import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.io.IOException;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
@@ -683,15 +684,22 @@ OMResponse runCommand(OMRequest request, TermIndex termIndex) {
if (s3Auth.hasSessionToken() && !s3Auth.getSessionToken().isEmpty()) {
// ThreadLocal carries session policy for OmMetadataReader
+ // Use Instant.MAX for creationTime so a future revocation check on this ThreadLocal
+ // identifier never treats the token as issued before a stored cutoff.
final STSTokenIdentifier rehydratedTokenIdentifier = new STSTokenIdentifier(
- s3Auth.hasResolvedStsTempAccessKeyId() ? s3Auth.getResolvedStsTempAccessKeyId() : "",
- s3Auth.hasResolvedStsOriginalAccessKeyId() ? s3Auth.getResolvedStsOriginalAccessKeyId() : "",
- s3Auth.hasResolvedStsRoleArn() ? s3Auth.getResolvedStsRoleArn() : "",
- java.time.Instant.MAX, // ensure it deterministically is not expired
- "", // no secretAccessKey needed
- s3Auth.hasResolvedStsSessionPolicy() ? s3Auth.getResolvedStsSessionPolicy() : "",
- null // no encryption key needed
- );
+ STSTokenIdentifier.Params.newBuilder()
+ .setTempAccessKeyId(
+ s3Auth.hasResolvedStsTempAccessKeyId() ? s3Auth.getResolvedStsTempAccessKeyId() : "")
+ .setOriginalAccessKeyId(
+ s3Auth.hasResolvedStsOriginalAccessKeyId() ? s3Auth.getResolvedStsOriginalAccessKeyId() : "")
+ .setRoleArn(s3Auth.hasResolvedStsRoleArn() ? s3Auth.getResolvedStsRoleArn() : "")
+ .setCreationTime(Instant.MAX)
+ .setExpiry(Instant.MAX) // ensure it deterministically is not expired
+ .setSecretAccessKey("") // no secretAccessKey needed
+ .setSessionPolicy(
+ s3Auth.hasResolvedStsSessionPolicy() ? s3Auth.getResolvedStsSessionPolicy() : "")
+ .setEncryptionKey(null) // no encryption key needed
+ .build());
OzoneManager.setStsTokenIdentifier(rehydratedTokenIdentifier);
isStsThreadLocalSet = true;
}
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java
index 6b9c6698cf9..29abe0eb8ee 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java
@@ -46,10 +46,10 @@
import org.apache.hadoop.ozone.om.helpers.BucketLayout;
import org.apache.hadoop.ozone.om.helpers.OMAuditLogger;
import org.apache.hadoop.ozone.om.helpers.OzoneFSUtils;
+import org.apache.hadoop.ozone.om.helpers.S3STSUtils;
import org.apache.hadoop.ozone.om.lock.OMLockDetails;
import org.apache.hadoop.ozone.om.protocolPB.grpc.GrpcClientConstants;
import org.apache.hadoop.ozone.om.ratis.utils.OzoneManagerRatisUtils;
-import org.apache.hadoop.ozone.om.request.s3.security.S3AssumeRoleRequest;
import org.apache.hadoop.ozone.om.response.OMClientResponse;
import org.apache.hadoop.ozone.om.upgrade.OMLayoutVersionManager;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
@@ -226,7 +226,7 @@ public OzoneManagerProtocolProtos.UserInfo getUserInfo() throws IOException {
// falling back to accessId if session token not present.
if (omRequest.hasS3Authentication()) {
final String accessKeyId = omRequest.getS3Authentication().getAccessId();
- if (accessKeyId.startsWith(S3AssumeRoleRequest.STS_TOKEN_PREFIX) &&
+ if (accessKeyId.startsWith(S3STSUtils.STS_TOKEN_PREFIX) &&
!omRequest.getS3Authentication().hasSessionToken()) {
throw new IOException("Error with STS token", new AuthenticationException(
"Missing session token for accessKeyId: " + accessKeyId));
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3AssumeRoleRequest.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3AssumeRoleRequest.java
index 4efd18b4b32..b6d650cc439 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3AssumeRoleRequest.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3AssumeRoleRequest.java
@@ -17,6 +17,10 @@
package org.apache.hadoop.ozone.om.request.s3.security;
+import static org.apache.hadoop.ozone.om.helpers.S3STSUtils.STS_ACCESS_KEY_ID_ALLOWED_CHARS;
+import static org.apache.hadoop.ozone.om.helpers.S3STSUtils.STS_ACCESS_KEY_ID_ALLOWED_CHARS_LENGTH;
+import static org.apache.hadoop.ozone.om.helpers.S3STSUtils.STS_ACCESS_KEY_ID_RANDOM_LENGTH;
+import static org.apache.hadoop.ozone.om.helpers.S3STSUtils.STS_TOKEN_PREFIX;
import static org.apache.hadoop.ozone.security.acl.AssumeRoleRequest.OzoneGrant;
import com.google.common.annotations.VisibleForTesting;
@@ -31,6 +35,7 @@
import java.util.Set;
import org.apache.hadoop.hdds.scm.client.HddsClientUtils;
import org.apache.hadoop.ipc_.ProtobufRpcEngine;
+import org.apache.hadoop.ozone.OzoneConsts;
import org.apache.hadoop.ozone.audit.AuditLogger;
import org.apache.hadoop.ozone.audit.OMAction;
import org.apache.hadoop.ozone.om.OzoneAclUtils;
@@ -70,16 +75,12 @@ public class S3AssumeRoleRequest extends OMClientRequest {
SECURE_RANDOM = secureRandom;
}
- private static final int STS_ACCESS_KEY_ID_LENGTH = 20;
private static final int STS_SECRET_ACCESS_KEY_LENGTH = 40;
private static final int STS_ROLE_ID_LENGTH = 16;
private static final String ASSUME_ROLE_ID_PREFIX = "AROA";
- private static final String CHARS_FOR_ACCESS_KEY_IDS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
- private static final int CHARS_FOR_ACCESS_KEY_IDS_LENGTH = CHARS_FOR_ACCESS_KEY_IDS.length();
- private static final String CHARS_FOR_SECRET_ACCESS_KEYS = CHARS_FOR_ACCESS_KEY_IDS +
+ private static final String CHARS_FOR_SECRET_ACCESS_KEYS = STS_ACCESS_KEY_ID_ALLOWED_CHARS +
"abcdefghijklmnopqrstuvwxyz/+";
private static final int CHARS_FOR_SECRET_ACCESS_KEYS_LENGTH = CHARS_FOR_SECRET_ACCESS_KEYS.length();
- public static final String STS_TOKEN_PREFIX = "ASIA";
private final Clock clock;
@@ -103,11 +104,13 @@ public OMRequest preExecute(OzoneManager ozoneManager) throws IOException {
// Generate temporary AWS credentials using cryptographically strong SecureRandom
final String tempAccessKeyId = STS_TOKEN_PREFIX + generateSecureRandomStringUsingChars(
- CHARS_FOR_ACCESS_KEY_IDS, CHARS_FOR_ACCESS_KEY_IDS_LENGTH, STS_ACCESS_KEY_ID_LENGTH);
+ STS_ACCESS_KEY_ID_ALLOWED_CHARS, STS_ACCESS_KEY_ID_ALLOWED_CHARS_LENGTH,
+ STS_ACCESS_KEY_ID_RANDOM_LENGTH);
final String secretAccessKey = generateSecureRandomStringUsingChars(
CHARS_FOR_SECRET_ACCESS_KEYS, CHARS_FOR_SECRET_ACCESS_KEYS_LENGTH, STS_SECRET_ACCESS_KEY_LENGTH);
final String roleId = ASSUME_ROLE_ID_PREFIX + generateSecureRandomStringUsingChars(
- CHARS_FOR_ACCESS_KEY_IDS, CHARS_FOR_ACCESS_KEY_IDS_LENGTH, STS_ROLE_ID_LENGTH);
+ STS_ACCESS_KEY_ID_ALLOWED_CHARS, STS_ACCESS_KEY_ID_ALLOWED_CHARS_LENGTH,
+ STS_ROLE_ID_LENGTH);
// Build UpdateAssumeRoleRequest with leader-generated credentials
final UpdateAssumeRoleRequest.Builder updateAssumeRoleRequestBuilder =
@@ -182,7 +185,7 @@ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, Execut
final long expirationEpochSeconds = clock.instant().plusSeconds(durationSeconds).getEpochSecond();
// Add tempAccessKeyId to the log so it can be determined which permanent user created the tempAccessKeyId
- auditMap.put("tempAccessKeyId", tempAccessKeyId);
+ auditMap.put(OzoneConsts.S3_STS_TEMP_ACCESS_KEY_ID, tempAccessKeyId);
final AssumeRoleResponse.Builder responseBuilder = AssumeRoleResponse.newBuilder()
.setAccessKeyId(tempAccessKeyId)
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3DeleteRevokedSTSTokensRequest.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3DeleteRevokedSTSTokensRequest.java
index f41b20353a8..81558ec5850 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3DeleteRevokedSTSTokensRequest.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3DeleteRevokedSTSTokensRequest.java
@@ -35,6 +35,7 @@
/**
* Handles DeleteRevokedSTSTokens requests submitted by {@link RevokedSTSTokenCleanupService}.
+ * Each request contains originalAccessKeyIds to remove from the revocation table.
*/
public class S3DeleteRevokedSTSTokensRequest extends OMClientRequest {
@@ -62,8 +63,8 @@ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, Execut
final DeleteRevokedSTSTokensRequest request = getOmRequest().getDeleteRevokedSTSTokensRequest();
final OMResponse.Builder omResponse = OmResponseUtil.getOMResponseBuilder(getOmRequest());
- final List sessionTokens = request.getSessionTokenList();
- return new S3DeleteRevokedSTSTokensResponse(sessionTokens, omResponse.build());
+ final List originalAccessKeyIds = request.getOriginalAccessKeyIdList();
+ return new S3DeleteRevokedSTSTokensResponse(originalAccessKeyIds, omResponse.build());
}
}
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java
index 52a92d8a556..5ce42618e2a 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java
@@ -17,16 +17,21 @@
package org.apache.hadoop.ozone.om.request.s3.security;
+import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INTERNAL_ERROR;
+import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_REQUEST;
+
import java.io.IOException;
import java.time.Clock;
import java.time.ZoneOffset;
import java.util.HashMap;
import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.hdds.utils.db.cache.CacheKey;
import org.apache.hadoop.hdds.utils.db.cache.CacheValue;
import org.apache.hadoop.ozone.OzoneConsts;
import org.apache.hadoop.ozone.audit.OMAction;
import org.apache.hadoop.ozone.om.OzoneManager;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.execution.flowcontrol.ExecutionContext;
import org.apache.hadoop.ozone.om.request.OMClientRequest;
import org.apache.hadoop.ozone.om.request.util.OmResponseUtil;
@@ -35,8 +40,8 @@
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
-import org.apache.hadoop.ozone.security.STSSecurityUtil;
-import org.apache.hadoop.ozone.security.STSTokenIdentifier;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.RevokeSTSTokenRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.UpdateRevokeSTSTokenRequest;
import org.apache.hadoop.security.UserGroupInformation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -44,10 +49,14 @@
/**
* Handles S3RevokeSTSTokenRequest request.
*
- * This request marks an STS session token as revoked by inserting
- * it into the {@code s3RevokedStsTokenTable}. Subsequent S3 requests
- * authenticated with the same STS session token will be rejected when the
- * revocation state has propagated.
+ * The client submits {@link RevokeSTSTokenRequest} with {@code originalAccessKeyId} only. On the
+ * leader, {@code preExecute} captures the revocation cutoff and builds an {@link UpdateRevokeSTSTokenRequest}
+ * that is replicated through Ratis so every OM applies the same cutoff.
+ *
+ * This request records a revocation cutoff for the given {@code originalAccessKeyId} in the
+ * {@code s3RevokedStsTokenTable}. Subsequent S3 requests authenticated with STS tokens whose
+ * {@code creationTime} is strictly before the cutoff will be rejected when the revocation state
+ * has propagated.
*/
public class S3RevokeSTSTokenRequest extends OMClientRequest {
@@ -61,48 +70,94 @@ public S3RevokeSTSTokenRequest(OMRequest omRequest) {
@Override
public OMRequest preExecute(OzoneManager ozoneManager) throws IOException {
final OMRequest omRequest = super.preExecute(ozoneManager);
- final OzoneManagerProtocolProtos.RevokeSTSTokenRequest revokeReq =
- omRequest.getRevokeSTSTokenRequest();
+ final RevokeSTSTokenRequest revokeReq = omRequest.getRevokeSTSTokenRequest();
+ validateRevokeRequestFields(revokeReq);
- // Get the original (long-lived) access key id from the session token
- // and enforce the same permission model that is used for S3 secret
+ // Use the original (long-lived) access key ID from the request and enforce
+ // the same permission model that is used for S3 secret
// operations (get/set/revoke). Only the owner of the original access
// key (i.e. the creator of the STS token) or an S3 / tenant admin is allowed
// to revoke its temporary STS credentials.
- final String sessionToken = revokeReq.getSessionToken();
- final STSTokenIdentifier stsTokenIdentifier = STSSecurityUtil.constructValidateAndDecryptSTSToken(
- sessionToken, ozoneManager.getSecretKeyClient(), CLOCK);
- final String originalAccessKeyId = stsTokenIdentifier.getOriginalAccessKeyId();
+ final String originalAccessKeyId = revokeReq.getOriginalAccessKeyId();
final UserGroupInformation ugi = S3SecretRequestHelper.getOrCreateUgi(originalAccessKeyId);
S3SecretRequestHelper.checkAccessIdSecretOpPermission(ozoneManager, ugi, originalAccessKeyId);
- return omRequest;
+ if (!ozoneManager.getS3SecretManager().hasS3Secret(originalAccessKeyId)) {
+ throw new OMException("originalAccessKeyId does not exist: " + originalAccessKeyId, INVALID_REQUEST);
+ }
+
+ final long revocationTimeMillis = CLOCK.millis();
+ final UpdateRevokeSTSTokenRequest updateRevokeSTSTokenRequest = UpdateRevokeSTSTokenRequest.newBuilder()
+ .setOriginalAccessKeyId(originalAccessKeyId)
+ .setRevocationTimeMillis(revocationTimeMillis)
+ .build();
+
+ return omRequest.toBuilder()
+ .setUpdateRevokeSTSTokenRequest(updateRevokeSTSTokenRequest)
+ .build();
}
@Override
public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, ExecutionContext context) {
final OMResponse.Builder omResponse = OmResponseUtil.getOMResponseBuilder(getOmRequest());
+ IOException exception = null;
+ OMClientResponse omClientResponse;
+ String originalAccessKeyId = null;
+
+ try {
+ validateReplicatedRevokeRequestFields(getOmRequest());
+ final UpdateRevokeSTSTokenRequest updateRevokeSTSTokenRequest = getOmRequest().getUpdateRevokeSTSTokenRequest();
+ originalAccessKeyId = updateRevokeSTSTokenRequest.getOriginalAccessKeyId();
+ final long revocationTimeMillis = updateRevokeSTSTokenRequest.getRevocationTimeMillis();
- final OzoneManagerProtocolProtos.RevokeSTSTokenRequest revokeReq = getOmRequest().getRevokeSTSTokenRequest();
- final String sessionToken = revokeReq.getSessionToken();
+ // All actual DB mutations are done in the response's addToDBBatch().
+ omClientResponse = new S3RevokeSTSTokenResponse(originalAccessKeyId, revocationTimeMillis, omResponse.build());
- // All actual DB mutations are done in the response's addToDBBatch().
- final OMClientResponse omClientResponse = new S3RevokeSTSTokenResponse(
- sessionToken, omResponse.build());
+ // Update the cache immediately so subsequent validation checks see the revocation
+ ozoneManager.getMetadataManager().getS3RevokedStsTokenTable().addCacheEntry(
+ new CacheKey<>(originalAccessKeyId), CacheValue.get(context.getIndex(), revocationTimeMillis));
+
+ LOG.info(
+ "Marked STS tokens as revoked for originalAccessKeyId={} with cutoff time {}.",
+ originalAccessKeyId, revocationTimeMillis);
+ } catch (IOException ex) {
+ exception = ex;
+ omClientResponse = new S3RevokeSTSTokenResponse(null, 0L, createErrorOMResponse(omResponse, ex));
+ }
// Audit log
final Map auditMap = new HashMap<>();
final OzoneManagerProtocolProtos.UserInfo userInfo = getOmRequest().getUserInfo();
auditMap.put(OzoneConsts.S3_REVOKESTSTOKEN_USER, userInfo.getUserName());
- markForAudit(ozoneManager.getAuditLogger(), buildAuditMessage(
- OMAction.REVOKE_STS_TOKEN, auditMap, null, userInfo));
+ if (originalAccessKeyId != null) {
+ auditMap.put(OzoneConsts.S3_STS_ORIGINAL_ACCESS_KEY_ID, originalAccessKeyId);
+ }
+ markForAudit(
+ ozoneManager.getAuditLogger(), buildAuditMessage(OMAction.REVOKE_STS_TOKEN, auditMap, exception, userInfo));
+ return omClientResponse;
+ }
- // Update the cache immediately so subsequent validation checks see the revocation
- ozoneManager.getMetadataManager().getS3RevokedStsTokenTable().addCacheEntry(
- new CacheKey<>(sessionToken), CacheValue.get(context.getIndex(), CLOCK.millis()));
+ private static void validateRevokeRequestFields(RevokeSTSTokenRequest revokeReq) throws OMException {
+ final String originalAccessKeyId = revokeReq.getOriginalAccessKeyId();
+ if (StringUtils.isEmpty(originalAccessKeyId)) {
+ throw new OMException("originalAccessKeyId is required for STS token revocation", INVALID_REQUEST);
+ }
+ if (originalAccessKeyId.length() >= OzoneConsts.OZONE_MAXIMUM_ACCESS_ID_LENGTH) {
+ throw new OMException("originalAccessKeyId length is invalid: " + originalAccessKeyId.length(), INVALID_REQUEST);
+ }
+ }
- LOG.info("Marked STS session token '{}' as revoked.", sessionToken);
- return omClientResponse;
+ private static void validateReplicatedRevokeRequestFields(OMRequest omRequest) throws OMException {
+ if (!omRequest.hasUpdateRevokeSTSTokenRequest()) {
+ throw new OMException("updateRevokeSTSTokenRequest is required for STS token revocation", INTERNAL_ERROR);
+ }
+ final String originalAccessKeyId = omRequest.getRevokeSTSTokenRequest().getOriginalAccessKeyId();
+ final UpdateRevokeSTSTokenRequest updateRevokeSTSTokenRequest = omRequest.getUpdateRevokeSTSTokenRequest();
+ if (!originalAccessKeyId.equals(updateRevokeSTSTokenRequest.getOriginalAccessKeyId())) {
+ throw new OMException(
+ "originalAccessKeyId mismatch between revokeSTSTokenRequest and updateRevokeSTSTokenRequest",
+ INTERNAL_ERROR);
+ }
}
}
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/security/S3DeleteRevokedSTSTokensResponse.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/security/S3DeleteRevokedSTSTokensResponse.java
index cb44e7f466d..a1b255689de 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/security/S3DeleteRevokedSTSTokensResponse.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/security/S3DeleteRevokedSTSTokensResponse.java
@@ -36,16 +36,16 @@
@CleanupTableInfo(cleanupTables = {S3_REVOKED_STS_TOKEN_TABLE})
public class S3DeleteRevokedSTSTokensResponse extends OMClientResponse {
- private final List sessionTokens;
+ private final List originalAccessKeyIds;
- public S3DeleteRevokedSTSTokensResponse(List sessionTokens, @Nonnull OMResponse omResponse) {
+ public S3DeleteRevokedSTSTokensResponse(List originalAccessKeyIds, @Nonnull OMResponse omResponse) {
super(omResponse);
- this.sessionTokens = sessionTokens;
+ this.originalAccessKeyIds = originalAccessKeyIds;
}
@Override
public void addToDBBatch(OMMetadataManager omMetadataManager, BatchOperation batchOperation) throws IOException {
- if (sessionTokens == null || sessionTokens.isEmpty()) {
+ if (originalAccessKeyIds == null || originalAccessKeyIds.isEmpty()) {
return;
}
if (!getOMResponse().hasStatus() || getOMResponse().getStatus() != OK) {
@@ -57,8 +57,8 @@ public void addToDBBatch(OMMetadataManager omMetadataManager, BatchOperation bat
return;
}
- for (String sessionToken : sessionTokens) {
- table.deleteWithBatch(batchOperation, sessionToken);
+ for (String originalAccessKeyId : originalAccessKeyIds) {
+ table.deleteWithBatch(batchOperation, originalAccessKeyId);
}
}
}
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/security/S3RevokeSTSTokenResponse.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/security/S3RevokeSTSTokenResponse.java
index 5b1a8cf3b01..db9233357ed 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/security/S3RevokeSTSTokenResponse.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/security/S3RevokeSTSTokenResponse.java
@@ -22,8 +22,6 @@
import jakarta.annotation.Nonnull;
import java.io.IOException;
-import java.time.Clock;
-import java.time.ZoneOffset;
import org.apache.hadoop.hdds.utils.db.BatchOperation;
import org.apache.hadoop.hdds.utils.db.Table;
import org.apache.hadoop.ozone.om.OMMetadataManager;
@@ -37,22 +35,23 @@
@CleanupTableInfo(cleanupTables = {S3_REVOKED_STS_TOKEN_TABLE})
public class S3RevokeSTSTokenResponse extends OMClientResponse {
- private static final Clock CLOCK = Clock.system(ZoneOffset.UTC);
+ private final String originalAccessKeyId;
+ private final long revocationTimeMillis;
- private final String sessionToken;
-
- public S3RevokeSTSTokenResponse(String sessionToken, @Nonnull OMResponse omResponse) {
+ public S3RevokeSTSTokenResponse(String originalAccessKeyId, long revocationTimeMillis,
+ @Nonnull OMResponse omResponse) {
super(omResponse);
- this.sessionToken = sessionToken;
+ this.originalAccessKeyId = originalAccessKeyId;
+ this.revocationTimeMillis = revocationTimeMillis;
}
@Override
public void addToDBBatch(OMMetadataManager omMetadataManager, BatchOperation batchOperation) throws IOException {
- if (sessionToken != null && getOMResponse().hasStatus() && getOMResponse().getStatus() == OK) {
+ if (originalAccessKeyId != null && getOMResponse().hasStatus() && getOMResponse().getStatus() == OK) {
final Table table = omMetadataManager.getS3RevokedStsTokenTable();
if (table != null) {
- // Store insertionTimeMillis as value
- table.putWithBatch(batchOperation, sessionToken, CLOCK.millis());
+ // Store revocationTimeMillis as value
+ table.putWithBatch(batchOperation, originalAccessKeyId, revocationTimeMillis);
}
}
}
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/RevokedSTSTokenCleanupService.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/RevokedSTSTokenCleanupService.java
index 3d9668d6469..c627f6a21cb 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/RevokedSTSTokenCleanupService.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/RevokedSTSTokenCleanupService.java
@@ -37,6 +37,7 @@
import org.apache.hadoop.ozone.om.OMConfigKeys;
import org.apache.hadoop.ozone.om.OMMetadataManager;
import org.apache.hadoop.ozone.om.OzoneManager;
+import org.apache.hadoop.ozone.om.helpers.S3STSUtils;
import org.apache.hadoop.ozone.om.ratis.utils.OzoneManagerRatisUtils;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.DeleteRevokedSTSTokensRequest;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
@@ -57,7 +58,8 @@ public class RevokedSTSTokenCleanupService extends BackgroundService {
// Use a single thread
private static final int REVOKED_STS_TOKEN_CLEANER_CORE_POOL_SIZE = 1;
private static final Clock CLOCK = Clock.system(ZoneOffset.UTC);
- private static final long CLEANUP_THRESHOLD = 12 * 60 * 60 * 1000L; // 12 hours in milliseconds
+ // Keep revocation entries until max STS token lifetime after the cutoff was captured.
+ private static final long CLEANUP_THRESHOLD = TimeUnit.SECONDS.toMillis(S3STSUtils.MAX_DURATION_SECONDS); // 12 hours
private final OzoneManager ozoneManager;
private final OMMetadataManager metadataManager;
@@ -124,7 +126,7 @@ private boolean shouldRun() {
return !suspended.get() && ozoneManager.isLeaderReady();
}
- private class RevokedSTSTokenCleanupTask implements BackgroundTask {
+ private final class RevokedSTSTokenCleanupTask implements BackgroundTask {
@Override
public BackgroundTaskResult call() throws Exception {
@@ -143,17 +145,17 @@ public BackgroundTaskResult call() throws Exception {
iterator.seekToFirst();
while (iterator.hasNext()) {
final Table.KeyValue entry = iterator.next();
- final String sessionToken = entry.getKey();
- final Long initialCreationTimeMillis = entry.getValue();
+ final String originalAccessKeyId = entry.getKey();
+ final Long revocationTimeMillis = entry.getValue();
- if (shouldCleanup(initialCreationTimeMillis)) {
- // Calculate the size this token would add to the protobuf message.
+ if (shouldCleanup(revocationTimeMillis)) {
+ // Calculate the size this originalAccessKeyId would add to the protobuf message.
// Make a copy of the batch to do the size check
final List batchCopyWithCandidate = new ArrayList<>(batch);
- batchCopyWithCandidate.add(sessionToken);
+ batchCopyWithCandidate.add(originalAccessKeyId);
int batchWithCandidateSize = getBatchSerializedSize(batchCopyWithCandidate);
- // If adding this token would exceed the limit, submit the current batch
+ // If adding this originalAccessKeyId would exceed the limit, submit the current batch
if (batchWithCandidateSize > ratisByteLimit) {
if (!batch.isEmpty()) {
if (submitCleanupRequest(batch)) {
@@ -163,22 +165,22 @@ public BackgroundTaskResult call() throws Exception {
}
batch.clear();
- // Re-calculate the size of the candidate token alone in an empty batch
+ // Re-calculate the size of the candidate key alone in an empty batch
// to check if it exceeds the limit by itself.
final List singleCandidateBatch = new ArrayList<>();
- singleCandidateBatch.add(sessionToken);
+ singleCandidateBatch.add(originalAccessKeyId);
batchWithCandidateSize = getBatchSerializedSize(singleCandidateBatch);
}
- // Check if the single token exceeds the limit (either strictly single or after flush)
+ // Check if the single key exceeds the limit (either strictly single or after flush)
if (batchWithCandidateSize > ratisByteLimit) {
LOG.error(
- "Single revoked STS Token size ({}) would exceed the ratisByteLimit ({}). SessionToken " +
- "initialCreationTimeMillis: {}", batchWithCandidateSize, ratisByteLimit, initialCreationTimeMillis);
+ "Single originalAccessKeyId entry size ({}) would exceed the ratisByteLimit ({}). " +
+ "revocationTimeMillis: {}", batchWithCandidateSize, ratisByteLimit, revocationTimeMillis);
continue;
}
}
- batch.add(sessionToken);
+ batch.add(originalAccessKeyId);
}
}
} catch (IOException e) {
@@ -213,16 +215,16 @@ public BackgroundTaskResult call() throws Exception {
}
/**
- * Returns true if the given STS session token has been in the table past the cleanup threshold.
+ * Returns true if the revocation cutoff is older than the cleanup threshold.
*/
- private boolean shouldCleanup(long initialCreationTimeMillis) {
+ private boolean shouldCleanup(long revocationTimeMillis) {
final long now = CLOCK.millis();
- if (now - initialCreationTimeMillis > CLEANUP_THRESHOLD) {
+ if (now - revocationTimeMillis > CLEANUP_THRESHOLD) {
if (LOG.isDebugEnabled()) {
LOG.debug(
- "Revoked STS token entry created at {} is older than 12 hours, will clean up. Current time: {}",
- initialCreationTimeMillis, now);
+ "Revoked STS token cutoff at {} is older than {} ms, will clean up. Current time: {}",
+ revocationTimeMillis, CLEANUP_THRESHOLD, now);
}
return true;
}
@@ -230,11 +232,11 @@ private boolean shouldCleanup(long initialCreationTimeMillis) {
}
/**
- * Builds and submits an OMRequest to delete the provided revoked STS token(s).
+ * Builds and submits an OMRequest to delete the provided originalAccessKeyId revocation entries.
*/
- private boolean submitCleanupRequest(List sessionTokens) {
+ private boolean submitCleanupRequest(List originalAccessKeyIds) {
final DeleteRevokedSTSTokensRequest request = DeleteRevokedSTSTokensRequest.newBuilder()
- .addAllSessionToken(sessionTokens)
+ .addAllOriginalAccessKeyId(originalAccessKeyIds)
.build();
final OMRequest omRequest = OMRequest.newBuilder()
@@ -254,9 +256,9 @@ private boolean submitCleanupRequest(List sessionTokens) {
}
}
- private int getBatchSerializedSize(List sessionTokenBatch) {
+ private int getBatchSerializedSize(List originalAccessKeyIdBatch) {
final DeleteRevokedSTSTokensRequest request = DeleteRevokedSTSTokensRequest.newBuilder()
- .addAllSessionToken(sessionTokenBatch)
+ .addAllOriginalAccessKeyId(originalAccessKeyIdBatch)
.build();
return request.getSerializedSize();
diff --git 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
index 08ac1f2bee1..6612fff2bad 100644
--- 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
@@ -75,8 +75,10 @@ public static void validateS3Credential(OMRequest omRequest,
token, ozoneManager.getSecretKeyClient(), CLOCK);
// Ensure the token is not revoked
- if (isRevokedStsToken(token, ozoneManager)) {
- LOG.info("Session token has been revoked: {}, {}", stsTokenIdentifier.getTempAccessKeyId(), token);
+ if (isRevokedStsToken(stsTokenIdentifier, ozoneManager)) {
+ LOG.info(
+ "STS token has been revoked for originalAccessKeyId={}, tempAccessKeyId={}",
+ stsTokenIdentifier.getOriginalAccessKeyId(), stsTokenIdentifier.getTempAccessKeyId());
throw new OMException("STS token has been revoked", REVOKED_TOKEN);
}
@@ -157,11 +159,12 @@ private static void validateSTSTokenAwsSignature(STSTokenIdentifier stsTokenIden
}
/**
- * Returns true if the STS session token is present in the revoked STS token table.
+ * Returns true if the STS token was created before the revocation cutoff for its originalAccessKeyId.
*/
- private static boolean isRevokedStsToken(String sessionToken, OzoneManager ozoneManager)
+ private static boolean isRevokedStsToken(STSTokenIdentifier stsTokenIdentifier, OzoneManager ozoneManager)
throws OMException {
try {
+ final String originalAccessKeyId = stsTokenIdentifier.getOriginalAccessKeyId();
final OMMetadataManager metadataManager = ozoneManager.getMetadataManager();
if (metadataManager == null) {
final String msg = "Could not determine STS revocation: metadataManager is null";
@@ -176,7 +179,9 @@ private static boolean isRevokedStsToken(String sessionToken, OzoneManager ozone
throw new OMException(msg, INTERNAL_ERROR);
}
- return revokedStsTokenTable.getIfExist(sessionToken) != null;
+ final Long revocationTimeMillis = revokedStsTokenTable.getIfExist(originalAccessKeyId);
+ return revocationTimeMillis != null
+ && stsTokenIdentifier.getCreationTime().toEpochMilli() < revocationTimeMillis;
} catch (Exception e) {
final String msg = "Could not determine STS revocation because of Exception: " + e.getMessage();
LOG.warn(msg, e);
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSSecurityUtil.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSSecurityUtil.java
index 2212ad6db79..03a1fdba017 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSSecurityUtil.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSSecurityUtil.java
@@ -157,7 +157,7 @@ private static Token decodeTokenFromString(String encodedTok
try {
token.decodeFromUrlString(encodedToken);
return token;
- } catch (IOException e) {
+ } catch (IOException | RuntimeException e) {
throw new SecretManager.InvalidToken("Failed to decode STS token string: " + e);
}
}
@@ -180,6 +180,9 @@ static void ensureEssentialFieldsArePresentInToken(STSTokenIdentifier stsTokenId
if (StringUtils.isEmpty(stsTokenIdentifier.getSecretAccessKey())) {
throw new SecretManager.InvalidToken("Invalid STS token - secretAccessKey is null/empty");
}
+ if (stsTokenIdentifier.getCreationTime() == null) {
+ throw new SecretManager.InvalidToken("Invalid STS token - creationTime is null");
+ }
}
/**
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenIdentifier.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenIdentifier.java
index 8c13aac5190..cc229083d9e 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenIdentifier.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenIdentifier.java
@@ -46,6 +46,7 @@ public class STSTokenIdentifier extends ShortLivedTokenIdentifier {
private String originalAccessKeyId;
private String secretAccessKey;
private String sessionPolicy;
+ private Instant creationTime;
// Encryption key derived from ManagedSecretKey for this token
private transient byte[] encryptionKey;
@@ -63,23 +64,135 @@ public STSTokenIdentifier() {
/**
* Create a new STS token identifier with encryption support.
*
- * @param tempAccessKeyId the temporary access key ID (owner)
- * @param originalAccessKeyId the original long-lived access key ID that created this token
- * @param roleArn the ARN of the assumed role
- * @param expiry the token expiration time
- * @param secretAccessKey the secret access key associated with the temporary access key ID
- * @param sessionPolicy an optional opaque identifier that further limits the scope of
- * the permissions granted by the role
- * @param encryptionKey the key bytes for encrypting sensitive fields
+ * @param params the STS token creation parameters
*/
- public STSTokenIdentifier(String tempAccessKeyId, String originalAccessKeyId, String roleArn, Instant expiry,
- String secretAccessKey, String sessionPolicy, byte[] encryptionKey) {
- super(tempAccessKeyId, expiry);
- this.originalAccessKeyId = originalAccessKeyId;
- this.roleArn = roleArn;
- this.secretAccessKey = secretAccessKey;
- this.sessionPolicy = sessionPolicy;
- this.encryptionKey = encryptionKey != null ? encryptionKey.clone() : null;
+ public STSTokenIdentifier(Params params) {
+ super(params.getTempAccessKeyId(), params.getExpiry());
+ this.originalAccessKeyId = params.getOriginalAccessKeyId();
+ this.roleArn = params.getRoleArn();
+ this.creationTime = params.getCreationTime();
+ this.secretAccessKey = params.getSecretAccessKey();
+ this.sessionPolicy = params.getSessionPolicy();
+ this.encryptionKey = params.getEncryptionKey(); // already cloned via Params
+ }
+
+ /**
+ * Parameters for constructing an {@link STSTokenIdentifier}.
+ */
+ public static final class Params {
+ private final String tempAccessKeyId;
+ private final String originalAccessKeyId;
+ private final String roleArn;
+ private final Instant creationTime;
+ private final Instant expiry;
+ private final String secretAccessKey;
+ private final String sessionPolicy;
+ private final byte[] encryptionKey;
+
+ private Params(Builder builder) {
+ this.tempAccessKeyId = builder.tempAccessKeyId;
+ this.originalAccessKeyId = builder.originalAccessKeyId;
+ this.roleArn = builder.roleArn;
+ this.creationTime = builder.creationTime;
+ this.expiry = builder.expiry;
+ this.secretAccessKey = builder.secretAccessKey;
+ this.sessionPolicy = builder.sessionPolicy;
+ this.encryptionKey = builder.encryptionKey;
+ }
+
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ public String getTempAccessKeyId() {
+ return tempAccessKeyId;
+ }
+
+ public String getOriginalAccessKeyId() {
+ return originalAccessKeyId;
+ }
+
+ public String getRoleArn() {
+ return roleArn;
+ }
+
+ public Instant getCreationTime() {
+ return creationTime;
+ }
+
+ public Instant getExpiry() {
+ return expiry;
+ }
+
+ public String getSecretAccessKey() {
+ return secretAccessKey;
+ }
+
+ public String getSessionPolicy() {
+ return sessionPolicy;
+ }
+
+ public byte[] getEncryptionKey() {
+ return encryptionKey != null ? encryptionKey.clone() : null;
+ }
+
+ /**
+ * Builder for {@link Params}.
+ */
+ public static final class Builder {
+ private String tempAccessKeyId;
+ private String originalAccessKeyId;
+ private String roleArn;
+ private Instant creationTime;
+ private Instant expiry;
+ private String secretAccessKey;
+ private String sessionPolicy;
+ private byte[] encryptionKey;
+
+ public Builder setTempAccessKeyId(String value) {
+ this.tempAccessKeyId = value;
+ return this;
+ }
+
+ public Builder setOriginalAccessKeyId(String value) {
+ this.originalAccessKeyId = value;
+ return this;
+ }
+
+ public Builder setRoleArn(String value) {
+ this.roleArn = value;
+ return this;
+ }
+
+ public Builder setCreationTime(Instant value) {
+ this.creationTime = value;
+ return this;
+ }
+
+ public Builder setExpiry(Instant value) {
+ this.expiry = value;
+ return this;
+ }
+
+ public Builder setSecretAccessKey(String value) {
+ this.secretAccessKey = value;
+ return this;
+ }
+
+ public Builder setSessionPolicy(String value) {
+ this.sessionPolicy = value;
+ return this;
+ }
+
+ public Builder setEncryptionKey(byte[] value) {
+ this.encryptionKey = value != null ? value.clone() : null;
+ return this;
+ }
+
+ public Params build() {
+ return new Params(this);
+ }
+ }
}
@Override
@@ -123,6 +236,7 @@ public OMTokenProto toProtoBuf() {
builder
.setType(OMTokenProto.Type.S3_STS_TOKEN)
+ .setIssueDate(creationTime.toEpochMilli())
.setMaxDate(getExpiry().toEpochMilli())
.setOwner(getOwnerId() != null ? getOwnerId() : "")
.setAccessKeyId(getOwnerId() != null ? getOwnerId() : "")
@@ -146,6 +260,9 @@ public void fromProtoBuf(OMTokenProto token) throws IOException {
setOwnerId(token.getOwner());
setExpiry(Instant.ofEpochMilli(token.getMaxDate()));
+ if (token.hasIssueDate()) {
+ this.creationTime = Instant.ofEpochMilli(token.getIssueDate());
+ }
if (token.hasOriginalAccessKeyId()) {
this.originalAccessKeyId = token.getOriginalAccessKeyId();
}
@@ -244,6 +361,10 @@ public String getSessionPolicy() {
return sessionPolicy;
}
+ public Instant getCreationTime() {
+ return creationTime;
+ }
+
public void setEncryptionKey(byte[] encryptionKey) {
this.encryptionKey = encryptionKey.clone();
}
@@ -265,13 +386,13 @@ public boolean equals(Object o) {
final STSTokenIdentifier that = (STSTokenIdentifier) o;
return Objects.equals(roleArn, that.roleArn) && Objects.equals(secretAccessKey, that.secretAccessKey) &&
Objects.equals(originalAccessKeyId, that.originalAccessKeyId) &&
- Objects.equals(sessionPolicy, that.sessionPolicy);
+ Objects.equals(sessionPolicy, that.sessionPolicy) && Objects.equals(creationTime, that.creationTime);
}
@Override
public int hashCode() {
return Objects.hash(
- super.hashCode(), roleArn, secretAccessKey, originalAccessKeyId, sessionPolicy);
+ super.hashCode(), roleArn, secretAccessKey, originalAccessKeyId, sessionPolicy, creationTime);
}
@Override
@@ -279,7 +400,7 @@ public String toString() {
// Intentionally left off secretAccessKey
return "STSTokenIdentifier{" + "tempAccessKeyId='" + getOwnerId() + "'" +
", originalAccessKeyId='" + originalAccessKeyId + "', roleArn='" + roleArn + "'" +
- ", expiry='" + getExpiry() + "', secretKeyId='" + getSecretKeyId() + "'" +
- ", sessionPolicy='" + sessionPolicy + "'}";
+ ", creationTime='" + creationTime + "', expiry='" + getExpiry() + "', secretKeyId='" + getSecretKeyId() +
+ "', sessionPolicy='" + sessionPolicy + "'}";
}
}
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenSecretManager.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenSecretManager.java
index f72b1892de8..8cddc50f18a 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenSecretManager.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenSecretManager.java
@@ -85,7 +85,8 @@ public Token generateToken(STSTokenIdentifier tokenIdentifie
*/
public String createSTSTokenString(String tempAccessKeyId, String originalAccessKeyId, String roleArn,
int durationSeconds, String secretAccessKey, String sessionPolicy, Clock clock) throws IOException {
- final Instant expiration = clock.instant().plusSeconds(durationSeconds);
+ final Instant creationTime = clock.instant();
+ final Instant expiration = creationTime.plusSeconds(durationSeconds);
// Get the current secret key for encryption
final ManagedSecretKey currentSecretKey = secretKeyClient.getCurrentSecretKey();
@@ -94,8 +95,16 @@ public String createSTSTokenString(String tempAccessKeyId, String originalAccess
// Note - the encryptionKey will NOT be encoded in the token. When generateToken() is called, it eventually calls
// the write() method in STSTokenIdentifier which calls toProtoBuf(), and the encryptionKey is not
// serialized there.
- final STSTokenIdentifier identifier = new STSTokenIdentifier(
- tempAccessKeyId, originalAccessKeyId, roleArn, expiration, secretAccessKey, sessionPolicy, encryptionKey);
+ final STSTokenIdentifier identifier = new STSTokenIdentifier(STSTokenIdentifier.Params.newBuilder()
+ .setTempAccessKeyId(tempAccessKeyId)
+ .setOriginalAccessKeyId(originalAccessKeyId)
+ .setRoleArn(roleArn)
+ .setCreationTime(creationTime)
+ .setExpiry(expiration)
+ .setSecretAccessKey(secretAccessKey)
+ .setSessionPolicy(sessionPolicy)
+ .setEncryptionKey(encryptionKey)
+ .build());
final Token token = generateToken(identifier);
return token.encodeToUrlString();
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3RevokeSTSTokenRequest.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3RevokeSTSTokenRequest.java
index 0f3d2519b30..99b6d6a98e9 100644
--- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3RevokeSTSTokenRequest.java
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3RevokeSTSTokenRequest.java
@@ -19,7 +19,9 @@
import static org.apache.hadoop.security.authentication.util.KerberosName.DEFAULT_MECHANISM;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
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.Mockito.eq;
import static org.mockito.Mockito.mock;
@@ -29,16 +31,16 @@
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;
-import org.apache.hadoop.hdds.security.symmetric.SecretKeyClient;
import org.apache.hadoop.hdds.utils.db.Table;
import org.apache.hadoop.hdds.utils.db.cache.CacheKey;
-import org.apache.hadoop.hdds.utils.db.cache.CacheValue;
import org.apache.hadoop.ipc_.ExternalCall;
import org.apache.hadoop.ipc_.Server;
+import org.apache.hadoop.ozone.OzoneConsts;
import org.apache.hadoop.ozone.audit.AuditLogger;
import org.apache.hadoop.ozone.om.OMMetadataManager;
import org.apache.hadoop.ozone.om.OMMultiTenantManager;
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.om.execution.flowcontrol.ExecutionContext;
import org.apache.hadoop.ozone.om.request.OMClientRequest;
@@ -46,11 +48,8 @@
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
-import org.apache.hadoop.ozone.security.STSTokenSecretManager;
-import org.apache.hadoop.ozone.security.SecretKeyTestClient;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.authentication.util.KerberosName;
-import org.apache.ozone.test.MockClock;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -60,22 +59,22 @@
*/
public class TestS3RevokeSTSTokenRequest {
- private static final MockClock CLOCK = MockClock.newInstance();
+ private static final String TEST_KERBEROS_RULES =
+ "RULE:[2:$1@$0](.*@EXAMPLE.COM)s/@.*//\n" + "RULE:[1:$1@$0](.*@EXAMPLE.COM)s/@.*//\n" + "DEFAULT";
- private STSTokenSecretManager stsTokenSecretManager;
- private SecretKeyClient secretKeyClient;
private OMMultiTenantManager omMultiTenantManager;
+ private String kerberosMechanismBeforeTest;
+ private String kerberosRulesBeforeTest;
@BeforeEach
public void setUp() throws Exception {
+ kerberosMechanismBeforeTest = KerberosName.getRuleMechanism();
+ kerberosRulesBeforeTest = KerberosName.getRules();
+ KerberosName.setRuleMechanism(DEFAULT_MECHANISM);
// Initialize KerberosName rules so that UGI short names derived from
// principals like "alice@EXAMPLE.COM" are computed correctly.
- KerberosName.setRuleMechanism(DEFAULT_MECHANISM);
- KerberosName.setRules(
- "RULE:[2:$1@$0](.*@EXAMPLE.COM)s/@.*//\n" + "RULE:[1:$1@$0](.*@EXAMPLE.COM)s/@.*//\n" + "DEFAULT");
+ KerberosName.setRules(TEST_KERBEROS_RULES);
- secretKeyClient = new SecretKeyTestClient();
- stsTokenSecretManager = new STSTokenSecretManager(secretKeyClient);
// Multi-tenant manager mock used for tests that exercise the S3 multi-tenancy permission branch.
omMultiTenantManager = mock(OMMultiTenantManager.class);
}
@@ -83,15 +82,15 @@ public void setUp() throws Exception {
@AfterEach
public void tearDown() {
Server.getCurCall().remove();
+ KerberosName.setRuleMechanism(kerberosMechanismBeforeTest);
+ KerberosName.setRules(kerberosRulesBeforeTest);
}
@Test
public void testPreExecuteFailsForNonOwnerOfOriginalAccessKey() throws Exception {
- // Verify that preExecute enforces permissions based on the original access key id encoded in the STS token
+ // Verify that preExecute enforces permissions based on the request's original access key ID
// and rejects revocation attempts from non-owners.
- final String tempAccessKeyId = "ASIA12345678";
final String originalAccessKeyId = "original-access-key-id";
- final String sessionToken = createSessionToken(tempAccessKeyId, originalAccessKeyId);
// An RPC call running another Kerberos identity should NOT be allowed to revoke the token whose original
// access key id is different.
@@ -100,24 +99,10 @@ public void testPreExecuteFailsForNonOwnerOfOriginalAccessKey() throws Exception
OMException ex;
try (OzoneManager ozoneManager = mock(OzoneManager.class)) {
- when(ozoneManager.isS3MultiTenancyEnabled()).thenReturn(false);
- when(ozoneManager.isS3Admin(any(UserGroupInformation.class)))
- .thenReturn(false);
- when(ozoneManager.getSecretKeyClient()).thenReturn(secretKeyClient);
-
- final OzoneManagerProtocolProtos.RevokeSTSTokenRequest revokeRequest =
- OzoneManagerProtocolProtos.RevokeSTSTokenRequest.newBuilder()
- .setSessionToken(sessionToken)
- .build();
-
- final OMRequest omRequest = OMRequest.newBuilder()
- .setClientId(UUID.randomUUID().toString())
- .setCmdType(Type.RevokeSTSToken)
- .setRevokeSTSTokenRequest(revokeRequest)
- .build();
-
- final OMClientRequest omClientRequest = new S3RevokeSTSTokenRequest(omRequest);
+ configureOzoneManagerForPreExecute(ozoneManager, originalAccessKeyId, true);
+ when(ozoneManager.isS3Admin(any(UserGroupInformation.class))).thenReturn(false);
+ final OMClientRequest omClientRequest = new S3RevokeSTSTokenRequest(buildRevokeOmRequest(originalAccessKeyId));
ex = assertThrows(OMException.class, () -> omClientRequest.preExecute(ozoneManager));
}
assertEquals(OMException.ResultCodes.USER_MISMATCH, ex.getResult());
@@ -125,36 +110,25 @@ public void testPreExecuteFailsForNonOwnerOfOriginalAccessKey() throws Exception
@Test
public void testPreExecuteSucceedsForOriginalAccessKeyOwner() throws Exception {
- // Verify that preExecute allows the owner of the original access key id (as encoded in the STS token)
+ // Verify that preExecute allows the owner of the original access key ID from the revoke request
// to revoke the temporary credentials.
- final String tempAccessKeyId = "ASIA4567891230";
final String originalAccessKeyId = "original-access-key-id";
- final String sessionToken = createSessionToken(tempAccessKeyId, originalAccessKeyId);
// Simulate RPC call running as originalAccessKeyId
final UserGroupInformation originalUgi = UserGroupInformation.createRemoteUser(originalAccessKeyId);
Server.getCurCall().set(new StubCall(originalUgi));
final OzoneManager ozoneManager = mock(OzoneManager.class);
- when(ozoneManager.isS3MultiTenancyEnabled()).thenReturn(false);
- when(ozoneManager.isS3Admin(any(UserGroupInformation.class)))
- .thenReturn(false);
- when(ozoneManager.getSecretKeyClient()).thenReturn(secretKeyClient);
-
- final OzoneManagerProtocolProtos.RevokeSTSTokenRequest revokeRequest =
- OzoneManagerProtocolProtos.RevokeSTSTokenRequest.newBuilder()
- .setSessionToken(sessionToken)
- .build();
-
- final OMRequest omRequest = OMRequest.newBuilder()
- .setClientId(UUID.randomUUID().toString())
- .setCmdType(Type.RevokeSTSToken)
- .setRevokeSTSTokenRequest(revokeRequest)
- .build();
+ configureOzoneManagerForPreExecute(ozoneManager, originalAccessKeyId, true);
+ when(ozoneManager.isS3Admin(any(UserGroupInformation.class))).thenReturn(false);
- final OMClientRequest omClientRequest = new S3RevokeSTSTokenRequest(omRequest);
+ final OMClientRequest omClientRequest = new S3RevokeSTSTokenRequest(buildRevokeOmRequest(originalAccessKeyId));
final OMRequest result = omClientRequest.preExecute(ozoneManager);
+
assertEquals(Type.RevokeSTSToken, result.getCmdType());
+ assertTrue(result.hasUpdateRevokeSTSTokenRequest());
+ assertEquals(originalAccessKeyId, result.getUpdateRevokeSTSTokenRequest().getOriginalAccessKeyId());
+ assertTrue(result.getUpdateRevokeSTSTokenRequest().getRevocationTimeMillis() > 0L);
}
@Test
@@ -163,40 +137,23 @@ public void testPreExecuteSucceedsForTenantAccessIdOwner() throws Exception {
// the tenant access ID owner is allowed to revoke the temporary credentials.
final String tenantId = "finance";
final String originalAccessKeyId = "alice@EXAMPLE.COM";
- final String tempAccessKeyId = "ASIA123456789";
- final String sessionToken = createSessionToken(tempAccessKeyId, originalAccessKeyId);
// Caller short name "alice" should match the owner username returned from the multi-tenant manager.
final UserGroupInformation callerUgi = UserGroupInformation.createRemoteUser(originalAccessKeyId);
Server.getCurCall().set(new StubCall(callerUgi));
final OzoneManager ozoneManager = mock(OzoneManager.class);
+ configureOzoneManagerForPreExecute(ozoneManager, originalAccessKeyId, true);
when(ozoneManager.isS3MultiTenancyEnabled()).thenReturn(true);
when(ozoneManager.getMultiTenantManager()).thenReturn(omMultiTenantManager);
- when(ozoneManager.getSecretKeyClient()).thenReturn(secretKeyClient);
// Original access key id is assigned to a tenant and owned by "alice".
- when(omMultiTenantManager.getTenantForAccessID(originalAccessKeyId))
- .thenReturn(Optional.of(tenantId));
- when(omMultiTenantManager.getUserNameGivenAccessId(originalAccessKeyId))
- .thenReturn("alice");
+ when(omMultiTenantManager.getTenantForAccessID(originalAccessKeyId)).thenReturn(Optional.of(tenantId));
+ when(omMultiTenantManager.getUserNameGivenAccessId(originalAccessKeyId)).thenReturn("alice");
// Not a tenant admin; ownership should be sufficient.
- when(omMultiTenantManager.isTenantAdmin(callerUgi, tenantId, false))
- .thenReturn(false);
-
- final OzoneManagerProtocolProtos.RevokeSTSTokenRequest revokeRequest =
- OzoneManagerProtocolProtos.RevokeSTSTokenRequest.newBuilder()
- .setSessionToken(sessionToken)
- .build();
-
- final OMRequest omRequest = OMRequest.newBuilder()
- .setClientId(UUID.randomUUID().toString())
- .setCmdType(Type.RevokeSTSToken)
- .setRevokeSTSTokenRequest(revokeRequest)
- .build();
-
- final OMClientRequest omClientRequest = new S3RevokeSTSTokenRequest(omRequest);
+ when(omMultiTenantManager.isTenantAdmin(callerUgi, tenantId, false)).thenReturn(false);
+ final OMClientRequest omClientRequest = new S3RevokeSTSTokenRequest(buildRevokeOmRequest(originalAccessKeyId));
final OMRequest result = omClientRequest.preExecute(ozoneManager);
assertEquals(Type.RevokeSTSToken, result.getCmdType());
}
@@ -207,40 +164,23 @@ public void testPreExecuteSucceedsForTenantAdmin() throws Exception {
// tenant admin (who is not the owner) is allowed to revoke the temporary credentials.
final String tenantId = "finance";
final String originalAccessKeyId = "alice@EXAMPLE.COM";
- final String tempAccessKeyId = "ASIA4567890123";
- final String sessionToken = createSessionToken(tempAccessKeyId, originalAccessKeyId);
// Caller short name "bob" does not own the access ID but will be configured as tenant admin.
final UserGroupInformation callerUgi = UserGroupInformation.createRemoteUser("bob@EXAMPLE.COM");
Server.getCurCall().set(new StubCall(callerUgi));
final OzoneManager ozoneManager = mock(OzoneManager.class);
+ configureOzoneManagerForPreExecute(ozoneManager, originalAccessKeyId, true);
when(ozoneManager.isS3MultiTenancyEnabled()).thenReturn(true);
when(ozoneManager.getMultiTenantManager()).thenReturn(omMultiTenantManager);
- when(ozoneManager.getSecretKeyClient()).thenReturn(secretKeyClient);
// Original access key id is assigned to a tenant and owned by "alice".
- when(omMultiTenantManager.getTenantForAccessID(originalAccessKeyId))
- .thenReturn(Optional.of(tenantId));
- when(omMultiTenantManager.getUserNameGivenAccessId(originalAccessKeyId))
- .thenReturn("alice");
+ when(omMultiTenantManager.getTenantForAccessID(originalAccessKeyId)).thenReturn(Optional.of(tenantId));
+ when(omMultiTenantManager.getUserNameGivenAccessId(originalAccessKeyId)).thenReturn("alice");
// Caller is configured as tenant admin so the check should pass.
- when(omMultiTenantManager.isTenantAdmin(callerUgi, tenantId, false))
- .thenReturn(true);
-
- final OzoneManagerProtocolProtos.RevokeSTSTokenRequest revokeRequest =
- OzoneManagerProtocolProtos.RevokeSTSTokenRequest.newBuilder()
- .setSessionToken(sessionToken)
- .build();
-
- final OMRequest omRequest = OMRequest.newBuilder()
- .setClientId(UUID.randomUUID().toString())
- .setCmdType(Type.RevokeSTSToken)
- .setRevokeSTSTokenRequest(revokeRequest)
- .build();
-
- final OMClientRequest omClientRequest = new S3RevokeSTSTokenRequest(omRequest);
+ when(omMultiTenantManager.isTenantAdmin(callerUgi, tenantId, false)).thenReturn(true);
+ final OMClientRequest omClientRequest = new S3RevokeSTSTokenRequest(buildRevokeOmRequest(originalAccessKeyId));
final OMRequest result = omClientRequest.preExecute(ozoneManager);
assertEquals(Type.RevokeSTSToken, result.getCmdType());
}
@@ -251,8 +191,6 @@ public void testPreExecuteFailsForNonOwnerNonAdminInTenant() throws Exception {
// non-owner, non-admin caller is rejected.
final String tenantId = "finance";
final String originalAccessKeyId = "alice@EXAMPLE.COM";
- final String tempAccessKeyId = "ASIA123456789";
- final String sessionToken = createSessionToken(tempAccessKeyId, originalAccessKeyId);
// Caller short name "carol" does not own the access ID and is not
// configured as tenant admin.
@@ -261,42 +199,65 @@ public void testPreExecuteFailsForNonOwnerNonAdminInTenant() throws Exception {
final OMException ex;
try (OzoneManager ozoneManager = mock(OzoneManager.class)) {
+ configureOzoneManagerForPreExecute(ozoneManager, originalAccessKeyId, true);
when(ozoneManager.isS3MultiTenancyEnabled()).thenReturn(true);
when(ozoneManager.getMultiTenantManager()).thenReturn(omMultiTenantManager);
- when(ozoneManager.getSecretKeyClient()).thenReturn(secretKeyClient);
-
// Original access key id is assigned to a tenant and owned by "alice".
- when(omMultiTenantManager.getTenantForAccessID(originalAccessKeyId))
- .thenReturn(Optional.of(tenantId));
- when(omMultiTenantManager.getUserNameGivenAccessId(originalAccessKeyId))
- .thenReturn("alice");
+ when(omMultiTenantManager.getTenantForAccessID(originalAccessKeyId)).thenReturn(Optional.of(tenantId));
+ when(omMultiTenantManager.getUserNameGivenAccessId(originalAccessKeyId)).thenReturn("alice");
// Caller is not a tenant admin.
- when(omMultiTenantManager.isTenantAdmin(callerUgi, tenantId, false))
- .thenReturn(false);
+ when(omMultiTenantManager.isTenantAdmin(callerUgi, tenantId, false)).thenReturn(false);
- final OzoneManagerProtocolProtos.RevokeSTSTokenRequest revokeRequest =
- OzoneManagerProtocolProtos.RevokeSTSTokenRequest.newBuilder()
- .setSessionToken(sessionToken)
- .build();
+ final OMClientRequest omClientRequest = new S3RevokeSTSTokenRequest(buildRevokeOmRequest(originalAccessKeyId));
+ ex = assertThrows(OMException.class, () -> omClientRequest.preExecute(ozoneManager));
+ }
+ assertEquals(OMException.ResultCodes.USER_MISMATCH, ex.getResult());
+ }
- final OMRequest omRequest = OMRequest.newBuilder()
- .setClientId(UUID.randomUUID().toString())
- .setCmdType(Type.RevokeSTSToken)
- .setRevokeSTSTokenRequest(revokeRequest)
- .build();
+ @Test
+ public void testPreExecuteRejectsUnknownOriginalAccessKeyId() throws Exception {
+ // Reject revocation when originalAccessKeyId has no S3 secret in RocksDB.
+ final String originalAccessKeyId = "unknown-access-key-id";
+ final UserGroupInformation callerUgi = UserGroupInformation.createRemoteUser(originalAccessKeyId);
+ Server.getCurCall().set(new StubCall(callerUgi));
- final OMClientRequest omClientRequest = new S3RevokeSTSTokenRequest(omRequest);
+ try (OzoneManager ozoneManager = mock(OzoneManager.class)) {
+ final S3SecretManager s3SecretManager = configureOzoneManagerForPreExecute(
+ ozoneManager, originalAccessKeyId, false);
+ final OMClientRequest omClientRequest = new S3RevokeSTSTokenRequest(buildRevokeOmRequest(originalAccessKeyId));
+ final OMException ex = assertThrows(OMException.class, () -> omClientRequest.preExecute(ozoneManager));
+ assertEquals(OMException.ResultCodes.INVALID_REQUEST, ex.getResult());
+ assertTrue(ex.getMessage().contains("does not exist"));
+ assertTrue(ex.getMessage().contains(originalAccessKeyId));
+ verify(s3SecretManager).hasS3Secret(originalAccessKeyId);
+ }
+ }
- ex = assertThrows(OMException.class, () -> omClientRequest.preExecute(ozoneManager));
+ @Test
+ public void testPreExecuteRejectsUnknownOriginalAccessKeyIdForS3Admin() throws Exception {
+ // S3 admins may revoke other principals' tokens, but not for unknown access key IDs.
+ final String originalAccessKeyId = "unknown-access-key-id";
+ final UserGroupInformation adminUgi = UserGroupInformation.createRemoteUser("om-admin");
+ Server.getCurCall().set(new StubCall(adminUgi));
+
+ try (OzoneManager ozoneManager = mock(OzoneManager.class)) {
+ final S3SecretManager s3SecretManager = configureOzoneManagerForPreExecute(
+ ozoneManager, originalAccessKeyId, false);
+ when(ozoneManager.isS3Admin(adminUgi)).thenReturn(true);
+
+ final OMClientRequest omClientRequest = new S3RevokeSTSTokenRequest(buildRevokeOmRequest(originalAccessKeyId));
+ final OMException ex = assertThrows(OMException.class, () -> omClientRequest.preExecute(ozoneManager));
+ assertEquals(OMException.ResultCodes.INVALID_REQUEST, ex.getResult());
+ assertTrue(ex.getMessage().contains("does not exist"));
+ assertTrue(ex.getMessage().contains(originalAccessKeyId));
+ verify(s3SecretManager).hasS3Secret(originalAccessKeyId);
}
- assertEquals(OMException.ResultCodes.USER_MISMATCH, ex.getResult());
}
@Test
- public void testValidateAndUpdateCacheUpdatesCacheImmediately() throws Exception {
- final String tempAccessKeyId = "ASIA4567891230";
+ public void testValidateAndUpdateCacheUpdatesCacheImmediately() {
final String originalAccessKeyId = "original-access-key-id";
- final String sessionToken = createSessionToken(tempAccessKeyId, originalAccessKeyId);
+ final long revocationTimeMillis = 1_700_000_000_000L;
final OzoneManager ozoneManager = mock(OzoneManager.class);
final OMMetadataManager omMetadataManager = mock(OMMetadataManager.class);
@@ -311,25 +272,135 @@ public void testValidateAndUpdateCacheUpdatesCacheImmediately() throws Exception
final OzoneManagerProtocolProtos.RevokeSTSTokenRequest revokeRequest =
OzoneManagerProtocolProtos.RevokeSTSTokenRequest.newBuilder()
- .setSessionToken(sessionToken)
+ .setOriginalAccessKeyId(originalAccessKeyId)
+ .build();
+ final OzoneManagerProtocolProtos.UpdateRevokeSTSTokenRequest updateRevokeRequest =
+ OzoneManagerProtocolProtos.UpdateRevokeSTSTokenRequest.newBuilder()
+ .setOriginalAccessKeyId(originalAccessKeyId)
+ .setRevocationTimeMillis(revocationTimeMillis)
.build();
final OMRequest omRequest = OMRequest.newBuilder()
.setClientId(UUID.randomUUID().toString())
.setCmdType(Type.RevokeSTSToken)
.setRevokeSTSTokenRequest(revokeRequest)
+ .setUpdateRevokeSTSTokenRequest(updateRevokeRequest)
.build();
final S3RevokeSTSTokenRequest s3RevokeSTSTokenRequest = new S3RevokeSTSTokenRequest(omRequest);
final OMClientResponse omClientResponse = s3RevokeSTSTokenRequest.validateAndUpdateCache(ozoneManager, context);
assertEquals(OzoneManagerProtocolProtos.Status.OK, omClientResponse.getOMResponse().getStatus());
- verify(s3RevokedStsTokenTable).addCacheEntry(eq(new CacheKey<>(sessionToken)), any(CacheValue.class));
+ verify(s3RevokedStsTokenTable).addCacheEntry(
+ eq(new CacheKey<>(originalAccessKeyId)), any());
+ assertNotNull(s3RevokeSTSTokenRequest.getAuditBuilder().getAuditMap());
+ assertEquals(
+ originalAccessKeyId, s3RevokeSTSTokenRequest.getAuditBuilder().getAuditMap().get(
+ OzoneConsts.S3_STS_ORIGINAL_ACCESS_KEY_ID));
+ }
+
+ @Test
+ public void testValidateAndUpdateCacheRejectsMissingUpdateRevokeSTSTokenRequest() {
+ final String originalAccessKeyId = "original-access-key-id";
+
+ final OzoneManager ozoneManager = mock(OzoneManager.class);
+ final OMMetadataManager omMetadataManager = mock(OMMetadataManager.class);
+ @SuppressWarnings("unchecked")
+ final Table s3RevokedStsTokenTable = mock(Table.class);
+ final ExecutionContext context = mock(ExecutionContext.class);
+
+ when(ozoneManager.getMetadataManager()).thenReturn(omMetadataManager);
+ when(omMetadataManager.getS3RevokedStsTokenTable()).thenReturn(s3RevokedStsTokenTable);
+
+ final OzoneManagerProtocolProtos.RevokeSTSTokenRequest revokeRequest =
+ OzoneManagerProtocolProtos.RevokeSTSTokenRequest.newBuilder()
+ .setOriginalAccessKeyId(originalAccessKeyId)
+ .build();
+
+ final OMRequest omRequest = OMRequest.newBuilder()
+ .setClientId(UUID.randomUUID().toString())
+ .setCmdType(Type.RevokeSTSToken)
+ .setRevokeSTSTokenRequest(revokeRequest)
+ .build();
+
+ final S3RevokeSTSTokenRequest s3RevokeSTSTokenRequest = new S3RevokeSTSTokenRequest(omRequest);
+ final OMClientResponse omClientResponse =
+ s3RevokeSTSTokenRequest.validateAndUpdateCache(ozoneManager, context);
+ assertEquals(OzoneManagerProtocolProtos.Status.INTERNAL_ERROR, omClientResponse.getOMResponse().getStatus());
+ }
+
+ @Test
+ public void testValidateAndUpdateCacheRejectsMismatchedOriginalAccessKeyId() {
+ final String originalAccessKeyId = "original-access-key-id";
+ final String mismatchedAccessKeyId = "other-access-key-id";
+ final long revocationTimeMillis = 1_700_000_000_000L;
+
+ final OzoneManager ozoneManager = mock(OzoneManager.class);
+ final OMMetadataManager omMetadataManager = mock(OMMetadataManager.class);
+ @SuppressWarnings("unchecked")
+ final Table s3RevokedStsTokenTable = mock(Table.class);
+ final ExecutionContext context = mock(ExecutionContext.class);
+
+ when(ozoneManager.getMetadataManager()).thenReturn(omMetadataManager);
+ when(omMetadataManager.getS3RevokedStsTokenTable()).thenReturn(s3RevokedStsTokenTable);
+
+ final OMRequest omRequest = OMRequest.newBuilder()
+ .setClientId(UUID.randomUUID().toString())
+ .setCmdType(Type.RevokeSTSToken)
+ .setRevokeSTSTokenRequest(OzoneManagerProtocolProtos.RevokeSTSTokenRequest.newBuilder()
+ .setOriginalAccessKeyId(originalAccessKeyId)
+ .build())
+ .setUpdateRevokeSTSTokenRequest(OzoneManagerProtocolProtos.UpdateRevokeSTSTokenRequest.newBuilder()
+ .setOriginalAccessKeyId(mismatchedAccessKeyId)
+ .setRevocationTimeMillis(revocationTimeMillis)
+ .build())
+ .build();
+
+ final S3RevokeSTSTokenRequest s3RevokeSTSTokenRequest = new S3RevokeSTSTokenRequest(omRequest);
+ final OMClientResponse omClientResponse =
+ s3RevokeSTSTokenRequest.validateAndUpdateCache(ozoneManager, context);
+ assertEquals(OzoneManagerProtocolProtos.Status.INTERNAL_ERROR, omClientResponse.getOMResponse().getStatus());
+ }
+
+ @Test
+ public void testPreExecuteRejectsOverlongOriginalAccessKeyId() throws Exception {
+ final StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < OzoneConsts.OZONE_MAXIMUM_ACCESS_ID_LENGTH; i++) {
+ sb.append('a');
+ }
+ final UserGroupInformation callerUgi = UserGroupInformation.createRemoteUser("caller");
+ Server.getCurCall().set(new StubCall(callerUgi));
+
+ try (OzoneManager ozoneManager = mock(OzoneManager.class)) {
+ configureOzoneManagerForPreExecute(ozoneManager, sb.toString(), false);
+ final OMClientRequest omClientRequest = new S3RevokeSTSTokenRequest(buildRevokeOmRequest(sb.toString()));
+ final OMException ex = assertThrows(OMException.class, () -> omClientRequest.preExecute(ozoneManager));
+ assertEquals(OMException.ResultCodes.INVALID_REQUEST, ex.getResult());
+ }
+ }
+
+ private static OMRequest buildRevokeOmRequest(String originalAccessKeyId) {
+ final OzoneManagerProtocolProtos.RevokeSTSTokenRequest revokeRequest =
+ OzoneManagerProtocolProtos.RevokeSTSTokenRequest.newBuilder()
+ .setOriginalAccessKeyId(originalAccessKeyId)
+ .build();
+
+ return OMRequest.newBuilder()
+ .setClientId(UUID.randomUUID().toString())
+ .setCmdType(Type.RevokeSTSToken)
+ .setRevokeSTSTokenRequest(revokeRequest)
+ .build();
+ }
+
+ private static S3SecretManager configureOzoneManagerForPreExecute(OzoneManager ozoneManager,
+ String originalAccessKeyId, boolean hasSecret) throws IOException {
+ when(ozoneManager.isS3MultiTenancyEnabled()).thenReturn(false);
+ final S3SecretManager s3SecretManager = mock(S3SecretManager.class);
+ when(ozoneManager.getS3SecretManager()).thenReturn(s3SecretManager);
+ when(s3SecretManager.hasS3Secret(originalAccessKeyId)).thenReturn(hasSecret);
+ return s3SecretManager;
}
- /**
- * Stub used to inject a remote user into the ProtobufRpcEngine.Server.getRemoteUser() thread-local.
- */
private static final class StubCall extends ExternalCall {
private final UserGroupInformation ugi;
@@ -343,10 +414,4 @@ public UserGroupInformation getRemoteUser() {
return ugi;
}
}
-
- private String createSessionToken(String tempAccessKeyId, String originalAccessKeyId) throws IOException {
- return stsTokenSecretManager.createSTSTokenString(
- tempAccessKeyId, originalAccessKeyId, "arn:aws:iam::123456789012:role/test-role", 3600,
- "test-secret-access-key", "test-session-policy", CLOCK);
- }
}
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestRevokedSTSTokenCleanupService.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestRevokedSTSTokenCleanupService.java
index d7cf3630b95..2b734cea245 100644
--- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestRevokedSTSTokenCleanupService.java
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestRevokedSTSTokenCleanupService.java
@@ -75,13 +75,13 @@ public void setUp() {
@Test
public void submitsCleanupRequestForOnlyExpiredTokens() throws Exception {
- // If there are two revoked entries, one expired and one not expired, only the expired session token should be
- // submitted for cleanup.
+ // If there are two revoked entries, one expired and one not expired, only the expired
+ // originalAccessKeyId should be submitted for cleanup.
final long nowMillis = testClock.millis();
final long expiredCreationTimeMillis = nowMillis - TimeUnit.HOURS.toMillis(13); // older than 12h threshold
final long validCreationTimeMillis = nowMillis - TimeUnit.HOURS.toMillis(1);
- revokedStsTokenTable.put("session-token-a", expiredCreationTimeMillis);
- revokedStsTokenTable.put("session-token-b", validCreationTimeMillis);
+ revokedStsTokenTable.put("original-access-key-a", expiredCreationTimeMillis);
+ revokedStsTokenTable.put("original-access-key-b", validCreationTimeMillis);
final AtomicReference capturedRequest = new AtomicReference<>();
@@ -100,7 +100,7 @@ public void submitsCleanupRequestForOnlyExpiredTokens() throws Exception {
final DeleteRevokedSTSTokensRequest deleteRevokedSTSTokensRequest =
omRequest.getDeleteRevokedSTSTokensRequest();
- assertThat(deleteRevokedSTSTokensRequest.getSessionTokenList()).containsExactly("session-token-a");
+ assertThat(deleteRevokedSTSTokensRequest.getOriginalAccessKeyIdList()).containsExactly("original-access-key-a");
}
}
@@ -109,8 +109,8 @@ public void doesNotSubmitRequestWhenThereAreNoExpiredTokens() throws Exception {
// If only non-expired entries exist in the revoked sts token table, no cleanup request should be submitted and
// no metrics should be updated.
final long nowMillis = testClock.millis();
- revokedStsTokenTable.put("session-token-c", nowMillis - TimeUnit.HOURS.toMillis(1));
- revokedStsTokenTable.put("session-token-d", nowMillis - TimeUnit.HOURS.toMillis(2));
+ revokedStsTokenTable.put("original-access-key-c", nowMillis - TimeUnit.HOURS.toMillis(1));
+ revokedStsTokenTable.put("original-access-key-d", nowMillis - TimeUnit.HOURS.toMillis(2));
final AtomicReference capturedRequest = new AtomicReference<>();
@@ -149,8 +149,8 @@ public void doesNotUpdateMetricsOnRatisSubmissionServiceExceptionFailure() throw
// If there are expired tokens in the table but the OM request submission to clean up the entries fails with a
// service exception, the metrics should not be updated
final long nowMillis = testClock.millis();
- revokedStsTokenTable.put("session-token-e", nowMillis - TimeUnit.HOURS.toMillis(13));
- revokedStsTokenTable.put("session-token-f", nowMillis - TimeUnit.HOURS.toMillis(14));
+ revokedStsTokenTable.put("original-access-key-e", nowMillis - TimeUnit.HOURS.toMillis(13));
+ revokedStsTokenTable.put("original-access-key-f", nowMillis - TimeUnit.HOURS.toMillis(14));
final AtomicInteger submitAttempts = new AtomicInteger(0);
@@ -172,7 +172,7 @@ public void doesNotUpdateMetricsOnNonSuccessfulResponse() throws Exception {
// If there is an expired token in the table but the OM request submission to clean up the entries gets a
// non-successful response, the metrics should not be updated
final long nowMillis = testClock.millis();
- revokedStsTokenTable.put("session-token-f", nowMillis - TimeUnit.HOURS.toMillis(20));
+ revokedStsTokenTable.put("original-access-key-f", nowMillis - TimeUnit.HOURS.toMillis(20));
try (MockedStatic ozoneManagerRatisUtilsMock = mockStatic(OzoneManagerRatisUtils.class)) {
// Return a non-successful response
@@ -190,9 +190,9 @@ public void doesNotUpdateMetricsOnNonSuccessfulResponse() throws Exception {
public void handlesAllExpiredTokens() throws Exception {
// If all the tokens in the table are expired on a particular run, ensure the metrics are updated appropriately
final long nowMillis = testClock.millis();
- revokedStsTokenTable.put("session-token-g", nowMillis - TimeUnit.HOURS.toMillis(13));
- revokedStsTokenTable.put("session-token-h", nowMillis - TimeUnit.HOURS.toMillis(14));
- revokedStsTokenTable.put("session-token-i", nowMillis - TimeUnit.HOURS.toMillis(15));
+ revokedStsTokenTable.put("original-access-key-g", nowMillis - TimeUnit.HOURS.toMillis(13));
+ revokedStsTokenTable.put("original-access-key-h", nowMillis - TimeUnit.HOURS.toMillis(14));
+ revokedStsTokenTable.put("original-access-key-i", nowMillis - TimeUnit.HOURS.toMillis(15));
final AtomicReference capturedRequest = new AtomicReference<>();
@@ -211,8 +211,8 @@ public void handlesAllExpiredTokens() throws Exception {
final DeleteRevokedSTSTokensRequest deleteRevokedSTSTokensRequest =
omRequest.getDeleteRevokedSTSTokensRequest();
- assertThat(deleteRevokedSTSTokensRequest.getSessionTokenList())
- .containsExactlyInAnyOrder("session-token-g", "session-token-h", "session-token-i");
+ assertThat(deleteRevokedSTSTokensRequest.getOriginalAccessKeyIdList())
+ .containsExactlyInAnyOrder("original-access-key-g", "original-access-key-h", "original-access-key-i");
}
}
@@ -221,9 +221,9 @@ public void submitsMultipleRequestsWhenBatchSizeIsExceeded() throws Exception {
// If the tokens exceed the configured batch size, multiple requests should be submitted
final long nowMillis = testClock.millis();
- // Create 10 expired tokens
+ // Create 10 expired originalAccessKeyIds
for (int i = 0; i < 10; i++) {
- revokedStsTokenTable.put("session-token-" + i, nowMillis - TimeUnit.HOURS.toMillis(13));
+ revokedStsTokenTable.put(String.format("AKIA%07d", i), nowMillis - TimeUnit.HOURS.toMillis(13));
}
// Set a very small ratisByteLimit (100 bytes) to force batching. A single token request will be small, but 10
@@ -245,7 +245,7 @@ public void submitsMultipleRequestsWhenBatchSizeIsExceeded() throws Exception {
// Verify all tokens were included across the requests
final int totalTokens = capturedRequests.stream()
- .mapToInt(r -> r.getDeleteRevokedSTSTokensRequest().getSessionTokenList().size())
+ .mapToInt(r -> r.getDeleteRevokedSTSTokensRequest().getOriginalAccessKeyIdList().size())
.sum();
assertThat(totalTokens).isEqualTo(10);
assertThat(revokedSTSTokenCleanupService.getSubmittedDeletedEntryCount()).isEqualTo(10);
@@ -254,7 +254,7 @@ public void submitsMultipleRequestsWhenBatchSizeIsExceeded() throws Exception {
@Test
public void testSingleOversizedExpiredTokenAndItIsTheOnlyExpiredToken() throws Exception {
- // One sessionToken is larger than the ratisByteLimit, and it is the only expired token
+ // One originalAccessKeyId is larger than the ratisByteLimit, and it is the only expired entry
final long nowMillis = testClock.millis();
// Serialized size for largeToken is 102 > 90 (the effective ratisByteLimit) .
final String largeToken = new String(new char[100]).replace('\0', 'a');
@@ -279,10 +279,10 @@ public void testSingleOversizedExpiredTokenAndItIsTheOnlyExpiredToken() throws E
@Test
public void testSingleOversizedExpiredTokenAndThereAreMultipleExpiredTokens() throws Exception {
- // One sessionToken is larger than the ratisByteLimit, and it is not the only expired token
+ // One originalAccessKeyId is larger than the ratisByteLimit, and it is not the only expired entry
final long nowMillis = testClock.millis();
- final String smallToken = "session-token-j";
- final String largeToken = "session-token-k-" + new String(new char[90]).replace('\0', 'a'); // > 90 bytes
+ final String smallToken = "AKIASMALL01";
+ final String largeToken = "AKIALARGE-" + new String(new char[90]).replace('\0', 'a'); // > 90 bytes
revokedStsTokenTable.put(smallToken, nowMillis - TimeUnit.HOURS.toMillis(13));
revokedStsTokenTable.put(largeToken, nowMillis - TimeUnit.HOURS.toMillis(13));
@@ -308,9 +308,9 @@ public void testExpiredAndNonExpiredTokensWithSmallRatisByteLimit() throws Excep
// Expired and non-expired entries with ratisByteLimit of 100
final long nowMillis = testClock.millis();
- revokedStsTokenTable.put("session-token-l", nowMillis - TimeUnit.HOURS.toMillis(13));
- revokedStsTokenTable.put("session-token-m", nowMillis - TimeUnit.HOURS.toMillis(1)); // Should be skipped
- revokedStsTokenTable.put("session-token-n", nowMillis - TimeUnit.HOURS.toMillis(13));
+ revokedStsTokenTable.put("original-access-key-l", nowMillis - TimeUnit.HOURS.toMillis(13));
+ revokedStsTokenTable.put("original-access-key-m", nowMillis - TimeUnit.HOURS.toMillis(1)); // Should be skipped
+ revokedStsTokenTable.put("original-access-key-n", nowMillis - TimeUnit.HOURS.toMillis(13));
ozoneConfiguration.setStorageSize(
OMConfigKeys.OZONE_OM_RATIS_LOG_APPENDER_QUEUE_BYTE_LIMIT, 100, StorageUnit.BYTES);
@@ -323,10 +323,11 @@ public void testExpiredAndNonExpiredTokensWithSmallRatisByteLimit() throws Excep
final RevokedSTSTokenCleanupService revokedSTSTokenCleanupService = createAndRunCleanupService();
assertThat(revokedSTSTokenCleanupService.getRunCount()).isEqualTo(1);
- // session-token-l and session-token-n fit in one batch. session-token-m is ignored because it is not expired.
+ // original-access-key-l and original-access-key-n fit in one batch.
+ // original-access-key-m is ignored because it is not expired.
assertThat(capturedRequests).hasSize(1);
- assertThat(capturedRequests.get(0).getDeleteRevokedSTSTokensRequest().getSessionTokenList())
- .containsExactly("session-token-l", "session-token-n");
+ assertThat(capturedRequests.get(0).getDeleteRevokedSTSTokensRequest().getOriginalAccessKeyIdList())
+ .containsExactly("original-access-key-l", "original-access-key-n");
assertThat(revokedSTSTokenCleanupService.getSubmittedDeletedEntryCount()).isEqualTo(2);
}
}
@@ -359,12 +360,12 @@ public void testExpiredTokenMatchesRatisByteLimitExactly() throws Exception {
public void testCallIdCountIncreasesAcrossBatches() throws Exception {
// Force small batch of 40 bytes (which should trigger multiple calls to OzoneManagerRatisUtils.submitRequest)
// and ensure the callIdCount increases across each batch
- // session-token-1 and session-token-2 are in first batch, and session-token-3 is in second batch.
+ // AKIA0000001 and AKIA0000002 are in first batch, and AKIA0000003 is in second batch.
final long nowMillis = testClock.millis();
- revokedStsTokenTable.put("session-token-1", nowMillis - TimeUnit.HOURS.toMillis(13));
- revokedStsTokenTable.put("session-token-2", nowMillis - TimeUnit.HOURS.toMillis(13));
- revokedStsTokenTable.put("session-token-3", nowMillis - TimeUnit.HOURS.toMillis(13));
+ revokedStsTokenTable.put("AKIA0000001", nowMillis - TimeUnit.HOURS.toMillis(13));
+ revokedStsTokenTable.put("AKIA0000002", nowMillis - TimeUnit.HOURS.toMillis(13));
+ revokedStsTokenTable.put("AKIA0000003", nowMillis - TimeUnit.HOURS.toMillis(13));
ozoneConfiguration.setStorageSize(OMConfigKeys.OZONE_OM_RATIS_LOG_APPENDER_QUEUE_BYTE_LIMIT, 40, StorageUnit.BYTES);
diff --git 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
index 99c358b929d..814f79b1e84 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
@@ -66,12 +66,10 @@ public class TestS3SecurityUtil {
}
@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
+ public void testValidateS3CredentialFailsWhenTokenCreatedBeforeRevocationCutoff() throws Exception {
validateS3CredentialHelper(
new TestConfig()
- .setTokenRevoked(true)
+ .setRevocationCutoffOffsetMs(1)
.setExpectedResult(REVOKED_TOKEN)
.setExpectedMessage("STS token has been revoked"));
}
@@ -162,6 +160,22 @@ public void testValidateS3CredentialFailsWhenRequestAccessIdEmpty() throws Excep
.setExpectedMessage("STS token validation failed - accessKeyId is invalid for session token"));
}
+ @Test
+ public void testValidateS3CredentialSuccessWhenTokenCreatedAfterRevocationCutoff() throws Exception {
+ validateS3CredentialHelper(
+ new TestConfig()
+ .setRevocationCutoffOffsetMs(-1)
+ .setExpectedResult(null));
+ }
+
+ @Test
+ public void testValidateS3CredentialSuccessWhenTokenCreatedAtRevocationCutoff() throws Exception {
+ validateS3CredentialHelper(
+ new TestConfig()
+ .setRevocationCutoffOffsetMs(0)
+ .setExpectedResult(null));
+ }
+
private void validateS3CredentialHelper(TestConfig config) throws Exception {
try (OzoneManager ozoneManager = mock(OzoneManager.class)) {
when(ozoneManager.isSecurityEnabled()).thenReturn(true);
@@ -188,12 +202,13 @@ private void validateS3CredentialHelper(TestConfig config) throws Exception {
}
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();
+ final String originalAccessKeyId = stsTokenIdentifier.getOriginalAccessKeyId();
+ if (config.revocationCutoffOffsetMs != null && config.revokedSTSTokenTable != null) {
+ final long revocationTimeMillis = stsTokenIdentifier.getCreationTime().toEpochMilli() +
+ config.revocationCutoffOffsetMs;
+ config.revokedSTSTokenTable.put(originalAccessKeyId, revocationTimeMillis);
+ }
try (MockedStatic stsSecurityUtilMock = mockStatic(STSSecurityUtil.class, CALLS_REAL_METHODS);
MockedStatic awsV4AuthValidatorMock = mockStatic(
@@ -229,10 +244,16 @@ private void validateS3CredentialHelper(TestConfig config) throws Exception {
}
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);
+ return new STSTokenIdentifier(STSTokenIdentifier.Params.newBuilder()
+ .setTempAccessKeyId(TEMP_ACCESS_KEY_ID)
+ .setOriginalAccessKeyId("original-access-key-id")
+ .setRoleArn("arn:aws:iam::123456789012:role/test-role")
+ .setCreationTime(CLOCK.instant())
+ .setExpiry(CLOCK.instant().plusSeconds(3600))
+ .setSecretAccessKey("secret-access-key")
+ .setSessionPolicy("session-policy")
+ .setEncryptionKey(ENCRYPTION_KEY)
+ .build());
}
private static OMRequest createRequestWithSessionToken(String accessId, boolean includeAccessId) {
@@ -258,7 +279,7 @@ private static OMRequest createRequestWithSessionToken(String accessId, boolean
private static final class TestConfig {
private OMMetadataManager metadataManager = mock(OMMetadataManager.class);
private Table revokedSTSTokenTable = new InMemoryTestTable<>();
- private boolean isTokenRevoked = false;
+ private Long revocationCutoffOffsetMs = null;
private boolean isOriginalAccessKeyIdRevoked = false;
private boolean shouldOriginalAccessKeyIdCheckThrowError = false;
private String requestAccessId = TEMP_ACCESS_KEY_ID;
@@ -277,9 +298,8 @@ TestConfig setRevokedSTSTokenTable(Table table) {
return this;
}
- @SuppressWarnings("SameParameterValue")
- TestConfig setTokenRevoked(boolean isRevoked) {
- this.isTokenRevoked = isRevoked;
+ TestConfig setRevocationCutoffOffsetMs(long offsetMs) {
+ this.revocationCutoffOffsetMs = offsetMs;
return this;
}
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSSecurityUtil.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSSecurityUtil.java
index d2033deabec..594ac858bfb 100644
--- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSSecurityUtil.java
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSSecurityUtil.java
@@ -17,6 +17,7 @@
package org.apache.hadoop.ozone.security;
+import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_TOKEN;
import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.TOKEN_EXPIRED;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -98,6 +99,7 @@ public void testConstructValidateAndDecryptSTSTokenSuccess() throws IOException
assertThat(result.getRoleArn()).isEqualTo(ROLE_ARN);
assertThat(result.getSecretAccessKey()).isEqualTo(SECRET_ACCESS_KEY);
assertThat(result.getSessionPolicy()).isEqualTo(SESSION_POLICY);
+ assertThat(result.getCreationTime()).isEqualTo(clock.instant());
assertThat(result.isExpired(clock.instant())).isFalse();
final long expirationEpochMillis = result.getExpiry().toEpochMilli();
assertThat(expirationEpochMillis).isEqualTo(clock.millis() + (DURATION_SECONDS * 1000));
@@ -126,6 +128,16 @@ public void testConstructValidateAndDecryptSTSTokenInvalidFormat() {
.hasMessageContaining("Invalid STS token format: Failed to decode STS token string");
}
+ @Test
+ public void testConstructValidateAndDecryptSTSTokenRuntimeDecodeFailure() {
+ assertThatThrownBy(() ->
+ STSSecurityUtil.constructValidateAndDecryptSTSToken("not-a-valid-token", secretKeyClient, clock))
+ .isInstanceOf(OMException.class)
+ .satisfies(exception -> assertThat(((OMException) exception).getResult()).isEqualTo(INVALID_TOKEN))
+ .hasMessageContaining("Invalid STS token format: Failed to decode STS token string")
+ .hasMessageContaining("NegativeArraySizeException");
+ }
+
@Test
public void testConstructValidateAndDecryptSTSTokenInvalidKind() throws Exception {
// Create a valid identifier to use as base
@@ -303,7 +315,8 @@ public void testConstructValidateAndDecryptSTSTokenEmptyString() {
assertThatThrownBy(() ->
STSSecurityUtil.constructValidateAndDecryptSTSToken("", secretKeyClient, clock))
.isInstanceOf(OMException.class)
- .hasMessage("Invalid STS token format: Failed to decode STS token string: java.io.EOFException");
+ .hasMessage(
+ "Invalid STS token format: Failed to decode STS token string: java.io.EOFException");
}
@Test
@@ -330,8 +343,7 @@ public void testConstructValidateAndDecryptMultipleTokens() throws Exception {
@Test
public void testEnsureEssentialFieldsArePresentInTokenMissingExpiry() {
- final STSTokenIdentifier tokenIdentifier = new STSTokenIdentifier(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, null, SECRET_ACCESS_KEY, SESSION_POLICY, ENCRYPTION_KEY);
+ final STSTokenIdentifier tokenIdentifier = new STSTokenIdentifier(paramsBuilder().setExpiry(null).build());
assertThatThrownBy(() -> STSSecurityUtil.ensureEssentialFieldsArePresentInToken(tokenIdentifier))
.isInstanceOf(SecretManager.InvalidToken.class)
@@ -340,8 +352,7 @@ public void testEnsureEssentialFieldsArePresentInTokenMissingExpiry() {
@Test
public void testEnsureEssentialFieldsArePresentInTokenMissingTempAccessKeyId() {
- final STSTokenIdentifier tokenIdentifier = new STSTokenIdentifier(
- null, ORIGINAL_ACCESS_KEY, ROLE_ARN, clock.instant(), SECRET_ACCESS_KEY, SESSION_POLICY, ENCRYPTION_KEY);
+ final STSTokenIdentifier tokenIdentifier = new STSTokenIdentifier(paramsBuilder().setTempAccessKeyId(null).build());
assertThatThrownBy(() -> STSSecurityUtil.ensureEssentialFieldsArePresentInToken(tokenIdentifier))
.isInstanceOf(SecretManager.InvalidToken.class)
@@ -350,8 +361,7 @@ public void testEnsureEssentialFieldsArePresentInTokenMissingTempAccessKeyId() {
@Test
public void testEnsureEssentialFieldsArePresentInTokenMissingRoleArn() {
- final STSTokenIdentifier tokenIdentifier = new STSTokenIdentifier(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, null, clock.instant(), SECRET_ACCESS_KEY, SESSION_POLICY, ENCRYPTION_KEY);
+ final STSTokenIdentifier tokenIdentifier = new STSTokenIdentifier(paramsBuilder().setRoleArn(null).build());
assertThatThrownBy(() -> STSSecurityUtil.ensureEssentialFieldsArePresentInToken(tokenIdentifier))
.isInstanceOf(SecretManager.InvalidToken.class)
@@ -361,7 +371,7 @@ public void testEnsureEssentialFieldsArePresentInTokenMissingRoleArn() {
@Test
public void testEnsureEssentialFieldsArePresentInTokenMissingOriginalAccessKeyId() {
final STSTokenIdentifier tokenIdentifier = new STSTokenIdentifier(
- TEMP_ACCESS_KEY, null, ROLE_ARN, clock.instant(), SECRET_ACCESS_KEY, SESSION_POLICY, ENCRYPTION_KEY);
+ paramsBuilder().setOriginalAccessKeyId(null).build());
assertThatThrownBy(() -> STSSecurityUtil.ensureEssentialFieldsArePresentInToken(tokenIdentifier))
.isInstanceOf(SecretManager.InvalidToken.class)
@@ -370,14 +380,22 @@ public void testEnsureEssentialFieldsArePresentInTokenMissingOriginalAccessKeyId
@Test
public void testEnsureEssentialFieldsArePresentInTokenMissingSecretAccessKey() {
- final STSTokenIdentifier tokenIdentifier = new STSTokenIdentifier(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, clock.instant(), null, SESSION_POLICY, ENCRYPTION_KEY);
+ final STSTokenIdentifier tokenIdentifier = new STSTokenIdentifier(paramsBuilder().setSecretAccessKey(null).build());
assertThatThrownBy(() -> STSSecurityUtil.ensureEssentialFieldsArePresentInToken(tokenIdentifier))
.isInstanceOf(SecretManager.InvalidToken.class)
.hasMessage("Invalid STS token - secretAccessKey is null/empty");
}
+ @Test
+ public void testEnsureEssentialFieldsArePresentInTokenMissingCreationTime() {
+ final STSTokenIdentifier tokenIdentifier = new STSTokenIdentifier(paramsBuilder().setCreationTime(null).build());
+
+ assertThatThrownBy(() -> STSSecurityUtil.ensureEssentialFieldsArePresentInToken(tokenIdentifier))
+ .isInstanceOf(SecretManager.InvalidToken.class)
+ .hasMessage("Invalid STS token - creationTime is null");
+ }
+
@Test
public void testEnsureResolvedStsFieldsInvariantsSuccess() throws Exception {
final String tokenString = tokenSecretManager.createSTSTokenString(
@@ -449,4 +467,16 @@ public void testEnsureResolvedStsFieldsInvariantsNoS3Auth() throws Exception {
// Should not throw
STSSecurityUtil.ensureResolvedStsFieldsInvariants(request);
}
+
+ private STSTokenIdentifier.Params.Builder paramsBuilder() {
+ return STSTokenIdentifier.Params.newBuilder()
+ .setTempAccessKeyId(TEMP_ACCESS_KEY)
+ .setOriginalAccessKeyId(ORIGINAL_ACCESS_KEY)
+ .setRoleArn(ROLE_ARN)
+ .setCreationTime(clock.instant())
+ .setExpiry(clock.instant().plusSeconds(DURATION_SECONDS))
+ .setSecretAccessKey(SECRET_ACCESS_KEY)
+ .setSessionPolicy(SESSION_POLICY)
+ .setEncryptionKey(ENCRYPTION_KEY);
+ }
}
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenEncryption.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenEncryption.java
index 1eb880f9dd0..870e99c3da1 100644
--- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenEncryption.java
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenEncryption.java
@@ -76,12 +76,21 @@ public void testSTSTokenIdentifierEncryption() throws Exception {
final String roleArn = "arn:aws:iam::123456789012:role/TestRole";
final String secretAccessKey = "mySecretAccessKey123456";
// Use millisecond precision to match serialization format
- final Instant expiry = Instant.ofEpochMilli(Instant.now().plusSeconds(3600).toEpochMilli());
+ final Instant creationTime = Instant.ofEpochMilli(1_700_000_000_000L);
+ final Instant expiry = creationTime.plusSeconds(3600);
final String sessionPolicy = "test-session-policy";
-
+
// Create token identifier with encryption
- final STSTokenIdentifier tokenId = new STSTokenIdentifier(
- tempAccessKeyId, originalAccessKeyId, roleArn, expiry, secretAccessKey, sessionPolicy, keyBytes);
+ final STSTokenIdentifier tokenId = new STSTokenIdentifier(STSTokenIdentifier.Params.newBuilder()
+ .setTempAccessKeyId(tempAccessKeyId)
+ .setOriginalAccessKeyId(originalAccessKeyId)
+ .setRoleArn(roleArn)
+ .setCreationTime(creationTime)
+ .setExpiry(expiry)
+ .setSecretAccessKey(secretAccessKey)
+ .setSessionPolicy(sessionPolicy)
+ .setEncryptionKey(keyBytes)
+ .build());
tokenId.setSecretKeyId(UUID.randomUUID());
// Convert to protobuf
@@ -100,6 +109,7 @@ public void testSTSTokenIdentifierEncryption() throws Exception {
assertEquals(roleArn, decodedTokenId.getRoleArn());
assertEquals(secretAccessKey, decodedTokenId.getSecretAccessKey());
assertEquals(expiry, decodedTokenId.getExpiry());
+ assertEquals(creationTime, decodedTokenId.getCreationTime());
}
@Test
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenIdentifier.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenIdentifier.java
index 09a786faaea..4ff08087e9f 100644
--- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenIdentifier.java
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenIdentifier.java
@@ -38,6 +38,7 @@
public class TestSTSTokenIdentifier {
private static final byte[] ENCRYPTION_KEY = new byte[5];
+ private static final Instant CREATION_TIME = Instant.ofEpochMilli(1_700_000_000_000L);
{
ThreadLocalRandom.current().nextBytes(ENCRYPTION_KEY);
@@ -45,9 +46,14 @@ public class TestSTSTokenIdentifier {
@Test
public void testKindAndService() {
- final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(
- "tempAccessKeyId", "originalAccessKeyId", "roleArn",
- Instant.now().plusSeconds(3600), "secretAccessKey", "sessionPolicy", ENCRYPTION_KEY);
+ final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccessKeyId")
+ .setOriginalAccessKeyId("originalAccessKeyId")
+ .setRoleArn("roleArn")
+ .setExpiry(Instant.now().plusSeconds(3600))
+ .setSecretAccessKey("secretAccessKey")
+ .setSessionPolicy("sessionPolicy")
+ .build());
assertEquals("STSToken", stsTokenIdentifier.getKind().toString());
assertEquals("STS", stsTokenIdentifier.getService());
@@ -59,9 +65,14 @@ public void testProtoBufRoundTrip() throws IOException {
// so use a millisecond-precision Instant to avoid nanos-only differences across
// platforms/JDKs during round-trips.
final Instant expiry = Instant.now().plusSeconds(7200).truncatedTo(ChronoUnit.MILLIS);
- final STSTokenIdentifier originalTokenIdentifier = new STSTokenIdentifier(
- "tempAccess", "origAccess", "arn:aws:iam::123456789012:role/RoleY",
- expiry, "secretKey", "sessionPolicy", ENCRYPTION_KEY);
+ final STSTokenIdentifier originalTokenIdentifier = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccess")
+ .setOriginalAccessKeyId("origAccess")
+ .setRoleArn("arn:aws:iam::123456789012:role/RoleY")
+ .setExpiry(expiry)
+ .setSecretAccessKey("secretKey")
+ .setSessionPolicy("sessionPolicy")
+ .build());
final UUID secretKeyId = UUID.randomUUID();
originalTokenIdentifier.setSecretKeyId(secretKeyId);
@@ -69,6 +80,7 @@ public void testProtoBufRoundTrip() throws IOException {
assertThat(proto.getType()).isEqualTo(OMTokenProto.Type.S3_STS_TOKEN);
assertThat(proto.getOwner()).isEqualTo("tempAccess");
assertThat(proto.getMaxDate()).isEqualTo(expiry.toEpochMilli());
+ assertThat(proto.getIssueDate()).isEqualTo(CREATION_TIME.toEpochMilli());
assertThat(proto.getOriginalAccessKeyId()).isEqualTo("origAccess");
assertThat(proto.getRoleArn()).isEqualTo("arn:aws:iam::123456789012:role/RoleY");
assertThat(proto.getSecretAccessKey()).isNotEqualTo("secretKey"); // must be encrypted
@@ -81,6 +93,7 @@ public void testProtoBufRoundTrip() throws IOException {
assertThat(parsedTokenIdentifier.getOwnerId()).isEqualTo("tempAccess");
assertThat(parsedTokenIdentifier.getExpiry()).isEqualTo(expiry);
+ assertThat(parsedTokenIdentifier.getCreationTime()).isEqualTo(CREATION_TIME);
assertThat(parsedTokenIdentifier.getOriginalAccessKeyId()).isEqualTo("origAccess");
assertThat(parsedTokenIdentifier.getRoleArn()).isEqualTo("arn:aws:iam::123456789012:role/RoleY");
assertThat(parsedTokenIdentifier.getSecretAccessKey()).isEqualTo("secretKey");
@@ -99,9 +112,14 @@ public void testFromProtoBufInvalidSecretKeyId() {
.setSecretKeyId("not-a-uuid")
.build();
- final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(
- "tempAccessKeyId", "originalAccessKeyId", "roleArn", Instant.now(),
- "secretAccessKey", "sessionPolicy", ENCRYPTION_KEY);
+ final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccessKeyId")
+ .setOriginalAccessKeyId("originalAccessKeyId")
+ .setRoleArn("roleArn")
+ .setExpiry(Instant.now())
+ .setSecretAccessKey("secretAccessKey")
+ .setSessionPolicy("sessionPolicy")
+ .build());
final IOException ex = assertThrows(IOException.class, () -> stsTokenIdentifier.fromProtoBuf(invalid));
assertThat(ex.getMessage()).isEqualTo("Invalid secretKeyId format in STS token: not-a-uuid");
@@ -110,9 +128,13 @@ public void testFromProtoBufInvalidSecretKeyId() {
@Test
public void testProtobufRoundTripWithNullSessionPolicy() throws IOException {
final Instant expiry = Instant.now().plusSeconds(7200);
- final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(
- "tempAccess", "origAccess", "arn:aws:iam::123456789012:role/RoleX",
- expiry, "secretKey", null, ENCRYPTION_KEY);
+ final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccess")
+ .setOriginalAccessKeyId("origAccess")
+ .setRoleArn("arn:aws:iam::123456789012:role/RoleX")
+ .setExpiry(expiry)
+ .setSecretAccessKey("secretKey")
+ .build());
final UUID secretKeyId = UUID.randomUUID();
stsTokenIdentifier.setSecretKeyId(secretKeyId);
@@ -129,9 +151,14 @@ public void testProtobufRoundTripWithNullSessionPolicy() throws IOException {
@Test
public void testProtobufRoundTripWithEmptySessionPolicy() throws IOException {
final Instant expiry = Instant.now().plusSeconds(4000);
- final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(
- "tempAccess", "origAccess", "arn:aws:iam::123456789012:role/RoleZ",
- expiry, "secretKey", "", ENCRYPTION_KEY);
+ final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccess")
+ .setOriginalAccessKeyId("origAccess")
+ .setRoleArn("arn:aws:iam::123456789012:role/RoleZ")
+ .setExpiry(expiry)
+ .setSecretAccessKey("secretKey")
+ .setSessionPolicy("")
+ .build());
final UUID secretKeyId = UUID.randomUUID();
stsTokenIdentifier.setSecretKeyId(secretKeyId);
@@ -153,9 +180,14 @@ public void testFromProtoBufInvalidTokenType() {
.setMaxDate(Instant.now().toEpochMilli())
.build();
- final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(
- "tempAccessKeyId", "origAccessKeyId", "roleArn", Instant.now(),
- "secretAccessKey", "sessionPolicy", ENCRYPTION_KEY);
+ final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccessKeyId")
+ .setOriginalAccessKeyId("origAccessKeyId")
+ .setRoleArn("roleArn")
+ .setExpiry(Instant.now())
+ .setSecretAccessKey("secretAccessKey")
+ .setSessionPolicy("sessionPolicy")
+ .build());
final IllegalArgumentException ex = assertThrows(
IllegalArgumentException.class, () -> stsTokenIdentifier.fromProtoBuf(invalidType));
@@ -169,9 +201,14 @@ public void testWriteToAndReadFromByteArray() throws Exception {
// compared to the original object, which is compared using equals().
final Instant expiry =
Instant.now().plusSeconds(1000).truncatedTo(ChronoUnit.MILLIS);
- final STSTokenIdentifier originalTokenIdentifier = new STSTokenIdentifier(
- "tempAccessKeyId", "originalAccessKeyId", "roleArn", expiry,
- "secretAccessKey", "sessionPolicy", ENCRYPTION_KEY);
+ final STSTokenIdentifier originalTokenIdentifier = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccessKeyId")
+ .setOriginalAccessKeyId("originalAccessKeyId")
+ .setRoleArn("roleArn")
+ .setExpiry(expiry)
+ .setSecretAccessKey("secretAccessKey")
+ .setSessionPolicy("sessionPolicy")
+ .build());
originalTokenIdentifier.setSecretKeyId(UUID.randomUUID());
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -196,9 +233,14 @@ public void testWriteToAndReadFromByteArrayWithDifferentSecretKeyIds() throws Ex
}
final Instant expiry = Instant.now().plusSeconds(1500);
- final STSTokenIdentifier originalTokenIdentifier = new STSTokenIdentifier(
- "tempAccessKeyId", "originalAccessKeyId", "roleArn", expiry,
- "secretAccessKey", "sessionPolicy", ENCRYPTION_KEY);
+ final STSTokenIdentifier originalTokenIdentifier = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccessKeyId")
+ .setOriginalAccessKeyId("originalAccessKeyId")
+ .setRoleArn("roleArn")
+ .setExpiry(expiry)
+ .setSecretAccessKey("secretAccessKey")
+ .setSessionPolicy("sessionPolicy")
+ .build());
originalTokenIdentifier.setSecretKeyId(uuid1);
final ByteArrayOutputStream baos1 = new ByteArrayOutputStream();
@@ -206,9 +248,14 @@ public void testWriteToAndReadFromByteArrayWithDifferentSecretKeyIds() throws Ex
originalTokenIdentifier.write(out);
}
- final STSTokenIdentifier anotherTokenIdentifier = new STSTokenIdentifier(
- "tempAccessKeyId", "originalAccessKeyId", "roleArn", expiry,
- "secretAccessKey", "sessionPolicy", ENCRYPTION_KEY);
+ final STSTokenIdentifier anotherTokenIdentifier = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccessKeyId")
+ .setOriginalAccessKeyId("originalAccessKeyId")
+ .setRoleArn("roleArn")
+ .setExpiry(expiry)
+ .setSecretAccessKey("secretAccessKey")
+ .setSessionPolicy("sessionPolicy")
+ .build());
anotherTokenIdentifier.setSecretKeyId(uuid2);
final ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
@@ -236,9 +283,14 @@ public void testWriteToAndReadFromByteArrayWithSameSecretKeyIds() throws Excepti
final UUID uuid = UUID.randomUUID();
final Instant expiry = Instant.now().plusSeconds(1700);
- final STSTokenIdentifier originalTokenIdentifier = new STSTokenIdentifier(
- "tempAccessKeyId", "originalAccessKeyId", "roleArn", expiry,
- "secretAccessKey", "sessionPolicy", ENCRYPTION_KEY);
+ final STSTokenIdentifier originalTokenIdentifier = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccessKeyId")
+ .setOriginalAccessKeyId("originalAccessKeyId")
+ .setRoleArn("roleArn")
+ .setExpiry(expiry)
+ .setSecretAccessKey("secretAccessKey")
+ .setSessionPolicy("sessionPolicy")
+ .build());
originalTokenIdentifier.setSecretKeyId(uuid);
final ByteArrayOutputStream baos1 = new ByteArrayOutputStream();
@@ -246,9 +298,14 @@ public void testWriteToAndReadFromByteArrayWithSameSecretKeyIds() throws Excepti
originalTokenIdentifier.write(out);
}
- final STSTokenIdentifier anotherTokenIdentifier = new STSTokenIdentifier(
- "tempAccessKeyId", "originalAccessKeyId", "roleArn", expiry,
- "secretAccessKey", "sessionPolicy", ENCRYPTION_KEY);
+ final STSTokenIdentifier anotherTokenIdentifier = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccessKeyId")
+ .setOriginalAccessKeyId("originalAccessKeyId")
+ .setRoleArn("roleArn")
+ .setExpiry(expiry)
+ .setSecretAccessKey("secretAccessKey")
+ .setSessionPolicy("sessionPolicy")
+ .build());
anotherTokenIdentifier.setSecretKeyId(uuid);
final ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
@@ -279,13 +336,20 @@ public void testGettersReturnCorrectValues() {
final String secretAccessKey = "mySecretKey";
final String sessionPolicy = "myPolicy";
- final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(
- tempAccessKeyId, originalAccessKeyId, roleArn, expiry, secretAccessKey, sessionPolicy, ENCRYPTION_KEY);
+ final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId(tempAccessKeyId)
+ .setOriginalAccessKeyId(originalAccessKeyId)
+ .setRoleArn(roleArn)
+ .setExpiry(expiry)
+ .setSecretAccessKey(secretAccessKey)
+ .setSessionPolicy(sessionPolicy)
+ .build());
assertThat(stsTokenIdentifier.getOwnerId()).isEqualTo(tempAccessKeyId);
assertThat(stsTokenIdentifier.getTempAccessKeyId()).isEqualTo(tempAccessKeyId);
assertThat(stsTokenIdentifier.getOriginalAccessKeyId()).isEqualTo(originalAccessKeyId);
assertThat(stsTokenIdentifier.getRoleArn()).isEqualTo(roleArn);
+ assertThat(stsTokenIdentifier.getCreationTime()).isEqualTo(CREATION_TIME);
assertThat(stsTokenIdentifier.getExpiry()).isEqualTo(expiry);
assertThat(stsTokenIdentifier.getSecretAccessKey()).isEqualTo(secretAccessKey);
assertThat(stsTokenIdentifier.getSessionPolicy()).isEqualTo(sessionPolicy);
@@ -296,14 +360,24 @@ public void testEqualsAndHashCode() {
final Instant expiry = Instant.now().plusSeconds(3600);
final UUID uuid = UUID.randomUUID();
- final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(
- "tempAccessKeyId", "originalAccessKeyId", "roleArn", expiry,
- "secretAccessKey", "sessionPolicy", ENCRYPTION_KEY);
+ final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccessKeyId")
+ .setOriginalAccessKeyId("originalAccessKeyId")
+ .setRoleArn("roleArn")
+ .setExpiry(expiry)
+ .setSecretAccessKey("secretAccessKey")
+ .setSessionPolicy("sessionPolicy")
+ .build());
stsTokenIdentifier.setSecretKeyId(uuid);
- final STSTokenIdentifier stsTokenIdentifier2 = new STSTokenIdentifier(
- "tempAccessKeyId", "originalAccessKeyId", "roleArn", expiry,
- "secretAccessKey", "sessionPolicy", ENCRYPTION_KEY);
+ final STSTokenIdentifier stsTokenIdentifier2 = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccessKeyId")
+ .setOriginalAccessKeyId("originalAccessKeyId")
+ .setRoleArn("roleArn")
+ .setExpiry(expiry)
+ .setSecretAccessKey("secretAccessKey")
+ .setSessionPolicy("sessionPolicy")
+ .build());
stsTokenIdentifier2.setSecretKeyId(uuid);
assertThat(stsTokenIdentifier).isEqualTo(stsTokenIdentifier2);
@@ -314,13 +388,23 @@ public void testEqualsAndHashCode() {
public void testNotEqualsWhenTempAccessKeyIdDiffers() {
final Instant expiry = Instant.now().plusSeconds(3600);
- final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(
- "tempAccessKeyId1", "originalAccessKeyId", "roleArn",
- expiry, "secretAccessKey", "sessionPolicy", ENCRYPTION_KEY);
-
- final STSTokenIdentifier stsTokenIdentifier2 = new STSTokenIdentifier(
- "tempAccessKeyId2", "originalAccessKeyId", "roleArn", expiry,
- "secretAccessKey", "sessionPolicy", ENCRYPTION_KEY);
+ final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccessKeyId1")
+ .setOriginalAccessKeyId("originalAccessKeyId")
+ .setRoleArn("roleArn")
+ .setExpiry(expiry)
+ .setSecretAccessKey("secretAccessKey")
+ .setSessionPolicy("sessionPolicy")
+ .build());
+
+ final STSTokenIdentifier stsTokenIdentifier2 = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccessKeyId2")
+ .setOriginalAccessKeyId("originalAccessKeyId")
+ .setRoleArn("roleArn")
+ .setExpiry(expiry)
+ .setSecretAccessKey("secretAccessKey")
+ .setSessionPolicy("sessionPolicy")
+ .build());
assertThat(stsTokenIdentifier).isNotEqualTo(stsTokenIdentifier2);
}
@@ -329,13 +413,23 @@ public void testNotEqualsWhenTempAccessKeyIdDiffers() {
public void testNotEqualsWhenOriginalAccessKeyIdDiffers() {
final Instant expiry = Instant.now().plusSeconds(3600);
- final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(
- "tempAccessKeyId", "originalAccessKeyId1", "roleArn", expiry,
- "secretAccessKey", "sessionPolicy", ENCRYPTION_KEY);
-
- final STSTokenIdentifier stsTokenIdentifier2 = new STSTokenIdentifier(
- "tempAccessKeyId", "originalAccessKeyId2", "roleArn", expiry,
- "secretAccessKey", "sessionPolicy", ENCRYPTION_KEY);
+ final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccessKeyId")
+ .setOriginalAccessKeyId("originalAccessKeyId1")
+ .setRoleArn("roleArn")
+ .setExpiry(expiry)
+ .setSecretAccessKey("secretAccessKey")
+ .setSessionPolicy("sessionPolicy")
+ .build());
+
+ final STSTokenIdentifier stsTokenIdentifier2 = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccessKeyId")
+ .setOriginalAccessKeyId("originalAccessKeyId2")
+ .setRoleArn("roleArn")
+ .setExpiry(expiry)
+ .setSecretAccessKey("secretAccessKey")
+ .setSessionPolicy("sessionPolicy")
+ .build());
assertThat(stsTokenIdentifier).isNotEqualTo(stsTokenIdentifier2);
}
@@ -344,26 +438,46 @@ public void testNotEqualsWhenOriginalAccessKeyIdDiffers() {
public void testNotEqualsWhenRoleArnDiffers() {
final Instant expiry = Instant.now().plusSeconds(3600);
- final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(
- "tempAccessKeyId", "originalAccessKeyId", "roleArn1", expiry,
- "secretAccessKey", "sessionPolicy", ENCRYPTION_KEY);
-
- final STSTokenIdentifier stsTokenIdentifier2 = new STSTokenIdentifier(
- "tempAccessKeyId", "originalAccessKeyId", "roleArn2", expiry,
- "secretAccessKey", "sessionPolicy", ENCRYPTION_KEY);
+ final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccessKeyId")
+ .setOriginalAccessKeyId("originalAccessKeyId")
+ .setRoleArn("roleArn1")
+ .setExpiry(expiry)
+ .setSecretAccessKey("secretAccessKey")
+ .setSessionPolicy("sessionPolicy")
+ .build());
+
+ final STSTokenIdentifier stsTokenIdentifier2 = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccessKeyId")
+ .setOriginalAccessKeyId("originalAccessKeyId")
+ .setRoleArn("roleArn2")
+ .setExpiry(expiry)
+ .setSecretAccessKey("secretAccessKey")
+ .setSessionPolicy("sessionPolicy")
+ .build());
assertThat(stsTokenIdentifier).isNotEqualTo(stsTokenIdentifier2);
}
@Test
public void testNotEqualsWhenExpirationDiffers() {
- final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(
- "tempAccessKeyId", "originalAccessKeyId", "roleArn",
- Instant.now().plusSeconds(3600), "secretAccessKey", "sessionPolicy", ENCRYPTION_KEY);
-
- final STSTokenIdentifier stsTokenIdentifier2 = new STSTokenIdentifier(
- "tempAccessKeyId", "originalAccessKeyId", "roleArn",
- Instant.now().plusSeconds(7600), "secretAccessKey", "sessionPolicy", ENCRYPTION_KEY);
+ final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccessKeyId")
+ .setOriginalAccessKeyId("originalAccessKeyId")
+ .setRoleArn("roleArn")
+ .setExpiry(Instant.now().plusSeconds(3600))
+ .setSecretAccessKey("secretAccessKey")
+ .setSessionPolicy("sessionPolicy")
+ .build());
+
+ final STSTokenIdentifier stsTokenIdentifier2 = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccessKeyId")
+ .setOriginalAccessKeyId("originalAccessKeyId")
+ .setRoleArn("roleArn")
+ .setExpiry(Instant.now().plusSeconds(7600))
+ .setSecretAccessKey("secretAccessKey")
+ .setSessionPolicy("sessionPolicy")
+ .build());
assertThat(stsTokenIdentifier).isNotEqualTo(stsTokenIdentifier2);
}
@@ -372,13 +486,23 @@ public void testNotEqualsWhenExpirationDiffers() {
public void testNotEqualsWhenSecretAccessKeyDiffers() {
final Instant expiry = Instant.now().plusSeconds(3600);
- final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(
- "tempAccessKeyId", "originalAccessKeyId", "roleArn", expiry,
- "secretAccessKey1", "sessionPolicy", ENCRYPTION_KEY);
-
- final STSTokenIdentifier stsTokenIdentifier2 = new STSTokenIdentifier(
- "tempAccessKeyId", "originalAccessKeyId", "roleArn", expiry,
- "secretAccessKey2", "sessionPolicy", ENCRYPTION_KEY);
+ final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccessKeyId")
+ .setOriginalAccessKeyId("originalAccessKeyId")
+ .setRoleArn("roleArn")
+ .setExpiry(expiry)
+ .setSecretAccessKey("secretAccessKey1")
+ .setSessionPolicy("sessionPolicy")
+ .build());
+
+ final STSTokenIdentifier stsTokenIdentifier2 = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccessKeyId")
+ .setOriginalAccessKeyId("originalAccessKeyId")
+ .setRoleArn("roleArn")
+ .setExpiry(expiry)
+ .setSecretAccessKey("secretAccessKey2")
+ .setSessionPolicy("sessionPolicy")
+ .build());
assertThat(stsTokenIdentifier).isNotEqualTo(stsTokenIdentifier2);
}
@@ -387,13 +511,23 @@ public void testNotEqualsWhenSecretAccessKeyDiffers() {
public void testNotEqualsWhenSessionPolicyDiffers() {
final Instant expiry = Instant.now().plusSeconds(3600);
- final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(
- "tempAccessKeyId", "originalAccessKeyId", "roleArn", expiry,
- "secretAccessKey", "sessionPolicy1", ENCRYPTION_KEY);
-
- final STSTokenIdentifier stsTokenIdentifier2 = new STSTokenIdentifier(
- "tempAccessKeyId", "originalAccessKeyId", "roleArn", expiry,
- "secretAccessKey", "sessionPolicy2", ENCRYPTION_KEY);
+ final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccessKeyId")
+ .setOriginalAccessKeyId("originalAccessKeyId")
+ .setRoleArn("roleArn")
+ .setExpiry(expiry)
+ .setSecretAccessKey("secretAccessKey")
+ .setSessionPolicy("sessionPolicy1")
+ .build());
+
+ final STSTokenIdentifier stsTokenIdentifier2 = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccessKeyId")
+ .setOriginalAccessKeyId("originalAccessKeyId")
+ .setRoleArn("roleArn")
+ .setExpiry(expiry)
+ .setSecretAccessKey("secretAccessKey")
+ .setSessionPolicy("sessionPolicy2")
+ .build());
assertThat(stsTokenIdentifier).isNotEqualTo(stsTokenIdentifier2);
}
@@ -403,14 +537,20 @@ public void testToString() {
final Instant expiry = Instant.now().plusSeconds(3600);
final UUID uuid = UUID.randomUUID();
- final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(
- "tempAccessKeyId", "originalAccessKeyId", "roleArn", expiry,
- "secretAccessKey", "sessionPolicy", ENCRYPTION_KEY);
+ final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccessKeyId")
+ .setOriginalAccessKeyId("originalAccessKeyId")
+ .setRoleArn("roleArn")
+ .setExpiry(expiry)
+ .setSecretAccessKey("secretAccessKey")
+ .setSessionPolicy("sessionPolicy")
+ .build());
stsTokenIdentifier.setSecretKeyId(uuid);
final String stsTokenIdentifierStr = stsTokenIdentifier.toString();
final String expectedString = "STSTokenIdentifier{" + "tempAccessKeyId='tempAccessKeyId'" +
- ", originalAccessKeyId='originalAccessKeyId'" + ", roleArn='roleArn'" + ", expiry='" + expiry +
+ ", originalAccessKeyId='originalAccessKeyId'" + ", roleArn='roleArn'" +
+ ", creationTime='" + CREATION_TIME + "', expiry='" + expiry +
"', secretKeyId='" + uuid + "', sessionPolicy='sessionPolicy'" + '}';
assertEquals(expectedString, stsTokenIdentifierStr);
@@ -418,9 +558,14 @@ public void testToString() {
@Test
public void testNotEqualsWithNull() {
- final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(
- "tempAccessKeyId", "originalAccessKeyId", "roleArn", Instant.now(),
- "secretAccessKey", "sessionPolicy", ENCRYPTION_KEY);
+ final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccessKeyId")
+ .setOriginalAccessKeyId("originalAccessKeyId")
+ .setRoleArn("roleArn")
+ .setExpiry(Instant.now())
+ .setSecretAccessKey("secretAccessKey")
+ .setSessionPolicy("sessionPolicy")
+ .build());
assertThat(stsTokenIdentifier).isNotEqualTo(null);
}
@@ -431,24 +576,39 @@ public void testEqualsWithDifferentEncryptionKeys() {
final UUID uuid = UUID.randomUUID();
// Create first identifier with the default key
- final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(
- "tempAccessKeyId", "originalAccessKeyId", "roleArn", expiry,
- "secretAccessKey", "sessionPolicy", ENCRYPTION_KEY);
+ final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccessKeyId")
+ .setOriginalAccessKeyId("originalAccessKeyId")
+ .setRoleArn("roleArn")
+ .setExpiry(expiry)
+ .setSecretAccessKey("secretAccessKey")
+ .setSessionPolicy("sessionPolicy")
+ .build());
stsTokenIdentifier.setSecretKeyId(uuid);
// Create second identifier with a different encryption key but otherwise same parameters
byte[] differentKey = new byte[5];
new SecureRandom().nextBytes(differentKey);
- final STSTokenIdentifier stsTokenIdentifier2 = new STSTokenIdentifier(
- "tempAccessKeyId", "originalAccessKeyId", "roleArn", expiry,
- "secretAccessKey", "sessionPolicy", differentKey);
+ final STSTokenIdentifier stsTokenIdentifier2 = new STSTokenIdentifier(paramsBuilder()
+ .setTempAccessKeyId("tempAccessKeyId")
+ .setOriginalAccessKeyId("originalAccessKeyId")
+ .setRoleArn("roleArn")
+ .setExpiry(expiry)
+ .setSecretAccessKey("secretAccessKey")
+ .setSessionPolicy("sessionPolicy")
+ .setEncryptionKey(differentKey)
+ .build());
stsTokenIdentifier2.setSecretKeyId(uuid);
// They should still be equal because encryptionKey is transient/ignored for identity
assertThat(stsTokenIdentifier).isEqualTo(stsTokenIdentifier2);
assertThat(stsTokenIdentifier.hashCode()).isEqualTo(stsTokenIdentifier2.hashCode());
}
-}
-
+ private static STSTokenIdentifier.Params.Builder paramsBuilder() {
+ return STSTokenIdentifier.Params.newBuilder()
+ .setCreationTime(CREATION_TIME)
+ .setEncryptionKey(ENCRYPTION_KEY);
+ }
+}
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java
index 800aeabe97c..525ee5f0da0 100644
--- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java
@@ -98,6 +98,7 @@ public void testCreateSTSTokenStringContainsCorrectFields() throws IOException {
assertEquals(ROLE_ARN, identifier.getRoleArn());
assertEquals(SECRET_ACCESS_KEY, identifier.getSecretAccessKey());
assertEquals(SESSION_POLICY, identifier.getSessionPolicy());
+ assertEquals(clock.instant(), identifier.getCreationTime());
assertNotNull(identifier.getSecretKeyId());
assertEquals(new Text("STSToken"), identifier.getKind());
assertEquals("STS", identifier.getService());
diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
index 63e25fca628..c8c7d168b18 100644
--- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
+++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
@@ -605,6 +605,14 @@ protected AuditMessage.Builder auditMessageFor(AuditAction op) {
Map auditMap = getAuditParameters();
auditMap.put("x-amz-request-id", requestIdentifier.getRequestId());
auditMap.put("x-amz-id-2", requestIdentifier.getAmzId());
+ if (s3Auth != null) {
+ // For STS temporary credentials, record the originalAccessKeyId (the permanent principal that
+ // created the token) so the audit trail is not limited to the opaque tempAccessKeyId.
+ final String originalAccessKeyId = AuditUtils.getStsOriginalAccessKeyId(s3Auth.getSessionToken());
+ if (originalAccessKeyId != null) {
+ auditMap.put(OzoneConsts.S3_STS_ORIGINAL_ACCESS_KEY_ID, originalAccessKeyId);
+ }
+ }
AuditMessage.Builder builder = new AuditMessage.Builder()
.forOperation(op)
diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/AuditUtils.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/AuditUtils.java
index 978f96ddb35..85486df8c3d 100644
--- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/AuditUtils.java
+++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/AuditUtils.java
@@ -19,10 +19,13 @@
import static org.apache.hadoop.ozone.s3.ClientIpFilter.CLIENT_IP_HEADER;
+import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.ws.rs.container.ContainerRequestContext;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto;
+import org.apache.hadoop.security.token.Token;
/**
* Common utilities for operation auditing purposes.
@@ -31,6 +34,26 @@ public final class AuditUtils {
private AuditUtils() {
}
+ /**
+ * Extracts (if possible) the STS {@code originalAccessKeyId} from a session token so it can be
+ * recorded in the S3 Gateway audit log. Like the S3 access id already recorded as the
+ * audit user, this reflects what the client presented. Returns {@code null} when no usable value
+ * can be extracted so the audit path is never disrupted by a missing or malformed token.
+ */
+ public static String getStsOriginalAccessKeyId(String sessionToken) {
+ if (sessionToken == null || sessionToken.isEmpty()) {
+ return null;
+ }
+ try {
+ final Token> token = new Token<>();
+ token.decodeFromUrlString(sessionToken);
+ final String originalAccessKeyId = OMTokenProto.parseFrom(token.getIdentifier()).getOriginalAccessKeyId();
+ return originalAccessKeyId.isEmpty() ? null : originalAccessKeyId;
+ } catch (IOException | RuntimeException e) {
+ return null;
+ }
+ }
+
public static Map getAuditParameters(
ContainerRequestContext context) {
Map res = new HashMap<>();
diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestEndpointBase.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestEndpointBase.java
index 9865345a916..0f23eb221ef 100644
--- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestEndpointBase.java
+++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestEndpointBase.java
@@ -36,10 +36,16 @@
import java.util.stream.Stream;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
+import org.apache.hadoop.io.Text;
import org.apache.hadoop.ozone.OzoneConsts;
+import org.apache.hadoop.ozone.audit.AuditMessage;
+import org.apache.hadoop.ozone.audit.S3GAction;
import org.apache.hadoop.ozone.client.OzoneVolume;
import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto;
import org.apache.hadoop.ozone.s3.exception.OS3Exception;
+import org.apache.hadoop.ozone.s3.signature.SignatureInfo;
+import org.apache.hadoop.security.token.Token;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
@@ -49,6 +55,7 @@
* Test methods of the EndpointBase.
*/
public class TestEndpointBase {
+ private static final String ORIGINAL_ACCESS_KEY_ID_PARAM = "originalAccessKeyId";
/**
* Verify s3 metadata key "gdprEnabled" can't be set up directly
@@ -146,6 +153,38 @@ public void init() { }
assertFalse(endpointBase.isExpiredToken(new OMException(ResultCodes.INVALID_TOKEN)));
}
+ @Test
+ public void testAuditMessageIncludesStsOriginalAccessKeyId() throws Exception {
+ final String originalAccessKeyId = "AKIAORIGINAL123";
+ final OMTokenProto proto = OMTokenProto.newBuilder()
+ .setType(OMTokenProto.Type.S3_STS_TOKEN)
+ .setOriginalAccessKeyId(originalAccessKeyId)
+ .build();
+ final SignatureInfo signatureInfo = new SignatureInfo.Builder(SignatureInfo.Version.V4)
+ .setAwsAccessId("ASIAEXAMPLE123")
+ .setSignature("signature")
+ .setStringToSign("string-to-sign")
+ .setSessionToken(encodeSessionToken(proto))
+ .build();
+ final AuditEndpoint endpointBase = newAuditEndpoint(signatureInfo);
+
+ assertThat(endpointBase.auditMessageForTest().getParams())
+ .containsEntry(ORIGINAL_ACCESS_KEY_ID_PARAM, originalAccessKeyId);
+ }
+
+ @Test
+ public void testAuditMessageOmitsStsOriginalAccessKeyIdForNonStsRequest() {
+ final SignatureInfo signatureInfo = new SignatureInfo.Builder(SignatureInfo.Version.V4)
+ .setAwsAccessId("AKIAEXAMPLE123")
+ .setSignature("signature")
+ .setStringToSign("string-to-sign")
+ .build();
+ final AuditEndpoint endpointBase = newAuditEndpoint(signatureInfo);
+
+ assertThat(endpointBase.auditMessageForTest().getParams())
+ .doesNotContainKey(ORIGINAL_ACCESS_KEY_ID_PARAM);
+ }
+
@Test
public void testListS3BucketsHandlesRuntimeExceptionWrappingOMException() throws Exception {
final EndpointBase endpointBase = new EndpointBase() {
@@ -209,4 +248,22 @@ private static Stream reservedInternalMetadataKeyPrefixCases() {
RESERVED_USER_METADATA_KEY_PREFIX.toUpperCase(Locale.ROOT) + "cache-control");
}
+ private static String encodeSessionToken(OMTokenProto proto) throws Exception {
+ final Token> token = new Token<>(
+ proto.toByteArray(), new byte[0], new Text("OzoneToken"), new Text("sts"));
+ return token.encodeToUrlString();
+ }
+
+ private static AuditEndpoint newAuditEndpoint(SignatureInfo signatureInfo) {
+ return new EndpointBuilder<>(AuditEndpoint::new)
+ .setSignatureInfo(signatureInfo)
+ .build();
+ }
+
+ private static final class AuditEndpoint extends EndpointBase {
+ private AuditMessage.Builder auditMessageForTest() {
+ return auditMessageFor(S3GAction.GET_KEY);
+ }
+ }
+
}
diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestAuditUtils.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestAuditUtils.java
new file mode 100644
index 00000000000..b5952290dc9
--- /dev/null
+++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestAuditUtils.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.s3.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.apache.hadoop.io.Text;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto;
+import org.apache.hadoop.security.token.Token;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for {@link AuditUtils}. */
+public class TestAuditUtils {
+
+ @Test
+ public void extractsOriginalAccessKeyIdFromSessionToken() throws Exception {
+ final OMTokenProto proto = OMTokenProto.newBuilder()
+ .setType(OMTokenProto.Type.S3_STS_TOKEN)
+ .setOriginalAccessKeyId("AKIAORIGINAL123")
+ .build();
+
+ assertEquals("AKIAORIGINAL123", AuditUtils.getStsOriginalAccessKeyId(encodeSessionToken(proto)));
+ }
+
+ @Test
+ public void returnsNullWhenOriginalAccessKeyIdAbsent() throws Exception {
+ final OMTokenProto proto = OMTokenProto.newBuilder()
+ .setType(OMTokenProto.Type.S3_STS_TOKEN)
+ .build();
+
+ assertNull(AuditUtils.getStsOriginalAccessKeyId(encodeSessionToken(proto)));
+ }
+
+ @Test
+ public void returnsNullForNullEmptyOrMalformedToken() {
+ assertNull(AuditUtils.getStsOriginalAccessKeyId(null));
+ assertNull(AuditUtils.getStsOriginalAccessKeyId(""));
+ assertNull(AuditUtils.getStsOriginalAccessKeyId("not-a-valid-token"));
+ }
+
+ private static String encodeSessionToken(OMTokenProto proto) throws Exception {
+ final Token> token = new Token<>(
+ proto.toByteArray(), new byte[0], new Text("OzoneToken"), new Text("sts"));
+ return token.encodeToUrlString();
+ }
+}
From 106deb661f26db691db9f637ef6d8be6ba902880 Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Sun, 16 Aug 2026 16:58:53 -0700
Subject: [PATCH 06/20] redesign: update smoke tests
---
.../smoketest/security/ozone-secure-sts.robot | 38 +++++++++++++++----
1 file changed, 31 insertions(+), 7 deletions(-)
diff --git a/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot b/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot
index c0362d74704..4a01bfd93ee 100644
--- a/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot
+++ b/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot
@@ -66,6 +66,7 @@ ${ACTION_MATCHES_PUTOBJECT_CREATE_WRITE_ROLE_ARN} arn:aws:iam::123456789012:rol
${ACTION_MATCHES_GETOBJECT_PUTOBJECT_ROLE_ARN} arn:aws:iam::123456789012:role/${ACTION_MATCHES_GETOBJECT_PUTOBJECT_ROLE}
${ACTION_MATCHES_UPLOADPARTCOPY_EXPECTED_OWNER_ROLE_ARN} arn:aws:iam::123456789012:role/${ACTION_MATCHES_UPLOADPARTCOPY_EXPECTED_OWNER_ROLE}
${ACTION_MATCHES_GET_STAR_READ_ROLE_ARN} arn:aws:iam::123456789012:role/${ACTION_MATCHES_GET_STAR_READ_ROLE}
+${TEST_USER_ADMIN} testuser
${TEST_USER_NON_ADMIN} testuser2
@{ICEBERG_OBJECT_KEYS} file1.txt file1again.txt folder/pepper.txt folder/salt.txt userA/userA.txt userB/userB.txt userAfile.txt
@{ICEBERG_LISTABLE_OBJECT_KEYS_OBS} file1.txt file1again.txt folder/pepper.txt folder/salt.txt userA/userA.txt userB/userB.txt userAfile.txt zeroByteFile zeroByteFolder/
@@ -254,6 +255,17 @@ Configure STS Profile With Bogus Credential Part
Configure STS Profile ${STS_ACCESS_KEY_ID} ${STS_SECRET_KEY} bogusSessionToken
END
+Verify STS Token Revocation And Post Revocation Assume Role
+ [Arguments] ${bucket} ${role_arn} ${revoker_user} ${revoker_keytab}
+ Assume Role And Configure STS Profile perm_access_key_id=${PERMANENT_ACCESS_KEY_ID} perm_secret_key=${PERMANENT_SECRET_KEY} role_arn=${role_arn}
+ Get Object Should Succeed ${bucket} ${ICEBERG_BUCKET_TESTFILE}
+ Kinit test user ${revoker_user} ${revoker_keytab}
+ ${output} = Execute ozone s3 revokeststoken -o ${PERMANENT_ACCESS_KEY_ID} -y ${OM_HA_PARAM}
+ Should Contain ${output} STS tokens revoked for originalAccessKeyId
+ Get Object Should Fail ${bucket} ${ICEBERG_BUCKET_TESTFILE} AccessDenied
+ Assume Role And Configure STS Profile perm_access_key_id=${PERMANENT_ACCESS_KEY_ID} perm_secret_key=${PERMANENT_SECRET_KEY} role_arn=${role_arn}
+ Get Object Should Succeed ${bucket} ${ICEBERG_BUCKET_TESTFILE}
+
*** Test Cases ***
Create User in Ranger
${user_json} = Set Variable { "loginId": "${ICEBERG_SVC_CATALOG_USER}", "name": "${ICEBERG_SVC_CATALOG_USER}", "password": "Password123", "firstName": "Iceberg REST", "lastName": "Catalog", "emailAddress": "${ICEBERG_SVC_CATALOG_USER}@example.com", "userRoleList": ["ROLE_USER"], "userPermList": [ { "moduleId": 1, "isAllowed": 1 }, { "moduleId": 3, "isAllowed": 1 }, { "moduleId": 7, "isAllowed": 1 } ] }
@@ -558,27 +570,32 @@ Verify Token Revocation via CLI
FOR ${bucket} ${role_arn} IN
... ${ICEBERG_BUCKET_OBS} ${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
... ${ICEBERG_BUCKET_FSO} ${ICEBERG_ALL_ACCESS_ROLE_FSO_ARN}
- Assume Role And Configure STS Profile perm_access_key_id=${PERMANENT_ACCESS_KEY_ID} perm_secret_key=${PERMANENT_SECRET_KEY} role_arn=${role_arn}
- ${output} = Execute ozone s3 revokeststoken -t ${STS_SESSION_TOKEN} -y ${OM_HA_PARAM}
- Should Contain ${output} STS token revoked for sessionToken
- # Trying to use the token for even get-object should now fail.
- Get Object Should Fail ${bucket} ${ICEBERG_BUCKET_TESTFILE} AccessDenied
+ # Owner of the original access key can revoke the STS token.
+ Verify STS Token Revocation And Post Revocation Assume Role ${bucket} ${role_arn} ${ICEBERG_SVC_CATALOG_USER} ${ICEBERG_SVC_CATALOG_USER}.keytab
+ # S3 admin can also revoke an STS token owned by another user.
+ Verify STS Token Revocation And Post Revocation Assume Role ${bucket} ${role_arn} ${TEST_USER_ADMIN} ${TEST_USER_ADMIN}.keytab
END
Non-Admin Cannot Revoke STS Token
FOR ${role_arn} IN ${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN} ${ICEBERG_ALL_ACCESS_ROLE_FSO_ARN}
# Create a token first.
Assume Role And Get Temporary Credentials perm_access_key_id=${PERMANENT_ACCESS_KEY_ID} perm_secret_key=${PERMANENT_SECRET_KEY} role_arn=${role_arn}
- ${token_to_revoke} = Set Variable ${STS_SESSION_TOKEN}
# Kinit as non-admin user.
Kinit test user ${TEST_USER_NON_ADMIN} ${TEST_USER_NON_ADMIN}.keytab
# Try to revoke - should give USER_MISMATCH error.
- ${output} = Execute And Ignore Error ozone s3 revokeststoken -t ${token_to_revoke} -y ${OM_HA_PARAM}
+ ${output} = Execute And Ignore Error ozone s3 revokeststoken -o ${PERMANENT_ACCESS_KEY_ID} -y ${OM_HA_PARAM}
Should Contain ${output} USER_MISMATCH
END
+Revoke STS Token Should Fail For Unknown Original Access Key Id
+ # Revoking a bogus originalAccessKeyId must fail before writing to the revocation table.
+ Kinit test user ${TEST_USER_ADMIN} ${TEST_USER_ADMIN}.keytab
+ ${output} = Execute And Ignore Error ozone s3 revokeststoken -o bogus-original-access-key-id -y ${OM_HA_PARAM}
+ Should Contain ${output} INVALID_REQUEST
+ Should Contain ${output} does not exist
+
List Objects V1 and V2 IAM Session Policy Matrix for OBS and FSO
Kinit test user ${ICEBERG_SVC_CATALOG_USER} ${ICEBERG_SVC_CATALOG_USER}.keytab
@@ -613,6 +630,13 @@ Tampered STS Token Service, Policy, or Signature Must Fail
Get Object Should Fail ${ICEBERG_BUCKET_OBS} ${ICEBERG_BUCKET_TESTFILE} AccessDenied
Put Object Should Fail ${ICEBERG_BUCKET_OBS} ${ICEBERG_BUCKET_TESTFILE} AccessDenied
+ # Exercise malformed token decoding with incorrect token structure. Unlike the earlier
+ # bogusSessionToken credential-part check, this literal decodes to a negative Writable
+ # length and covers unchecked decoder failures such as NegativeArraySizeException.
+ Configure STS Profile ${STS_ACCESS_KEY_ID} ${STS_SECRET_KEY} not-a-valid-token
+ Get Object Should Fail ${ICEBERG_BUCKET_OBS} ${ICEBERG_BUCKET_TESTFILE} AccessDenied
+ Put Object Should Fail ${ICEBERG_BUCKET_OBS} ${ICEBERG_BUCKET_TESTFILE} AccessDenied
+
Assume Role Session Policy With Multiple Buckets Should Access All Buckets
${session_policy} = Set Variable {"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}/*"},{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_FSO}/*"}]}
Assume Role And Get Temporary Credentials policy_json=${session_policy} perm_access_key_id=${PERMANENT_ACCESS_KEY_ID} perm_secret_key=${PERMANENT_SECRET_KEY} role_arn=${ICEBERG_MULTI_BUCKET_ROLE_ARN}
From 7ab385757c204d3cc20ce919d0e09fb2ae18770f Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Sun, 23 Aug 2026 13:41:26 -0700
Subject: [PATCH 07/20] redesign: ensure only validated originalAccessKeyId is
added to audit log
---
.../ozone/client/protocol/ClientProtocol.java | 5 +-
.../hadoop/ozone/client/rpc/RpcClient.java | 33 ++-
.../ozone/client/rpc/TestRpcClient.java | 107 +++++++++-
.../om/helpers/KeyInfoWithVolumeContext.java | 26 ++-
.../ozone/om/helpers/S3VolumeContext.java | 35 +++-
.../hadoop/ozone/om/protocol/S3Auth.java | 10 +
.../helpers/TestKeyInfoWithVolumeContext.java | 70 +++++++
.../ozone/om/helpers/TestS3VolumeContext.java | 66 ++++++
.../src/main/proto/OmClientProtocol.proto | 4 +
.../hadoop/ozone/om/OmMetadataReader.java | 1 +
.../apache/hadoop/ozone/om/OmSnapshot.java | 1 +
.../apache/hadoop/ozone/om/OzoneManager.java | 4 +
.../ozone/security/TestS3SecurityUtil.java | 27 ++-
.../ozone/security/TestSTSSecurityUtil.java | 20 ++
.../ozone/s3/endpoint/BucketEndpoint.java | 4 +-
.../ozone/s3/endpoint/EndpointBase.java | 36 +++-
.../hadoop/ozone/s3/util/AuditUtils.java | 23 ---
.../ozone/s3/endpoint/TestEndpointBase.java | 188 ++++++++++++++++--
.../hadoop/ozone/s3/util/TestAuditUtils.java | 62 ------
19 files changed, 601 insertions(+), 121 deletions(-)
create mode 100644 hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestKeyInfoWithVolumeContext.java
create mode 100644 hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestS3VolumeContext.java
delete mode 100644 hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestAuditUtils.java
diff --git a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
index b5f7baa0ef2..7900fa4a41f 100644
--- a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
+++ b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
@@ -142,8 +142,9 @@ OzoneVolume getVolumeDetails(String volumeName)
throws IOException;
/**
- * @return Raw GetS3VolumeContextResponse.
- * S3Auth won't be updated with actual userPrincipal by this call.
+ * @return S3 volume context from OM.
+ * When thread-local {@link S3Auth} is set, implementations update it with OM-returned
+ * {@code userPrincipal} and, when present, validated STS {@code originalAccessKeyId}.
* @throws IOException
*/
S3VolumeContext getS3VolumeContext() throws IOException;
diff --git a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
index 9ca47013462..d960cad0bca 100644
--- a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
+++ b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
@@ -211,6 +211,8 @@ public class RpcClient implements ClientProtocol {
private final XceiverClientFactory xceiverClientManager;
private final UserGroupInformation ugi;
private UserGroupInformation s3gUgi;
+ // Cached per thread for the current S3 Gateway request - cleared with thread-local S3Auth.
+ private final ThreadLocal cachedS3VolumeContext = new ThreadLocal<>();
private final ClientId clientId = ClientId.randomId();
private final boolean unsafeByteBufferConversion;
private Text dtService;
@@ -505,12 +507,31 @@ public OzoneVolume getVolumeDetails(String volumeName)
@Override
public S3VolumeContext getS3VolumeContext() throws IOException {
- S3VolumeContext resp = ozoneManagerClient.getS3VolumeContext();
- String userPrincipal = resp.getUserPrincipal();
- updateS3Principal(userPrincipal);
+ final S3VolumeContext cached = cachedS3VolumeContext.get();
+ if (cached != null) {
+ return cached;
+ }
+ final S3VolumeContext resp = ozoneManagerClient.getS3VolumeContext();
+ updateS3Principal(resp.getUserPrincipal());
+ updateValidatedStsOriginalAccessKeyId(resp.getStsOriginalAccessKeyId());
+ cachedS3VolumeContext.set(resp);
return resp;
}
+ private void updateValidatedStsOriginalAccessKeyId(String stsOriginalAccessKeyId) {
+ final S3Auth s3Auth = this.getThreadLocalS3Auth();
+ if (s3Auth != null && StringUtils.isNotEmpty(stsOriginalAccessKeyId)) {
+ LOG.debug("Updating S3Auth.validatedStsOriginalAccessKeyId to {}", stsOriginalAccessKeyId);
+ s3Auth.setValidatedStsOriginalAccessKeyId(stsOriginalAccessKeyId);
+ this.setThreadLocalS3Auth(s3Auth);
+ }
+ }
+
+ private void updateS3Context(KeyInfoWithVolumeContext keyInfoWithS3Context) {
+ keyInfoWithS3Context.getUserPrincipal().ifPresent(this::updateS3Principal);
+ keyInfoWithS3Context.getStsOriginalAccessKeyId().ifPresent(this::updateValidatedStsOriginalAccessKeyId);
+ }
+
private void updateS3Principal(String userPrincipal) {
S3Auth s3Auth = this.getThreadLocalS3Auth();
// Update user principal if needed to be used for KMS client
@@ -1979,7 +2000,7 @@ private OmKeyInfo getS3KeyInfo(
.build();
KeyInfoWithVolumeContext keyInfoWithS3Context =
ozoneManagerClient.getKeyInfo(keyArgs, true);
- keyInfoWithS3Context.getUserPrincipal().ifPresent(this::updateS3Principal);
+ updateS3Context(keyInfoWithS3Context);
return keyInfoWithS3Context.getKeyInfo();
}
@@ -2004,7 +2025,7 @@ private OmKeyInfo getS3PartKeyInfo(
.build();
KeyInfoWithVolumeContext keyInfoWithS3Context =
ozoneManagerClient.getKeyInfo(keyArgs, true);
- keyInfoWithS3Context.getUserPrincipal().ifPresent(this::updateS3Principal);
+ updateS3Context(keyInfoWithS3Context);
return keyInfoWithS3Context.getKeyInfo();
}
@@ -2896,6 +2917,7 @@ public OzoneKey headS3Object(String bucketName, String keyName)
@Override
public void setThreadLocalS3Auth(
S3Auth ozoneSharedSecretAuth) {
+ cachedS3VolumeContext.remove();
ozoneManagerClient.setThreadLocalS3Auth(ozoneSharedSecretAuth);
this.s3gUgi = UserGroupInformation.createRemoteUser(getThreadLocalS3Auth().getUserPrincipal());
}
@@ -2908,6 +2930,7 @@ public S3Auth getThreadLocalS3Auth() {
@Override
public void clearThreadLocalS3Auth() {
ozoneManagerClient.clearThreadLocalS3Auth();
+ cachedS3VolumeContext.remove();
}
@Override
diff --git a/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/rpc/TestRpcClient.java b/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/rpc/TestRpcClient.java
index 999b892ff7b..990a1ee21c7 100644
--- a/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/rpc/TestRpcClient.java
+++ b/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/rpc/TestRpcClient.java
@@ -21,20 +21,30 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
import org.apache.hadoop.hdds.scm.XceiverClientFactory;
import org.apache.hadoop.ozone.OzoneManagerVersion;
import org.apache.hadoop.ozone.client.MockOmTransport;
import org.apache.hadoop.ozone.client.MockXceiverClientFactory;
+import org.apache.hadoop.ozone.om.helpers.S3VolumeContext;
import org.apache.hadoop.ozone.om.helpers.ServiceInfo;
import org.apache.hadoop.ozone.om.helpers.ServiceInfoEx;
+import org.apache.hadoop.ozone.om.protocol.S3Auth;
import org.apache.hadoop.ozone.om.protocolPB.OmTransport;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetS3VolumeContextResponse;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Status;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.VolumeInfo;
import org.apache.ozone.test.GenericTestUtils;
import org.apache.ozone.test.GenericTestUtils.LogCapturer;
import org.junit.jupiter.api.Test;
@@ -228,6 +238,64 @@ public void testFutureVersionShouldNotBeAnExpectedVersion() {
() -> validateOmVersion(OzoneManagerVersion.FUTURE_VERSION, null));
}
+ @Test
+ public void testGetS3VolumeContextCachesResponseWithinSameS3Auth() throws IOException {
+ final CountingS3VolumeContextTransport transport = new CountingS3VolumeContextTransport();
+ final RpcClient rpcClient = createRpcClient(transport);
+ try {
+ final S3Auth s3Auth = new S3Auth("sign", "sig", "ASIAEXAMPLE", "ASIAEXAMPLE");
+ rpcClient.setThreadLocalS3Auth(s3Auth);
+
+ final S3VolumeContext first = rpcClient.getS3VolumeContext();
+ final S3VolumeContext second = rpcClient.getS3VolumeContext();
+
+ assertEquals(1, transport.getS3VolumeContextCallCount());
+ assertSame(first, second);
+ assertEquals("AKIAORIGINAL123", s3Auth.getValidatedStsOriginalAccessKeyId());
+ assertEquals("alice", s3Auth.getUserPrincipal());
+ } finally {
+ rpcClient.close();
+ }
+ }
+
+ @Test
+ public void testClearThreadLocalS3AuthClearsS3VolumeContextCache() throws IOException {
+ final CountingS3VolumeContextTransport transport = new CountingS3VolumeContextTransport();
+ final RpcClient rpcClient = createRpcClient(transport);
+ try {
+ rpcClient.setThreadLocalS3Auth(new S3Auth("sign", "sig", "ASIAEXAMPLE", "ASIAEXAMPLE"));
+ rpcClient.getS3VolumeContext();
+ rpcClient.getS3VolumeContext();
+ assertEquals(1, transport.getS3VolumeContextCallCount());
+
+ rpcClient.clearThreadLocalS3Auth();
+ rpcClient.setThreadLocalS3Auth(new S3Auth("sign", "sig", "ASIAEXAMPLE", "ASIAEXAMPLE"));
+ rpcClient.getS3VolumeContext();
+
+ assertEquals(2, transport.getS3VolumeContextCallCount());
+ } finally {
+ rpcClient.close();
+ }
+ }
+
+ @Test
+ public void testSetThreadLocalS3AuthClearsS3VolumeContextCache() throws IOException {
+ final CountingS3VolumeContextTransport transport = new CountingS3VolumeContextTransport();
+ final RpcClient rpcClient = createRpcClient(transport);
+ try {
+ rpcClient.setThreadLocalS3Auth(new S3Auth("sign", "sig", "ASIAEXAMPLE", "ASIAEXAMPLE"));
+ rpcClient.getS3VolumeContext();
+ assertEquals(1, transport.getS3VolumeContextCallCount());
+
+ rpcClient.setThreadLocalS3Auth(new S3Auth("sign2", "sig2", "ASIAEXAMPLE2", "ASIAEXAMPLE2"));
+ rpcClient.getS3VolumeContext();
+
+ assertEquals(2, transport.getS3VolumeContextCallCount());
+ } finally {
+ rpcClient.close();
+ }
+ }
+
@Test
public void testCloseTwiceDoesNotWarn() throws IOException {
RpcClient rpcClient = createRpcClient();
@@ -250,11 +318,15 @@ public void testCloseTwiceDoesNotWarn() throws IOException {
}
private static RpcClient createRpcClient() throws IOException {
+ return createRpcClient(new MockOmTransport());
+ }
+
+ private static RpcClient createRpcClient(MockOmTransport transport) throws IOException {
OzoneConfiguration config = new OzoneConfiguration();
return new RpcClient(config, null) {
@Override
protected OmTransport createOmTransport(String omServiceId) {
- return new MockOmTransport();
+ return transport;
}
@Override
@@ -264,4 +336,37 @@ protected XceiverClientFactory createXceiverClientFactory(
}
};
}
+
+ private static final class CountingS3VolumeContextTransport extends MockOmTransport {
+ private final AtomicInteger getS3VolumeContextCallCount = new AtomicInteger();
+
+ @Override
+ public OMResponse submitRequest(OMRequest payload) throws IOException {
+ if (payload.getCmdType() == Type.GetS3VolumeContext) {
+ getS3VolumeContextCallCount.incrementAndGet();
+ final VolumeInfo volumeInfo = VolumeInfo.newBuilder()
+ .setVolume("s3v")
+ .setAdminName("admin")
+ .setOwnerName("owner")
+ .build();
+ final GetS3VolumeContextResponse getS3VolumeContextResponse =
+ GetS3VolumeContextResponse.newBuilder()
+ .setVolumeInfo(volumeInfo)
+ .setUserPrincipal("alice")
+ .setStsOriginalAccessKeyId("AKIAORIGINAL123")
+ .build();
+ return OMResponse.newBuilder()
+ .setCmdType(payload.getCmdType())
+ .setSuccess(true)
+ .setStatus(Status.OK)
+ .setGetS3VolumeContextResponse(getS3VolumeContextResponse)
+ .build();
+ }
+ return super.submitRequest(payload);
+ }
+
+ private int getS3VolumeContextCallCount() {
+ return getS3VolumeContextCallCount.get();
+ }
+ }
}
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/KeyInfoWithVolumeContext.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/KeyInfoWithVolumeContext.java
index d6d54d3c174..f8098549b6b 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/KeyInfoWithVolumeContext.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/KeyInfoWithVolumeContext.java
@@ -19,6 +19,7 @@
import java.io.IOException;
import java.util.Optional;
+import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetKeyInfoResponse;
/**
@@ -35,13 +36,24 @@ public class KeyInfoWithVolumeContext {
*/
private final Optional userPrincipal;
+ /**
+ * OM-validated originalAccessKeyId for the current STS session token, when present.
+ */
+ private final Optional stsOriginalAccessKeyId;
+
private final OmKeyInfo keyInfo;
public KeyInfoWithVolumeContext(OmVolumeArgs volumeArgs,
String userPrincipal,
OmKeyInfo keyInfo) {
+ this(volumeArgs, userPrincipal, null, keyInfo);
+ }
+
+ public KeyInfoWithVolumeContext(OmVolumeArgs volumeArgs, String userPrincipal, String stsOriginalAccessKeyId,
+ OmKeyInfo keyInfo) {
this.volumeArgs = Optional.ofNullable(volumeArgs);
this.userPrincipal = Optional.ofNullable(userPrincipal);
+ this.stsOriginalAccessKeyId = Optional.ofNullable(stsOriginalAccessKeyId);
this.keyInfo = keyInfo;
}
@@ -51,6 +63,7 @@ public static KeyInfoWithVolumeContext fromProtobuf(
.setVolumeArgs(proto.hasVolumeInfo() ?
OmVolumeArgs.getFromProtobuf(proto.getVolumeInfo()) : null)
.setUserPrincipal(proto.getUserPrincipal())
+ .setStsOriginalAccessKeyId(proto.hasStsOriginalAccessKeyId() ? proto.getStsOriginalAccessKeyId() : null)
.setKeyInfo(OmKeyInfo.getFromProtobuf(proto.getKeyInfo()))
.build();
}
@@ -59,6 +72,7 @@ public GetKeyInfoResponse toProtobuf(int clientVersion) {
GetKeyInfoResponse.Builder builder = GetKeyInfoResponse.newBuilder();
volumeArgs.ifPresent(v -> builder.setVolumeInfo(v.getProtobuf()));
userPrincipal.ifPresent(builder::setUserPrincipal);
+ stsOriginalAccessKeyId.filter(StringUtils::isNotEmpty).ifPresent(builder::setStsOriginalAccessKeyId);
builder.setKeyInfo(keyInfo.getProtobuf(clientVersion));
return builder.build();
}
@@ -75,6 +89,10 @@ public Optional getUserPrincipal() {
return userPrincipal;
}
+ public Optional getStsOriginalAccessKeyId() {
+ return stsOriginalAccessKeyId;
+ }
+
public static Builder newBuilder() {
return new Builder();
}
@@ -85,6 +103,7 @@ public static Builder newBuilder() {
public static class Builder {
private OmVolumeArgs volumeArgs;
private String userPrincipal;
+ private String stsOriginalAccessKeyId;
private OmKeyInfo keyInfo;
public Builder setVolumeArgs(OmVolumeArgs volumeArgs) {
@@ -97,13 +116,18 @@ public Builder setUserPrincipal(String userPrincipal) {
return this;
}
+ public Builder setStsOriginalAccessKeyId(String stsOriginalAccessKeyId) {
+ this.stsOriginalAccessKeyId = stsOriginalAccessKeyId;
+ return this;
+ }
+
public Builder setKeyInfo(OmKeyInfo keyInfo) {
this.keyInfo = keyInfo;
return this;
}
public KeyInfoWithVolumeContext build() {
- return new KeyInfoWithVolumeContext(volumeArgs, userPrincipal, keyInfo);
+ return new KeyInfoWithVolumeContext(volumeArgs, userPrincipal, stsOriginalAccessKeyId, keyInfo);
}
}
}
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/S3VolumeContext.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/S3VolumeContext.java
index 19d428d0a9e..673e43ef908 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/S3VolumeContext.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/S3VolumeContext.java
@@ -17,6 +17,7 @@
package org.apache.hadoop.ozone.om.helpers;
+import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetS3VolumeContextResponse;
/**
@@ -35,9 +36,19 @@ public class S3VolumeContext {
*/
private final String userPrincipal;
+ /**
+ * OM-validated originalAccessKeyId for the current STS session token, when present.
+ */
+ private final String stsOriginalAccessKeyId;
+
public S3VolumeContext(OmVolumeArgs omVolumeArgs, String userPrincipal) {
+ this(omVolumeArgs, userPrincipal, null);
+ }
+
+ public S3VolumeContext(OmVolumeArgs omVolumeArgs, String userPrincipal, String stsOriginalAccessKeyId) {
this.omVolumeArgs = omVolumeArgs;
this.userPrincipal = userPrincipal;
+ this.stsOriginalAccessKeyId = stsOriginalAccessKeyId;
}
public OmVolumeArgs getOmVolumeArgs() {
@@ -48,17 +59,25 @@ public String getUserPrincipal() {
return userPrincipal;
}
+ public String getStsOriginalAccessKeyId() {
+ return stsOriginalAccessKeyId;
+ }
+
public static S3VolumeContext fromProtobuf(GetS3VolumeContextResponse resp) {
return new S3VolumeContext(
OmVolumeArgs.getFromProtobuf(resp.getVolumeInfo()),
- resp.getUserPrincipal());
+ resp.getUserPrincipal(),
+ resp.hasStsOriginalAccessKeyId() ? resp.getStsOriginalAccessKeyId() : null);
}
public GetS3VolumeContextResponse getProtobuf() {
- return GetS3VolumeContextResponse.newBuilder()
+ final GetS3VolumeContextResponse.Builder builder = GetS3VolumeContextResponse.newBuilder()
.setVolumeInfo(omVolumeArgs.getProtobuf())
- .setUserPrincipal(userPrincipal)
- .build();
+ .setUserPrincipal(userPrincipal);
+ if (StringUtils.isNotEmpty(stsOriginalAccessKeyId)) {
+ builder.setStsOriginalAccessKeyId(stsOriginalAccessKeyId);
+ }
+ return builder.build();
}
public static S3VolumeContext.Builder newBuilder() {
@@ -71,6 +90,7 @@ public static S3VolumeContext.Builder newBuilder() {
public static final class Builder {
private OmVolumeArgs omVolumeArgs;
private String userPrincipal;
+ private String stsOriginalAccessKeyId;
private Builder() {
}
@@ -85,8 +105,13 @@ public Builder setUserPrincipal(String userPrincipal) {
return this;
}
+ public Builder setStsOriginalAccessKeyId(String stsOriginalAccessKeyId) {
+ this.stsOriginalAccessKeyId = stsOriginalAccessKeyId;
+ return this;
+ }
+
public S3VolumeContext build() {
- return new S3VolumeContext(omVolumeArgs, userPrincipal);
+ return new S3VolumeContext(omVolumeArgs, userPrincipal, stsOriginalAccessKeyId);
}
}
}
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/S3Auth.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/S3Auth.java
index 577339c96ac..37c8438836b 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/S3Auth.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/S3Auth.java
@@ -31,6 +31,8 @@ public class S3Auth {
private String sessionToken;
// S3 action without s3: prefix (e.g. PutObject), set by S3 Gateway for use in finer-grained STS permissions.
private String s3Action;
+ // OM-validated originalAccessKeyId for the current STS session token, when present.
+ private String validatedStsOriginalAccessKeyId;
public S3Auth(final String stringToSign,
final String signature,
@@ -77,4 +79,12 @@ public String getS3Action() {
public void setS3Action(String s3Action) {
this.s3Action = s3Action;
}
+
+ public String getValidatedStsOriginalAccessKeyId() {
+ return validatedStsOriginalAccessKeyId;
+ }
+
+ public void setValidatedStsOriginalAccessKeyId(String validatedStsOriginalAccessKeyId) {
+ this.validatedStsOriginalAccessKeyId = validatedStsOriginalAccessKeyId;
+ }
}
diff --git a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestKeyInfoWithVolumeContext.java b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestKeyInfoWithVolumeContext.java
new file mode 100644
index 00000000000..98c03f9c5ea
--- /dev/null
+++ b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestKeyInfoWithVolumeContext.java
@@ -0,0 +1,70 @@
+/*
+ * 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.om.helpers;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetKeyInfoResponse;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.KeyInfo;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for {@link KeyInfoWithVolumeContext}. */
+public class TestKeyInfoWithVolumeContext {
+
+ @Test
+ public void fromProtobufReadsStsOriginalAccessKeyId() throws Exception {
+ final GetKeyInfoResponse proto = GetKeyInfoResponse.newBuilder()
+ .setKeyInfo(minimalKeyInfo())
+ .setUserPrincipal("alice")
+ .setStsOriginalAccessKeyId("AKIAORIGINAL123")
+ .build();
+
+ final KeyInfoWithVolumeContext decoded = KeyInfoWithVolumeContext.fromProtobuf(proto);
+
+ assertEquals("alice", decoded.getUserPrincipal().orElse(null));
+ assertEquals("AKIAORIGINAL123", decoded.getStsOriginalAccessKeyId().orElse(null));
+ assertEquals("key", decoded.getKeyInfo().getKeyName());
+ }
+
+ @Test
+ public void omitsStsOriginalAccessKeyIdWhenUnset() throws Exception {
+ final GetKeyInfoResponse proto = GetKeyInfoResponse.newBuilder()
+ .setKeyInfo(minimalKeyInfo())
+ .setUserPrincipal("alice")
+ .build();
+
+ final KeyInfoWithVolumeContext decoded = KeyInfoWithVolumeContext.fromProtobuf(proto);
+
+ assertEquals("alice", decoded.getUserPrincipal().orElse(null));
+ assertFalse(decoded.getStsOriginalAccessKeyId().isPresent());
+ }
+
+ private static KeyInfo minimalKeyInfo() {
+ return KeyInfo.newBuilder()
+ .setVolumeName("s3v")
+ .setBucketName("bucket")
+ .setKeyName("key")
+ .setDataSize(0L)
+ .setCreationTime(0L)
+ .setModificationTime(0L)
+ .setType(HddsProtos.ReplicationType.STAND_ALONE)
+ .build();
+ }
+}
diff --git a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestS3VolumeContext.java b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestS3VolumeContext.java
new file mode 100644
index 00000000000..30e75e651a3
--- /dev/null
+++ b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestS3VolumeContext.java
@@ -0,0 +1,66 @@
+/*
+ * 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.om.helpers;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetS3VolumeContextResponse;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for {@link S3VolumeContext}. */
+public class TestS3VolumeContext {
+
+ @Test
+ public void roundTripsStsOriginalAccessKeyId() {
+ final OmVolumeArgs volumeArgs = OmVolumeArgs.newBuilder()
+ .setVolume("s3v")
+ .setAdminName("admin")
+ .setOwnerName("owner")
+ .build();
+ final S3VolumeContext context = S3VolumeContext.newBuilder()
+ .setOmVolumeArgs(volumeArgs)
+ .setUserPrincipal("alice")
+ .setStsOriginalAccessKeyId("AKIAORIGINAL123")
+ .build();
+
+ final GetS3VolumeContextResponse proto = context.getProtobuf();
+ final S3VolumeContext decoded = S3VolumeContext.fromProtobuf(proto);
+
+ assertEquals("alice", decoded.getUserPrincipal());
+ assertEquals("AKIAORIGINAL123", decoded.getStsOriginalAccessKeyId());
+ }
+
+ @Test
+ public void omitsStsOriginalAccessKeyIdWhenUnset() {
+ final OmVolumeArgs volumeArgs = OmVolumeArgs.newBuilder()
+ .setVolume("s3v")
+ .setAdminName("admin")
+ .setOwnerName("owner")
+ .build();
+ final S3VolumeContext context = S3VolumeContext.newBuilder()
+ .setOmVolumeArgs(volumeArgs)
+ .setUserPrincipal("alice")
+ .build();
+
+ final S3VolumeContext decoded = S3VolumeContext.fromProtobuf(context.getProtobuf());
+
+ assertEquals("alice", decoded.getUserPrincipal());
+ assertNull(decoded.getStsOriginalAccessKeyId());
+ }
+}
diff --git a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
index 029a78a335d..78f89685f90 100644
--- a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
+++ b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
@@ -1411,6 +1411,8 @@ message GetKeyInfoResponse {
optional KeyInfo keyInfo = 1;
optional VolumeInfo volumeInfo = 2;
optional string UserPrincipal = 3;
+ // Set only after OM cryptographically validates the STS session token.
+ optional string stsOriginalAccessKeyId = 4;
}
message RenameKeysRequest {
@@ -2375,6 +2377,8 @@ message GetS3VolumeContextResponse {
optional VolumeInfo volumeInfo = 1;
// Piggybacked username (principal) response to be used for KMS client operations
optional string userPrincipal = 2;
+ // Set only after OM cryptographically validates the STS session token.
+ optional string stsOriginalAccessKeyId = 3;
}
/**
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
index 434d05132bf..8e5ea0ed220 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
@@ -202,6 +202,7 @@ public KeyInfoWithVolumeContext getKeyInfo(final OmKeyArgs args,
s3VolumeContext.ifPresent(context -> {
builder.setVolumeArgs(context.getOmVolumeArgs());
builder.setUserPrincipal(context.getUserPrincipal());
+ builder.setStsOriginalAccessKeyId(context.getStsOriginalAccessKeyId());
});
return builder.build();
} catch (Exception ex) {
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshot.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshot.java
index 6d3a56f40ed..4272014d70e 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshot.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshot.java
@@ -315,6 +315,7 @@ private KeyInfoWithVolumeContext denormalizeKeyInfoWithVolumeContext(
.setKeyInfo(denormalizeOmKeyInfo(k.getKeyInfo()))
.setVolumeArgs(k.getVolumeArgs().orElse(null))
.setUserPrincipal(k.getUserPrincipal().orElse(null))
+ .setStsOriginalAccessKeyId(k.getStsOriginalAccessKeyId().orElse(null))
.build();
}
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
index 04455a525a9..b1ef381796f 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
@@ -4187,6 +4187,10 @@ S3VolumeContext getS3VolumeContext(boolean skipChecks) throws IOException {
final S3VolumeContext.Builder s3VolumeContext = S3VolumeContext.newBuilder()
.setOmVolumeArgs(volumeInfo)
.setUserPrincipal(userPrincipal);
+ final STSTokenIdentifier stsTokenIdentifier = getStsTokenIdentifier();
+ if (stsTokenIdentifier != null) {
+ s3VolumeContext.setStsOriginalAccessKeyId(stsTokenIdentifier.getOriginalAccessKeyId());
+ }
perfMetrics.addS3VolumeContextLatencyNs(Time.monotonicNowNanos() - start);
return s3VolumeContext.build();
}
diff --git 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
index 814f79b1e84..24e5b48bb4c 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
@@ -22,6 +22,7 @@
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.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -50,6 +51,7 @@
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.AfterEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
@@ -65,6 +67,11 @@ public class TestS3SecurityUtil {
ThreadLocalRandom.current().nextBytes(ENCRYPTION_KEY);
}
+ @AfterEach
+ public void tearDown() {
+ OzoneManager.setStsTokenIdentifier(null);
+ }
+
@Test
public void testValidateS3CredentialFailsWhenTokenCreatedBeforeRevocationCutoff() throws Exception {
validateS3CredentialHelper(
@@ -160,6 +167,15 @@ public void testValidateS3CredentialFailsWhenRequestAccessIdEmpty() throws Excep
.setExpectedMessage("STS token validation failed - accessKeyId is invalid for session token"));
}
+ @Test
+ public void testValidateS3CredentialFailsWhenAwsSignatureInvalid() throws Exception {
+ validateS3CredentialHelper(
+ new TestConfig()
+ .setAwsSignatureValid(false)
+ .setExpectedResult(INVALID_TOKEN)
+ .setExpectedMessage("STS token validation failed for token"));
+ }
+
@Test
public void testValidateS3CredentialSuccessWhenTokenCreatedAfterRevocationCutoff() throws Exception {
validateS3CredentialHelper(
@@ -221,7 +237,7 @@ private void validateS3CredentialHelper(TestConfig config) throws Exception {
// Mock AWS V4 signature validation
awsV4AuthValidatorMock.when(() -> AWSV4AuthValidator.validateRequest(anyString(), anyString(), anyString()))
- .thenReturn(true);
+ .thenReturn(config.awsSignatureValid);
final OMRequest omRequest = createRequestWithSessionToken(
config.requestAccessId, config.includeAccessId);
@@ -236,8 +252,11 @@ private void validateS3CredentialHelper(TestConfig config) throws Exception {
"Expected exception message to contain: '" + config.expectedMessage + "' but was: '" +
omException.getMessage() + "'");
}
+ assertNull(
+ OzoneManager.getStsTokenIdentifier(), "STS token identifier must not be set when validation fails");
} else {
assertDoesNotThrow(() -> S3SecurityUtil.validateS3Credential(omRequest, ozoneManager));
+ assertEquals(stsTokenIdentifier, OzoneManager.getStsTokenIdentifier());
}
}
}
@@ -284,6 +303,7 @@ private static final class TestConfig {
private boolean shouldOriginalAccessKeyIdCheckThrowError = false;
private String requestAccessId = TEMP_ACCESS_KEY_ID;
private boolean includeAccessId = true;
+ private boolean awsSignatureValid = true;
private OMException.ResultCodes expectedResult = null;
private String expectedMessage = null;
@@ -326,6 +346,11 @@ TestConfig setIncludeAccessId(boolean includeAccessId) {
return this;
}
+ TestConfig setAwsSignatureValid(boolean awsSignatureValid) {
+ this.awsSignatureValid = awsSignatureValid;
+ return this;
+ }
+
TestConfig setExpectedResult(OMException.ResultCodes result) {
this.expectedResult = result;
return this;
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSSecurityUtil.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSSecurityUtil.java
index 594ac858bfb..c02df40e1a1 100644
--- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSSecurityUtil.java
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSSecurityUtil.java
@@ -286,6 +286,26 @@ public void testConstructValidateAndDecryptSTSTokenSecretKeyRetrievalException()
"key: something went wrong");
}
+ @Test
+ public void testConstructValidateAndDecryptSTSTokenRejectsForgedOriginalAccessKeyId() throws Exception {
+ final String validTokenString = tokenSecretManager.createSTSTokenString(
+ TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+
+ final Token validToken = new Token<>();
+ validToken.decodeFromUrlString(validTokenString);
+ final OMTokenProto forgedProto = OMTokenProto.parseFrom(validToken.getIdentifier()).toBuilder()
+ .setOriginalAccessKeyId("forged-original-access-key")
+ .build();
+ final Token forgedToken = new Token<>(
+ forgedProto.toByteArray(), validToken.getPassword(), validToken.getKind(), validToken.getService());
+
+ assertThatThrownBy(() ->
+ STSSecurityUtil.constructValidateAndDecryptSTSToken(
+ forgedToken.encodeToUrlString(), secretKeyClient, clock))
+ .isInstanceOf(OMException.class)
+ .hasMessageContaining("Invalid STS token format: Invalid STS token - signature is not correct for token");
+ }
+
@Test
public void testConstructValidateAndDecryptSTSTokenInvalidSignature() throws Exception {
// Create a valid token string
diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
index c833d5f22f4..3bf2eb346ea 100644
--- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
+++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
@@ -404,9 +404,9 @@ public MultiDeleteResponse multiDelete(
if (!result.getErrors().isEmpty()) {
auditMultiDeleteFailure(context, deleteKeys, new Exception("MultiDelete Exception"));
} else {
- AuditMessage.Builder message = auditMessageFor(context.getAction());
+ AuditMessage.Builder message = auditMessageForSuccess(context.getAction());
message.getParams().put("failedDeletes", deleteKeys.toString());
- AUDIT.logWriteSuccess(message.withResult(AuditEventStatus.SUCCESS).build());
+ AUDIT.logWriteSuccess(message.build());
}
return result;
diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
index c8c7d168b18..c2047e2f6f3 100644
--- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
+++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
@@ -312,7 +312,7 @@ protected T runWithS3ActionString(String s3Action, Chec
}
protected OzoneVolume getVolume() throws IOException {
- return client.getObjectStore().getS3Volume();
+ return getClient().getObjectStore().getS3Volume();
}
/**
@@ -606,10 +606,8 @@ protected AuditMessage.Builder auditMessageFor(AuditAction op) {
auditMap.put("x-amz-request-id", requestIdentifier.getRequestId());
auditMap.put("x-amz-id-2", requestIdentifier.getAmzId());
if (s3Auth != null) {
- // For STS temporary credentials, record the originalAccessKeyId (the permanent principal that
- // created the token) so the audit trail is not limited to the opaque tempAccessKeyId.
- final String originalAccessKeyId = AuditUtils.getStsOriginalAccessKeyId(s3Auth.getSessionToken());
- if (originalAccessKeyId != null) {
+ final String originalAccessKeyId = s3Auth.getValidatedStsOriginalAccessKeyId();
+ if (StringUtils.isNotEmpty(originalAccessKeyId)) {
auditMap.put(OzoneConsts.S3_STS_ORIGINAL_ACCESS_KEY_ID, originalAccessKeyId);
}
}
@@ -629,6 +627,7 @@ protected AuditMessage.Builder auditMessageFor(AuditAction op) {
}
protected AuditMessage.Builder auditMessageForSuccess(AuditAction op) {
+ resolveValidatedStsOriginalAccessKeyIdForAudit();
return auditMessageFor(op)
.withResult(AuditEventStatus.SUCCESS);
}
@@ -639,6 +638,26 @@ protected AuditMessage.Builder auditMessageForFailure(AuditAction op, Throwable
.withException(throwable);
}
+ /**
+ * Populates {@link S3Auth#getValidatedStsOriginalAccessKeyId()} from OM when the request carries
+ * an STS session token but the validated id is not yet available (e.g. bucket-only paths).
+ * Called only from the success-audit path; failure audits must not trigger an OM round-trip.
+ * Never disrupts auditing when OM validation fails.
+ */
+ private void resolveValidatedStsOriginalAccessKeyIdForAudit() {
+ if (s3Auth == null || StringUtils.isEmpty(s3Auth.getSessionToken())) {
+ return;
+ }
+ if (StringUtils.isNotEmpty(s3Auth.getValidatedStsOriginalAccessKeyId())) {
+ return;
+ }
+ try {
+ getClient().getObjectStore().getS3VolumeContext();
+ } catch (IOException | RuntimeException e) {
+ LOG.debug("Could not resolve validated STS context for audit", e);
+ }
+ }
+
@VisibleForTesting
public void setClient(OzoneClient ozoneClient) {
this.client = ozoneClient;
@@ -710,6 +729,13 @@ public S3GatewayMetrics getMetrics() {
return S3GatewayMetrics.getMetrics();
}
+ @VisibleForTesting
+ void setValidatedStsOriginalAccessKeyIdForTest() {
+ if (s3Auth != null) {
+ s3Auth.setValidatedStsOriginalAccessKeyId("AKIAORIGINAL123");
+ }
+ }
+
protected Map getAuditParameters() {
return AuditUtils.getAuditParameters(context);
}
diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/AuditUtils.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/AuditUtils.java
index 85486df8c3d..978f96ddb35 100644
--- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/AuditUtils.java
+++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/AuditUtils.java
@@ -19,13 +19,10 @@
import static org.apache.hadoop.ozone.s3.ClientIpFilter.CLIENT_IP_HEADER;
-import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.ws.rs.container.ContainerRequestContext;
-import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto;
-import org.apache.hadoop.security.token.Token;
/**
* Common utilities for operation auditing purposes.
@@ -34,26 +31,6 @@ public final class AuditUtils {
private AuditUtils() {
}
- /**
- * Extracts (if possible) the STS {@code originalAccessKeyId} from a session token so it can be
- * recorded in the S3 Gateway audit log. Like the S3 access id already recorded as the
- * audit user, this reflects what the client presented. Returns {@code null} when no usable value
- * can be extracted so the audit path is never disrupted by a missing or malformed token.
- */
- public static String getStsOriginalAccessKeyId(String sessionToken) {
- if (sessionToken == null || sessionToken.isEmpty()) {
- return null;
- }
- try {
- final Token> token = new Token<>();
- token.decodeFromUrlString(sessionToken);
- final String originalAccessKeyId = OMTokenProto.parseFrom(token.getIdentifier()).getOriginalAccessKeyId();
- return originalAccessKeyId.isEmpty() ? null : originalAccessKeyId;
- } catch (IOException | RuntimeException e) {
- return null;
- }
- }
-
public static Map getAuditParameters(
ContainerRequestContext context) {
Map res = new HashMap<>();
diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestEndpointBase.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestEndpointBase.java
index 0f23eb221ef..f0d91631cb0 100644
--- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestEndpointBase.java
+++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestEndpointBase.java
@@ -26,13 +26,22 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
import java.util.stream.Stream;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
@@ -40,8 +49,14 @@
import org.apache.hadoop.ozone.OzoneConsts;
import org.apache.hadoop.ozone.audit.AuditMessage;
import org.apache.hadoop.ozone.audit.S3GAction;
+import org.apache.hadoop.ozone.client.ObjectStore;
+import org.apache.hadoop.ozone.client.OzoneClient;
import org.apache.hadoop.ozone.client.OzoneVolume;
+import org.apache.hadoop.ozone.client.protocol.ClientProtocol;
import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.apache.hadoop.ozone.om.helpers.OmVolumeArgs;
+import org.apache.hadoop.ozone.om.helpers.S3VolumeContext;
+import org.apache.hadoop.ozone.om.protocol.S3Auth;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto;
import org.apache.hadoop.ozone.s3.exception.OS3Exception;
import org.apache.hadoop.ozone.s3.signature.SignatureInfo;
@@ -49,6 +64,7 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.stubbing.Answer;
/**
* Tests the s3 EndpointBase class methods.
@@ -56,6 +72,8 @@
*/
public class TestEndpointBase {
private static final String ORIGINAL_ACCESS_KEY_ID_PARAM = "originalAccessKeyId";
+ private static final String FORGED_STS_ORIGINAL_ACCESS_KEY_ID = "FORGED-ORIGINAL-ACCESS-KEY";
+ private static final String STS_TEMP_ACCESS_KEY_ID = "ASIAEXAMPLE123";
/**
* Verify s3 metadata key "gdprEnabled" can't be set up directly
@@ -154,22 +172,77 @@ public void init() { }
}
@Test
- public void testAuditMessageIncludesStsOriginalAccessKeyId() throws Exception {
+ public void testAuditMessageIncludesValidatedStsOriginalAccessKeyId() throws Exception {
final String originalAccessKeyId = "AKIAORIGINAL123";
- final OMTokenProto proto = OMTokenProto.newBuilder()
- .setType(OMTokenProto.Type.S3_STS_TOKEN)
- .setOriginalAccessKeyId(originalAccessKeyId)
- .build();
- final SignatureInfo signatureInfo = new SignatureInfo.Builder(SignatureInfo.Version.V4)
- .setAwsAccessId("ASIAEXAMPLE123")
- .setSignature("signature")
- .setStringToSign("string-to-sign")
- .setSessionToken(encodeSessionToken(proto))
- .build();
- final AuditEndpoint endpointBase = newAuditEndpoint(signatureInfo);
+ // Pass the forged token to newAuditEndpoint because we need a session token present (so the request is
+ // for STS), but deliberately make its embedded originalAccessKeyId wrong, so the test can prove the audit path
+ // ignores it and only trusts the validated field.
+ final AuditEndpoint endpointBase = newAuditEndpoint(stsSignatureInfoWithForgedOriginalAccessKeyId());
+ endpointBase.setValidatedStsOriginalAccessKeyIdForTest();
assertThat(endpointBase.auditMessageForTest().getParams())
- .containsEntry(ORIGINAL_ACCESS_KEY_ID_PARAM, originalAccessKeyId);
+ .containsEntry(ORIGINAL_ACCESS_KEY_ID_PARAM, originalAccessKeyId)
+ .doesNotContainValue(FORGED_STS_ORIGINAL_ACCESS_KEY_ID);
+ }
+
+ @Test
+ public void testAuditMessageResolvesValidatedStsOriginalAccessKeyIdFromOm() throws Exception {
+ final String originalAccessKeyId = "AKIAORIGINAL123";
+ final StsAuditEndpointFixture fixture = newStsAuditEndpointFixture(
+ stsSignatureInfoWithForgedOriginalAccessKeyId(),
+ (objectStore, s3AuthRef) -> stubGetS3VolumeContext(
+ objectStore, invocation -> {
+ final S3Auth auth = s3AuthRef.get();
+ if (auth != null) {
+ auth.setValidatedStsOriginalAccessKeyId(originalAccessKeyId);
+ }
+ final OmVolumeArgs volumeArgs = OmVolumeArgs.newBuilder()
+ .setVolume("s3v")
+ .setAdminName("admin")
+ .setOwnerName("owner")
+ .build();
+ return S3VolumeContext.newBuilder()
+ .setOmVolumeArgs(volumeArgs)
+ .setUserPrincipal("alice")
+ .setStsOriginalAccessKeyId(originalAccessKeyId)
+ .build();
+ }));
+
+ assertThat(fixture.getEndpoint().auditMessageForTest().getParams())
+ .containsEntry(ORIGINAL_ACCESS_KEY_ID_PARAM, originalAccessKeyId)
+ .doesNotContainValue(FORGED_STS_ORIGINAL_ACCESS_KEY_ID);
+ verify(fixture.getObjectStore()).getS3VolumeContext();
+ }
+
+ @Test
+ public void testAuditMessageOmitsStsOriginalAccessKeyIdWhenNotValidated() throws Exception {
+ final AuditEndpoint endpointBase = newAuditEndpoint(stsSignatureInfoWithForgedOriginalAccessKeyId());
+
+ assertThat(endpointBase.auditMessageForTest().getParams())
+ .doesNotContainKey(ORIGINAL_ACCESS_KEY_ID_PARAM);
+ }
+
+ @Test
+ public void testFailureAuditOmitsStsOriginalAccessKeyIdWhenNotValidated() throws Exception {
+ final StsAuditEndpointFixture fixture = newStsAuditEndpointFixture(stsSignatureInfoWithForgedOriginalAccessKeyId());
+
+ assertThat(fixture.getEndpoint().auditMessageForFailureTest(
+ new OMException("STS token validation failed", ResultCodes.INVALID_TOKEN)).getParams())
+ .doesNotContainKey(ORIGINAL_ACCESS_KEY_ID_PARAM)
+ .doesNotContainValue(FORGED_STS_ORIGINAL_ACCESS_KEY_ID);
+ verify(fixture.getObjectStore(), never()).getS3VolumeContext();
+ }
+
+ @Test
+ public void testAuditMessageSuccessIgnoresRuntimeExceptionFromOmResolution() throws Exception {
+ final StsAuditEndpointFixture fixture = newStsAuditEndpointFixture(
+ stsSignatureInfoWithForgedOriginalAccessKeyId(), objectStore -> stubGetS3VolumeContextToThrow(
+ objectStore, new RuntimeException("OM unavailable")));
+
+ assertThat(fixture.getEndpoint().auditMessageForTest().getParams())
+ .doesNotContainKey(ORIGINAL_ACCESS_KEY_ID_PARAM)
+ .doesNotContainValue(FORGED_STS_ORIGINAL_ACCESS_KEY_ID);
+ verify(fixture.getObjectStore()).getS3VolumeContext();
}
@Test
@@ -254,15 +327,102 @@ private static String encodeSessionToken(OMTokenProto proto) throws Exception {
return token.encodeToUrlString();
}
+ private static SignatureInfo stsSignatureInfoWithForgedOriginalAccessKeyId() throws Exception {
+ final OMTokenProto proto = OMTokenProto.newBuilder()
+ .setType(OMTokenProto.Type.S3_STS_TOKEN)
+ .setOriginalAccessKeyId(FORGED_STS_ORIGINAL_ACCESS_KEY_ID)
+ .build();
+ return new SignatureInfo.Builder(SignatureInfo.Version.V4)
+ .setAwsAccessId(STS_TEMP_ACCESS_KEY_ID)
+ .setSignature("signature")
+ .setStringToSign("string-to-sign")
+ .setSessionToken(encodeSessionToken(proto))
+ .build();
+ }
+
+ private static void stubGetS3VolumeContext(ObjectStore objectStore, Answer answer) {
+ try {
+ doAnswer(answer).when(objectStore).getS3VolumeContext();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static void stubGetS3VolumeContextToThrow(ObjectStore objectStore, RuntimeException toThrow) {
+ try {
+ doThrow(toThrow).when(objectStore).getS3VolumeContext();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static StsAuditEndpointFixture newStsAuditEndpointFixture(SignatureInfo signatureInfo)
+ throws Exception {
+ return newStsAuditEndpointFixture(signatureInfo, (Consumer) objectStore -> { });
+ }
+
+ private static StsAuditEndpointFixture newStsAuditEndpointFixture(
+ SignatureInfo signatureInfo,
+ Consumer objectStoreConfigurer) throws Exception {
+ return newStsAuditEndpointFixture(signatureInfo, (objectStore, s3AuthRef) ->
+ objectStoreConfigurer.accept(objectStore));
+ }
+
+ private static StsAuditEndpointFixture newStsAuditEndpointFixture(
+ SignatureInfo signatureInfo,
+ BiConsumer> objectStoreConfigurer) throws Exception {
+ final OzoneClient client = mock(OzoneClient.class);
+ final ObjectStore objectStore = mock(ObjectStore.class);
+ final ClientProtocol clientProtocol = mock(ClientProtocol.class);
+ final AtomicReference s3AuthRef = new AtomicReference<>();
+
+ doAnswer(invocation -> {
+ s3AuthRef.set(invocation.getArgument(0));
+ return null;
+ }).when(clientProtocol).setThreadLocalS3Auth(any(S3Auth.class));
+ when(clientProtocol.getThreadLocalS3Auth()).thenAnswer(invocation -> s3AuthRef.get());
+ when(client.getObjectStore()).thenReturn(objectStore);
+ when(objectStore.getClientProxy()).thenReturn(clientProtocol);
+ objectStoreConfigurer.accept(objectStore, s3AuthRef);
+
+ final AuditEndpoint endpoint = new EndpointBuilder<>(AuditEndpoint::new)
+ .setClient(client)
+ .setSignatureInfo(signatureInfo)
+ .build();
+ return new StsAuditEndpointFixture(endpoint, objectStore);
+ }
+
private static AuditEndpoint newAuditEndpoint(SignatureInfo signatureInfo) {
return new EndpointBuilder<>(AuditEndpoint::new)
.setSignatureInfo(signatureInfo)
.build();
}
+ private static final class StsAuditEndpointFixture {
+ private final AuditEndpoint endpoint;
+ private final ObjectStore objectStore;
+
+ private StsAuditEndpointFixture(AuditEndpoint endpoint, ObjectStore objectStore) {
+ this.endpoint = endpoint;
+ this.objectStore = objectStore;
+ }
+
+ private AuditEndpoint getEndpoint() {
+ return endpoint;
+ }
+
+ private ObjectStore getObjectStore() {
+ return objectStore;
+ }
+ }
+
private static final class AuditEndpoint extends EndpointBase {
private AuditMessage.Builder auditMessageForTest() {
- return auditMessageFor(S3GAction.GET_KEY);
+ return auditMessageForSuccess(S3GAction.GET_KEY);
+ }
+
+ private AuditMessage.Builder auditMessageForFailureTest(Throwable throwable) {
+ return auditMessageForFailure(S3GAction.GET_KEY, throwable);
}
}
diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestAuditUtils.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestAuditUtils.java
deleted file mode 100644
index b5952290dc9..00000000000
--- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestAuditUtils.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.hadoop.ozone.s3.util;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
-
-import org.apache.hadoop.io.Text;
-import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto;
-import org.apache.hadoop.security.token.Token;
-import org.junit.jupiter.api.Test;
-
-/** Unit tests for {@link AuditUtils}. */
-public class TestAuditUtils {
-
- @Test
- public void extractsOriginalAccessKeyIdFromSessionToken() throws Exception {
- final OMTokenProto proto = OMTokenProto.newBuilder()
- .setType(OMTokenProto.Type.S3_STS_TOKEN)
- .setOriginalAccessKeyId("AKIAORIGINAL123")
- .build();
-
- assertEquals("AKIAORIGINAL123", AuditUtils.getStsOriginalAccessKeyId(encodeSessionToken(proto)));
- }
-
- @Test
- public void returnsNullWhenOriginalAccessKeyIdAbsent() throws Exception {
- final OMTokenProto proto = OMTokenProto.newBuilder()
- .setType(OMTokenProto.Type.S3_STS_TOKEN)
- .build();
-
- assertNull(AuditUtils.getStsOriginalAccessKeyId(encodeSessionToken(proto)));
- }
-
- @Test
- public void returnsNullForNullEmptyOrMalformedToken() {
- assertNull(AuditUtils.getStsOriginalAccessKeyId(null));
- assertNull(AuditUtils.getStsOriginalAccessKeyId(""));
- assertNull(AuditUtils.getStsOriginalAccessKeyId("not-a-valid-token"));
- }
-
- private static String encodeSessionToken(OMTokenProto proto) throws Exception {
- final Token> token = new Token<>(
- proto.toByteArray(), new byte[0], new Text("OzoneToken"), new Text("sts"));
- return token.encodeToUrlString();
- }
-}
From e7b521b23947ec422b66cd6b2935f47ab665336a Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Sun, 23 Aug 2026 14:57:02 -0700
Subject: [PATCH 08/20] Revert "redesign: ensure only validated
originalAccessKeyId is added to audit log"
This reverts commit d3c9ea52a76eb74c8ad888210ff3feaca8ff06ca.
---
.../ozone/client/protocol/ClientProtocol.java | 5 +-
.../hadoop/ozone/client/rpc/RpcClient.java | 33 +--
.../ozone/client/rpc/TestRpcClient.java | 107 +---------
.../om/helpers/KeyInfoWithVolumeContext.java | 26 +--
.../ozone/om/helpers/S3VolumeContext.java | 35 +---
.../hadoop/ozone/om/protocol/S3Auth.java | 10 -
.../helpers/TestKeyInfoWithVolumeContext.java | 70 -------
.../ozone/om/helpers/TestS3VolumeContext.java | 66 ------
.../src/main/proto/OmClientProtocol.proto | 4 -
.../hadoop/ozone/om/OmMetadataReader.java | 1 -
.../apache/hadoop/ozone/om/OmSnapshot.java | 1 -
.../apache/hadoop/ozone/om/OzoneManager.java | 4 -
.../ozone/security/TestS3SecurityUtil.java | 27 +--
.../ozone/security/TestSTSSecurityUtil.java | 20 --
.../ozone/s3/endpoint/BucketEndpoint.java | 4 +-
.../ozone/s3/endpoint/EndpointBase.java | 36 +---
.../hadoop/ozone/s3/util/AuditUtils.java | 23 +++
.../ozone/s3/endpoint/TestEndpointBase.java | 188 ++----------------
.../hadoop/ozone/s3/util/TestAuditUtils.java | 62 ++++++
19 files changed, 121 insertions(+), 601 deletions(-)
delete mode 100644 hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestKeyInfoWithVolumeContext.java
delete mode 100644 hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestS3VolumeContext.java
create mode 100644 hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestAuditUtils.java
diff --git a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
index 7900fa4a41f..b5f7baa0ef2 100644
--- a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
+++ b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
@@ -142,9 +142,8 @@ OzoneVolume getVolumeDetails(String volumeName)
throws IOException;
/**
- * @return S3 volume context from OM.
- * When thread-local {@link S3Auth} is set, implementations update it with OM-returned
- * {@code userPrincipal} and, when present, validated STS {@code originalAccessKeyId}.
+ * @return Raw GetS3VolumeContextResponse.
+ * S3Auth won't be updated with actual userPrincipal by this call.
* @throws IOException
*/
S3VolumeContext getS3VolumeContext() throws IOException;
diff --git a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
index d960cad0bca..9ca47013462 100644
--- a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
+++ b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
@@ -211,8 +211,6 @@ public class RpcClient implements ClientProtocol {
private final XceiverClientFactory xceiverClientManager;
private final UserGroupInformation ugi;
private UserGroupInformation s3gUgi;
- // Cached per thread for the current S3 Gateway request - cleared with thread-local S3Auth.
- private final ThreadLocal cachedS3VolumeContext = new ThreadLocal<>();
private final ClientId clientId = ClientId.randomId();
private final boolean unsafeByteBufferConversion;
private Text dtService;
@@ -507,31 +505,12 @@ public OzoneVolume getVolumeDetails(String volumeName)
@Override
public S3VolumeContext getS3VolumeContext() throws IOException {
- final S3VolumeContext cached = cachedS3VolumeContext.get();
- if (cached != null) {
- return cached;
- }
- final S3VolumeContext resp = ozoneManagerClient.getS3VolumeContext();
- updateS3Principal(resp.getUserPrincipal());
- updateValidatedStsOriginalAccessKeyId(resp.getStsOriginalAccessKeyId());
- cachedS3VolumeContext.set(resp);
+ S3VolumeContext resp = ozoneManagerClient.getS3VolumeContext();
+ String userPrincipal = resp.getUserPrincipal();
+ updateS3Principal(userPrincipal);
return resp;
}
- private void updateValidatedStsOriginalAccessKeyId(String stsOriginalAccessKeyId) {
- final S3Auth s3Auth = this.getThreadLocalS3Auth();
- if (s3Auth != null && StringUtils.isNotEmpty(stsOriginalAccessKeyId)) {
- LOG.debug("Updating S3Auth.validatedStsOriginalAccessKeyId to {}", stsOriginalAccessKeyId);
- s3Auth.setValidatedStsOriginalAccessKeyId(stsOriginalAccessKeyId);
- this.setThreadLocalS3Auth(s3Auth);
- }
- }
-
- private void updateS3Context(KeyInfoWithVolumeContext keyInfoWithS3Context) {
- keyInfoWithS3Context.getUserPrincipal().ifPresent(this::updateS3Principal);
- keyInfoWithS3Context.getStsOriginalAccessKeyId().ifPresent(this::updateValidatedStsOriginalAccessKeyId);
- }
-
private void updateS3Principal(String userPrincipal) {
S3Auth s3Auth = this.getThreadLocalS3Auth();
// Update user principal if needed to be used for KMS client
@@ -2000,7 +1979,7 @@ private OmKeyInfo getS3KeyInfo(
.build();
KeyInfoWithVolumeContext keyInfoWithS3Context =
ozoneManagerClient.getKeyInfo(keyArgs, true);
- updateS3Context(keyInfoWithS3Context);
+ keyInfoWithS3Context.getUserPrincipal().ifPresent(this::updateS3Principal);
return keyInfoWithS3Context.getKeyInfo();
}
@@ -2025,7 +2004,7 @@ private OmKeyInfo getS3PartKeyInfo(
.build();
KeyInfoWithVolumeContext keyInfoWithS3Context =
ozoneManagerClient.getKeyInfo(keyArgs, true);
- updateS3Context(keyInfoWithS3Context);
+ keyInfoWithS3Context.getUserPrincipal().ifPresent(this::updateS3Principal);
return keyInfoWithS3Context.getKeyInfo();
}
@@ -2917,7 +2896,6 @@ public OzoneKey headS3Object(String bucketName, String keyName)
@Override
public void setThreadLocalS3Auth(
S3Auth ozoneSharedSecretAuth) {
- cachedS3VolumeContext.remove();
ozoneManagerClient.setThreadLocalS3Auth(ozoneSharedSecretAuth);
this.s3gUgi = UserGroupInformation.createRemoteUser(getThreadLocalS3Auth().getUserPrincipal());
}
@@ -2930,7 +2908,6 @@ public S3Auth getThreadLocalS3Auth() {
@Override
public void clearThreadLocalS3Auth() {
ozoneManagerClient.clearThreadLocalS3Auth();
- cachedS3VolumeContext.remove();
}
@Override
diff --git a/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/rpc/TestRpcClient.java b/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/rpc/TestRpcClient.java
index 990a1ee21c7..999b892ff7b 100644
--- a/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/rpc/TestRpcClient.java
+++ b/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/rpc/TestRpcClient.java
@@ -21,30 +21,20 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
-import java.util.concurrent.atomic.AtomicInteger;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
import org.apache.hadoop.hdds.scm.XceiverClientFactory;
import org.apache.hadoop.ozone.OzoneManagerVersion;
import org.apache.hadoop.ozone.client.MockOmTransport;
import org.apache.hadoop.ozone.client.MockXceiverClientFactory;
-import org.apache.hadoop.ozone.om.helpers.S3VolumeContext;
import org.apache.hadoop.ozone.om.helpers.ServiceInfo;
import org.apache.hadoop.ozone.om.helpers.ServiceInfoEx;
-import org.apache.hadoop.ozone.om.protocol.S3Auth;
import org.apache.hadoop.ozone.om.protocolPB.OmTransport;
-import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetS3VolumeContextResponse;
-import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
-import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
-import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Status;
-import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
-import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.VolumeInfo;
import org.apache.ozone.test.GenericTestUtils;
import org.apache.ozone.test.GenericTestUtils.LogCapturer;
import org.junit.jupiter.api.Test;
@@ -238,64 +228,6 @@ public void testFutureVersionShouldNotBeAnExpectedVersion() {
() -> validateOmVersion(OzoneManagerVersion.FUTURE_VERSION, null));
}
- @Test
- public void testGetS3VolumeContextCachesResponseWithinSameS3Auth() throws IOException {
- final CountingS3VolumeContextTransport transport = new CountingS3VolumeContextTransport();
- final RpcClient rpcClient = createRpcClient(transport);
- try {
- final S3Auth s3Auth = new S3Auth("sign", "sig", "ASIAEXAMPLE", "ASIAEXAMPLE");
- rpcClient.setThreadLocalS3Auth(s3Auth);
-
- final S3VolumeContext first = rpcClient.getS3VolumeContext();
- final S3VolumeContext second = rpcClient.getS3VolumeContext();
-
- assertEquals(1, transport.getS3VolumeContextCallCount());
- assertSame(first, second);
- assertEquals("AKIAORIGINAL123", s3Auth.getValidatedStsOriginalAccessKeyId());
- assertEquals("alice", s3Auth.getUserPrincipal());
- } finally {
- rpcClient.close();
- }
- }
-
- @Test
- public void testClearThreadLocalS3AuthClearsS3VolumeContextCache() throws IOException {
- final CountingS3VolumeContextTransport transport = new CountingS3VolumeContextTransport();
- final RpcClient rpcClient = createRpcClient(transport);
- try {
- rpcClient.setThreadLocalS3Auth(new S3Auth("sign", "sig", "ASIAEXAMPLE", "ASIAEXAMPLE"));
- rpcClient.getS3VolumeContext();
- rpcClient.getS3VolumeContext();
- assertEquals(1, transport.getS3VolumeContextCallCount());
-
- rpcClient.clearThreadLocalS3Auth();
- rpcClient.setThreadLocalS3Auth(new S3Auth("sign", "sig", "ASIAEXAMPLE", "ASIAEXAMPLE"));
- rpcClient.getS3VolumeContext();
-
- assertEquals(2, transport.getS3VolumeContextCallCount());
- } finally {
- rpcClient.close();
- }
- }
-
- @Test
- public void testSetThreadLocalS3AuthClearsS3VolumeContextCache() throws IOException {
- final CountingS3VolumeContextTransport transport = new CountingS3VolumeContextTransport();
- final RpcClient rpcClient = createRpcClient(transport);
- try {
- rpcClient.setThreadLocalS3Auth(new S3Auth("sign", "sig", "ASIAEXAMPLE", "ASIAEXAMPLE"));
- rpcClient.getS3VolumeContext();
- assertEquals(1, transport.getS3VolumeContextCallCount());
-
- rpcClient.setThreadLocalS3Auth(new S3Auth("sign2", "sig2", "ASIAEXAMPLE2", "ASIAEXAMPLE2"));
- rpcClient.getS3VolumeContext();
-
- assertEquals(2, transport.getS3VolumeContextCallCount());
- } finally {
- rpcClient.close();
- }
- }
-
@Test
public void testCloseTwiceDoesNotWarn() throws IOException {
RpcClient rpcClient = createRpcClient();
@@ -318,15 +250,11 @@ public void testCloseTwiceDoesNotWarn() throws IOException {
}
private static RpcClient createRpcClient() throws IOException {
- return createRpcClient(new MockOmTransport());
- }
-
- private static RpcClient createRpcClient(MockOmTransport transport) throws IOException {
OzoneConfiguration config = new OzoneConfiguration();
return new RpcClient(config, null) {
@Override
protected OmTransport createOmTransport(String omServiceId) {
- return transport;
+ return new MockOmTransport();
}
@Override
@@ -336,37 +264,4 @@ protected XceiverClientFactory createXceiverClientFactory(
}
};
}
-
- private static final class CountingS3VolumeContextTransport extends MockOmTransport {
- private final AtomicInteger getS3VolumeContextCallCount = new AtomicInteger();
-
- @Override
- public OMResponse submitRequest(OMRequest payload) throws IOException {
- if (payload.getCmdType() == Type.GetS3VolumeContext) {
- getS3VolumeContextCallCount.incrementAndGet();
- final VolumeInfo volumeInfo = VolumeInfo.newBuilder()
- .setVolume("s3v")
- .setAdminName("admin")
- .setOwnerName("owner")
- .build();
- final GetS3VolumeContextResponse getS3VolumeContextResponse =
- GetS3VolumeContextResponse.newBuilder()
- .setVolumeInfo(volumeInfo)
- .setUserPrincipal("alice")
- .setStsOriginalAccessKeyId("AKIAORIGINAL123")
- .build();
- return OMResponse.newBuilder()
- .setCmdType(payload.getCmdType())
- .setSuccess(true)
- .setStatus(Status.OK)
- .setGetS3VolumeContextResponse(getS3VolumeContextResponse)
- .build();
- }
- return super.submitRequest(payload);
- }
-
- private int getS3VolumeContextCallCount() {
- return getS3VolumeContextCallCount.get();
- }
- }
}
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/KeyInfoWithVolumeContext.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/KeyInfoWithVolumeContext.java
index f8098549b6b..d6d54d3c174 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/KeyInfoWithVolumeContext.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/KeyInfoWithVolumeContext.java
@@ -19,7 +19,6 @@
import java.io.IOException;
import java.util.Optional;
-import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetKeyInfoResponse;
/**
@@ -36,24 +35,13 @@ public class KeyInfoWithVolumeContext {
*/
private final Optional userPrincipal;
- /**
- * OM-validated originalAccessKeyId for the current STS session token, when present.
- */
- private final Optional stsOriginalAccessKeyId;
-
private final OmKeyInfo keyInfo;
public KeyInfoWithVolumeContext(OmVolumeArgs volumeArgs,
String userPrincipal,
OmKeyInfo keyInfo) {
- this(volumeArgs, userPrincipal, null, keyInfo);
- }
-
- public KeyInfoWithVolumeContext(OmVolumeArgs volumeArgs, String userPrincipal, String stsOriginalAccessKeyId,
- OmKeyInfo keyInfo) {
this.volumeArgs = Optional.ofNullable(volumeArgs);
this.userPrincipal = Optional.ofNullable(userPrincipal);
- this.stsOriginalAccessKeyId = Optional.ofNullable(stsOriginalAccessKeyId);
this.keyInfo = keyInfo;
}
@@ -63,7 +51,6 @@ public static KeyInfoWithVolumeContext fromProtobuf(
.setVolumeArgs(proto.hasVolumeInfo() ?
OmVolumeArgs.getFromProtobuf(proto.getVolumeInfo()) : null)
.setUserPrincipal(proto.getUserPrincipal())
- .setStsOriginalAccessKeyId(proto.hasStsOriginalAccessKeyId() ? proto.getStsOriginalAccessKeyId() : null)
.setKeyInfo(OmKeyInfo.getFromProtobuf(proto.getKeyInfo()))
.build();
}
@@ -72,7 +59,6 @@ public GetKeyInfoResponse toProtobuf(int clientVersion) {
GetKeyInfoResponse.Builder builder = GetKeyInfoResponse.newBuilder();
volumeArgs.ifPresent(v -> builder.setVolumeInfo(v.getProtobuf()));
userPrincipal.ifPresent(builder::setUserPrincipal);
- stsOriginalAccessKeyId.filter(StringUtils::isNotEmpty).ifPresent(builder::setStsOriginalAccessKeyId);
builder.setKeyInfo(keyInfo.getProtobuf(clientVersion));
return builder.build();
}
@@ -89,10 +75,6 @@ public Optional getUserPrincipal() {
return userPrincipal;
}
- public Optional getStsOriginalAccessKeyId() {
- return stsOriginalAccessKeyId;
- }
-
public static Builder newBuilder() {
return new Builder();
}
@@ -103,7 +85,6 @@ public static Builder newBuilder() {
public static class Builder {
private OmVolumeArgs volumeArgs;
private String userPrincipal;
- private String stsOriginalAccessKeyId;
private OmKeyInfo keyInfo;
public Builder setVolumeArgs(OmVolumeArgs volumeArgs) {
@@ -116,18 +97,13 @@ public Builder setUserPrincipal(String userPrincipal) {
return this;
}
- public Builder setStsOriginalAccessKeyId(String stsOriginalAccessKeyId) {
- this.stsOriginalAccessKeyId = stsOriginalAccessKeyId;
- return this;
- }
-
public Builder setKeyInfo(OmKeyInfo keyInfo) {
this.keyInfo = keyInfo;
return this;
}
public KeyInfoWithVolumeContext build() {
- return new KeyInfoWithVolumeContext(volumeArgs, userPrincipal, stsOriginalAccessKeyId, keyInfo);
+ return new KeyInfoWithVolumeContext(volumeArgs, userPrincipal, keyInfo);
}
}
}
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/S3VolumeContext.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/S3VolumeContext.java
index 673e43ef908..19d428d0a9e 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/S3VolumeContext.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/S3VolumeContext.java
@@ -17,7 +17,6 @@
package org.apache.hadoop.ozone.om.helpers;
-import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetS3VolumeContextResponse;
/**
@@ -36,19 +35,9 @@ public class S3VolumeContext {
*/
private final String userPrincipal;
- /**
- * OM-validated originalAccessKeyId for the current STS session token, when present.
- */
- private final String stsOriginalAccessKeyId;
-
public S3VolumeContext(OmVolumeArgs omVolumeArgs, String userPrincipal) {
- this(omVolumeArgs, userPrincipal, null);
- }
-
- public S3VolumeContext(OmVolumeArgs omVolumeArgs, String userPrincipal, String stsOriginalAccessKeyId) {
this.omVolumeArgs = omVolumeArgs;
this.userPrincipal = userPrincipal;
- this.stsOriginalAccessKeyId = stsOriginalAccessKeyId;
}
public OmVolumeArgs getOmVolumeArgs() {
@@ -59,25 +48,17 @@ public String getUserPrincipal() {
return userPrincipal;
}
- public String getStsOriginalAccessKeyId() {
- return stsOriginalAccessKeyId;
- }
-
public static S3VolumeContext fromProtobuf(GetS3VolumeContextResponse resp) {
return new S3VolumeContext(
OmVolumeArgs.getFromProtobuf(resp.getVolumeInfo()),
- resp.getUserPrincipal(),
- resp.hasStsOriginalAccessKeyId() ? resp.getStsOriginalAccessKeyId() : null);
+ resp.getUserPrincipal());
}
public GetS3VolumeContextResponse getProtobuf() {
- final GetS3VolumeContextResponse.Builder builder = GetS3VolumeContextResponse.newBuilder()
+ return GetS3VolumeContextResponse.newBuilder()
.setVolumeInfo(omVolumeArgs.getProtobuf())
- .setUserPrincipal(userPrincipal);
- if (StringUtils.isNotEmpty(stsOriginalAccessKeyId)) {
- builder.setStsOriginalAccessKeyId(stsOriginalAccessKeyId);
- }
- return builder.build();
+ .setUserPrincipal(userPrincipal)
+ .build();
}
public static S3VolumeContext.Builder newBuilder() {
@@ -90,7 +71,6 @@ public static S3VolumeContext.Builder newBuilder() {
public static final class Builder {
private OmVolumeArgs omVolumeArgs;
private String userPrincipal;
- private String stsOriginalAccessKeyId;
private Builder() {
}
@@ -105,13 +85,8 @@ public Builder setUserPrincipal(String userPrincipal) {
return this;
}
- public Builder setStsOriginalAccessKeyId(String stsOriginalAccessKeyId) {
- this.stsOriginalAccessKeyId = stsOriginalAccessKeyId;
- return this;
- }
-
public S3VolumeContext build() {
- return new S3VolumeContext(omVolumeArgs, userPrincipal, stsOriginalAccessKeyId);
+ return new S3VolumeContext(omVolumeArgs, userPrincipal);
}
}
}
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/S3Auth.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/S3Auth.java
index 37c8438836b..577339c96ac 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/S3Auth.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/S3Auth.java
@@ -31,8 +31,6 @@ public class S3Auth {
private String sessionToken;
// S3 action without s3: prefix (e.g. PutObject), set by S3 Gateway for use in finer-grained STS permissions.
private String s3Action;
- // OM-validated originalAccessKeyId for the current STS session token, when present.
- private String validatedStsOriginalAccessKeyId;
public S3Auth(final String stringToSign,
final String signature,
@@ -79,12 +77,4 @@ public String getS3Action() {
public void setS3Action(String s3Action) {
this.s3Action = s3Action;
}
-
- public String getValidatedStsOriginalAccessKeyId() {
- return validatedStsOriginalAccessKeyId;
- }
-
- public void setValidatedStsOriginalAccessKeyId(String validatedStsOriginalAccessKeyId) {
- this.validatedStsOriginalAccessKeyId = validatedStsOriginalAccessKeyId;
- }
}
diff --git a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestKeyInfoWithVolumeContext.java b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestKeyInfoWithVolumeContext.java
deleted file mode 100644
index 98c03f9c5ea..00000000000
--- a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestKeyInfoWithVolumeContext.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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.om.helpers;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-
-import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
-import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetKeyInfoResponse;
-import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.KeyInfo;
-import org.junit.jupiter.api.Test;
-
-/** Unit tests for {@link KeyInfoWithVolumeContext}. */
-public class TestKeyInfoWithVolumeContext {
-
- @Test
- public void fromProtobufReadsStsOriginalAccessKeyId() throws Exception {
- final GetKeyInfoResponse proto = GetKeyInfoResponse.newBuilder()
- .setKeyInfo(minimalKeyInfo())
- .setUserPrincipal("alice")
- .setStsOriginalAccessKeyId("AKIAORIGINAL123")
- .build();
-
- final KeyInfoWithVolumeContext decoded = KeyInfoWithVolumeContext.fromProtobuf(proto);
-
- assertEquals("alice", decoded.getUserPrincipal().orElse(null));
- assertEquals("AKIAORIGINAL123", decoded.getStsOriginalAccessKeyId().orElse(null));
- assertEquals("key", decoded.getKeyInfo().getKeyName());
- }
-
- @Test
- public void omitsStsOriginalAccessKeyIdWhenUnset() throws Exception {
- final GetKeyInfoResponse proto = GetKeyInfoResponse.newBuilder()
- .setKeyInfo(minimalKeyInfo())
- .setUserPrincipal("alice")
- .build();
-
- final KeyInfoWithVolumeContext decoded = KeyInfoWithVolumeContext.fromProtobuf(proto);
-
- assertEquals("alice", decoded.getUserPrincipal().orElse(null));
- assertFalse(decoded.getStsOriginalAccessKeyId().isPresent());
- }
-
- private static KeyInfo minimalKeyInfo() {
- return KeyInfo.newBuilder()
- .setVolumeName("s3v")
- .setBucketName("bucket")
- .setKeyName("key")
- .setDataSize(0L)
- .setCreationTime(0L)
- .setModificationTime(0L)
- .setType(HddsProtos.ReplicationType.STAND_ALONE)
- .build();
- }
-}
diff --git a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestS3VolumeContext.java b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestS3VolumeContext.java
deleted file mode 100644
index 30e75e651a3..00000000000
--- a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestS3VolumeContext.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * 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.om.helpers;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
-
-import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetS3VolumeContextResponse;
-import org.junit.jupiter.api.Test;
-
-/** Unit tests for {@link S3VolumeContext}. */
-public class TestS3VolumeContext {
-
- @Test
- public void roundTripsStsOriginalAccessKeyId() {
- final OmVolumeArgs volumeArgs = OmVolumeArgs.newBuilder()
- .setVolume("s3v")
- .setAdminName("admin")
- .setOwnerName("owner")
- .build();
- final S3VolumeContext context = S3VolumeContext.newBuilder()
- .setOmVolumeArgs(volumeArgs)
- .setUserPrincipal("alice")
- .setStsOriginalAccessKeyId("AKIAORIGINAL123")
- .build();
-
- final GetS3VolumeContextResponse proto = context.getProtobuf();
- final S3VolumeContext decoded = S3VolumeContext.fromProtobuf(proto);
-
- assertEquals("alice", decoded.getUserPrincipal());
- assertEquals("AKIAORIGINAL123", decoded.getStsOriginalAccessKeyId());
- }
-
- @Test
- public void omitsStsOriginalAccessKeyIdWhenUnset() {
- final OmVolumeArgs volumeArgs = OmVolumeArgs.newBuilder()
- .setVolume("s3v")
- .setAdminName("admin")
- .setOwnerName("owner")
- .build();
- final S3VolumeContext context = S3VolumeContext.newBuilder()
- .setOmVolumeArgs(volumeArgs)
- .setUserPrincipal("alice")
- .build();
-
- final S3VolumeContext decoded = S3VolumeContext.fromProtobuf(context.getProtobuf());
-
- assertEquals("alice", decoded.getUserPrincipal());
- assertNull(decoded.getStsOriginalAccessKeyId());
- }
-}
diff --git a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
index 78f89685f90..029a78a335d 100644
--- a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
+++ b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
@@ -1411,8 +1411,6 @@ message GetKeyInfoResponse {
optional KeyInfo keyInfo = 1;
optional VolumeInfo volumeInfo = 2;
optional string UserPrincipal = 3;
- // Set only after OM cryptographically validates the STS session token.
- optional string stsOriginalAccessKeyId = 4;
}
message RenameKeysRequest {
@@ -2377,8 +2375,6 @@ message GetS3VolumeContextResponse {
optional VolumeInfo volumeInfo = 1;
// Piggybacked username (principal) response to be used for KMS client operations
optional string userPrincipal = 2;
- // Set only after OM cryptographically validates the STS session token.
- optional string stsOriginalAccessKeyId = 3;
}
/**
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
index 8e5ea0ed220..434d05132bf 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
@@ -202,7 +202,6 @@ public KeyInfoWithVolumeContext getKeyInfo(final OmKeyArgs args,
s3VolumeContext.ifPresent(context -> {
builder.setVolumeArgs(context.getOmVolumeArgs());
builder.setUserPrincipal(context.getUserPrincipal());
- builder.setStsOriginalAccessKeyId(context.getStsOriginalAccessKeyId());
});
return builder.build();
} catch (Exception ex) {
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshot.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshot.java
index 4272014d70e..6d3a56f40ed 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshot.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshot.java
@@ -315,7 +315,6 @@ private KeyInfoWithVolumeContext denormalizeKeyInfoWithVolumeContext(
.setKeyInfo(denormalizeOmKeyInfo(k.getKeyInfo()))
.setVolumeArgs(k.getVolumeArgs().orElse(null))
.setUserPrincipal(k.getUserPrincipal().orElse(null))
- .setStsOriginalAccessKeyId(k.getStsOriginalAccessKeyId().orElse(null))
.build();
}
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
index b1ef381796f..04455a525a9 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
@@ -4187,10 +4187,6 @@ S3VolumeContext getS3VolumeContext(boolean skipChecks) throws IOException {
final S3VolumeContext.Builder s3VolumeContext = S3VolumeContext.newBuilder()
.setOmVolumeArgs(volumeInfo)
.setUserPrincipal(userPrincipal);
- final STSTokenIdentifier stsTokenIdentifier = getStsTokenIdentifier();
- if (stsTokenIdentifier != null) {
- s3VolumeContext.setStsOriginalAccessKeyId(stsTokenIdentifier.getOriginalAccessKeyId());
- }
perfMetrics.addS3VolumeContextLatencyNs(Time.monotonicNowNanos() - start);
return s3VolumeContext.build();
}
diff --git 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
index 24e5b48bb4c..814f79b1e84 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
@@ -22,7 +22,6 @@
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.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -51,7 +50,6 @@
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.AfterEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
@@ -67,11 +65,6 @@ public class TestS3SecurityUtil {
ThreadLocalRandom.current().nextBytes(ENCRYPTION_KEY);
}
- @AfterEach
- public void tearDown() {
- OzoneManager.setStsTokenIdentifier(null);
- }
-
@Test
public void testValidateS3CredentialFailsWhenTokenCreatedBeforeRevocationCutoff() throws Exception {
validateS3CredentialHelper(
@@ -167,15 +160,6 @@ public void testValidateS3CredentialFailsWhenRequestAccessIdEmpty() throws Excep
.setExpectedMessage("STS token validation failed - accessKeyId is invalid for session token"));
}
- @Test
- public void testValidateS3CredentialFailsWhenAwsSignatureInvalid() throws Exception {
- validateS3CredentialHelper(
- new TestConfig()
- .setAwsSignatureValid(false)
- .setExpectedResult(INVALID_TOKEN)
- .setExpectedMessage("STS token validation failed for token"));
- }
-
@Test
public void testValidateS3CredentialSuccessWhenTokenCreatedAfterRevocationCutoff() throws Exception {
validateS3CredentialHelper(
@@ -237,7 +221,7 @@ private void validateS3CredentialHelper(TestConfig config) throws Exception {
// Mock AWS V4 signature validation
awsV4AuthValidatorMock.when(() -> AWSV4AuthValidator.validateRequest(anyString(), anyString(), anyString()))
- .thenReturn(config.awsSignatureValid);
+ .thenReturn(true);
final OMRequest omRequest = createRequestWithSessionToken(
config.requestAccessId, config.includeAccessId);
@@ -252,11 +236,8 @@ private void validateS3CredentialHelper(TestConfig config) throws Exception {
"Expected exception message to contain: '" + config.expectedMessage + "' but was: '" +
omException.getMessage() + "'");
}
- assertNull(
- OzoneManager.getStsTokenIdentifier(), "STS token identifier must not be set when validation fails");
} else {
assertDoesNotThrow(() -> S3SecurityUtil.validateS3Credential(omRequest, ozoneManager));
- assertEquals(stsTokenIdentifier, OzoneManager.getStsTokenIdentifier());
}
}
}
@@ -303,7 +284,6 @@ private static final class TestConfig {
private boolean shouldOriginalAccessKeyIdCheckThrowError = false;
private String requestAccessId = TEMP_ACCESS_KEY_ID;
private boolean includeAccessId = true;
- private boolean awsSignatureValid = true;
private OMException.ResultCodes expectedResult = null;
private String expectedMessage = null;
@@ -346,11 +326,6 @@ TestConfig setIncludeAccessId(boolean includeAccessId) {
return this;
}
- TestConfig setAwsSignatureValid(boolean awsSignatureValid) {
- this.awsSignatureValid = awsSignatureValid;
- return this;
- }
-
TestConfig setExpectedResult(OMException.ResultCodes result) {
this.expectedResult = result;
return this;
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSSecurityUtil.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSSecurityUtil.java
index c02df40e1a1..594ac858bfb 100644
--- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSSecurityUtil.java
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSSecurityUtil.java
@@ -286,26 +286,6 @@ public void testConstructValidateAndDecryptSTSTokenSecretKeyRetrievalException()
"key: something went wrong");
}
- @Test
- public void testConstructValidateAndDecryptSTSTokenRejectsForgedOriginalAccessKeyId() throws Exception {
- final String validTokenString = tokenSecretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY, clock);
-
- final Token validToken = new Token<>();
- validToken.decodeFromUrlString(validTokenString);
- final OMTokenProto forgedProto = OMTokenProto.parseFrom(validToken.getIdentifier()).toBuilder()
- .setOriginalAccessKeyId("forged-original-access-key")
- .build();
- final Token forgedToken = new Token<>(
- forgedProto.toByteArray(), validToken.getPassword(), validToken.getKind(), validToken.getService());
-
- assertThatThrownBy(() ->
- STSSecurityUtil.constructValidateAndDecryptSTSToken(
- forgedToken.encodeToUrlString(), secretKeyClient, clock))
- .isInstanceOf(OMException.class)
- .hasMessageContaining("Invalid STS token format: Invalid STS token - signature is not correct for token");
- }
-
@Test
public void testConstructValidateAndDecryptSTSTokenInvalidSignature() throws Exception {
// Create a valid token string
diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
index 3bf2eb346ea..c833d5f22f4 100644
--- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
+++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
@@ -404,9 +404,9 @@ public MultiDeleteResponse multiDelete(
if (!result.getErrors().isEmpty()) {
auditMultiDeleteFailure(context, deleteKeys, new Exception("MultiDelete Exception"));
} else {
- AuditMessage.Builder message = auditMessageForSuccess(context.getAction());
+ AuditMessage.Builder message = auditMessageFor(context.getAction());
message.getParams().put("failedDeletes", deleteKeys.toString());
- AUDIT.logWriteSuccess(message.build());
+ AUDIT.logWriteSuccess(message.withResult(AuditEventStatus.SUCCESS).build());
}
return result;
diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
index c2047e2f6f3..c8c7d168b18 100644
--- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
+++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
@@ -312,7 +312,7 @@ protected T runWithS3ActionString(String s3Action, Chec
}
protected OzoneVolume getVolume() throws IOException {
- return getClient().getObjectStore().getS3Volume();
+ return client.getObjectStore().getS3Volume();
}
/**
@@ -606,8 +606,10 @@ protected AuditMessage.Builder auditMessageFor(AuditAction op) {
auditMap.put("x-amz-request-id", requestIdentifier.getRequestId());
auditMap.put("x-amz-id-2", requestIdentifier.getAmzId());
if (s3Auth != null) {
- final String originalAccessKeyId = s3Auth.getValidatedStsOriginalAccessKeyId();
- if (StringUtils.isNotEmpty(originalAccessKeyId)) {
+ // For STS temporary credentials, record the originalAccessKeyId (the permanent principal that
+ // created the token) so the audit trail is not limited to the opaque tempAccessKeyId.
+ final String originalAccessKeyId = AuditUtils.getStsOriginalAccessKeyId(s3Auth.getSessionToken());
+ if (originalAccessKeyId != null) {
auditMap.put(OzoneConsts.S3_STS_ORIGINAL_ACCESS_KEY_ID, originalAccessKeyId);
}
}
@@ -627,7 +629,6 @@ protected AuditMessage.Builder auditMessageFor(AuditAction op) {
}
protected AuditMessage.Builder auditMessageForSuccess(AuditAction op) {
- resolveValidatedStsOriginalAccessKeyIdForAudit();
return auditMessageFor(op)
.withResult(AuditEventStatus.SUCCESS);
}
@@ -638,26 +639,6 @@ protected AuditMessage.Builder auditMessageForFailure(AuditAction op, Throwable
.withException(throwable);
}
- /**
- * Populates {@link S3Auth#getValidatedStsOriginalAccessKeyId()} from OM when the request carries
- * an STS session token but the validated id is not yet available (e.g. bucket-only paths).
- * Called only from the success-audit path; failure audits must not trigger an OM round-trip.
- * Never disrupts auditing when OM validation fails.
- */
- private void resolveValidatedStsOriginalAccessKeyIdForAudit() {
- if (s3Auth == null || StringUtils.isEmpty(s3Auth.getSessionToken())) {
- return;
- }
- if (StringUtils.isNotEmpty(s3Auth.getValidatedStsOriginalAccessKeyId())) {
- return;
- }
- try {
- getClient().getObjectStore().getS3VolumeContext();
- } catch (IOException | RuntimeException e) {
- LOG.debug("Could not resolve validated STS context for audit", e);
- }
- }
-
@VisibleForTesting
public void setClient(OzoneClient ozoneClient) {
this.client = ozoneClient;
@@ -729,13 +710,6 @@ public S3GatewayMetrics getMetrics() {
return S3GatewayMetrics.getMetrics();
}
- @VisibleForTesting
- void setValidatedStsOriginalAccessKeyIdForTest() {
- if (s3Auth != null) {
- s3Auth.setValidatedStsOriginalAccessKeyId("AKIAORIGINAL123");
- }
- }
-
protected Map getAuditParameters() {
return AuditUtils.getAuditParameters(context);
}
diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/AuditUtils.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/AuditUtils.java
index 978f96ddb35..85486df8c3d 100644
--- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/AuditUtils.java
+++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/AuditUtils.java
@@ -19,10 +19,13 @@
import static org.apache.hadoop.ozone.s3.ClientIpFilter.CLIENT_IP_HEADER;
+import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.ws.rs.container.ContainerRequestContext;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto;
+import org.apache.hadoop.security.token.Token;
/**
* Common utilities for operation auditing purposes.
@@ -31,6 +34,26 @@ public final class AuditUtils {
private AuditUtils() {
}
+ /**
+ * Extracts (if possible) the STS {@code originalAccessKeyId} from a session token so it can be
+ * recorded in the S3 Gateway audit log. Like the S3 access id already recorded as the
+ * audit user, this reflects what the client presented. Returns {@code null} when no usable value
+ * can be extracted so the audit path is never disrupted by a missing or malformed token.
+ */
+ public static String getStsOriginalAccessKeyId(String sessionToken) {
+ if (sessionToken == null || sessionToken.isEmpty()) {
+ return null;
+ }
+ try {
+ final Token> token = new Token<>();
+ token.decodeFromUrlString(sessionToken);
+ final String originalAccessKeyId = OMTokenProto.parseFrom(token.getIdentifier()).getOriginalAccessKeyId();
+ return originalAccessKeyId.isEmpty() ? null : originalAccessKeyId;
+ } catch (IOException | RuntimeException e) {
+ return null;
+ }
+ }
+
public static Map getAuditParameters(
ContainerRequestContext context) {
Map res = new HashMap<>();
diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestEndpointBase.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestEndpointBase.java
index f0d91631cb0..0f23eb221ef 100644
--- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestEndpointBase.java
+++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestEndpointBase.java
@@ -26,22 +26,13 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Map;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.BiConsumer;
-import java.util.function.Consumer;
import java.util.stream.Stream;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
@@ -49,14 +40,8 @@
import org.apache.hadoop.ozone.OzoneConsts;
import org.apache.hadoop.ozone.audit.AuditMessage;
import org.apache.hadoop.ozone.audit.S3GAction;
-import org.apache.hadoop.ozone.client.ObjectStore;
-import org.apache.hadoop.ozone.client.OzoneClient;
import org.apache.hadoop.ozone.client.OzoneVolume;
-import org.apache.hadoop.ozone.client.protocol.ClientProtocol;
import org.apache.hadoop.ozone.om.exceptions.OMException;
-import org.apache.hadoop.ozone.om.helpers.OmVolumeArgs;
-import org.apache.hadoop.ozone.om.helpers.S3VolumeContext;
-import org.apache.hadoop.ozone.om.protocol.S3Auth;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto;
import org.apache.hadoop.ozone.s3.exception.OS3Exception;
import org.apache.hadoop.ozone.s3.signature.SignatureInfo;
@@ -64,7 +49,6 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
-import org.mockito.stubbing.Answer;
/**
* Tests the s3 EndpointBase class methods.
@@ -72,8 +56,6 @@
*/
public class TestEndpointBase {
private static final String ORIGINAL_ACCESS_KEY_ID_PARAM = "originalAccessKeyId";
- private static final String FORGED_STS_ORIGINAL_ACCESS_KEY_ID = "FORGED-ORIGINAL-ACCESS-KEY";
- private static final String STS_TEMP_ACCESS_KEY_ID = "ASIAEXAMPLE123";
/**
* Verify s3 metadata key "gdprEnabled" can't be set up directly
@@ -172,77 +154,22 @@ public void init() { }
}
@Test
- public void testAuditMessageIncludesValidatedStsOriginalAccessKeyId() throws Exception {
+ public void testAuditMessageIncludesStsOriginalAccessKeyId() throws Exception {
final String originalAccessKeyId = "AKIAORIGINAL123";
- // Pass the forged token to newAuditEndpoint because we need a session token present (so the request is
- // for STS), but deliberately make its embedded originalAccessKeyId wrong, so the test can prove the audit path
- // ignores it and only trusts the validated field.
- final AuditEndpoint endpointBase = newAuditEndpoint(stsSignatureInfoWithForgedOriginalAccessKeyId());
- endpointBase.setValidatedStsOriginalAccessKeyIdForTest();
-
- assertThat(endpointBase.auditMessageForTest().getParams())
- .containsEntry(ORIGINAL_ACCESS_KEY_ID_PARAM, originalAccessKeyId)
- .doesNotContainValue(FORGED_STS_ORIGINAL_ACCESS_KEY_ID);
- }
-
- @Test
- public void testAuditMessageResolvesValidatedStsOriginalAccessKeyIdFromOm() throws Exception {
- final String originalAccessKeyId = "AKIAORIGINAL123";
- final StsAuditEndpointFixture fixture = newStsAuditEndpointFixture(
- stsSignatureInfoWithForgedOriginalAccessKeyId(),
- (objectStore, s3AuthRef) -> stubGetS3VolumeContext(
- objectStore, invocation -> {
- final S3Auth auth = s3AuthRef.get();
- if (auth != null) {
- auth.setValidatedStsOriginalAccessKeyId(originalAccessKeyId);
- }
- final OmVolumeArgs volumeArgs = OmVolumeArgs.newBuilder()
- .setVolume("s3v")
- .setAdminName("admin")
- .setOwnerName("owner")
- .build();
- return S3VolumeContext.newBuilder()
- .setOmVolumeArgs(volumeArgs)
- .setUserPrincipal("alice")
- .setStsOriginalAccessKeyId(originalAccessKeyId)
- .build();
- }));
-
- assertThat(fixture.getEndpoint().auditMessageForTest().getParams())
- .containsEntry(ORIGINAL_ACCESS_KEY_ID_PARAM, originalAccessKeyId)
- .doesNotContainValue(FORGED_STS_ORIGINAL_ACCESS_KEY_ID);
- verify(fixture.getObjectStore()).getS3VolumeContext();
- }
-
- @Test
- public void testAuditMessageOmitsStsOriginalAccessKeyIdWhenNotValidated() throws Exception {
- final AuditEndpoint endpointBase = newAuditEndpoint(stsSignatureInfoWithForgedOriginalAccessKeyId());
+ final OMTokenProto proto = OMTokenProto.newBuilder()
+ .setType(OMTokenProto.Type.S3_STS_TOKEN)
+ .setOriginalAccessKeyId(originalAccessKeyId)
+ .build();
+ final SignatureInfo signatureInfo = new SignatureInfo.Builder(SignatureInfo.Version.V4)
+ .setAwsAccessId("ASIAEXAMPLE123")
+ .setSignature("signature")
+ .setStringToSign("string-to-sign")
+ .setSessionToken(encodeSessionToken(proto))
+ .build();
+ final AuditEndpoint endpointBase = newAuditEndpoint(signatureInfo);
assertThat(endpointBase.auditMessageForTest().getParams())
- .doesNotContainKey(ORIGINAL_ACCESS_KEY_ID_PARAM);
- }
-
- @Test
- public void testFailureAuditOmitsStsOriginalAccessKeyIdWhenNotValidated() throws Exception {
- final StsAuditEndpointFixture fixture = newStsAuditEndpointFixture(stsSignatureInfoWithForgedOriginalAccessKeyId());
-
- assertThat(fixture.getEndpoint().auditMessageForFailureTest(
- new OMException("STS token validation failed", ResultCodes.INVALID_TOKEN)).getParams())
- .doesNotContainKey(ORIGINAL_ACCESS_KEY_ID_PARAM)
- .doesNotContainValue(FORGED_STS_ORIGINAL_ACCESS_KEY_ID);
- verify(fixture.getObjectStore(), never()).getS3VolumeContext();
- }
-
- @Test
- public void testAuditMessageSuccessIgnoresRuntimeExceptionFromOmResolution() throws Exception {
- final StsAuditEndpointFixture fixture = newStsAuditEndpointFixture(
- stsSignatureInfoWithForgedOriginalAccessKeyId(), objectStore -> stubGetS3VolumeContextToThrow(
- objectStore, new RuntimeException("OM unavailable")));
-
- assertThat(fixture.getEndpoint().auditMessageForTest().getParams())
- .doesNotContainKey(ORIGINAL_ACCESS_KEY_ID_PARAM)
- .doesNotContainValue(FORGED_STS_ORIGINAL_ACCESS_KEY_ID);
- verify(fixture.getObjectStore()).getS3VolumeContext();
+ .containsEntry(ORIGINAL_ACCESS_KEY_ID_PARAM, originalAccessKeyId);
}
@Test
@@ -327,102 +254,15 @@ private static String encodeSessionToken(OMTokenProto proto) throws Exception {
return token.encodeToUrlString();
}
- private static SignatureInfo stsSignatureInfoWithForgedOriginalAccessKeyId() throws Exception {
- final OMTokenProto proto = OMTokenProto.newBuilder()
- .setType(OMTokenProto.Type.S3_STS_TOKEN)
- .setOriginalAccessKeyId(FORGED_STS_ORIGINAL_ACCESS_KEY_ID)
- .build();
- return new SignatureInfo.Builder(SignatureInfo.Version.V4)
- .setAwsAccessId(STS_TEMP_ACCESS_KEY_ID)
- .setSignature("signature")
- .setStringToSign("string-to-sign")
- .setSessionToken(encodeSessionToken(proto))
- .build();
- }
-
- private static void stubGetS3VolumeContext(ObjectStore objectStore, Answer answer) {
- try {
- doAnswer(answer).when(objectStore).getS3VolumeContext();
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
- private static void stubGetS3VolumeContextToThrow(ObjectStore objectStore, RuntimeException toThrow) {
- try {
- doThrow(toThrow).when(objectStore).getS3VolumeContext();
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
- private static StsAuditEndpointFixture newStsAuditEndpointFixture(SignatureInfo signatureInfo)
- throws Exception {
- return newStsAuditEndpointFixture(signatureInfo, (Consumer) objectStore -> { });
- }
-
- private static StsAuditEndpointFixture newStsAuditEndpointFixture(
- SignatureInfo signatureInfo,
- Consumer objectStoreConfigurer) throws Exception {
- return newStsAuditEndpointFixture(signatureInfo, (objectStore, s3AuthRef) ->
- objectStoreConfigurer.accept(objectStore));
- }
-
- private static StsAuditEndpointFixture newStsAuditEndpointFixture(
- SignatureInfo signatureInfo,
- BiConsumer> objectStoreConfigurer) throws Exception {
- final OzoneClient client = mock(OzoneClient.class);
- final ObjectStore objectStore = mock(ObjectStore.class);
- final ClientProtocol clientProtocol = mock(ClientProtocol.class);
- final AtomicReference s3AuthRef = new AtomicReference<>();
-
- doAnswer(invocation -> {
- s3AuthRef.set(invocation.getArgument(0));
- return null;
- }).when(clientProtocol).setThreadLocalS3Auth(any(S3Auth.class));
- when(clientProtocol.getThreadLocalS3Auth()).thenAnswer(invocation -> s3AuthRef.get());
- when(client.getObjectStore()).thenReturn(objectStore);
- when(objectStore.getClientProxy()).thenReturn(clientProtocol);
- objectStoreConfigurer.accept(objectStore, s3AuthRef);
-
- final AuditEndpoint endpoint = new EndpointBuilder<>(AuditEndpoint::new)
- .setClient(client)
- .setSignatureInfo(signatureInfo)
- .build();
- return new StsAuditEndpointFixture(endpoint, objectStore);
- }
-
private static AuditEndpoint newAuditEndpoint(SignatureInfo signatureInfo) {
return new EndpointBuilder<>(AuditEndpoint::new)
.setSignatureInfo(signatureInfo)
.build();
}
- private static final class StsAuditEndpointFixture {
- private final AuditEndpoint endpoint;
- private final ObjectStore objectStore;
-
- private StsAuditEndpointFixture(AuditEndpoint endpoint, ObjectStore objectStore) {
- this.endpoint = endpoint;
- this.objectStore = objectStore;
- }
-
- private AuditEndpoint getEndpoint() {
- return endpoint;
- }
-
- private ObjectStore getObjectStore() {
- return objectStore;
- }
- }
-
private static final class AuditEndpoint extends EndpointBase {
private AuditMessage.Builder auditMessageForTest() {
- return auditMessageForSuccess(S3GAction.GET_KEY);
- }
-
- private AuditMessage.Builder auditMessageForFailureTest(Throwable throwable) {
- return auditMessageForFailure(S3GAction.GET_KEY, throwable);
+ return auditMessageFor(S3GAction.GET_KEY);
}
}
diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestAuditUtils.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestAuditUtils.java
new file mode 100644
index 00000000000..b5952290dc9
--- /dev/null
+++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestAuditUtils.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.s3.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.apache.hadoop.io.Text;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto;
+import org.apache.hadoop.security.token.Token;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for {@link AuditUtils}. */
+public class TestAuditUtils {
+
+ @Test
+ public void extractsOriginalAccessKeyIdFromSessionToken() throws Exception {
+ final OMTokenProto proto = OMTokenProto.newBuilder()
+ .setType(OMTokenProto.Type.S3_STS_TOKEN)
+ .setOriginalAccessKeyId("AKIAORIGINAL123")
+ .build();
+
+ assertEquals("AKIAORIGINAL123", AuditUtils.getStsOriginalAccessKeyId(encodeSessionToken(proto)));
+ }
+
+ @Test
+ public void returnsNullWhenOriginalAccessKeyIdAbsent() throws Exception {
+ final OMTokenProto proto = OMTokenProto.newBuilder()
+ .setType(OMTokenProto.Type.S3_STS_TOKEN)
+ .build();
+
+ assertNull(AuditUtils.getStsOriginalAccessKeyId(encodeSessionToken(proto)));
+ }
+
+ @Test
+ public void returnsNullForNullEmptyOrMalformedToken() {
+ assertNull(AuditUtils.getStsOriginalAccessKeyId(null));
+ assertNull(AuditUtils.getStsOriginalAccessKeyId(""));
+ assertNull(AuditUtils.getStsOriginalAccessKeyId("not-a-valid-token"));
+ }
+
+ private static String encodeSessionToken(OMTokenProto proto) throws Exception {
+ final Token> token = new Token<>(
+ proto.toByteArray(), new byte[0], new Text("OzoneToken"), new Text("sts"));
+ return token.encodeToUrlString();
+ }
+}
From 44ab570b647cb80aef26eb09ea3e945ff5cc709f Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Sun, 23 Aug 2026 15:07:45 -0700
Subject: [PATCH 09/20] redesign: indicate that originalAccessKeyId in s3g log
is unverified
---
.../org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java | 3 ++-
.../java/org/apache/hadoop/ozone/s3/util/AuditUtils.java | 7 ++++---
.../apache/hadoop/ozone/s3/endpoint/TestEndpointBase.java | 2 +-
3 files changed, 7 insertions(+), 5 deletions(-)
diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
index c8c7d168b18..5bc8935f0b3 100644
--- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
+++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
@@ -608,9 +608,10 @@ protected AuditMessage.Builder auditMessageFor(AuditAction op) {
if (s3Auth != null) {
// For STS temporary credentials, record the originalAccessKeyId (the permanent principal that
// created the token) so the audit trail is not limited to the opaque tempAccessKeyId.
+ // This value is decoded from the client-presented session token only - OM validates it separately.
final String originalAccessKeyId = AuditUtils.getStsOriginalAccessKeyId(s3Auth.getSessionToken());
if (originalAccessKeyId != null) {
- auditMap.put(OzoneConsts.S3_STS_ORIGINAL_ACCESS_KEY_ID, originalAccessKeyId);
+ auditMap.put(OzoneConsts.S3_STS_ORIGINAL_ACCESS_KEY_ID, originalAccessKeyId + " (unverified)");
}
}
diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/AuditUtils.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/AuditUtils.java
index 85486df8c3d..17567c9be96 100644
--- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/AuditUtils.java
+++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/AuditUtils.java
@@ -36,9 +36,10 @@ private AuditUtils() {
/**
* Extracts (if possible) the STS {@code originalAccessKeyId} from a session token so it can be
- * recorded in the S3 Gateway audit log. Like the S3 access id already recorded as the
- * audit user, this reflects what the client presented. Returns {@code null} when no usable value
- * can be extracted so the audit path is never disrupted by a missing or malformed token.
+ * recorded in the S3 Gateway audit log. Like the S3 access id already recorded as the audit user,
+ * this reflects what the client presented and is not cryptographically validated by S3 Gateway.
+ * Returns {@code null} when no usable value can be extracted so the audit path is never disrupted
+ * by a missing or malformed token.
*/
public static String getStsOriginalAccessKeyId(String sessionToken) {
if (sessionToken == null || sessionToken.isEmpty()) {
diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestEndpointBase.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestEndpointBase.java
index 0f23eb221ef..b72287287fc 100644
--- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestEndpointBase.java
+++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestEndpointBase.java
@@ -169,7 +169,7 @@ public void testAuditMessageIncludesStsOriginalAccessKeyId() throws Exception {
final AuditEndpoint endpointBase = newAuditEndpoint(signatureInfo);
assertThat(endpointBase.auditMessageForTest().getParams())
- .containsEntry(ORIGINAL_ACCESS_KEY_ID_PARAM, originalAccessKeyId);
+ .containsEntry(ORIGINAL_ACCESS_KEY_ID_PARAM, originalAccessKeyId + " (unverified)");
}
@Test
From 520f74133796dc35ff988318c617e316d25c9442 Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Mon, 24 Aug 2026 14:47:44 -0700
Subject: [PATCH 10/20] pr review update for ChenSammi - use
ACCESS_ID_NOT_FOUND in S3RevokeSTSTokenRequest
---
.../dist/src/main/smoketest/security/ozone-secure-sts.robot | 2 +-
.../ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java | 3 ++-
.../om/request/s3/security/TestS3RevokeSTSTokenRequest.java | 4 ++--
3 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot b/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot
index 4a01bfd93ee..3ee4e19a5ae 100644
--- a/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot
+++ b/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot
@@ -593,7 +593,7 @@ Revoke STS Token Should Fail For Unknown Original Access Key Id
# Revoking a bogus originalAccessKeyId must fail before writing to the revocation table.
Kinit test user ${TEST_USER_ADMIN} ${TEST_USER_ADMIN}.keytab
${output} = Execute And Ignore Error ozone s3 revokeststoken -o bogus-original-access-key-id -y ${OM_HA_PARAM}
- Should Contain ${output} INVALID_REQUEST
+ Should Contain ${output} ACCESS_ID_NOT_FOUND
Should Contain ${output} does not exist
List Objects V1 and V2 IAM Session Policy Matrix for OBS and FSO
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java
index 5ce42618e2a..0012675a801 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java
@@ -17,6 +17,7 @@
package org.apache.hadoop.ozone.om.request.s3.security;
+import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.ACCESS_ID_NOT_FOUND;
import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INTERNAL_ERROR;
import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_REQUEST;
@@ -84,7 +85,7 @@ public OMRequest preExecute(OzoneManager ozoneManager) throws IOException {
S3SecretRequestHelper.checkAccessIdSecretOpPermission(ozoneManager, ugi, originalAccessKeyId);
if (!ozoneManager.getS3SecretManager().hasS3Secret(originalAccessKeyId)) {
- throw new OMException("originalAccessKeyId does not exist: " + originalAccessKeyId, INVALID_REQUEST);
+ throw new OMException("originalAccessKeyId does not exist: " + originalAccessKeyId, ACCESS_ID_NOT_FOUND);
}
final long revocationTimeMillis = CLOCK.millis();
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3RevokeSTSTokenRequest.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3RevokeSTSTokenRequest.java
index 99b6d6a98e9..e73f9f53a71 100644
--- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3RevokeSTSTokenRequest.java
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3RevokeSTSTokenRequest.java
@@ -226,7 +226,7 @@ public void testPreExecuteRejectsUnknownOriginalAccessKeyId() throws Exception {
ozoneManager, originalAccessKeyId, false);
final OMClientRequest omClientRequest = new S3RevokeSTSTokenRequest(buildRevokeOmRequest(originalAccessKeyId));
final OMException ex = assertThrows(OMException.class, () -> omClientRequest.preExecute(ozoneManager));
- assertEquals(OMException.ResultCodes.INVALID_REQUEST, ex.getResult());
+ assertEquals(OMException.ResultCodes.ACCESS_ID_NOT_FOUND, ex.getResult());
assertTrue(ex.getMessage().contains("does not exist"));
assertTrue(ex.getMessage().contains(originalAccessKeyId));
verify(s3SecretManager).hasS3Secret(originalAccessKeyId);
@@ -247,7 +247,7 @@ public void testPreExecuteRejectsUnknownOriginalAccessKeyIdForS3Admin() throws E
final OMClientRequest omClientRequest = new S3RevokeSTSTokenRequest(buildRevokeOmRequest(originalAccessKeyId));
final OMException ex = assertThrows(OMException.class, () -> omClientRequest.preExecute(ozoneManager));
- assertEquals(OMException.ResultCodes.INVALID_REQUEST, ex.getResult());
+ assertEquals(OMException.ResultCodes.ACCESS_ID_NOT_FOUND, ex.getResult());
assertTrue(ex.getMessage().contains("does not exist"));
assertTrue(ex.getMessage().contains(originalAccessKeyId));
verify(s3SecretManager).hasS3Secret(originalAccessKeyId);
From 9f18d714d4590e93a80cb17c628b10e000cc2acb Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Mon, 24 Aug 2026 15:16:01 -0700
Subject: [PATCH 11/20] pr review updates for ChenSammi - reuse
RevokeSTSTokenRequest instead of having UpdateRevokeSTSTokenRequest
---
.../src/main/proto/OmClientProtocol.proto | 12 +----
.../s3/security/S3RevokeSTSTokenRequest.java | 36 +++++++------
.../security/TestS3RevokeSTSTokenRequest.java | 51 +++++++------------
3 files changed, 38 insertions(+), 61 deletions(-)
diff --git a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
index 029a78a335d..638cf99bd1c 100644
--- a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
+++ b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
@@ -333,7 +333,6 @@ message OMRequest {
optional RevokeSTSTokenRequest revokeSTSTokenRequest = 155;
optional DeleteRevokedSTSTokensRequest deleteRevokedSTSTokensRequest = 156;
optional UpdateAssumeRoleRequest updateAssumeRoleRequest = 157;
- optional UpdateRevokeSTSTokenRequest updateRevokeSTSTokenRequest = 158;
}
message OMResponse {
@@ -2536,15 +2535,8 @@ message UpdateAssumeRoleRequest {
message RevokeSTSTokenRequest {
required string originalAccessKeyId = 1;
-}
-
-/**
- This request will be used internally by OM to replicate the revocation cutoff captured by the leader
- across the OMs in HA mode.
-*/
-message UpdateRevokeSTSTokenRequest {
- required string originalAccessKeyId = 1;
- required uint64 revocationTimeMillis = 2;
+ // Leader-generated revocation cutoff, replicated across OMs in HA mode.
+ optional uint64 revocationTimeMillis = 2;
}
message RevokeSTSTokenResponse {
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java
index 0012675a801..18d6a9870f4 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java
@@ -42,7 +42,6 @@
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.RevokeSTSTokenRequest;
-import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.UpdateRevokeSTSTokenRequest;
import org.apache.hadoop.security.UserGroupInformation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -51,8 +50,8 @@
* Handles S3RevokeSTSTokenRequest request.
*
* The client submits {@link RevokeSTSTokenRequest} with {@code originalAccessKeyId} only. On the
- * leader, {@code preExecute} captures the revocation cutoff and builds an {@link UpdateRevokeSTSTokenRequest}
- * that is replicated through Ratis so every OM applies the same cutoff.
+ * leader, {@code preExecute} captures the revocation cutoff in {@code revocationTimeMillis} and
+ * replicates the updated request through Ratis so every OM applies the same cutoff.
*
* This request records a revocation cutoff for the given {@code originalAccessKeyId} in the
* {@code s3RevokedStsTokenTable}. Subsequent S3 requests authenticated with STS tokens whose
@@ -89,13 +88,12 @@ public OMRequest preExecute(OzoneManager ozoneManager) throws IOException {
}
final long revocationTimeMillis = CLOCK.millis();
- final UpdateRevokeSTSTokenRequest updateRevokeSTSTokenRequest = UpdateRevokeSTSTokenRequest.newBuilder()
- .setOriginalAccessKeyId(originalAccessKeyId)
+ final RevokeSTSTokenRequest updatedRevokeReq = revokeReq.toBuilder()
.setRevocationTimeMillis(revocationTimeMillis)
.build();
return omRequest.toBuilder()
- .setUpdateRevokeSTSTokenRequest(updateRevokeSTSTokenRequest)
+ .setRevokeSTSTokenRequest(updatedRevokeReq)
.build();
}
@@ -107,10 +105,9 @@ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, Execut
String originalAccessKeyId = null;
try {
- validateReplicatedRevokeRequestFields(getOmRequest());
- final UpdateRevokeSTSTokenRequest updateRevokeSTSTokenRequest = getOmRequest().getUpdateRevokeSTSTokenRequest();
- originalAccessKeyId = updateRevokeSTSTokenRequest.getOriginalAccessKeyId();
- final long revocationTimeMillis = updateRevokeSTSTokenRequest.getRevocationTimeMillis();
+ final RevokeSTSTokenRequest revokeReq = validateReplicatedRevokeRequestFields(getOmRequest());
+ originalAccessKeyId = revokeReq.getOriginalAccessKeyId();
+ final long revocationTimeMillis = revokeReq.getRevocationTimeMillis();
// All actual DB mutations are done in the response's addToDBBatch().
omClientResponse = new S3RevokeSTSTokenResponse(originalAccessKeyId, revocationTimeMillis, omResponse.build());
@@ -147,18 +144,19 @@ private static void validateRevokeRequestFields(RevokeSTSTokenRequest revokeReq)
if (originalAccessKeyId.length() >= OzoneConsts.OZONE_MAXIMUM_ACCESS_ID_LENGTH) {
throw new OMException("originalAccessKeyId length is invalid: " + originalAccessKeyId.length(), INVALID_REQUEST);
}
+ if (revokeReq.hasRevocationTimeMillis()) {
+ throw new OMException("revocationTimeMillis must not be set by client", INVALID_REQUEST);
+ }
}
- private static void validateReplicatedRevokeRequestFields(OMRequest omRequest) throws OMException {
- if (!omRequest.hasUpdateRevokeSTSTokenRequest()) {
- throw new OMException("updateRevokeSTSTokenRequest is required for STS token revocation", INTERNAL_ERROR);
+ private static RevokeSTSTokenRequest validateReplicatedRevokeRequestFields(OMRequest omRequest) throws OMException {
+ if (!omRequest.hasRevokeSTSTokenRequest()) {
+ throw new OMException("revokeSTSTokenRequest is required for STS token revocation", INTERNAL_ERROR);
}
- final String originalAccessKeyId = omRequest.getRevokeSTSTokenRequest().getOriginalAccessKeyId();
- final UpdateRevokeSTSTokenRequest updateRevokeSTSTokenRequest = omRequest.getUpdateRevokeSTSTokenRequest();
- if (!originalAccessKeyId.equals(updateRevokeSTSTokenRequest.getOriginalAccessKeyId())) {
- throw new OMException(
- "originalAccessKeyId mismatch between revokeSTSTokenRequest and updateRevokeSTSTokenRequest",
- INTERNAL_ERROR);
+ final RevokeSTSTokenRequest revokeReq = omRequest.getRevokeSTSTokenRequest();
+ if (!revokeReq.hasRevocationTimeMillis()) {
+ throw new OMException("revocationTimeMillis is required for STS token revocation", INTERNAL_ERROR);
}
+ return revokeReq;
}
}
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3RevokeSTSTokenRequest.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3RevokeSTSTokenRequest.java
index e73f9f53a71..212eeeadee3 100644
--- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3RevokeSTSTokenRequest.java
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3RevokeSTSTokenRequest.java
@@ -126,9 +126,9 @@ public void testPreExecuteSucceedsForOriginalAccessKeyOwner() throws Exception {
final OMRequest result = omClientRequest.preExecute(ozoneManager);
assertEquals(Type.RevokeSTSToken, result.getCmdType());
- assertTrue(result.hasUpdateRevokeSTSTokenRequest());
- assertEquals(originalAccessKeyId, result.getUpdateRevokeSTSTokenRequest().getOriginalAccessKeyId());
- assertTrue(result.getUpdateRevokeSTSTokenRequest().getRevocationTimeMillis() > 0L);
+ assertTrue(result.getRevokeSTSTokenRequest().hasRevocationTimeMillis());
+ assertEquals(originalAccessKeyId, result.getRevokeSTSTokenRequest().getOriginalAccessKeyId());
+ assertTrue(result.getRevokeSTSTokenRequest().getRevocationTimeMillis() > 0L);
}
@Test
@@ -272,10 +272,6 @@ public void testValidateAndUpdateCacheUpdatesCacheImmediately() {
final OzoneManagerProtocolProtos.RevokeSTSTokenRequest revokeRequest =
OzoneManagerProtocolProtos.RevokeSTSTokenRequest.newBuilder()
- .setOriginalAccessKeyId(originalAccessKeyId)
- .build();
- final OzoneManagerProtocolProtos.UpdateRevokeSTSTokenRequest updateRevokeRequest =
- OzoneManagerProtocolProtos.UpdateRevokeSTSTokenRequest.newBuilder()
.setOriginalAccessKeyId(originalAccessKeyId)
.setRevocationTimeMillis(revocationTimeMillis)
.build();
@@ -284,7 +280,6 @@ public void testValidateAndUpdateCacheUpdatesCacheImmediately() {
.setClientId(UUID.randomUUID().toString())
.setCmdType(Type.RevokeSTSToken)
.setRevokeSTSTokenRequest(revokeRequest)
- .setUpdateRevokeSTSTokenRequest(updateRevokeRequest)
.build();
final S3RevokeSTSTokenRequest s3RevokeSTSTokenRequest = new S3RevokeSTSTokenRequest(omRequest);
@@ -300,7 +295,7 @@ public void testValidateAndUpdateCacheUpdatesCacheImmediately() {
}
@Test
- public void testValidateAndUpdateCacheRejectsMissingUpdateRevokeSTSTokenRequest() {
+ public void testValidateAndUpdateCacheRejectsMissingRevocationTimeMillis() {
final String originalAccessKeyId = "original-access-key-id";
final OzoneManager ozoneManager = mock(OzoneManager.class);
@@ -330,36 +325,28 @@ public void testValidateAndUpdateCacheRejectsMissingUpdateRevokeSTSTokenRequest(
}
@Test
- public void testValidateAndUpdateCacheRejectsMismatchedOriginalAccessKeyId() {
+ public void testPreExecuteRejectsClientSuppliedRevocationTimeMillis() throws Exception {
final String originalAccessKeyId = "original-access-key-id";
- final String mismatchedAccessKeyId = "other-access-key-id";
- final long revocationTimeMillis = 1_700_000_000_000L;
-
- final OzoneManager ozoneManager = mock(OzoneManager.class);
- final OMMetadataManager omMetadataManager = mock(OMMetadataManager.class);
- @SuppressWarnings("unchecked")
- final Table s3RevokedStsTokenTable = mock(Table.class);
- final ExecutionContext context = mock(ExecutionContext.class);
-
- when(ozoneManager.getMetadataManager()).thenReturn(omMetadataManager);
- when(omMetadataManager.getS3RevokedStsTokenTable()).thenReturn(s3RevokedStsTokenTable);
+ final UserGroupInformation callerUgi = UserGroupInformation.createRemoteUser(originalAccessKeyId);
+ Server.getCurCall().set(new StubCall(callerUgi));
+ final OzoneManagerProtocolProtos.RevokeSTSTokenRequest revokeRequest =
+ OzoneManagerProtocolProtos.RevokeSTSTokenRequest.newBuilder()
+ .setOriginalAccessKeyId(originalAccessKeyId)
+ .setRevocationTimeMillis(1_700_000_000_000L)
+ .build();
final OMRequest omRequest = OMRequest.newBuilder()
.setClientId(UUID.randomUUID().toString())
.setCmdType(Type.RevokeSTSToken)
- .setRevokeSTSTokenRequest(OzoneManagerProtocolProtos.RevokeSTSTokenRequest.newBuilder()
- .setOriginalAccessKeyId(originalAccessKeyId)
- .build())
- .setUpdateRevokeSTSTokenRequest(OzoneManagerProtocolProtos.UpdateRevokeSTSTokenRequest.newBuilder()
- .setOriginalAccessKeyId(mismatchedAccessKeyId)
- .setRevocationTimeMillis(revocationTimeMillis)
- .build())
+ .setRevokeSTSTokenRequest(revokeRequest)
.build();
- final S3RevokeSTSTokenRequest s3RevokeSTSTokenRequest = new S3RevokeSTSTokenRequest(omRequest);
- final OMClientResponse omClientResponse =
- s3RevokeSTSTokenRequest.validateAndUpdateCache(ozoneManager, context);
- assertEquals(OzoneManagerProtocolProtos.Status.INTERNAL_ERROR, omClientResponse.getOMResponse().getStatus());
+ try (OzoneManager ozoneManager = mock(OzoneManager.class)) {
+ configureOzoneManagerForPreExecute(ozoneManager, originalAccessKeyId, true);
+ final OMClientRequest omClientRequest = new S3RevokeSTSTokenRequest(omRequest);
+ final OMException ex = assertThrows(OMException.class, () -> omClientRequest.preExecute(ozoneManager));
+ assertEquals(OMException.ResultCodes.INVALID_REQUEST, ex.getResult());
+ }
}
@Test
From 8d38c52e99fb44df8b51ee7caabe40fd0988f078 Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Mon, 24 Aug 2026 15:22:37 -0700
Subject: [PATCH 12/20] pr review updates for ChenSammi - remove unnecessary
validation in S3RevokeSTSTokenRequest
---
.../s3/security/S3RevokeSTSTokenRequest.java | 3 ---
.../security/TestS3RevokeSTSTokenRequest.java | 17 -----------------
2 files changed, 20 deletions(-)
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java
index 18d6a9870f4..ac02afc5879 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java
@@ -141,9 +141,6 @@ private static void validateRevokeRequestFields(RevokeSTSTokenRequest revokeReq)
if (StringUtils.isEmpty(originalAccessKeyId)) {
throw new OMException("originalAccessKeyId is required for STS token revocation", INVALID_REQUEST);
}
- if (originalAccessKeyId.length() >= OzoneConsts.OZONE_MAXIMUM_ACCESS_ID_LENGTH) {
- throw new OMException("originalAccessKeyId length is invalid: " + originalAccessKeyId.length(), INVALID_REQUEST);
- }
if (revokeReq.hasRevocationTimeMillis()) {
throw new OMException("revocationTimeMillis must not be set by client", INVALID_REQUEST);
}
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3RevokeSTSTokenRequest.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3RevokeSTSTokenRequest.java
index 212eeeadee3..c75a1fbccc7 100644
--- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3RevokeSTSTokenRequest.java
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3RevokeSTSTokenRequest.java
@@ -349,23 +349,6 @@ public void testPreExecuteRejectsClientSuppliedRevocationTimeMillis() throws Exc
}
}
- @Test
- public void testPreExecuteRejectsOverlongOriginalAccessKeyId() throws Exception {
- final StringBuilder sb = new StringBuilder();
- for (int i = 0; i < OzoneConsts.OZONE_MAXIMUM_ACCESS_ID_LENGTH; i++) {
- sb.append('a');
- }
- final UserGroupInformation callerUgi = UserGroupInformation.createRemoteUser("caller");
- Server.getCurCall().set(new StubCall(callerUgi));
-
- try (OzoneManager ozoneManager = mock(OzoneManager.class)) {
- configureOzoneManagerForPreExecute(ozoneManager, sb.toString(), false);
- final OMClientRequest omClientRequest = new S3RevokeSTSTokenRequest(buildRevokeOmRequest(sb.toString()));
- final OMException ex = assertThrows(OMException.class, () -> omClientRequest.preExecute(ozoneManager));
- assertEquals(OMException.ResultCodes.INVALID_REQUEST, ex.getResult());
- }
- }
-
private static OMRequest buildRevokeOmRequest(String originalAccessKeyId) {
final OzoneManagerProtocolProtos.RevokeSTSTokenRequest revokeRequest =
OzoneManagerProtocolProtos.RevokeSTSTokenRequest.newBuilder()
From 5ae44f8bc04038bd22dc515022fd1a9220e3b6eb Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Mon, 24 Aug 2026 16:26:31 -0700
Subject: [PATCH 13/20] pr review updates for ChenSammi - update
S3RevokeSTSTokenRequest audit params
---
.../s3/security/S3RevokeSTSTokenRequest.java | 15 +++++----------
.../s3/security/TestS3RevokeSTSTokenRequest.java | 2 +-
2 files changed, 6 insertions(+), 11 deletions(-)
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java
index ac02afc5879..9953598e814 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java
@@ -38,7 +38,6 @@
import org.apache.hadoop.ozone.om.request.util.OmResponseUtil;
import org.apache.hadoop.ozone.om.response.OMClientResponse;
import org.apache.hadoop.ozone.om.response.s3.security.S3RevokeSTSTokenResponse;
-import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.RevokeSTSTokenRequest;
@@ -102,11 +101,12 @@ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, Execut
final OMResponse.Builder omResponse = OmResponseUtil.getOMResponseBuilder(getOmRequest());
IOException exception = null;
OMClientResponse omClientResponse;
- String originalAccessKeyId = null;
+ final Map auditMap = new HashMap<>();
try {
final RevokeSTSTokenRequest revokeReq = validateReplicatedRevokeRequestFields(getOmRequest());
- originalAccessKeyId = revokeReq.getOriginalAccessKeyId();
+ final String originalAccessKeyId = revokeReq.getOriginalAccessKeyId();
+ auditMap.put(OzoneConsts.S3_REVOKESTSTOKEN_USER, originalAccessKeyId);
final long revocationTimeMillis = revokeReq.getRevocationTimeMillis();
// All actual DB mutations are done in the response's addToDBBatch().
@@ -125,14 +125,9 @@ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, Execut
}
// Audit log
- final Map auditMap = new HashMap<>();
- final OzoneManagerProtocolProtos.UserInfo userInfo = getOmRequest().getUserInfo();
- auditMap.put(OzoneConsts.S3_REVOKESTSTOKEN_USER, userInfo.getUserName());
- if (originalAccessKeyId != null) {
- auditMap.put(OzoneConsts.S3_STS_ORIGINAL_ACCESS_KEY_ID, originalAccessKeyId);
- }
markForAudit(
- ozoneManager.getAuditLogger(), buildAuditMessage(OMAction.REVOKE_STS_TOKEN, auditMap, exception, userInfo));
+ ozoneManager.getAuditLogger(), buildAuditMessage(
+ OMAction.REVOKE_STS_TOKEN, auditMap, exception, getOmRequest().getUserInfo()));
return omClientResponse;
}
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3RevokeSTSTokenRequest.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3RevokeSTSTokenRequest.java
index c75a1fbccc7..1b6caed9cb4 100644
--- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3RevokeSTSTokenRequest.java
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3RevokeSTSTokenRequest.java
@@ -291,7 +291,7 @@ public void testValidateAndUpdateCacheUpdatesCacheImmediately() {
assertNotNull(s3RevokeSTSTokenRequest.getAuditBuilder().getAuditMap());
assertEquals(
originalAccessKeyId, s3RevokeSTSTokenRequest.getAuditBuilder().getAuditMap().get(
- OzoneConsts.S3_STS_ORIGINAL_ACCESS_KEY_ID));
+ OzoneConsts.S3_REVOKESTSTOKEN_USER));
}
@Test
From 7c52c697664f80f0b426e102942cb9726b5a5255 Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Mon, 24 Aug 2026 18:38:22 -0700
Subject: [PATCH 14/20] pr review comments for ChenSammi - use same secretKey
for encryption and signing
---
.../ozone/security/STSSecurityUtil.java | 4 +
.../ozone/security/STSTokenSecretManager.java | 20 ++++-
.../security/TestSTSTokenSecretManager.java | 74 ++++++++++++++++++-
3 files changed, 93 insertions(+), 5 deletions(-)
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSSecurityUtil.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSSecurityUtil.java
index 03a1fdba017..41aefeee768 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSSecurityUtil.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSSecurityUtil.java
@@ -154,6 +154,10 @@ private static ManagedSecretKey getValidatedSecretKey(UUID secretKeyId, SecretKe
private static Token decodeTokenFromString(String encodedToken)
throws SecretManager.InvalidToken {
final Token token = new Token<>();
+ // token.decodeFromUrlString() only declares IOException, but deserialization can throw
+ // unchecked exceptions (e.g. NegativeArraySizeException) when malformed input decodes to a
+ // negative byte-array length. Map those to InvalidToken (via catching RuntimeException)
+ // instead of failing the OM request.
try {
token.decodeFromUrlString(encodedToken);
return token;
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenSecretManager.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenSecretManager.java
index 8cddc50f18a..66273299fea 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenSecretManager.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenSecretManager.java
@@ -17,9 +17,11 @@
package org.apache.hadoop.ozone.security;
+import com.google.common.base.Preconditions;
import java.io.IOException;
import java.time.Clock;
import java.time.Instant;
+import java.util.Objects;
import org.apache.hadoop.hdds.annotation.InterfaceAudience;
import org.apache.hadoop.hdds.annotation.InterfaceStability;
import org.apache.hadoop.hdds.security.symmetric.ManagedSecretKey;
@@ -65,6 +67,15 @@ public STSTokenSecretManager(SecretKeySignerClient secretKeyClient) {
public Token generateToken(STSTokenIdentifier tokenIdentifier) {
final ManagedSecretKey secretKey = secretKeyClient.getCurrentSecretKey();
tokenIdentifier.setSecretKeyId(secretKey.getId());
+ return generateToken(tokenIdentifier, secretKey);
+ }
+
+ private Token generateToken(STSTokenIdentifier tokenIdentifier, ManagedSecretKey secretKey) {
+ Objects.requireNonNull(
+ tokenIdentifier.getSecretKeyId(), "secretKeyId must be set on the token identifier before signing");
+ Preconditions.checkArgument(
+ secretKey.getId().equals(tokenIdentifier.getSecretKeyId()), "secretKeyId on the token identifier " +
+ "must match the signing secret key");
final byte[] identifierBytes = tokenIdentifier.getBytes();
final byte[] password = secretKey.sign(identifierBytes);
return new Token<>(identifierBytes, password, tokenIdentifier.getKind(), new Text(tokenIdentifier.getService()));
@@ -88,9 +99,9 @@ public String createSTSTokenString(String tempAccessKeyId, String originalAccess
final Instant creationTime = clock.instant();
final Instant expiration = creationTime.plusSeconds(durationSeconds);
- // Get the current secret key for encryption
- final ManagedSecretKey currentSecretKey = secretKeyClient.getCurrentSecretKey();
- final byte[] encryptionKey = currentSecretKey.getSecretKey().getEncoded();
+ // Get the current secret key once for encryption, secretKeyId, and signing.
+ final ManagedSecretKey secretKey = secretKeyClient.getCurrentSecretKey();
+ final byte[] encryptionKey = secretKey.getSecretKey().getEncoded();
// Note - the encryptionKey will NOT be encoded in the token. When generateToken() is called, it eventually calls
// the write() method in STSTokenIdentifier which calls toProtoBuf(), and the encryptionKey is not
@@ -105,8 +116,9 @@ public String createSTSTokenString(String tempAccessKeyId, String originalAccess
.setSessionPolicy(sessionPolicy)
.setEncryptionKey(encryptionKey)
.build());
+ identifier.setSecretKeyId(secretKey.getId());
- final Token token = generateToken(identifier);
+ final Token token = generateToken(identifier, secretKey);
return token.encodeToUrlString();
}
}
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java
index 525ee5f0da0..e2e28fd4e7d 100644
--- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java
@@ -26,12 +26,16 @@
import java.io.IOException;
import java.nio.charset.StandardCharsets;
+import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
+import java.util.HashMap;
+import java.util.Map;
import java.util.UUID;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.apache.hadoop.hdds.security.symmetric.ManagedSecretKey;
+import org.apache.hadoop.hdds.security.symmetric.SecretKeyClient;
import org.apache.hadoop.hdds.security.symmetric.SecretKeySignerClient;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.security.token.Token;
@@ -70,7 +74,7 @@ public void setUp() throws Exception {
final UUID keyId = UUID.fromString("00000000-0000-0000-0000-000000000000");
when(mockSecretKey.getId()).thenReturn(keyId);
when(mockSecretKey.getSecretKey()).thenReturn(sharedSecretKey);
- when(mockSecretKey.sign(any(STSTokenIdentifier.class)))
+ when(mockSecretKey.sign(any(byte[].class)))
.thenReturn("mock-signature".getBytes(StandardCharsets.UTF_8));
when(mockSecretKeyClient.getCurrentSecretKey()).thenReturn(mockSecretKey);
@@ -119,4 +123,72 @@ public void testCreateSTSTokenStringWithNullSessionPolicy() throws IOException {
identifier.readFromByteArray(token.getIdentifier());
assertTrue(identifier.getSessionPolicy().isEmpty());
}
+
+ /**
+ * createSTSTokenString() must use a single getCurrentSecretKey() for encryption, secretKeyId, and signing. If a
+ * second fetch happened during signing, a key rotation between calls would encrypt with the old key but stamp the
+ * token with the new key id.
+ */
+ @Test
+ public void testCreateSTSTokenStringValidatesWhenSecretKeyRotatesDuringCreation() throws Exception {
+ // ManagedSecretKey.isExpired() uses Instant.now(), not the test clock.
+ final Instant keyCreationTime = Instant.now();
+ final ManagedSecretKey encryptionKey = createManagedSecretKey(
+ UUID.fromString("11111111-1111-1111-1111-111111111111"),
+ "encryption-key-material-012345678901".getBytes(StandardCharsets.US_ASCII),
+ keyCreationTime);
+ final ManagedSecretKey signingKey = createManagedSecretKey(
+ UUID.fromString("22222222-2222-2222-2222-222222222222"),
+ "signing-key-material-01234567890123".getBytes(StandardCharsets.US_ASCII),
+ keyCreationTime);
+
+ final RotatingSecretKeyTestClient rotatingSecretKeyClient = new RotatingSecretKeyTestClient(
+ encryptionKey, signingKey);
+ final STSTokenSecretManager rotatingSecretManager = new STSTokenSecretManager(rotatingSecretKeyClient);
+
+ final String tokenString = rotatingSecretManager.createSTSTokenString(
+ TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+
+ final STSTokenIdentifier result = STSSecurityUtil.constructValidateAndDecryptSTSToken(
+ tokenString, rotatingSecretKeyClient, clock);
+ assertEquals(SECRET_ACCESS_KEY, result.getSecretAccessKey());
+ assertEquals(encryptionKey.getId(), result.getSecretKeyId());
+ assertEquals(1, rotatingSecretKeyClient.getCurrentSecretKeyCallCount());
+ }
+
+ private static ManagedSecretKey createManagedSecretKey(UUID id, byte[] keyBytes, Instant creationTime) {
+ final SecretKey secretKey = new SecretKeySpec(keyBytes, "HmacSHA256");
+ return new ManagedSecretKey(id, creationTime, creationTime.plus(Duration.ofHours(1)), secretKey);
+ }
+
+ /**
+ * Returns different current keys on consecutive getCurrentSecretKey() calls to simulate rotation.
+ */
+ private static final class RotatingSecretKeyTestClient implements SecretKeyClient {
+ private final ManagedSecretKey firstKey;
+ private final ManagedSecretKey secondKey;
+ private final Map keysById = new HashMap<>();
+ private int getCurrentSecretKeyCallCount;
+
+ private RotatingSecretKeyTestClient(ManagedSecretKey firstKey, ManagedSecretKey secondKey) {
+ this.firstKey = firstKey;
+ this.secondKey = secondKey;
+ keysById.put(firstKey.getId(), firstKey);
+ keysById.put(secondKey.getId(), secondKey);
+ }
+
+ @Override
+ public synchronized ManagedSecretKey getCurrentSecretKey() {
+ return getCurrentSecretKeyCallCount++ == 0 ? firstKey : secondKey;
+ }
+
+ @Override
+ public ManagedSecretKey getSecretKey(UUID id) {
+ return keysById.get(id);
+ }
+
+ private int getCurrentSecretKeyCallCount() {
+ return getCurrentSecretKeyCallCount;
+ }
+ }
}
From 1c09e28b2992fdd06f7efd9b31fe6512ebd7ca02 Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Mon, 24 Aug 2026 18:49:42 -0700
Subject: [PATCH 15/20] pr review updates for ChenSammi - remove adding
originalAccessKeyId to s3g audit log
---
.../org/apache/hadoop/ozone/OzoneConsts.java | 1 -
.../ozone/s3/endpoint/EndpointBase.java | 9 ---
.../hadoop/ozone/s3/util/AuditUtils.java | 24 -------
.../ozone/s3/endpoint/TestEndpointBase.java | 57 -----------------
.../hadoop/ozone/s3/util/TestAuditUtils.java | 62 -------------------
5 files changed, 153 deletions(-)
delete mode 100644 hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestAuditUtils.java
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneConsts.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneConsts.java
index d2ebc831e4b..188bd65559f 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneConsts.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/OzoneConsts.java
@@ -314,7 +314,6 @@ public final class OzoneConsts {
public static final String S3_SETSECRET_USER = "S3SetSecretUser";
public static final String S3_REVOKESECRET_USER = "S3RevokeSecretUser";
public static final String S3_REVOKESTSTOKEN_USER = "S3RevokeSTSTokenUser";
- public static final String S3_STS_ORIGINAL_ACCESS_KEY_ID = "originalAccessKeyId";
public static final String S3_STS_TEMP_ACCESS_KEY_ID = "tempAccessKeyId";
public static final String RENAMED_KEYS_MAP = "renamedKeysMap";
public static final String UNRENAMED_KEYS_MAP = "unRenamedKeysMap";
diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
index 5bc8935f0b3..63e25fca628 100644
--- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
+++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
@@ -605,15 +605,6 @@ protected AuditMessage.Builder auditMessageFor(AuditAction op) {
Map auditMap = getAuditParameters();
auditMap.put("x-amz-request-id", requestIdentifier.getRequestId());
auditMap.put("x-amz-id-2", requestIdentifier.getAmzId());
- if (s3Auth != null) {
- // For STS temporary credentials, record the originalAccessKeyId (the permanent principal that
- // created the token) so the audit trail is not limited to the opaque tempAccessKeyId.
- // This value is decoded from the client-presented session token only - OM validates it separately.
- final String originalAccessKeyId = AuditUtils.getStsOriginalAccessKeyId(s3Auth.getSessionToken());
- if (originalAccessKeyId != null) {
- auditMap.put(OzoneConsts.S3_STS_ORIGINAL_ACCESS_KEY_ID, originalAccessKeyId + " (unverified)");
- }
- }
AuditMessage.Builder builder = new AuditMessage.Builder()
.forOperation(op)
diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/AuditUtils.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/AuditUtils.java
index 17567c9be96..978f96ddb35 100644
--- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/AuditUtils.java
+++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/AuditUtils.java
@@ -19,13 +19,10 @@
import static org.apache.hadoop.ozone.s3.ClientIpFilter.CLIENT_IP_HEADER;
-import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.ws.rs.container.ContainerRequestContext;
-import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto;
-import org.apache.hadoop.security.token.Token;
/**
* Common utilities for operation auditing purposes.
@@ -34,27 +31,6 @@ public final class AuditUtils {
private AuditUtils() {
}
- /**
- * Extracts (if possible) the STS {@code originalAccessKeyId} from a session token so it can be
- * recorded in the S3 Gateway audit log. Like the S3 access id already recorded as the audit user,
- * this reflects what the client presented and is not cryptographically validated by S3 Gateway.
- * Returns {@code null} when no usable value can be extracted so the audit path is never disrupted
- * by a missing or malformed token.
- */
- public static String getStsOriginalAccessKeyId(String sessionToken) {
- if (sessionToken == null || sessionToken.isEmpty()) {
- return null;
- }
- try {
- final Token> token = new Token<>();
- token.decodeFromUrlString(sessionToken);
- final String originalAccessKeyId = OMTokenProto.parseFrom(token.getIdentifier()).getOriginalAccessKeyId();
- return originalAccessKeyId.isEmpty() ? null : originalAccessKeyId;
- } catch (IOException | RuntimeException e) {
- return null;
- }
- }
-
public static Map getAuditParameters(
ContainerRequestContext context) {
Map res = new HashMap<>();
diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestEndpointBase.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestEndpointBase.java
index b72287287fc..9865345a916 100644
--- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestEndpointBase.java
+++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestEndpointBase.java
@@ -36,16 +36,10 @@
import java.util.stream.Stream;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
-import org.apache.hadoop.io.Text;
import org.apache.hadoop.ozone.OzoneConsts;
-import org.apache.hadoop.ozone.audit.AuditMessage;
-import org.apache.hadoop.ozone.audit.S3GAction;
import org.apache.hadoop.ozone.client.OzoneVolume;
import org.apache.hadoop.ozone.om.exceptions.OMException;
-import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto;
import org.apache.hadoop.ozone.s3.exception.OS3Exception;
-import org.apache.hadoop.ozone.s3.signature.SignatureInfo;
-import org.apache.hadoop.security.token.Token;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
@@ -55,7 +49,6 @@
* Test methods of the EndpointBase.
*/
public class TestEndpointBase {
- private static final String ORIGINAL_ACCESS_KEY_ID_PARAM = "originalAccessKeyId";
/**
* Verify s3 metadata key "gdprEnabled" can't be set up directly
@@ -153,38 +146,6 @@ public void init() { }
assertFalse(endpointBase.isExpiredToken(new OMException(ResultCodes.INVALID_TOKEN)));
}
- @Test
- public void testAuditMessageIncludesStsOriginalAccessKeyId() throws Exception {
- final String originalAccessKeyId = "AKIAORIGINAL123";
- final OMTokenProto proto = OMTokenProto.newBuilder()
- .setType(OMTokenProto.Type.S3_STS_TOKEN)
- .setOriginalAccessKeyId(originalAccessKeyId)
- .build();
- final SignatureInfo signatureInfo = new SignatureInfo.Builder(SignatureInfo.Version.V4)
- .setAwsAccessId("ASIAEXAMPLE123")
- .setSignature("signature")
- .setStringToSign("string-to-sign")
- .setSessionToken(encodeSessionToken(proto))
- .build();
- final AuditEndpoint endpointBase = newAuditEndpoint(signatureInfo);
-
- assertThat(endpointBase.auditMessageForTest().getParams())
- .containsEntry(ORIGINAL_ACCESS_KEY_ID_PARAM, originalAccessKeyId + " (unverified)");
- }
-
- @Test
- public void testAuditMessageOmitsStsOriginalAccessKeyIdForNonStsRequest() {
- final SignatureInfo signatureInfo = new SignatureInfo.Builder(SignatureInfo.Version.V4)
- .setAwsAccessId("AKIAEXAMPLE123")
- .setSignature("signature")
- .setStringToSign("string-to-sign")
- .build();
- final AuditEndpoint endpointBase = newAuditEndpoint(signatureInfo);
-
- assertThat(endpointBase.auditMessageForTest().getParams())
- .doesNotContainKey(ORIGINAL_ACCESS_KEY_ID_PARAM);
- }
-
@Test
public void testListS3BucketsHandlesRuntimeExceptionWrappingOMException() throws Exception {
final EndpointBase endpointBase = new EndpointBase() {
@@ -248,22 +209,4 @@ private static Stream reservedInternalMetadataKeyPrefixCases() {
RESERVED_USER_METADATA_KEY_PREFIX.toUpperCase(Locale.ROOT) + "cache-control");
}
- private static String encodeSessionToken(OMTokenProto proto) throws Exception {
- final Token> token = new Token<>(
- proto.toByteArray(), new byte[0], new Text("OzoneToken"), new Text("sts"));
- return token.encodeToUrlString();
- }
-
- private static AuditEndpoint newAuditEndpoint(SignatureInfo signatureInfo) {
- return new EndpointBuilder<>(AuditEndpoint::new)
- .setSignatureInfo(signatureInfo)
- .build();
- }
-
- private static final class AuditEndpoint extends EndpointBase {
- private AuditMessage.Builder auditMessageForTest() {
- return auditMessageFor(S3GAction.GET_KEY);
- }
- }
-
}
diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestAuditUtils.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestAuditUtils.java
deleted file mode 100644
index b5952290dc9..00000000000
--- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestAuditUtils.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.hadoop.ozone.s3.util;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
-
-import org.apache.hadoop.io.Text;
-import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto;
-import org.apache.hadoop.security.token.Token;
-import org.junit.jupiter.api.Test;
-
-/** Unit tests for {@link AuditUtils}. */
-public class TestAuditUtils {
-
- @Test
- public void extractsOriginalAccessKeyIdFromSessionToken() throws Exception {
- final OMTokenProto proto = OMTokenProto.newBuilder()
- .setType(OMTokenProto.Type.S3_STS_TOKEN)
- .setOriginalAccessKeyId("AKIAORIGINAL123")
- .build();
-
- assertEquals("AKIAORIGINAL123", AuditUtils.getStsOriginalAccessKeyId(encodeSessionToken(proto)));
- }
-
- @Test
- public void returnsNullWhenOriginalAccessKeyIdAbsent() throws Exception {
- final OMTokenProto proto = OMTokenProto.newBuilder()
- .setType(OMTokenProto.Type.S3_STS_TOKEN)
- .build();
-
- assertNull(AuditUtils.getStsOriginalAccessKeyId(encodeSessionToken(proto)));
- }
-
- @Test
- public void returnsNullForNullEmptyOrMalformedToken() {
- assertNull(AuditUtils.getStsOriginalAccessKeyId(null));
- assertNull(AuditUtils.getStsOriginalAccessKeyId(""));
- assertNull(AuditUtils.getStsOriginalAccessKeyId("not-a-valid-token"));
- }
-
- private static String encodeSessionToken(OMTokenProto proto) throws Exception {
- final Token> token = new Token<>(
- proto.toByteArray(), new byte[0], new Text("OzoneToken"), new Text("sts"));
- return token.encodeToUrlString();
- }
-}
From e6b6614f3a6357ece5a25c9d53031945d13ee247 Mon Sep 17 00:00:00 2001
From: fmorg-git
Date: Tue, 25 Aug 2026 00:29:37 -0700
Subject: [PATCH 16/20] pr review update for copilot - ensure
originalAccessKeyId is not lost on replicated request
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---
.../ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java | 3 +++
1 file changed, 3 insertions(+)
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java
index 9953598e814..02e6cac1b3d 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3RevokeSTSTokenRequest.java
@@ -146,6 +146,9 @@ private static RevokeSTSTokenRequest validateReplicatedRevokeRequestFields(OMReq
throw new OMException("revokeSTSTokenRequest is required for STS token revocation", INTERNAL_ERROR);
}
final RevokeSTSTokenRequest revokeReq = omRequest.getRevokeSTSTokenRequest();
+ if (StringUtils.isEmpty(revokeReq.getOriginalAccessKeyId())) {
+ throw new OMException("originalAccessKeyId is required for STS token revocation", INTERNAL_ERROR);
+ }
if (!revokeReq.hasRevocationTimeMillis()) {
throw new OMException("revocationTimeMillis is required for STS token revocation", INTERNAL_ERROR);
}
From 77732e48fe4ab09ed14ce6ab2e125f2ee6f21235 Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Tue, 25 Aug 2026 22:27:23 -0700
Subject: [PATCH 17/20] design doc update
---
hadoop-hdds/docs/content/design/ozone-sts.md | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/hadoop-hdds/docs/content/design/ozone-sts.md b/hadoop-hdds/docs/content/design/ozone-sts.md
index 3acbafdcf9a..931531172a5 100644
--- a/hadoop-hdds/docs/content/design/ozone-sts.md
+++ b/hadoop-hdds/docs/content/design/ozone-sts.md
@@ -41,8 +41,9 @@ solutions that want to aggregate data across multiple cloud providers.
# 3. How Ozone STS Works
-The initial implementation of Ozone STS supports only the [AssumeRole](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html)
-API from the AWS specification. A new STS endpoint on port `9880` (port `9881` for https) will be created to service STS requests in the S3 Gateway at the root path (`/`).
+The initial implementation of Ozone STS supports the [AssumeRole](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html)
+and [GetCallerIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html)
+APIs from the AWS specification. A new STS endpoint on port `9880` (port `9881` for https) will be created to service STS requests in the S3 Gateway at the root path (`/`).
We use a separate port for STS to align with AWS so we don't have conflicts at a later time. This means we have:
- Admin port for Ozone specific S3 admin operations
- STS port for STS APIs, analogous to AWS' separate STS endpoint
@@ -66,6 +67,11 @@ return value of the AssumeRole call will be temporary credentials consisting of
an IAM policy is specified, the temporary credential will have the permissions comprising the intersection of the role permissions
and the IAM policy permissions. **Note:** If the IAM policy is specified and does not grant any permissions, then
the generated temporary credentials won't have any permissions and will essentially be useless.
+- [GetCallerIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html) returns the account,
+ARN, and user ID for the caller credentials used to sign the request. Ozone uses a static account ID of `123456789012`.
+For permanent S3 credentials, `UserId` is the resolved Kerberos principal and `Arn` is `arn:aws:iam::123456789012:user/`
+where `` is the short username of the Kerberos principal. For STS temporary credentials, `UserId` is
+the `AssumedRoleId` and `Arn` is the assumed-role user ARN from the session token.
## 3.2 Limitations in AssumeRole API Support
From 58f2fa12d4ff217aeecfa63dcc9eaaadb2c05dd7 Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Tue, 25 Aug 2026 22:29:28 -0700
Subject: [PATCH 18/20] proto update and related classes
---
.../hadoop/ozone/client/ObjectStore.java | 10 ++
.../ozone/client/protocol/ClientProtocol.java | 8 ++
.../hadoop/ozone/client/rpc/RpcClient.java | 6 +
.../java/org/apache/hadoop/ozone/OmUtils.java | 2 +
.../ozone/om/helpers/CallerIdentityInfo.java | 88 ++++++++++++++
.../om/protocol/OzoneManagerProtocol.java | 10 ++
...ManagerProtocolClientSideTranslatorPB.java | 10 ++
.../om/helpers/TestCallerIdentityInfo.java | 107 ++++++++++++++++++
.../src/main/proto/OmClientProtocol.proto | 14 +++
.../OzoneManagerRequestHandler.java | 24 ++++
.../ozone/client/ClientProtocolStub.java | 6 +
11 files changed, 285 insertions(+)
create mode 100644 hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/CallerIdentityInfo.java
create mode 100644 hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestCallerIdentityInfo.java
diff --git a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/ObjectStore.java b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/ObjectStore.java
index ce0f780b72d..32af4cacd4f 100644
--- a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/ObjectStore.java
+++ b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/ObjectStore.java
@@ -36,6 +36,7 @@
import org.apache.hadoop.ozone.client.protocol.ClientProtocol;
import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.helpers.AssumeRoleResponseInfo;
+import org.apache.hadoop.ozone.om.helpers.CallerIdentityInfo;
import org.apache.hadoop.ozone.om.helpers.BucketLayout;
import org.apache.hadoop.ozone.om.helpers.DeleteTenantState;
import org.apache.hadoop.ozone.om.helpers.OmVolumeArgs;
@@ -812,6 +813,15 @@ public AssumeRoleResponseInfo assumeRole(String roleArn, String roleSessionName,
return proxy.assumeRole(roleArn, roleSessionName, durationSeconds, awsIamSessionPolicy, requestId);
}
+ /**
+ * Returns the caller identity for the current S3-authenticated request.
+ * @return CallerIdentityInfo containing account, arn, and userId
+ * @throws IOException if an error occurs during the GetCallerIdentity operation
+ */
+ public CallerIdentityInfo getCallerIdentity() throws IOException {
+ return proxy.getCallerIdentity();
+ }
+
/**
* Revokes STS tokens for the given original access key ID.
* @param originalAccessKeyId The original long-lived access key ID whose STS tokens to revoke
diff --git a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
index b5f7baa0ef2..73f099879bc 100644
--- a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
+++ b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
@@ -48,6 +48,7 @@
import org.apache.hadoop.ozone.om.OMConfigKeys;
import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.helpers.AssumeRoleResponseInfo;
+import org.apache.hadoop.ozone.om.helpers.CallerIdentityInfo;
import org.apache.hadoop.ozone.om.helpers.DeleteTenantState;
import org.apache.hadoop.ozone.om.helpers.ErrorInfo;
import org.apache.hadoop.ozone.om.helpers.LeaseKeyInfo;
@@ -1647,6 +1648,13 @@ void deleteObjectTagging(String volumeName, String bucketName, String keyName)
AssumeRoleResponseInfo assumeRole(String roleArn, String roleSessionName, int durationSeconds,
String awsIamSessionPolicy, String requestId) throws IOException;
+ /**
+ * Returns the caller identity for the current S3-authenticated request.
+ * @return CallerIdentityInfo containing account, arn, and userId
+ * @throws IOException if an error occurs during the GetCallerIdentity operation
+ */
+ CallerIdentityInfo getCallerIdentity() throws IOException;
+
/**
* Revokes STS tokens for the given original access key ID.
* @param originalAccessKeyId The original long-lived access key ID whose STS tokens to revoke
diff --git a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
index 9ca47013462..22fad194442 100644
--- a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
+++ b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
@@ -130,6 +130,7 @@
import org.apache.hadoop.ozone.om.OmConfig;
import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.helpers.AssumeRoleResponseInfo;
+import org.apache.hadoop.ozone.om.helpers.CallerIdentityInfo;
import org.apache.hadoop.ozone.om.helpers.BasicOmKeyInfo;
import org.apache.hadoop.ozone.om.helpers.BucketEncryptionKeyInfo;
import org.apache.hadoop.ozone.om.helpers.BucketLayout;
@@ -3021,6 +3022,11 @@ public AssumeRoleResponseInfo assumeRole(String roleArn, String roleSessionName,
return ozoneManagerClient.assumeRole(roleArn, roleSessionName, durationSeconds, awsIamSessionPolicy, requestId);
}
+ @Override
+ public CallerIdentityInfo getCallerIdentity() throws IOException {
+ return ozoneManagerClient.getCallerIdentity();
+ }
+
@Override
public void revokeSTSToken(String originalAccessKeyId) throws IOException {
ozoneManagerClient.revokeSTSToken(originalAccessKeyId);
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java
index 5da80215fad..31530900cf2 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java
@@ -238,6 +238,7 @@ public static boolean isReadOnly(OMRequest omRequest) {
case FinalizeUpgradeProgress:
case PrepareStatus:
case GetS3VolumeContext:
+ case GetCallerIdentity:
case ListTenant:
case TenantGetUserInfo:
case TenantListUser:
@@ -383,6 +384,7 @@ public static boolean shouldSendToFollower(OMRequest omRequest) {
case FinalizeUpgradeProgress:
case PrepareStatus:
case GetS3VolumeContext:
+ case GetCallerIdentity:
case ListTenant:
case TenantGetUserInfo:
case TenantListUser:
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/CallerIdentityInfo.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/CallerIdentityInfo.java
new file mode 100644
index 00000000000..5a014f19fcf
--- /dev/null
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/CallerIdentityInfo.java
@@ -0,0 +1,88 @@
+/*
+ * 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.om.helpers;
+
+import java.util.Objects;
+import net.jcip.annotations.Immutable;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetCallerIdentityResponse;
+
+/**
+ * Utility class to handle GetCallerIdentityResponse protobuf message.
+ */
+@Immutable
+public class CallerIdentityInfo {
+
+ private final String account;
+ private final String arn;
+ private final String userId;
+
+ public CallerIdentityInfo(String account, String arn, String userId) {
+ this.account = account;
+ this.arn = arn;
+ this.userId = userId;
+ }
+
+ public String getAccount() {
+ return account;
+ }
+
+ public String getArn() {
+ return arn;
+ }
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public static CallerIdentityInfo fromProtobuf(GetCallerIdentityResponse response) {
+ return new CallerIdentityInfo(response.getAccount(), response.getArn(), response.getUserId());
+ }
+
+ public GetCallerIdentityResponse getProtobuf() {
+ return GetCallerIdentityResponse.newBuilder()
+ .setAccount(account)
+ .setArn(arn)
+ .setUserId(userId)
+ .build();
+ }
+
+ @Override
+ public String toString() {
+ return "CallerIdentityInfo{" + "account='" + account + "', arn='" + arn + "', userId='" + userId + "'}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final CallerIdentityInfo that = (CallerIdentityInfo) o;
+ return Objects.equals(account, that.account) && Objects.equals(arn, that.arn) &&
+ Objects.equals(userId, that.userId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(account, arn, userId);
+ }
+}
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
index 46254e3d6f6..7cea19d7a90 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
@@ -30,6 +30,7 @@
import org.apache.hadoop.ozone.om.OMConfigKeys;
import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.helpers.AssumeRoleResponseInfo;
+import org.apache.hadoop.ozone.om.helpers.CallerIdentityInfo;
import org.apache.hadoop.ozone.om.helpers.DBUpdates;
import org.apache.hadoop.ozone.om.helpers.DeleteTenantState;
import org.apache.hadoop.ozone.om.helpers.ErrorInfo;
@@ -1335,6 +1336,15 @@ default AssumeRoleResponseInfo assumeRole(String roleArn, String roleSessionName
throw new UnsupportedOperationException("OzoneManager does not require this to be implemented");
}
+ /**
+ * Returns the caller identity for the current S3-authenticated request.
+ * @return CallerIdentityInfo containing account, arn, and userId
+ * @throws IOException if an error occurs during the GetCallerIdentity operation
+ */
+ default CallerIdentityInfo getCallerIdentity() throws IOException {
+ throw new UnsupportedOperationException("OzoneManager does not require this to be implemented");
+ }
+
/**
* Revokes STS tokens for the given original access key ID.
* @param originalAccessKeyId The original long-lived access key ID whose STS tokens to revoke
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
index 7077fd1b02c..b5a9e3a2bbc 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
@@ -60,6 +60,7 @@
import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.helpers.AssumeRoleResponseInfo;
import org.apache.hadoop.ozone.om.helpers.BasicOmKeyInfo;
+import org.apache.hadoop.ozone.om.helpers.CallerIdentityInfo;
import org.apache.hadoop.ozone.om.helpers.DBUpdates;
import org.apache.hadoop.ozone.om.helpers.DeleteTenantState;
import org.apache.hadoop.ozone.om.helpers.ErrorInfo;
@@ -2980,6 +2981,15 @@ public AssumeRoleResponseInfo assumeRole(String roleArn, String roleSessionName,
handleError(submitRequest(omRequest)).getAssumeRoleResponse());
}
+ @Override
+ public CallerIdentityInfo getCallerIdentity() throws IOException {
+ final OMRequest omRequest = createOMRequest(Type.GetCallerIdentity)
+ .setGetCallerIdentityRequest(OzoneManagerProtocolProtos.GetCallerIdentityRequest.newBuilder().build())
+ .build();
+
+ return CallerIdentityInfo.fromProtobuf(handleError(submitRequest(omRequest)).getGetCallerIdentityResponse());
+ }
+
@Override
public void revokeSTSToken(String originalAccessKeyId) throws IOException {
final OzoneManagerProtocolProtos.RevokeSTSTokenRequest request =
diff --git a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestCallerIdentityInfo.java b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestCallerIdentityInfo.java
new file mode 100644
index 00000000000..f6bc0334acf
--- /dev/null
+++ b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestCallerIdentityInfo.java
@@ -0,0 +1,107 @@
+/*
+ * 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.om.helpers;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetCallerIdentityResponse;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test CallerIdentityInfo.
+ */
+public class TestCallerIdentityInfo {
+
+ private static final String ACCOUNT = "123456789012";
+ private static final String ARN = "arn:aws:iam::123456789012:user/om";
+ private static final String USER_ID = "om/polarisclient@root.comops.site";
+
+ @Test
+ public void testConstructor() {
+ final CallerIdentityInfo identity = new CallerIdentityInfo(ACCOUNT, ARN, USER_ID);
+
+ assertEquals(ACCOUNT, identity.getAccount());
+ assertEquals(ARN, identity.getArn());
+ assertEquals(USER_ID, identity.getUserId());
+ }
+
+ @Test
+ public void testProtobufConversion() {
+ final CallerIdentityInfo identity = new CallerIdentityInfo(ACCOUNT, ARN, USER_ID);
+
+ final GetCallerIdentityResponse proto = identity.getProtobuf();
+
+ assertNotNull(proto);
+ assertEquals(ACCOUNT, proto.getAccount());
+ assertEquals(ARN, proto.getArn());
+ assertEquals(USER_ID, proto.getUserId());
+ }
+
+ @Test
+ public void testFromProtobuf() {
+ final GetCallerIdentityResponse proto = GetCallerIdentityResponse.newBuilder()
+ .setAccount(ACCOUNT)
+ .setArn(ARN)
+ .setUserId(USER_ID)
+ .build();
+
+ final CallerIdentityInfo identity = CallerIdentityInfo.fromProtobuf(proto);
+
+ assertEquals(ACCOUNT, identity.getAccount());
+ assertEquals(ARN, identity.getArn());
+ assertEquals(USER_ID, identity.getUserId());
+ }
+
+ @Test
+ public void testProtobufRoundTrip() {
+ final CallerIdentityInfo original = new CallerIdentityInfo(ACCOUNT, ARN, USER_ID);
+
+ final CallerIdentityInfo recovered = CallerIdentityInfo.fromProtobuf(original.getProtobuf());
+
+ assertEquals(original, recovered);
+ }
+
+ @Test
+ public void testEqualsAndHashCodeWithIdenticalObjects() {
+ final CallerIdentityInfo identity1 = new CallerIdentityInfo(ACCOUNT, ARN, USER_ID);
+ final CallerIdentityInfo identity2 = new CallerIdentityInfo(ACCOUNT, ARN, USER_ID);
+
+ assertEquals(identity1, identity2);
+ assertEquals(identity1.hashCode(), identity2.hashCode());
+ }
+
+ @Test
+ public void testNotEqualsWithDifferentArn() {
+ final CallerIdentityInfo identity1 = new CallerIdentityInfo(ACCOUNT, ARN, USER_ID);
+ final CallerIdentityInfo identity2 = new CallerIdentityInfo(
+ ACCOUNT, "arn:aws:iam::123456789012:user/other", USER_ID);
+
+ assertNotEquals(identity1, identity2);
+ assertNotEquals(identity1.hashCode(), identity2.hashCode());
+ }
+
+ @Test
+ public void testToString() {
+ final CallerIdentityInfo identity = new CallerIdentityInfo(ACCOUNT, ARN, USER_ID);
+
+ assertEquals(
+ "CallerIdentityInfo{account='123456789012', arn='" + ARN + "', userId='" + USER_ID + "'}", identity.toString());
+ }
+}
diff --git a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
index 638cf99bd1c..062a92d20fa 100644
--- a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
+++ b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
@@ -169,6 +169,7 @@ enum Type {
AssumeRole = 153;
RevokeSTSToken = 154;
DeleteRevokedSTSTokens = 155;
+ GetCallerIdentity = 156;
}
enum SafeMode {
@@ -333,6 +334,7 @@ message OMRequest {
optional RevokeSTSTokenRequest revokeSTSTokenRequest = 155;
optional DeleteRevokedSTSTokensRequest deleteRevokedSTSTokensRequest = 156;
optional UpdateAssumeRoleRequest updateAssumeRoleRequest = 157;
+ optional GetCallerIdentityRequest getCallerIdentityRequest = 158;
}
message OMResponse {
@@ -481,6 +483,7 @@ message OMResponse {
optional AssumeRoleResponse assumeRoleResponse = 153;
optional RevokeSTSTokenResponse revokeSTSTokenResponse = 154;
optional DeleteRevokedSTSTokensResponse deleteRevokedSTSTokensResponse = 155;
+ optional GetCallerIdentityResponse getCallerIdentityResponse = 156;
}
enum Status {
@@ -1600,6 +1603,8 @@ message OMTokenProto {
optional string originalAccessKeyId = 18;
optional string secretAccessKey = 19;
optional string sessionPolicy = 20;
+ optional string assumedRoleId = 21;
+ optional string assumedRoleUserArn = 22;
}
message SecretKeyProto {
@@ -2553,6 +2558,15 @@ message DeleteRevokedSTSTokensRequest {
message DeleteRevokedSTSTokensResponse {
}
+message GetCallerIdentityRequest {
+}
+
+message GetCallerIdentityResponse {
+ optional string account = 1;
+ optional string arn = 2;
+ optional string userId = 3;
+}
+
enum ReadConsistencyProto {
// Unspecified consistency, the read consistency behavior is decided
// by the OM
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
index 21afb1e42f7..0ff43231856 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
@@ -61,6 +61,7 @@
import org.apache.hadoop.hdds.scm.protocolPB.OzonePBHelper;
import org.apache.hadoop.hdds.utils.FaultInjector;
import org.apache.hadoop.ozone.OzoneAcl;
+import org.apache.hadoop.ozone.om.OzoneAclUtils;
import org.apache.hadoop.ozone.om.OzoneManager;
import org.apache.hadoop.ozone.om.OzoneManagerPrepareState;
import org.apache.hadoop.ozone.om.exceptions.OMException;
@@ -86,6 +87,7 @@
import org.apache.hadoop.ozone.om.helpers.OpenKeySession;
import org.apache.hadoop.ozone.om.helpers.OzoneFileStatus;
import org.apache.hadoop.ozone.om.helpers.OzoneFileStatusLight;
+import org.apache.hadoop.ozone.om.helpers.S3STSUtils;
import org.apache.hadoop.ozone.om.helpers.ServiceInfo;
import org.apache.hadoop.ozone.om.helpers.ServiceInfoEx;
import org.apache.hadoop.ozone.om.helpers.SnapshotDiffJob;
@@ -113,6 +115,7 @@
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.FinalizeUpgradeProgressResponse;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetBucketTaggingRequest;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetBucketTaggingResponse;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetCallerIdentityResponse;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetFileStatusRequest;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetFileStatusResponse;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetKeyInfoRequest;
@@ -171,11 +174,13 @@
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.TenantListUserResponse;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
import org.apache.hadoop.ozone.request.validation.RequestProcessingPhase;
+import org.apache.hadoop.ozone.security.STSTokenIdentifier;
import org.apache.hadoop.ozone.security.acl.OzoneObjInfo;
import org.apache.hadoop.ozone.snapshot.ListSnapshotResponse;
import org.apache.hadoop.ozone.upgrade.UpgradeFinalization.StatusAndMessages;
import org.apache.hadoop.ozone.util.PayloadUtils;
import org.apache.hadoop.ozone.util.ProtobufUtils;
+import org.apache.hadoop.security.UserGroupInformation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -316,6 +321,9 @@ public OMResponse handleReadRequest(OMRequest request) {
getS3VolumeContext();
responseBuilder.setGetS3VolumeContextResponse(s3VolumeContextResponse);
break;
+ case GetCallerIdentity:
+ responseBuilder.setGetCallerIdentityResponse(getCallerIdentity());
+ break;
case TenantGetUserInfo:
impl.checkS3MultiTenancyEnabled();
TenantGetUserInfoResponse getUserInfoResponse = tenantGetUserInfo(
@@ -1448,6 +1456,22 @@ private GetS3VolumeContextResponse getS3VolumeContext()
return impl.getS3VolumeContext().getProtobuf();
}
+ private GetCallerIdentityResponse getCallerIdentity() throws OMException {
+ impl.checkS3STSEnabled();
+ if (OzoneManager.getS3Auth() == null) {
+ throw new OMException(
+ "GetCallerIdentity does not have S3 authentication", OMException.ResultCodes.INVALID_REQUEST);
+ }
+ final STSTokenIdentifier stsTokenIdentifier = OzoneManager.getStsTokenIdentifier();
+ if (stsTokenIdentifier != null) {
+ return S3STSUtils.resolveCallerIdentityForStsCredentials(
+ stsTokenIdentifier.getAssumedRoleId(), stsTokenIdentifier.getAssumedRoleUserArn()).getProtobuf();
+ }
+ final String resolvedPrincipal = OzoneAclUtils.accessIdToUserPrincipal(OzoneManager.getS3AuthEffectiveAccessId());
+ final String kerberosShortName = UserGroupInformation.createRemoteUser(resolvedPrincipal).getShortUserName();
+ return S3STSUtils.resolveCallerIdentityForPermanentCredentials(resolvedPrincipal, kerberosShortName).getProtobuf();
+ }
+
@DisallowedUntilLayoutVersion(FILESYSTEM_SNAPSHOT)
private SnapshotDiffResponse snapshotDiff(
SnapshotDiffRequest snapshotDiffRequest) throws IOException {
diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
index abd80cbc1fc..6fd318dc906 100644
--- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
+++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
@@ -39,6 +39,7 @@
import org.apache.hadoop.ozone.client.protocol.ClientProtocol;
import org.apache.hadoop.ozone.client.protocol.ListStatusLightOptions;
import org.apache.hadoop.ozone.om.helpers.AssumeRoleResponseInfo;
+import org.apache.hadoop.ozone.om.helpers.CallerIdentityInfo;
import org.apache.hadoop.ozone.om.helpers.DeleteTenantState;
import org.apache.hadoop.ozone.om.helpers.ErrorInfo;
import org.apache.hadoop.ozone.om.helpers.LeaseKeyInfo;
@@ -898,6 +899,11 @@ public AssumeRoleResponseInfo assumeRole(String roleArn, String roleSessionName,
return null;
}
+ @Override
+ public CallerIdentityInfo getCallerIdentity() throws IOException {
+ return null;
+ }
+
@Override
public void revokeSTSToken(String originalAccessKeyId) throws IOException {
}
From 2ac4e9767bf05e36fdc9f5103ef5b3753f704821 Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Tue, 25 Aug 2026 22:32:33 -0700
Subject: [PATCH 19/20] main logic updates
---
.../hadoop/ozone/om/helpers/S3STSUtils.java | 34 +++-
.../helpers/TestS3STSUtilsCallerIdentity.java | 59 +++++++
.../s3/security/S3AssumeRoleRequest.java | 156 +++++++++++++++--
.../ozone/security/STSTokenIdentifier.java | 53 +++++-
.../ozone/security/STSTokenSecretManager.java | 159 ++++++++++++++++--
.../ozone/security/TestSTSSecurityUtil.java | 76 +++++----
.../security/TestSTSTokenIdentifier.java | 14 ++
.../security/TestSTSTokenSecretManager.java | 25 ++-
.../apache/hadoop/ozone/audit/S3GAction.java | 1 +
.../ozone/s3/util/S3GActionIamMapper.java | 1 +
.../ozone/s3sts/S3AssumeRoleResponseXml.java | 23 +--
.../s3sts/S3GetCallerIdentityResponseXml.java | 92 ++++++++++
.../hadoop/ozone/s3sts/S3STSEndpoint.java | 126 ++++++++++----
.../ozone/s3sts/S3STSResponseMetadata.java | 42 +++++
.../ozone/s3/util/TestS3GActionIamMapper.java | 1 +
.../hadoop/ozone/s3sts/TestS3STSEndpoint.java | 79 +++++++++
16 files changed, 823 insertions(+), 118 deletions(-)
create mode 100644 hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestS3STSUtilsCallerIdentity.java
create mode 100644 hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3GetCallerIdentityResponseXml.java
create mode 100644 hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSResponseMetadata.java
diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/S3STSUtils.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/S3STSUtils.java
index 64221089655..17bf084f9b3 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/S3STSUtils.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/S3STSUtils.java
@@ -44,11 +44,43 @@ public final class S3STSUtils {
public static final String STS_ACCESS_KEY_ID_ALLOWED_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static final int STS_ACCESS_KEY_ID_ALLOWED_CHARS_LENGTH = STS_ACCESS_KEY_ID_ALLOWED_CHARS.length();
public static final int STS_ACCESS_KEY_ID_RANDOM_LENGTH = 20;
- public static final int STS_ACCESS_KEY_ID_LENGTH = STS_TOKEN_PREFIX.length() + STS_ACCESS_KEY_ID_RANDOM_LENGTH;
+
+ public static final String OZONE_STATIC_ACCOUNT_ID = "123456789012";
private S3STSUtils() {
}
+ /**
+ * Builds an IAM user ARN for the given Kerberos short name.
+ */
+ public static String toIamUserArn(String kerberosShortName) {
+ return "arn:aws:iam::" + OZONE_STATIC_ACCOUNT_ID + ":user/" + kerberosShortName;
+ }
+
+ /**
+ * Resolves the caller identity for GetCallerIdentity with permanent S3 credentials.
+ *
+ * @param resolvedPrincipal full Kerberos principal of the caller
+ * @param kerberosShortName short username
+ * @return caller identity with account, arn, and userId
+ */
+ public static CallerIdentityInfo resolveCallerIdentityForPermanentCredentials(String resolvedPrincipal,
+ String kerberosShortName) {
+ return new CallerIdentityInfo(OZONE_STATIC_ACCOUNT_ID, toIamUserArn(kerberosShortName), resolvedPrincipal);
+ }
+
+ /**
+ * Resolves the caller identity for GetCallerIdentity with temporary STS credentials.
+ *
+ * @param assumedRoleId assumed role ID from the STS token
+ * @param assumedRoleUserArn assumed role user ARN from the STS token
+ * @return caller identity with account, arn, and userId
+ */
+ public static CallerIdentityInfo resolveCallerIdentityForStsCredentials(String assumedRoleId,
+ String assumedRoleUserArn) {
+ return new CallerIdentityInfo(OZONE_STATIC_ACCOUNT_ID, assumedRoleUserArn, assumedRoleId);
+ }
+
/**
* Adds standard AssumeRole audit params.
*/
diff --git a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestS3STSUtilsCallerIdentity.java b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestS3STSUtilsCallerIdentity.java
new file mode 100644
index 00000000000..f991bd7bffd
--- /dev/null
+++ b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestS3STSUtilsCallerIdentity.java
@@ -0,0 +1,59 @@
+/*
+ * 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.om.helpers;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test caller identity resolution helpers in S3STSUtils.
+ */
+public class TestS3STSUtilsCallerIdentity {
+
+ private static final String PRINCIPAL = "om/polarisclient@example.com";
+ private static final String KERBEROS_SHORT_NAME = "om";
+ private static final String ASSUMED_ROLE_ID = "AROATEST123456789:testsess";
+ private static final String ASSUMED_ROLE_USER_ARN =
+ "arn:aws:sts::123456789012:assumed-role/test-role/testsess";
+
+ @Test
+ public void testToIamUserArn() {
+ assertEquals("arn:aws:iam::123456789012:user/om", S3STSUtils.toIamUserArn(KERBEROS_SHORT_NAME));
+ }
+
+ @Test
+ public void testResolveCallerIdentityForPermanentCredentials() {
+ final CallerIdentityInfo identity = S3STSUtils.resolveCallerIdentityForPermanentCredentials(
+ PRINCIPAL, KERBEROS_SHORT_NAME);
+
+ assertEquals(S3STSUtils.OZONE_STATIC_ACCOUNT_ID, identity.getAccount());
+ assertEquals("arn:aws:iam::123456789012:user/om", identity.getArn());
+ assertEquals(PRINCIPAL, identity.getUserId());
+ }
+
+ @Test
+ public void testResolveCallerIdentityForStsCredentials() {
+ final CallerIdentityInfo identity = S3STSUtils.resolveCallerIdentityForStsCredentials(
+ ASSUMED_ROLE_ID, ASSUMED_ROLE_USER_ARN);
+
+ assertEquals(S3STSUtils.OZONE_STATIC_ACCOUNT_ID, identity.getAccount());
+ assertEquals(ASSUMED_ROLE_USER_ARN, identity.getArn());
+ assertEquals(ASSUMED_ROLE_ID, identity.getUserId());
+ }
+}
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3AssumeRoleRequest.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3AssumeRoleRequest.java
index b6d650cc439..788ba74b3c5 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3AssumeRoleRequest.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3AssumeRoleRequest.java
@@ -53,6 +53,7 @@
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.AssumeRoleResponse;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.UpdateAssumeRoleRequest;
+import org.apache.hadoop.ozone.security.STSTokenSecretManager;
import org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver;
import org.apache.hadoop.security.UserGroupInformation;
@@ -174,12 +175,21 @@ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, Execut
"S3AssumeRoleRequest does not have S3 authentication", OMException.ResultCodes.INVALID_REQUEST);
}
- // Generate session token using leader-generated credentials
- final String sessionToken = generateSessionToken(
- targetRoleName, omRequest, ozoneManager, assumeRoleRequest, secretAccessKey, tempAccessKeyId);
-
// Generate AssumedRoleId for response using leader-generated roleId
final String assumedRoleId = roleId + ":" + roleSessionName;
+ final String assumedRoleUserArn = S3STSUtils.toAssumedRoleUserArn(roleArn, roleSessionName);
+
+ // Generate session token using leader-generated credentials
+ final String sessionToken = generateSessionToken(GenerateSessionTokenParams.newBuilder()
+ .setTargetRoleName(targetRoleName)
+ .setOmRequest(omRequest)
+ .setOzoneManager(ozoneManager)
+ .setAssumeRoleRequest(assumeRoleRequest)
+ .setSecretAccessKey(secretAccessKey)
+ .setTempAccessKeyId(tempAccessKeyId)
+ .setAssumedRoleId(assumedRoleId)
+ .setAssumedRoleUserArn(assumedRoleUserArn)
+ .build());
// Calculate expiration of session token
final long expirationEpochSeconds = clock.instant().plusSeconds(durationSeconds).getEpochSecond();
@@ -219,9 +229,10 @@ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, Execut
/**
* Generates session token using components from the AssumeRoleRequest.
*/
- private String generateSessionToken(String targetRoleName, OMRequest omRequest,
- OzoneManager ozoneManager, AssumeRoleRequest assumeRoleRequest, String secretAccessKey,
- String tempAccessKeyId) throws IOException {
+ private String generateSessionToken(GenerateSessionTokenParams params) throws IOException {
+ final OzoneManager ozoneManager = params.getOzoneManager();
+ final OMRequest omRequest = params.getOmRequest();
+ final AssumeRoleRequest assumeRoleRequest = params.getAssumeRoleRequest();
InetAddress remoteIp = ProtobufRpcEngine.Server.getRemoteIp();
if (remoteIp == null) {
@@ -242,11 +253,136 @@ private String generateSessionToken(String targetRoleName, OMRequest omRequest,
final String roleArn = assumeRoleRequest.getRoleArn();
final String sessionPolicy = getSessionPolicy(
ozoneManager, originalAccessKeyId, assumeRoleRequest.getAwsIamSessionPolicy(), hostName, remoteIp, ugi,
- targetRoleName);
+ params.getTargetRoleName());
return ozoneManager.getSTSTokenSecretManager().createSTSTokenString(
- tempAccessKeyId, originalAccessKeyId, roleArn, assumeRoleRequest.getDurationSeconds(), secretAccessKey,
- sessionPolicy, clock);
+ STSTokenSecretManager.CreateSTSTokenParams.newBuilder()
+ .setTempAccessKeyId(params.getTempAccessKeyId())
+ .setOriginalAccessKeyId(originalAccessKeyId)
+ .setRoleArn(roleArn)
+ .setDurationSeconds(assumeRoleRequest.getDurationSeconds())
+ .setSecretAccessKey(params.getSecretAccessKey())
+ .setSessionPolicy(sessionPolicy)
+ .setAssumedRoleId(params.getAssumedRoleId())
+ .setAssumedRoleUserArn(params.getAssumedRoleUserArn())
+ .setClock(clock)
+ .build());
+ }
+
+ /**
+ * Parameters for {@link #generateSessionToken(GenerateSessionTokenParams)}.
+ */
+ private static final class GenerateSessionTokenParams {
+ private final String targetRoleName;
+ private final OMRequest omRequest;
+ private final OzoneManager ozoneManager;
+ private final AssumeRoleRequest assumeRoleRequest;
+ private final String secretAccessKey;
+ private final String tempAccessKeyId;
+ private final String assumedRoleId;
+ private final String assumedRoleUserArn;
+
+ private GenerateSessionTokenParams(Builder builder) {
+ this.targetRoleName = builder.targetRoleName;
+ this.omRequest = builder.omRequest;
+ this.ozoneManager = builder.ozoneManager;
+ this.assumeRoleRequest = builder.assumeRoleRequest;
+ this.secretAccessKey = builder.secretAccessKey;
+ this.tempAccessKeyId = builder.tempAccessKeyId;
+ this.assumedRoleId = builder.assumedRoleId;
+ this.assumedRoleUserArn = builder.assumedRoleUserArn;
+ }
+
+ static Builder newBuilder() {
+ return new Builder();
+ }
+
+ String getTargetRoleName() {
+ return targetRoleName;
+ }
+
+ OMRequest getOmRequest() {
+ return omRequest;
+ }
+
+ OzoneManager getOzoneManager() {
+ return ozoneManager;
+ }
+
+ AssumeRoleRequest getAssumeRoleRequest() {
+ return assumeRoleRequest;
+ }
+
+ String getSecretAccessKey() {
+ return secretAccessKey;
+ }
+
+ String getTempAccessKeyId() {
+ return tempAccessKeyId;
+ }
+
+ String getAssumedRoleId() {
+ return assumedRoleId;
+ }
+
+ String getAssumedRoleUserArn() {
+ return assumedRoleUserArn;
+ }
+
+ private static final class Builder {
+ private String targetRoleName;
+ private OMRequest omRequest;
+ private OzoneManager ozoneManager;
+ private AssumeRoleRequest assumeRoleRequest;
+ private String secretAccessKey;
+ private String tempAccessKeyId;
+ private String assumedRoleId;
+ private String assumedRoleUserArn;
+
+ Builder setTargetRoleName(String value) {
+ this.targetRoleName = value;
+ return this;
+ }
+
+ Builder setOmRequest(OMRequest value) {
+ this.omRequest = value;
+ return this;
+ }
+
+ Builder setOzoneManager(OzoneManager value) {
+ this.ozoneManager = value;
+ return this;
+ }
+
+ Builder setAssumeRoleRequest(AssumeRoleRequest value) {
+ this.assumeRoleRequest = value;
+ return this;
+ }
+
+ Builder setSecretAccessKey(String value) {
+ this.secretAccessKey = value;
+ return this;
+ }
+
+ Builder setTempAccessKeyId(String value) {
+ this.tempAccessKeyId = value;
+ return this;
+ }
+
+ Builder setAssumedRoleId(String value) {
+ this.assumedRoleId = value;
+ return this;
+ }
+
+ Builder setAssumedRoleUserArn(String value) {
+ this.assumedRoleUserArn = value;
+ return this;
+ }
+
+ GenerateSessionTokenParams build() {
+ return new GenerateSessionTokenParams(this);
+ }
+ }
}
/**
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenIdentifier.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenIdentifier.java
index cc229083d9e..9535be2f030 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenIdentifier.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenIdentifier.java
@@ -46,6 +46,8 @@ public class STSTokenIdentifier extends ShortLivedTokenIdentifier {
private String originalAccessKeyId;
private String secretAccessKey;
private String sessionPolicy;
+ private String assumedRoleId;
+ private String assumedRoleUserArn;
private Instant creationTime;
// Encryption key derived from ManagedSecretKey for this token
@@ -73,6 +75,8 @@ public STSTokenIdentifier(Params params) {
this.creationTime = params.getCreationTime();
this.secretAccessKey = params.getSecretAccessKey();
this.sessionPolicy = params.getSessionPolicy();
+ this.assumedRoleId = params.getAssumedRoleId();
+ this.assumedRoleUserArn = params.getAssumedRoleUserArn();
this.encryptionKey = params.getEncryptionKey(); // already cloned via Params
}
@@ -87,6 +91,8 @@ public static final class Params {
private final Instant expiry;
private final String secretAccessKey;
private final String sessionPolicy;
+ private final String assumedRoleId;
+ private final String assumedRoleUserArn;
private final byte[] encryptionKey;
private Params(Builder builder) {
@@ -97,6 +103,8 @@ private Params(Builder builder) {
this.expiry = builder.expiry;
this.secretAccessKey = builder.secretAccessKey;
this.sessionPolicy = builder.sessionPolicy;
+ this.assumedRoleId = builder.assumedRoleId;
+ this.assumedRoleUserArn = builder.assumedRoleUserArn;
this.encryptionKey = builder.encryptionKey;
}
@@ -132,6 +140,14 @@ public String getSessionPolicy() {
return sessionPolicy;
}
+ public String getAssumedRoleId() {
+ return assumedRoleId;
+ }
+
+ public String getAssumedRoleUserArn() {
+ return assumedRoleUserArn;
+ }
+
public byte[] getEncryptionKey() {
return encryptionKey != null ? encryptionKey.clone() : null;
}
@@ -147,6 +163,8 @@ public static final class Builder {
private Instant expiry;
private String secretAccessKey;
private String sessionPolicy;
+ private String assumedRoleId;
+ private String assumedRoleUserArn;
private byte[] encryptionKey;
public Builder setTempAccessKeyId(String value) {
@@ -184,6 +202,16 @@ public Builder setSessionPolicy(String value) {
return this;
}
+ public Builder setAssumedRoleId(String value) {
+ this.assumedRoleId = value;
+ return this;
+ }
+
+ public Builder setAssumedRoleUserArn(String value) {
+ this.assumedRoleUserArn = value;
+ return this;
+ }
+
public Builder setEncryptionKey(byte[] value) {
this.encryptionKey = value != null ? value.clone() : null;
return this;
@@ -243,7 +271,9 @@ public OMTokenProto toProtoBuf() {
.setOriginalAccessKeyId(originalAccessKeyId != null ? originalAccessKeyId : "")
.setRoleArn(roleArn != null ? roleArn : "")
.setSecretAccessKey(secretAccessKey != null ? encryptSensitiveField(secretAccessKey) : "")
- .setSessionPolicy(sessionPolicy != null ? sessionPolicy : "");
+ .setSessionPolicy(sessionPolicy != null ? sessionPolicy : "")
+ .setAssumedRoleId(assumedRoleId != null ? assumedRoleId : "")
+ .setAssumedRoleUserArn(assumedRoleUserArn != null ? assumedRoleUserArn : "");
return builder.build();
}
@@ -286,6 +316,12 @@ public void fromProtoBuf(OMTokenProto token) throws IOException {
if (token.hasSessionPolicy()) {
this.sessionPolicy = token.getSessionPolicy();
}
+ if (token.hasAssumedRoleId()) {
+ this.assumedRoleId = token.getAssumedRoleId();
+ }
+ if (token.hasAssumedRoleUserArn()) {
+ this.assumedRoleUserArn = token.getAssumedRoleUserArn();
+ }
}
/**
@@ -361,6 +397,14 @@ public String getSessionPolicy() {
return sessionPolicy;
}
+ public String getAssumedRoleId() {
+ return assumedRoleId;
+ }
+
+ public String getAssumedRoleUserArn() {
+ return assumedRoleUserArn;
+ }
+
public Instant getCreationTime() {
return creationTime;
}
@@ -386,13 +430,15 @@ public boolean equals(Object o) {
final STSTokenIdentifier that = (STSTokenIdentifier) o;
return Objects.equals(roleArn, that.roleArn) && Objects.equals(secretAccessKey, that.secretAccessKey) &&
Objects.equals(originalAccessKeyId, that.originalAccessKeyId) &&
- Objects.equals(sessionPolicy, that.sessionPolicy) && Objects.equals(creationTime, that.creationTime);
+ Objects.equals(sessionPolicy, that.sessionPolicy) && Objects.equals(assumedRoleId, that.assumedRoleId) &&
+ Objects.equals(assumedRoleUserArn, that.assumedRoleUserArn) && Objects.equals(creationTime, that.creationTime);
}
@Override
public int hashCode() {
return Objects.hash(
- super.hashCode(), roleArn, secretAccessKey, originalAccessKeyId, sessionPolicy, creationTime);
+ super.hashCode(), roleArn, secretAccessKey, originalAccessKeyId, sessionPolicy, assumedRoleId,
+ assumedRoleUserArn, creationTime);
}
@Override
@@ -400,6 +446,7 @@ public String toString() {
// Intentionally left off secretAccessKey
return "STSTokenIdentifier{" + "tempAccessKeyId='" + getOwnerId() + "'" +
", originalAccessKeyId='" + originalAccessKeyId + "', roleArn='" + roleArn + "'" +
+ ", assumedRoleId='" + assumedRoleId + "', assumedRoleUserArn='" + assumedRoleUserArn + "'" +
", creationTime='" + creationTime + "', expiry='" + getExpiry() + "', secretKeyId='" + getSecretKeyId() +
"', sessionPolicy='" + sessionPolicy + "'}";
}
diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenSecretManager.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenSecretManager.java
index 66273299fea..be952f108f3 100644
--- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenSecretManager.java
+++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenSecretManager.java
@@ -84,20 +84,12 @@ private Token generateToken(STSTokenIdentifier tokenIdentifi
/**
* Create an STS token and return it as an encoded string.
*
- * @param tempAccessKeyId the temporary access key ID
- * @param originalAccessKeyId the original long-lived access key ID
- * @param roleArn the ARN of the assumed role
- * @param durationSeconds how long the token should be valid for
- * @param secretAccessKey the secret access key associated with the temporary access key ID
- * @param sessionPolicy an optional opaque identifier that further limits the scope of
- * the permissions granted by the role
- * @param clock the system clock
+ * @param params the STS token creation parameters
* @return base64 encoded token string
*/
- public String createSTSTokenString(String tempAccessKeyId, String originalAccessKeyId, String roleArn,
- int durationSeconds, String secretAccessKey, String sessionPolicy, Clock clock) throws IOException {
- final Instant creationTime = clock.instant();
- final Instant expiration = creationTime.plusSeconds(durationSeconds);
+ public String createSTSTokenString(CreateSTSTokenParams params) throws IOException {
+ final Instant creationTime = params.getClock().instant();
+ final Instant expiration = creationTime.plusSeconds(params.getDurationSeconds());
// Get the current secret key once for encryption, secretKeyId, and signing.
final ManagedSecretKey secretKey = secretKeyClient.getCurrentSecretKey();
@@ -107,13 +99,15 @@ public String createSTSTokenString(String tempAccessKeyId, String originalAccess
// the write() method in STSTokenIdentifier which calls toProtoBuf(), and the encryptionKey is not
// serialized there.
final STSTokenIdentifier identifier = new STSTokenIdentifier(STSTokenIdentifier.Params.newBuilder()
- .setTempAccessKeyId(tempAccessKeyId)
- .setOriginalAccessKeyId(originalAccessKeyId)
- .setRoleArn(roleArn)
+ .setTempAccessKeyId(params.getTempAccessKeyId())
+ .setOriginalAccessKeyId(params.getOriginalAccessKeyId())
+ .setRoleArn(params.getRoleArn())
.setCreationTime(creationTime)
.setExpiry(expiration)
- .setSecretAccessKey(secretAccessKey)
- .setSessionPolicy(sessionPolicy)
+ .setSecretAccessKey(params.getSecretAccessKey())
+ .setSessionPolicy(params.getSessionPolicy())
+ .setAssumedRoleId(params.getAssumedRoleId())
+ .setAssumedRoleUserArn(params.getAssumedRoleUserArn())
.setEncryptionKey(encryptionKey)
.build());
identifier.setSecretKeyId(secretKey.getId());
@@ -121,6 +115,137 @@ public String createSTSTokenString(String tempAccessKeyId, String originalAccess
final Token token = generateToken(identifier, secretKey);
return token.encodeToUrlString();
}
+
+ /**
+ * Parameters for {@link #createSTSTokenString(CreateSTSTokenParams)}.
+ */
+ public static final class CreateSTSTokenParams {
+ private final String tempAccessKeyId;
+ private final String originalAccessKeyId;
+ private final String roleArn;
+ private final int durationSeconds;
+ private final String secretAccessKey;
+ private final String sessionPolicy;
+ private final String assumedRoleId;
+ private final String assumedRoleUserArn;
+ private final Clock clock;
+
+ private CreateSTSTokenParams(Builder builder) {
+ this.tempAccessKeyId = builder.tempAccessKeyId;
+ this.originalAccessKeyId = builder.originalAccessKeyId;
+ this.roleArn = builder.roleArn;
+ this.durationSeconds = builder.durationSeconds;
+ this.secretAccessKey = builder.secretAccessKey;
+ this.sessionPolicy = builder.sessionPolicy;
+ this.assumedRoleId = builder.assumedRoleId;
+ this.assumedRoleUserArn = builder.assumedRoleUserArn;
+ this.clock = builder.clock;
+ }
+
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ public String getTempAccessKeyId() {
+ return tempAccessKeyId;
+ }
+
+ public String getOriginalAccessKeyId() {
+ return originalAccessKeyId;
+ }
+
+ public String getRoleArn() {
+ return roleArn;
+ }
+
+ public int getDurationSeconds() {
+ return durationSeconds;
+ }
+
+ public String getSecretAccessKey() {
+ return secretAccessKey;
+ }
+
+ public String getSessionPolicy() {
+ return sessionPolicy;
+ }
+
+ public String getAssumedRoleId() {
+ return assumedRoleId;
+ }
+
+ public String getAssumedRoleUserArn() {
+ return assumedRoleUserArn;
+ }
+
+ public Clock getClock() {
+ return clock;
+ }
+
+ /**
+ * Builder for {@link CreateSTSTokenParams}.
+ */
+ public static final class Builder {
+ private String tempAccessKeyId;
+ private String originalAccessKeyId;
+ private String roleArn;
+ private int durationSeconds;
+ private String secretAccessKey;
+ private String sessionPolicy;
+ private String assumedRoleId;
+ private String assumedRoleUserArn;
+ private Clock clock;
+
+ public Builder setTempAccessKeyId(String value) {
+ this.tempAccessKeyId = value;
+ return this;
+ }
+
+ public Builder setOriginalAccessKeyId(String value) {
+ this.originalAccessKeyId = value;
+ return this;
+ }
+
+ public Builder setRoleArn(String value) {
+ this.roleArn = value;
+ return this;
+ }
+
+ public Builder setDurationSeconds(int value) {
+ this.durationSeconds = value;
+ return this;
+ }
+
+ public Builder setSecretAccessKey(String value) {
+ this.secretAccessKey = value;
+ return this;
+ }
+
+ public Builder setSessionPolicy(String value) {
+ this.sessionPolicy = value;
+ return this;
+ }
+
+ public Builder setAssumedRoleId(String value) {
+ this.assumedRoleId = value;
+ return this;
+ }
+
+ public Builder setAssumedRoleUserArn(String value) {
+ this.assumedRoleUserArn = value;
+ return this;
+ }
+
+ public Builder setClock(Clock value) {
+ this.clock = value;
+ return this;
+ }
+
+ public CreateSTSTokenParams build() {
+ return new CreateSTSTokenParams(this);
+ }
+ }
+ }
}
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSSecurityUtil.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSSecurityUtil.java
index 594ac858bfb..eb9ac4ecde5 100644
--- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSSecurityUtil.java
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSSecurityUtil.java
@@ -54,6 +54,9 @@ public class TestSTSSecurityUtil {
private static final String ROLE_ARN = "arn:aws:iam::123456789012:role/test-role";
private static final String SECRET_ACCESS_KEY = "test-secret-access-key";
private static final String SESSION_POLICY = "test-session-policy";
+ private static final String ASSUMED_ROLE_ID = "AROATEST123456789:testsess";
+ private static final String ASSUMED_ROLE_USER_ARN =
+ "arn:aws:sts::123456789012:assumed-role/test-role/testsess";
private static final int DURATION_SECONDS = 3600;
private static final byte[] ENCRYPTION_KEY = new byte[5];
@@ -86,8 +89,7 @@ public void testConstructValidateAndDecryptSTSTokenInvalidProtobuf() throws IOEx
@Test
public void testConstructValidateAndDecryptSTSTokenSuccess() throws IOException {
// Create a valid token
- final String tokenString = tokenSecretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+ final String tokenString = createStsTokenString();
// Validate and decrypt the token
final STSTokenIdentifier result = STSSecurityUtil.constructValidateAndDecryptSTSToken(
@@ -108,8 +110,7 @@ public void testConstructValidateAndDecryptSTSTokenSuccess() throws IOException
@Test
public void testConstructValidateAndDecryptSTSTokenSuccessWithNullSessionPolicy() throws Exception {
// Create a valid token with null session policy
- final String tokenString = tokenSecretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, null, clock);
+ final String tokenString = createStsTokenString(DURATION_SECONDS, null);
// Validate and decrypt the token
final STSTokenIdentifier result = STSSecurityUtil.constructValidateAndDecryptSTSToken(
@@ -141,8 +142,7 @@ public void testConstructValidateAndDecryptSTSTokenRuntimeDecodeFailure() {
@Test
public void testConstructValidateAndDecryptSTSTokenInvalidKind() throws Exception {
// Create a valid identifier to use as base
- final String validTokenString = tokenSecretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+ final String validTokenString = createStsTokenString();
final Token validToken = new Token<>();
validToken.decodeFromUrlString(validTokenString);
@@ -164,8 +164,7 @@ public void testConstructValidateAndDecryptSTSTokenInvalidKind() throws Exceptio
@Test
public void testConstructValidateAndDecryptSTSTokenInvalidService() throws Exception {
// Create a token with incorrect service
- final String validTokenString = tokenSecretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+ final String validTokenString = createStsTokenString();
final Token validToken = new Token<>();
validToken.decodeFromUrlString(validTokenString);
@@ -185,8 +184,7 @@ public void testConstructValidateAndDecryptSTSTokenInvalidService() throws Excep
@Test
public void testConstructValidateAndDecryptSTSTokenExpired() throws Exception {
// Create a token that expires immediately (durationSeconds of 0)
- final String tokenString = tokenSecretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, 0, SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+ final String tokenString = createStsTokenString(0, SESSION_POLICY);
// Fast-forward time to ensure token is expired
clock.fastForward(100);
@@ -202,8 +200,7 @@ public void testConstructValidateAndDecryptSTSTokenExpired() throws Exception {
@Test
public void testConstructValidateAndDecryptSTSTokenSecretKeyNotFound() throws Exception {
// Create a valid token string
- final String validTokenString = tokenSecretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+ final String validTokenString = createStsTokenString();
// Create a mock secret key client that returns null for the key
final SecretKeyClient mockKeyClient = mock(SecretKeyClient.class);
@@ -221,8 +218,7 @@ public void testConstructValidateAndDecryptSTSTokenSecretKeyNotFound() throws Ex
@Test
public void testConstructValidateAndDecryptSTSTokenInvalidSecretKeyId() throws Exception {
// Create a valid identifier to use as base
- final String validTokenString = tokenSecretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+ final String validTokenString = createStsTokenString();
final Token validToken = new Token<>();
validToken.decodeFromUrlString(validTokenString);
@@ -247,8 +243,7 @@ public void testConstructValidateAndDecryptSTSTokenInvalidSecretKeyId() throws E
@Test
public void testConstructValidateAndDecryptSTSTokenExpiredSecretKey() throws Exception {
// Create a valid token string
- final String validTokenString = tokenSecretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+ final String validTokenString = createStsTokenString();
// Create a mock secret key that is expired
final ManagedSecretKey expiredSecretKey = mock(ManagedSecretKey.class);
@@ -270,8 +265,7 @@ public void testConstructValidateAndDecryptSTSTokenExpiredSecretKey() throws Exc
@Test
public void testConstructValidateAndDecryptSTSTokenSecretKeyRetrievalException() throws Exception {
// Create a valid token string
- final String validTokenString = tokenSecretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+ final String validTokenString = createStsTokenString();
// Create a mock secret key client that throws an exception
final SecretKeyClient mockKeyClient = mock(SecretKeyClient.class);
@@ -289,8 +283,7 @@ public void testConstructValidateAndDecryptSTSTokenSecretKeyRetrievalException()
@Test
public void testConstructValidateAndDecryptSTSTokenInvalidSignature() throws Exception {
// Create a valid token string
- final String validTokenString = tokenSecretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+ final String validTokenString = createStsTokenString();
final Token validToken = new Token<>();
validToken.decodeFromUrlString(validTokenString);
@@ -322,13 +315,11 @@ public void testConstructValidateAndDecryptSTSTokenEmptyString() {
@Test
public void testConstructValidateAndDecryptMultipleTokens() throws Exception {
// Create multiple tokens and validate them all
- final String token1 = tokenSecretManager.createSTSTokenString(
- "temp-key-1", "orig-key-1", "role-arn-1", DURATION_SECONDS,
- "secret-key-1", "policy-1", clock);
+ final String token1 = createStsTokenString(DURATION_SECONDS, "secret-key-1", "policy-1",
+ "temp-key-1", "orig-key-1", "role-arn-1");
- final String token2 = tokenSecretManager.createSTSTokenString(
- "temp-key-2", "orig-key-2", "role-arn-2", DURATION_SECONDS,
- "secret-key-2", "policy-2", clock);
+ final String token2 = createStsTokenString(DURATION_SECONDS, "secret-key-2", "policy-2",
+ "temp-key-2", "orig-key-2", "role-arn-2");
final STSTokenIdentifier result1 = STSSecurityUtil.constructValidateAndDecryptSTSToken(
token1, secretKeyClient, clock);
@@ -398,8 +389,7 @@ public void testEnsureEssentialFieldsArePresentInTokenMissingCreationTime() {
@Test
public void testEnsureResolvedStsFieldsInvariantsSuccess() throws Exception {
- final String tokenString = tokenSecretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+ final String tokenString = createStsTokenString();
final S3Authentication s3Auth = S3Authentication.newBuilder()
.setSessionToken(tokenString)
@@ -438,9 +428,7 @@ public void testEnsureResolvedStsFieldsInvariantsMissingSessionToken() {
@Test
public void testEnsureResolvedStsFieldsInvariantsMissingResolvedFields() throws Exception {
- final String tokenString = tokenSecretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
- SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+ final String tokenString = createStsTokenString();
final S3Authentication s3Auth = S3Authentication.newBuilder()
.setSessionToken(tokenString)
@@ -468,6 +456,32 @@ public void testEnsureResolvedStsFieldsInvariantsNoS3Auth() throws Exception {
STSSecurityUtil.ensureResolvedStsFieldsInvariants(request);
}
+ private String createStsTokenString() throws IOException {
+ return createStsTokenString(DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY,
+ TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN);
+ }
+
+ private String createStsTokenString(int durationSeconds, String sessionPolicy)
+ throws IOException {
+ return createStsTokenString(durationSeconds, TestSTSSecurityUtil.SECRET_ACCESS_KEY, sessionPolicy,
+ TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN);
+ }
+
+ private String createStsTokenString(int durationSeconds, String secretAccessKey, String sessionPolicy,
+ String tempAccessKey, String originalAccessKey, String roleArn) throws IOException {
+ return tokenSecretManager.createSTSTokenString(STSTokenSecretManager.CreateSTSTokenParams.newBuilder()
+ .setTempAccessKeyId(tempAccessKey)
+ .setOriginalAccessKeyId(originalAccessKey)
+ .setRoleArn(roleArn)
+ .setDurationSeconds(durationSeconds)
+ .setSecretAccessKey(secretAccessKey)
+ .setSessionPolicy(sessionPolicy)
+ .setAssumedRoleId(ASSUMED_ROLE_ID)
+ .setAssumedRoleUserArn(ASSUMED_ROLE_USER_ARN)
+ .setClock(clock)
+ .build());
+ }
+
private STSTokenIdentifier.Params.Builder paramsBuilder() {
return STSTokenIdentifier.Params.newBuilder()
.setTempAccessKeyId(TEMP_ACCESS_KEY)
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenIdentifier.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenIdentifier.java
index 4ff08087e9f..ba26747b90a 100644
--- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenIdentifier.java
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenIdentifier.java
@@ -72,6 +72,8 @@ public void testProtoBufRoundTrip() throws IOException {
.setExpiry(expiry)
.setSecretAccessKey("secretKey")
.setSessionPolicy("sessionPolicy")
+ .setAssumedRoleId("AROATEST123456789:testsess")
+ .setAssumedRoleUserArn("arn:aws:sts::123456789012:assumed-role/RoleY/testsess")
.build());
final UUID secretKeyId = UUID.randomUUID();
originalTokenIdentifier.setSecretKeyId(secretKeyId);
@@ -85,6 +87,9 @@ public void testProtoBufRoundTrip() throws IOException {
assertThat(proto.getRoleArn()).isEqualTo("arn:aws:iam::123456789012:role/RoleY");
assertThat(proto.getSecretAccessKey()).isNotEqualTo("secretKey"); // must be encrypted
assertThat(proto.getSessionPolicy()).isEqualTo("sessionPolicy");
+ assertThat(proto.getAssumedRoleId()).isEqualTo("AROATEST123456789:testsess");
+ assertThat(proto.getAssumedRoleUserArn())
+ .isEqualTo("arn:aws:sts::123456789012:assumed-role/RoleY/testsess");
assertThat(proto.getSecretKeyId()).isEqualTo(secretKeyId.toString());
final STSTokenIdentifier parsedTokenIdentifier = new STSTokenIdentifier();
@@ -99,6 +104,9 @@ public void testProtoBufRoundTrip() throws IOException {
assertThat(parsedTokenIdentifier.getSecretAccessKey()).isEqualTo("secretKey");
assertThat(parsedTokenIdentifier.getSecretKeyId()).isEqualTo(secretKeyId);
assertThat(parsedTokenIdentifier.getSessionPolicy()).isEqualTo("sessionPolicy");
+ assertThat(parsedTokenIdentifier.getAssumedRoleId()).isEqualTo("AROATEST123456789:testsess");
+ assertThat(parsedTokenIdentifier.getAssumedRoleUserArn())
+ .isEqualTo("arn:aws:sts::123456789012:assumed-role/RoleY/testsess");
assertThat(parsedTokenIdentifier).isEqualTo(originalTokenIdentifier);
assertThat(parsedTokenIdentifier.hashCode()).isEqualTo(originalTokenIdentifier.hashCode());
}
@@ -208,6 +216,8 @@ public void testWriteToAndReadFromByteArray() throws Exception {
.setExpiry(expiry)
.setSecretAccessKey("secretAccessKey")
.setSessionPolicy("sessionPolicy")
+ .setAssumedRoleId("AROATEST123456789:testsess")
+ .setAssumedRoleUserArn("arn:aws:sts::123456789012:assumed-role/test-role/testsess")
.build());
originalTokenIdentifier.setSecretKeyId(UUID.randomUUID());
@@ -544,12 +554,16 @@ public void testToString() {
.setExpiry(expiry)
.setSecretAccessKey("secretAccessKey")
.setSessionPolicy("sessionPolicy")
+ .setAssumedRoleId("AROATEST123456789:testsess")
+ .setAssumedRoleUserArn("arn:aws:sts::123456789012:assumed-role/test-role/testsess")
.build());
stsTokenIdentifier.setSecretKeyId(uuid);
final String stsTokenIdentifierStr = stsTokenIdentifier.toString();
final String expectedString = "STSTokenIdentifier{" + "tempAccessKeyId='tempAccessKeyId'" +
", originalAccessKeyId='originalAccessKeyId'" + ", roleArn='roleArn'" +
+ ", assumedRoleId='AROATEST123456789:testsess'" +
+ ", assumedRoleUserArn='arn:aws:sts::123456789012:assumed-role/test-role/testsess'" +
", creationTime='" + CREATION_TIME + "', expiry='" + expiry +
"', secretKeyId='" + uuid + "', sessionPolicy='sessionPolicy'" + '}';
diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java
index e2e28fd4e7d..2f21e8203e0 100644
--- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java
+++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java
@@ -56,6 +56,8 @@ public class TestSTSTokenSecretManager {
private static final String ROLE_ARN = "arn:aws:iam::123456789012:role/test-role";
private static final String SECRET_ACCESS_KEY = "test-secret-access-key";
private static final String SESSION_POLICY = "test-session-policy";
+ private static final String ASSUMED_ROLE_ID = "AROATEST123456789:testsess";
+ private static final String ASSUMED_ROLE_USER_ARN = "arn:aws:sts::123456789012:assumed-role/test-role/testsess";
private static final int DURATION_SECONDS = 3600;
private static SecretKey sharedSecretKey;
@@ -84,8 +86,7 @@ public void setUp() throws Exception {
@Test
public void testCreateSTSTokenStringContainsCorrectFields() throws IOException {
- final String tokenString = secretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+ final String tokenString = secretManager.createSTSTokenString(createStsTokenParamsBuilder().build());
// Decode the token
final Token token = new Token<>();
@@ -102,6 +103,8 @@ public void testCreateSTSTokenStringContainsCorrectFields() throws IOException {
assertEquals(ROLE_ARN, identifier.getRoleArn());
assertEquals(SECRET_ACCESS_KEY, identifier.getSecretAccessKey());
assertEquals(SESSION_POLICY, identifier.getSessionPolicy());
+ assertEquals(ASSUMED_ROLE_ID, identifier.getAssumedRoleId());
+ assertEquals(ASSUMED_ROLE_USER_ARN, identifier.getAssumedRoleUserArn());
assertEquals(clock.instant(), identifier.getCreationTime());
assertNotNull(identifier.getSecretKeyId());
assertEquals(new Text("STSToken"), identifier.getKind());
@@ -112,7 +115,7 @@ public void testCreateSTSTokenStringContainsCorrectFields() throws IOException {
@Test
public void testCreateSTSTokenStringWithNullSessionPolicy() throws IOException {
final String tokenString = secretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, null, clock);
+ createStsTokenParamsBuilder().setSessionPolicy(null).build());
// Decode the token
final Token token = new Token<>();
@@ -146,8 +149,7 @@ public void testCreateSTSTokenStringValidatesWhenSecretKeyRotatesDuringCreation(
encryptionKey, signingKey);
final STSTokenSecretManager rotatingSecretManager = new STSTokenSecretManager(rotatingSecretKeyClient);
- final String tokenString = rotatingSecretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+ final String tokenString = rotatingSecretManager.createSTSTokenString(createStsTokenParamsBuilder().build());
final STSTokenIdentifier result = STSSecurityUtil.constructValidateAndDecryptSTSToken(
tokenString, rotatingSecretKeyClient, clock);
@@ -156,6 +158,19 @@ public void testCreateSTSTokenStringValidatesWhenSecretKeyRotatesDuringCreation(
assertEquals(1, rotatingSecretKeyClient.getCurrentSecretKeyCallCount());
}
+ private STSTokenSecretManager.CreateSTSTokenParams.Builder createStsTokenParamsBuilder() {
+ return STSTokenSecretManager.CreateSTSTokenParams.newBuilder()
+ .setTempAccessKeyId(TEMP_ACCESS_KEY)
+ .setOriginalAccessKeyId(ORIGINAL_ACCESS_KEY)
+ .setRoleArn(ROLE_ARN)
+ .setDurationSeconds(DURATION_SECONDS)
+ .setSecretAccessKey(SECRET_ACCESS_KEY)
+ .setSessionPolicy(SESSION_POLICY)
+ .setAssumedRoleId(ASSUMED_ROLE_ID)
+ .setAssumedRoleUserArn(ASSUMED_ROLE_USER_ARN)
+ .setClock(clock);
+ }
+
private static ManagedSecretKey createManagedSecretKey(UUID id, byte[] keyBytes, Instant creationTime) {
final SecretKey secretKey = new SecretKeySpec(keyBytes, "HmacSHA256");
return new ManagedSecretKey(id, creationTime, creationTime.plus(Duration.ofHours(1)), secretKey);
diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/audit/S3GAction.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/audit/S3GAction.java
index abdb64e5ff2..f215cd6cb0f 100644
--- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/audit/S3GAction.java
+++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/audit/S3GAction.java
@@ -66,6 +66,7 @@ public enum S3GAction implements AuditAction {
// STS endpoint
ASSUME_ROLE,
+ GET_CALLER_IDENTITY,
GET_OBJECT_ATTRIBUTES;
diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3GActionIamMapper.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3GActionIamMapper.java
index 9953ebe2020..223b057b5bb 100644
--- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3GActionIamMapper.java
+++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3GActionIamMapper.java
@@ -85,6 +85,7 @@ private S3GActionIamMapper() {
case GENERATE_SECRET:
case REVOKE_SECRET:
case ASSUME_ROLE:
+ case GET_CALLER_IDENTITY:
default:
return null;
}
diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3AssumeRoleResponseXml.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3AssumeRoleResponseXml.java
index bd4be9a7eaf..6c8b73906a7 100644
--- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3AssumeRoleResponseXml.java
+++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3AssumeRoleResponseXml.java
@@ -33,7 +33,7 @@ public class S3AssumeRoleResponseXml {
private AssumeRoleResult assumeRoleResult;
@XmlElement(name = "ResponseMetadata")
- private ResponseMetadata responseMetadata;
+ private S3STSResponseMetadata responseMetadata;
public AssumeRoleResult getAssumeRoleResult() {
return assumeRoleResult;
@@ -43,11 +43,11 @@ public void setAssumeRoleResult(AssumeRoleResult assumeRoleResult) {
this.assumeRoleResult = assumeRoleResult;
}
- public ResponseMetadata getResponseMetadata() {
+ public S3STSResponseMetadata getResponseMetadata() {
return responseMetadata;
}
- public void setResponseMetadata(ResponseMetadata responseMetadata) {
+ public void setResponseMetadata(S3STSResponseMetadata responseMetadata) {
this.responseMetadata = responseMetadata;
}
@@ -157,23 +157,6 @@ public void setArn(String arn) {
this.arn = arn;
}
}
-
- /**
- * ResponseMetadata element.
- */
- @XmlAccessorType(XmlAccessType.FIELD)
- public static class ResponseMetadata {
- @XmlElement(name = "RequestId")
- private String requestId;
-
- public String getRequestId() {
- return requestId;
- }
-
- public void setRequestId(String requestId) {
- this.requestId = requestId;
- }
- }
}
diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3GetCallerIdentityResponseXml.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3GetCallerIdentityResponseXml.java
new file mode 100644
index 00000000000..594ed12e55d
--- /dev/null
+++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3GetCallerIdentityResponseXml.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.s3sts;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+
+/**
+ * JAXB model for AWS STS GetCallerIdentityResponse.
+ */
+@XmlAccessorType(XmlAccessType.FIELD)
+@XmlRootElement(name = "GetCallerIdentityResponse", namespace = "https://sts.amazonaws.com/doc/2011-06-15/")
+public class S3GetCallerIdentityResponseXml {
+
+ @XmlElement(name = "GetCallerIdentityResult")
+ private GetCallerIdentityResult getCallerIdentityResult;
+
+ @XmlElement(name = "ResponseMetadata")
+ private S3STSResponseMetadata responseMetadata;
+
+ public GetCallerIdentityResult getGetCallerIdentityResult() {
+ return getCallerIdentityResult;
+ }
+
+ public void setGetCallerIdentityResult(GetCallerIdentityResult getCallerIdentityResult) {
+ this.getCallerIdentityResult = getCallerIdentityResult;
+ }
+
+ public S3STSResponseMetadata getResponseMetadata() {
+ return responseMetadata;
+ }
+
+ public void setResponseMetadata(S3STSResponseMetadata responseMetadata) {
+ this.responseMetadata = responseMetadata;
+ }
+
+ /**
+ * GetCallerIdentityResult element.
+ */
+ @XmlAccessorType(XmlAccessType.FIELD)
+ public static class GetCallerIdentityResult {
+ @XmlElement(name = "Arn")
+ private String arn;
+
+ @XmlElement(name = "UserId")
+ private String userId;
+
+ @XmlElement(name = "Account")
+ private String account;
+
+ public String getArn() {
+ return arn;
+ }
+
+ public void setArn(String arn) {
+ this.arn = arn;
+ }
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
+ public String getAccount() {
+ return account;
+ }
+
+ public void setAccount(String account) {
+ this.account = account;
+ }
+ }
+}
diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSEndpoint.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSEndpoint.java
index d6ed5339a44..2c6200d1e66 100644
--- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSEndpoint.java
+++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSEndpoint.java
@@ -60,6 +60,7 @@
import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.helpers.AssumeRoleResponseInfo;
import org.apache.hadoop.ozone.om.helpers.AwsRoleArnValidator;
+import org.apache.hadoop.ozone.om.helpers.CallerIdentityInfo;
import org.apache.hadoop.ozone.om.helpers.S3STSUtils;
import org.apache.hadoop.ozone.s3.RequestIdentifier;
import org.apache.hadoop.ozone.s3.exception.OS3Exception;
@@ -73,8 +74,8 @@
* This endpoint provides temporary security credentials compatible with
* AWS STS API, exposed on port 9880 or 9881 at the root path ({@code /}).
*
- * Currently supports only AssumeRole operation. Other STS operations will
- * return appropriate error responses.
+ * Currently supports AssumeRole and GetCallerIdentity operations. Other STS
+ * operations will return appropriate error responses.
*
* @see AWS STS API Reference
*/
@@ -113,7 +114,8 @@ public class S3STSEndpoint extends S3STSEndpointBase {
static {
try {
- JAXB_CONTEXT = JAXBContext.newInstance(S3AssumeRoleResponseXml.class);
+ JAXB_CONTEXT = JAXBContext.newInstance(
+ S3AssumeRoleResponseXml.class, S3GetCallerIdentityResponseXml.class, S3STSResponseMetadata.class);
} catch (JAXBException e) {
throw new RuntimeException("Failed to initialize JAXBContext: " + e, e);
}
@@ -193,11 +195,12 @@ private Response handleSTSRequest(Set paramNamesToValidate, String actio
case ASSUME_ROLE_ACTION:
return handleAssumeRole(
paramNamesToValidate, roleArn, roleSessionName, durationSeconds, awsIamSessionPolicy, version, requestId);
+ case GET_CALLER_IDENTITY_ACTION:
+ return handleGetCallerIdentity(version, requestId);
// These operations are not supported yet
case GET_SESSION_TOKEN_ACTION:
case ASSUME_ROLE_WITH_SAML_ACTION:
case ASSUME_ROLE_WITH_WEB_IDENTITY_ACTION:
- case GET_CALLER_IDENTITY_ACTION:
case DECODE_AUTHORIZATION_MESSAGE_ACTION:
case GET_ACCESS_KEY_INFO_ACTION:
throw new OSTSException(STS_INVALID_ACTION_NOT_IMPLEMENTED)
@@ -307,39 +310,76 @@ private Response handleAssumeRole(Set paramNamesToValidate, String roleA
.header("Content-Type", "text/xml")
.build();
} catch (IOException e) {
- LOG.error("Error during AssumeRole processing", e);
-
+ throw toStsProcessingException(
+ S3GAction.ASSUME_ROLE, auditParams, e, action, "User is not authorized to perform: sts:AssumeRole on " +
+ "resource: " + roleArn);
+ } catch (Exception e) {
getAuditLogger().logWriteFailure(buildAuditMessageForFailure(S3GAction.ASSUME_ROLE, auditParams, e));
+ throw e;
+ }
+ }
- if (e instanceof OMException) {
- final OMException omException = (OMException) e;
- if (omException.getResult() == OMException.ResultCodes.ACCESS_DENIED ||
- omException.getResult() == OMException.ResultCodes.PERMISSION_DENIED ||
- omException.getResult() == OMException.ResultCodes.TOKEN_EXPIRED) {
- throw new OSTSException(ACCESS_DENIED)
- .withMessage("User is not authorized to perform: sts:AssumeRole on resource: " + roleArn);
- }
- if (omException.getResult() == OMException.ResultCodes.INVALID_TOKEN) {
- throw new OSTSException(STS_INVALID_CLIENT_TOKEN_ID);
- }
- if (omException.getResult() == OMException.ResultCodes.NOT_SUPPORTED_OPERATION ||
- omException.getResult() == OMException.ResultCodes.FEATURE_NOT_ENABLED) {
- throw new OSTSException(STS_UNSUPPORTED_OPERATION).withMessage(omException.getMessage());
- }
- if (omException.getResult() == OMException.ResultCodes.INVALID_REQUEST) {
- throw new OSTSException(STS_VALIDATION_ERROR).withMessage(omException.getMessage());
- }
- if (omException.getResult() == OMException.ResultCodes.MALFORMED_POLICY_DOCUMENT) {
- throw new OSTSException(STS_MALFORMED_POLICY_DOCUMENT).withMessage(omException.getMessage());
- }
- }
- throw new OSTSException(STS_INTERNAL_FAILURE, e).withType("Receiver");
+ private Response handleGetCallerIdentity(String version, String requestId) throws OSTSException {
+ final String action = GET_CALLER_IDENTITY_ACTION;
+ final Map auditParams = getAuditParameters();
+ auditParams.put("action", action);
+ auditParams.put("requestId", requestId);
+
+ if (version == null || !version.equals(EXPECTED_VERSION)) {
+ final OSTSException exception = new OSTSException(STS_INVALID_ACTION)
+ .withMessage("Could not find operation " + action + " for version " +
+ (version == null ? "NO_VERSION_SPECIFIED. Expected version is: " + EXPECTED_VERSION : version));
+ getAuditLogger().logWriteFailure(buildAuditMessageForFailure(
+ S3GAction.GET_CALLER_IDENTITY, auditParams, exception));
+ throw exception;
+ }
+
+ try {
+ final CallerIdentityInfo identityInfo = getClient().getObjectStore().getCallerIdentity();
+ final String responseXml = generateGetCallerIdentityResponse(identityInfo, requestId);
+ getAuditLogger().logWriteSuccess(buildAuditMessageForSuccess(S3GAction.GET_CALLER_IDENTITY, auditParams));
+ return Response.ok(responseXml)
+ .header("Content-Type", "text/xml")
+ .build();
+ } catch (IOException e) {
+ throw toStsProcessingException(
+ S3GAction.GET_CALLER_IDENTITY, auditParams, e, action, "User is not authorized to perform: " +
+ "sts:GetCallerIdentity");
} catch (Exception e) {
- getAuditLogger().logWriteFailure(buildAuditMessageForFailure(S3GAction.ASSUME_ROLE, auditParams, e));
+ getAuditLogger().logWriteFailure(buildAuditMessageForFailure(S3GAction.GET_CALLER_IDENTITY, auditParams, e));
throw e;
}
}
+ private OSTSException toStsProcessingException(S3GAction auditAction, Map auditParams, IOException e,
+ String operationName, String accessDeniedMessage) {
+ LOG.error("Error during {} processing", operationName, e);
+ getAuditLogger().logWriteFailure(buildAuditMessageForFailure(auditAction, auditParams, e));
+
+ if (e instanceof OMException) {
+ final OMException omException = (OMException) e;
+ if (omException.getResult() == OMException.ResultCodes.ACCESS_DENIED ||
+ omException.getResult() == OMException.ResultCodes.PERMISSION_DENIED ||
+ omException.getResult() == OMException.ResultCodes.TOKEN_EXPIRED) {
+ return new OSTSException(ACCESS_DENIED).withMessage(accessDeniedMessage);
+ }
+ if (omException.getResult() == OMException.ResultCodes.INVALID_TOKEN) {
+ return new OSTSException(STS_INVALID_CLIENT_TOKEN_ID);
+ }
+ if (omException.getResult() == OMException.ResultCodes.NOT_SUPPORTED_OPERATION ||
+ omException.getResult() == OMException.ResultCodes.FEATURE_NOT_ENABLED) {
+ return new OSTSException(STS_UNSUPPORTED_OPERATION).withMessage(omException.getMessage());
+ }
+ if (omException.getResult() == OMException.ResultCodes.INVALID_REQUEST) {
+ return new OSTSException(STS_VALIDATION_ERROR).withMessage(omException.getMessage());
+ }
+ if (omException.getResult() == OMException.ResultCodes.MALFORMED_POLICY_DOCUMENT) {
+ return new OSTSException(STS_MALFORMED_POLICY_DOCUMENT).withMessage(omException.getMessage());
+ }
+ }
+ return new OSTSException(STS_INTERNAL_FAILURE, e).withType("Receiver");
+ }
+
private AssumeRoleParamValidationResult validateAssumeRoleParameters(Set paramNamesToValidate) {
if (paramNamesToValidate == null || paramNamesToValidate.isEmpty()) {
return AssumeRoleParamValidationResult.empty();
@@ -450,7 +490,7 @@ private String generateAssumeRoleResponse(String assumedRoleUserArn, AssumeRoleR
user.setArn(assumedRoleUserArn);
result.setAssumedRoleUser(user);
response.setAssumeRoleResult(result);
- final S3AssumeRoleResponseXml.ResponseMetadata meta = new S3AssumeRoleResponseXml.ResponseMetadata();
+ final S3STSResponseMetadata meta = new S3STSResponseMetadata();
meta.setRequestId(requestId);
response.setResponseMetadata(meta);
@@ -463,5 +503,29 @@ private String generateAssumeRoleResponse(String assumedRoleUserArn, AssumeRoleR
throw new IOException("Failed to marshal AssumeRole response", e);
}
}
+
+ private String generateGetCallerIdentityResponse(CallerIdentityInfo identityInfo, String requestId)
+ throws IOException {
+ try {
+ final S3GetCallerIdentityResponseXml response = new S3GetCallerIdentityResponseXml();
+ final S3GetCallerIdentityResponseXml.GetCallerIdentityResult result =
+ new S3GetCallerIdentityResponseXml.GetCallerIdentityResult();
+ result.setAccount(identityInfo.getAccount());
+ result.setArn(identityInfo.getArn());
+ result.setUserId(identityInfo.getUserId());
+ response.setGetCallerIdentityResult(result);
+ final S3STSResponseMetadata meta = new S3STSResponseMetadata();
+ meta.setRequestId(requestId);
+ response.setResponseMetadata(meta);
+
+ final Marshaller marshaller = JAXB_CONTEXT.createMarshaller();
+ marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
+ final StringWriter stringWriter = new StringWriter();
+ marshaller.marshal(response, stringWriter);
+ return stringWriter.toString();
+ } catch (JAXBException e) {
+ throw new IOException("Failed to marshal GetCallerIdentity response", e);
+ }
+ }
}
diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSResponseMetadata.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSResponseMetadata.java
new file mode 100644
index 00000000000..a43ec76b443
--- /dev/null
+++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSResponseMetadata.java
@@ -0,0 +1,42 @@
+/*
+ * 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.s3sts;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlType;
+
+/**
+ * JAXB model for AWS STS ResponseMetadata element shared across STS responses.
+ */
+@XmlAccessorType(XmlAccessType.FIELD)
+@XmlType(name = "ResponseMetadata", namespace = "https://sts.amazonaws.com/doc/2011-06-15/")
+public class S3STSResponseMetadata {
+
+ @XmlElement(name = "RequestId")
+ private String requestId;
+
+ public String getRequestId() {
+ return requestId;
+ }
+
+ public void setRequestId(String requestId) {
+ this.requestId = requestId;
+ }
+}
diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestS3GActionIamMapper.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestS3GActionIamMapper.java
index c7ae9e4e924..82f0134cffe 100644
--- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestS3GActionIamMapper.java
+++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestS3GActionIamMapper.java
@@ -62,6 +62,7 @@ public void copyActionsReturnNull() {
@Test
public void nonIamActionsReturnNull() {
assertNull(S3GActionIamMapper.toS3ActionString(S3GAction.ASSUME_ROLE));
+ assertNull(S3GActionIamMapper.toS3ActionString(S3GAction.GET_CALLER_IDENTITY));
assertNull(S3GActionIamMapper.toS3ActionString(S3GAction.GENERATE_SECRET));
assertNull(S3GActionIamMapper.toS3ActionString(S3GAction.REVOKE_SECRET));
}
diff --git a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3sts/TestS3STSEndpoint.java b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3sts/TestS3STSEndpoint.java
index 36adf2359c4..379bd27eb98 100644
--- a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3sts/TestS3STSEndpoint.java
+++ b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3sts/TestS3STSEndpoint.java
@@ -51,6 +51,7 @@
import org.apache.hadoop.ozone.client.OzoneClientStub;
import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.helpers.AssumeRoleResponseInfo;
+import org.apache.hadoop.ozone.om.helpers.CallerIdentityInfo;
import org.apache.hadoop.ozone.s3.OzoneConfigurationHolder;
import org.apache.hadoop.ozone.s3.RequestIdentifier;
import org.apache.hadoop.ozone.s3.exception.OSTSException;
@@ -103,6 +104,11 @@ public void setup() throws Exception {
"session-token",
Instant.now().plusSeconds(3600).getEpochSecond(),
"AROA1234567890123456:test-session"));
+ when(objectStore.getCallerIdentity())
+ .thenReturn(new CallerIdentityInfo(
+ "123456789012",
+ "arn:aws:iam::123456789012:user/test-user",
+ "test-user"));
when(clientStub.getObjectStore()).thenReturn(objectStore);
endpoint = new S3STSEndpoint();
@@ -560,6 +566,79 @@ public void testStsWhenActionNotImplemented() throws Exception {
"Operation GetSessionToken is not supported yet.");
}
+ @Test
+ public void testStsGetCallerIdentitySuccessForGetMethod() throws Exception {
+ final Response response = endpoint.get("GetCallerIdentity", null, null, null, "2011-06-15", null);
+
+ assertEquals(200, response.getStatus());
+ verify(objectStore).getCallerIdentity();
+ verify(auditLogger).logWriteSuccess(any(AuditMessage.class));
+ verify(auditLogger, never()).logWriteFailure(any(AuditMessage.class));
+
+ final Document doc = parseXml((String) response.getEntity());
+ assertEquals("GetCallerIdentityResponse", doc.getDocumentElement().getLocalName());
+ assertEquals(STS_NS, doc.getDocumentElement().getNamespaceURI());
+ assertEquals(
+ "123456789012", doc.getElementsByTagNameNS(STS_NS, "Account").item(0).getTextContent());
+ assertEquals(
+ "arn:aws:iam::123456789012:user/test-user", doc.getElementsByTagNameNS(STS_NS, "Arn").item(0).getTextContent());
+ assertEquals(
+ "test-user", doc.getElementsByTagNameNS(STS_NS, "UserId").item(0).getTextContent());
+ }
+
+ @Test
+ public void testStsGetCallerIdentityIgnoresExtraParameters() throws Exception {
+ final Response response = endpoint.get("GetCallerIdentity", ROLE_ARN, ROLE_SESSION_NAME, 3600, "2011-06-15", null);
+
+ assertEquals(200, response.getStatus());
+ verify(objectStore).getCallerIdentity();
+ }
+
+ @Test
+ public void testStsGetCallerIdentityIgnoresExtraParametersForPostMethod() throws Exception {
+ formParameters = new Form();
+ formParameters.param("Action", "GetCallerIdentity");
+ formParameters.param("Version", "2011-06-15");
+ formParameters.param("RoleArn", ROLE_ARN);
+ formParameters.param("RoleSessionName", ROLE_SESSION_NAME);
+ formParameters.param("DurationSeconds", "3600");
+
+ final Response response = endpoint.post(formParameters);
+
+ assertEquals(200, response.getStatus());
+ verify(objectStore).getCallerIdentity();
+ }
+
+ @Test
+ public void testStsGetCallerIdentityRejectsMissingVersion() throws Exception {
+ final OSTSException ex = assertThrows(
+ OSTSException.class, () -> endpoint.get("GetCallerIdentity", null, null, null, null, null));
+
+ assertEquals(400, ex.getHttpCode());
+ verify(auditLogger).logWriteFailure(any(AuditMessage.class));
+ verify(objectStore, never()).getCallerIdentity();
+
+ ex.setRequestId(REQUEST_ID);
+ assertStsErrorXml(
+ ex.toXml(), AWS_FAULT_NS, "Sender", "InvalidAction",
+ "Could not find operation GetCallerIdentity for version NO_VERSION_SPECIFIED");
+ }
+
+ @Test
+ public void testStsGetCallerIdentityRejectsInvalidVersion() throws Exception {
+ final OSTSException ex = assertThrows(
+ OSTSException.class, () -> endpoint.get("GetCallerIdentity", null, null, null, "2020-01-01", null));
+
+ assertEquals(400, ex.getHttpCode());
+ verify(auditLogger).logWriteFailure(any(AuditMessage.class));
+ verify(objectStore, never()).getCallerIdentity();
+
+ ex.setRequestId(REQUEST_ID);
+ assertStsErrorXml(
+ ex.toXml(), AWS_FAULT_NS, "Sender", "InvalidAction",
+ "Could not find operation GetCallerIdentity for version 2020-01-01");
+ }
+
@Test
public void testStsMissingRoleSessionName() throws Exception {
final OSTSException ex = assertThrows(OSTSException.class, () ->
From 50ed08ac88dab471fa1c17e6a9ccd07797858a06 Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Tue, 25 Aug 2026 22:33:15 -0700
Subject: [PATCH 20/20] update smoke tests
---
.../security/ozone-secure-sts.resource | 37 +++++++++++++++++++
.../smoketest/security/ozone-secure-sts.robot | 32 ++++++++++++++++
2 files changed, 69 insertions(+)
diff --git a/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.resource b/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.resource
index 19cb6f4e202..02c16a9418c 100644
--- a/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.resource
+++ b/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.resource
@@ -103,10 +103,14 @@ Assume Role And Get Temporary Credentials
${stsAccessKeyId} = Execute echo '${json}' | jq -r '.Credentials.AccessKeyId'
${stsSecretKey} = Execute echo '${json}' | jq -r '.Credentials.SecretAccessKey'
${stsSessionToken} = Execute echo '${json}' | jq -r '.Credentials.SessionToken'
+ ${assumedRoleId} = Execute echo '${json}' | jq -r '.AssumedRoleUser.AssumedRoleId'
+ ${assumedRoleUserArn} = Execute echo '${json}' | jq -r '.AssumedRoleUser.Arn'
Should Start With ${stsAccessKeyId} ASIA
Set Global Variable ${STS_ACCESS_KEY_ID} ${stsAccessKeyId}
Set Global Variable ${STS_SECRET_KEY} ${stsSecretKey}
Set Global Variable ${STS_SESSION_TOKEN} ${stsSessionToken}
+ Set Global Variable ${STS_ASSUMED_ROLE_ID} ${assumedRoleId}
+ Set Global Variable ${STS_ASSUMED_ROLE_USER_ARN} ${assumedRoleUserArn}
${expected_duration} = Set Variable ${duration_seconds}
# Ensure the expected duration defaults to 3600 seconds (1 hour) if not specified
@@ -328,3 +332,36 @@ Assert Listed Keys Should Equal
Sort List ${expected_sorted}
${actual_list} = Evaluate json.loads($actual_keys_json) modules=json
Lists Should Be Equal ${actual_list} ${expected_sorted}
+
+Get Caller Identity
+ [Arguments] ${profile}
+ ${json} = Execute aws sts get-caller-identity --endpoint-url ${STS_ENDPOINT_URL} --output json --profile ${profile}
+ ${account} = Execute echo '${json}' | jq -r '.Account'
+ ${arn} = Execute echo '${json}' | jq -r '.Arn'
+ ${userId} = Execute echo '${json}' | jq -r '.UserId'
+ [Return] ${json} ${account} ${arn} ${userId}
+
+Get Caller Identity Using Curl
+ # AWS CLI always sends Version=2011-06-15 and rejects unknown flags, so use curl to test version validation.
+ # curl 7.76.1's --aws-sigv4 omits the port from the signed canonical "host" header (it signs "host:s3g"
+ # while sending "Host: s3g:9880"), so Ozone's SigV4 validation for the non-default STS port rejects it.
+ # Send an explicit Host header without the port so the sent and signed host values match.
+ # This should also work for newer versions of curl as well
+ [Arguments] ${perm_access_key_id} ${perm_secret_key} ${api_version}=2011-06-15 ${extra_curl_params}=${EMPTY}
+ ${sts_host} = Evaluate urllib.parse.urlparse("${STS_ENDPOINT_URL}").hostname modules=urllib.parse
+ ${cmd} = Set Variable curl --silent --show-error --include --request POST --aws-sigv4 "aws:amz:us-east-1:sts" --user '${perm_access_key_id}:${perm_secret_key}' --header "Host: ${sts_host}" --header "Content-Type: application/x-www-form-urlencoded" --data-urlencode "Action=GetCallerIdentity"
+ ${cmd} = Set Variable If '${api_version}' != '${EMPTY}' ${cmd} --data-urlencode "Version=${api_version}" ${cmd}
+ ${cmd} = Set Variable If '${extra_curl_params}' != '${EMPTY}' ${cmd} ${extra_curl_params} ${cmd}
+ ${cmd} = Set Variable ${cmd} ${STS_ENDPOINT_URL}
+ ${output} = Execute And Ignore Error ${cmd}
+ [Return] ${output}
+
+Get Caller Identity Should Fail
+ [Arguments] ${expected_error_contains} ${api_version}=${EMPTY}
+ ${output} = Get Caller Identity Using Curl ${PERMANENT_ACCESS_KEY_ID} ${PERMANENT_SECRET_KEY} api_version=${api_version}
+ Should Contain ${output} ${expected_error_contains}
+ @{http_codes} = Get Regexp Matches ${output} (?m)^HTTP/[0-9.]+ ([0-9]{3}) 1
+ ${code_count} = Get Length ${http_codes}
+ Should Be True ${code_count} > 0 Expected to find an HTTP status code in curl output, but none was found.
+ ${http_code} = Get From List ${http_codes} -1
+ Should Be Equal As Strings ${http_code} 400
diff --git a/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot b/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot
index 3ee4e19a5ae..b62a1053f5a 100644
--- a/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot
+++ b/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot
@@ -1264,6 +1264,38 @@ STS session policy containing only GetObject must deny DeleteObjects
${output} = Execute aws s3api --endpoint-url ${S3G_ENDPOINT_URL} delete-bucket --bucket ${bucket} --profile sts
Should Not Contain ${output} AccessDenied
+Get Caller Identity With Permanent Credentials Should Succeed
+ Configure AWS Profile permanent ${PERMANENT_ACCESS_KEY_ID} ${PERMANENT_SECRET_KEY}
+ ${json} ${account} ${arn} ${userId} = Get Caller Identity permanent
+ Should Be Equal ${account} 123456789012
+ ${principal} = Execute klist | awk '/Default principal/ {print $3}'
+ Should Be Equal ${userId} ${principal}
+ Should Be Equal ${arn} arn:aws:iam::123456789012:user/${ICEBERG_SVC_CATALOG_USER}
+
+Get Caller Identity With STS Credentials Should Succeed
+ Assume Role And Configure STS Profile perm_access_key_id=${PERMANENT_ACCESS_KEY_ID} perm_secret_key=${PERMANENT_SECRET_KEY} role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+ ${json} ${account} ${arn} ${userId} = Get Caller Identity sts
+ Should Be Equal ${account} 123456789012
+ Should Be Equal ${userId} ${STS_ASSUMED_ROLE_ID}
+ Should Be Equal ${arn} ${STS_ASSUMED_ROLE_USER_ARN}
+
+Get Caller Identity Ignores Extra Parameters
+ # curl 7.76.1 produces an invalid SigV4 signature for STS GET requests with --get --data-urlencode.
+ ${extra_curl_params} = Set Variable --data-urlencode "RoleArn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}" --data-urlencode "RoleSessionName=${ROLE_SESSION_NAME}" --data-urlencode "DurationSeconds=3600"
+ ${output} = Get Caller Identity Using Curl ${PERMANENT_ACCESS_KEY_ID} ${PERMANENT_SECRET_KEY} extra_curl_params=${extra_curl_params}
+ Should Contain ${output} 123456789012
+ @{http_codes} = Get Regexp Matches ${output} (?m)^HTTP/[0-9.]+ ([0-9]{3}) 1
+ ${code_count} = Get Length ${http_codes}
+ Should Be True ${code_count} > 0 Expected to find an HTTP status code in curl output, but none was found.
+ ${http_code} = Get From List ${http_codes} -1
+ Should Be Equal As Strings ${http_code} 200
+
+Get Caller Identity Rejects Missing Version
+ Get Caller Identity Should Fail InvalidAction
+
+Get Caller Identity Rejects Invalid Version
+ Get Caller Identity Should Fail InvalidAction api_version=2020-01-01
+
Expired STS temporary credentials must return ExpiredToken on S3 APIs
# Increase timeout to account for 15 minute STS token expiration plus the time to execute the api calls
[Timeout] 25 minutes