This is an automated email from the ASF dual-hosted git repository.
ChenSammi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git
The following commit(s) were added to refs/heads/master by this push:
new 54c21025222 HDDS-16583. STS credential validation updates (#11305)
54c21025222 is described below
commit 54c2102522278c26ede7912c3a0328369c2275e9
Author: fmorg-git <[email protected]>
AuthorDate: Wed Sep 23 23:28:21 2026 -0700
HDDS-16583. STS credential validation updates (#11305)
---
.../src/main/proto/OmClientProtocol.proto | 3 +
.../ozone/om/ratis/OzoneManagerStateMachine.java | 22 +--
.../hadoop/ozone/om/request/OMClientRequest.java | 45 +------
.../hadoop/ozone/security/S3SecurityUtil.java | 7 +
.../hadoop/ozone/security/STSSecurityUtil.java | 149 ++++++++++++++++++---
.../om/ratis/TestOzoneManagerStateMachine.java | 79 +++++++++--
.../request/TestOMClientRequestWithUserInfo.java | 8 ++
.../hadoop/ozone/security/TestS3SecurityUtil.java | 55 ++++++++
.../hadoop/ozone/security/TestSTSSecurityUtil.java | 110 ++++++++++++++-
9 files changed, 378 insertions(+), 100 deletions(-)
diff --git
a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
index bec4a40810f..75aedd475ea 100644
--- a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
+++ b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
@@ -2435,6 +2435,9 @@ message S3Authentication {
// S3 action without the s3: prefix for this request (e.g. GetObject), set
by S3 Gateway for use
// in finer-grained STS permissions.
optional string s3Action = 10;
+ // Resolved so GetCallerIdentity can return the assumed-role identity from
the session token.
+ optional string resolvedStsAssumedRoleId = 11;
+ optional string resolvedStsAssumedRoleUserArn = 12;
}
message RecoverLeaseRequest {
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/OzoneManagerStateMachine.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/OzoneManagerStateMachine.java
index c80a7977a1f..8353df01fb1 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/OzoneManagerStateMachine.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/OzoneManagerStateMachine.java
@@ -24,7 +24,6 @@
import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.io.IOException;
-import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
@@ -711,24 +710,9 @@ OMResponse runCommand(OMRequest request, TermIndex
termIndex) {
OzoneManager.setS3Auth(s3Auth);
isS3AuthThreadLocalSet = true;
- if (s3Auth.hasSessionToken() && !s3Auth.getSessionToken().isEmpty()) {
- // ThreadLocal carries session policy for OmMetadataReader
- // Use Instant.MAX for creationTime so a future revocation check on
this ThreadLocal
- // identifier never treats the token as issued before a stored
cutoff.
- final STSTokenIdentifier rehydratedTokenIdentifier = new
STSTokenIdentifier(
- STSTokenIdentifier.Params.newBuilder()
- .setTempAccessKeyId(
- s3Auth.hasResolvedStsTempAccessKeyId() ?
s3Auth.getResolvedStsTempAccessKeyId() : "")
- .setOriginalAccessKeyId(
- s3Auth.hasResolvedStsOriginalAccessKeyId() ?
s3Auth.getResolvedStsOriginalAccessKeyId() : "")
- .setRoleArn(s3Auth.hasResolvedStsRoleArn() ?
s3Auth.getResolvedStsRoleArn() : "")
- .setCreationTime(Instant.MAX)
- .setExpiry(Instant.MAX) // ensure it deterministically is
not expired
- .setSecretAccessKey(null) // no secretAccessKey needed
- .setSessionPolicy(
- s3Auth.hasResolvedStsSessionPolicy() ?
s3Auth.getResolvedStsSessionPolicy() : "")
- .setManagedSecretKey(null) // no ManagedSecretKey needed
- .build());
+ // ThreadLocal carries session policy for OmMetadataReader
+ final STSTokenIdentifier rehydratedTokenIdentifier =
STSSecurityUtil.rehydrateStsTokenIdentifier(s3Auth);
+ if (rehydratedTokenIdentifier != null) {
OzoneManager.setStsTokenIdentifier(rehydratedTokenIdentifier);
isStsThreadLocalSet = true;
}
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java
index a3da6ab7e6e..b6cca710d78 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java
@@ -27,7 +27,6 @@
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
-import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.hdds.utils.TransactionInfo;
import org.apache.hadoop.ipc_.ProtobufRpcEngine;
@@ -56,7 +55,7 @@
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.LayoutVersion;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
-import org.apache.hadoop.ozone.security.STSTokenIdentifier;
+import org.apache.hadoop.ozone.security.STSSecurityUtil;
import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer;
import org.apache.hadoop.ozone.security.acl.OzoneObj;
import org.apache.hadoop.ozone.security.acl.OzoneObjInfo;
@@ -129,52 +128,14 @@ public OMRequest preExecute(OzoneManager ozoneManager)
}
if (requestBuilder.hasS3Authentication()) {
- final OzoneManagerProtocolProtos.S3Authentication s3Auth =
requestBuilder.getS3Authentication();
- final boolean hasSessionToken = s3Auth.hasSessionToken() &&
!s3Auth.getSessionToken().isEmpty();
- final STSTokenIdentifier stsTokenIdentifier =
OzoneManager.getStsTokenIdentifier();
-
- // This should not happen, so explicitly throw an error. An existing
sessionToken
- // implies prior STS validation must have populated the ThreadLocal.
- if (ozoneManager.isSecurityEnabled() && hasSessionToken &&
stsTokenIdentifier == null) {
- throw new OMException(
- "S3Authentication has session token but no STS token identifier in
OzoneManager ThreadLocal",
- OMException.ResultCodes.INVALID_REQUEST);
- }
-
- requestBuilder.setS3Authentication(resolveS3Authentication(s3Auth,
stsTokenIdentifier));
+ requestBuilder.setS3Authentication(
+
STSSecurityUtil.resolveS3Authentication(requestBuilder.getS3Authentication(),
ozoneManager));
}
omRequest = requestBuilder.build();
return omRequest;
}
- private static OzoneManagerProtocolProtos.S3Authentication
resolveS3Authentication(
- OzoneManagerProtocolProtos.S3Authentication s3Auth, STSTokenIdentifier
stsTokenIdentifier) {
- final OzoneManagerProtocolProtos.S3Authentication.Builder s3AuthBuilder =
s3Auth.toBuilder();
-
- if (s3Auth.hasSessionToken() && !s3Auth.getSessionToken().isEmpty() &&
stsTokenIdentifier != null) {
- s3AuthBuilder.setResolvedStsSessionPolicy(
- StringUtils.defaultString(stsTokenIdentifier.getSessionPolicy()));
- s3AuthBuilder.setResolvedStsRoleArn(
- StringUtils.defaultString(stsTokenIdentifier.getRoleArn()));
- s3AuthBuilder.setResolvedStsOriginalAccessKeyId(
-
StringUtils.defaultString(stsTokenIdentifier.getOriginalAccessKeyId()));
- s3AuthBuilder.setResolvedStsTempAccessKeyId(
- StringUtils.defaultString(stsTokenIdentifier.getTempAccessKeyId()));
- final UUID secretKeyId = stsTokenIdentifier.getSecretKeyId();
- s3AuthBuilder.setResolvedStsSecretKeyId(
- secretKeyId != null ? secretKeyId.toString() : "");
- } else {
- s3AuthBuilder.clearResolvedStsSessionPolicy();
- s3AuthBuilder.clearResolvedStsRoleArn();
- s3AuthBuilder.clearResolvedStsOriginalAccessKeyId();
- s3AuthBuilder.clearResolvedStsTempAccessKeyId();
- s3AuthBuilder.clearResolvedStsSecretKeyId();
- }
-
- return s3AuthBuilder.build();
- }
-
/**
* Performs any request specific failure handling during request
* submission. An example of this would be an undo of any steps
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 6612fff2bad..883e9b38edf 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
@@ -36,6 +36,7 @@
import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.exceptions.OMLeaderNotReadyException;
import org.apache.hadoop.ozone.om.exceptions.OMNotLeaderException;
+import org.apache.hadoop.ozone.om.ratis.utils.OzoneManagerRatisUtils;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.S3Authentication;
import
org.apache.hadoop.ozone.protocolPB.OzoneManagerProtocolServerSideTranslatorPB;
@@ -71,6 +72,12 @@ public static void validateS3Credential(OMRequest omRequest,
if (omRequest.getS3Authentication().hasSessionToken()) {
final String token = omRequest.getS3Authentication().getSessionToken();
if (!token.isEmpty()) {
+ // The revocation checks below read this OM's local metadata, and
OzoneManagerProtocolServerSideTranslatorPB
+ // skips its own leader check for any request carrying S3
authentication. A follower can be arbitrarily
+ // behind the leader, so it may not yet know that a token, or the
principal that created it, was revoked.
+ // The non-STS path below gets this check from
OzoneDelegationTokenSecretManager.retrievePassword.
+ OzoneManagerRatisUtils.checkLeaderStatus(ozoneManager);
+
final STSTokenIdentifier stsTokenIdentifier =
STSSecurityUtil.constructValidateAndDecryptSTSToken(
token, ozoneManager.getSecretKeyClient(), CLOCK);
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 8862f16d235..5f4c23e37cc 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
@@ -17,6 +17,7 @@
package org.apache.hadoop.ozone.security;
+import static
org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_REQUEST;
import static
org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_TOKEN;
import static
org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.TOKEN_EXPIRED;
@@ -24,14 +25,16 @@
import com.google.protobuf.InvalidProtocolBufferException;
import java.io.IOException;
import java.time.Clock;
+import java.time.Instant;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.hdds.annotation.InterfaceAudience;
import org.apache.hadoop.hdds.annotation.InterfaceStability;
import org.apache.hadoop.hdds.security.symmetric.ManagedSecretKey;
import org.apache.hadoop.hdds.security.symmetric.SecretKeyClient;
+import org.apache.hadoop.ozone.om.OzoneManager;
import org.apache.hadoop.ozone.om.exceptions.OMException;
-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.OMTokenProto;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.S3Authentication;
import org.apache.hadoop.security.token.SecretManager;
@@ -97,11 +100,18 @@ private static STSTokenIdentifier
verifyAndDecryptToken(Token<STSTokenIdentifier
throw new SecretManager.InvalidToken("Invalid STS token - secretKeyId
was not valid: " + proto.getSecretKeyId());
}
+ final ManagedSecretKey secretKey = getValidatedSecretKey(secretKeyId,
secretKeyClient);
+
+ // Verify the signature over the identifier bytes before anything is
decrypted or parsed out of them, so
+ // that only bytes this cluster actually issued are ever fed to the
cipher. Keeping this first also means a
+ // forged token always fails the same way instead of revealing which field
was rejected.
+ if (!secretKey.isValidSignature(tokenBytes, token.getPassword())) {
+ throw new SecretManager.InvalidToken("Invalid STS token - signature is
not correct");
+ }
+
final STSTokenIdentifier tokenId = new STSTokenIdentifier();
- final ManagedSecretKey secretKey;
+ tokenId.setManagedSecretKey(secretKey);
try {
- secretKey = getValidatedSecretKey(secretKeyId, secretKeyClient);
- tokenId.setManagedSecretKey(secretKey);
tokenId.readFromByteArray(tokenBytes);
} catch (OMException e) {
throw e;
@@ -117,11 +127,6 @@ private static STSTokenIdentifier
verifyAndDecryptToken(Token<STSTokenIdentifier
throw new OMException("Invalid STS token - token expired at " +
tokenId.getExpiry(), TOKEN_EXPIRED);
}
- // Verify token signature against the original identifier bytes
- if (!tokenId.isValidSignature(tokenBytes, token.getPassword())) {
- throw new SecretManager.InvalidToken("Invalid STS token - signature is
not correct for token: " + tokenId);
- }
-
return tokenId;
}
@@ -202,8 +207,7 @@ static void
ensureEssentialFieldsArePresentInToken(STSTokenIdentifier stsTokenId
* @param request OM request possibly containing S3 authentication
* @throws OMException if resolved fields and session token presence are
inconsistent
*/
- public static void
ensureResolvedStsFieldsInvariants(OzoneManagerProtocolProtos.OMRequest request)
- throws OMException {
+ public static void ensureResolvedStsFieldsInvariants(OMRequest request)
throws OMException {
if (!request.hasS3Authentication()) {
return;
}
@@ -213,23 +217,128 @@ public static void
ensureResolvedStsFieldsInvariants(OzoneManagerProtocolProtos.
if (!hasSessionToken) {
// If sessionToken is missing/empty, resolved fields must be empty.
- if (s3Auth.hasResolvedStsSessionPolicy() ||
s3Auth.hasResolvedStsRoleArn() ||
- s3Auth.hasResolvedStsOriginalAccessKeyId() ||
s3Auth.hasResolvedStsTempAccessKeyId() ||
- s3Auth.hasResolvedStsSecretKeyId()) {
+ if (hasAnyResolvedStsField(s3Auth)) {
throw new OMException("Resolved STS fields must be empty when
sessionToken is not present", INVALID_TOKEN);
}
return;
}
- ensureResolvedFieldsArePresent(s3Auth);
+ if (!hasAllResolvedStsFields(s3Auth)) {
+ throw new OMException("Resolved STS fields must be present when
sessionToken is present", INVALID_TOKEN);
+ }
+
+ if (hasPartialResolvedStsAssumedRoleFields(s3Auth)) {
+ throw new OMException(
+ "Resolved STS assumed-role fields must both be present or both be
absent when sessionToken is present",
+ INVALID_TOKEN);
+ }
}
- private static void ensureResolvedFieldsArePresent(S3Authentication s3Auth)
throws OMException {
- if (!s3Auth.hasResolvedStsSessionPolicy() ||
!s3Auth.hasResolvedStsRoleArn() ||
- !s3Auth.hasResolvedStsOriginalAccessKeyId() ||
!s3Auth.hasResolvedStsTempAccessKeyId() ||
- !s3Auth.hasResolvedStsSecretKeyId()) {
- throw new OMException("Resolved STS fields must be present when
sessionToken is present", INVALID_TOKEN);
+ private static boolean hasAnyResolvedStsField(S3Authentication s3Auth) {
+ return s3Auth.hasResolvedStsSessionPolicy() ||
s3Auth.hasResolvedStsRoleArn() ||
+ s3Auth.hasResolvedStsOriginalAccessKeyId() ||
s3Auth.hasResolvedStsTempAccessKeyId() ||
+ s3Auth.hasResolvedStsSecretKeyId() ||
s3Auth.hasResolvedStsAssumedRoleId() ||
+ s3Auth.hasResolvedStsAssumedRoleUserArn();
+ }
+
+ private static boolean hasAllResolvedStsFields(S3Authentication s3Auth) {
+ // Assumed-role id/ARN are optional here: they are used for
GetCallerIdentity, not apply-path ACL,
+ // and may be absent in Ratis log entries committed by a pre-upgrade OM
leader during rolling upgrade.
+ return s3Auth.hasResolvedStsSessionPolicy() &&
s3Auth.hasResolvedStsRoleArn() &&
+ s3Auth.hasResolvedStsOriginalAccessKeyId() &&
s3Auth.hasResolvedStsTempAccessKeyId() &&
+ s3Auth.hasResolvedStsSecretKeyId();
+ }
+
+ private static boolean
hasPartialResolvedStsAssumedRoleFields(S3Authentication s3Auth) {
+ return s3Auth.hasResolvedStsAssumedRoleId() !=
s3Auth.hasResolvedStsAssumedRoleUserArn();
+ }
+
+ /**
+ * Copies the STS state that {@link S3SecurityUtil#validateS3Credential} put
in the {@link OzoneManager}
+ * thread local onto {@code s3Auth}, or clears the resolved fields when the
request has no session token.
+ *
+ * <p>The resolved fields are the only STS state visible to the Ratis apply
thread, because the thread locals
+ * belong to the RPC handler thread and do not follow the request onto a
Ratis thread. Resolving here, where
+ * the token has just been verified, is what lets the apply thread rebuild
the request context without
+ * repeating any crypto.</p>
+ *
+ * <p>Returns {@code s3Auth} itself when there is nothing to resolve and
nothing to clear.</p>
+ *
+ * @throws OMException if a session token is present but the token
identifier is not, which means the
+ * request reached this point without passing STS validation
+ */
+ public static S3Authentication resolveS3Authentication(S3Authentication
s3Auth, OzoneManager ozoneManager)
+ throws OMException {
+ final boolean hasSessionToken = s3Auth.hasSessionToken() &&
!s3Auth.getSessionToken().isEmpty();
+ final STSTokenIdentifier stsTokenIdentifier =
OzoneManager.getStsTokenIdentifier();
+
+ // This should not happen, so explicitly throw an error. An existing
sessionToken
+ // implies prior STS validation must have populated the ThreadLocal.
+ if (ozoneManager.isSecurityEnabled() && hasSessionToken &&
stsTokenIdentifier == null) {
+ throw new OMException(
+ "S3Authentication has session token but no STS token identifier in
OzoneManager ThreadLocal",
+ INVALID_REQUEST);
+ }
+
+ if (!hasSessionToken || stsTokenIdentifier == null) {
+ return hasAnyResolvedStsField(s3Auth) ? clearResolvedStsFields(s3Auth) :
s3Auth;
+ }
+
+ final UUID secretKeyId = stsTokenIdentifier.getSecretKeyId();
+ return s3Auth.toBuilder()
+
.setResolvedStsSessionPolicy(StringUtils.defaultString(stsTokenIdentifier.getSessionPolicy()))
+
.setResolvedStsRoleArn(StringUtils.defaultString(stsTokenIdentifier.getRoleArn()))
+
.setResolvedStsOriginalAccessKeyId(StringUtils.defaultString(stsTokenIdentifier.getOriginalAccessKeyId()))
+
.setResolvedStsTempAccessKeyId(StringUtils.defaultString(stsTokenIdentifier.getTempAccessKeyId()))
+ .setResolvedStsSecretKeyId(secretKeyId != null ?
secretKeyId.toString() : "")
+
.setResolvedStsAssumedRoleId(StringUtils.defaultString(stsTokenIdentifier.getAssumedRoleId()))
+
.setResolvedStsAssumedRoleUserArn(StringUtils.defaultString(stsTokenIdentifier.getAssumedRoleUserArn()))
+ .build();
+ }
+
+ private static S3Authentication clearResolvedStsFields(S3Authentication
s3Auth) {
+ return s3Auth.toBuilder()
+ .clearResolvedStsSessionPolicy()
+ .clearResolvedStsRoleArn()
+ .clearResolvedStsOriginalAccessKeyId()
+ .clearResolvedStsTempAccessKeyId()
+ .clearResolvedStsSecretKeyId()
+ .clearResolvedStsAssumedRoleId()
+ .clearResolvedStsAssumedRoleUserArn()
+ .build();
+ }
+
+ /**
+ * Rebuilds the {@link STSTokenIdentifier} for a request from the resolved
fields written by
+ * {@link #resolveS3Authentication(S3Authentication, OzoneManager)}, or
returns null when the request has
+ * no session token.
+ *
+ * <p>Performs no crypto and does not contact {@link SecretKeyClient}: the
token was already verified on
+ * the leader RPC path, so only the resolved values are needed for
authorization on the apply thread.
+ * {@code creationTime} and {@code expiry} are set to {@link Instant#MAX} so
that a revocation or expiry
+ * check against this identifier is deterministic and never treats it as
issued before a stored cutoff.</p>
+ *
+ * <p>Call {@link #ensureResolvedStsFieldsInvariants(OMRequest)} first: it
guarantees the five core resolved
+ * fields (session policy, role ARN, original/temp access key IDs, secret
key ID) are present. Assumed-role
+ * id/ARN may be absent on legacy Ratis log entries; when present, both must
be set.</p>
+ */
+ public static STSTokenIdentifier
rehydrateStsTokenIdentifier(S3Authentication s3Auth) {
+ if (!s3Auth.hasSessionToken() || s3Auth.getSessionToken().isEmpty()) {
+ return null;
}
+
+ return new STSTokenIdentifier(STSTokenIdentifier.Params.newBuilder()
+ .setTempAccessKeyId(s3Auth.getResolvedStsTempAccessKeyId())
+ .setOriginalAccessKeyId(s3Auth.getResolvedStsOriginalAccessKeyId())
+ .setRoleArn(s3Auth.getResolvedStsRoleArn())
+ .setCreationTime(Instant.MAX)
+ .setExpiry(Instant.MAX)
+ .setSecretAccessKey(null) // no secretAccessKey needed
+ .setSessionPolicy(s3Auth.getResolvedStsSessionPolicy())
+ .setAssumedRoleId(s3Auth.getResolvedStsAssumedRoleId())
+ .setAssumedRoleUserArn(s3Auth.getResolvedStsAssumedRoleUserArn())
+ .setManagedSecretKey(null) // no ManagedSecretKey needed
+ .build());
}
}
diff --git
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/ratis/TestOzoneManagerStateMachine.java
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/ratis/TestOzoneManagerStateMachine.java
index 072bb84188e..40975d5af61 100644
---
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/ratis/TestOzoneManagerStateMachine.java
+++
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/ratis/TestOzoneManagerStateMachine.java
@@ -419,19 +419,8 @@ public void testRunCommandRuntimeExceptionTerminates()
throws Exception {
public void testRunCommandSetsAndClearsStsThreadLocal() throws Exception {
when(om.isSecurityEnabled()).thenReturn(true);
- final OzoneManagerProtocolProtos.S3Authentication s3Auth =
- OzoneManagerProtocolProtos.S3Authentication.newBuilder()
- .setAccessId("accessId")
- .setSessionToken("sessionToken")
- .setResolvedStsSessionPolicy("sessionPolicy")
- .setResolvedStsRoleArn("roleArn")
- .setResolvedStsOriginalAccessKeyId("originalAccessKeyId")
- .setResolvedStsTempAccessKeyId("tempAccessKeyId")
- .setResolvedStsSecretKeyId("secretKeyId")
- .build();
-
final OMRequest request = sampleWriteRequest().toBuilder()
- .setS3Authentication(s3Auth)
+ .setS3Authentication(stsS3Authentication())
.build();
final TermIndex ti = TermIndex.valueOf(1, 5);
@@ -452,6 +441,8 @@ public void testRunCommandSetsAndClearsStsThreadLocal()
throws Exception {
assertEquals("originalAccessKeyId",
OzoneManager.getStsTokenIdentifier().getOriginalAccessKeyId());
assertEquals("roleArn",
OzoneManager.getStsTokenIdentifier().getRoleArn());
assertEquals("sessionPolicy",
OzoneManager.getStsTokenIdentifier().getSessionPolicy());
+ assertEquals("assumedRoleId",
OzoneManager.getStsTokenIdentifier().getAssumedRoleId());
+ assertEquals("assumedRoleUserArn",
OzoneManager.getStsTokenIdentifier().getAssumedRoleUserArn());
return clientResponse;
}).when(handler).handleWriteRequest(eq(request), any(), eq(doubleBuffer));
@@ -464,6 +455,44 @@ public void testRunCommandSetsAndClearsStsThreadLocal()
throws Exception {
assertNull(OzoneManager.getStsTokenIdentifier(), "Expected STS ThreadLocal
to be cleared after runCommand");
}
+ @Test
+ public void testRunCommandSetsStsThreadLocalWithLegacyResolvedFields()
throws Exception {
+ when(om.isSecurityEnabled()).thenReturn(true);
+
+ final OMRequest request = sampleWriteRequest().toBuilder()
+ .setS3Authentication(legacyStsS3Authentication())
+ .build();
+ final TermIndex ti = TermIndex.valueOf(1, 5);
+
+ final OMResponse expectedResponse = OMResponse.newBuilder()
+ .setCmdType(Type.CreateKey)
+ .setStatus(Status.OK)
+ .setSuccess(true)
+ .build();
+
+ final OMClientResponse clientResponse = mock(OMClientResponse.class);
+ when(clientResponse.getOMResponse()).thenReturn(expectedResponse);
+ when(clientResponse.getOmLockDetails()).thenReturn(null);
+
+ doAnswer(invocation -> {
+ assertNotNull(OzoneManager.getStsTokenIdentifier(),
+ "Expected STS ThreadLocal to be set during
handler.handleWriteRequest");
+ assertEquals("tempAccessKeyId",
OzoneManager.getStsTokenIdentifier().getTempAccessKeyId());
+ assertEquals("originalAccessKeyId",
OzoneManager.getStsTokenIdentifier().getOriginalAccessKeyId());
+ assertEquals("roleArn",
OzoneManager.getStsTokenIdentifier().getRoleArn());
+ assertEquals("sessionPolicy",
OzoneManager.getStsTokenIdentifier().getSessionPolicy());
+ assertEquals("",
OzoneManager.getStsTokenIdentifier().getAssumedRoleId());
+ assertEquals("",
OzoneManager.getStsTokenIdentifier().getAssumedRoleUserArn());
+ return clientResponse;
+ }).when(handler).handleWriteRequest(eq(request), any(), eq(doubleBuffer));
+
+ OMResponse result = sm.runCommand(request, ti);
+
+ assertNotNull(result);
+ assertTrue(result.getSuccess());
+ assertNull(OzoneManager.getStsTokenIdentifier(), "Expected STS ThreadLocal
to be cleared after runCommand");
+ }
+
@Test
public void testRunCommandMissingResolvedStsFieldsReturnsErrorResponse()
throws Exception {
when(om.isSecurityEnabled()).thenReturn(true);
@@ -1288,6 +1317,32 @@ private OMRequest sampleReadRequest() {
.build();
}
+ private OzoneManagerProtocolProtos.S3Authentication stsS3Authentication() {
+ return OzoneManagerProtocolProtos.S3Authentication.newBuilder()
+ .setAccessId("accessId")
+ .setSessionToken("sessionToken")
+ .setResolvedStsSessionPolicy("sessionPolicy")
+ .setResolvedStsRoleArn("roleArn")
+ .setResolvedStsOriginalAccessKeyId("originalAccessKeyId")
+ .setResolvedStsTempAccessKeyId("tempAccessKeyId")
+ .setResolvedStsSecretKeyId("secretKeyId")
+ .setResolvedStsAssumedRoleId("assumedRoleId")
+ .setResolvedStsAssumedRoleUserArn("assumedRoleUserArn")
+ .build();
+ }
+
+ private OzoneManagerProtocolProtos.S3Authentication
legacyStsS3Authentication() {
+ return OzoneManagerProtocolProtos.S3Authentication.newBuilder()
+ .setAccessId("accessId")
+ .setSessionToken("sessionToken")
+ .setResolvedStsSessionPolicy("sessionPolicy")
+ .setResolvedStsRoleArn("roleArn")
+ .setResolvedStsOriginalAccessKeyId("originalAccessKeyId")
+ .setResolvedStsTempAccessKeyId("tempAccessKeyId")
+ .setResolvedStsSecretKeyId("secretKeyId")
+ .build();
+ }
+
private TransactionContext mockTrx(OMRequest request, long term, long index)
{
return mockTrx(request, term, index, request);
}
diff --git
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/TestOMClientRequestWithUserInfo.java
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/TestOMClientRequestWithUserInfo.java
index 3dd695ee8ff..7a0923eb0e0 100644
---
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/TestOMClientRequestWithUserInfo.java
+++
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/TestOMClientRequestWithUserInfo.java
@@ -237,6 +237,8 @@ public void testPreExecuteOverwritesResolvedStsFields()
throws Exception {
final String originalAccessKeyId = "AKIAORIGINAL";
final String roleArn = "arn:aws:iam::123456789012:role/test-role";
final String sessionPolicy = "test-session-policy";
+ final String assumedRoleId = "AROATEST123456789:testsess";
+ final String assumedRoleUserArn =
"arn:aws:sts::123456789012:assumed-role/test-role/testsess";
final UUID secretKeyId = UUID.randomUUID();
final STSTokenIdentifier stsTokenIdentifier =
mock(STSTokenIdentifier.class);
@@ -245,6 +247,8 @@ public void testPreExecuteOverwritesResolvedStsFields()
throws Exception {
when(stsTokenIdentifier.getOriginalAccessKeyId()).thenReturn(originalAccessKeyId);
when(stsTokenIdentifier.getTempAccessKeyId()).thenReturn(accessId);
when(stsTokenIdentifier.getSecretKeyId()).thenReturn(secretKeyId);
+ when(stsTokenIdentifier.getAssumedRoleId()).thenReturn(assumedRoleId);
+
when(stsTokenIdentifier.getAssumedRoleUserArn()).thenReturn(assumedRoleUserArn);
final S3Authentication s3Authentication = S3Authentication.newBuilder()
.setAccessId(accessId)
@@ -256,6 +260,8 @@ public void testPreExecuteOverwritesResolvedStsFields()
throws Exception {
.setResolvedStsOriginalAccessKeyId("client-original-access-key-id")
.setResolvedStsTempAccessKeyId("client-temp-access-key-id")
.setResolvedStsSecretKeyId("client-secret-key-id")
+ .setResolvedStsAssumedRoleId("client-assumed-role-id")
+ .setResolvedStsAssumedRoleUserArn("client-assumed-role-user-arn")
.build();
OzoneManager.setS3Auth(s3Authentication);
@@ -282,6 +288,8 @@ public void testPreExecuteOverwritesResolvedStsFields()
throws Exception {
assertEquals(originalAccessKeyId,
modifiedS3Auth.getResolvedStsOriginalAccessKeyId());
assertEquals(accessId, modifiedS3Auth.getResolvedStsTempAccessKeyId());
assertEquals(secretKeyId.toString(),
modifiedS3Auth.getResolvedStsSecretKeyId());
+ assertEquals(assumedRoleId,
modifiedS3Auth.getResolvedStsAssumedRoleId());
+ assertEquals(assumedRoleUserArn,
modifiedS3Auth.getResolvedStsAssumedRoleUserArn());
} finally {
OzoneManager.setStsTokenIdentifier(null);
OzoneManager.setS3Auth(null);
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 f9d641f60b7..21c24384859 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.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -31,9 +32,12 @@
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import com.google.protobuf.ServiceException;
import java.io.IOException;
import java.time.Clock;
import java.time.Duration;
@@ -49,6 +53,7 @@
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.exceptions.OMNotLeaderException;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.S3Authentication;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
@@ -175,6 +180,56 @@ public void
testValidateS3CredentialSuccessWhenTokenCreatedAtRevocationCutoff()
.setExpectedResult(null));
}
+ @Test
+ public void testValidateS3CredentialFailsWhenNotLeader() throws Exception {
+ try (OzoneManager ozoneManager = mock(OzoneManager.class)) {
+ when(ozoneManager.isSecurityEnabled()).thenReturn(true);
+ final OMNotLeaderException notLeaderException = new
OMNotLeaderException("om1 is not the leader");
+ doThrow(notLeaderException).when(ozoneManager).checkLeaderStatus();
+
+ try (MockedStatic<STSSecurityUtil> stsSecurityUtilMock =
mockStatic(STSSecurityUtil.class, CALLS_REAL_METHODS)) {
+ final OMRequest omRequest =
createRequestWithSessionToken(TEMP_ACCESS_KEY_ID, true);
+
+ final ServiceException serviceException = assertThrows(
+ ServiceException.class, () ->
S3SecurityUtil.validateS3Credential(omRequest, ozoneManager));
+ assertSame(notLeaderException, serviceException.getCause());
+
+ // A follower must reject before it reads its own revocation metadata,
which may be stale.
+ stsSecurityUtilMock.verify(
+ () ->
STSSecurityUtil.constructValidateAndDecryptSTSToken(anyString(),
any(SecretKeyClient.class),
+ any(Clock.class)),
+ never());
+ }
+ }
+ }
+
+ @Test
+ public void testValidateS3CredentialDoesNotCheckLeaderOutsideStsPath()
throws Exception {
+ try (OzoneManager ozoneManager = mock(OzoneManager.class)) {
+ when(ozoneManager.isSecurityEnabled()).thenReturn(true);
+ doThrow(new OMNotLeaderException("om1 is not the
leader")).when(ozoneManager).checkLeaderStatus();
+
+ final OzoneDelegationTokenSecretManager delegationTokenMgr =
mock(OzoneDelegationTokenSecretManager.class);
+
when(ozoneManager.getDelegationTokenMgr()).thenReturn(delegationTokenMgr);
+
+ final OMRequest omRequest = OMRequest.newBuilder()
+ .setClientId(UUID.randomUUID().toString())
+ .setCmdType(Type.CreateVolume)
+ .setS3Authentication(S3Authentication.newBuilder()
+ .setStringToSign("string-to-sign")
+ .setSignature("signature")
+ .setAccessId(TEMP_ACCESS_KEY_ID)
+ .build())
+ .build();
+
+ // Permanent S3 credentials get their leader check from
OzoneDelegationTokenSecretManager.retrievePassword,
+ // so validateS3Credential must not add a second one outside the session
token branch.
+ assertDoesNotThrow(() -> S3SecurityUtil.validateS3Credential(omRequest,
ozoneManager));
+ verify(ozoneManager, never()).checkLeaderStatus();
+
verify(delegationTokenMgr).retrievePassword(any(OzoneTokenIdentifier.class));
+ }
+ }
+
private void validateS3CredentialHelper(TestConfig config) throws Exception {
try (OzoneManager ozoneManager = mock(OzoneManager.class)) {
when(ozoneManager.isSecurityEnabled()).thenReturn(true);
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 769ae0da5f1..676a286111c 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
@@ -204,9 +204,7 @@ public void
testConstructValidateAndDecryptSTSTokenSecretKeyNotFound() throws Ex
assertThatThrownBy(() ->
STSSecurityUtil.constructValidateAndDecryptSTSToken(validTokenString,
mockKeyClient, clock))
.isInstanceOf(OMException.class)
- .hasMessage(
- "Invalid STS token format: Invalid STS token - could not
readFromByteArray: Secret key not found for " +
- "STS token secretKeyId: " + secretKeyId);
+ .hasMessage("Invalid STS token format: Secret key not found for STS
token secretKeyId: " + secretKeyId);
}
@Test
@@ -269,9 +267,7 @@ public void
testConstructValidateAndDecryptSTSTokenSecretKeyRetrievalException()
assertThatThrownBy(() ->
STSSecurityUtil.constructValidateAndDecryptSTSToken(validTokenString,
mockKeyClient, clock))
.isInstanceOf(OMException.class)
- .hasMessage(
- "Invalid STS token format: Invalid STS token - could not
readFromByteArray: Failed to retrieve secret " +
- "key: something went wrong");
+ .hasMessage("Invalid STS token format: Failed to retrieve secret key:
something went wrong");
}
@Test
@@ -293,7 +289,33 @@ public void
testConstructValidateAndDecryptSTSTokenInvalidSignature() throws Exc
assertThatThrownBy(() ->
STSSecurityUtil.constructValidateAndDecryptSTSToken(invalidTokenString,
secretKeyClient, clock))
.isInstanceOf(OMException.class)
- .hasMessageContaining("Invalid STS token format: Invalid STS token -
signature is not correct for token");
+ .hasMessage("Invalid STS token format: Invalid STS token - signature
is not correct");
+ }
+
+ @Test
+ public void
testConstructValidateAndDecryptSTSTokenRejectsTamperedIdentifierBeforeDecrypting()
throws Exception {
+ final String validTokenString = createStsTokenString();
+
+ final Token<STSTokenIdentifier> validToken = new Token<>();
+ validToken.decodeFromUrlString(validTokenString);
+
+ // Corrupt the encrypted secretAccessKey while keeping the original
signature. Decrypting these bytes would
+ // fail inside the cipher, so the signature check must reject them before
readFromByteArray is ever reached.
+ final OMTokenProto proto =
OMTokenProto.parseFrom(validToken.getIdentifier());
+ final byte[] tamperedSecret =
proto.getSecretAccessKey().getBytes(StandardCharsets.UTF_8);
+ tamperedSecret[tamperedSecret.length - 1] ^= 0x01;
+ final OMTokenProto tamperedProto = proto.toBuilder()
+ .setSecretAccessKey(new String(tamperedSecret, StandardCharsets.UTF_8))
+ .build();
+
+ final Token<STSTokenIdentifier> tamperedToken = new Token<>(
+ tamperedProto.toByteArray(), validToken.getPassword(),
validToken.getKind(), validToken.getService());
+
+ assertThatThrownBy(() ->
+
STSSecurityUtil.constructValidateAndDecryptSTSToken(tamperedToken.encodeToUrlString(),
secretKeyClient, clock))
+ .isInstanceOf(OMException.class)
+ .satisfies(exception -> assertThat(((OMException)
exception).getResult()).isEqualTo(INVALID_TOKEN))
+ .hasMessage("Invalid STS token format: Invalid STS token - signature
is not correct");
}
@Test
@@ -414,6 +436,8 @@ public void testEnsureResolvedStsFieldsInvariantsSuccess()
throws Exception {
.setResolvedStsOriginalAccessKeyId(ORIGINAL_ACCESS_KEY)
.setResolvedStsTempAccessKeyId(TEMP_ACCESS_KEY)
.setResolvedStsSecretKeyId(secretKeyId.toString())
+ .setResolvedStsAssumedRoleId(ASSUMED_ROLE_ID)
+ .setResolvedStsAssumedRoleUserArn(ASSUMED_ROLE_USER_ARN)
.build();
final OMRequest request = OMRequest.newBuilder()
@@ -425,6 +449,78 @@ public void testEnsureResolvedStsFieldsInvariantsSuccess()
throws Exception {
STSSecurityUtil.ensureResolvedStsFieldsInvariants(request);
}
+ @Test
+ public void
testEnsureResolvedStsFieldsInvariantsSuccessWithLegacyResolvedFields() throws
Exception {
+ final String tokenString = createStsTokenString();
+
+ final S3Authentication s3Auth = S3Authentication.newBuilder()
+ .setSessionToken(tokenString)
+ .setResolvedStsSessionPolicy(SESSION_POLICY)
+ .setResolvedStsRoleArn(ROLE_ARN)
+ .setResolvedStsOriginalAccessKeyId(ORIGINAL_ACCESS_KEY)
+ .setResolvedStsTempAccessKeyId(TEMP_ACCESS_KEY)
+ .setResolvedStsSecretKeyId(secretKeyId.toString())
+ .build();
+
+ final OMRequest request = OMRequest.newBuilder()
+ .setCmdType(Type.CreateBucket)
+ .setClientId("client-id")
+ .setS3Authentication(s3Auth)
+ .build();
+
+ STSSecurityUtil.ensureResolvedStsFieldsInvariants(request);
+ }
+
+ @Test
+ public void
testEnsureResolvedStsFieldsInvariantsRejectsPartialAssumedRoleFields() throws
Exception {
+ final String tokenString = createStsTokenString();
+
+ final S3Authentication s3Auth = S3Authentication.newBuilder()
+ .setSessionToken(tokenString)
+ .setResolvedStsSessionPolicy(SESSION_POLICY)
+ .setResolvedStsRoleArn(ROLE_ARN)
+ .setResolvedStsOriginalAccessKeyId(ORIGINAL_ACCESS_KEY)
+ .setResolvedStsTempAccessKeyId(TEMP_ACCESS_KEY)
+ .setResolvedStsSecretKeyId(secretKeyId.toString())
+ .setResolvedStsAssumedRoleId(ASSUMED_ROLE_ID)
+ .build();
+
+ final OMRequest request = OMRequest.newBuilder()
+ .setCmdType(Type.CreateBucket)
+ .setClientId("client-id")
+ .setS3Authentication(s3Auth)
+ .build();
+
+ assertThatThrownBy(() ->
STSSecurityUtil.ensureResolvedStsFieldsInvariants(request))
+ .isInstanceOf(OMException.class)
+ .hasMessageContaining("Resolved STS assumed-role fields must both be
present or both be absent");
+ }
+
+ @Test
+ public void
testEnsureResolvedStsFieldsInvariantsRejectsPartialAssumedRoleUserArnOnly()
throws Exception {
+ final String tokenString = createStsTokenString();
+
+ final S3Authentication s3Auth = S3Authentication.newBuilder()
+ .setSessionToken(tokenString)
+ .setResolvedStsSessionPolicy(SESSION_POLICY)
+ .setResolvedStsRoleArn(ROLE_ARN)
+ .setResolvedStsOriginalAccessKeyId(ORIGINAL_ACCESS_KEY)
+ .setResolvedStsTempAccessKeyId(TEMP_ACCESS_KEY)
+ .setResolvedStsSecretKeyId(secretKeyId.toString())
+ .setResolvedStsAssumedRoleUserArn(ASSUMED_ROLE_USER_ARN)
+ .build();
+
+ final OMRequest request = OMRequest.newBuilder()
+ .setCmdType(Type.CreateBucket)
+ .setClientId("client-id")
+ .setS3Authentication(s3Auth)
+ .build();
+
+ assertThatThrownBy(() ->
STSSecurityUtil.ensureResolvedStsFieldsInvariants(request))
+ .isInstanceOf(OMException.class)
+ .hasMessageContaining("Resolved STS assumed-role fields must both be
present or both be absent");
+ }
+
@Test
public void testEnsureResolvedStsFieldsInvariantsMissingSessionToken() {
final S3Authentication s3Auth = S3Authentication.newBuilder()
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]