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 4b450b4980c HDDS-14472. [STS] Refactor constants and validation 
methods to shared location (#9658)
4b450b4980c is described below

commit 4b450b4980c5ce40f5a96052b78bed4b1ab6ed15
Author: fmorg-git <[email protected]>
AuthorDate: Fri Jan 30 14:03:59 2026 -0800

    HDDS-14472. [STS] Refactor constants and validation methods to shared 
location (#9658)
---
 .../ozone/om/helpers}/AwsRoleArnValidator.java     |  21 +--
 .../apache/hadoop/ozone/om/helpers/S3STSUtils.java | 126 ++++++++++++++++++
 .../ozone/om/helpers}/TestAwsRoleArnValidator.java |  20 +--
 .../request/s3/security/S3AssumeRoleRequest.java   |  28 +---
 .../s3/security/TestS3AssumeRoleRequest.java       |  18 ++-
 .../apache/hadoop/ozone/s3sts/S3STSEndpoint.java   | 143 ++++++---------------
 .../hadoop/ozone/s3sts/TestS3STSEndpoint.java      |  50 +++++--
 7 files changed, 242 insertions(+), 164 deletions(-)

diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/AwsRoleArnValidator.java
 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/AwsRoleArnValidator.java
similarity index 87%
rename from 
hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/AwsRoleArnValidator.java
rename to 
hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/AwsRoleArnValidator.java
index 1f5af2fcc59..a60a514c674 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/AwsRoleArnValidator.java
+++ 
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/AwsRoleArnValidator.java
@@ -15,9 +15,9 @@
  * limitations under the License.
  */
 
-package org.apache.hadoop.ozone.om.request.s3.security;
+package org.apache.hadoop.ozone.om.helpers;
 
-import org.apache.commons.lang3.StringUtils;
+import com.google.common.base.Strings;
 import org.apache.hadoop.ozone.om.exceptions.OMException;
 
 /**
@@ -46,8 +46,10 @@ private AwsRoleArnValidator() {
    * @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);
+    if (Strings.isNullOrEmpty(roleArn)) {
+      throw new OMException(
+          "Value null at 'roleArn' failed to satisfy constraint: Member must 
not be null",
+          OMException.ResultCodes.INVALID_REQUEST);
     }
 
     final int roleArnLength = roleArn.length();
@@ -125,7 +127,7 @@ private static boolean isAllDigits(String s) {
    */
   private static boolean hasCharNotAllowedInIamRoleArn(String s) {
     for (int i = 0; i < s.length(); i++) {
-      if (!isCharAllowedInIamRoleArn(s.charAt(i))) {
+      if (!isCharAllowedInIamRoleArn(s.codePointAt(i))) {
         return true;
       }
     }
@@ -134,12 +136,11 @@ private static boolean 
hasCharNotAllowedInIamRoleArn(String s) {
 
   /**
    * Checks if the supplied char is allowed in IAM Role ARN.
+   * Pattern: 
[\u0009\u000A\u000D\u0020-\u007E\u0085\u00A0-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]+
    */
-  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 == '-';
+  private static boolean isCharAllowedInIamRoleArn(int c) {
+    return c == 0x09 || c == 0x0A || c == 0x0D || (c >= 0x20 && c <= 0x7E) || 
c == 0x85 || (c >= 0xA0 && c <= 0xD7FF) ||
+        (c >= 0xE000 && c <= 0xFFFD) || (c >= 0x10000 && c <= 0x10FFFF);
   }
 }
 
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 8d261e6c68e..c70c01a8723 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
@@ -17,13 +17,28 @@
 
 package org.apache.hadoop.ozone.om.helpers;
 
+import static 
org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_REQUEST;
+
 import com.google.common.base.Strings;
 import java.util.Map;
+import net.jcip.annotations.Immutable;
+import org.apache.hadoop.ozone.om.exceptions.OMException;
 
 /**
  * Utility class containing constants and validation methods shared by STS 
endpoint and OzoneManager processing.
  */
+@Immutable
 public final class S3STSUtils {
+  // STS API constants
+  public static final int DEFAULT_DURATION_SECONDS = 3600;    // 1 hour
+  public static final int MAX_DURATION_SECONDS = 43200;       // 12 hours
+  public static final int MIN_DURATION_SECONDS = 900;         // 15 minutes
+
+  public static final int ASSUME_ROLE_SESSION_NAME_MIN_LENGTH = 2;
+  public static final int ASSUME_ROLE_SESSION_NAME_MAX_LENGTH = 64;
+
+  // AWS limit for session policy is 2048 characters
+  public static final int MAX_SESSION_POLICY_LENGTH = 2048;
 
   private S3STSUtils() {
   }
@@ -41,4 +56,115 @@ public static void addAssumeRoleAuditParams(Map<String, 
String> auditParams, Str
     auditParams.put("isPolicyIncluded", 
Strings.isNullOrEmpty(awsIamSessionPolicy) ? "N" : "Y");
     auditParams.put("requestId", requestId);
   }
+
+  /**
+   * Validates the duration in seconds.
+   * @param durationSeconds duration in seconds
+   * @return validated duration
+   * @throws OMException if duration is invalid
+   */
+  public static int validateDuration(Integer durationSeconds) throws 
OMException {
+    if (durationSeconds == null) {
+      return DEFAULT_DURATION_SECONDS;
+    }
+
+    if (durationSeconds < MIN_DURATION_SECONDS || durationSeconds > 
MAX_DURATION_SECONDS) {
+      throw new OMException(
+          "Invalid Value: DurationSeconds must be between " + 
MIN_DURATION_SECONDS + " and " + MAX_DURATION_SECONDS +
+          " seconds", INVALID_REQUEST);
+    }
+
+    return durationSeconds;
+  }
+
+  /**
+   * Validates the role session name.
+   * @param roleSessionName role session name
+   * @throws OMException if role session name is invalid
+   */
+  public static void validateRoleSessionName(String roleSessionName) throws 
OMException {
+    if (Strings.isNullOrEmpty(roleSessionName)) {
+      throw new OMException(
+          "Value null at 'roleSessionName' failed to satisfy constraint: 
Member must not be null", INVALID_REQUEST);
+    }
+
+    final int roleSessionNameLength = roleSessionName.length();
+    if (roleSessionNameLength < ASSUME_ROLE_SESSION_NAME_MIN_LENGTH ||
+        roleSessionNameLength > ASSUME_ROLE_SESSION_NAME_MAX_LENGTH) {
+      throw new OMException("Invalid RoleSessionName length " + 
roleSessionNameLength + ": it must be " +
+          ASSUME_ROLE_SESSION_NAME_MIN_LENGTH + "-" + 
ASSUME_ROLE_SESSION_NAME_MAX_LENGTH + " characters long and " +
+          "contain only alphanumeric characters and +, =, ,, ., @, -", 
INVALID_REQUEST);
+    }
+
+    // AWS allows: alphanumeric, +, =, ,, ., @, -
+    // Pattern: [\w+=,.@-]*
+    // Don't use regex for performance reasons
+    for (int i = 0; i < roleSessionNameLength; i++) {
+      final char c = roleSessionName.charAt(i);
+      if (!isRoleSessionNameChar(c)) {
+        throw new OMException("Invalid character '" + c + "' in 
RoleSessionName: it must be " +
+            ASSUME_ROLE_SESSION_NAME_MIN_LENGTH + "-" + 
ASSUME_ROLE_SESSION_NAME_MAX_LENGTH + " characters long and " +
+            "contain only alphanumeric characters and +, =, ,, ., @, -", 
INVALID_REQUEST);
+      }
+    }
+  }
+
+  private static boolean isRoleSessionNameChar(char c) {
+    return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c 
<= '9') ||
+        c == '_' || c == '+' || c == '=' || c == ',' || c == '.' || c == '@' 
|| c == '-';
+  }
+
+  /**
+   * Validates the session policy length.
+   * @param awsIamSessionPolicy session policy
+   * @throws OMException if policy length is invalid
+   */
+  public static void validateSessionPolicy(String awsIamSessionPolicy) throws 
OMException {
+    if (awsIamSessionPolicy != null && awsIamSessionPolicy.length() > 
MAX_SESSION_POLICY_LENGTH) {
+      throw new OMException(
+          "Value '" + awsIamSessionPolicy + "' at 'policy' failed to satisfy 
constraint: Member " +
+              "must have length less than or equal to " + 
MAX_SESSION_POLICY_LENGTH, INVALID_REQUEST);
+    }
+  }
+
+  /**
+   * Generates the assumed role user ARN.
+   * @param validRoleArn                  valid role ARN
+   * @param roleSessionName               role session name
+   * @return assumed role user ARN
+   */
+  public static String toAssumedRoleUserArn(String validRoleArn, String 
roleSessionName) {
+    // We already know the roleArn is valid, so perform the conversion for 
assumed role user arn format
+    // RoleArn format: arn:aws:iam::<account-id>:role/<role-name>
+    // Assumed role user arn format: 
arn:aws:sts::<account-id>:assumed-role/<role-name>/<role-session-name>
+    final String[] parts = splitRoleArnWithoutRegex(validRoleArn);
+
+    final String partition = parts[1];
+    final String accountId = parts[4];
+    final String resource = parts[5];
+    final String roleName = resource.substring("role/".length());
+
+    //noinspection StringBufferReplaceableByString
+    final StringBuilder stringBuilder = new StringBuilder("arn:");
+    stringBuilder.append(partition);
+    stringBuilder.append(":sts::");
+    stringBuilder.append(accountId);
+    stringBuilder.append(":assumed-role/");
+    stringBuilder.append(roleName);
+    stringBuilder.append('/');
+    stringBuilder.append(roleSessionName);
+    return stringBuilder.toString();
+  }
+
+  private static String[] splitRoleArnWithoutRegex(String roleArn) {
+    final String[] parts = new String[6];
+    int start = 0;
+    for (int i = 0; i < 5; i++) {
+      final int end = roleArn.indexOf(':', start);
+      parts[i] = roleArn.substring(start, end);
+      start = end + 1;
+    }
+    parts[5] = roleArn.substring(start);
+    return parts;
+  }
 }
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestAwsRoleArnValidator.java
 
b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestAwsRoleArnValidator.java
similarity index 89%
rename from 
hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestAwsRoleArnValidator.java
rename to 
hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestAwsRoleArnValidator.java
index b5deffc1e0d..ea6db63c555 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestAwsRoleArnValidator.java
+++ 
b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestAwsRoleArnValidator.java
@@ -15,11 +15,12 @@
  * limitations under the License.
  */
 
-package org.apache.hadoop.ozone.om.request.s3.security;
+package org.apache.hadoop.ozone.om.helpers;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
+import org.apache.commons.lang3.StringUtils;
 import org.apache.hadoop.ozone.om.exceptions.OMException;
 import org.junit.jupiter.api.Test;
 
@@ -38,12 +39,12 @@ public void 
testValidateAndExtractRoleNameFromArnSuccessCases() throws OMExcepti
     
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 arnPrefixLen511 = StringUtils.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 roleName64 = StringUtils.repeat('A', 64);
     final String arn64 = "arn:aws:iam::123456789012:role/" + roleName64;
     
assertThat(AwsRoleArnValidator.validateAndExtractRoleNameFromArn(arn64)).isEqualTo(roleName64);
   }
@@ -61,7 +62,8 @@ public void 
testValidateAndExtractRoleNameFromArnFailureCases() {
     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");
+    assertThat(e2.getMessage()).isEqualTo(
+        "Value null at 'roleArn' failed to satisfy constraint: Member must not 
be null");
 
     // String without role name
     final OMException e3 = assertThrows(
@@ -90,7 +92,8 @@ public void 
testValidateAndExtractRoleNameFromArnFailureCases() {
     final OMException e6 = assertThrows(
         OMException.class, () -> 
AwsRoleArnValidator.validateAndExtractRoleNameFromArn(""));
     
assertThat(e6.getResult()).isEqualTo(OMException.ResultCodes.INVALID_REQUEST);
-    assertThat(e6.getMessage()).isEqualTo("Role ARN is required");
+    assertThat(e6.getMessage()).isEqualTo(
+        "Value null at 'roleArn' failed to satisfy constraint: Member must not 
be null");
 
     // String with only slash
     final OMException e7 = assertThrows(
@@ -102,10 +105,10 @@ public void 
testValidateAndExtractRoleNameFromArnFailureCases() {
     final OMException e8 = assertThrows(
         OMException.class, () -> 
AwsRoleArnValidator.validateAndExtractRoleNameFromArn("     "));
     
assertThat(e8.getResult()).isEqualTo(OMException.ResultCodes.INVALID_REQUEST);
-    assertThat(e8.getMessage()).isEqualTo("Role ARN is required");
+    assertThat(e8.getMessage()).isEqualTo("Role ARN length must be between 20 
and 2048");
 
     // Path name too long (> 511 characters)
-    final String arnPrefixLen512 = S3SecurityTestUtils.repeat('q', 511) + "/"; 
// 511 chars + '/' = 512
+    final String arnPrefixLen512 = StringUtils.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));
@@ -120,7 +123,7 @@ public void 
testValidateAndExtractRoleNameFromArnFailureCases() {
     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 roleName65 = StringUtils.repeat('B', 65);
     final String roleArn65 = "arn:aws:iam::123456789012:role/" + roleName65;
     final OMException e11 = assertThrows(
         OMException.class, () -> 
AwsRoleArnValidator.validateAndExtractRoleNameFromArn(roleArn65));
@@ -128,4 +131,3 @@ public void 
testValidateAndExtractRoleNameFromArnFailureCases() {
     assertThat(e11.getMessage()).isEqualTo("Invalid role name: " + roleName65);
   }
 }
-
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 1b00454b70c..030a4aeffeb 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
@@ -37,6 +37,7 @@
 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.helpers.AwsRoleArnValidator;
 import org.apache.hadoop.ozone.om.helpers.S3STSUtils;
 import org.apache.hadoop.ozone.om.request.OMClientRequest;
 import org.apache.hadoop.ozone.om.request.util.OmResponseUtil;
@@ -68,14 +69,10 @@ public class S3AssumeRoleRequest extends OMClientRequest {
     SECURE_RANDOM = 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 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 +
@@ -113,14 +110,10 @@ public OMClientResponse 
validateAndUpdateCache(OzoneManager ozoneManager, Execut
     OMClientResponse omClientResponse;
     try {
       // Validate duration
-      if (durationSeconds < MIN_TOKEN_EXPIRATION_SECONDS || durationSeconds > 
MAX_TOKEN_EXPIRATION_SECONDS) {
-        throw new OMException(
-            "Duration must be between " + MIN_TOKEN_EXPIRATION_SECONDS + " and 
" + MAX_TOKEN_EXPIRATION_SECONDS,
-            OMException.ResultCodes.INVALID_REQUEST);
-      }
+      S3STSUtils.validateDuration(durationSeconds);
 
       // Validate role session name
-      validateRoleSessionName(roleSessionName);
+      S3STSUtils.validateRoleSessionName(roleSessionName);
 
       // Validate role ARN and extract role
       final String targetRoleName = 
AwsRoleArnValidator.validateAndExtractRoleNameFromArn(roleArn);
@@ -178,21 +171,6 @@ public OMClientResponse 
validateAndUpdateCache(OzoneManager ozoneManager, Execut
     return omClientResponse;
   }
 
-  /**
-   * Ensures RoleSessionName is valid.
-   */
-  private void validateRoleSessionName(String roleSessionName) throws 
OMException {
-    if (StringUtils.isBlank(roleSessionName)) {
-      throw new OMException("RoleSessionName is required", 
OMException.ResultCodes.INVALID_REQUEST);
-    }
-    if (roleSessionName.length() < ASSUME_ROLE_SESSION_NAME_MIN_LENGTH ||
-        roleSessionName.length() > ASSUME_ROLE_SESSION_NAME_MAX_LENGTH) {
-      throw new OMException(
-          "RoleSessionName length must be between " + 
ASSUME_ROLE_SESSION_NAME_MIN_LENGTH + " and " +
-          ASSUME_ROLE_SESSION_NAME_MAX_LENGTH, 
OMException.ResultCodes.INVALID_REQUEST);
-    }
-  }
-
   /**
    * Generates session token using components from the AssumeRoleRequest.
    */
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 bda871386fe..004a6b0ab69 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
@@ -154,7 +154,8 @@ public void testInvalidDurationTooShort() {
     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.getMessage()).isEqualTo(
+        "Invalid Value: DurationSeconds must be between 900 and 43200 
seconds");
     assertThat(omResponse.hasAssumeRoleResponse()).isFalse();
     assertMarkForAuditCalled(request);
   }
@@ -175,7 +176,8 @@ public void testInvalidDurationTooLong() {
     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.getMessage()).isEqualTo(
+        "Invalid Value: DurationSeconds must be between 900 and 43200 
seconds");
     assertThat(omResponse.hasAssumeRoleResponse()).isFalse();
     assertMarkForAuditCalled(request);
   }
@@ -355,7 +357,8 @@ public void testAssumeRoleWithEmptySessionName() {
     final S3AssumeRoleRequest request = new S3AssumeRoleRequest(omRequest, 
CLOCK);
     final OMClientResponse response = 
request.validateAndUpdateCache(ozoneManager, context);
     
assertThat(response.getOMResponse().getStatus()).isEqualTo(Status.INVALID_REQUEST);
-    
assertThat(response.getOMResponse().getMessage()).isEqualTo("RoleSessionName is 
required");
+    assertThat(response.getOMResponse().getMessage()).isEqualTo(
+        "Value null at 'roleSessionName' failed to satisfy constraint: Member 
must not be null");
     assertMarkForAuditCalled(request);
   }
 
@@ -374,7 +377,9 @@ public void testInvalidAssumeRoleSessionNameTooShort() {
     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.getMessage()).isEqualTo(
+        "Invalid RoleSessionName length 1: it must be 2-64 characters long and 
contain only alphanumeric " +
+        "characters and +, =, ,, ., @, -");
     assertThat(omResponse.hasAssumeRoleResponse()).isFalse();
     assertMarkForAuditCalled(request);
   }
@@ -395,7 +400,10 @@ public void testInvalidRoleSessionNameTooLong() {
     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.getMessage()).isEqualTo(
+        "Invalid RoleSessionName length 70: it must be 2-64 characters long 
and contain only alphanumeric " +
+        "characters and +, =, ,, ., @, -"
+    );
     assertThat(omResponse.hasAssumeRoleResponse()).isFalse();
     assertMarkForAuditCalled(request);
   }
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 62bef03586a..e4da7b604c7 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
@@ -23,13 +23,14 @@
 import static javax.ws.rs.core.Response.Status.NOT_IMPLEMENTED;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Strings;
 import java.io.IOException;
 import java.io.StringWriter;
 import java.time.Instant;
 import java.time.ZoneOffset;
 import java.time.format.DateTimeFormatter;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 import javax.inject.Inject;
 import javax.ws.rs.FormParam;
 import javax.ws.rs.GET;
@@ -45,6 +46,7 @@
 import org.apache.hadoop.ozone.audit.S3GAction;
 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.S3STSUtils;
 import org.apache.hadoop.ozone.s3.RequestIdentifier;
 import org.apache.hadoop.ozone.s3.exception.OS3Exception;
@@ -71,7 +73,6 @@ public class S3STSEndpoint extends S3STSEndpointBase {
 
   // STS API constants
   private static final String ASSUME_ROLE_ACTION = "AssumeRole";
-  private static final String ROLE_DURATION_SECONDS_PARAM = "DurationSeconds";
   private static final String GET_SESSION_TOKEN_ACTION = "GetSessionToken";
   private static final String ASSUME_ROLE_WITH_SAML_ACTION = 
"AssumeRoleWithSAML";
   private static final String ASSUME_ROLE_WITH_WEB_IDENTITY_ACTION = 
"AssumeRoleWithWebIdentity";
@@ -86,13 +87,6 @@ public class S3STSEndpoint extends S3STSEndpointBase {
   private static final String ACCESS_DENIED = "AccessDenied";
   private static final String INVALID_CLIENT_TOKEN_ID = "InvalidClientTokenId";
 
-  // Default token duration (in seconds) - AWS default is 3600 (1 hour)
-  // TODO - add these constants and also validations in a common place that 
both endpoint and backend can use
-  private static final int DEFAULT_DURATION_SECONDS = 3600;
-  private static final int MAX_DURATION_SECONDS = 43200; // 12 hours
-  private static final int MIN_DURATION_SECONDS = 900;   // 15 minutes
-  private static final int MAX_SESSION_POLICY_SIZE = 2048;
-
   @Inject
   private RequestIdentifier requestIdentifier;
 
@@ -195,19 +189,10 @@ private Response handleAssumeRole(String roleArn, String 
roleSessionName, Intege
     final Map<String, String> auditParams = getAuditParameters();
     S3STSUtils.addAssumeRoleAuditParams(
         auditParams, roleArn, roleSessionName, awsIamSessionPolicy,
-        durationSeconds == null ? DEFAULT_DURATION_SECONDS : durationSeconds,
+        durationSeconds == null ? S3STSUtils.DEFAULT_DURATION_SECONDS : 
durationSeconds,
         requestId);
 
-    int duration;
-    try {
-      // Validate parameters
-      duration = validateDuration(durationSeconds);
-    } catch (IllegalArgumentException e) {
-      final OSTSException exception = new OSTSException(VALIDATION_ERROR, 
e.getMessage(), BAD_REQUEST.getStatusCode());
-      
getAuditLogger().logWriteFailure(buildAuditMessageForFailure(S3GAction.ASSUME_ROLE,
 auditParams, exception));
-      throw exception;
-    }
-
+    // Validate parameters
     if (version == null || !version.equals(EXPECTED_VERSION)) {
       final OSTSException exception = new OSTSException(
           INVALID_ACTION, "Could not find operation " + action + " for version 
" +
@@ -217,50 +202,48 @@ private Response handleAssumeRole(String roleArn, String 
roleSessionName, Intege
       throw exception;
     }
 
-    if (roleArn == null || roleArn.isEmpty()) {
-      final OSTSException exception = new OSTSException(
-          VALIDATION_ERROR, "Value null at 'roleArn' failed to satisfy 
constraint: Member must not be null",
-          BAD_REQUEST.getStatusCode());
-      
getAuditLogger().logWriteFailure(buildAuditMessageForFailure(S3GAction.ASSUME_ROLE,
 auditParams, exception));
-      throw exception;
+    final Set<String> validationErrors = new HashSet<>();
+    int duration = durationSeconds == null ? 
S3STSUtils.DEFAULT_DURATION_SECONDS : durationSeconds;
+    try {
+      duration = S3STSUtils.validateDuration(durationSeconds);
+    } catch (OMException e) {
+      validationErrors.add(e.getMessage());
     }
 
-    if (roleSessionName == null || roleSessionName.isEmpty()) {
-      final OSTSException exception = new OSTSException(
-          VALIDATION_ERROR, "Value null at 'roleSessionName' failed to satisfy 
constraint: Member must not be null",
-          BAD_REQUEST.getStatusCode());
-      
getAuditLogger().logWriteFailure(buildAuditMessageForFailure(S3GAction.ASSUME_ROLE,
 auditParams, exception));
-      throw exception;
+    try {
+      AwsRoleArnValidator.validateAndExtractRoleNameFromArn(roleArn);
+    } catch (OMException e) {
+      validationErrors.add(e.getMessage());
     }
 
-    // Validate role session name format (AWS requirements)
-    if (!isValidRoleSessionName(roleSessionName)) {
-      final OSTSException exception = new OSTSException(
-          VALIDATION_ERROR, "Invalid RoleSessionName: must be 2-64 characters 
long and " +
-          "contain only alphanumeric characters, +, =, ,, ., @, -",
-          BAD_REQUEST.getStatusCode());
-      
getAuditLogger().logWriteFailure(buildAuditMessageForFailure(S3GAction.ASSUME_ROLE,
 auditParams, exception));
-      throw exception;
+    try {
+      S3STSUtils.validateRoleSessionName(roleSessionName);
+    } catch (OMException e) {
+      validationErrors.add(e.getMessage());
     }
 
-    // Check Policy size if available
-    if (awsIamSessionPolicy != null && awsIamSessionPolicy.length() > 
MAX_SESSION_POLICY_SIZE) {
-      final OSTSException exception = new OSTSException(
-          VALIDATION_ERROR, "Value '" + awsIamSessionPolicy + "' at 'policy' 
failed to satisfy constraint: Member " +
-          "must have length less than or equal to 2048", 
BAD_REQUEST.getStatusCode());
-      
getAuditLogger().logWriteFailure(buildAuditMessageForFailure(S3GAction.ASSUME_ROLE,
 auditParams, exception));
-      throw exception;
+    try {
+      S3STSUtils.validateSessionPolicy(awsIamSessionPolicy);
+    } catch (OMException e) {
+      validationErrors.add(e.getMessage());
     }
 
-    final String assumedRoleUserArn;
-    try {
-      assumedRoleUserArn = toAssumedRoleUserArn(roleArn, roleSessionName);
-    } catch (IllegalArgumentException e) {
-      final OSTSException exception = new OSTSException(VALIDATION_ERROR, 
e.getMessage(), BAD_REQUEST.getStatusCode());
+    final int numValidationErrors = validationErrors.size();
+    if (numValidationErrors > 0) {
+      //noinspection StringBufferReplaceableByString
+      final StringBuilder builder = new StringBuilder();
+      builder.append(numValidationErrors);
+      builder.append(" validation ");
+      builder.append(numValidationErrors > 1 ? "errors detected: " : "error 
detected: ");
+      builder.append(String.join(";", validationErrors));
+      final String validationMessage = builder.toString();
+      final OSTSException exception = new OSTSException(
+          VALIDATION_ERROR, validationMessage, BAD_REQUEST.getStatusCode());
       
getAuditLogger().logWriteFailure(buildAuditMessageForFailure(S3GAction.ASSUME_ROLE,
 auditParams, exception));
       throw exception;
     }
 
+    final String assumedRoleUserArn = S3STSUtils.toAssumedRoleUserArn(roleArn, 
roleSessionName);
     try {
       final AssumeRoleResponseInfo responseInfo = getClient()
           .getObjectStore()
@@ -301,29 +284,6 @@ private Response handleAssumeRole(String roleArn, String 
roleSessionName, Intege
     }
   }
 
-  private int validateDuration(Integer durationSeconds) throws 
IllegalArgumentException {
-    if (durationSeconds == null) {
-      return DEFAULT_DURATION_SECONDS;
-    }
-
-    if (durationSeconds < MIN_DURATION_SECONDS || durationSeconds > 
MAX_DURATION_SECONDS) {
-      throw new IllegalArgumentException(
-          "Invalid Value: " + ROLE_DURATION_SECONDS_PARAM + " must be between 
" + MIN_DURATION_SECONDS +
-              " and " + MAX_DURATION_SECONDS + " seconds");
-    }
-
-    return durationSeconds;
-  }
-
-  private boolean isValidRoleSessionName(String roleSessionName) {
-    if (roleSessionName.length() < 2 || roleSessionName.length() > 64) {
-      return false;
-    }
-
-    // AWS allows: alphanumeric, +, =, ,, ., @, -
-    return roleSessionName.matches("[a-zA-Z0-9+=,.@\\-]+");
-  }
-
   private String generateAssumeRoleResponse(String assumedRoleUserArn, 
AssumeRoleResponseInfo responseInfo,
       String requestId) throws IOException {
     final String accessKeyId = responseInfo.getAccessKeyId();
@@ -362,36 +322,5 @@ private String generateAssumeRoleResponse(String 
assumedRoleUserArn, AssumeRoleR
       throw new IOException("Failed to marshal AssumeRole response", e);
     }
   }
-
-  private String toAssumedRoleUserArn(String roleArn, String roleSessionName) {
-    // RoleArn format: arn:aws:iam::<account-id>:role/<role-name>
-    // Assumed role user arn format: 
arn:aws:sts::<account-id>:assumed-role/<role-name>/<role-session-name>
-    // TODO - refactor and reuse AwsRoleArnValidator for validation in future 
PR
-    final String errMsg = "Invalid RoleArn: must be in the format 
arn:aws:iam::<account-id>:role/<role-name>";
-    final String[] parts = roleArn.split(":", 6);
-    if (parts.length != 6 || !"arn".equals(parts[0]) || parts[1].isEmpty() || 
!"iam".equals(parts[2])) {
-      throw new IllegalArgumentException(errMsg);
-    }
-
-    final String partition = parts[1];
-    final String accountId = parts[4];
-    final String resource = parts[5]; // role/<name>
-
-    if (Strings.isNullOrEmpty(accountId) || Strings.isNullOrEmpty(resource) || 
!resource.startsWith("role/") ||
-        resource.length() == "role/".length()) {
-      throw new IllegalArgumentException(errMsg);
-    }
-
-    final String roleName = resource.substring("role/".length());
-    //noinspection StringBufferReplaceableByString
-    final StringBuilder stringBuilder = new StringBuilder("arn:");
-    stringBuilder.append(partition);
-    stringBuilder.append(":sts::");
-    stringBuilder.append(accountId);
-    stringBuilder.append(":assumed-role/");
-    stringBuilder.append(roleName);
-    stringBuilder.append('/');
-    stringBuilder.append(roleSessionName);
-    return stringBuilder.toString();
-  }
 }
+
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 34891d08945..d0eaca9a5dc 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
@@ -118,8 +118,7 @@ public void setup() throws Exception {
 
   @Test
   public void testStsAssumeRoleValidForGetMethod() throws Exception {
-    Response response = endpoint.get(
-        "AssumeRole", ROLE_ARN, ROLE_SESSION_NAME, 3600, "2011-06-15", null);
+    final Response response = endpoint.get("AssumeRole", ROLE_ARN, 
ROLE_SESSION_NAME, 3600, "2011-06-15", null);
 
     assertEquals(200, response.getStatus());
     verify(auditLogger).logWriteSuccess(any(AuditMessage.class));
@@ -157,6 +156,7 @@ public void testStsAssumeRoleValidForGetMethod() throws 
Exception {
 
   @Test
   public void testStsAssumeRoleValidForPostMethod() throws Exception {
+    //noinspection resource
     final Response response = endpoint.post("AssumeRole", ROLE_ARN, 
ROLE_SESSION_NAME, 3600, "2011-06-15", null);
 
     assertEquals(200, response.getStatus());
@@ -305,7 +305,7 @@ public void testStsInvalidRoleArn() throws Exception {
     final String requestId = "test-request-id";
     ex.setRequestId(requestId);
     assertStsErrorXml(ex.toXml(), STS_NS, "Sender", "ValidationError",
-        "Invalid RoleArn: must be in the format 
arn:aws:iam::<account-id>:role/<role-name>");
+        "Invalid role ARN (does not start with arn:aws:iam::)");
   }
 
   @Test
@@ -335,7 +335,7 @@ public void testStsInvalidRoleArnMissingRoleName() throws 
Exception {
 
     final String requestId = "test-request-id";
     ex.setRequestId(requestId);
-    assertStsErrorXml(ex.toXml(), STS_NS, "Sender", "ValidationError", 
"Invalid RoleArn: must be in the format");
+    assertStsErrorXml(ex.toXml(), STS_NS, "Sender", "ValidationError", 
"Invalid role ARN: missing role name");
   }
 
   @Test
@@ -351,7 +351,7 @@ public void testStsInvalidRoleArnMissingAccountId() throws 
Exception {
 
     final String requestId = "test-request-id";
     ex.setRequestId(requestId);
-    assertStsErrorXml(ex.toXml(), STS_NS, "Sender", "ValidationError", 
"Invalid RoleArn: must be in the format"
+    assertStsErrorXml(ex.toXml(), STS_NS, "Sender", "ValidationError", 
"Invalid AWS account ID in ARN"
     );
   }
 
@@ -395,7 +395,10 @@ public void 
testStsInvalidRoleSessionNameWithInvalidCharacter() throws Exception
 
     final String requestId = "test-request-id";
     ex.setRequestId(requestId);
-    assertStsErrorXml(ex.toXml(), STS_NS, "Sender", "ValidationError", 
"Invalid RoleSessionName");
+    assertStsErrorXml(
+        ex.toXml(), STS_NS, "Sender", "ValidationError", "1 validation error 
detected: " +
+        "Invalid character '/' in RoleSessionName: it must be 2-64 characters 
long and contain only alphanumeric " +
+        "characters and +, =, ,, ., @, -");
   }
 
   @Test
@@ -410,7 +413,9 @@ public void testStsInvalidRoleSessionNameTooShort() throws 
Exception {
 
     final String requestId = "test-request-id";
     ex.setRequestId(requestId);
-    assertStsErrorXml(ex.toXml(), STS_NS, "Sender", "ValidationError", 
"Invalid RoleSessionName");
+    assertStsErrorXml(
+        ex.toXml(), STS_NS, "Sender", "ValidationError", "1 validation error 
detected: Invalid RoleSessionName " +
+        "length 1: it must be 2-64 characters long and contain only 
alphanumeric characters and +, =, ,, ., @, -");
   }
 
   @Test
@@ -426,7 +431,7 @@ public void testStsInvalidRoleArnResourceType() throws 
Exception {
 
     final String requestId = "test-request-id";
     ex.setRequestId(requestId);
-    assertStsErrorXml(ex.toXml(), STS_NS, "Sender", "ValidationError", 
"Invalid RoleArn: must be in the format");
+    assertStsErrorXml(ex.toXml(), STS_NS, "Sender", "ValidationError", 
"Invalid role ARN (unexpected field count)");
   }
 
   @Test
@@ -481,6 +486,35 @@ public void testStsIOExceptionWrappedAsInternalFailure() 
throws Exception {
     assertStsErrorXml(ex.toXml(), STS_NS, "Receiver", "InternalFailure", "An 
internal error has occurred.");
   }
 
+  @Test
+  public void testStsMultipleValidationErrors() throws Exception {
+    final String invalidRoleSessionName = "test/session";
+    final String tooLargePolicy = 
RandomStringUtils.insecure().nextAlphanumeric(2049);
+    final int invalidDurationSeconds = -1;
+
+    final OSTSException ex = assertThrows(OSTSException.class, () ->
+        endpoint.get("AssumeRole", ROLE_ARN, invalidRoleSessionName, 
invalidDurationSeconds, "2011-06-15",
+            tooLargePolicy));
+
+    assertEquals(400, ex.getHttpCode());
+    verify(auditLogger).logWriteFailure(any(AuditMessage.class));
+    verify(auditLogger, never()).logWriteSuccess(any(AuditMessage.class));
+
+    final String requestId = "test-request-id";
+    ex.setRequestId(requestId);
+
+    final String xml = ex.toXml();
+    // The order of individual validation errors is not guaranteed because 
it's a HashSet, so check
+    // that multiple messages are included
+    final Document doc = parseXml(xml);
+    final String message = 
doc.getElementsByTagName("Message").item(0).getTextContent();
+    assertTrue(message.contains("3 validation errors detected"));
+    assertTrue(message.contains("Invalid Value: DurationSeconds"));
+    assertTrue(message.contains("Invalid character '/' in RoleSessionName"));
+    assertTrue(message.contains(
+        "'policy' failed to satisfy constraint: Member must have length less 
than or equal to 2048"));
+  }
+
   private static Document parseXml(String xml) throws Exception {
     final DocumentBuilderFactory documentBuilderFactory = 
DocumentBuilderFactory.newInstance();
     documentBuilderFactory.setNamespaceAware(true);


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


Reply via email to