From 64bd6f73dd4c8b7b60f06c0ac20b403204bb3e5b Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Sun, 16 Aug 2026 16:23:34 -0700
Subject: [PATCH 01/17] 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 6cc94eadd4bc..7ee1d9fcd73e 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 5f7e3f221f7703977536883a3ff1456551193f3e Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Sun, 16 Aug 2026 16:29:09 -0700
Subject: [PATCH 02/17] 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 bd045ef04e03..ce0f780b72de 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 807cd2757cfc..b5f7baa0ef2c 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 733c915dcd03..9ca47013462d 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 604669487c68..46254e3d6f63 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 c60bc60db700..7077fd1b02c7 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 c06d54209a0b..029a78a335dd 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 b79984fa93e7..abd80cbc1fcf 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 21e89abb56d262dd0e3ba089bb9a48796c0ff4fe Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Sun, 16 Aug 2026 16:30:39 -0700
Subject: [PATCH 03/17] 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 97f6cf920365..f1a7a51d3e26 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 2e99871e17c4..08600bb99504 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 d0d11c5ba94f..c284b460dd68 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 fc939e400ad1908a3389f9ba9464baac06e36f22 Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Sun, 16 Aug 2026 16:31:05 -0700
Subject: [PATCH 04/17] 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 274304217f86..9cd715d581a1 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 582926339275cf7920c4db671f22c264bf58f84d Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Sun, 16 Aug 2026 16:37:22 -0700
Subject: [PATCH 05/17] 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 e7e78b812063..d2ebc831e4b9 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 c72c351402e3..db79aa505f01 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 763c8fe9bfa4..642210896559 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 3db85f508051..a5e4154b43d0 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 6b9c6698cf9f..29abe0eb8eec 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 4efd18b4b327..b6d650cc4393 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 f41b20353a83..81558ec58504 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 52a92d8a5560..5ce42618e2ac 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 cb44e7f466d9..a1b255689de5 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 5b1a8cf3b019..db9233357ed2 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 3d9668d6469c..c627f6a21cb7 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 08ac1f2bee11..6612fff2bad8 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 2212ad6db797..03a1fdba017d 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 8c13aac51905..cc229083d9eb 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 f72b1892de85..8cddc50f18ab 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 0f3d2519b30c..99b6d6a98e9f 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 d7cf3630b955..2b734cea2456 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 99c358b929d2..814f79b1e84b 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 d2033deabec1..594ac858bfb8 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 1eb880f9dd03..870e99c3da18 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 09a786faaea3..4ff08087e9fe 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 800aeabe97c5..525ee5f0da08 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 63e25fca628e..c8c7d168b18e 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 978f96ddb35e..85486df8c3d0 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 9865345a9162..0f23eb221ef6 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 000000000000..b5952290dc9d
--- /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 942d82cfc5e745a2ef9b3c9dff810cf2ba35687c Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Sun, 16 Aug 2026 16:58:53 -0700
Subject: [PATCH 06/17] 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 a1f34e818b76..eb3e9ea7d55e 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 } ] }
@@ -555,27 +567,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
@@ -610,6 +627,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 d3c9ea52a76eb74c8ad888210ff3feaca8ff06ca Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Sun, 23 Aug 2026 13:41:26 -0700
Subject: [PATCH 07/17] 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 b5f7baa0ef2c..7900fa4a41f1 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 9ca47013462d..d960cad0bca2 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 999b892ff7bb..990a1ee21c73 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 d6d54d3c174d..f8098549b6b9 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 19d428d0a9e1..673e43ef9081 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 577339c96ac3..37c8438836bd 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 000000000000..98c03f9c5ead
--- /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 000000000000..30e75e651a3e
--- /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 029a78a335dd..78f89685f900 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 434d05132bf5..8e5ea0ed220e 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 6d3a56f40ed0..4272014d70e8 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 04455a525a99..b1ef381796f4 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 814f79b1e84b..24e5b48bb4c8 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 594ac858bfb8..c02df40e1a16 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 c833d5f22f43..3bf2eb346ea7 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 c8c7d168b18e..c2047e2f6f3b 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 85486df8c3d0..978f96ddb35e 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 0f23eb221ef6..f0d91631cb02 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 b5952290dc9d..000000000000
--- 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 28037ce63c0bbb0703a313479a3851565d207106 Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Sun, 23 Aug 2026 14:57:02 -0700
Subject: [PATCH 08/17] 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 7900fa4a41f1..b5f7baa0ef2c 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 d960cad0bca2..9ca47013462d 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 990a1ee21c73..999b892ff7bb 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 f8098549b6b9..d6d54d3c174d 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 673e43ef9081..19d428d0a9e1 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 37c8438836bd..577339c96ac3 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 98c03f9c5ead..000000000000
--- 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 30e75e651a3e..000000000000
--- 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 78f89685f900..029a78a335dd 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 8e5ea0ed220e..434d05132bf5 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 4272014d70e8..6d3a56f40ed0 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 b1ef381796f4..04455a525a99 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 24e5b48bb4c8..814f79b1e84b 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 c02df40e1a16..594ac858bfb8 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 3bf2eb346ea7..c833d5f22f43 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 c2047e2f6f3b..c8c7d168b18e 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 978f96ddb35e..85486df8c3d0 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 f0d91631cb02..0f23eb221ef6 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 000000000000..b5952290dc9d
--- /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 4418fb963877fbcd23cf9e2605000d0dc5e7364e Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Sun, 23 Aug 2026 15:07:45 -0700
Subject: [PATCH 09/17] 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 c8c7d168b18e..5bc8935f0b36 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 85486df8c3d0..17567c9be960 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 0f23eb221ef6..b72287287fc3 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 43f7d23760529da5a54a385f2872648d57a266bc Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Mon, 24 Aug 2026 14:47:44 -0700
Subject: [PATCH 10/17] 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 eb3e9ea7d55e..4d5903261bf7 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
@@ -590,7 +590,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 5ce42618e2ac..0012675a8015 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 99b6d6a98e9f..e73f9f53a710 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 734a613122ee8cdfca589175e6619db09045f50b Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Mon, 24 Aug 2026 15:16:01 -0700
Subject: [PATCH 11/17] 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 029a78a335dd..638cf99bd1ce 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 0012675a8015..18d6a9870f47 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 e73f9f53a710..212eeeadee33 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 4e50ca53d7380c2c5b44a9d4cf9513cc78c5ab61 Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Mon, 24 Aug 2026 15:22:37 -0700
Subject: [PATCH 12/17] 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 18d6a9870f47..ac02afc58793 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 212eeeadee33..c75a1fbccc70 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 3bcbe0348e1f0c7a013e05bf48d9f9deeb2fa357 Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Mon, 24 Aug 2026 16:26:31 -0700
Subject: [PATCH 13/17] 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 ac02afc58793..9953598e8140 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 c75a1fbccc70..1b6caed9cb40 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 10ef46a3811a4faa14d92f55bd215e715479f4f6 Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Mon, 24 Aug 2026 18:38:22 -0700
Subject: [PATCH 14/17] 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 03a1fdba017d..41aefeee7682 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 8cddc50f18ab..66273299fead 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 525ee5f0da08..e2e28fd4e7d3 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 99a1064b33f7a16f4ef87911bb79a6bb7f31ff43 Mon Sep 17 00:00:00 2001
From: Fabian Morgan
Date: Mon, 24 Aug 2026 18:49:42 -0700
Subject: [PATCH 15/17] 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 d2ebc831e4b9..188bd65559ff 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 5bc8935f0b36..63e25fca628e 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 17567c9be960..978f96ddb35e 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 b72287287fc3..9865345a9162 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 b5952290dc9d..000000000000
--- 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 286a06a70e1fd34cdad32d12876fab1fb9301af2 Mon Sep 17 00:00:00 2001
From: fmorg-git
Date: Tue, 25 Aug 2026 00:29:37 -0700
Subject: [PATCH 16/17] 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 9953598e8140..02e6cac1b3d4 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 220a4cc2a9ea9b2517f4996c90b0b676b659ff56 Mon Sep 17 00:00:00 2001
From: Sammi Chen
Date: Wed, 26 Aug 2026 17:13:39 +0800
Subject: [PATCH 17/17] set ManagedSecretKey to STSTokenIdentifier
---
.../om/ratis/OzoneManagerStateMachine.java | 4 +-
.../ozone/security/STSSecurityUtil.java | 4 +-
.../ozone/security/STSTokenIdentifier.java | 101 +++++++++++-------
.../ozone/security/STSTokenSecretManager.java | 30 ++----
.../ozone/security/TestS3SecurityUtil.java | 25 +++--
.../ozone/security/TestSTSSecurityUtil.java | 10 +-
.../security/TestSTSTokenEncryption.java | 15 ++-
.../security/TestSTSTokenIdentifier.java | 81 +++++++-------
.../security/TestSTSTokenSecretManager.java | 8 +-
9 files changed, 155 insertions(+), 123 deletions(-)
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 a5e4154b43d0..5b9c453d8936 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
@@ -695,10 +695,10 @@ OMResponse runCommand(OMRequest request, TermIndex termIndex) {
.setRoleArn(s3Auth.hasResolvedStsRoleArn() ? s3Auth.getResolvedStsRoleArn() : "")
.setCreationTime(Instant.MAX)
.setExpiry(Instant.MAX) // ensure it deterministically is not expired
- .setSecretAccessKey("") // no secretAccessKey needed
+ .setSecretAccessKey(null) // no secretAccessKey needed
.setSessionPolicy(
s3Auth.hasResolvedStsSessionPolicy() ? s3Auth.getResolvedStsSessionPolicy() : "")
- .setEncryptionKey(null) // no encryption key needed
+ .setManagedSecretKey(null) // no ManagedSecretKey needed
.build());
OzoneManager.setStsTokenIdentifier(rehydratedTokenIdentifier);
isStsThreadLocalSet = true;
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 41aefeee7682..ead735f12eac 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
@@ -101,7 +101,7 @@ private static STSTokenIdentifier verifyAndDecryptToken(Token generateToken(STSTokenIdentifier tokenIdentifier) {
- final ManagedSecretKey secretKey = secretKeyClient.getCurrentSecretKey();
- tokenIdentifier.setSecretKeyId(secretKey.getId());
- return generateToken(tokenIdentifier, secretKey);
- }
-
- private Token generateToken(STSTokenIdentifier tokenIdentifier, ManagedSecretKey secretKey) {
+ // Note - the ManagedSecretKey will NOT be encoded in the token. When generateToken() is called,
+ // it eventually calls the write() method in STSTokenIdentifier which calls toProtoBuf(), and the
+ // ManagedSecretKey is not serialized there.
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");
+ tokenIdentifier.getManagedSecretKey(), "ManagedSecretKey must be set on the token identifier before signing");
final byte[] identifierBytes = tokenIdentifier.getBytes();
- final byte[] password = secretKey.sign(identifierBytes);
+ final byte[] password = tokenIdentifier.sign(identifierBytes);
return new Token<>(identifierBytes, password, tokenIdentifier.getKind(), new Text(tokenIdentifier.getService()));
}
@@ -99,13 +91,6 @@ public String createSTSTokenString(String tempAccessKeyId, String originalAccess
final Instant creationTime = clock.instant();
final Instant expiration = creationTime.plusSeconds(durationSeconds);
- // 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
- // serialized there.
final STSTokenIdentifier identifier = new STSTokenIdentifier(STSTokenIdentifier.Params.newBuilder()
.setTempAccessKeyId(tempAccessKeyId)
.setOriginalAccessKeyId(originalAccessKeyId)
@@ -114,11 +99,10 @@ public String createSTSTokenString(String tempAccessKeyId, String originalAccess
.setExpiry(expiration)
.setSecretAccessKey(secretAccessKey)
.setSessionPolicy(sessionPolicy)
- .setEncryptionKey(encryptionKey)
+ .setManagedSecretKey(secretKeyClient.getCurrentSecretKey())
.build());
- identifier.setSecretKeyId(secretKey.getId());
- final Token token = generateToken(identifier, secretKey);
+ final Token token = generateToken(identifier);
return token.encodeToUrlString();
}
}
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 814f79b1e84b..f9d641f60b75 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
@@ -36,8 +36,11 @@
import java.io.IOException;
import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
import java.util.UUID;
-import java.util.concurrent.ThreadLocalRandom;
+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.utils.db.InMemoryTestTable;
import org.apache.hadoop.hdds.utils.db.Table;
@@ -57,14 +60,10 @@
* Tests for STS revocation handling in {@link S3SecurityUtil}.
*/
public class TestS3SecurityUtil {
- private static final byte[] ENCRYPTION_KEY = new byte[5];
+ private static final ManagedSecretKey MANAGED_SECRET_KEY = createManagedSecretKey();
private static final MockClock CLOCK = MockClock.newInstance();
private static final String TEMP_ACCESS_KEY_ID = "temp-access-key-id";
- {
- ThreadLocalRandom.current().nextBytes(ENCRYPTION_KEY);
- }
-
@Test
public void testValidateS3CredentialFailsWhenTokenCreatedBeforeRevocationCutoff() throws Exception {
validateS3CredentialHelper(
@@ -252,10 +251,22 @@ private STSTokenIdentifier createSTSTokenIdentifier() {
.setExpiry(CLOCK.instant().plusSeconds(3600))
.setSecretAccessKey("secret-access-key")
.setSessionPolicy("session-policy")
- .setEncryptionKey(ENCRYPTION_KEY)
+ .setManagedSecretKey(MANAGED_SECRET_KEY)
.build());
}
+ private static ManagedSecretKey createManagedSecretKey() {
+ final byte[] keyBytes = new byte[32];
+ for (int i = 0; i < keyBytes.length; i++) {
+ keyBytes[i] = (byte) i;
+ }
+ return new ManagedSecretKey(
+ UUID.randomUUID(),
+ Instant.EPOCH,
+ Instant.EPOCH.plus(Duration.ofDays(1)),
+ new SecretKeySpec(keyBytes, "HmacSHA256"));
+ }
+
private static OMRequest createRequestWithSessionToken(String accessId, boolean includeAccessId) {
final S3Authentication.Builder s3AuthenticationBuilder = S3Authentication.newBuilder()
.setStringToSign("string-to-sign")
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 594ac858bfb8..290d848535a8 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
@@ -30,7 +30,6 @@
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.UUID;
-import java.util.concurrent.ThreadLocalRandom;
import org.apache.hadoop.hdds.security.exception.SCMSecurityException;
import org.apache.hadoop.hdds.security.symmetric.ManagedSecretKey;
import org.apache.hadoop.hdds.security.symmetric.SecretKeyClient;
@@ -55,17 +54,12 @@ public class TestSTSSecurityUtil {
private static final String SECRET_ACCESS_KEY = "test-secret-access-key";
private static final String SESSION_POLICY = "test-session-policy";
private static final int DURATION_SECONDS = 3600;
- private static final byte[] ENCRYPTION_KEY = new byte[5];
-
+ private static final ManagedSecretKey MANAGED_SECRET_KEY = new SecretKeyTestClient().getCurrentSecretKey();
private final SecretKeyTestClient secretKeyClient = new SecretKeyTestClient();
private final STSTokenSecretManager tokenSecretManager = new STSTokenSecretManager(secretKeyClient);
private final UUID secretKeyId = secretKeyClient.getCurrentSecretKey().getId();
private final MockClock clock = new MockClock(Instant.ofEpochMilli(1764819000), ZoneOffset.UTC);
- {
- ThreadLocalRandom.current().nextBytes(ENCRYPTION_KEY);
- }
-
@Test
public void testConstructValidateAndDecryptSTSTokenInvalidProtobuf() throws IOException {
// Create a token whose identifier bytes are not a valid OMTokenProto
@@ -477,6 +471,6 @@ private STSTokenIdentifier.Params.Builder paramsBuilder() {
.setExpiry(clock.instant().plusSeconds(DURATION_SECONDS))
.setSecretAccessKey(SECRET_ACCESS_KEY)
.setSessionPolicy(SESSION_POLICY)
- .setEncryptionKey(ENCRYPTION_KEY);
+ .setManagedSecretKey(MANAGED_SECRET_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 870e99c3da18..268e672a38fb 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
@@ -23,12 +23,14 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.nio.charset.StandardCharsets;
+import java.time.Duration;
import java.time.Instant;
import java.util.Base64;
import java.util.UUID;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
+import org.apache.hadoop.hdds.security.symmetric.ManagedSecretKey;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
import org.apache.hadoop.ozone.security.STSTokenEncryption.STSTokenEncryptionException;
import org.junit.jupiter.api.BeforeAll;
@@ -43,11 +45,17 @@ public class TestSTSTokenEncryption {
private static final int HKDF_SALT_LENGTH = 16; // 128 bits
private static SecretKey sharedSecretKey;
+ private static ManagedSecretKey managedSecretKey;
@BeforeAll
public static void setUpClass() {
final byte[] keyBytes = "01234567890123456789012345678901".getBytes(StandardCharsets.US_ASCII);
sharedSecretKey = new SecretKeySpec(keyBytes, "HmacSHA256");
+ managedSecretKey = new ManagedSecretKey(
+ UUID.randomUUID(),
+ Instant.EPOCH,
+ Instant.EPOCH.plus(Duration.ofDays(1)),
+ sharedSecretKey);
}
@Test
@@ -69,8 +77,6 @@ public void testEncryptDecryptRoundTrip() throws Exception {
@Test
public void testSTSTokenIdentifierEncryption() throws Exception {
- final byte[] keyBytes = sharedSecretKey.getEncoded();
-
final String tempAccessKeyId = "ASIA123TEMPKEY";
final String originalAccessKeyId = "AKIA123ORIGINAL";
final String roleArn = "arn:aws:iam::123456789012:role/TestRole";
@@ -89,9 +95,8 @@ public void testSTSTokenIdentifierEncryption() throws Exception {
.setExpiry(expiry)
.setSecretAccessKey(secretAccessKey)
.setSessionPolicy(sessionPolicy)
- .setEncryptionKey(keyBytes)
+ .setManagedSecretKey(managedSecretKey)
.build());
- tokenId.setSecretKeyId(UUID.randomUUID());
// Convert to protobuf
final OzoneManagerProtocolProtos.OMTokenProto omTokenProto = tokenId.toProtoBuf();
@@ -100,7 +105,7 @@ public void testSTSTokenIdentifierEncryption() throws Exception {
// Create new token identifier from protobuf with decryption key
final STSTokenIdentifier decodedTokenId = new STSTokenIdentifier();
- decodedTokenId.setEncryptionKey(keyBytes);
+ decodedTokenId.setManagedSecretKey(managedSecretKey);
decodedTokenId.readFromByteArray(protobufBytes);
// Verify all fields are correctly decrypted
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 4ff08087e9fe..c2136388e2a0 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
@@ -24,11 +24,13 @@
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
-import java.security.SecureRandom;
+import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
+import javax.crypto.spec.SecretKeySpec;
+import org.apache.hadoop.hdds.security.symmetric.ManagedSecretKey;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto;
import org.junit.jupiter.api.Test;
@@ -37,11 +39,13 @@
*/
public class TestSTSTokenIdentifier {
- private static final byte[] ENCRYPTION_KEY = new byte[5];
+ private static final byte[] SECRET_KEY_BYTES = new byte[5];
+ private static final ManagedSecretKey MANAGED_SECRET_KEY;
private static final Instant CREATION_TIME = Instant.ofEpochMilli(1_700_000_000_000L);
- {
- ThreadLocalRandom.current().nextBytes(ENCRYPTION_KEY);
+ static {
+ ThreadLocalRandom.current().nextBytes(SECRET_KEY_BYTES);
+ MANAGED_SECRET_KEY = createManagedSecretKey(SECRET_KEY_BYTES);
}
@Test
@@ -72,9 +76,9 @@ public void testProtoBufRoundTrip() throws IOException {
.setExpiry(expiry)
.setSecretAccessKey("secretKey")
.setSessionPolicy("sessionPolicy")
+ .setManagedSecretKey(MANAGED_SECRET_KEY)
.build());
- final UUID secretKeyId = UUID.randomUUID();
- originalTokenIdentifier.setSecretKeyId(secretKeyId);
+ final UUID secretKeyId = MANAGED_SECRET_KEY.getId();
final OMTokenProto proto = originalTokenIdentifier.toProtoBuf();
assertThat(proto.getType()).isEqualTo(OMTokenProto.Type.S3_STS_TOKEN);
@@ -88,7 +92,7 @@ public void testProtoBufRoundTrip() throws IOException {
assertThat(proto.getSecretKeyId()).isEqualTo(secretKeyId.toString());
final STSTokenIdentifier parsedTokenIdentifier = new STSTokenIdentifier();
- parsedTokenIdentifier.setEncryptionKey(ENCRYPTION_KEY);
+ parsedTokenIdentifier.setManagedSecretKey(MANAGED_SECRET_KEY);
parsedTokenIdentifier.fromProtoBuf(proto);
assertThat(parsedTokenIdentifier.getOwnerId()).isEqualTo("tempAccess");
@@ -134,15 +138,14 @@ public void testProtobufRoundTripWithNullSessionPolicy() throws IOException {
.setRoleArn("arn:aws:iam::123456789012:role/RoleX")
.setExpiry(expiry)
.setSecretAccessKey("secretKey")
+ .setManagedSecretKey(MANAGED_SECRET_KEY)
.build());
- final UUID secretKeyId = UUID.randomUUID();
- stsTokenIdentifier.setSecretKeyId(secretKeyId);
final OMTokenProto proto = stsTokenIdentifier.toProtoBuf();
assertThat(proto.getSessionPolicy()).isEmpty();
final STSTokenIdentifier parsedTokenIdentifier = new STSTokenIdentifier();
- parsedTokenIdentifier.setEncryptionKey(ENCRYPTION_KEY);
+ parsedTokenIdentifier.setManagedSecretKey(MANAGED_SECRET_KEY);
parsedTokenIdentifier.fromProtoBuf(proto);
assertThat(parsedTokenIdentifier.getSessionPolicy()).isEmpty();
@@ -158,15 +161,14 @@ public void testProtobufRoundTripWithEmptySessionPolicy() throws IOException {
.setExpiry(expiry)
.setSecretAccessKey("secretKey")
.setSessionPolicy("")
+ .setManagedSecretKey(MANAGED_SECRET_KEY)
.build());
- final UUID secretKeyId = UUID.randomUUID();
- stsTokenIdentifier.setSecretKeyId(secretKeyId);
final OMTokenProto proto = stsTokenIdentifier.toProtoBuf();
assertThat(proto.getSessionPolicy()).isEmpty();
final STSTokenIdentifier parsedTokenIdentifier = new STSTokenIdentifier();
- parsedTokenIdentifier.setEncryptionKey(ENCRYPTION_KEY);
+ parsedTokenIdentifier.setManagedSecretKey(MANAGED_SECRET_KEY);
parsedTokenIdentifier.fromProtoBuf(proto);
assertThat(parsedTokenIdentifier.getSessionPolicy()).isEmpty();
@@ -208,8 +210,8 @@ public void testWriteToAndReadFromByteArray() throws Exception {
.setExpiry(expiry)
.setSecretAccessKey("secretAccessKey")
.setSessionPolicy("sessionPolicy")
+ .setManagedSecretKey(MANAGED_SECRET_KEY)
.build());
- originalTokenIdentifier.setSecretKeyId(UUID.randomUUID());
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (DataOutputStream out = new DataOutputStream(baos)) {
@@ -218,20 +220,14 @@ public void testWriteToAndReadFromByteArray() throws Exception {
final byte[] bytes = baos.toByteArray();
final STSTokenIdentifier parsedTokenIdentifier = new STSTokenIdentifier();
- parsedTokenIdentifier.setEncryptionKey(ENCRYPTION_KEY);
+ parsedTokenIdentifier.setManagedSecretKey(MANAGED_SECRET_KEY);
parsedTokenIdentifier.readFromByteArray(bytes);
assertThat(parsedTokenIdentifier).isEqualTo(originalTokenIdentifier);
}
@Test
- public void testWriteToAndReadFromByteArrayWithDifferentSecretKeyIds() throws Exception {
- final UUID uuid1 = UUID.randomUUID();
- UUID uuid2 = UUID.randomUUID();
- if (uuid2.equals(uuid1)) {
- uuid2 = UUID.randomUUID();
- }
-
+ public void testWriteToAndReadFromByteArrayWithDifferentSecretKeys() throws Exception {
final Instant expiry = Instant.now().plusSeconds(1500);
final STSTokenIdentifier originalTokenIdentifier = new STSTokenIdentifier(paramsBuilder()
.setTempAccessKeyId("tempAccessKeyId")
@@ -240,14 +236,16 @@ public void testWriteToAndReadFromByteArrayWithDifferentSecretKeyIds() throws Ex
.setExpiry(expiry)
.setSecretAccessKey("secretAccessKey")
.setSessionPolicy("sessionPolicy")
+ .setManagedSecretKey(MANAGED_SECRET_KEY)
.build());
- originalTokenIdentifier.setSecretKeyId(uuid1);
final ByteArrayOutputStream baos1 = new ByteArrayOutputStream();
try (DataOutputStream out = new DataOutputStream(baos1)) {
originalTokenIdentifier.write(out);
}
+ byte[] rawBytes = new byte[5];
+ ManagedSecretKey managedSecretKey2 = createManagedSecretKey(rawBytes);
final STSTokenIdentifier anotherTokenIdentifier = new STSTokenIdentifier(paramsBuilder()
.setTempAccessKeyId("tempAccessKeyId")
.setOriginalAccessKeyId("originalAccessKeyId")
@@ -255,8 +253,8 @@ public void testWriteToAndReadFromByteArrayWithDifferentSecretKeyIds() throws Ex
.setExpiry(expiry)
.setSecretAccessKey("secretAccessKey")
.setSessionPolicy("sessionPolicy")
+ .setManagedSecretKey(managedSecretKey2)
.build());
- anotherTokenIdentifier.setSecretKeyId(uuid2);
final ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
try (DataOutputStream out = new DataOutputStream(baos2)) {
@@ -270,17 +268,16 @@ public void testWriteToAndReadFromByteArrayWithDifferentSecretKeyIds() throws Ex
final byte[] byteArr2 = baos2.toByteArray();
assertThat(byteArr1).isNotEqualTo(byteArr2);
final STSTokenIdentifier tokenFromByteArr1 = new STSTokenIdentifier();
- tokenFromByteArr1.setEncryptionKey(ENCRYPTION_KEY);
+ tokenFromByteArr1.setManagedSecretKey(MANAGED_SECRET_KEY);
tokenFromByteArr1.readFromByteArray(byteArr1);
final STSTokenIdentifier tokenFromByteArr2 = new STSTokenIdentifier();
- tokenFromByteArr2.setEncryptionKey(ENCRYPTION_KEY);
+ tokenFromByteArr2.setManagedSecretKey(managedSecretKey2);
tokenFromByteArr2.readFromByteArray(byteArr2);
assertThat(tokenFromByteArr1).isNotEqualTo(tokenFromByteArr2);
}
@Test
public void testWriteToAndReadFromByteArrayWithSameSecretKeyIds() throws Exception {
- final UUID uuid = UUID.randomUUID();
final Instant expiry = Instant.now().plusSeconds(1700);
final STSTokenIdentifier originalTokenIdentifier = new STSTokenIdentifier(paramsBuilder()
@@ -290,8 +287,8 @@ public void testWriteToAndReadFromByteArrayWithSameSecretKeyIds() throws Excepti
.setExpiry(expiry)
.setSecretAccessKey("secretAccessKey")
.setSessionPolicy("sessionPolicy")
+ .setManagedSecretKey(MANAGED_SECRET_KEY)
.build());
- originalTokenIdentifier.setSecretKeyId(uuid);
final ByteArrayOutputStream baos1 = new ByteArrayOutputStream();
try (DataOutputStream out = new DataOutputStream(baos1)) {
@@ -305,8 +302,8 @@ public void testWriteToAndReadFromByteArrayWithSameSecretKeyIds() throws Excepti
.setExpiry(expiry)
.setSecretAccessKey("secretAccessKey")
.setSessionPolicy("sessionPolicy")
+ .setManagedSecretKey(MANAGED_SECRET_KEY)
.build());
- anotherTokenIdentifier.setSecretKeyId(uuid);
final ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
try (DataOutputStream out = new DataOutputStream(baos2)) {
@@ -319,10 +316,10 @@ public void testWriteToAndReadFromByteArrayWithSameSecretKeyIds() throws Excepti
final byte[] byteArr2 = baos2.toByteArray();
assertThat(byteArr1).isNotEqualTo(byteArr2);
final STSTokenIdentifier tokenFromByteArr1 = new STSTokenIdentifier();
- tokenFromByteArr1.setEncryptionKey(ENCRYPTION_KEY);
+ tokenFromByteArr1.setManagedSecretKey(MANAGED_SECRET_KEY);
tokenFromByteArr1.readFromByteArray(byteArr1);
final STSTokenIdentifier tokenFromByteArr2 = new STSTokenIdentifier();
- tokenFromByteArr2.setEncryptionKey(ENCRYPTION_KEY);
+ tokenFromByteArr2.setManagedSecretKey(MANAGED_SECRET_KEY);
tokenFromByteArr2.readFromByteArray(byteArr2);
assertThat(tokenFromByteArr1).isEqualTo(tokenFromByteArr2);
}
@@ -571,7 +568,7 @@ public void testNotEqualsWithNull() {
}
@Test
- public void testEqualsWithDifferentEncryptionKeys() {
+ public void testEqualsWithDifferentManagedSecretKeys() {
final Instant expiry = Instant.now().plusSeconds(3600).truncatedTo(ChronoUnit.MILLIS);
final UUID uuid = UUID.randomUUID();
@@ -586,9 +583,9 @@ public void testEqualsWithDifferentEncryptionKeys() {
.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);
+ // Create second identifier with a different ManagedSecretKey but otherwise same parameters
+ byte[] differentKeyBytes = new byte[5];
+ ThreadLocalRandom.current().nextBytes(differentKeyBytes);
final STSTokenIdentifier stsTokenIdentifier2 = new STSTokenIdentifier(paramsBuilder()
.setTempAccessKeyId("tempAccessKeyId")
@@ -597,18 +594,26 @@ public void testEqualsWithDifferentEncryptionKeys() {
.setExpiry(expiry)
.setSecretAccessKey("secretAccessKey")
.setSessionPolicy("sessionPolicy")
- .setEncryptionKey(differentKey)
+ .setManagedSecretKey(createManagedSecretKey(differentKeyBytes))
.build());
stsTokenIdentifier2.setSecretKeyId(uuid);
- // They should still be equal because encryptionKey is transient/ignored for identity
+ // They should still be equal because managedSecretKey is transient/ignored for identity
assertThat(stsTokenIdentifier).isEqualTo(stsTokenIdentifier2);
assertThat(stsTokenIdentifier.hashCode()).isEqualTo(stsTokenIdentifier2.hashCode());
}
+ private static ManagedSecretKey createManagedSecretKey(byte[] keyBytes) {
+ return new ManagedSecretKey(
+ UUID.randomUUID(),
+ CREATION_TIME,
+ CREATION_TIME.plus(Duration.ofDays(1)),
+ new SecretKeySpec(keyBytes, "HmacSHA256"));
+ }
+
private static STSTokenIdentifier.Params.Builder paramsBuilder() {
return STSTokenIdentifier.Params.newBuilder()
.setCreationTime(CREATION_TIME)
- .setEncryptionKey(ENCRYPTION_KEY);
+ .setManagedSecretKey(MANAGED_SECRET_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 e2e28fd4e7d3..4408652dbb9d 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
@@ -93,7 +93,9 @@ public void testCreateSTSTokenStringContainsCorrectFields() throws IOException {
// Verify the token identifier fields
final STSTokenIdentifier identifier = new STSTokenIdentifier();
- identifier.setEncryptionKey(sharedSecretKey.getEncoded());
+ identifier.setManagedSecretKey(createManagedSecretKey(
+ UUID.fromString("00000000-0000-0000-0000-000000000000"),
+ sharedSecretKey.getEncoded(), Instant.now()));
identifier.readFromByteArray(token.getIdentifier());
final Instant expiration = identifier.getExpiry();
@@ -119,7 +121,9 @@ public void testCreateSTSTokenStringWithNullSessionPolicy() throws IOException {
token.decodeFromUrlString(tokenString);
final STSTokenIdentifier identifier = new STSTokenIdentifier();
- identifier.setEncryptionKey(sharedSecretKey.getEncoded());
+ identifier.setManagedSecretKey(createManagedSecretKey(
+ UUID.fromString("00000000-0000-0000-0000-000000000000"),
+ sharedSecretKey.getEncoded(), Instant.now()));
identifier.readFromByteArray(token.getIdentifier());
assertTrue(identifier.getSessionPolicy().isEmpty());
}