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]