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/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
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..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,13 +67,16 @@ public class TestS3SecurityUtil {
ThreadLocalRandom.current().nextBytes(ENCRYPTION_KEY);
}
+ @AfterEach
+ public void tearDown() {
+ OzoneManager.setStsTokenIdentifier(null);
+ }
+
@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 +167,31 @@ 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(
+ 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 +218,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(
@@ -206,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);
@@ -221,18 +252,27 @@ 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());
}
}
}
}
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,11 +298,12 @@ 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;
private boolean includeAccessId = true;
+ private boolean awsSignatureValid = true;
private OMException.ResultCodes expectedResult = null;
private String expectedMessage = null;
@@ -277,9 +318,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;
}
@@ -306,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 d2033deabec1..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
@@ -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
@@ -274,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
@@ -303,7 +335,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 +363,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 +372,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 +381,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 +391,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 +400,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 +487,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/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 63e25fca628e..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();
}
/**
@@ -605,6 +605,12 @@ 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) {
+ final String originalAccessKeyId = s3Auth.getValidatedStsOriginalAccessKeyId();
+ if (StringUtils.isNotEmpty(originalAccessKeyId)) {
+ auditMap.put(OzoneConsts.S3_STS_ORIGINAL_ACCESS_KEY_ID, originalAccessKeyId);
+ }
+ }
AuditMessage.Builder builder = new AuditMessage.Builder()
.forOperation(op)
@@ -621,6 +627,7 @@ protected AuditMessage.Builder auditMessageFor(AuditAction op) {
}
protected AuditMessage.Builder auditMessageForSuccess(AuditAction op) {
+ resolveValidatedStsOriginalAccessKeyIdForAudit();
return auditMessageFor(op)
.withResult(AuditEventStatus.SUCCESS);
}
@@ -631,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;
@@ -702,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/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
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..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,29 +26,54 @@
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;
+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.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;
+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;
+import org.mockito.stubbing.Answer;
/**
* Tests the s3 EndpointBase class methods.
* Test methods of the EndpointBase.
*/
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
@@ -146,6 +171,93 @@ public void init() { }
assertFalse(endpointBase.isExpiredToken(new OMException(ResultCodes.INVALID_TOKEN)));
}
+ @Test
+ public void testAuditMessageIncludesValidatedStsOriginalAccessKeyId() 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());
+
+ 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
+ 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 +321,109 @@ 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 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);
+ }
+ }
+
}