Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
}
}
Expand All @@ -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);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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() {
}
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
}
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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));
Expand All @@ -120,12 +123,11 @@ 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));
assertThat(e11.getResult()).isEqualTo(OMException.ResultCodes.INVALID_REQUEST);
assertThat(e11.getMessage()).isEqualTo("Invalid role name: " + roleName65);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 +
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
}

Expand All @@ -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);
}
Expand All @@ -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);
}
Expand Down
Loading