This is an automated email from the ASF dual-hosted git repository.
sodonnell pushed a commit to branch HDDS-13323-sts
in repository https://gitbox.apache.org/repos/asf/ozone.git
The following commit(s) were added to refs/heads/HDDS-13323-sts by this push:
new 36852379d5c HDDS-13997. [STS] Plumbing for passing STS token through
S3 api processing (#9372)
36852379d5c is described below
commit 36852379d5c4adf7b4022efbc3c5d153528acb7b
Author: fmorg-git <[email protected]>
AuthorDate: Fri Dec 5 02:53:56 2025 -0800
HDDS-13997. [STS] Plumbing for passing STS token through S3 api processing
(#9372)
---
.../apache/hadoop/ozone/om/protocol/S3Auth.java | 10 +
...OzoneManagerProtocolClientSideTranslatorPB.java | 12 +-
.../apache/hadoop/ozone/om/OmMetadataReader.java | 6 +-
.../org/apache/hadoop/ozone/om/OzoneManager.java | 65 ++++-
.../hadoop/ozone/om/request/OMClientRequest.java | 31 +-
.../request/s3/security/S3AssumeRoleRequest.java | 8 +-
...OzoneManagerProtocolServerSideTranslatorPB.java | 1 +
.../hadoop/ozone/security/S3SecurityUtil.java | 35 +++
.../hadoop/ozone/security/STSSecurityUtil.java | 153 ++++++++++
.../ozone/security/STSTokenSecretManager.java | 26 +-
.../hadoop/ozone/om/TestOzoneManagerS3Auth.java | 123 ++++++++
.../request/TestOMClientRequestWithUserInfo.java | 137 +++++++++
.../hadoop/ozone/security/TestSTSSecurityUtil.java | 318 +++++++++++++++++++++
.../ozone/security/TestSTSTokenSecretManager.java | 16 +-
14 files changed, 917 insertions(+), 24 deletions(-)
diff --git
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/S3Auth.java
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/S3Auth.java
index 84acade8f9a..fa023dfc811 100644
---
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/S3Auth.java
+++
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/S3Auth.java
@@ -27,6 +27,8 @@ public class S3Auth {
public static final String S3_AUTH_CHECK = "ozone.s3.auth.check";
// User principal to be used for KMS encryption and decryption
private String userPrincipal;
+ // Optional STS session token when using temporary credentials
+ private String sessionToken;
public S3Auth(final String stringToSign,
final String signature,
@@ -57,4 +59,12 @@ public String getUserPrincipal() {
public void setUserPrincipal(String userPrincipal) {
this.userPrincipal = userPrincipal;
}
+
+ public String getSessionToken() {
+ return sessionToken;
+ }
+
+ public void setSessionToken(String sessionToken) {
+ this.sessionToken = sessionToken;
+ }
}
diff --git
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
index c3c173a8cae..36adbe7b37f 100644
---
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
+++
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
@@ -314,15 +314,21 @@ private OMResponse submitRequest(OMRequest omRequest)
OMRequest.Builder builder = OMRequest.newBuilder(omRequest);
// Insert S3 Authentication information for each request.
if (getThreadLocalS3Auth() != null) {
- builder.setS3Authentication(
+ final S3Authentication.Builder s3AuthBuilder =
S3Authentication.newBuilder()
.setSignature(
threadLocalS3Auth.get().getSignature())
.setStringToSign(
threadLocalS3Auth.get().getStringTosSign())
.setAccessId(
- threadLocalS3Auth.get().getAccessID())
- .build());
+ threadLocalS3Auth.get().getAccessID());
+
+ // Include STS session token if present so OM can validate it
+ if (threadLocalS3Auth.get().getSessionToken() != null) {
+
s3AuthBuilder.setSessionToken(threadLocalS3Auth.get().getSessionToken());
+ }
+
+ builder.setS3Authentication(s3AuthBuilder.build());
}
if (s3AuthCheck && getThreadLocalS3Auth() == null) {
throw new IllegalArgumentException("S3 Auth expected to " +
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
index c413c96956f..2fac369e3a2 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
@@ -488,8 +488,9 @@ void checkAcls(ResourceType resType, StoreType store,
throws IOException {
UserGroupInformation user;
if (getS3Auth() != null) {
+ final String effectiveAccessId =
OzoneManager.getS3AuthEffectiveAccessId();
String principal =
- OzoneAclUtils.accessIdToUserPrincipal(getS3Auth().getAccessId());
+ OzoneAclUtils.accessIdToUserPrincipal(effectiveAccessId);
user = UserGroupInformation.createRemoteUser(principal);
} else {
user = ProtobufRpcEngine.Server.getRemoteUser();
@@ -523,8 +524,9 @@ void checkAcls(ResourceType resType, StoreType store,
throws IOException {
UserGroupInformation user;
if (getS3Auth() != null) {
+ final String effectiveAccessId =
OzoneManager.getS3AuthEffectiveAccessId();
String principal =
- OzoneAclUtils.accessIdToUserPrincipal(getS3Auth().getAccessId());
+ OzoneAclUtils.accessIdToUserPrincipal(effectiveAccessId);
user = UserGroupInformation.createRemoteUser(principal);
} else {
user = ProtobufRpcEngine.Server.getRemoteUser();
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
index 5fe753871a1..e6c5f916cc1 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
@@ -307,6 +307,7 @@
import org.apache.hadoop.ozone.security.OMCertificateClient;
import org.apache.hadoop.ozone.security.OzoneDelegationTokenSecretManager;
import org.apache.hadoop.ozone.security.OzoneTokenIdentifier;
+import org.apache.hadoop.ozone.security.STSTokenIdentifier;
import org.apache.hadoop.ozone.security.STSTokenSecretManager;
import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer;
import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLIdentityType;
@@ -378,6 +379,9 @@ public final class OzoneManager extends
ServiceRuntimeInfoImpl
private static final ThreadLocal<S3Authentication> S3_AUTH =
new ThreadLocal<>();
+ // STS token (if present)
+ private static final ThreadLocal<STSTokenIdentifier> STS_TOKEN = new
ThreadLocal<>();
+
private static boolean securityEnabled = false;
private final ReconfigurationHandler reconfigurationHandler;
@@ -858,6 +862,47 @@ public static S3Authentication getS3Auth() {
return S3_AUTH.get();
}
+ /**
+ * Set the STS token identifier for the current RPC handler thread.
+ */
+ public static void setStsTokenIdentifier(STSTokenIdentifier val) {
+ STS_TOKEN.set(val);
+ }
+
+ /**
+ * Get the STS token identifier for the current RPC handler thread.
+ */
+ public static STSTokenIdentifier getStsTokenIdentifier() {
+ return STS_TOKEN.get();
+ }
+
+ /**
+ * Returns the effective accessId for the current request. If STS temporary
credentials are being used,
+ * the access key id will be the original access key id (i.e. the creator of
the token).
+ */
+ public static String getS3AuthEffectiveAccessId() throws OMException {
+ final S3Authentication s3Auth = getS3Auth();
+ if (s3Auth == null) {
+ return null;
+ }
+
+ // If session token is present, try to resolve originalAccessKeyId from
token
+ if (s3Auth.hasSessionToken() && !s3Auth.getSessionToken().isEmpty()) {
+ final STSTokenIdentifier stsTokenIdentifier = getStsTokenIdentifier();
+ if (stsTokenIdentifier == null) {
+ throw new OMException(
+ "OMClientRequest has session token but no token identifier in
OzoneManager", INVALID_REQUEST);
+ }
+ final String originalAccessKeyId =
stsTokenIdentifier.getOriginalAccessKeyId();
+ if (originalAccessKeyId != null && !originalAccessKeyId.isEmpty()) {
+ return originalAccessKeyId;
+ } else {
+ throw new OMException("Invalid STS Token format - could not find
originalAccessKeyId", INVALID_REQUEST);
+ }
+ }
+ return s3Auth.getAccessId();
+ }
+
/** Returns the ThreadName prefix for the current OM. */
public String getThreadNamePrefix() {
return threadPrefix;
@@ -1269,6 +1314,15 @@ private void stopSecretManager() {
}
}
+ /**
+ * Get the secret key client for this OzoneManager.
+ *
+ * @return the secret key client
+ */
+ public SecretKeyClient getSecretKeyClient() {
+ return secretKeyClient;
+ }
+
@Override
public UUID refetchSecretKey() {
secretKeyClient.refetchSecretKey();
@@ -3836,7 +3890,12 @@ S3VolumeContext getS3VolumeContext(boolean skipChecks)
throws IOException {
s3Volume);
}
} else {
- String accessId = s3Auth.getAccessId();
+ // If this S3 request is authenticated with an STS session token, map
+ // the request to the *original* long-lived access ID so that the
+ // temporary credentials inherit that user's ACLs. Otherwise, fall back
+ // to the accessId included directly in the S3Authentication.
+ final String accessId = getS3AuthEffectiveAccessId();
+
// If S3 Multi-Tenancy is not enabled, all S3 requests will be redirected
// to the default s3v for compatibility
final Optional<String> optionalTenantId = isS3MultiTenancyEnabled() ?
@@ -4621,8 +4680,8 @@ public ResolvedBucket resolveBucketLink(Pair<String,
String> requested,
if (aclEnabled) {
UserGroupInformation ugi = getRemoteUser();
if (getS3Auth() != null) {
- ugi = UserGroupInformation.createRemoteUser(
- OzoneAclUtils.accessIdToUserPrincipal(getS3Auth().getAccessId()));
+ final String principal =
OzoneAclUtils.accessIdToUserPrincipal(getS3AuthEffectiveAccessId());
+ ugi = UserGroupInformation.createRemoteUser(principal);
}
InetAddress remoteIp = Server.getRemoteIp();
resolved = resolveBucketLink(requested, new HashSet<>(),
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 e7689a90b81..b527b20ea65 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
@@ -48,6 +48,7 @@
import org.apache.hadoop.ozone.om.lock.OMLockDetails;
import org.apache.hadoop.ozone.om.protocolPB.grpc.GrpcClientConstants;
import org.apache.hadoop.ozone.om.ratis.utils.OzoneManagerRatisUtils;
+import org.apache.hadoop.ozone.om.request.s3.security.S3AssumeRoleRequest;
import org.apache.hadoop.ozone.om.response.OMClientResponse;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.LayoutVersion;
@@ -168,11 +169,33 @@ public OzoneManagerProtocolProtos.UserInfo getUserInfo()
throws IOException {
OzoneManagerProtocolProtos.UserInfo.Builder userInfo =
OzoneManagerProtocolProtos.UserInfo.newBuilder();
- // If S3 Authentication is set, determine user based on access ID.
+ // If S3 Authentication is set, determine user based on STS token first,
+ // falling back to accessId if session token not present.
if (omRequest.hasS3Authentication()) {
- String principal = OzoneAclUtils.accessIdToUserPrincipal(
- omRequest.getS3Authentication().getAccessId());
- userInfo.setUserName(principal);
+ final String accessKeyId = omRequest.getS3Authentication().getAccessId();
+ if (accessKeyId.startsWith(S3AssumeRoleRequest.STS_TOKEN_PREFIX) &&
+ !omRequest.getS3Authentication().hasSessionToken()) {
+ throw new IOException("Error with STS token", new
AuthenticationException(
+ "Missing session token for accessKeyId: " + accessKeyId));
+ }
+ if (omRequest.getS3Authentication().hasSessionToken()) {
+ try {
+ final String originalAccessKeyId =
OzoneManager.getS3AuthEffectiveAccessId();
+ if (originalAccessKeyId != null && !originalAccessKeyId.isEmpty()) {
+ final String principal =
OzoneAclUtils.accessIdToUserPrincipal(originalAccessKeyId);
+ userInfo.setUserName(principal);
+ } else {
+ throw new AuthenticationException(
+ "Invalid STS Token - originalAccessKeyId was null or empty: "
+ originalAccessKeyId);
+ }
+ } catch (Exception e) {
+ throw new IOException("Error with STS Token", e);
+ }
+ } else {
+ String principal = OzoneAclUtils.accessIdToUserPrincipal(
+ omRequest.getS3Authentication().getAccessId());
+ userInfo.setUserName(principal);
+ }
} else if (user != null) {
// Added not null checks, as in UT's these values might be null.
userInfo.setUserName(user.getUserName());
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3AssumeRoleRequest.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3AssumeRoleRequest.java
index 9d092eaba01..b02f78e5643 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3AssumeRoleRequest.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3AssumeRoleRequest.java
@@ -21,7 +21,9 @@
import java.io.IOException;
import java.net.InetAddress;
import java.security.SecureRandom;
+import java.time.Clock;
import java.time.Instant;
+import java.time.ZoneOffset;
import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.ipc.ProtobufRpcEngine;
import org.apache.hadoop.ozone.om.OzoneAclUtils;
@@ -58,7 +60,6 @@ public class S3AssumeRoleRequest extends OMClientRequest {
private static final int MIN_TOKEN_EXPIRATION_SECONDS = 900; // 15
minutes in seconds
private static final int MAX_TOKEN_EXPIRATION_SECONDS = 43200; // 12 hours
in seconds
- private static final String STS_TOKEN_PREFIX = "ASIA";
private static final int STS_ACCESS_KEY_ID_LENGTH = 20;
private static final int STS_SECRET_ACCESS_KEY_LENGTH = 40;
private static final int STS_ROLE_ID_LENGTH = 16;
@@ -70,6 +71,9 @@ public class S3AssumeRoleRequest extends OMClientRequest {
private static final String CHARS_FOR_SECRET_ACCESS_KEYS =
CHARS_FOR_ACCESS_KEY_IDS +
"abcdefghijklmnopqrstuvwxyz/+";
private static final int CHARS_FOR_SECRET_ACCESS_KEYS_LENGTH =
CHARS_FOR_SECRET_ACCESS_KEYS.length();
+ private static final Clock CLOCK = Clock.system(ZoneOffset.UTC);
+
+ public static final String STS_TOKEN_PREFIX = "ASIA";
public S3AssumeRoleRequest(OMRequest omRequest) {
super(omRequest);
@@ -197,7 +201,7 @@ private String generateSessionToken(String targetRoleName,
OMRequest omRequest,
return ozoneManager.getSTSTokenSecretManager().createSTSTokenString(
tempAccessKeyId, originalAccessKeyId, roleArn,
assumeRoleRequest.getDurationSeconds(), secretAccessKey,
- sessionPolicy);
+ sessionPolicy, CLOCK);
}
/**
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerProtocolServerSideTranslatorPB.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerProtocolServerSideTranslatorPB.java
index 251e81e83ed..de384ef9c4d 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerProtocolServerSideTranslatorPB.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerProtocolServerSideTranslatorPB.java
@@ -174,6 +174,7 @@ private OMResponse internalProcessRequest(OMRequest
request) throws ServiceExcep
return ozoneManager.getOmExecutionFlow().submit(request);
} finally {
OzoneManager.setS3Auth(null);
+ OzoneManager.setStsTokenIdentifier(null);
}
}
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 cbe0ce414fe..e31f822b2fb 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
@@ -21,6 +21,8 @@
import static
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto.Type.S3AUTHINFO;
import com.google.protobuf.ServiceException;
+import java.time.Clock;
+import java.time.ZoneOffset;
import org.apache.hadoop.hdds.annotation.InterfaceAudience;
import org.apache.hadoop.hdds.annotation.InterfaceStability;
import org.apache.hadoop.io.Text;
@@ -41,6 +43,8 @@
@InterfaceStability.Evolving
public final class S3SecurityUtil {
+ private static final Clock CLOCK = Clock.system(ZoneOffset.UTC);
+
private S3SecurityUtil() {
}
@@ -54,6 +58,19 @@ private S3SecurityUtil() {
public static void validateS3Credential(OMRequest omRequest,
OzoneManager ozoneManager) throws ServiceException, OMException {
if (ozoneManager.isSecurityEnabled()) {
+ // If STS session token is present, decode, decrypt and validate it once
and save in thread-local
+ if (omRequest.getS3Authentication().hasSessionToken()) {
+ final String token = omRequest.getS3Authentication().getSessionToken();
+ if (!token.isEmpty()) {
+ final STSTokenIdentifier stsTokenIdentifier =
STSSecurityUtil.constructValidateAndDecryptSTSToken(
+ token, ozoneManager.getSecretKeyClient(), CLOCK);
+ // HMAC signature and expiration were validated above. Now validate
AWS signature.
+ validateSTSTokenAwsSignature(stsTokenIdentifier, omRequest);
+ OzoneManager.setStsTokenIdentifier(stsTokenIdentifier);
+ return;
+ }
+ }
+
OzoneTokenIdentifier s3Token = constructS3Token(omRequest);
try {
// authenticate user with signature verification through
@@ -89,4 +106,22 @@ private static OzoneTokenIdentifier
constructS3Token(OMRequest omRequest) {
s3Token.setOwner(new Text(auth.getAccessId()));
return s3Token;
}
+
+ /**
+ * Validates the AWS signature of an STSTokenIdentifier that has already
been decrypted.
+ * @param stsTokenIdentifier the decrypted STS token
+ * @param omRequest the OMRequest containing STS token
+ * @throws OMException if the AWS signature validation fails
+ */
+ private static void validateSTSTokenAwsSignature(STSTokenIdentifier
stsTokenIdentifier, OMRequest omRequest)
+ throws OMException {
+ final String secretAccessKey = stsTokenIdentifier.getSecretAccessKey();
+ final S3Authentication s3Authentication = omRequest.getS3Authentication();
+ if (AWSV4AuthValidator.validateRequest(
+ s3Authentication.getStringToSign(), s3Authentication.getSignature(),
secretAccessKey)) {
+ return;
+ }
+ throw new OMException(
+ "STS token validation failed for token: " +
omRequest.getS3Authentication().getSessionToken(), INVALID_TOKEN);
+ }
}
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
new file mode 100644
index 00000000000..c3fb14d24b1
--- /dev/null
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSSecurityUtil.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.security;
+
+import static
org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_TOKEN;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.io.IOException;
+import java.time.Clock;
+import java.util.UUID;
+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.exceptions.OMException;
+import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto;
+import org.apache.hadoop.security.token.SecretManager;
+import org.apache.hadoop.security.token.Token;
+
+/**
+ * Utility class with methods to validate and decrypt STS tokens.
+ */
[email protected]
[email protected]
+public final class STSSecurityUtil {
+ private STSSecurityUtil() {
+ }
+
+ /**
+ * Constructs, validates and decrypts STS session token.
+ *
+ * @param sessionToken the session token from the x-amz-security-token
header
+ * @param secretKeyClient the Ozone Manager secretKeyClient
+ * @param clock the system clock
+ * @return the STSTokenIdentifier with decrypted secretAccessKey
+ * @throws OMException if the token is not valid or processing failed
otherwise
+ */
+ public static STSTokenIdentifier constructValidateAndDecryptSTSToken(String
sessionToken,
+ SecretKeyClient secretKeyClient, Clock clock) throws OMException {
+ try {
+ final Token<STSTokenIdentifier> token =
decodeTokenFromString(sessionToken);
+ return verifyAndDecryptToken(token, secretKeyClient, clock);
+ } catch (SecretManager.InvalidToken e) {
+ throw new OMException("Invalid STS token format: " + e.getMessage(), e,
INVALID_TOKEN);
+ }
+ }
+
+ /**
+ * Verifies an STS Token by performing multiple checks.
+ *
+ * @param token the token to verify
+ * @param clock the system clock
+ * @return the STSTokenIdentifier with decrypted secretAccessKey
+ * @throws SecretManager.InvalidToken if the token is invalid
+ */
+ private static STSTokenIdentifier
verifyAndDecryptToken(Token<STSTokenIdentifier> token,
+ SecretKeyClient secretKeyClient, Clock clock) throws
SecretManager.InvalidToken {
+ if (!STSTokenIdentifier.KIND_NAME.equals(token.getKind())) {
+ throw new SecretManager.InvalidToken("Invalid STS token - kind is
incorrect: " + token.getKind());
+ }
+
+ if (!STSTokenIdentifier.STS_SERVICE.equals(token.getService().toString()))
{
+ throw new SecretManager.InvalidToken("Invalid STS token - service is
incorrect: " + token.getService());
+ }
+
+ final byte[] tokenBytes = token.getIdentifier();
+ final OMTokenProto proto;
+ try {
+ proto = OMTokenProto.parseFrom(tokenBytes);
+ } catch (InvalidProtocolBufferException e) {
+ throw new SecretManager.InvalidToken("Invalid STS token - could not
parse protocol buffer: " + e.getMessage());
+ }
+ final UUID secretKeyId;
+ try {
+ secretKeyId = UUID.fromString(proto.getSecretKeyId());
+ } catch (IllegalArgumentException e) {
+ throw new SecretManager.InvalidToken("Invalid STS token - secretKeyId
was not valid: " + proto.getSecretKeyId());
+ }
+
+ final STSTokenIdentifier tokenId = new STSTokenIdentifier();
+ final ManagedSecretKey secretKey;
+ try {
+ secretKey = getValidatedSecretKey(secretKeyId, secretKeyClient);
+ tokenId.setEncryptionKey(secretKey.getSecretKey().getEncoded());
+ tokenId.readFromByteArray(tokenBytes);
+ } catch (IOException e) {
+ throw new SecretManager.InvalidToken("Invalid STS token - could not
readFromByteArray: " + e.getMessage());
+ }
+
+ // Check expiration
+ if (tokenId.isExpired(clock.instant())) {
+ throw new SecretManager.InvalidToken("Invalid STS token - token expired
at " + tokenId.getExpiry());
+ }
+
+ // Verify token signature against the original identifier bytes
+ if (!secretKey.isValidSignature(tokenBytes, token.getPassword())) {
+ throw new SecretManager.InvalidToken("Invalid STS token - signature is
not correct for token: " + tokenId);
+ }
+
+ return tokenId;
+ }
+
+ private static ManagedSecretKey getValidatedSecretKey(UUID secretKeyId,
SecretKeyClient secretKeyClient)
+ throws SecretManager.InvalidToken {
+ if (secretKeyId == null) {
+ throw new SecretManager.InvalidToken("STS token missing secret key ID");
+ }
+
+ final ManagedSecretKey secretKey;
+ try {
+ secretKey = secretKeyClient.getSecretKey(secretKeyId);
+ } catch (Exception e) {
+ throw new SecretManager.InvalidToken("Failed to retrieve secret key: " +
e.getMessage());
+ }
+
+ if (secretKey == null) {
+ throw new SecretManager.InvalidToken("Secret key not found for STS token
secretKeyId: " + secretKeyId);
+ }
+
+ if (secretKey.isExpired()) {
+ throw new SecretManager.InvalidToken("Token cannot be verified due to
expired secret key " + secretKeyId);
+ }
+
+ return secretKey;
+ }
+
+ private static Token<STSTokenIdentifier> decodeTokenFromString(String
encodedToken)
+ throws SecretManager.InvalidToken {
+ final Token<STSTokenIdentifier> token = new Token<>();
+ try {
+ token.decodeFromUrlString(encodedToken);
+ return token;
+ } catch (IOException e) {
+ throw new SecretManager.InvalidToken("Failed to decode STS token string:
" + e);
+ }
+ }
+}
+
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 598a5a71675..f72b1892de8 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
@@ -18,12 +18,14 @@
package org.apache.hadoop.ozone.security;
import java.io.IOException;
+import java.time.Clock;
import java.time.Instant;
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.SecretKeySignerClient;
import org.apache.hadoop.hdds.security.token.ShortLivedTokenSecretManager;
+import org.apache.hadoop.io.Text;
import org.apache.hadoop.security.token.Token;
/**
@@ -49,6 +51,25 @@ public STSTokenSecretManager(SecretKeySignerClient
secretKeyClient) {
this.secretKeyClient = secretKeyClient;
}
+ /**
+ * Override token generation so that we first compute the identifier bytes,
then sign exactly those bytes, and
+ * return a Token that contains those same identifier bytes. This avoids
non-determinism from multiple serializations
+ * which would break unit tests. If we used the inherited generateToken()
in ShortLivedTokenSecretManager, it
+ * would have made two serialization calls:
+ * 1) in the call to secretKey.sign() in the createPassword() method
+ * 2) in the call to tokenIdentifier.getBytes() for the Token constructor
+ * These two calls would produce different secretAccessKey encrypted values
because of the random initialization
+ * vector and random salt and hence give non-deterministic return value, so
here we are only serializing once.
+ */
+ @Override
+ public Token<STSTokenIdentifier> generateToken(STSTokenIdentifier
tokenIdentifier) {
+ final ManagedSecretKey secretKey = secretKeyClient.getCurrentSecretKey();
+ tokenIdentifier.setSecretKeyId(secretKey.getId());
+ final byte[] identifierBytes = tokenIdentifier.getBytes();
+ final byte[] password = secretKey.sign(identifierBytes);
+ return new Token<>(identifierBytes, password, tokenIdentifier.getKind(),
new Text(tokenIdentifier.getService()));
+ }
+
/**
* Create an STS token and return it as an encoded string.
*
@@ -59,11 +80,12 @@ public STSTokenSecretManager(SecretKeySignerClient
secretKeyClient) {
* @param secretAccessKey the secret access key associated with the
temporary access key ID
* @param sessionPolicy an optional opaque identifier that further
limits the scope of
* the permissions granted by the role
+ * @param clock the system clock
* @return base64 encoded token string
*/
public String createSTSTokenString(String tempAccessKeyId, String
originalAccessKeyId, String roleArn,
- int durationSeconds, String secretAccessKey, String sessionPolicy)
throws IOException {
- final Instant expiration = Instant.now().plusSeconds(durationSeconds);
+ int durationSeconds, String secretAccessKey, String sessionPolicy, Clock
clock) throws IOException {
+ final Instant expiration = clock.instant().plusSeconds(durationSeconds);
// Get the current secret key for encryption
final ManagedSecretKey currentSecretKey =
secretKeyClient.getCurrentSecretKey();
diff --git
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOzoneManagerS3Auth.java
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOzoneManagerS3Auth.java
new file mode 100644
index 00000000000..8ce4ee08beb
--- /dev/null
+++
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOzoneManagerS3Auth.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om;
+
+import static
org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_REQUEST;
+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.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.S3Authentication;
+import org.apache.hadoop.ozone.security.STSTokenIdentifier;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test S3 Authentication logic in OzoneManager.
+ */
+public class TestOzoneManagerS3Auth {
+
+ @AfterEach
+ public void tearDown() {
+ OzoneManager.setS3Auth(null);
+ OzoneManager.setStsTokenIdentifier(null);
+ }
+
+ @Test
+ public void testGetS3AuthEffectiveAccessIdNoS3Auth() throws Exception {
+ OzoneManager.setS3Auth(null);
+ assertNull(OzoneManager.getS3AuthEffectiveAccessId());
+ }
+
+ @Test
+ public void testGetS3AuthEffectiveAccessIdNormal() throws Exception {
+ final String accessId = "accessId";
+ final S3Authentication s3Auth = S3Authentication.newBuilder()
+ .setAccessId(accessId)
+ .setSignature("signature")
+ .setStringToSign("stringToSign")
+ .build();
+ OzoneManager.setS3Auth(s3Auth);
+
+ assertEquals(accessId, OzoneManager.getS3AuthEffectiveAccessId());
+ }
+
+ @Test
+ public void testGetS3AuthEffectiveAccessIdWithSessionToken() throws
Exception {
+ final String tempAccessId = "ASIA12345";
+ final String originalAccessId = "AKIAORIG98765";
+ final String sessionToken = "sessionToken";
+
+ final S3Authentication s3Auth = S3Authentication.newBuilder()
+ .setAccessId(tempAccessId)
+ .setSignature("signature")
+ .setStringToSign("stringToSign")
+ .setSessionToken(sessionToken)
+ .build();
+ OzoneManager.setS3Auth(s3Auth);
+
+ final STSTokenIdentifier stsToken = mock(STSTokenIdentifier.class);
+ when(stsToken.getOriginalAccessKeyId()).thenReturn(originalAccessId);
+ OzoneManager.setStsTokenIdentifier(stsToken);
+
+ assertEquals(originalAccessId, OzoneManager.getS3AuthEffectiveAccessId());
+ }
+
+ @Test
+ public void testGetS3AuthEffectiveAccessIdWithEmptySessionToken() throws
Exception {
+ final String accessId = "AKIAORIG98765";
+ final String emptySessionToken = "";
+
+ final S3Authentication s3Auth = S3Authentication.newBuilder()
+ .setAccessId(accessId)
+ .setSignature("signature")
+ .setStringToSign("stringToSign")
+ .setSessionToken(emptySessionToken)
+ .build();
+ OzoneManager.setS3Auth(s3Auth);
+
+ // Empty session token should be treated as if no session token is present.
+ assertEquals(accessId, OzoneManager.getS3AuthEffectiveAccessId());
+ }
+
+ @Test
+ public void
testGetS3AuthEffectiveAccessIdWithSessionTokenMissingOriginalAccessKey() {
+ final String tempAccessId = "ASIA12345";
+ final String sessionToken = "sessionToken";
+
+ final S3Authentication s3Auth = S3Authentication.newBuilder()
+ .setAccessId(tempAccessId)
+ .setSignature("signature")
+ .setStringToSign("stringToSign")
+ .setSessionToken(sessionToken)
+ .build();
+ OzoneManager.setS3Auth(s3Auth);
+
+ final STSTokenIdentifier stsToken = mock(STSTokenIdentifier.class);
+ when(stsToken.getOriginalAccessKeyId()).thenReturn(null); // Missing
original ID
+ OzoneManager.setStsTokenIdentifier(stsToken);
+
+ final OMException ex = assertThrows(OMException.class,
OzoneManager::getS3AuthEffectiveAccessId);
+ assertEquals(INVALID_REQUEST, ex.getResult());
+ assertEquals("Invalid STS Token format - could not find
originalAccessKeyId", ex.getMessage());
+ }
+}
+
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 9fda60374c1..d891a3bc8b9 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
@@ -22,6 +22,7 @@
import static
org.apache.hadoop.ozone.om.request.OMRequestTestUtils.newCreateBucketRequest;
import static org.junit.jupiter.api.Assertions.assertEquals;
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.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
@@ -47,6 +48,8 @@
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.BucketInfo;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
+import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.S3Authentication;
+import org.apache.hadoop.ozone.security.STSTokenIdentifier;
import org.apache.hadoop.security.UserGroupInformation;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -163,4 +166,138 @@ public void testUserInfoInCaseOfGrpcTransport() throws
IOException {
}
}
+ @Test
+ public void testUserInfoWithSTSToken() throws IOException {
+ final String accessId = "ASIA12345";
+ final String signature = "Signature";
+ final String stringToSign = "StringToSign";
+ final String sessionToken = "SessionToken";
+ final String originalAccessKeyId = "AKIAORIGINAL";
+
+ final STSTokenIdentifier stsTokenIdentifier =
mock(STSTokenIdentifier.class);
+
when(stsTokenIdentifier.getOriginalAccessKeyId()).thenReturn(originalAccessKeyId);
+
+ final S3Authentication s3Authentication = S3Authentication.newBuilder()
+ .setAccessId(accessId)
+ .setSignature(signature)
+ .setStringToSign(stringToSign)
+ .setSessionToken(sessionToken)
+ .build();
+
+ OzoneManager.setS3Auth(s3Authentication);
+ OzoneManager.setStsTokenIdentifier(stsTokenIdentifier);
+
+ try {
+ final OMRequest.Builder omRequestBuilder = OMRequest.newBuilder()
+ .setCmdType(OzoneManagerProtocolProtos.Type.CommitKey)
+ .setClientId(UUID.randomUUID().toString())
+ .setS3Authentication(s3Authentication);
+
+ final OMRequest omRequest = omRequestBuilder.build();
+ final OMClientRequest omClientRequest = new
OMKeyCommitRequest(omRequest, mock(BucketLayout.class));
+
+ final OzoneManagerProtocolProtos.UserInfo userInfo =
omClientRequest.getUserInfo();
+ assertEquals(originalAccessKeyId, userInfo.getUserName());
+ } finally {
+ OzoneManager.setStsTokenIdentifier(null);
+ OzoneManager.setS3Auth(null);
+ }
+ }
+
+ @Test
+ public void testUserInfoWithSTSAccessKeyMissingSessionToken() {
+ final String accessId = "ASIA12345";
+ final String signature = "Signature";
+ final String stringToSign = "StringToSign";
+
+ final OMRequest omRequest = OMRequest.newBuilder()
+ .setCmdType(OzoneManagerProtocolProtos.Type.CommitKey)
+ .setClientId(UUID.randomUUID().toString())
+ .setS3Authentication(S3Authentication.newBuilder()
+ .setAccessId(accessId)
+ .setSignature(signature)
+ .setStringToSign(stringToSign)
+ .build())
+ .build();
+
+ final OMClientRequest omClientRequest = new OMKeyCommitRequest(omRequest,
mock(BucketLayout.class));
+ final IOException ex = assertThrows(IOException.class,
omClientRequest::getUserInfo);
+
+ assertEquals("Error with STS token", ex.getMessage());
+ assertEquals("Missing session token for accessKeyId: " + accessId,
ex.getCause().getMessage());
+ }
+
+ @Test
+ public void testUserInfoWithSessionTokenButNoStsTokenIdentifier() {
+ final String accessId = "ASIA12345";
+ final String signature = "Signature";
+ final String stringToSign = "StringToSign";
+ final String sessionToken = "SessionToken";
+
+ final S3Authentication s3Authentication = S3Authentication.newBuilder()
+ .setAccessId(accessId)
+ .setSignature(signature)
+ .setStringToSign(stringToSign)
+ .setSessionToken(sessionToken)
+ .build();
+
+ final OMRequest omRequest = OMRequest.newBuilder()
+ .setCmdType(OzoneManagerProtocolProtos.Type.CommitKey)
+ .setClientId(UUID.randomUUID().toString())
+ .setS3Authentication(s3Authentication)
+ .build();
+
+ OzoneManager.setS3Auth(s3Authentication);
+ OzoneManager.setStsTokenIdentifier(null);
+
+ try {
+ final OMClientRequest omClientRequest = new
OMKeyCommitRequest(omRequest, mock(BucketLayout.class));
+ final IOException ex = assertThrows(IOException.class,
omClientRequest::getUserInfo);
+
+ assertEquals("Error with STS Token", ex.getMessage());
+ assertEquals(
+ "OMClientRequest has session token but no token identifier in
OzoneManager", ex.getCause().getMessage());
+ } finally {
+ OzoneManager.setS3Auth(null);
+ }
+ }
+
+ @Test
+ public void testUserInfoWithSessionTokenButEmptyOriginalAccessKeyId() {
+ final String accessId = "ASIA12345";
+ final String signature = "Signature";
+ final String stringToSign = "StringToSign";
+ final String sessionToken = "SessionToken";
+
+ final STSTokenIdentifier stsTokenIdentifier =
mock(STSTokenIdentifier.class);
+ when(stsTokenIdentifier.getOriginalAccessKeyId()).thenReturn("");
+
+ final S3Authentication s3Authentication = S3Authentication.newBuilder()
+ .setAccessId(accessId)
+ .setSignature(signature)
+ .setStringToSign(stringToSign)
+ .setSessionToken(sessionToken)
+ .build();
+
+ OzoneManager.setS3Auth(s3Authentication);
+ OzoneManager.setStsTokenIdentifier(stsTokenIdentifier);
+
+ try {
+ final OMRequest omRequest = OMRequest.newBuilder()
+ .setCmdType(OzoneManagerProtocolProtos.Type.CommitKey)
+ .setClientId(UUID.randomUUID().toString())
+ .setS3Authentication(s3Authentication)
+ .build();
+
+ final OMClientRequest omClientRequest = new
OMKeyCommitRequest(omRequest, mock(BucketLayout.class));
+ final IOException ex = assertThrows(IOException.class,
omClientRequest::getUserInfo);
+
+ assertEquals("Error with STS Token", ex.getMessage());
+ assertEquals("Invalid STS Token format - could not find
originalAccessKeyId", ex.getCause().getMessage());
+ } finally {
+ OzoneManager.setS3Auth(null);
+ OzoneManager.setStsTokenIdentifier(null);
+ }
+ }
+
}
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
new file mode 100644
index 00000000000..96c83287705
--- /dev/null
+++
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSSecurityUtil.java
@@ -0,0 +1,318 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.security;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.util.UUID;
+import org.apache.hadoop.hdds.security.exception.SCMSecurityException;
+import org.apache.hadoop.hdds.security.symmetric.ManagedSecretKey;
+import org.apache.hadoop.hdds.security.symmetric.SecretKeyClient;
+import org.apache.hadoop.io.Text;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto;
+import org.apache.hadoop.security.token.Token;
+import org.apache.ozone.test.TestClock;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for STSSecurityUtil.
+ */
+public class TestSTSSecurityUtil {
+ private static final String TEMP_ACCESS_KEY = "temp-access-key";
+ private static final String ORIGINAL_ACCESS_KEY = "original-access-key";
+ private static final String ROLE_ARN =
"arn:aws:iam::123456789012:role/test-role";
+ private static final String SECRET_ACCESS_KEY = "test-secret-access-key";
+ private static final String SESSION_POLICY = "test-session-policy";
+ private static final int DURATION_SECONDS = 3600;
+
+ private final SecretKeyTestClient secretKeyClient = new
SecretKeyTestClient();
+ private final STSTokenSecretManager tokenSecretManager = new
STSTokenSecretManager(secretKeyClient);
+ private final UUID secretKeyId =
secretKeyClient.getCurrentSecretKey().getId();
+ private final TestClock clock = new
TestClock(Instant.ofEpochMilli(1764819000), ZoneOffset.UTC);
+
+ @Test
+ public void testConstructValidateAndDecryptSTSTokenInvalidProtobuf() throws
IOException {
+ // Create a token whose identifier bytes are not a valid OMTokenProto
+ final Token<STSTokenIdentifier> token = new Token<>(
+ new byte[] {0x01, 0x02, 0x03}, new byte[] {0x04},
STSTokenIdentifier.KIND_NAME,
+ new Text(STSTokenIdentifier.STS_SERVICE));
+
+ final String tokenString = token.encodeToUrlString();
+
+ assertThatThrownBy(() ->
+ STSSecurityUtil.constructValidateAndDecryptSTSToken(tokenString,
secretKeyClient, clock))
+ .isInstanceOf(OMException.class)
+ .hasMessage(
+ "Invalid STS token format: Invalid STS token - could not parse
protocol buffer: Protocol message " +
+ "contained an invalid tag (zero).");
+ }
+
+ @Test
+ public void testConstructValidateAndDecryptSTSTokenSuccess() throws
IOException {
+ // Create a valid token
+ final String tokenString = tokenSecretManager.createSTSTokenString(
+ TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+
+ // Validate and decrypt the token
+ final STSTokenIdentifier result =
STSSecurityUtil.constructValidateAndDecryptSTSToken(
+ tokenString, secretKeyClient, clock);
+
+ // Verify the result
+ assertThat(result.getOwnerId()).isEqualTo(TEMP_ACCESS_KEY);
+ assertThat(result.getOriginalAccessKeyId()).isEqualTo(ORIGINAL_ACCESS_KEY);
+ assertThat(result.getRoleArn()).isEqualTo(ROLE_ARN);
+ assertThat(result.getSecretAccessKey()).isEqualTo(SECRET_ACCESS_KEY);
+ assertThat(result.getSessionPolicy()).isEqualTo(SESSION_POLICY);
+ assertThat(result.isExpired(clock.instant())).isFalse();
+ final long expirationEpochMillis = result.getExpiry().toEpochMilli();
+ assertThat(expirationEpochMillis).isEqualTo(clock.millis() +
(DURATION_SECONDS * 1000));
+ }
+
+ @Test
+ public void
testConstructValidateAndDecryptSTSTokenSuccessWithNullSessionPolicy() throws
Exception {
+ // Create a valid token with null session policy
+ final String tokenString = tokenSecretManager.createSTSTokenString(
+ TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, null, clock);
+
+ // Validate and decrypt the token
+ final STSTokenIdentifier result =
STSSecurityUtil.constructValidateAndDecryptSTSToken(
+ tokenString, secretKeyClient, clock);
+
+ // Verify the result
+ assertThat(result.getSessionPolicy()).isEmpty();
+ }
+
+ @Test
+ public void testConstructValidateAndDecryptSTSTokenInvalidFormat() {
+ // Try to decrypt an invalid token string
+ assertThatThrownBy(() ->
+
STSSecurityUtil.constructValidateAndDecryptSTSToken("invalid-token-format",
secretKeyClient, clock))
+ .isInstanceOf(OMException.class)
+ .hasMessageContaining("Invalid STS token format: Failed to decode STS
token string");
+ }
+
+ @Test
+ public void testConstructValidateAndDecryptSTSTokenInvalidKind() throws
Exception {
+ // Create a valid identifier to use as base
+ final String validTokenString = tokenSecretManager.createSTSTokenString(
+ TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+
+ final Token<STSTokenIdentifier> validToken = new Token<>();
+ validToken.decodeFromUrlString(validTokenString);
+
+ // Create token with wrong kind
+ final Token<STSTokenIdentifier> token = new Token<>(
+ validToken.getIdentifier(), validToken.getPassword(), new
Text("WRONG_KIND"),
+ new Text(STSTokenIdentifier.STS_SERVICE));
+
+ final String invalidTokenString = token.encodeToUrlString();
+
+ // Try to validate the token with wrong kind
+ assertThatThrownBy(() ->
+
STSSecurityUtil.constructValidateAndDecryptSTSToken(invalidTokenString,
secretKeyClient, clock))
+ .isInstanceOf(OMException.class)
+ .hasMessage("Invalid STS token format: Invalid STS token - kind is
incorrect: WRONG_KIND");
+ }
+
+ @Test
+ public void testConstructValidateAndDecryptSTSTokenInvalidService() throws
Exception {
+ // Create a token with incorrect service
+ final String validTokenString = tokenSecretManager.createSTSTokenString(
+ TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+
+ final Token<STSTokenIdentifier> validToken = new Token<>();
+ validToken.decodeFromUrlString(validTokenString);
+
+ final Token<STSTokenIdentifier> token = new Token<>(
+ validToken.getIdentifier(), validToken.getPassword(),
validToken.getKind(), new Text("WRONG_SERVICE"));
+
+ final String invalidTokenString = token.encodeToUrlString();
+
+ // Try to validate the token with wrong service
+ assertThatThrownBy(() ->
+
STSSecurityUtil.constructValidateAndDecryptSTSToken(invalidTokenString,
secretKeyClient, clock))
+ .isInstanceOf(OMException.class)
+ .hasMessage("Invalid STS token format: Invalid STS token - service is
incorrect: WRONG_SERVICE");
+ }
+
+ @Test
+ public void testConstructValidateAndDecryptSTSTokenExpired() throws
Exception {
+ // Create a token that expires immediately (durationSeconds of 0)
+ final String tokenString = tokenSecretManager.createSTSTokenString(
+ TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, 0, SECRET_ACCESS_KEY,
SESSION_POLICY, clock);
+
+ // Fast-forward time to ensure token is expired
+ clock.fastForward(100);
+
+ // Try to validate the expired token
+ assertThatThrownBy(() ->
+ STSSecurityUtil.constructValidateAndDecryptSTSToken(tokenString,
secretKeyClient, clock))
+ .isInstanceOf(OMException.class)
+ .hasMessageContaining("Invalid STS token format: Invalid STS token -
token expired at");
+ }
+
+ @Test
+ public void testConstructValidateAndDecryptSTSTokenSecretKeyNotFound()
throws Exception {
+ // Create a valid token string
+ final String validTokenString = tokenSecretManager.createSTSTokenString(
+ TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+
+ // Create a mock secret key client that returns null for the key
+ final SecretKeyClient mockKeyClient = mock(SecretKeyClient.class);
+ when(mockKeyClient.getSecretKey(any())).thenReturn(null);
+
+ // Try to validate the token when secret key is not found
+ 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);
+ }
+
+ @Test
+ public void testConstructValidateAndDecryptSTSTokenInvalidSecretKeyId()
throws Exception {
+ // Create a valid identifier to use as base
+ final String validTokenString = tokenSecretManager.createSTSTokenString(
+ TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+
+ final Token<STSTokenIdentifier> validToken = new Token<>();
+ validToken.decodeFromUrlString(validTokenString);
+
+ // Rewrite the identifier with an invalid secretKeyId in the protobuf
+ final byte[] identifierBytes = validToken.getIdentifier();
+ final OMTokenProto proto = OMTokenProto.parseFrom(identifierBytes);
+
+ final OMTokenProto invalidProto =
proto.toBuilder().setSecretKeyId("not-a-uuid").build();
+
+ final Token<STSTokenIdentifier> brokenToken = new Token<>(
+ invalidProto.toByteArray(), validToken.getPassword(),
validToken.getKind(), validToken.getService());
+
+ final String invalidTokenString = brokenToken.encodeToUrlString();
+
+ assertThatThrownBy(() ->
+
STSSecurityUtil.constructValidateAndDecryptSTSToken(invalidTokenString,
secretKeyClient, clock))
+ .isInstanceOf(OMException.class)
+ .hasMessage("Invalid STS token format: Invalid STS token - secretKeyId
was not valid: not-a-uuid");
+ }
+
+ @Test
+ public void testConstructValidateAndDecryptSTSTokenExpiredSecretKey() throws
Exception {
+ // Create a valid token string
+ final String validTokenString = tokenSecretManager.createSTSTokenString(
+ TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+
+ // Create a mock secret key that is expired
+ final ManagedSecretKey expiredSecretKey = mock(ManagedSecretKey.class);
+ when(expiredSecretKey.isExpired()).thenReturn(true);
+
+ final SecretKeyClient mockKeyClient = mock(SecretKeyClient.class);
+ when(mockKeyClient.getSecretKey(any())).thenReturn(expiredSecretKey);
+
+ // Try to validate the token with expired secret key
+ assertThatThrownBy(() ->
+ STSSecurityUtil.constructValidateAndDecryptSTSToken(validTokenString,
mockKeyClient, clock))
+ .isInstanceOf(OMException.class)
+ .hasMessage(
+ "Invalid STS token format: Invalid STS token - could not
readFromByteArray: Token cannot be " +
+ "verified due to expired secret key " + secretKeyId);
+ }
+
+ @Test
+ public void
testConstructValidateAndDecryptSTSTokenSecretKeyRetrievalException() throws
Exception {
+ // Create a valid token string
+ final String validTokenString = tokenSecretManager.createSTSTokenString(
+ TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+
+ // Create a mock secret key client that throws an exception
+ final SecretKeyClient mockKeyClient = mock(SecretKeyClient.class);
+ when(mockKeyClient.getSecretKey(any())).thenThrow(new
SCMSecurityException("something went wrong"));
+
+ // Try to validate the token when secret key retrieval fails
+ 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");
+ }
+
+ @Test
+ public void testConstructValidateAndDecryptSTSTokenInvalidSignature() throws
Exception {
+ // Create a valid token string
+ final String validTokenString = tokenSecretManager.createSTSTokenString(
+ TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
+
+ final Token<STSTokenIdentifier> validToken = new Token<>();
+ validToken.decodeFromUrlString(validTokenString);
+
+ // Create a token with invalid signature (wrong password)
+ final Token<STSTokenIdentifier> invalidToken = new Token<>(
+ validToken.getIdentifier(),
"wrong-signature".getBytes(StandardCharsets.UTF_8), validToken.getKind(),
+ validToken.getService());
+
+ final String invalidTokenString = invalidToken.encodeToUrlString();
+
+ // Try to validate the token with invalid signature
+ assertThatThrownBy(() ->
+
STSSecurityUtil.constructValidateAndDecryptSTSToken(invalidTokenString,
secretKeyClient, clock))
+ .isInstanceOf(OMException.class)
+ .hasMessageContaining("Invalid STS token format: Invalid STS token -
signature is not correct for token");
+ }
+
+ @Test
+ public void testConstructValidateAndDecryptSTSTokenEmptyString() {
+ // Try to decrypt an empty token string
+ assertThatThrownBy(() ->
+ STSSecurityUtil.constructValidateAndDecryptSTSToken("",
secretKeyClient, clock))
+ .isInstanceOf(OMException.class)
+ .hasMessage("Invalid STS token format: Failed to decode STS token
string: java.io.EOFException");
+ }
+
+ @Test
+ public void testConstructValidateAndDecryptMultipleTokens() throws Exception
{
+ // Create multiple tokens and validate them all
+ final String token1 = tokenSecretManager.createSTSTokenString(
+ "temp-key-1", "orig-key-1", "role-arn-1", DURATION_SECONDS,
+ "secret-key-1", "policy-1", clock);
+
+ final String token2 = tokenSecretManager.createSTSTokenString(
+ "temp-key-2", "orig-key-2", "role-arn-2", DURATION_SECONDS,
+ "secret-key-2", "policy-2", clock);
+
+ final STSTokenIdentifier result1 =
STSSecurityUtil.constructValidateAndDecryptSTSToken(
+ token1, secretKeyClient, clock);
+ final STSTokenIdentifier result2 =
STSSecurityUtil.constructValidateAndDecryptSTSToken(
+ token2, secretKeyClient, clock);
+
+ assertThat(result1.getOwnerId()).isEqualTo("temp-key-1");
+ assertThat(result1.getOriginalAccessKeyId()).isEqualTo("orig-key-1");
+ assertThat(result2.getOwnerId()).isEqualTo("temp-key-2");
+ assertThat(result2.getOriginalAccessKeyId()).isEqualTo("orig-key-2");
+ }
+}
+
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 5eb7868c4a2..ad7d3df71ff 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
@@ -27,6 +27,7 @@
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
+import java.time.ZoneOffset;
import java.util.UUID;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
@@ -34,6 +35,7 @@
import org.apache.hadoop.hdds.security.symmetric.SecretKeySignerClient;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.security.token.Token;
+import org.apache.ozone.test.TestClock;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -43,6 +45,8 @@
*/
public class TestSTSTokenSecretManager {
private STSTokenSecretManager secretManager;
+ private TestClock clock;
+
private static final String TEMP_ACCESS_KEY = "temp-access-key";
private static final String ORIGINAL_ACCESS_KEY = "original-access-key";
private static final String ROLE_ARN =
"arn:aws:iam::123456789012:role/test-role";
@@ -71,14 +75,13 @@ public void setUp() throws Exception {
when(mockSecretKeyClient.getCurrentSecretKey()).thenReturn(mockSecretKey);
secretManager = new STSTokenSecretManager(mockSecretKeyClient);
+ clock = new TestClock(Instant.ofEpochMilli(1764819000), ZoneOffset.UTC);
}
@Test
public void testCreateSTSTokenStringContainsCorrectFields() throws
IOException {
- final Instant beforeCreation = Instant.now();
-
final String tokenString = secretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, SESSION_POLICY);
+ TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, SESSION_POLICY, clock);
// Decode the token
final Token<STSTokenIdentifier> token = new Token<>();
@@ -88,7 +91,6 @@ public void testCreateSTSTokenStringContainsCorrectFields()
throws IOException {
final STSTokenIdentifier identifier = new STSTokenIdentifier();
identifier.setEncryptionKey(sharedSecretKey.getEncoded());
identifier.readFromByteArray(token.getIdentifier());
- final Instant afterCreation = Instant.now();
final Instant expiration = identifier.getExpiry();
assertEquals(TEMP_ACCESS_KEY, identifier.getTempAccessKeyId());
@@ -99,15 +101,13 @@ public void
testCreateSTSTokenStringContainsCorrectFields() throws IOException {
assertNotNull(identifier.getSecretKeyId());
assertEquals(new Text("STSToken"), identifier.getKind());
assertEquals("STS", identifier.getService());
- // Verify expiration is approximately durationSeconds in the future
- assertTrue(expiration.isAfter(beforeCreation.plusSeconds(DURATION_SECONDS
- 1)));
- assertTrue(expiration.isBefore(afterCreation.plusSeconds(DURATION_SECONDS
+ 1)));
+ assertEquals(clock.millis() + (DURATION_SECONDS * 1000),
expiration.toEpochMilli());
}
@Test
public void testCreateSTSTokenStringWithNullSessionPolicy() throws
IOException {
final String tokenString = secretManager.createSTSTokenString(
- TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, null);
+ TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
SECRET_ACCESS_KEY, null, clock);
// Decode the token
final Token<STSTokenIdentifier> token = new Token<>();
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]