This is an automated email from the ASF dual-hosted git repository.

ChenSammi 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 5bc5a1e362f HDDS-16290. [STS] Implement GetCallerIdentity API (#11121)
5bc5a1e362f is described below

commit 5bc5a1e362f5143198683b511a32984f6319a8fb
Author: fmorg-git <[email protected]>
AuthorDate: Thu Sep 3 18:10:08 2026 -0700

    HDDS-16290. [STS] Implement GetCallerIdentity API (#11121)
---
 hadoop-hdds/docs/content/design/ozone-sts.md       |  12 +-
 .../apache/hadoop/ozone/client/ObjectStore.java    |  10 ++
 .../ozone/client/protocol/ClientProtocol.java      |   8 +
 .../apache/hadoop/ozone/client/rpc/RpcClient.java  |   6 +
 .../main/java/org/apache/hadoop/ozone/OmUtils.java |   2 +
 .../ozone/om/helpers/CallerIdentityInfo.java       |  88 +++++++++
 .../apache/hadoop/ozone/om/helpers/S3STSUtils.java |  34 +++-
 .../ozone/om/protocol/OzoneManagerProtocol.java    |  10 ++
 ...OzoneManagerProtocolClientSideTranslatorPB.java |  10 ++
 .../ozone/om/helpers/TestCallerIdentityInfo.java   | 107 +++++++++++
 .../om/helpers/TestS3STSUtilsCallerIdentity.java   |  59 ++++++
 .../smoketest/security/ozone-secure-sts.resource   |  37 ++++
 .../main/smoketest/security/ozone-secure-sts.robot |  32 ++++
 .../src/main/proto/OmClientProtocol.proto          |  14 ++
 .../request/s3/security/S3AssumeRoleRequest.java   | 199 +++++++++++++++++++--
 .../protocolPB/OzoneManagerRequestHandler.java     |  24 +++
 .../hadoop/ozone/security/STSTokenIdentifier.java  |  53 +++++-
 .../ozone/security/STSTokenSecretManager.java      | 158 ++++++++++++++--
 .../s3/security/TestS3AssumeRoleRequest.java       |  41 ++++-
 .../hadoop/ozone/security/TestSTSSecurityUtil.java |  84 +++++----
 .../ozone/security/TestSTSTokenIdentifier.java     |  14 ++
 .../ozone/security/TestSTSTokenSecretManager.java  |  25 ++-
 .../org/apache/hadoop/ozone/audit/S3GAction.java   |   1 +
 .../hadoop/ozone/s3/util/S3GActionIamMapper.java   |   1 +
 .../ozone/s3sts/S3AssumeRoleResponseXml.java       |  23 +--
 .../s3sts/S3GetCallerIdentityResponseXml.java      |  92 ++++++++++
 .../apache/hadoop/ozone/s3sts/S3STSEndpoint.java   | 126 +++++++++----
 .../hadoop/ozone/s3sts/S3STSResponseMetadata.java  |  42 +++++
 .../hadoop/ozone/client/ClientProtocolStub.java    |   6 +
 .../ozone/s3/util/TestS3GActionIamMapper.java      |   1 +
 .../hadoop/ozone/s3sts/TestS3STSEndpoint.java      |  79 ++++++++
 31 files changed, 1261 insertions(+), 137 deletions(-)

diff --git a/hadoop-hdds/docs/content/design/ozone-sts.md 
b/hadoop-hdds/docs/content/design/ozone-sts.md
index 9963534d848..ccca985ae57 100644
--- a/hadoop-hdds/docs/content/design/ozone-sts.md
+++ b/hadoop-hdds/docs/content/design/ozone-sts.md
@@ -41,8 +41,9 @@ solutions that want to aggregate data across multiple cloud 
providers.
 
 # 3. How Ozone STS Works
 
-The initial implementation of Ozone STS supports only the 
[AssumeRole](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html)
-API from the AWS specification.  A new STS endpoint on port `9880` (port 
`9881` for https) will be created to service STS requests in the S3 Gateway at 
the root path (`/`).
+The initial implementation of Ozone STS supports the 
[AssumeRole](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html)
+and 
[GetCallerIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html)
+APIs from the AWS specification.  A new STS endpoint on port `9880` (port 
`9881` for https) will be created to service STS requests in the S3 Gateway at 
the root path (`/`).
 We use a separate port for STS to align with AWS so we don't have conflicts at 
a later time.  This means we have:
 - Admin port for Ozone specific S3 admin operations
 - STS port for STS APIs, analogous to AWS' separate STS endpoint
@@ -66,6 +67,11 @@ return value of the AssumeRole call will be temporary 
credentials consisting of
 an IAM policy is specified, the temporary credential will have the permissions 
comprising the intersection of the role permissions
 and the IAM policy permissions. **Note:** If the IAM policy is specified and 
does not grant any permissions, then
 the generated temporary credentials won't have any permissions and will 
essentially be useless.
+- 
[GetCallerIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html)
 returns the account, 
+ARN, and user ID for the caller credentials used to sign the request. Ozone 
uses a static account ID of `123456789012`. 
+For permanent S3 credentials, `UserId` is the resolved Kerberos principal and 
`Arn` is `arn:aws:iam::123456789012:user/<kerberosShortName>` 
+where `<kerberosShortName>` is the short username of the Kerberos principal. 
For STS temporary credentials, `UserId` is 
+the `AssumedRoleId` and `Arn` is the assumed-role user ARN from the session 
token.
 
 ## 3.2 Limitations in AssumeRole API Support
 
@@ -151,6 +157,8 @@ credential will have the permissions and actions comprising 
the intersection of
 - creation time of the token (via `OMTokenProto#issueDate`, exposed as 
`STSTokenIdentifier#getCreationTime()`)
 - expiration time of the token (via `ShortLivedTokenIdentifier#getExpiry()`)
 - UUID of the OzoneManager secret key used to sign the sessionToken and 
encrypt the secretAccessKey (via `ShortLivedTokenIdentifier#getSecretKeyId()`)
+- assumedRoleId - the generated identifier of the role from the AssumeRole 
call response (this is used for GetCallerIdentity api)
+- assumedRoleUserArn - the arn from the AssumeRole call response (this is used 
for GetCallerIdentity api)
 
 ## 3.5 STS Token Revocation
 
diff --git 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/ObjectStore.java
 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/ObjectStore.java
index ce0f780b72d..2cc610d4007 100644
--- 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/ObjectStore.java
+++ 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/ObjectStore.java
@@ -37,6 +37,7 @@
 import org.apache.hadoop.ozone.om.exceptions.OMException;
 import org.apache.hadoop.ozone.om.helpers.AssumeRoleResponseInfo;
 import org.apache.hadoop.ozone.om.helpers.BucketLayout;
+import org.apache.hadoop.ozone.om.helpers.CallerIdentityInfo;
 import org.apache.hadoop.ozone.om.helpers.DeleteTenantState;
 import org.apache.hadoop.ozone.om.helpers.OmVolumeArgs;
 import org.apache.hadoop.ozone.om.helpers.S3SecretValue;
@@ -812,6 +813,15 @@ public AssumeRoleResponseInfo assumeRole(String roleArn, 
String roleSessionName,
     return proxy.assumeRole(roleArn, roleSessionName, durationSeconds, 
awsIamSessionPolicy, requestId);
   }
 
+  /**
+   * Returns the caller identity for the current S3-authenticated request.
+   * @return CallerIdentityInfo containing account, arn, and userId
+   * @throws IOException if an error occurs during the GetCallerIdentity 
operation
+   */
+  public CallerIdentityInfo getCallerIdentity() throws IOException {
+    return proxy.getCallerIdentity();
+  }
+
   /**
    * Revokes STS tokens for the given original access key ID.
    * @param originalAccessKeyId     The original long-lived access key ID 
whose STS tokens to revoke
diff --git 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
index b5f7baa0ef2..73f099879bc 100644
--- 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
+++ 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/protocol/ClientProtocol.java
@@ -48,6 +48,7 @@
 import org.apache.hadoop.ozone.om.OMConfigKeys;
 import org.apache.hadoop.ozone.om.exceptions.OMException;
 import org.apache.hadoop.ozone.om.helpers.AssumeRoleResponseInfo;
+import org.apache.hadoop.ozone.om.helpers.CallerIdentityInfo;
 import org.apache.hadoop.ozone.om.helpers.DeleteTenantState;
 import org.apache.hadoop.ozone.om.helpers.ErrorInfo;
 import org.apache.hadoop.ozone.om.helpers.LeaseKeyInfo;
@@ -1647,6 +1648,13 @@ void deleteObjectTagging(String volumeName, String 
bucketName, String keyName)
   AssumeRoleResponseInfo assumeRole(String roleArn, String roleSessionName, 
int durationSeconds,
       String awsIamSessionPolicy, String requestId) throws IOException;
 
+  /**
+   * Returns the caller identity for the current S3-authenticated request.
+   * @return CallerIdentityInfo containing account, arn, and userId
+   * @throws IOException if an error occurs during the GetCallerIdentity 
operation
+   */
+  CallerIdentityInfo getCallerIdentity() throws IOException;
+
   /**
    * Revokes STS tokens for the given original access key ID.
    * @param originalAccessKeyId     The original long-lived access key ID 
whose STS tokens to revoke
diff --git 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
index 9ca47013462..dcba71af525 100644
--- 
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
+++ 
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/rpc/RpcClient.java
@@ -133,6 +133,7 @@
 import org.apache.hadoop.ozone.om.helpers.BasicOmKeyInfo;
 import org.apache.hadoop.ozone.om.helpers.BucketEncryptionKeyInfo;
 import org.apache.hadoop.ozone.om.helpers.BucketLayout;
+import org.apache.hadoop.ozone.om.helpers.CallerIdentityInfo;
 import org.apache.hadoop.ozone.om.helpers.DeleteTenantState;
 import org.apache.hadoop.ozone.om.helpers.ErrorInfo;
 import org.apache.hadoop.ozone.om.helpers.KeyInfoWithVolumeContext;
@@ -3021,6 +3022,11 @@ public AssumeRoleResponseInfo assumeRole(String roleArn, 
String roleSessionName,
     return ozoneManagerClient.assumeRole(roleArn, roleSessionName, 
durationSeconds, awsIamSessionPolicy, requestId);
   }
 
+  @Override
+  public CallerIdentityInfo getCallerIdentity() throws IOException {
+    return ozoneManagerClient.getCallerIdentity();
+  }
+
   @Override
   public void revokeSTSToken(String originalAccessKeyId) throws IOException {
     ozoneManagerClient.revokeSTSToken(originalAccessKeyId);
diff --git 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java
index 5da80215fad..31530900cf2 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java
@@ -238,6 +238,7 @@ public static boolean isReadOnly(OMRequest omRequest) {
     case FinalizeUpgradeProgress:
     case PrepareStatus:
     case GetS3VolumeContext:
+    case GetCallerIdentity:
     case ListTenant:
     case TenantGetUserInfo:
     case TenantListUser:
@@ -383,6 +384,7 @@ public static boolean shouldSendToFollower(OMRequest 
omRequest) {
     case FinalizeUpgradeProgress:
     case PrepareStatus:
     case GetS3VolumeContext:
+    case GetCallerIdentity:
     case ListTenant:
     case TenantGetUserInfo:
     case TenantListUser:
diff --git 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/CallerIdentityInfo.java
 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/CallerIdentityInfo.java
new file mode 100644
index 00000000000..5a014f19fcf
--- /dev/null
+++ 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/CallerIdentityInfo.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om.helpers;
+
+import java.util.Objects;
+import net.jcip.annotations.Immutable;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetCallerIdentityResponse;
+
+/**
+ * Utility class to handle GetCallerIdentityResponse protobuf message.
+ */
+@Immutable
+public class CallerIdentityInfo {
+
+  private final String account;
+  private final String arn;
+  private final String userId;
+
+  public CallerIdentityInfo(String account, String arn, String userId) {
+    this.account = account;
+    this.arn = arn;
+    this.userId = userId;
+  }
+
+  public String getAccount() {
+    return account;
+  }
+
+  public String getArn() {
+    return arn;
+  }
+
+  public String getUserId() {
+    return userId;
+  }
+
+  public static CallerIdentityInfo fromProtobuf(GetCallerIdentityResponse 
response) {
+    return new CallerIdentityInfo(response.getAccount(), response.getArn(), 
response.getUserId());
+  }
+
+  public GetCallerIdentityResponse getProtobuf() {
+    return GetCallerIdentityResponse.newBuilder()
+        .setAccount(account)
+        .setArn(arn)
+        .setUserId(userId)
+        .build();
+  }
+
+  @Override
+  public String toString() {
+    return "CallerIdentityInfo{" + "account='" + account + "', arn='" + arn + 
"', userId='" + userId + "'}";
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+
+    final CallerIdentityInfo that = (CallerIdentityInfo) o;
+    return Objects.equals(account, that.account) && Objects.equals(arn, 
that.arn) &&
+        Objects.equals(userId, that.userId);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(account, arn, userId);
+  }
+}
diff --git 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/S3STSUtils.java
 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/S3STSUtils.java
index 64221089655..17bf084f9b3 100644
--- 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/S3STSUtils.java
+++ 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/S3STSUtils.java
@@ -44,11 +44,43 @@ public final class S3STSUtils {
   public static final String STS_ACCESS_KEY_ID_ALLOWED_CHARS = 
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
   public static final int STS_ACCESS_KEY_ID_ALLOWED_CHARS_LENGTH = 
STS_ACCESS_KEY_ID_ALLOWED_CHARS.length();
   public static final int STS_ACCESS_KEY_ID_RANDOM_LENGTH = 20;
-  public static final int STS_ACCESS_KEY_ID_LENGTH = STS_TOKEN_PREFIX.length() 
+ STS_ACCESS_KEY_ID_RANDOM_LENGTH;
+
+  public static final String OZONE_STATIC_ACCOUNT_ID = "123456789012";
 
   private S3STSUtils() {
   }
 
+  /**
+   * Builds an IAM user ARN for the given Kerberos short name.
+   */
+  public static String toIamUserArn(String kerberosShortName) {
+    return "arn:aws:iam::" + OZONE_STATIC_ACCOUNT_ID + ":user/" + 
kerberosShortName;
+  }
+
+  /**
+   * Resolves the caller identity for GetCallerIdentity with permanent S3 
credentials.
+   *
+   * @param resolvedPrincipal     full Kerberos principal of the caller
+   * @param kerberosShortName     short username
+   * @return caller identity with account, arn, and userId
+   */
+  public static CallerIdentityInfo 
resolveCallerIdentityForPermanentCredentials(String resolvedPrincipal,
+      String kerberosShortName) {
+    return new CallerIdentityInfo(OZONE_STATIC_ACCOUNT_ID, 
toIamUserArn(kerberosShortName), resolvedPrincipal);
+  }
+
+  /**
+   * Resolves the caller identity for GetCallerIdentity with temporary STS 
credentials.
+   *
+   * @param assumedRoleId      assumed role ID from the STS token
+   * @param assumedRoleUserArn assumed role user ARN from the STS token
+   * @return caller identity with account, arn, and userId
+   */
+  public static CallerIdentityInfo 
resolveCallerIdentityForStsCredentials(String assumedRoleId,
+      String assumedRoleUserArn) {
+    return new CallerIdentityInfo(OZONE_STATIC_ACCOUNT_ID, assumedRoleUserArn, 
assumedRoleId);
+  }
+
   /**
    * Adds standard AssumeRole audit params.
    */
diff --git 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
index 46254e3d6f6..7cea19d7a90 100644
--- 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
+++ 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
@@ -30,6 +30,7 @@
 import org.apache.hadoop.ozone.om.OMConfigKeys;
 import org.apache.hadoop.ozone.om.exceptions.OMException;
 import org.apache.hadoop.ozone.om.helpers.AssumeRoleResponseInfo;
+import org.apache.hadoop.ozone.om.helpers.CallerIdentityInfo;
 import org.apache.hadoop.ozone.om.helpers.DBUpdates;
 import org.apache.hadoop.ozone.om.helpers.DeleteTenantState;
 import org.apache.hadoop.ozone.om.helpers.ErrorInfo;
@@ -1335,6 +1336,15 @@ default AssumeRoleResponseInfo assumeRole(String 
roleArn, String roleSessionName
     throw new UnsupportedOperationException("OzoneManager does not require 
this to be implemented");
   }
 
+  /**
+   * Returns the caller identity for the current S3-authenticated request.
+   * @return CallerIdentityInfo containing account, arn, and userId
+   * @throws IOException if an error occurs during the GetCallerIdentity 
operation
+   */
+  default CallerIdentityInfo getCallerIdentity() throws IOException {
+    throw new UnsupportedOperationException("OzoneManager does not require 
this to be implemented");
+  }
+
   /**
    * Revokes STS tokens for the given original access key ID.
    * @param originalAccessKeyId     The original long-lived access key ID 
whose STS tokens to revoke
diff --git 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
index 7077fd1b02c..b5a9e3a2bbc 100644
--- 
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
+++ 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
@@ -60,6 +60,7 @@
 import org.apache.hadoop.ozone.om.exceptions.OMException;
 import org.apache.hadoop.ozone.om.helpers.AssumeRoleResponseInfo;
 import org.apache.hadoop.ozone.om.helpers.BasicOmKeyInfo;
+import org.apache.hadoop.ozone.om.helpers.CallerIdentityInfo;
 import org.apache.hadoop.ozone.om.helpers.DBUpdates;
 import org.apache.hadoop.ozone.om.helpers.DeleteTenantState;
 import org.apache.hadoop.ozone.om.helpers.ErrorInfo;
@@ -2980,6 +2981,15 @@ public AssumeRoleResponseInfo assumeRole(String roleArn, 
String roleSessionName,
         handleError(submitRequest(omRequest)).getAssumeRoleResponse());
   }
 
+  @Override
+  public CallerIdentityInfo getCallerIdentity() throws IOException {
+    final OMRequest omRequest = createOMRequest(Type.GetCallerIdentity)
+        
.setGetCallerIdentityRequest(OzoneManagerProtocolProtos.GetCallerIdentityRequest.newBuilder().build())
+        .build();
+
+    return 
CallerIdentityInfo.fromProtobuf(handleError(submitRequest(omRequest)).getGetCallerIdentityResponse());
+  }
+
   @Override
   public void revokeSTSToken(String originalAccessKeyId) throws IOException {
     final OzoneManagerProtocolProtos.RevokeSTSTokenRequest request =
diff --git 
a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestCallerIdentityInfo.java
 
b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestCallerIdentityInfo.java
new file mode 100644
index 00000000000..f6bc0334acf
--- /dev/null
+++ 
b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestCallerIdentityInfo.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om.helpers;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetCallerIdentityResponse;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test CallerIdentityInfo.
+ */
+public class TestCallerIdentityInfo {
+
+  private static final String ACCOUNT = "123456789012";
+  private static final String ARN = "arn:aws:iam::123456789012:user/om";
+  private static final String USER_ID = "om/[email protected]";
+
+  @Test
+  public void testConstructor() {
+    final CallerIdentityInfo identity = new CallerIdentityInfo(ACCOUNT, ARN, 
USER_ID);
+
+    assertEquals(ACCOUNT, identity.getAccount());
+    assertEquals(ARN, identity.getArn());
+    assertEquals(USER_ID, identity.getUserId());
+  }
+
+  @Test
+  public void testProtobufConversion() {
+    final CallerIdentityInfo identity = new CallerIdentityInfo(ACCOUNT, ARN, 
USER_ID);
+
+    final GetCallerIdentityResponse proto = identity.getProtobuf();
+
+    assertNotNull(proto);
+    assertEquals(ACCOUNT, proto.getAccount());
+    assertEquals(ARN, proto.getArn());
+    assertEquals(USER_ID, proto.getUserId());
+  }
+
+  @Test
+  public void testFromProtobuf() {
+    final GetCallerIdentityResponse proto = 
GetCallerIdentityResponse.newBuilder()
+        .setAccount(ACCOUNT)
+        .setArn(ARN)
+        .setUserId(USER_ID)
+        .build();
+
+    final CallerIdentityInfo identity = CallerIdentityInfo.fromProtobuf(proto);
+
+    assertEquals(ACCOUNT, identity.getAccount());
+    assertEquals(ARN, identity.getArn());
+    assertEquals(USER_ID, identity.getUserId());
+  }
+
+  @Test
+  public void testProtobufRoundTrip() {
+    final CallerIdentityInfo original = new CallerIdentityInfo(ACCOUNT, ARN, 
USER_ID);
+
+    final CallerIdentityInfo recovered = 
CallerIdentityInfo.fromProtobuf(original.getProtobuf());
+
+    assertEquals(original, recovered);
+  }
+
+  @Test
+  public void testEqualsAndHashCodeWithIdenticalObjects() {
+    final CallerIdentityInfo identity1 = new CallerIdentityInfo(ACCOUNT, ARN, 
USER_ID);
+    final CallerIdentityInfo identity2 = new CallerIdentityInfo(ACCOUNT, ARN, 
USER_ID);
+
+    assertEquals(identity1, identity2);
+    assertEquals(identity1.hashCode(), identity2.hashCode());
+  }
+
+  @Test
+  public void testNotEqualsWithDifferentArn() {
+    final CallerIdentityInfo identity1 = new CallerIdentityInfo(ACCOUNT, ARN, 
USER_ID);
+    final CallerIdentityInfo identity2 = new CallerIdentityInfo(
+        ACCOUNT, "arn:aws:iam::123456789012:user/other", USER_ID);
+
+    assertNotEquals(identity1, identity2);
+    assertNotEquals(identity1.hashCode(), identity2.hashCode());
+  }
+
+  @Test
+  public void testToString() {
+    final CallerIdentityInfo identity = new CallerIdentityInfo(ACCOUNT, ARN, 
USER_ID);
+
+    assertEquals(
+        "CallerIdentityInfo{account='123456789012', arn='" + ARN + "', 
userId='" + USER_ID + "'}", identity.toString());
+  }
+}
diff --git 
a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestS3STSUtilsCallerIdentity.java
 
b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestS3STSUtilsCallerIdentity.java
new file mode 100644
index 00000000000..f991bd7bffd
--- /dev/null
+++ 
b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestS3STSUtilsCallerIdentity.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om.helpers;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test caller identity resolution helpers in S3STSUtils.
+ */
+public class TestS3STSUtilsCallerIdentity {
+
+  private static final String PRINCIPAL = "om/[email protected]";
+  private static final String KERBEROS_SHORT_NAME = "om";
+  private static final String ASSUMED_ROLE_ID = "AROATEST123456789:testsess";
+  private static final String ASSUMED_ROLE_USER_ARN =
+      "arn:aws:sts::123456789012:assumed-role/test-role/testsess";
+
+  @Test
+  public void testToIamUserArn() {
+    assertEquals("arn:aws:iam::123456789012:user/om", 
S3STSUtils.toIamUserArn(KERBEROS_SHORT_NAME));
+  }
+
+  @Test
+  public void testResolveCallerIdentityForPermanentCredentials() {
+    final CallerIdentityInfo identity = 
S3STSUtils.resolveCallerIdentityForPermanentCredentials(
+        PRINCIPAL, KERBEROS_SHORT_NAME);
+
+    assertEquals(S3STSUtils.OZONE_STATIC_ACCOUNT_ID, identity.getAccount());
+    assertEquals("arn:aws:iam::123456789012:user/om", identity.getArn());
+    assertEquals(PRINCIPAL, identity.getUserId());
+  }
+
+  @Test
+  public void testResolveCallerIdentityForStsCredentials() {
+    final CallerIdentityInfo identity = 
S3STSUtils.resolveCallerIdentityForStsCredentials(
+        ASSUMED_ROLE_ID, ASSUMED_ROLE_USER_ARN);
+
+    assertEquals(S3STSUtils.OZONE_STATIC_ACCOUNT_ID, identity.getAccount());
+    assertEquals(ASSUMED_ROLE_USER_ARN, identity.getArn());
+    assertEquals(ASSUMED_ROLE_ID, identity.getUserId());
+  }
+}
diff --git 
a/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.resource 
b/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.resource
index ebfbe55a653..4ebf2ec1b3b 100644
--- a/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.resource
+++ b/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.resource
@@ -111,10 +111,14 @@ Assume Role And Get Temporary Credentials
     ${stsAccessKeyId} =           Execute                       echo '${json}' 
| jq -r '.Credentials.AccessKeyId'
     ${stsSecretKey} =             Execute                       echo '${json}' 
| jq -r '.Credentials.SecretAccessKey'
     ${stsSessionToken} =          Execute                       echo '${json}' 
| jq -r '.Credentials.SessionToken'
+    ${assumedRoleId} =            Execute                       echo '${json}' 
| jq -r '.AssumedRoleUser.AssumedRoleId'
+    ${assumedRoleUserArn} =       Execute                       echo '${json}' 
| jq -r '.AssumedRoleUser.Arn'
     Should Start With             ${stsAccessKeyId}             ASIA
     Set Global Variable           ${STS_ACCESS_KEY_ID}          
${stsAccessKeyId}
     Set Global Variable           ${STS_SECRET_KEY}             ${stsSecretKey}
     Set Global Variable           ${STS_SESSION_TOKEN}          
${stsSessionToken}
+    Set Global Variable           ${STS_ASSUMED_ROLE_ID}        
${assumedRoleId}
+    Set Global Variable           ${STS_ASSUMED_ROLE_USER_ARN}  
${assumedRoleUserArn}
 
     ${expected_duration} =        Set Variable                  
${duration_seconds}
     # Ensure the expected duration defaults to 3600 seconds (1 hour) if not 
specified
@@ -336,3 +340,36 @@ Assert Listed Keys Should Equal
     Sort List                     ${expected_sorted}
     ${actual_list} =              Evaluate                      
json.loads($actual_keys_json)    modules=json
     Lists Should Be Equal         ${actual_list}                
${expected_sorted}
+
+Get Caller Identity
+    [Arguments]                   ${profile}
+    ${json} =                     Execute                       aws sts 
get-caller-identity --endpoint-url ${STS_ENDPOINT_URL} --output json --profile 
${profile}
+    ${account} =                  Execute                       echo '${json}' 
| jq -r '.Account'
+    ${arn} =                      Execute                       echo '${json}' 
| jq -r '.Arn'
+    ${userId} =                   Execute                       echo '${json}' 
| jq -r '.UserId'
+    [Return]                      ${json}                       ${account}    
${arn}    ${userId}
+
+Get Caller Identity Using Curl
+    # AWS CLI always sends Version=2011-06-15 and rejects unknown flags, so 
use curl to test version validation.
+    # curl 7.76.1's --aws-sigv4 omits the port from the signed canonical 
"host" header (it signs "host:s3g"
+    # while sending "Host: s3g:9880"), so Ozone's SigV4 validation for the 
non-default STS port rejects it.
+    # Send an explicit Host header without the port so the sent and signed 
host values match.
+    # This should also work for newer versions of curl as well
+    [Arguments]                   ${perm_access_key_id}  ${perm_secret_key}  
${api_version}=2011-06-15  ${extra_curl_params}=${EMPTY}
+    ${sts_host} =                 Evaluate                      
urllib.parse.urlparse("${STS_ENDPOINT_URL}").hostname    modules=urllib.parse
+    ${cmd} =                      Set Variable                  curl --silent 
--show-error --include --request POST --aws-sigv4 "aws:amz:us-east-1:sts" 
--user '${perm_access_key_id}:${perm_secret_key}' --header "Host: ${sts_host}" 
--header "Content-Type: application/x-www-form-urlencoded" --data-urlencode 
"Action=GetCallerIdentity"
+    ${cmd} =                      Set Variable If               
'${api_version}' != '${EMPTY}'    ${cmd} --data-urlencode 
"Version=${api_version}"    ${cmd}
+    ${cmd} =                      Set Variable If               
'${extra_curl_params}' != '${EMPTY}'    ${cmd} ${extra_curl_params}    ${cmd}
+    ${cmd} =                      Set Variable                  ${cmd} 
${STS_ENDPOINT_URL}
+    ${output} =                   Execute And Ignore Error      ${cmd}
+    [Return]                      ${output}
+
+Get Caller Identity Should Fail
+    [Arguments]                   ${expected_error_contains}    
${api_version}=${EMPTY}
+    ${output} =                   Get Caller Identity Using Curl  
${PERMANENT_ACCESS_KEY_ID}  ${PERMANENT_SECRET_KEY}  api_version=${api_version}
+    Should Contain                ${output}                     
${expected_error_contains}
+    @{http_codes} =               Get Regexp Matches            ${output}    
(?m)^HTTP/[0-9.]+ ([0-9]{3})    1
+    ${code_count} =               Get Length                    ${http_codes}
+    Should Be True                ${code_count} > 0             Expected to 
find an HTTP status code in curl output, but none was found.
+    ${http_code} =                Get From List                 ${http_codes}  
  -1
+    Should Be Equal As Strings    ${http_code}                  400
diff --git 
a/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot 
b/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot
index b07a38e1a23..2368b1da68e 100644
--- a/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot
+++ b/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot
@@ -1367,6 +1367,38 @@ STS Role On Chained Linked Bucket Grants GetObject On 
Source Bucket
     Head Bucket Should Succeed    ${STS_LINK_BUCKET_CHAIN_FINAL}
     Get Object Should Succeed     ${STS_LINK_BUCKET_CHAIN_FINAL}            
${STS_LINK_BUCKET_CHAIN_TESTFILE}
 
+Get Caller Identity With Permanent Credentials Should Succeed
+    Configure AWS Profile         permanent  ${PERMANENT_ACCESS_KEY_ID}  
${PERMANENT_SECRET_KEY}
+    ${json}  ${account}  ${arn}  ${userId} =                    Get Caller 
Identity    permanent
+    Should Be Equal               ${account}                    123456789012
+    ${principal} =                Execute                       klist | awk 
'/Default principal/ {print $3}'
+    Should Be Equal               ${userId}                     ${principal}
+    Should Be Equal               ${arn}                        
arn:aws:iam::123456789012:user/${ICEBERG_SVC_CATALOG_USER}
+
+Get Caller Identity With STS Credentials Should Succeed
+    Assume Role And Configure STS Profile                       
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}  
perm_secret_key=${PERMANENT_SECRET_KEY}  
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+    ${json}  ${account}  ${arn}  ${userId} =                    Get Caller 
Identity    sts
+    Should Be Equal               ${account}                    123456789012
+    Should Be Equal               ${userId}                     
${STS_ASSUMED_ROLE_ID}
+    Should Be Equal               ${arn}                        
${STS_ASSUMED_ROLE_USER_ARN}
+
+Get Caller Identity Ignores Extra Parameters
+    # curl 7.76.1 produces an invalid SigV4 signature for STS GET requests 
with --get --data-urlencode.
+    ${extra_curl_params} =        Set Variable                  
--data-urlencode "RoleArn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}" --data-urlencode 
"RoleSessionName=${ROLE_SESSION_NAME}" --data-urlencode "DurationSeconds=3600"
+    ${output} =                   Get Caller Identity Using Curl  
${PERMANENT_ACCESS_KEY_ID}  ${PERMANENT_SECRET_KEY}  
extra_curl_params=${extra_curl_params}
+    Should Contain                ${output}                     123456789012
+    @{http_codes} =               Get Regexp Matches            ${output}    
(?m)^HTTP/[0-9.]+ ([0-9]{3})    1
+    ${code_count} =               Get Length                    ${http_codes}
+    Should Be True                ${code_count} > 0             Expected to 
find an HTTP status code in curl output, but none was found.
+    ${http_code} =                Get From List                 ${http_codes}  
  -1
+    Should Be Equal As Strings    ${http_code}                  200
+
+Get Caller Identity Rejects Missing Version
+    Get Caller Identity Should Fail                             InvalidAction
+
+Get Caller Identity Rejects Invalid Version
+    Get Caller Identity Should Fail                             InvalidAction  
  api_version=2020-01-01
+
 Expired STS temporary credentials must return ExpiredToken on S3 APIs
     # Increase timeout to account for 15 minute STS token expiration plus the 
time to execute the api calls
     [Timeout]                     25 minutes
diff --git 
a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto 
b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
index 53f7c20e5d9..3702b021ac2 100644
--- a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
+++ b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
@@ -169,6 +169,7 @@ enum Type {
   AssumeRole = 153;
   RevokeSTSToken = 154;
   DeleteRevokedSTSTokens = 155;
+  GetCallerIdentity = 156;
 }
 
 enum SafeMode {
@@ -333,6 +334,7 @@ message OMRequest {
   optional RevokeSTSTokenRequest            revokeSTSTokenRequest          = 
155;
   optional DeleteRevokedSTSTokensRequest    deleteRevokedSTSTokensRequest  = 
156;
   optional UpdateAssumeRoleRequest          updateAssumeRoleRequest        = 
157;
+  optional GetCallerIdentityRequest         getCallerIdentityRequest       = 
158;
 }
 
 message OMResponse {
@@ -481,6 +483,7 @@ message OMResponse {
   optional AssumeRoleResponse                assumeRoleResponse             = 
153;
   optional RevokeSTSTokenResponse            revokeSTSTokenResponse         = 
154;
   optional DeleteRevokedSTSTokensResponse    deleteRevokedSTSTokensResponse = 
155;
+  optional GetCallerIdentityResponse         getCallerIdentityResponse      = 
156;
 }
 
 enum Status {
@@ -1600,6 +1603,8 @@ message OMTokenProto {
     optional string originalAccessKeyId = 18;
     optional string secretAccessKey     = 19;
     optional string sessionPolicy       = 20;
+    optional string assumedRoleId       = 21;
+    optional string assumedRoleUserArn  = 22;
 }
 
 message SecretKeyProto {
@@ -2555,6 +2560,15 @@ message DeleteRevokedSTSTokensRequest {
 message DeleteRevokedSTSTokensResponse {
 }
 
+message GetCallerIdentityRequest {
+}
+
+message GetCallerIdentityResponse {
+  optional string account = 1;
+  optional string arn     = 2;
+  optional string userId  = 3;
+}
+
 enum ReadConsistencyProto {
   // Unspecified consistency, the read consistency behavior is decided
   // by the OM
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/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 693d280419b..173ae19461f 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
@@ -27,6 +27,9 @@
 import com.google.common.base.Strings;
 import java.io.IOException;
 import java.net.InetAddress;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
 import java.time.Clock;
 import java.time.Instant;
@@ -59,6 +62,7 @@
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.AssumeRoleResponse;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.UpdateAssumeRoleRequest;
+import org.apache.hadoop.ozone.security.STSTokenSecretManager;
 import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLType;
 import org.apache.hadoop.ozone.security.acl.IOzoneObj;
 import org.apache.hadoop.ozone.security.acl.OzoneObj;
@@ -131,23 +135,31 @@ public OMRequest preExecute(OzoneManager ozoneManager) 
throws IOException {
       S3STSUtils.validateDuration(durationSeconds);
       S3STSUtils.validateRoleSessionName(roleSessionName);
       final String targetRoleName = 
AwsRoleArnValidator.validateAndExtractRoleNameFromArn(roleArn);
-      
-      // Generate temporary AWS credentials using cryptographically strong 
SecureRandom
+
+      // Generate temporary AWS credentials using cryptographically strong 
SecureRandom, and a
+      // deterministic roleId derived from the role ARN.
       final String tempAccessKeyId = STS_TOKEN_PREFIX + 
generateSecureRandomStringUsingChars(
           STS_ACCESS_KEY_ID_ALLOWED_CHARS, 
STS_ACCESS_KEY_ID_ALLOWED_CHARS_LENGTH,
           STS_ACCESS_KEY_ID_RANDOM_LENGTH);
       final String secretAccessKey = generateSecureRandomStringUsingChars(
           CHARS_FOR_SECRET_ACCESS_KEYS, CHARS_FOR_SECRET_ACCESS_KEYS_LENGTH, 
STS_SECRET_ACCESS_KEY_LENGTH);
-      final String roleId = ASSUME_ROLE_ID_PREFIX + 
generateSecureRandomStringUsingChars(
-          STS_ACCESS_KEY_ID_ALLOWED_CHARS, 
STS_ACCESS_KEY_ID_ALLOWED_CHARS_LENGTH,
-          STS_ROLE_ID_LENGTH);
+      final String roleId = generateDeterministicRoleId(roleArn);
+      final String assumedRoleId = roleId + ":" + roleSessionName;
+      final String assumedRoleUserArn = 
S3STSUtils.toAssumedRoleUserArn(roleArn, roleSessionName);
 
       final Instant creationInstant = clock.instant();
-      final String sessionToken = generateSessionToken(
-          targetRoleName, omRequest, ozoneManager, assumeRoleRequest, 
secretAccessKey, tempAccessKeyId,
-          creationInstant);
+      final String sessionToken = 
generateSessionToken(GenerateSessionTokenParams.newBuilder()
+          .setTargetRoleName(targetRoleName)
+          .setOmRequest(omRequest)
+          .setOzoneManager(ozoneManager)
+          .setAssumeRoleRequest(assumeRoleRequest)
+          .setSecretAccessKey(secretAccessKey)
+          .setTempAccessKeyId(tempAccessKeyId)
+          .setAssumedRoleId(assumedRoleId)
+          .setAssumedRoleUserArn(assumedRoleUserArn)
+          .setCreationTime(creationInstant)
+          .build());
       final long expirationEpochSeconds = 
creationInstant.plusSeconds(durationSeconds).getEpochSecond();
-
       auditMap.put(OzoneConsts.S3_STS_TEMP_ACCESS_KEY_ID, tempAccessKeyId);
 
       // Build UpdateAssumeRoleRequest with leader-generated credentials and 
session token
@@ -245,9 +257,10 @@ public OMClientResponse 
validateAndUpdateCache(OzoneManager ozoneManager, Execut
   /**
    * Generates session token using components from the AssumeRoleRequest.
    */
-  private String generateSessionToken(String targetRoleName, OMRequest 
omRequest,
-      OzoneManager ozoneManager, AssumeRoleRequest assumeRoleRequest, String 
secretAccessKey,
-      String tempAccessKeyId, Instant creationInstant) throws IOException {
+  private String generateSessionToken(GenerateSessionTokenParams params) 
throws IOException {
+    final OzoneManager ozoneManager = params.getOzoneManager();
+    final OMRequest omRequest = params.getOmRequest();
+    final AssumeRoleRequest assumeRoleRequest = params.getAssumeRoleRequest();
 
     InetAddress remoteIp = ProtobufRpcEngine.Server.getRemoteIp();
     if (remoteIp == null) {
@@ -268,11 +281,148 @@ private String generateSessionToken(String 
targetRoleName, OMRequest omRequest,
     final String roleArn = assumeRoleRequest.getRoleArn();
     final String sessionPolicy = getSessionPolicy(
         ozoneManager, originalAccessKeyId, 
assumeRoleRequest.getAwsIamSessionPolicy(), hostName, remoteIp, ugi,
-        targetRoleName);
+        params.getTargetRoleName());
 
     return ozoneManager.getSTSTokenSecretManager().createSTSTokenString(
-        tempAccessKeyId, originalAccessKeyId, roleArn, 
assumeRoleRequest.getDurationSeconds(), secretAccessKey,
-        sessionPolicy, creationInstant);
+        STSTokenSecretManager.CreateSTSTokenParams.newBuilder()
+            .setTempAccessKeyId(params.getTempAccessKeyId())
+            .setOriginalAccessKeyId(originalAccessKeyId)
+            .setRoleArn(roleArn)
+            .setDurationSeconds(assumeRoleRequest.getDurationSeconds())
+            .setSecretAccessKey(params.getSecretAccessKey())
+            .setSessionPolicy(sessionPolicy)
+            .setAssumedRoleId(params.getAssumedRoleId())
+            .setAssumedRoleUserArn(params.getAssumedRoleUserArn())
+            .setCreationTime(params.getCreationTime())
+            .build());
+  }
+
+  /**
+   * Parameters for {@link #generateSessionToken(GenerateSessionTokenParams)}.
+   */
+  private static final class GenerateSessionTokenParams {
+    private final String targetRoleName;
+    private final OMRequest omRequest;
+    private final OzoneManager ozoneManager;
+    private final AssumeRoleRequest assumeRoleRequest;
+    private final String secretAccessKey;
+    private final String tempAccessKeyId;
+    private final String assumedRoleId;
+    private final String assumedRoleUserArn;
+    private final Instant creationInstant;
+
+    private GenerateSessionTokenParams(Builder builder) {
+      this.targetRoleName = builder.targetRoleName;
+      this.omRequest = builder.omRequest;
+      this.ozoneManager = builder.ozoneManager;
+      this.assumeRoleRequest = builder.assumeRoleRequest;
+      this.secretAccessKey = builder.secretAccessKey;
+      this.tempAccessKeyId = builder.tempAccessKeyId;
+      this.assumedRoleId = builder.assumedRoleId;
+      this.assumedRoleUserArn = builder.assumedRoleUserArn;
+      this.creationInstant = builder.creationInstant;
+    }
+
+    static Builder newBuilder() {
+      return new Builder();
+    }
+
+    String getTargetRoleName() {
+      return targetRoleName;
+    }
+
+    OMRequest getOmRequest() {
+      return omRequest;
+    }
+
+    OzoneManager getOzoneManager() {
+      return ozoneManager;
+    }
+
+    AssumeRoleRequest getAssumeRoleRequest() {
+      return assumeRoleRequest;
+    }
+
+    String getSecretAccessKey() {
+      return secretAccessKey;
+    }
+
+    String getTempAccessKeyId() {
+      return tempAccessKeyId;
+    }
+
+    String getAssumedRoleId() {
+      return assumedRoleId;
+    }
+
+    String getAssumedRoleUserArn() {
+      return assumedRoleUserArn;
+    }
+
+    Instant getCreationTime() {
+      return creationInstant;
+    }
+
+    private static final class Builder {
+      private String targetRoleName;
+      private OMRequest omRequest;
+      private OzoneManager ozoneManager;
+      private AssumeRoleRequest assumeRoleRequest;
+      private String secretAccessKey;
+      private String tempAccessKeyId;
+      private String assumedRoleId;
+      private String assumedRoleUserArn;
+      private Instant creationInstant;
+
+      Builder setTargetRoleName(String value) {
+        this.targetRoleName = value;
+        return this;
+      }
+
+      Builder setOmRequest(OMRequest value) {
+        this.omRequest = value;
+        return this;
+      }
+
+      Builder setOzoneManager(OzoneManager value) {
+        this.ozoneManager = value;
+        return this;
+      }
+
+      Builder setAssumeRoleRequest(AssumeRoleRequest value) {
+        this.assumeRoleRequest = value;
+        return this;
+      }
+
+      Builder setSecretAccessKey(String value) {
+        this.secretAccessKey = value;
+        return this;
+      }
+
+      Builder setTempAccessKeyId(String value) {
+        this.tempAccessKeyId = value;
+        return this;
+      }
+
+      Builder setAssumedRoleId(String value) {
+        this.assumedRoleId = value;
+        return this;
+      }
+
+      Builder setAssumedRoleUserArn(String value) {
+        this.assumedRoleUserArn = value;
+        return this;
+      }
+
+      Builder setCreationTime(Instant instant) {
+        this.creationInstant = instant;
+        return this;
+      }
+
+      GenerateSessionTokenParams build() {
+        return new GenerateSessionTokenParams(this);
+      }
+    }
   }
 
   /**
@@ -428,6 +578,25 @@ interface BucketLinkResolver {
     ResolvedBucket resolve(String volumeName, String bucketName) throws 
IOException;
   }
 
+  /**
+   * Generates a deterministic role ID from the role ARN so the same role 
returns the same ID on every
+   * AssumeRole invocation, matching AWS behavior where RoleId is stable for a 
given role.
+   */
+  @VisibleForTesting
+  static String generateDeterministicRoleId(String roleArn) {
+    try {
+      final MessageDigest digest = MessageDigest.getInstance("SHA-256");
+      final byte[] hash = 
digest.digest(roleArn.getBytes(StandardCharsets.UTF_8));
+      final StringBuilder sb = new StringBuilder(STS_ROLE_ID_LENGTH);
+      for (int i = 0; i < STS_ROLE_ID_LENGTH; i++) {
+        sb.append(STS_ACCESS_KEY_ID_ALLOWED_CHARS.charAt((hash[i] & 0xFF) % 
STS_ACCESS_KEY_ID_ALLOWED_CHARS_LENGTH));
+      }
+      return ASSUME_ROLE_ID_PREFIX + sb;
+    } catch (NoSuchAlgorithmException e) {
+      throw new IllegalStateException("SHA-256 not available", e);
+    }
+  }
+
   /**
    * Generates a cryptographically strong String of the supplied stringLength 
using supplied chars.
    */
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
index 21afb1e42f7..0ff43231856 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
@@ -61,6 +61,7 @@
 import org.apache.hadoop.hdds.scm.protocolPB.OzonePBHelper;
 import org.apache.hadoop.hdds.utils.FaultInjector;
 import org.apache.hadoop.ozone.OzoneAcl;
+import org.apache.hadoop.ozone.om.OzoneAclUtils;
 import org.apache.hadoop.ozone.om.OzoneManager;
 import org.apache.hadoop.ozone.om.OzoneManagerPrepareState;
 import org.apache.hadoop.ozone.om.exceptions.OMException;
@@ -86,6 +87,7 @@
 import org.apache.hadoop.ozone.om.helpers.OpenKeySession;
 import org.apache.hadoop.ozone.om.helpers.OzoneFileStatus;
 import org.apache.hadoop.ozone.om.helpers.OzoneFileStatusLight;
+import org.apache.hadoop.ozone.om.helpers.S3STSUtils;
 import org.apache.hadoop.ozone.om.helpers.ServiceInfo;
 import org.apache.hadoop.ozone.om.helpers.ServiceInfoEx;
 import org.apache.hadoop.ozone.om.helpers.SnapshotDiffJob;
@@ -113,6 +115,7 @@
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.FinalizeUpgradeProgressResponse;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetBucketTaggingRequest;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetBucketTaggingResponse;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetCallerIdentityResponse;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetFileStatusRequest;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetFileStatusResponse;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetKeyInfoRequest;
@@ -171,11 +174,13 @@
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.TenantListUserResponse;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
 import org.apache.hadoop.ozone.request.validation.RequestProcessingPhase;
+import org.apache.hadoop.ozone.security.STSTokenIdentifier;
 import org.apache.hadoop.ozone.security.acl.OzoneObjInfo;
 import org.apache.hadoop.ozone.snapshot.ListSnapshotResponse;
 import org.apache.hadoop.ozone.upgrade.UpgradeFinalization.StatusAndMessages;
 import org.apache.hadoop.ozone.util.PayloadUtils;
 import org.apache.hadoop.ozone.util.ProtobufUtils;
+import org.apache.hadoop.security.UserGroupInformation;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -316,6 +321,9 @@ public OMResponse handleReadRequest(OMRequest request) {
             getS3VolumeContext();
         responseBuilder.setGetS3VolumeContextResponse(s3VolumeContextResponse);
         break;
+      case GetCallerIdentity:
+        responseBuilder.setGetCallerIdentityResponse(getCallerIdentity());
+        break;
       case TenantGetUserInfo:
         impl.checkS3MultiTenancyEnabled();
         TenantGetUserInfoResponse getUserInfoResponse = tenantGetUserInfo(
@@ -1448,6 +1456,22 @@ private GetS3VolumeContextResponse getS3VolumeContext()
     return impl.getS3VolumeContext().getProtobuf();
   }
 
+  private GetCallerIdentityResponse getCallerIdentity() throws OMException {
+    impl.checkS3STSEnabled();
+    if (OzoneManager.getS3Auth() == null) {
+      throw new OMException(
+          "GetCallerIdentity does not have S3 authentication", 
OMException.ResultCodes.INVALID_REQUEST);
+    }
+    final STSTokenIdentifier stsTokenIdentifier = 
OzoneManager.getStsTokenIdentifier();
+    if (stsTokenIdentifier != null) {
+      return S3STSUtils.resolveCallerIdentityForStsCredentials(
+          stsTokenIdentifier.getAssumedRoleId(), 
stsTokenIdentifier.getAssumedRoleUserArn()).getProtobuf();
+    }
+    final String resolvedPrincipal = 
OzoneAclUtils.accessIdToUserPrincipal(OzoneManager.getS3AuthEffectiveAccessId());
+    final String kerberosShortName = 
UserGroupInformation.createRemoteUser(resolvedPrincipal).getShortUserName();
+    return 
S3STSUtils.resolveCallerIdentityForPermanentCredentials(resolvedPrincipal, 
kerberosShortName).getProtobuf();
+  }
+
   @DisallowedUntilLayoutVersion(FILESYSTEM_SNAPSHOT)
   private SnapshotDiffResponse snapshotDiff(
       SnapshotDiffRequest snapshotDiffRequest) throws IOException {
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenIdentifier.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenIdentifier.java
index e5629073c5a..c0928d93a2c 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenIdentifier.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenIdentifier.java
@@ -48,6 +48,8 @@ public class STSTokenIdentifier extends 
ShortLivedTokenIdentifier {
   private String originalAccessKeyId;
   private String secretAccessKey;
   private String sessionPolicy;
+  private String assumedRoleId;
+  private String assumedRoleUserArn;
   private Instant creationTime;
 
   // SCM secret key used for encrypting sensitive fields and signing this token
@@ -85,6 +87,8 @@ public STSTokenIdentifier(Params params) {
         throw new IllegalArgumentException("ManagedSecretKey is not set");
       }
     }
+    this.assumedRoleId = params.getAssumedRoleId();
+    this.assumedRoleUserArn = params.getAssumedRoleUserArn();
   }
 
   /**
@@ -99,6 +103,8 @@ public static final class Params {
     private final String secretAccessKey;
     private final String sessionPolicy;
     private final ManagedSecretKey managedSecretKey;
+    private final String assumedRoleId;
+    private final String assumedRoleUserArn;
 
     private Params(Builder builder) {
       this.tempAccessKeyId = builder.tempAccessKeyId;
@@ -109,6 +115,8 @@ private Params(Builder builder) {
       this.secretAccessKey = builder.secretAccessKey;
       this.sessionPolicy = builder.sessionPolicy;
       this.managedSecretKey = builder.managedSecretKey;
+      this.assumedRoleId = builder.assumedRoleId;
+      this.assumedRoleUserArn = builder.assumedRoleUserArn;
     }
 
     public static Builder newBuilder() {
@@ -147,6 +155,14 @@ public ManagedSecretKey getManagedSecretKey() {
       return managedSecretKey;
     }
 
+    public String getAssumedRoleId() {
+      return assumedRoleId;
+    }
+
+    public String getAssumedRoleUserArn() {
+      return assumedRoleUserArn;
+    }
+
     /**
      * Builder for {@link Params}.
      */
@@ -159,6 +175,8 @@ public static final class Builder {
       private String secretAccessKey;
       private String sessionPolicy;
       private ManagedSecretKey managedSecretKey;
+      private String assumedRoleId;
+      private String assumedRoleUserArn;
 
       public Builder setTempAccessKeyId(String value) {
         this.tempAccessKeyId = value;
@@ -200,6 +218,16 @@ public Builder setManagedSecretKey(ManagedSecretKey value) 
{
         return this;
       }
 
+      public Builder setAssumedRoleId(String value) {
+        this.assumedRoleId = value;
+        return this;
+      }
+
+      public Builder setAssumedRoleUserArn(String value) {
+        this.assumedRoleUserArn = value;
+        return this;
+      }
+
       public Params build() {
         return new Params(this);
       }
@@ -249,7 +277,9 @@ public OMTokenProto toProtoBuf() throws IOException {
         .setRoleArn(roleArn != null ? roleArn : "")
         .setSecretAccessKey(secretAccessKey != null ? 
encryptSensitiveField(secretAccessKey) : "")
         .setSecretKeyId(managedSecretKey.getId().toString())
-        .setSessionPolicy(sessionPolicy != null ? sessionPolicy : "");
+        .setSessionPolicy(sessionPolicy != null ? sessionPolicy : "")
+        .setAssumedRoleId(assumedRoleId != null ? assumedRoleId : "")
+        .setAssumedRoleUserArn(assumedRoleUserArn != null ? assumedRoleUserArn 
: "");
 
     return builder.build();
   }
@@ -292,6 +322,12 @@ public void fromProtoBuf(OMTokenProto token) throws 
IOException {
     if (token.hasSessionPolicy()) {
       this.sessionPolicy = token.getSessionPolicy();
     }
+    if (token.hasAssumedRoleId()) {
+      this.assumedRoleId = token.getAssumedRoleId();
+    }
+    if (token.hasAssumedRoleUserArn()) {
+      this.assumedRoleUserArn = token.getAssumedRoleUserArn();
+    }
   }
 
   /**
@@ -359,6 +395,14 @@ public String getSessionPolicy() {
     return sessionPolicy;
   }
 
+  public String getAssumedRoleId() {
+    return assumedRoleId;
+  }
+
+  public String getAssumedRoleUserArn() {
+    return assumedRoleUserArn;
+  }
+
   public Instant getCreationTime() {
     return creationTime;
   }
@@ -415,13 +459,15 @@ public boolean equals(Object o) {
     final STSTokenIdentifier that = (STSTokenIdentifier) o;
     return Objects.equals(roleArn, that.roleArn) && 
Objects.equals(secretAccessKey, that.secretAccessKey) &&
         Objects.equals(originalAccessKeyId, that.originalAccessKeyId) &&
-        Objects.equals(sessionPolicy, that.sessionPolicy) && 
Objects.equals(creationTime, that.creationTime);
+        Objects.equals(sessionPolicy, that.sessionPolicy) && 
Objects.equals(assumedRoleId, that.assumedRoleId) &&
+        Objects.equals(assumedRoleUserArn, that.assumedRoleUserArn) && 
Objects.equals(creationTime, that.creationTime);
   }
 
   @Override
   public int hashCode() {
     return Objects.hash(
-        super.hashCode(), roleArn, secretAccessKey, originalAccessKeyId, 
sessionPolicy, creationTime);
+        super.hashCode(), roleArn, secretAccessKey, originalAccessKeyId, 
sessionPolicy, assumedRoleId,
+        assumedRoleUserArn, creationTime);
   }
 
   @Override
@@ -429,6 +475,7 @@ public String toString() {
     // Intentionally left off secretAccessKey
     return "STSTokenIdentifier{" + "tempAccessKeyId='" + getOwnerId() + "'" +
         ", originalAccessKeyId='" + originalAccessKeyId + "', roleArn='" + 
roleArn + "'" +
+        ", assumedRoleId='" + assumedRoleId + "', assumedRoleUserArn='" + 
assumedRoleUserArn + "'" +
         ", creationTime='" + creationTime + "', expiry='" + getExpiry() + "', 
secretKeyId='" + getSecretKeyId() +
         "', sessionPolicy='" + sessionPolicy + "'}";
   }
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenSecretManager.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenSecretManager.java
index d9e5c4caf76..2a7b7b1feb2 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
@@ -75,34 +75,160 @@ public Token<STSTokenIdentifier> 
generateToken(STSTokenIdentifier tokenIdentifie
   /**
    * Create an STS token and return it as an encoded string.
    *
-   * @param tempAccessKeyId     the temporary access key ID
-   * @param originalAccessKeyId the original long-lived access key ID
-   * @param roleArn             the ARN of the assumed role
-   * @param durationSeconds     how long the token should be valid for
-   * @param secretAccessKey     the secret access key associated with the 
temporary access key ID
-   * @param sessionPolicy       an optional opaque identifier that further 
limits the scope of
-   *                            the permissions granted by the role
-   * @param creationTime        token creation time
+   * @param params  the STS token creation parameters
    * @return base64 encoded token string
    */
-  public String createSTSTokenString(String tempAccessKeyId, String 
originalAccessKeyId, String roleArn,
-      int durationSeconds, String secretAccessKey, String sessionPolicy, 
Instant creationTime) throws IOException {
-    final Instant expiration = creationTime.plusSeconds(durationSeconds);
+  public String createSTSTokenString(CreateSTSTokenParams params) throws 
IOException {
+    final Instant creationTime = params.getCreationTime();
+    final Instant expiration = 
creationTime.plusSeconds(params.getDurationSeconds());
 
     final STSTokenIdentifier identifier = new 
STSTokenIdentifier(STSTokenIdentifier.Params.newBuilder()
-        .setTempAccessKeyId(tempAccessKeyId)
-        .setOriginalAccessKeyId(originalAccessKeyId)
-        .setRoleArn(roleArn)
+        .setTempAccessKeyId(params.getTempAccessKeyId())
+        .setOriginalAccessKeyId(params.getOriginalAccessKeyId())
+        .setRoleArn(params.getRoleArn())
         .setCreationTime(creationTime)
         .setExpiry(expiration)
-        .setSecretAccessKey(secretAccessKey)
-        .setSessionPolicy(sessionPolicy)
+        .setSecretAccessKey(params.getSecretAccessKey())
+        .setSessionPolicy(params.getSessionPolicy())
         .setManagedSecretKey(secretKeyClient.getCurrentSecretKey())
+        .setAssumedRoleId(params.getAssumedRoleId())
+        .setAssumedRoleUserArn(params.getAssumedRoleUserArn())
         .build());
 
     final Token<STSTokenIdentifier> token = generateToken(identifier);
     return token.encodeToUrlString();
   }
+
+  /**
+   * Parameters for {@link #createSTSTokenString(CreateSTSTokenParams)}.
+   */
+  public static final class CreateSTSTokenParams {
+    private final String tempAccessKeyId;
+    private final String originalAccessKeyId;
+    private final String roleArn;
+    private final int durationSeconds;
+    private final String secretAccessKey;
+    private final String sessionPolicy;
+    private final String assumedRoleId;
+    private final String assumedRoleUserArn;
+    private final Instant creationTime;
+
+    private CreateSTSTokenParams(Builder builder) {
+      this.tempAccessKeyId = builder.tempAccessKeyId;
+      this.originalAccessKeyId = builder.originalAccessKeyId;
+      this.roleArn = builder.roleArn;
+      this.durationSeconds = builder.durationSeconds;
+      this.secretAccessKey = builder.secretAccessKey;
+      this.sessionPolicy = builder.sessionPolicy;
+      this.assumedRoleId = builder.assumedRoleId;
+      this.assumedRoleUserArn = builder.assumedRoleUserArn;
+      this.creationTime = builder.creationTime;
+    }
+
+    public static Builder newBuilder() {
+      return new Builder();
+    }
+
+    public String getTempAccessKeyId() {
+      return tempAccessKeyId;
+    }
+
+    public String getOriginalAccessKeyId() {
+      return originalAccessKeyId;
+    }
+
+    public String getRoleArn() {
+      return roleArn;
+    }
+
+    public int getDurationSeconds() {
+      return durationSeconds;
+    }
+
+    public String getSecretAccessKey() {
+      return secretAccessKey;
+    }
+
+    public String getSessionPolicy() {
+      return sessionPolicy;
+    }
+
+    public String getAssumedRoleId() {
+      return assumedRoleId;
+    }
+
+    public String getAssumedRoleUserArn() {
+      return assumedRoleUserArn;
+    }
+
+    public Instant getCreationTime() {
+      return creationTime;
+    }
+
+    /**
+     * Builder for {@link CreateSTSTokenParams}.
+     */
+    public static final class Builder {
+      private String tempAccessKeyId;
+      private String originalAccessKeyId;
+      private String roleArn;
+      private int durationSeconds;
+      private String secretAccessKey;
+      private String sessionPolicy;
+      private String assumedRoleId;
+      private String assumedRoleUserArn;
+      private Instant creationTime;
+
+      public Builder setTempAccessKeyId(String value) {
+        this.tempAccessKeyId = value;
+        return this;
+      }
+
+      public Builder setOriginalAccessKeyId(String value) {
+        this.originalAccessKeyId = value;
+        return this;
+      }
+
+      public Builder setRoleArn(String value) {
+        this.roleArn = value;
+        return this;
+      }
+
+      public Builder setDurationSeconds(int value) {
+        this.durationSeconds = value;
+        return this;
+      }
+
+      public Builder setSecretAccessKey(String value) {
+        this.secretAccessKey = value;
+        return this;
+      }
+
+      public Builder setSessionPolicy(String value) {
+        this.sessionPolicy = value;
+        return this;
+      }
+
+      public Builder setAssumedRoleId(String value) {
+        this.assumedRoleId = value;
+        return this;
+      }
+
+      public Builder setAssumedRoleUserArn(String value) {
+        this.assumedRoleUserArn = value;
+        return this;
+      }
+
+      public Builder setCreationTime(Instant creationTime) {
+        this.creationTime = creationTime;
+        return this;
+      }
+
+      public CreateSTSTokenParams build() {
+        return new CreateSTSTokenParams(this);
+      }
+    }
+  }
 }
 
 
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3AssumeRoleRequest.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3AssumeRoleRequest.java
index 57068d1a169..f779a349d0f 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3AssumeRoleRequest.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3AssumeRoleRequest.java
@@ -88,6 +88,7 @@
 public class TestS3AssumeRoleRequest {
 
   private static final String ROLE_ARN_1 = 
"arn:aws:iam::123456789012:role/MyRole1";
+  private static final String ROLE_ARN_2 = 
"arn:aws:iam::123456789012:role/MyRole2";
   private static final String SESSION_NAME = "testSessionName";
   private static final String ORIGINAL_ACCESS_KEY_ID = "origAccessKeyId";
   private static final String TARGET_ROLE_NAME = "targetRole";
@@ -306,11 +307,9 @@ public void testSuccessfulAssumeRoleGeneratesCredentials() 
throws IOException {
     assertThat(assumeRoleResponse.getSecretAccessKey().length()).isEqualTo(40);
 
     // AssumedRoleId: prefix AROA + 16 chars, followed by ":" and sessionName
+    final String expectedRoleId = 
S3AssumeRoleRequest.generateDeterministicRoleId(ROLE_ARN_1);
     assertThat(assumeRoleResponse.getAssumedRoleId())
-        .startsWith("AROA")
-        .contains(":" + SESSION_NAME);
-    final int expectedAssumedRoleIdLength = 4 + 16 + 1 + 
SESSION_NAME.length(); // 4 for AROA, 16 chars, 1 for ":"
-    
assertThat(assumeRoleResponse.getAssumedRoleId().length()).isEqualTo(expectedAssumedRoleIdLength);
+        .isEqualTo(expectedRoleId + ":" + SESSION_NAME);
 
     // Verify expiration added durationSeconds
     final long expirationEpochSeconds = 
assumeRoleResponse.getExpirationEpochSeconds();
@@ -318,6 +317,17 @@ public void testSuccessfulAssumeRoleGeneratesCredentials() 
throws IOException {
     assertMarkForAuditCalled(requestWithCredentials);
   }
 
+  @Test
+  public void testGenerateDeterministicRoleId() {
+    final String roleId1 = 
S3AssumeRoleRequest.generateDeterministicRoleId(ROLE_ARN_1);
+    final String roleId2 = 
S3AssumeRoleRequest.generateDeterministicRoleId(ROLE_ARN_1);
+    final String roleId3 = 
S3AssumeRoleRequest.generateDeterministicRoleId(ROLE_ARN_2);
+
+    assertThat(roleId1).startsWith("AROA").hasSize(4 + 16);
+    assertThat(roleId1).isEqualTo(roleId2);
+    assertThat(roleId1).isNotEqualTo(roleId3);
+  }
+
   @Test
   public void testGenerateSecureRandomStringUsingChars() {
     final String chars = "ABC";
@@ -373,12 +383,29 @@ public void testAssumeRoleCredentialsAreUnique() throws 
IOException {
     // Different session tokens
     
assertThat(assumeRoleResponse1.getSessionToken()).isNotEqualTo(assumeRoleResponse2.getSessionToken());
 
-    // Different assumed role IDs
-    
assertThat(assumeRoleResponse1.getAssumedRoleId()).isNotEqualTo(assumeRoleResponse2.getAssumedRoleId());
+    // Same assumed role ID for the same role and session name
+    
assertThat(assumeRoleResponse1.getAssumedRoleId()).isEqualTo(assumeRoleResponse2.getAssumedRoleId());
+
+    // Different role ARN yields a different assumed role ID
+    final OMRequest omRequestDifferentRole = baseOmRequestBuilder()
+        .setAssumeRoleRequest(
+            AssumeRoleRequest.newBuilder()
+                .setRoleArn(ROLE_ARN_2)
+                .setRoleSessionName(SESSION_NAME)
+                .setDurationSeconds(3600)
+                .setRequestId(REQUEST_ID)
+        ).build();
+    final S3AssumeRoleRequest request3 = new 
S3AssumeRoleRequest(omRequestDifferentRole, CLOCK);
+    final OMRequest preExecutedRequest3 = request3.preExecute(ozoneManager);
+    final S3AssumeRoleRequest requestWithCredentials3 = new 
S3AssumeRoleRequest(preExecutedRequest3, CLOCK);
+    final OMClientResponse response3 = 
requestWithCredentials3.validateAndUpdateCache(ozoneManager, context);
+    final AssumeRoleResponse assumeRoleResponse3 = 
response3.getOMResponse().getAssumeRoleResponse();
+    
assertThat(assumeRoleResponse1.getAssumedRoleId()).isNotEqualTo(assumeRoleResponse3.getAssumedRoleId());
 
     OMAuditLogger.log(requestWithCredentials1.getAuditBuilder());
     OMAuditLogger.log(requestWithCredentials2.getAuditBuilder());
-    verify(auditLogger, times(2)).logWrite(any(AuditMessage.class));
+    OMAuditLogger.log(requestWithCredentials3.getAuditBuilder());
+    verify(auditLogger, times(3)).logWrite(any(AuditMessage.class));
   }
 
   @Test
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 2f3a0350b57..769ae0da5f1 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
@@ -53,6 +53,9 @@ public class TestSTSSecurityUtil {
   private static final String ROLE_ARN = 
"arn:aws:iam::123456789012:role/test-role";
   private static final String SECRET_ACCESS_KEY = "test-secret-access-key";
   private static final String SESSION_POLICY = "test-session-policy";
+  private static final String ASSUMED_ROLE_ID = "AROATEST123456789:testsess";
+  private static final String ASSUMED_ROLE_USER_ARN =
+      "arn:aws:sts::123456789012:assumed-role/test-role/testsess";
   private static final int DURATION_SECONDS = 3600;
   private static final ManagedSecretKey MANAGED_SECRET_KEY = new 
SecretKeyTestClient().getCurrentSecretKey();
   private final SecretKeyTestClient secretKeyClient = new 
SecretKeyTestClient();
@@ -80,8 +83,7 @@ public void 
testConstructValidateAndDecryptSTSTokenInvalidProtobuf() throws IOEx
   @Test
   public void testConstructValidateAndDecryptSTSTokenSuccess() throws 
IOException {
     // Create a valid token
-    final String tokenString = 
tokenSecretManager.createSTSTokenString(TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY,
-        ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY, 
clock.instant());
+    final String tokenString = createStsTokenString();
 
     // Validate and decrypt the token
     final STSTokenIdentifier result = 
STSSecurityUtil.constructValidateAndDecryptSTSToken(
@@ -102,8 +104,7 @@ public void 
testConstructValidateAndDecryptSTSTokenSuccess() throws IOException
   @Test
   public void 
testConstructValidateAndDecryptSTSTokenSuccessWithNullSessionPolicy() throws 
Exception {
     // Create a valid token with null session policy
-    final String tokenString = tokenSecretManager.createSTSTokenString(
-        TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, 
SECRET_ACCESS_KEY, null, clock.instant());
+    final String tokenString = createStsTokenString(DURATION_SECONDS, null);
 
     // Validate and decrypt the token
     final STSTokenIdentifier result = 
STSSecurityUtil.constructValidateAndDecryptSTSToken(
@@ -135,8 +136,7 @@ public void 
testConstructValidateAndDecryptSTSTokenRuntimeDecodeFailure() {
   @Test
   public void testConstructValidateAndDecryptSTSTokenInvalidKind() throws 
Exception {
     // Create a valid identifier to use as base
-    final String validTokenString = 
tokenSecretManager.createSTSTokenString(TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY,
-        ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY, 
clock.instant());
+    final String validTokenString = createStsTokenString();
 
     final Token<STSTokenIdentifier> validToken = new Token<>();
     validToken.decodeFromUrlString(validTokenString);
@@ -158,8 +158,7 @@ public void 
testConstructValidateAndDecryptSTSTokenInvalidKind() throws Exceptio
   @Test
   public void testConstructValidateAndDecryptSTSTokenInvalidService() throws 
Exception {
     // Create a token with incorrect service
-    final String validTokenString = 
tokenSecretManager.createSTSTokenString(TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY,
-        ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY, 
clock.instant());
+    final String validTokenString = createStsTokenString();
 
     final Token<STSTokenIdentifier> validToken = new Token<>();
     validToken.decodeFromUrlString(validTokenString);
@@ -179,8 +178,7 @@ public void 
testConstructValidateAndDecryptSTSTokenInvalidService() throws Excep
   @Test
   public void testConstructValidateAndDecryptSTSTokenExpired() throws 
Exception {
     // Create a token that expires immediately (durationSeconds of 0)
-    final String tokenString = tokenSecretManager.createSTSTokenString(
-        TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, 0, SECRET_ACCESS_KEY, 
SESSION_POLICY, clock.instant());
+    final String tokenString = createStsTokenString(0, SESSION_POLICY);
 
     // Fast-forward time to ensure token is expired
     clock.fastForward(100);
@@ -196,8 +194,7 @@ public void 
testConstructValidateAndDecryptSTSTokenExpired() throws Exception {
   @Test
   public void testConstructValidateAndDecryptSTSTokenSecretKeyNotFound() 
throws Exception {
     // Create a valid token string
-    final String validTokenString = 
tokenSecretManager.createSTSTokenString(TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY,
-        ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY, 
clock.instant());
+    final String validTokenString = createStsTokenString();
 
     // Create a mock secret key client that returns null for the key
     final SecretKeyClient mockKeyClient = mock(SecretKeyClient.class);
@@ -215,8 +212,7 @@ public void 
testConstructValidateAndDecryptSTSTokenSecretKeyNotFound() throws Ex
   @Test
   public void testConstructValidateAndDecryptSTSTokenInvalidSecretKeyId() 
throws Exception {
     // Create a valid identifier to use as base
-    final String validTokenString = 
tokenSecretManager.createSTSTokenString(TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY,
-        ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY, 
clock.instant());
+    final String validTokenString = createStsTokenString();
 
     final Token<STSTokenIdentifier> validToken = new Token<>();
     validToken.decodeFromUrlString(validTokenString);
@@ -241,8 +237,7 @@ public void 
testConstructValidateAndDecryptSTSTokenInvalidSecretKeyId() throws E
   @Test
   public void testConstructValidateAndDecryptSTSTokenExpiredSecretKey() throws 
Exception {
     // Create a valid token string
-    final String validTokenString = 
tokenSecretManager.createSTSTokenString(TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY,
-        ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY, 
clock.instant());
+    final String validTokenString = createStsTokenString();
 
     // Create a mock secret key that is expired
     final ManagedSecretKey expiredSecretKey = mock(ManagedSecretKey.class);
@@ -264,8 +259,7 @@ public void 
testConstructValidateAndDecryptSTSTokenExpiredSecretKey() throws Exc
   @Test
   public void 
testConstructValidateAndDecryptSTSTokenSecretKeyRetrievalException() throws 
Exception {
     // Create a valid token string
-    final String validTokenString = 
tokenSecretManager.createSTSTokenString(TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY,
-        ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY, 
clock.instant());
+    final String validTokenString = createStsTokenString();
 
     // Create a mock secret key client that throws an exception
     final SecretKeyClient mockKeyClient = mock(SecretKeyClient.class);
@@ -283,8 +277,7 @@ public void 
testConstructValidateAndDecryptSTSTokenSecretKeyRetrievalException()
   @Test
   public void testConstructValidateAndDecryptSTSTokenInvalidSignature() throws 
Exception {
     // Create a valid token string
-    final String validTokenString = 
tokenSecretManager.createSTSTokenString(TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY,
-        ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY, 
clock.instant());
+    final String validTokenString = createStsTokenString();
 
     final Token<STSTokenIdentifier> validToken = new Token<>();
     validToken.decodeFromUrlString(validTokenString);
@@ -305,9 +298,7 @@ public void 
testConstructValidateAndDecryptSTSTokenInvalidSignature() throws Exc
 
   @Test
   public void testConstructValidateAndDecryptSTSTokenRejectsDoubledToken() 
throws Exception {
-    final String tokenString = tokenSecretManager.createSTSTokenString(
-        TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
-        SECRET_ACCESS_KEY, SESSION_POLICY, clock.instant());
+    final String tokenString = createStsTokenString();
 
     assertThatThrownBy(() ->
         STSSecurityUtil.constructValidateAndDecryptSTSToken(tokenString + 
tokenString, secretKeyClient, clock))
@@ -318,9 +309,7 @@ public void 
testConstructValidateAndDecryptSTSTokenRejectsDoubledToken() throws
 
   @Test
   public void testConstructValidateAndDecryptSTSTokenRejectsTokenWithSuffix() 
throws Exception {
-    final String tokenString = tokenSecretManager.createSTSTokenString(
-        TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
-        SECRET_ACCESS_KEY, SESSION_POLICY, clock.instant());
+    final String tokenString = createStsTokenString();
 
     assertThatThrownBy(() ->
         STSSecurityUtil.constructValidateAndDecryptSTSToken(tokenString + 
"garbage", secretKeyClient, clock))
@@ -342,13 +331,11 @@ public void 
testConstructValidateAndDecryptSTSTokenEmptyString() {
   @Test
   public void testConstructValidateAndDecryptMultipleTokens() throws Exception 
{
     // Create multiple tokens and validate them all
-    final String token1 = tokenSecretManager.createSTSTokenString(
-        "temp-key-1", "orig-key-1", "role-arn-1", DURATION_SECONDS,
-        "secret-key-1", "policy-1", clock.instant());
+    final String token1 = createStsTokenString(DURATION_SECONDS, 
"secret-key-1", "policy-1",
+        "temp-key-1", "orig-key-1", "role-arn-1");
 
-    final String token2 = tokenSecretManager.createSTSTokenString(
-        "temp-key-2", "orig-key-2", "role-arn-2", DURATION_SECONDS,
-        "secret-key-2", "policy-2", clock.instant());
+    final String token2 = createStsTokenString(DURATION_SECONDS, 
"secret-key-2", "policy-2",
+        "temp-key-2", "orig-key-2", "role-arn-2");
 
     final STSTokenIdentifier result1 = 
STSSecurityUtil.constructValidateAndDecryptSTSToken(
         token1, secretKeyClient, clock);
@@ -418,8 +405,7 @@ public void 
testEnsureEssentialFieldsArePresentInTokenMissingCreationTime() {
 
   @Test
   public void testEnsureResolvedStsFieldsInvariantsSuccess() throws Exception {
-    final String tokenString = 
tokenSecretManager.createSTSTokenString(TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY,
-        ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY, 
clock.instant());
+    final String tokenString = createStsTokenString();
 
     final S3Authentication s3Auth = S3Authentication.newBuilder()
         .setSessionToken(tokenString)
@@ -458,9 +444,7 @@ public void 
testEnsureResolvedStsFieldsInvariantsMissingSessionToken() {
 
   @Test
   public void testEnsureResolvedStsFieldsInvariantsMissingResolvedFields() 
throws Exception {
-    final String tokenString = tokenSecretManager.createSTSTokenString(
-        TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS,
-        SECRET_ACCESS_KEY, SESSION_POLICY, clock.instant());
+    final String tokenString = createStsTokenString();
 
     final S3Authentication s3Auth = S3Authentication.newBuilder()
         .setSessionToken(tokenString)
@@ -488,6 +472,32 @@ public void 
testEnsureResolvedStsFieldsInvariantsNoS3Auth() throws Exception {
     STSSecurityUtil.ensureResolvedStsFieldsInvariants(request);
   }
 
+  private String createStsTokenString() throws IOException {
+    return createStsTokenString(DURATION_SECONDS, SECRET_ACCESS_KEY, 
SESSION_POLICY,
+        TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN);
+  }
+
+  private String createStsTokenString(int durationSeconds, String 
sessionPolicy)
+      throws IOException {
+    return createStsTokenString(durationSeconds, 
TestSTSSecurityUtil.SECRET_ACCESS_KEY, sessionPolicy,
+        TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN);
+  }
+
+  private String createStsTokenString(int durationSeconds, String 
secretAccessKey, String sessionPolicy,
+      String tempAccessKey, String originalAccessKey, String roleArn) throws 
IOException {
+    return 
tokenSecretManager.createSTSTokenString(STSTokenSecretManager.CreateSTSTokenParams.newBuilder()
+        .setTempAccessKeyId(tempAccessKey)
+        .setOriginalAccessKeyId(originalAccessKey)
+        .setRoleArn(roleArn)
+        .setDurationSeconds(durationSeconds)
+        .setSecretAccessKey(secretAccessKey)
+        .setSessionPolicy(sessionPolicy)
+        .setAssumedRoleId(ASSUMED_ROLE_ID)
+        .setAssumedRoleUserArn(ASSUMED_ROLE_USER_ARN)
+        .setCreationTime(clock.instant())
+        .build());
+  }
+
   private STSTokenIdentifier.Params.Builder paramsBuilder() {
     return STSTokenIdentifier.Params.newBuilder()
         .setTempAccessKeyId(TEMP_ACCESS_KEY)
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenIdentifier.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenIdentifier.java
index c2136388e2a..ee2863e6259 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenIdentifier.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenIdentifier.java
@@ -77,6 +77,8 @@ public void testProtoBufRoundTrip() throws IOException {
         .setSecretAccessKey("secretKey")
         .setSessionPolicy("sessionPolicy")
         .setManagedSecretKey(MANAGED_SECRET_KEY)
+        .setAssumedRoleId("AROATEST123456789:testsess")
+        
.setAssumedRoleUserArn("arn:aws:sts::123456789012:assumed-role/RoleY/testsess")
         .build());
     final UUID secretKeyId = MANAGED_SECRET_KEY.getId();
 
@@ -89,6 +91,9 @@ public void testProtoBufRoundTrip() throws IOException {
     
assertThat(proto.getRoleArn()).isEqualTo("arn:aws:iam::123456789012:role/RoleY");
     assertThat(proto.getSecretAccessKey()).isNotEqualTo("secretKey");   // 
must be encrypted
     assertThat(proto.getSessionPolicy()).isEqualTo("sessionPolicy");
+    
assertThat(proto.getAssumedRoleId()).isEqualTo("AROATEST123456789:testsess");
+    assertThat(proto.getAssumedRoleUserArn())
+        .isEqualTo("arn:aws:sts::123456789012:assumed-role/RoleY/testsess");
     assertThat(proto.getSecretKeyId()).isEqualTo(secretKeyId.toString());
 
     final STSTokenIdentifier parsedTokenIdentifier = new STSTokenIdentifier();
@@ -103,6 +108,9 @@ public void testProtoBufRoundTrip() throws IOException {
     
assertThat(parsedTokenIdentifier.getSecretAccessKey()).isEqualTo("secretKey");
     assertThat(parsedTokenIdentifier.getSecretKeyId()).isEqualTo(secretKeyId);
     
assertThat(parsedTokenIdentifier.getSessionPolicy()).isEqualTo("sessionPolicy");
+    
assertThat(parsedTokenIdentifier.getAssumedRoleId()).isEqualTo("AROATEST123456789:testsess");
+    assertThat(parsedTokenIdentifier.getAssumedRoleUserArn())
+        .isEqualTo("arn:aws:sts::123456789012:assumed-role/RoleY/testsess");
     assertThat(parsedTokenIdentifier).isEqualTo(originalTokenIdentifier);
     
assertThat(parsedTokenIdentifier.hashCode()).isEqualTo(originalTokenIdentifier.hashCode());
   }
@@ -211,6 +219,8 @@ public void testWriteToAndReadFromByteArray() throws 
Exception {
         .setSecretAccessKey("secretAccessKey")
         .setSessionPolicy("sessionPolicy")
         .setManagedSecretKey(MANAGED_SECRET_KEY)
+        .setAssumedRoleId("AROATEST123456789:testsess")
+        
.setAssumedRoleUserArn("arn:aws:sts::123456789012:assumed-role/test-role/testsess")
         .build());
 
     final ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -541,12 +551,16 @@ public void testToString() {
         .setExpiry(expiry)
         .setSecretAccessKey("secretAccessKey")
         .setSessionPolicy("sessionPolicy")
+        .setAssumedRoleId("AROATEST123456789:testsess")
+        
.setAssumedRoleUserArn("arn:aws:sts::123456789012:assumed-role/test-role/testsess")
         .build());
     stsTokenIdentifier.setSecretKeyId(uuid);
 
     final String stsTokenIdentifierStr = stsTokenIdentifier.toString();
     final String expectedString = "STSTokenIdentifier{" + 
"tempAccessKeyId='tempAccessKeyId'" +
         ", originalAccessKeyId='originalAccessKeyId'" + ", roleArn='roleArn'" +
+        ", assumedRoleId='AROATEST123456789:testsess'" +
+        ", 
assumedRoleUserArn='arn:aws:sts::123456789012:assumed-role/test-role/testsess'" 
+
         ", creationTime='" + CREATION_TIME + "', expiry='" + expiry +
         "', secretKeyId='" + uuid + "', sessionPolicy='sessionPolicy'" + '}';
 
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java
index fe20c9dd2a4..022dab45508 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java
@@ -56,6 +56,8 @@ public class TestSTSTokenSecretManager {
   private static final String ROLE_ARN = 
"arn:aws:iam::123456789012:role/test-role";
   private static final String SECRET_ACCESS_KEY = "test-secret-access-key";
   private static final String SESSION_POLICY = "test-session-policy";
+  private static final String ASSUMED_ROLE_ID = "AROATEST123456789:testsess";
+  private static final String ASSUMED_ROLE_USER_ARN = 
"arn:aws:sts::123456789012:assumed-role/test-role/testsess";
   private static final int DURATION_SECONDS = 3600;
 
   private static SecretKey sharedSecretKey;
@@ -84,8 +86,7 @@ public void setUp() throws Exception {
 
   @Test
   public void testCreateSTSTokenStringContainsCorrectFields() throws 
IOException {
-    final String tokenString = 
secretManager.createSTSTokenString(TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY,
-        ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY, 
clock.instant());
+    final String tokenString = 
secretManager.createSTSTokenString(createStsTokenParamsBuilder().build());
 
     // Decode the token
     final Token<STSTokenIdentifier> token = new Token<>();
@@ -104,6 +105,8 @@ public void testCreateSTSTokenStringContainsCorrectFields() 
throws IOException {
     assertEquals(ROLE_ARN, identifier.getRoleArn());
     assertEquals(SECRET_ACCESS_KEY, identifier.getSecretAccessKey());
     assertEquals(SESSION_POLICY, identifier.getSessionPolicy());
+    assertEquals(ASSUMED_ROLE_ID, identifier.getAssumedRoleId());
+    assertEquals(ASSUMED_ROLE_USER_ARN, identifier.getAssumedRoleUserArn());
     assertEquals(clock.instant(), identifier.getCreationTime());
     assertNotNull(identifier.getSecretKeyId());
     assertEquals(new Text("STSToken"), identifier.getKind());
@@ -114,7 +117,7 @@ public void testCreateSTSTokenStringContainsCorrectFields() 
throws IOException {
   @Test
   public void testCreateSTSTokenStringWithNullSessionPolicy() throws 
IOException {
     final String tokenString = secretManager.createSTSTokenString(
-        TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY, ROLE_ARN, DURATION_SECONDS, 
SECRET_ACCESS_KEY, null, clock.instant());
+        createStsTokenParamsBuilder().setSessionPolicy(null).build());
 
     // Decode the token
     final Token<STSTokenIdentifier> token = new Token<>();
@@ -150,8 +153,7 @@ public void 
testCreateSTSTokenStringValidatesWhenSecretKeyRotatesDuringCreation(
         encryptionKey, signingKey);
     final STSTokenSecretManager rotatingSecretManager = new 
STSTokenSecretManager(rotatingSecretKeyClient);
 
-    final String tokenString = 
rotatingSecretManager.createSTSTokenString(TEMP_ACCESS_KEY, ORIGINAL_ACCESS_KEY,
-        ROLE_ARN, DURATION_SECONDS, SECRET_ACCESS_KEY, SESSION_POLICY, 
clock.instant());
+    final String tokenString = 
rotatingSecretManager.createSTSTokenString(createStsTokenParamsBuilder().build());
 
     final STSTokenIdentifier result = 
STSSecurityUtil.constructValidateAndDecryptSTSToken(
         tokenString, rotatingSecretKeyClient, clock);
@@ -160,6 +162,19 @@ public void 
testCreateSTSTokenStringValidatesWhenSecretKeyRotatesDuringCreation(
     assertEquals(1, rotatingSecretKeyClient.getCurrentSecretKeyCallCount());
   }
 
+  private STSTokenSecretManager.CreateSTSTokenParams.Builder 
createStsTokenParamsBuilder() {
+    return STSTokenSecretManager.CreateSTSTokenParams.newBuilder()
+        .setTempAccessKeyId(TEMP_ACCESS_KEY)
+        .setOriginalAccessKeyId(ORIGINAL_ACCESS_KEY)
+        .setRoleArn(ROLE_ARN)
+        .setDurationSeconds(DURATION_SECONDS)
+        .setSecretAccessKey(SECRET_ACCESS_KEY)
+        .setSessionPolicy(SESSION_POLICY)
+        .setAssumedRoleId(ASSUMED_ROLE_ID)
+        .setAssumedRoleUserArn(ASSUMED_ROLE_USER_ARN)
+        .setCreationTime(clock.instant());
+  }
+
   private static ManagedSecretKey createManagedSecretKey(UUID id, byte[] 
keyBytes, Instant creationTime) {
     final SecretKey secretKey = new SecretKeySpec(keyBytes, "HmacSHA256");
     return new ManagedSecretKey(id, creationTime, 
creationTime.plus(Duration.ofHours(1)), secretKey);
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/audit/S3GAction.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/audit/S3GAction.java
index abdb64e5ff2..f215cd6cb0f 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/audit/S3GAction.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/audit/S3GAction.java
@@ -66,6 +66,7 @@ public enum S3GAction implements AuditAction {
 
   // STS endpoint
   ASSUME_ROLE,
+  GET_CALLER_IDENTITY,
 
   GET_OBJECT_ATTRIBUTES;
   
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3GActionIamMapper.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3GActionIamMapper.java
index 9953ebe2020..223b057b5bb 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3GActionIamMapper.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3GActionIamMapper.java
@@ -85,6 +85,7 @@ private S3GActionIamMapper() {
     case GENERATE_SECRET:
     case REVOKE_SECRET:
     case ASSUME_ROLE:
+    case GET_CALLER_IDENTITY:
     default:
       return null;
     }
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3AssumeRoleResponseXml.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3AssumeRoleResponseXml.java
index bd4be9a7eaf..6c8b73906a7 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3AssumeRoleResponseXml.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3AssumeRoleResponseXml.java
@@ -33,7 +33,7 @@ public class S3AssumeRoleResponseXml {
   private AssumeRoleResult assumeRoleResult;
 
   @XmlElement(name = "ResponseMetadata")
-  private ResponseMetadata responseMetadata;
+  private S3STSResponseMetadata responseMetadata;
 
   public AssumeRoleResult getAssumeRoleResult() {
     return assumeRoleResult;
@@ -43,11 +43,11 @@ public void setAssumeRoleResult(AssumeRoleResult 
assumeRoleResult) {
     this.assumeRoleResult = assumeRoleResult;
   }
 
-  public ResponseMetadata getResponseMetadata() {
+  public S3STSResponseMetadata getResponseMetadata() {
     return responseMetadata;
   }
 
-  public void setResponseMetadata(ResponseMetadata responseMetadata) {
+  public void setResponseMetadata(S3STSResponseMetadata responseMetadata) {
     this.responseMetadata = responseMetadata;
   }
 
@@ -157,23 +157,6 @@ public void setArn(String arn) {
       this.arn = arn;
     }
   }
-
-  /**
-   * ResponseMetadata element.
-   */
-  @XmlAccessorType(XmlAccessType.FIELD)
-  public static class ResponseMetadata {
-    @XmlElement(name = "RequestId")
-    private String requestId;
-
-    public String getRequestId() {
-      return requestId;
-    }
-
-    public void setRequestId(String requestId) {
-      this.requestId = requestId;
-    }
-  }
 }
 
 
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3GetCallerIdentityResponseXml.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3GetCallerIdentityResponseXml.java
new file mode 100644
index 00000000000..594ed12e55d
--- /dev/null
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3GetCallerIdentityResponseXml.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.s3sts;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+
+/**
+ * JAXB model for AWS STS GetCallerIdentityResponse.
+ */
+@XmlAccessorType(XmlAccessType.FIELD)
+@XmlRootElement(name = "GetCallerIdentityResponse", namespace = 
"https://sts.amazonaws.com/doc/2011-06-15/";)
+public class S3GetCallerIdentityResponseXml {
+
+  @XmlElement(name = "GetCallerIdentityResult")
+  private GetCallerIdentityResult getCallerIdentityResult;
+
+  @XmlElement(name = "ResponseMetadata")
+  private S3STSResponseMetadata responseMetadata;
+
+  public GetCallerIdentityResult getGetCallerIdentityResult() {
+    return getCallerIdentityResult;
+  }
+
+  public void setGetCallerIdentityResult(GetCallerIdentityResult 
getCallerIdentityResult) {
+    this.getCallerIdentityResult = getCallerIdentityResult;
+  }
+
+  public S3STSResponseMetadata getResponseMetadata() {
+    return responseMetadata;
+  }
+
+  public void setResponseMetadata(S3STSResponseMetadata responseMetadata) {
+    this.responseMetadata = responseMetadata;
+  }
+
+  /**
+   * GetCallerIdentityResult element.
+   */
+  @XmlAccessorType(XmlAccessType.FIELD)
+  public static class GetCallerIdentityResult {
+    @XmlElement(name = "Arn")
+    private String arn;
+
+    @XmlElement(name = "UserId")
+    private String userId;
+
+    @XmlElement(name = "Account")
+    private String account;
+
+    public String getArn() {
+      return arn;
+    }
+
+    public void setArn(String arn) {
+      this.arn = arn;
+    }
+
+    public String getUserId() {
+      return userId;
+    }
+
+    public void setUserId(String userId) {
+      this.userId = userId;
+    }
+
+    public String getAccount() {
+      return account;
+    }
+
+    public void setAccount(String account) {
+      this.account = account;
+    }
+  }
+}
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSEndpoint.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSEndpoint.java
index d6ed5339a44..2c6200d1e66 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSEndpoint.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSEndpoint.java
@@ -60,6 +60,7 @@
 import org.apache.hadoop.ozone.om.exceptions.OMException;
 import org.apache.hadoop.ozone.om.helpers.AssumeRoleResponseInfo;
 import org.apache.hadoop.ozone.om.helpers.AwsRoleArnValidator;
+import org.apache.hadoop.ozone.om.helpers.CallerIdentityInfo;
 import org.apache.hadoop.ozone.om.helpers.S3STSUtils;
 import org.apache.hadoop.ozone.s3.RequestIdentifier;
 import org.apache.hadoop.ozone.s3.exception.OS3Exception;
@@ -73,8 +74,8 @@
  * This endpoint provides temporary security credentials compatible with
  * AWS STS API, exposed on port 9880 or 9881 at the root path ({@code /}).
  * <p>
- * Currently supports only AssumeRole operation. Other STS operations will
- * return appropriate error responses.
+ * Currently supports AssumeRole and GetCallerIdentity operations. Other STS
+ * operations will return appropriate error responses.
  *
  * @see <a href="https://docs.aws.amazon.com/STS/latest/APIReference/";>AWS STS 
API Reference</a>
  */
@@ -113,7 +114,8 @@ public class S3STSEndpoint extends S3STSEndpointBase {
 
   static {
     try {
-      JAXB_CONTEXT = JAXBContext.newInstance(S3AssumeRoleResponseXml.class);
+      JAXB_CONTEXT = JAXBContext.newInstance(
+          S3AssumeRoleResponseXml.class, S3GetCallerIdentityResponseXml.class, 
S3STSResponseMetadata.class);
     } catch (JAXBException e) {
       throw new RuntimeException("Failed to initialize JAXBContext: " + e, e);
     }
@@ -193,11 +195,12 @@ private Response handleSTSRequest(Set<String> 
paramNamesToValidate, String actio
       case ASSUME_ROLE_ACTION:
         return handleAssumeRole(
             paramNamesToValidate, roleArn, roleSessionName, durationSeconds, 
awsIamSessionPolicy, version, requestId);
+      case GET_CALLER_IDENTITY_ACTION:
+        return handleGetCallerIdentity(version, requestId);
       // These operations are not supported yet
       case GET_SESSION_TOKEN_ACTION:
       case ASSUME_ROLE_WITH_SAML_ACTION:
       case ASSUME_ROLE_WITH_WEB_IDENTITY_ACTION:
-      case GET_CALLER_IDENTITY_ACTION:
       case DECODE_AUTHORIZATION_MESSAGE_ACTION:
       case GET_ACCESS_KEY_INFO_ACTION:
         throw new OSTSException(STS_INVALID_ACTION_NOT_IMPLEMENTED)
@@ -307,39 +310,76 @@ private Response handleAssumeRole(Set<String> 
paramNamesToValidate, String roleA
           .header("Content-Type", "text/xml")
           .build();
     } catch (IOException e) {
-      LOG.error("Error during AssumeRole processing", e);
-
+      throw toStsProcessingException(
+          S3GAction.ASSUME_ROLE, auditParams, e, action, "User is not 
authorized to perform: sts:AssumeRole on " +
+              "resource: " + roleArn);
+    } catch (Exception e) {
       
getAuditLogger().logWriteFailure(buildAuditMessageForFailure(S3GAction.ASSUME_ROLE,
 auditParams, e));
+      throw e;
+    }
+  }
 
-      if (e instanceof OMException) {
-        final OMException omException = (OMException) e;
-        if (omException.getResult() == OMException.ResultCodes.ACCESS_DENIED ||
-            omException.getResult() == 
OMException.ResultCodes.PERMISSION_DENIED ||
-            omException.getResult() == OMException.ResultCodes.TOKEN_EXPIRED) {
-          throw new OSTSException(ACCESS_DENIED)
-              .withMessage("User is not authorized to perform: sts:AssumeRole 
on resource: " + roleArn);
-        }
-        if (omException.getResult() == OMException.ResultCodes.INVALID_TOKEN) {
-          throw new OSTSException(STS_INVALID_CLIENT_TOKEN_ID);
-        }
-        if (omException.getResult() == 
OMException.ResultCodes.NOT_SUPPORTED_OPERATION ||
-            omException.getResult() == 
OMException.ResultCodes.FEATURE_NOT_ENABLED) {
-          throw new 
OSTSException(STS_UNSUPPORTED_OPERATION).withMessage(omException.getMessage());
-        }
-        if (omException.getResult() == 
OMException.ResultCodes.INVALID_REQUEST) {
-          throw new 
OSTSException(STS_VALIDATION_ERROR).withMessage(omException.getMessage());
-        }
-        if (omException.getResult() == 
OMException.ResultCodes.MALFORMED_POLICY_DOCUMENT) {
-          throw new 
OSTSException(STS_MALFORMED_POLICY_DOCUMENT).withMessage(omException.getMessage());
-        }
-      }
-      throw new OSTSException(STS_INTERNAL_FAILURE, e).withType("Receiver");
+  private Response handleGetCallerIdentity(String version, String requestId) 
throws OSTSException {
+    final String action = GET_CALLER_IDENTITY_ACTION;
+    final Map<String, String> auditParams = getAuditParameters();
+    auditParams.put("action", action);
+    auditParams.put("requestId", requestId);
+
+    if (version == null || !version.equals(EXPECTED_VERSION)) {
+      final OSTSException exception = new OSTSException(STS_INVALID_ACTION)
+          .withMessage("Could not find operation " + action + " for version " +
+              (version == null ? "NO_VERSION_SPECIFIED.  Expected version is: 
" + EXPECTED_VERSION : version));
+      getAuditLogger().logWriteFailure(buildAuditMessageForFailure(
+          S3GAction.GET_CALLER_IDENTITY, auditParams, exception));
+      throw exception;
+    }
+
+    try {
+      final CallerIdentityInfo identityInfo = 
getClient().getObjectStore().getCallerIdentity();
+      final String responseXml = 
generateGetCallerIdentityResponse(identityInfo, requestId);
+      
getAuditLogger().logWriteSuccess(buildAuditMessageForSuccess(S3GAction.GET_CALLER_IDENTITY,
 auditParams));
+      return Response.ok(responseXml)
+          .header("Content-Type", "text/xml")
+          .build();
+    } catch (IOException e) {
+      throw toStsProcessingException(
+          S3GAction.GET_CALLER_IDENTITY, auditParams, e, action, "User is not 
authorized to perform: " +
+              "sts:GetCallerIdentity");
     } catch (Exception e) {
-      
getAuditLogger().logWriteFailure(buildAuditMessageForFailure(S3GAction.ASSUME_ROLE,
 auditParams, e));
+      
getAuditLogger().logWriteFailure(buildAuditMessageForFailure(S3GAction.GET_CALLER_IDENTITY,
 auditParams, e));
       throw e;
     }
   }
 
+  private OSTSException toStsProcessingException(S3GAction auditAction, 
Map<String, String> auditParams, IOException e,
+      String operationName, String accessDeniedMessage) {
+    LOG.error("Error during {} processing", operationName, e);
+    getAuditLogger().logWriteFailure(buildAuditMessageForFailure(auditAction, 
auditParams, e));
+
+    if (e instanceof OMException) {
+      final OMException omException = (OMException) e;
+      if (omException.getResult() == OMException.ResultCodes.ACCESS_DENIED ||
+          omException.getResult() == OMException.ResultCodes.PERMISSION_DENIED 
||
+          omException.getResult() == OMException.ResultCodes.TOKEN_EXPIRED) {
+        return new 
OSTSException(ACCESS_DENIED).withMessage(accessDeniedMessage);
+      }
+      if (omException.getResult() == OMException.ResultCodes.INVALID_TOKEN) {
+        return new OSTSException(STS_INVALID_CLIENT_TOKEN_ID);
+      }
+      if (omException.getResult() == 
OMException.ResultCodes.NOT_SUPPORTED_OPERATION ||
+          omException.getResult() == 
OMException.ResultCodes.FEATURE_NOT_ENABLED) {
+        return new 
OSTSException(STS_UNSUPPORTED_OPERATION).withMessage(omException.getMessage());
+      }
+      if (omException.getResult() == OMException.ResultCodes.INVALID_REQUEST) {
+        return new 
OSTSException(STS_VALIDATION_ERROR).withMessage(omException.getMessage());
+      }
+      if (omException.getResult() == 
OMException.ResultCodes.MALFORMED_POLICY_DOCUMENT) {
+        return new 
OSTSException(STS_MALFORMED_POLICY_DOCUMENT).withMessage(omException.getMessage());
+      }
+    }
+    return new OSTSException(STS_INTERNAL_FAILURE, e).withType("Receiver");
+  }
+
   private AssumeRoleParamValidationResult 
validateAssumeRoleParameters(Set<String> paramNamesToValidate) {
     if (paramNamesToValidate == null || paramNamesToValidate.isEmpty()) {
       return AssumeRoleParamValidationResult.empty();
@@ -450,7 +490,7 @@ private String generateAssumeRoleResponse(String 
assumedRoleUserArn, AssumeRoleR
       user.setArn(assumedRoleUserArn);
       result.setAssumedRoleUser(user);
       response.setAssumeRoleResult(result);
-      final S3AssumeRoleResponseXml.ResponseMetadata meta = new 
S3AssumeRoleResponseXml.ResponseMetadata();
+      final S3STSResponseMetadata meta = new S3STSResponseMetadata();
       meta.setRequestId(requestId);
       response.setResponseMetadata(meta);
 
@@ -463,5 +503,29 @@ private String generateAssumeRoleResponse(String 
assumedRoleUserArn, AssumeRoleR
       throw new IOException("Failed to marshal AssumeRole response", e);
     }
   }
+
+  private String generateGetCallerIdentityResponse(CallerIdentityInfo 
identityInfo, String requestId)
+      throws IOException {
+    try {
+      final S3GetCallerIdentityResponseXml response = new 
S3GetCallerIdentityResponseXml();
+      final S3GetCallerIdentityResponseXml.GetCallerIdentityResult result =
+          new S3GetCallerIdentityResponseXml.GetCallerIdentityResult();
+      result.setAccount(identityInfo.getAccount());
+      result.setArn(identityInfo.getArn());
+      result.setUserId(identityInfo.getUserId());
+      response.setGetCallerIdentityResult(result);
+      final S3STSResponseMetadata meta = new S3STSResponseMetadata();
+      meta.setRequestId(requestId);
+      response.setResponseMetadata(meta);
+
+      final Marshaller marshaller = JAXB_CONTEXT.createMarshaller();
+      marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
+      final StringWriter stringWriter = new StringWriter();
+      marshaller.marshal(response, stringWriter);
+      return stringWriter.toString();
+    } catch (JAXBException e) {
+      throw new IOException("Failed to marshal GetCallerIdentity response", e);
+    }
+  }
 }
 
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSResponseMetadata.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSResponseMetadata.java
new file mode 100644
index 00000000000..a43ec76b443
--- /dev/null
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3sts/S3STSResponseMetadata.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.s3sts;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlType;
+
+/**
+ * JAXB model for AWS STS ResponseMetadata element shared across STS responses.
+ */
+@XmlAccessorType(XmlAccessType.FIELD)
+@XmlType(name = "ResponseMetadata", namespace = 
"https://sts.amazonaws.com/doc/2011-06-15/";)
+public class S3STSResponseMetadata {
+
+  @XmlElement(name = "RequestId")
+  private String requestId;
+
+  public String getRequestId() {
+    return requestId;
+  }
+
+  public void setRequestId(String requestId) {
+    this.requestId = requestId;
+  }
+}
diff --git 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
index abd80cbc1fc..6fd318dc906 100644
--- 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
+++ 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/client/ClientProtocolStub.java
@@ -39,6 +39,7 @@
 import org.apache.hadoop.ozone.client.protocol.ClientProtocol;
 import org.apache.hadoop.ozone.client.protocol.ListStatusLightOptions;
 import org.apache.hadoop.ozone.om.helpers.AssumeRoleResponseInfo;
+import org.apache.hadoop.ozone.om.helpers.CallerIdentityInfo;
 import org.apache.hadoop.ozone.om.helpers.DeleteTenantState;
 import org.apache.hadoop.ozone.om.helpers.ErrorInfo;
 import org.apache.hadoop.ozone.om.helpers.LeaseKeyInfo;
@@ -898,6 +899,11 @@ public AssumeRoleResponseInfo assumeRole(String roleArn, 
String roleSessionName,
     return null;
   }
 
+  @Override
+  public CallerIdentityInfo getCallerIdentity() throws IOException {
+    return null;
+  }
+
   @Override
   public void revokeSTSToken(String originalAccessKeyId) throws IOException {
   }
diff --git 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestS3GActionIamMapper.java
 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestS3GActionIamMapper.java
index c7ae9e4e924..82f0134cffe 100644
--- 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestS3GActionIamMapper.java
+++ 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestS3GActionIamMapper.java
@@ -62,6 +62,7 @@ public void copyActionsReturnNull() {
   @Test
   public void nonIamActionsReturnNull() {
     assertNull(S3GActionIamMapper.toS3ActionString(S3GAction.ASSUME_ROLE));
+    
assertNull(S3GActionIamMapper.toS3ActionString(S3GAction.GET_CALLER_IDENTITY));
     assertNull(S3GActionIamMapper.toS3ActionString(S3GAction.GENERATE_SECRET));
     assertNull(S3GActionIamMapper.toS3ActionString(S3GAction.REVOKE_SECRET));
   }
diff --git 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3sts/TestS3STSEndpoint.java
 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3sts/TestS3STSEndpoint.java
index 36adf2359c4..379bd27eb98 100644
--- 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3sts/TestS3STSEndpoint.java
+++ 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3sts/TestS3STSEndpoint.java
@@ -51,6 +51,7 @@
 import org.apache.hadoop.ozone.client.OzoneClientStub;
 import org.apache.hadoop.ozone.om.exceptions.OMException;
 import org.apache.hadoop.ozone.om.helpers.AssumeRoleResponseInfo;
+import org.apache.hadoop.ozone.om.helpers.CallerIdentityInfo;
 import org.apache.hadoop.ozone.s3.OzoneConfigurationHolder;
 import org.apache.hadoop.ozone.s3.RequestIdentifier;
 import org.apache.hadoop.ozone.s3.exception.OSTSException;
@@ -103,6 +104,11 @@ public void setup() throws Exception {
             "session-token",
             Instant.now().plusSeconds(3600).getEpochSecond(),
             "AROA1234567890123456:test-session"));
+    when(objectStore.getCallerIdentity())
+        .thenReturn(new CallerIdentityInfo(
+                "123456789012",
+                "arn:aws:iam::123456789012:user/test-user",
+                "test-user"));
     when(clientStub.getObjectStore()).thenReturn(objectStore);
 
     endpoint = new S3STSEndpoint();
@@ -560,6 +566,79 @@ public void testStsWhenActionNotImplemented() throws 
Exception {
         "Operation GetSessionToken is not supported yet.");
   }
 
+  @Test
+  public void testStsGetCallerIdentitySuccessForGetMethod() throws Exception {
+    final Response response = endpoint.get("GetCallerIdentity", null, null, 
null, "2011-06-15", null);
+
+    assertEquals(200, response.getStatus());
+    verify(objectStore).getCallerIdentity();
+    verify(auditLogger).logWriteSuccess(any(AuditMessage.class));
+    verify(auditLogger, never()).logWriteFailure(any(AuditMessage.class));
+
+    final Document doc = parseXml((String) response.getEntity());
+    assertEquals("GetCallerIdentityResponse", 
doc.getDocumentElement().getLocalName());
+    assertEquals(STS_NS, doc.getDocumentElement().getNamespaceURI());
+    assertEquals(
+        "123456789012", doc.getElementsByTagNameNS(STS_NS, 
"Account").item(0).getTextContent());
+    assertEquals(
+        "arn:aws:iam::123456789012:user/test-user", 
doc.getElementsByTagNameNS(STS_NS, "Arn").item(0).getTextContent());
+    assertEquals(
+        "test-user", doc.getElementsByTagNameNS(STS_NS, 
"UserId").item(0).getTextContent());
+  }
+
+  @Test
+  public void testStsGetCallerIdentityIgnoresExtraParameters() throws 
Exception {
+    final Response response = endpoint.get("GetCallerIdentity", ROLE_ARN, 
ROLE_SESSION_NAME, 3600, "2011-06-15", null);
+
+    assertEquals(200, response.getStatus());
+    verify(objectStore).getCallerIdentity();
+  }
+
+  @Test
+  public void testStsGetCallerIdentityIgnoresExtraParametersForPostMethod() 
throws Exception {
+    formParameters = new Form();
+    formParameters.param("Action", "GetCallerIdentity");
+    formParameters.param("Version", "2011-06-15");
+    formParameters.param("RoleArn", ROLE_ARN);
+    formParameters.param("RoleSessionName", ROLE_SESSION_NAME);
+    formParameters.param("DurationSeconds", "3600");
+
+    final Response response = endpoint.post(formParameters);
+
+    assertEquals(200, response.getStatus());
+    verify(objectStore).getCallerIdentity();
+  }
+
+  @Test
+  public void testStsGetCallerIdentityRejectsMissingVersion() throws Exception 
{
+    final OSTSException ex = assertThrows(
+        OSTSException.class, () -> endpoint.get("GetCallerIdentity", null, 
null, null, null, null));
+
+    assertEquals(400, ex.getHttpCode());
+    verify(auditLogger).logWriteFailure(any(AuditMessage.class));
+    verify(objectStore, never()).getCallerIdentity();
+
+    ex.setRequestId(REQUEST_ID);
+    assertStsErrorXml(
+        ex.toXml(), AWS_FAULT_NS, "Sender", "InvalidAction",
+        "Could not find operation GetCallerIdentity for version 
NO_VERSION_SPECIFIED");
+  }
+
+  @Test
+  public void testStsGetCallerIdentityRejectsInvalidVersion() throws Exception 
{
+    final OSTSException ex = assertThrows(
+        OSTSException.class, () -> endpoint.get("GetCallerIdentity", null, 
null, null, "2020-01-01", null));
+
+    assertEquals(400, ex.getHttpCode());
+    verify(auditLogger).logWriteFailure(any(AuditMessage.class));
+    verify(objectStore, never()).getCallerIdentity();
+
+    ex.setRequestId(REQUEST_ID);
+    assertStsErrorXml(
+        ex.toXml(), AWS_FAULT_NS, "Sender", "InvalidAction",
+        "Could not find operation GetCallerIdentity for version 2020-01-01");
+  }
+
   @Test
   public void testStsMissingRoleSessionName() throws Exception {
     final OSTSException ex = assertThrows(OSTSException.class, () ->


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to