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

sodonnell pushed a commit to branch HDDS-13323-sts
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/HDDS-13323-sts by this push:
     new 7eaebe6e428 HDDS-13888. [STS] Introduce S3AssumeRoleRequest and 
S3AssumeRoleResponse (#9276)
7eaebe6e428 is described below

commit 7eaebe6e428e2caab816f2f57a50ddcdd1854922
Author: fmorg-git <[email protected]>
AuthorDate: Fri Nov 14 09:05:46 2025 -0800

    HDDS-13888. [STS] Introduce S3AssumeRoleRequest and S3AssumeRoleResponse 
(#9276)
---
 .../request/s3/security/AwsRoleArnValidator.java   | 145 ++++++++
 .../request/s3/security/S3AssumeRoleRequest.java   | 222 +++++++++++++
 .../response/s3/security/S3AssumeRoleResponse.java |  42 +++
 .../request/s3/security/S3SecurityTestUtils.java   |  44 +++
 .../s3/security/TestAwsRoleArnValidator.java       | 131 ++++++++
 .../s3/security/TestS3AssumeRoleRequest.java       | 364 +++++++++++++++++++++
 .../ozone/om/response/TestCleanupTableInfo.java    |   2 +
 .../s3/security/TestS3AssumeRoleResponse.java      | 133 ++++++++
 8 files changed, 1083 insertions(+)

diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/AwsRoleArnValidator.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/AwsRoleArnValidator.java
new file mode 100644
index 00000000000..1f5af2fcc59
--- /dev/null
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/AwsRoleArnValidator.java
@@ -0,0 +1,145 @@
+/*
+ * 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.request.s3.security;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+
+/**
+ * Validator for AWS IAM Role ARNs and extracts the role name from them.
+ */
+public final class AwsRoleArnValidator {
+
+  private static final int ASSUME_ROLE_NAME_MAX_LENGTH = 64;
+  private static final int ASSUME_ROLE_ARN_MIN_LENGTH = 20;
+  private static final int ASSUME_ROLE_ARN_MAX_LENGTH = 2048;
+
+  private AwsRoleArnValidator() {
+  }
+
+  /**
+   * Extract the role name from an AWS-style role ARN, falling back to the
+   * full ARN if parsing is not possible.
+   * Examples:
+   * <pre>{@code
+   * arn:aws:iam::123456789012:role/RoleA -> RoleA
+   * arn:aws:iam::123456789012:role/path/RoleB -> RoleB
+   * }</pre>
+   *
+   * @param roleArn the AWS role ARN to validate and extract from
+   * @return the extracted role name
+   * @throws OMException if the ARN is invalid
+   */
+  public static String validateAndExtractRoleNameFromArn(String roleArn) 
throws OMException {
+    if (StringUtils.isBlank(roleArn)) {
+      throw new OMException("Role ARN is required", 
OMException.ResultCodes.INVALID_REQUEST);
+    }
+
+    final int roleArnLength = roleArn.length();
+    if (roleArnLength < ASSUME_ROLE_ARN_MIN_LENGTH || roleArnLength > 
ASSUME_ROLE_ARN_MAX_LENGTH) {
+      throw new OMException(
+          "Role ARN length must be between " + ASSUME_ROLE_ARN_MIN_LENGTH + " 
and " +
+          ASSUME_ROLE_ARN_MAX_LENGTH, OMException.ResultCodes.INVALID_REQUEST);
+    }
+
+    // Expected format: arn:aws:iam::123456789012:role/[optional path 
segments/]RoleName
+    if (!roleArn.startsWith("arn:aws:iam::")) {
+      throw new OMException(
+          "Invalid role ARN (does not start with arn:aws:iam::): " + roleArn, 
OMException.ResultCodes.INVALID_REQUEST);
+    }
+
+    // Split ARN into parts: arn:aws:iam::accountId:role/path/name
+    final String[] parts = roleArn.split(":", 6);
+    if (parts.length < 6 || !parts[5].startsWith("role/")) {
+      throw new OMException(
+          "Invalid role ARN (unexpected field count): " + roleArn, 
OMException.ResultCodes.INVALID_REQUEST);
+    }
+
+    // Validate account ID (12 digits)
+    final String accountId = parts[4];
+    if (accountId.length() != 12 || !isAllDigits(accountId)) {
+      throw new OMException("Invalid AWS account ID in ARN", 
OMException.ResultCodes.INVALID_REQUEST);
+    }
+
+    // Extract role name (last segment after last slash)
+    final String rolePath = parts[5].substring(5); // Skip "role/"
+    if (rolePath.isEmpty() || rolePath.endsWith("/")) {
+      throw new OMException("Invalid role ARN: missing role name", 
OMException.ResultCodes.INVALID_REQUEST);
+    }
+
+    final String[] pathSegments = rolePath.split("/");
+    final String roleName = pathSegments[pathSegments.length - 1];
+
+    // Validate role name
+    if (roleName.isEmpty() || roleName.length() > ASSUME_ROLE_NAME_MAX_LENGTH 
||
+        hasCharNotAllowedInIamRoleArn(roleName)) {
+      throw new OMException("Invalid role name: " + roleName, 
OMException.ResultCodes.INVALID_REQUEST);
+    }
+
+    // Validate path segments if present
+    if (pathSegments.length > 1) {
+      final String pathPrefix = rolePath.substring(0, 
rolePath.lastIndexOf('/') + 1);
+      if (pathPrefix.length() > 511) {
+        throw new OMException(
+            "Role path length must be between 1 and 512 characters", 
OMException.ResultCodes.INVALID_REQUEST);
+      }
+      for (String segment : pathSegments) {
+        if (segment.isEmpty() || hasCharNotAllowedInIamRoleArn(segment)) {
+          throw new OMException("Invalid role path segment: " + segment, 
OMException.ResultCodes.INVALID_REQUEST);
+        }
+      }
+    }
+
+    return roleName;
+  }
+
+  /**
+   * Checks if all the characters in a String are numbers.
+   */
+  private static boolean isAllDigits(String s) {
+    for (int i = 0; i < s.length(); i++) {
+      if (!Character.isDigit(s.charAt(i))) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Checks if supplied string contains a char that is not allowed in IAM Role 
ARN.
+   */
+  private static boolean hasCharNotAllowedInIamRoleArn(String s) {
+    for (int i = 0; i < s.length(); i++) {
+      if (!isCharAllowedInIamRoleArn(s.charAt(i))) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Checks if the supplied char is allowed in IAM Role ARN.
+   */
+  private static boolean isCharAllowedInIamRoleArn(char c) {
+    return (c >= 'A' && c <= 'Z')
+        || (c >= 'a' && c <= 'z')
+        || (c >= '0' && c <= '9')
+        || c == '+' || c == '=' || c == ',' || c == '.' || c == '@' || c == 
'_' || c == '-';
+  }
+}
+
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
new file mode 100644
index 00000000000..dc31644c806
--- /dev/null
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3AssumeRoleRequest.java
@@ -0,0 +1,222 @@
+/*
+ * 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.request.s3.security;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.security.SecureRandom;
+import java.time.Instant;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.ipc.ProtobufRpcEngine;
+import org.apache.hadoop.ozone.om.OzoneAclUtils;
+import org.apache.hadoop.ozone.om.OzoneManager;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.apache.hadoop.ozone.om.execution.flowcontrol.ExecutionContext;
+import org.apache.hadoop.ozone.om.request.OMClientRequest;
+import org.apache.hadoop.ozone.om.request.util.OmResponseUtil;
+import org.apache.hadoop.ozone.om.response.OMClientResponse;
+import org.apache.hadoop.ozone.om.response.s3.security.S3AssumeRoleResponse;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.AssumeRoleRequest;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.AssumeRoleResponse;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
+import org.apache.hadoop.security.UserGroupInformation;
+
+/**
+ * Handles S3AssumeRoleRequest request.
+ */
+public class S3AssumeRoleRequest extends OMClientRequest {
+
+  private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+
+  private static final int MIN_TOKEN_EXPIRATION_SECONDS = 900;    // 15 
minutes in seconds
+  private static final int MAX_TOKEN_EXPIRATION_SECONDS = 43200;  // 12 hours 
in seconds
+  private static final String STS_TOKEN_PREFIX = "ASIA";
+  private static final int STS_ACCESS_KEY_ID_LENGTH = 20;
+  private static final int STS_SECRET_ACCESS_KEY_LENGTH = 40;
+  private static final int STS_ROLE_ID_LENGTH = 16;
+  private static final String ASSUME_ROLE_ID_PREFIX = "AROA";
+  private static final int ASSUME_ROLE_SESSION_NAME_MIN_LENGTH = 2;
+  private static final int ASSUME_ROLE_SESSION_NAME_MAX_LENGTH = 64;
+  private static final String CHARS_FOR_ACCESS_KEY_IDS = 
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+  private static final int CHARS_FOR_ACCESS_KEY_IDS_LENGTH = 
CHARS_FOR_ACCESS_KEY_IDS.length();
+  private static final String CHARS_FOR_SECRET_ACCESS_KEYS = 
CHARS_FOR_ACCESS_KEY_IDS +
+      "abcdefghijklmnopqrstuvwxyz/+";
+  private static final int CHARS_FOR_SECRET_ACCESS_KEYS_LENGTH = 
CHARS_FOR_SECRET_ACCESS_KEYS.length();
+
+  public S3AssumeRoleRequest(OMRequest omRequest) {
+    super(omRequest);
+  }
+
+  @Override
+  public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, 
ExecutionContext context) {
+    final OMRequest omRequest = getOmRequest();
+    final AssumeRoleRequest assumeRoleRequest = 
omRequest.getAssumeRoleRequest();
+    final int durationSeconds = assumeRoleRequest.getDurationSeconds();
+
+    // Validate duration
+    if (durationSeconds < MIN_TOKEN_EXPIRATION_SECONDS || durationSeconds > 
MAX_TOKEN_EXPIRATION_SECONDS) {
+      final OMException omException = new OMException(
+          "Duration must be between " + MIN_TOKEN_EXPIRATION_SECONDS + " and " 
+ MAX_TOKEN_EXPIRATION_SECONDS,
+          OMException.ResultCodes.INVALID_REQUEST);
+      return new S3AssumeRoleResponse(
+          
createErrorOMResponse(OmResponseUtil.getOMResponseBuilder(omRequest), 
omException));
+    }
+
+    // Validate role session name
+    final String roleSessionName = assumeRoleRequest.getRoleSessionName();
+    final S3AssumeRoleResponse roleSessionNameErrorResponse = 
validateRoleSessionName(roleSessionName, omRequest);
+    if (roleSessionNameErrorResponse != null) {
+      return roleSessionNameErrorResponse;
+    }
+
+    final String roleArn = assumeRoleRequest.getRoleArn();
+    try {
+      // Validate role ARN and extract role
+      final String targetRoleName = 
AwsRoleArnValidator.validateAndExtractRoleNameFromArn(roleArn);
+
+      if (!omRequest.hasS3Authentication()) {
+        final String msg = "S3AssumeRoleRequest does not have S3 
authentication";
+        final OMException omException = new OMException(msg, 
OMException.ResultCodes.INVALID_REQUEST);
+        return new S3AssumeRoleResponse(
+            
createErrorOMResponse(OmResponseUtil.getOMResponseBuilder(omRequest), 
omException));
+      }
+
+      // Generate temporary AWS credentials using cryptographically strong 
SecureRandom
+      final String tempAccessKeyId = STS_TOKEN_PREFIX + 
generateSecureRandomStringUsingChars(
+          CHARS_FOR_ACCESS_KEY_IDS, CHARS_FOR_ACCESS_KEY_IDS_LENGTH, 
STS_ACCESS_KEY_ID_LENGTH);
+      final String secretAccessKey = generateSecureRandomStringUsingChars(
+          CHARS_FOR_SECRET_ACCESS_KEYS, CHARS_FOR_SECRET_ACCESS_KEYS_LENGTH, 
STS_SECRET_ACCESS_KEY_LENGTH);
+      final String sessionToken = generateSessionToken(
+          targetRoleName, omRequest, ozoneManager, assumeRoleRequest, 
secretAccessKey);
+
+      // Generate AssumedRoleId for response
+      final String roleId = ASSUME_ROLE_ID_PREFIX + 
generateSecureRandomStringUsingChars(
+          CHARS_FOR_ACCESS_KEY_IDS, CHARS_FOR_ACCESS_KEY_IDS_LENGTH, 
STS_ROLE_ID_LENGTH);
+      final String assumedRoleId = roleId + ":" + roleSessionName;
+
+      // Calculate expiration of session token
+      final long expirationEpochSeconds = 
Instant.now().plusSeconds(durationSeconds).getEpochSecond();
+
+      final AssumeRoleResponse.Builder responseBuilder = 
AssumeRoleResponse.newBuilder()
+          .setAccessKeyId(tempAccessKeyId)
+          .setSecretAccessKey(secretAccessKey)
+          .setSessionToken(sessionToken)
+          .setExpirationEpochSeconds(expirationEpochSeconds)
+          .setAssumedRoleId(assumedRoleId);
+
+      return new S3AssumeRoleResponse(
+          OmResponseUtil.getOMResponseBuilder(omRequest)
+              .setAssumeRoleResponse(responseBuilder.build())
+              .build());
+    } catch (OMException e) {
+      return new 
S3AssumeRoleResponse(createErrorOMResponse(OmResponseUtil.getOMResponseBuilder(omRequest),
 e));
+    } catch (IOException e) {
+      final OMException omException = new OMException(
+          "Failed to generate STS token for role: " + roleArn, e, 
OMException.ResultCodes.INTERNAL_ERROR);
+      return new S3AssumeRoleResponse(
+          
createErrorOMResponse(OmResponseUtil.getOMResponseBuilder(omRequest), 
omException));
+    }
+  }
+
+  /**
+   * Ensures RoleSessionName is valid.
+   */
+  private S3AssumeRoleResponse validateRoleSessionName(String roleSessionName, 
OMRequest omRequest) {
+    if (StringUtils.isBlank(roleSessionName)) {
+      final OMException omException = new OMException(
+          "RoleSessionName is required", 
OMException.ResultCodes.INVALID_REQUEST);
+      return new S3AssumeRoleResponse(
+          
createErrorOMResponse(OmResponseUtil.getOMResponseBuilder(omRequest), 
omException));
+    }
+    if (roleSessionName.length() < ASSUME_ROLE_SESSION_NAME_MIN_LENGTH ||
+        roleSessionName.length() > ASSUME_ROLE_SESSION_NAME_MAX_LENGTH) {
+      final OMException omException = new OMException(
+          "RoleSessionName length must be between " + 
ASSUME_ROLE_SESSION_NAME_MIN_LENGTH + " and " +
+          ASSUME_ROLE_SESSION_NAME_MAX_LENGTH, 
OMException.ResultCodes.INVALID_REQUEST);
+      return new S3AssumeRoleResponse(
+          
createErrorOMResponse(OmResponseUtil.getOMResponseBuilder(omRequest), 
omException));
+    }
+    return null;
+  }
+
+  /**
+   * Generates session token using components from the AssumeRoleRequest.
+   */
+  private String generateSessionToken(String targetRoleName, OMRequest 
omRequest,
+      OzoneManager ozoneManager, AssumeRoleRequest assumeRoleRequest, String 
secretAccessKey) throws IOException {
+
+    InetAddress remoteIp = ProtobufRpcEngine.Server.getRemoteIp();
+    if (remoteIp == null) {
+      remoteIp = ozoneManager.getOmRpcServerAddr().getAddress();
+    }
+
+    final String hostName = remoteIp != null ? remoteIp.getHostName() :
+        ozoneManager.getOmRpcServerAddr().getHostName();
+
+    // Determine the caller's access key ID - this will be referred to as the 
original
+    // access key id.  When STS tokens are used, the tokens will be authorized 
as
+    // the kerberos principal associated to the original access key id, in 
conjunction with the
+    // role permissions and optional AWS IAM session policy permissions.
+    final String originalAccessKeyId = 
omRequest.getS3Authentication().getAccessId();
+
+    final String principal = 
OzoneAclUtils.accessIdToUserPrincipal(originalAccessKeyId);
+    final UserGroupInformation ugi = 
UserGroupInformation.createRemoteUser(principal);
+
+    final String roleArn = assumeRoleRequest.getRoleArn();
+    final String sessionPolicy = getSessionPolicy(
+        ozoneManager, originalAccessKeyId, 
assumeRoleRequest.getAwsIamSessionPolicy(), hostName, remoteIp, ugi,
+        targetRoleName);
+
+    // TODO sts - generate a real STS token in a future PR that incorporates 
the components above
+    final StringBuilder builder = new StringBuilder();
+    builder.append(originalAccessKeyId);
+    builder.append(':');
+    builder.append(roleArn);
+    builder.append(':');
+    builder.append(assumeRoleRequest.getDurationSeconds());
+    builder.append(':');
+    builder.append(secretAccessKey);
+    builder.append(':');
+    builder.append(sessionPolicy);
+    return builder.toString();
+  }
+
+  /**
+   * Calls utility to convert IAM Policy to Ozone nomenclature and uses this 
output as input
+   * to IAccessAuthorizer.generateAssumeRoleSessionPolicy() which is currently 
only implemented
+   * by RangerOzoneAuthorizer.
+   */
+  private String getSessionPolicy(OzoneManager ozoneManager, String 
originalAccessKeyId, String awsIamPolicy,
+      String hostName, InetAddress remoteIp, UserGroupInformation ugi, String 
targetRoleName) throws IOException {
+    // TODO sts - implement in a future PR
+    return null;
+  }
+
+  /**
+   * Generates a cryptographically strong String of the supplied stringLength 
using supplied chars.
+   */
+  @VisibleForTesting
+  static String generateSecureRandomStringUsingChars(String chars, int 
charsLength, int stringLength) {
+    final StringBuilder sb = new StringBuilder(stringLength);
+    for (int i = 0; i < stringLength; i++) {
+      sb.append(chars.charAt(SECURE_RANDOM.nextInt(charsLength)));
+    }
+    return sb.toString();
+  }
+}
diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/security/S3AssumeRoleResponse.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/security/S3AssumeRoleResponse.java
new file mode 100644
index 00000000000..18ed41a55f7
--- /dev/null
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/response/s3/security/S3AssumeRoleResponse.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.om.response.s3.security;
+
+import java.io.IOException;
+import org.apache.hadoop.hdds.utils.db.BatchOperation;
+import org.apache.hadoop.ozone.om.OMMetadataManager;
+import org.apache.hadoop.ozone.om.response.CleanupTableInfo;
+import org.apache.hadoop.ozone.om.response.OMClientResponse;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
+
+/**
+ * Response for AssumeRole request.
+ * This is a stateless operation that doesn't modify any database tables.
+ */
+@CleanupTableInfo()
+public class S3AssumeRoleResponse extends OMClientResponse {
+
+  public S3AssumeRoleResponse(OMResponse omResponse) {
+    super(omResponse);
+  }
+
+  @Override
+  public void addToDBBatch(OMMetadataManager omMetadataManager, BatchOperation 
batchOperation) throws IOException {
+    // No database changes for assume role - it's stateless
+  }
+}
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/S3SecurityTestUtils.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/S3SecurityTestUtils.java
new file mode 100644
index 00000000000..9c91864a3fe
--- /dev/null
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/S3SecurityTestUtils.java
@@ -0,0 +1,44 @@
+/*
+ * 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.request.s3.security;
+
+/**
+ * Utility methods for S3 security tests.
+ */
+public final class S3SecurityTestUtils {
+
+  private S3SecurityTestUtils() {
+    // Utility class, no instantiation
+  }
+
+  /**
+   * Generates a string of length count containing the char c repeated.
+   *
+   * @param c the character to repeat
+   * @param count the number of times to repeat the character
+   * @return a string with the character repeated count times
+   */
+  public static String repeat(char c, int count) {
+    final StringBuilder sb = new StringBuilder(count);
+    for (int i = 0; i < count; i++) {
+      sb.append(c);
+    }
+    return sb.toString();
+  }
+}
+
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestAwsRoleArnValidator.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestAwsRoleArnValidator.java
new file mode 100644
index 00000000000..b5deffc1e0d
--- /dev/null
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestAwsRoleArnValidator.java
@@ -0,0 +1,131 @@
+/*
+ * 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.request.s3.security;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.apache.hadoop.ozone.om.exceptions.OMException;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for AwsRoleArnValidator.
+ */
+public class TestAwsRoleArnValidator {
+
+  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/path/anotherLevel/Role2";
+
+  @Test
+  public void testValidateAndExtractRoleNameFromArnSuccessCases() throws 
OMException {
+    
assertThat(AwsRoleArnValidator.validateAndExtractRoleNameFromArn(ROLE_ARN_1)).isEqualTo("MyRole1");
+
+    
assertThat(AwsRoleArnValidator.validateAndExtractRoleNameFromArn(ROLE_ARN_2)).isEqualTo("Role2");
+
+    // Path name right at 511-char max boundary
+    final String arnPrefixLen511 = S3SecurityTestUtils.repeat('p', 510) + "/"; 
// 510 chars + '/' = 511
+    final String arnMaxPath = "arn:aws:iam::123456789012:role/" + 
arnPrefixLen511 + "RoleB";
+    
assertThat(AwsRoleArnValidator.validateAndExtractRoleNameFromArn(arnMaxPath)).isEqualTo("RoleB");
+
+    // Role name right at 64-char max boundary
+    final String roleName64 = S3SecurityTestUtils.repeat('A', 64);
+    final String arn64 = "arn:aws:iam::123456789012:role/" + roleName64;
+    
assertThat(AwsRoleArnValidator.validateAndExtractRoleNameFromArn(arn64)).isEqualTo(roleName64);
+  }
+
+  @Test
+  public void testValidateAndExtractRoleNameFromArnFailureCases() {
+    // Improper structure
+    final OMException e1 = assertThrows(
+        OMException.class, () -> 
AwsRoleArnValidator.validateAndExtractRoleNameFromArn("roleNoSlashNorColons"));
+    
assertThat(e1.getResult()).isEqualTo(OMException.ResultCodes.INVALID_REQUEST);
+    assertThat(e1.getMessage()).isEqualTo(
+        "Invalid role ARN (does not start with arn:aws:iam::): 
roleNoSlashNorColons");
+
+    // Null
+    final OMException e2 = assertThrows(
+        OMException.class, () -> 
AwsRoleArnValidator.validateAndExtractRoleNameFromArn(null));
+    
assertThat(e2.getResult()).isEqualTo(OMException.ResultCodes.INVALID_REQUEST);
+    assertThat(e2.getMessage()).isEqualTo("Role ARN is required");
+
+    // String without role name
+    final OMException e3 = assertThrows(
+        OMException.class,
+        () -> 
AwsRoleArnValidator.validateAndExtractRoleNameFromArn("arn:aws:iam::123456789012:role/"));
+    
assertThat(e3.getResult()).isEqualTo(OMException.ResultCodes.INVALID_REQUEST);
+    assertThat(e3.getMessage()).isEqualTo("Invalid role ARN: missing role 
name");
+
+    // No role resource and no role name
+    final OMException e4 = assertThrows(
+        OMException.class,
+        () -> 
AwsRoleArnValidator.validateAndExtractRoleNameFromArn("arn:aws:iam::123456789012"));
+    
assertThat(e4.getResult()).isEqualTo(OMException.ResultCodes.INVALID_REQUEST);
+    assertThat(e4.getMessage()).isEqualTo(
+        "Invalid role ARN (unexpected field count): 
arn:aws:iam::123456789012");
+
+    // No role resource but contains role name
+    final OMException e5 = assertThrows(
+        OMException.class,
+        () -> 
AwsRoleArnValidator.validateAndExtractRoleNameFromArn("arn:aws:iam::123456789012:WebRole"));
+    
assertThat(e5.getResult()).isEqualTo(OMException.ResultCodes.INVALID_REQUEST);
+    assertThat(e5.getMessage()).isEqualTo(
+        "Invalid role ARN (unexpected field count): 
arn:aws:iam::123456789012:WebRole");
+
+    // Empty string
+    final OMException e6 = assertThrows(
+        OMException.class, () -> 
AwsRoleArnValidator.validateAndExtractRoleNameFromArn(""));
+    
assertThat(e6.getResult()).isEqualTo(OMException.ResultCodes.INVALID_REQUEST);
+    assertThat(e6.getMessage()).isEqualTo("Role ARN is required");
+
+    // String with only slash
+    final OMException e7 = assertThrows(
+        OMException.class, () -> 
AwsRoleArnValidator.validateAndExtractRoleNameFromArn("/"));
+    
assertThat(e7.getResult()).isEqualTo(OMException.ResultCodes.INVALID_REQUEST);
+    assertThat(e7.getMessage()).isEqualTo("Role ARN length must be between 20 
and 2048");
+
+    // String with only whitespace
+    final OMException e8 = assertThrows(
+        OMException.class, () -> 
AwsRoleArnValidator.validateAndExtractRoleNameFromArn("     "));
+    
assertThat(e8.getResult()).isEqualTo(OMException.ResultCodes.INVALID_REQUEST);
+    assertThat(e8.getMessage()).isEqualTo("Role ARN is required");
+
+    // Path name too long (> 511 characters)
+    final String arnPrefixLen512 = S3SecurityTestUtils.repeat('q', 511) + "/"; 
// 511 chars + '/' = 512
+    final String arnTooLongPath = "arn:aws:iam::123456789012:role/" + 
arnPrefixLen512 + "RoleA";
+    final OMException e9 = assertThrows(
+        OMException.class, () -> 
AwsRoleArnValidator.validateAndExtractRoleNameFromArn(arnTooLongPath));
+    
assertThat(e9.getResult()).isEqualTo(OMException.ResultCodes.INVALID_REQUEST);
+    assertThat(e9.getMessage()).isEqualTo("Role path length must be between 1 
and 512 characters");
+
+    // Otherwise valid role ending in /
+    final OMException e10 = assertThrows(
+        OMException.class,
+        () -> 
AwsRoleArnValidator.validateAndExtractRoleNameFromArn("arn:aws:iam::123456789012:role/MyRole/"));
+    
assertThat(e10.getResult()).isEqualTo(OMException.ResultCodes.INVALID_REQUEST);
+    assertThat(e10.getMessage()).isEqualTo("Invalid role ARN: missing role 
name");  // MyRole/ is considered a path
+
+    // 65-char role name
+    final String roleName65 = S3SecurityTestUtils.repeat('B', 65);
+    final String roleArn65 = "arn:aws:iam::123456789012:role/" + roleName65;
+    final OMException e11 = assertThrows(
+        OMException.class, () -> 
AwsRoleArnValidator.validateAndExtractRoleNameFromArn(roleArn65));
+    
assertThat(e11.getResult()).isEqualTo(OMException.ResultCodes.INVALID_REQUEST);
+    assertThat(e11.getMessage()).isEqualTo("Invalid role name: " + roleName65);
+  }
+}
+
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
new file mode 100644
index 00000000000..9a826393a5f
--- /dev/null
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3AssumeRoleRequest.java
@@ -0,0 +1,364 @@
+/*
+ * 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.request.s3.security;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.net.InetSocketAddress;
+import java.time.Instant;
+import java.util.regex.Pattern;
+import org.apache.hadoop.ozone.om.OzoneManager;
+import org.apache.hadoop.ozone.om.execution.flowcontrol.ExecutionContext;
+import org.apache.hadoop.ozone.om.response.OMClientResponse;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.AssumeRoleRequest;
+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.OMResponse;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.S3Authentication;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Status;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for S3AssumeRoleRequest.
+ */
+public class TestS3AssumeRoleRequest {
+
+  private static final String ROLE_ARN_1 = 
"arn:aws:iam::123456789012:role/MyRole1";
+  private static final String SESSION_NAME = "testSessionName";
+  private static final String ORIGINAL_ACCESS_KEY_ID = "origAccessKeyId";
+
+  private OzoneManager ozoneManager;
+  private ExecutionContext context;
+
+  @BeforeEach
+  public void setup() {
+    ozoneManager = mock(OzoneManager.class);
+    when(ozoneManager.getOmRpcServerAddr()).thenReturn(
+        new InetSocketAddress("localhost", 9876));
+    context = ExecutionContext.of(1L, null);
+  }
+
+  @Test
+  public void testInvalidDurationTooShort() {
+    final OMRequest omRequest = baseOmRequestBuilder()
+        .setAssumeRoleRequest(
+            AssumeRoleRequest.newBuilder()
+                .setRoleArn(ROLE_ARN_1)
+                .setRoleSessionName(SESSION_NAME)
+                .setDurationSeconds(899)  // less than 900
+        ).build();
+
+    final OMClientResponse response = new S3AssumeRoleRequest(omRequest)
+        .validateAndUpdateCache(ozoneManager, context);
+    final OMResponse omResponse = response.getOMResponse();
+
+    assertThat(omResponse.getStatus()).isEqualTo(Status.INVALID_REQUEST);
+    assertThat(omResponse.getMessage()).isEqualTo("Duration must be between 
900 and 43200");
+    assertThat(omResponse.hasAssumeRoleResponse()).isFalse();
+  }
+
+  @Test
+  public void testInvalidDurationTooLong() {
+    final OMRequest omRequest = baseOmRequestBuilder()
+        .setAssumeRoleRequest(
+            AssumeRoleRequest.newBuilder()
+                .setRoleArn(ROLE_ARN_1)
+                .setRoleSessionName(SESSION_NAME)
+                .setDurationSeconds(43201)  // more than 43200
+        ).build();
+
+    final OMClientResponse response = new S3AssumeRoleRequest(omRequest)
+        .validateAndUpdateCache(ozoneManager, context);
+    final OMResponse omResponse = response.getOMResponse();
+
+    assertThat(omResponse.getStatus()).isEqualTo(Status.INVALID_REQUEST);
+    assertThat(omResponse.getMessage()).isEqualTo("Duration must be between 
900 and 43200");
+    assertThat(omResponse.hasAssumeRoleResponse()).isFalse();
+  }
+
+  @Test
+  public void testValidDurationMaxBoundary() {
+    final OMRequest omRequest = baseOmRequestBuilder()
+        .setAssumeRoleRequest(
+            AssumeRoleRequest.newBuilder()
+                .setRoleArn(ROLE_ARN_1)
+                .setRoleSessionName(SESSION_NAME)
+                .setDurationSeconds(43200)  // exactly max
+        ).build();
+
+    final OMClientResponse response = new S3AssumeRoleRequest(omRequest)
+        .validateAndUpdateCache(ozoneManager, context);
+    final OMResponse omResponse = response.getOMResponse();
+
+    assertThat(omResponse.getStatus()).isEqualTo(Status.OK);
+    assertThat(omResponse.hasAssumeRoleResponse()).isTrue();
+  }
+
+  @Test
+  public void testValidDurationMinBoundary() {
+    final OMRequest omRequest = baseOmRequestBuilder()
+        .setAssumeRoleRequest(
+            AssumeRoleRequest.newBuilder()
+                .setRoleArn(ROLE_ARN_1)
+                .setRoleSessionName(SESSION_NAME)
+                .setDurationSeconds(900)  // exactly min
+        ).build();
+
+    final OMClientResponse response = new S3AssumeRoleRequest(omRequest)
+        .validateAndUpdateCache(ozoneManager, context);
+    final OMResponse omResponse = response.getOMResponse();
+
+    assertThat(omResponse.getStatus()).isEqualTo(Status.OK);
+    assertThat(omResponse.hasAssumeRoleResponse()).isTrue();
+  }
+
+  @Test
+  public void testMissingS3Authentication() {
+    final OMRequest omRequest = OMRequest.newBuilder()  // note: not using 
baseOMRequestBuilder that has S3 auth
+        .setCmdType(Type.AssumeRole)
+        .setClientId("client-1")
+        .setAssumeRoleRequest(
+            AssumeRoleRequest.newBuilder()
+                .setRoleArn(ROLE_ARN_1)
+                .setRoleSessionName(SESSION_NAME)
+                .setDurationSeconds(3600)
+        ).build();
+
+    final OMClientResponse response = new S3AssumeRoleRequest(omRequest)
+        .validateAndUpdateCache(ozoneManager, context);
+    final OMResponse omResponse = response.getOMResponse();
+
+    assertThat(omResponse.getStatus()).isEqualTo(Status.INVALID_REQUEST);
+    assertThat(omResponse.getMessage()).isEqualTo("S3AssumeRoleRequest does 
not have S3 authentication");
+    assertThat(omResponse.hasAssumeRoleResponse()).isFalse();
+  }
+
+  @Test
+  public void testSuccessfulAssumeRoleGeneratesCredentials() {
+    final int durationSeconds = 3600;
+    final OMRequest omRequest = baseOmRequestBuilder()
+        .setAssumeRoleRequest(
+            AssumeRoleRequest.newBuilder()
+                .setRoleArn(ROLE_ARN_1)
+                .setRoleSessionName(SESSION_NAME)
+                .setDurationSeconds(durationSeconds)
+        ).build();
+
+    final long before = Instant.now().getEpochSecond();
+    final OMClientResponse clientResponse = new S3AssumeRoleRequest(omRequest)
+        .validateAndUpdateCache(ozoneManager, context);
+    final OMResponse omResponse = clientResponse.getOMResponse();
+
+    assertThat(omResponse.getStatus()).isEqualTo(Status.OK);
+    assertThat(omResponse.hasAssumeRoleResponse()).isTrue();
+    assertThat(omResponse.getCmdType()).isEqualTo(Type.AssumeRole);
+
+    final AssumeRoleResponse assumeRoleResponse = 
omResponse.getAssumeRoleResponse();
+
+    // AccessKeyId: prefix ASIA + 20 chars
+    assertThat(assumeRoleResponse.getAccessKeyId()).startsWith("ASIA");
+    assertThat(assumeRoleResponse.getAccessKeyId().length()).isEqualTo(24);  
// 20 chars + 4 chars from ASIA
+
+    // SecretAccessKey: 40 chars
+    assertThat(assumeRoleResponse.getSecretAccessKey().length()).isEqualTo(40);
+
+    // AssumedRoleId: prefix AROA + 16 chars, followed by ":" and sessionName
+    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);
+
+    // Expiration around now + durationSeconds (allow small skew)
+    final long after = Instant.now().getEpochSecond();
+    final long expirationEpochSeconds = 
assumeRoleResponse.getExpirationEpochSeconds();
+    assertThat(expirationEpochSeconds).isBetween(before + durationSeconds - 1, 
after + durationSeconds + 1);
+  }
+
+  @Test
+  public void testGenerateSecureRandomStringUsingChars() {
+    final String chars = "ABC";
+    final int length = 32;
+    final String s = S3AssumeRoleRequest.generateSecureRandomStringUsingChars(
+        chars, chars.length(), length);
+    assertThat(s).hasSize(length).matches(Pattern.compile("^[ABC]{" + length + 
"}$"));
+
+    // Test with length 0
+    final String empty = 
S3AssumeRoleRequest.generateSecureRandomStringUsingChars(
+        "ABC", 3, 0);
+    assertThat(empty).isEmpty();
+
+    // Test with length 1
+    final String single = 
S3AssumeRoleRequest.generateSecureRandomStringUsingChars(
+        "XYZ", 3, 1);
+    assertThat(single).hasSize(1).matches(Pattern.compile("^[XYZ]$"));
+  }
+
+  @Test
+  public void testAssumeRoleCredentialsAreUnique() {
+    // Test that multiple calls generate different credentials
+    final OMRequest omRequest = baseOmRequestBuilder()
+        .setAssumeRoleRequest(
+            AssumeRoleRequest.newBuilder()
+                .setRoleArn(ROLE_ARN_1)
+                .setRoleSessionName(SESSION_NAME)
+                .setDurationSeconds(3600)
+        ).build();
+
+    final OMClientResponse response1 = new S3AssumeRoleRequest(omRequest)
+        .validateAndUpdateCache(ozoneManager, context);
+    final OMClientResponse response2 = new S3AssumeRoleRequest(omRequest)
+        .validateAndUpdateCache(ozoneManager, context);
+
+    final AssumeRoleResponse assumeRoleResponse1 = 
response1.getOMResponse().getAssumeRoleResponse();
+    final AssumeRoleResponse assumeRoleResponse2 = 
response2.getOMResponse().getAssumeRoleResponse();
+
+    // Different access keys
+    
assertThat(assumeRoleResponse1.getAccessKeyId()).isNotEqualTo(assumeRoleResponse2.getAccessKeyId());
+
+    // Different secret keys
+    
assertThat(assumeRoleResponse1.getSecretAccessKey()).isNotEqualTo(assumeRoleResponse2.getSecretAccessKey());
+
+    // Different session tokens
+    
assertThat(assumeRoleResponse1.getSessionToken()).isNotEqualTo(assumeRoleResponse2.getSessionToken());
+
+    // Different assumed role IDs
+    
assertThat(assumeRoleResponse1.getAssumedRoleId()).isNotEqualTo(assumeRoleResponse2.getAssumedRoleId());
+  }
+
+  @Test
+  public void testAssumeRoleWithEmptySessionName() {
+    final OMRequest omRequest = baseOmRequestBuilder()
+        .setAssumeRoleRequest(
+            AssumeRoleRequest.newBuilder()
+                .setRoleArn(ROLE_ARN_1)
+                .setRoleSessionName("")
+                .setDurationSeconds(3600)
+        ).build();
+
+    final OMClientResponse response = new S3AssumeRoleRequest(omRequest)
+        .validateAndUpdateCache(ozoneManager, context);
+    
assertThat(response.getOMResponse().getStatus()).isEqualTo(Status.INVALID_REQUEST);
+    
assertThat(response.getOMResponse().getMessage()).isEqualTo("RoleSessionName is 
required");
+  }
+
+  @Test
+  public void testInvalidAssumeRoleSessionNameTooShort() {
+    final OMRequest omRequest = baseOmRequestBuilder()
+        .setAssumeRoleRequest(
+            AssumeRoleRequest.newBuilder()
+                .setRoleArn(ROLE_ARN_1)
+                .setRoleSessionName("T")   // Less than 2 characters
+        ).build();
+
+    final OMClientResponse response = new S3AssumeRoleRequest(omRequest)
+        .validateAndUpdateCache(ozoneManager, context);
+    final OMResponse omResponse = response.getOMResponse();
+
+    assertThat(omResponse.getStatus()).isEqualTo(Status.INVALID_REQUEST);
+    assertThat(omResponse.getMessage()).isEqualTo("RoleSessionName length must 
be between 2 and 64");
+    assertThat(omResponse.hasAssumeRoleResponse()).isFalse();
+  }
+
+  @Test
+  public void testInvalidRoleSessionNameTooLong() {
+    final String tooLongRoleSessionName = S3SecurityTestUtils.repeat('h', 70);
+    final OMRequest omRequest = baseOmRequestBuilder()
+        .setAssumeRoleRequest(
+            AssumeRoleRequest.newBuilder()
+                .setRoleArn(ROLE_ARN_1)
+                .setRoleSessionName(tooLongRoleSessionName)
+        ).build();
+
+    final OMClientResponse response = new S3AssumeRoleRequest(omRequest)
+        .validateAndUpdateCache(ozoneManager, context);
+    final OMResponse omResponse = response.getOMResponse();
+
+    assertThat(omResponse.getStatus()).isEqualTo(Status.INVALID_REQUEST);
+    assertThat(omResponse.getMessage()).isEqualTo("RoleSessionName length must 
be between 2 and 64");
+    assertThat(omResponse.hasAssumeRoleResponse()).isFalse();
+  }
+
+  @Test
+  public void testValidRoleSessionNameMaxLengthBoundary() {
+    final String roleSessionName = S3SecurityTestUtils.repeat('g', 64);
+    final OMRequest omRequest = baseOmRequestBuilder()
+        .setAssumeRoleRequest(
+            AssumeRoleRequest.newBuilder()
+                .setRoleArn(ROLE_ARN_1)
+                .setRoleSessionName(roleSessionName)  // exactly max length
+        ).build();
+
+    final OMClientResponse response = new S3AssumeRoleRequest(omRequest)
+        .validateAndUpdateCache(ozoneManager, context);
+    final OMResponse omResponse = response.getOMResponse();
+
+    assertThat(omResponse.getStatus()).isEqualTo(Status.OK);
+    assertThat(omResponse.hasAssumeRoleResponse()).isTrue();
+  }
+
+  @Test
+  public void testValidRoleSessionNameMinLengthBoundary() {
+    final OMRequest omRequest = baseOmRequestBuilder()
+        .setAssumeRoleRequest(
+            AssumeRoleRequest.newBuilder()
+                .setRoleArn(ROLE_ARN_1)
+                .setRoleSessionName("TT")   // exactly min length
+        ).build();
+
+    final OMClientResponse response = new S3AssumeRoleRequest(omRequest)
+        .validateAndUpdateCache(ozoneManager, context);
+    final OMResponse omResponse = response.getOMResponse();
+
+    assertThat(omResponse.getStatus()).isEqualTo(Status.OK);
+    assertThat(omResponse.hasAssumeRoleResponse()).isTrue();
+  }
+
+  @Test
+  public void testAssumeRoleWithSessionPolicyPresent() {
+    final String sessionPolicy = 
"{\"Version\":\"2012-10-17\",\"Statement\":[]}";
+    final OMRequest omRequest = baseOmRequestBuilder()
+        .setAssumeRoleRequest(
+            AssumeRoleRequest.newBuilder()
+                .setRoleArn(ROLE_ARN_1)
+                .setRoleSessionName(SESSION_NAME)
+                .setDurationSeconds(3600)
+                .setAwsIamSessionPolicy(sessionPolicy)
+        ).build();
+
+    final OMClientResponse response = new S3AssumeRoleRequest(omRequest)
+        .validateAndUpdateCache(ozoneManager, context);
+    assertThat(response.getOMResponse().getStatus()).isEqualTo(Status.OK);
+  }
+
+  private static OMRequest.Builder baseOmRequestBuilder() {
+    return OMRequest.newBuilder()
+        .setCmdType(Type.AssumeRole)
+        .setClientId("client-1")
+        .setS3Authentication(
+            S3Authentication.newBuilder()
+                .setAccessId(ORIGINAL_ACCESS_KEY_ID)
+        );
+  }
+}
+
+
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/response/TestCleanupTableInfo.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/response/TestCleanupTableInfo.java
index 3683110af1e..3ab4a8106cb 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/response/TestCleanupTableInfo.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/response/TestCleanupTableInfo.java
@@ -63,6 +63,7 @@
 import org.apache.hadoop.ozone.om.request.key.OMKeyCreateRequest;
 import org.apache.hadoop.ozone.om.response.file.OMFileCreateResponse;
 import org.apache.hadoop.ozone.om.response.key.OMKeyCreateResponse;
+import org.apache.hadoop.ozone.om.response.s3.security.S3AssumeRoleResponse;
 import org.apache.hadoop.ozone.om.response.util.OMEchoRPCWriteResponse;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.CreateFileRequest;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.CreateKeyRequest;
@@ -139,6 +140,7 @@ public void checkAnnotationAndTableName() {
     // OMEchoRPCWriteResponse does not need CleanupTable.
     subTypes.remove(OMEchoRPCWriteResponse.class);
     subTypes.remove(DummyOMClientResponse.class);
+    subTypes.remove(S3AssumeRoleResponse.class);
     subTypes.forEach(aClass -> {
       if (Modifier.isAbstract(aClass.getModifiers())) {
         assertFalse(aClass.isAnnotationPresent(CleanupTableInfo.class),
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/response/s3/security/TestS3AssumeRoleResponse.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/response/s3/security/TestS3AssumeRoleResponse.java
new file mode 100644
index 00000000000..879c6d67eb6
--- /dev/null
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/response/s3/security/TestS3AssumeRoleResponse.java
@@ -0,0 +1,133 @@
+/*
+ * 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.response.s3.security;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+import org.apache.hadoop.hdds.utils.db.BatchOperation;
+import org.apache.hadoop.ozone.om.OMMetadataManager;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.AssumeRoleResponse;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Status;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for S3AssumeRoleResponse.
+ */
+public class TestS3AssumeRoleResponse {
+
+  @Test
+  public void testAddToDBBatchIsNoOpAndResponseIsAccessible() throws Exception 
{
+    final AssumeRoleResponse assumeRoleResponse = 
AssumeRoleResponse.newBuilder()
+        .setAccessKeyId("ASIA123")
+        .setSecretAccessKey("secret-xyz")
+        .setSessionToken("session-token")
+        .setExpirationEpochSeconds(12345L)
+        .setAssumedRoleId("AROA123:session")
+        .build();
+
+    final OMResponse omResponse = OMResponse.newBuilder()
+        .setCmdType(Type.AssumeRole)
+        .setStatus(Status.OK)
+        .setSuccess(true)
+        .setAssumeRoleResponse(assumeRoleResponse)
+        .build();
+
+    final S3AssumeRoleResponse response = new S3AssumeRoleResponse(omResponse);
+
+    // Should not throw and should not interact with DB tables
+    final OMMetadataManager omMetadataManager = mock(OMMetadataManager.class);
+    final BatchOperation batchOperation = mock(BatchOperation.class);
+    response.addToDBBatch(omMetadataManager, batchOperation);
+
+    // Ensure the wrapped response is present and unchanged
+    assertThat(response.getOMResponse().getStatus()).isEqualTo(Status.OK);
+    assertThat(response.getOMResponse().hasAssumeRoleResponse()).isTrue();
+    
assertThat(response.getOMResponse().getAssumeRoleResponse()).isEqualTo(assumeRoleResponse);
+
+    // Verify that batch operations were never called
+    verifyNoInteractions(batchOperation);
+    verifyNoInteractions(omMetadataManager);
+  }
+
+  @Test
+  public void testResponseWithErrorStatus() {
+    final OMResponse errorResponse = OMResponse.newBuilder()
+        .setCmdType(Type.AssumeRole)
+        .setStatus(Status.INVALID_REQUEST)
+        .setSuccess(false)
+        .build();
+
+    final S3AssumeRoleResponse response = new 
S3AssumeRoleResponse(errorResponse);
+
+    
assertThat(response.getOMResponse().getStatus()).isEqualTo(Status.INVALID_REQUEST);
+    assertThat(response.getOMResponse().getSuccess()).isFalse();
+    assertThat(response.getOMResponse().hasAssumeRoleResponse()).isFalse();
+  }
+
+  @Test
+  public void testResponsePreservesAllAssumeRoleDetails() {
+    final String expectedAccessKeyId = "ASIA123";
+    final String expectedSecretAccessKey = "secretAccessKey";
+    final String expectedSessionToken = "sessionTokenData";
+    final long expectedExpiration = 1234567890L;
+    final String expectedAssumedRoleId = "AROA1234567890:mySession";
+
+    final AssumeRoleResponse assumeRoleResponse = 
AssumeRoleResponse.newBuilder()
+        .setAccessKeyId(expectedAccessKeyId)
+        .setSecretAccessKey(expectedSecretAccessKey)
+        .setSessionToken(expectedSessionToken)
+        .setExpirationEpochSeconds(expectedExpiration)
+        .setAssumedRoleId(expectedAssumedRoleId)
+        .build();
+
+    final OMResponse omResponse = OMResponse.newBuilder()
+        .setCmdType(Type.AssumeRole)
+        .setStatus(Status.OK)
+        .setSuccess(true)
+        .setAssumeRoleResponse(assumeRoleResponse)
+        .build();
+
+    final S3AssumeRoleResponse response = new S3AssumeRoleResponse(omResponse);
+
+    final AssumeRoleResponse retrievedResponse = 
response.getOMResponse().getAssumeRoleResponse();
+    
assertThat(retrievedResponse.getAccessKeyId()).isEqualTo(expectedAccessKeyId);
+    
assertThat(retrievedResponse.getSecretAccessKey()).isEqualTo(expectedSecretAccessKey);
+    
assertThat(retrievedResponse.getSessionToken()).isEqualTo(expectedSessionToken);
+    
assertThat(retrievedResponse.getExpirationEpochSeconds()).isEqualTo(expectedExpiration);
+    
assertThat(retrievedResponse.getAssumedRoleId()).isEqualTo(expectedAssumedRoleId);
+  }
+
+  @Test
+  public void testResponseWithEmptyAssumeRoleResponse() {
+    final OMResponse omResponse = OMResponse.newBuilder()
+        .setCmdType(Type.AssumeRole)
+        .setStatus(Status.OK)
+        .setSuccess(true)
+        .build();
+
+    final S3AssumeRoleResponse response = new S3AssumeRoleResponse(omResponse);
+
+    assertThat(response.getOMResponse().hasAssumeRoleResponse()).isFalse();
+  }
+}
+
+


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

Reply via email to