Skip to content

Commit 0e0f484

Browse files
author
Fabian Morgan
committed
refactor constants and validations to shared location
1 parent ce3e85c commit 0e0f484

7 files changed

Lines changed: 206 additions & 153 deletions

File tree

hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/AwsRoleArnValidator.java renamed to hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/AwsRoleArnValidator.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* limitations under the License.
1616
*/
1717

18-
package org.apache.hadoop.ozone.om.request.s3.security;
18+
package org.apache.hadoop.ozone.om.helpers;
1919

2020
import org.apache.commons.lang3.StringUtils;
2121
import org.apache.hadoop.ozone.om.exceptions.OMException;
@@ -125,7 +125,7 @@ private static boolean isAllDigits(String s) {
125125
*/
126126
private static boolean hasCharNotAllowedInIamRoleArn(String s) {
127127
for (int i = 0; i < s.length(); i++) {
128-
if (!isCharAllowedInIamRoleArn(s.charAt(i))) {
128+
if (!isCharAllowedInIamRoleArn(s.codePointAt(i))) {
129129
return true;
130130
}
131131
}
@@ -134,12 +134,11 @@ private static boolean hasCharNotAllowedInIamRoleArn(String s) {
134134

135135
/**
136136
* Checks if the supplied char is allowed in IAM Role ARN.
137+
* Pattern: [\u0009\u000A\u000D\u0020-\u007E\u0085\u00A0-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]+
137138
*/
138-
private static boolean isCharAllowedInIamRoleArn(char c) {
139-
return (c >= 'A' && c <= 'Z')
140-
|| (c >= 'a' && c <= 'z')
141-
|| (c >= '0' && c <= '9')
142-
|| c == '+' || c == '=' || c == ',' || c == '.' || c == '@' || c == '_' || c == '-';
139+
private static boolean isCharAllowedInIamRoleArn(int c) {
140+
return c == 0x09 || c == 0x0A || c == 0x0D || (c >= 0x20 && c <= 0x7E) || c == 0x85 || (c >= 0xA0 && c <= 0xD7FF) ||
141+
(c >= 0xE000 && c <= 0xFFFD) || (c >= 0x10000 && c <= 0x10FFFF);
143142
}
144143
}
145144

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.hadoop.ozone.om.helpers;
19+
20+
import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_REQUEST;
21+
22+
import net.jcip.annotations.Immutable;
23+
import org.apache.commons.lang3.StringUtils;
24+
import org.apache.hadoop.ozone.om.exceptions.OMException;
25+
26+
/**
27+
* Utility class containing constants and validation methods shared by STS endpoint and OzoneManager processing.
28+
*/
29+
@Immutable
30+
public final class S3STSUtils {
31+
// STS API constants
32+
public static final int DEFAULT_DURATION_SECONDS = 3600; // 1 hour
33+
public static final int MAX_DURATION_SECONDS = 43200; // 12 hours
34+
public static final int MIN_DURATION_SECONDS = 900; // 15 minutes
35+
36+
public static final int ASSUME_ROLE_SESSION_NAME_MIN_LENGTH = 2;
37+
public static final int ASSUME_ROLE_SESSION_NAME_MAX_LENGTH = 64;
38+
39+
// AWS limit for session policy is 2048 characters
40+
public static final int MAX_SESSION_POLICY_LENGTH = 2048;
41+
42+
private S3STSUtils() {
43+
}
44+
45+
/**
46+
* Validates the duration in seconds.
47+
* @param durationSeconds duration in seconds
48+
* @return validated duration
49+
* @throws OMException if duration is invalid
50+
*/
51+
public static int validateDuration(Integer durationSeconds) throws OMException {
52+
if (durationSeconds == null) {
53+
return DEFAULT_DURATION_SECONDS;
54+
}
55+
56+
if (durationSeconds < MIN_DURATION_SECONDS || durationSeconds > MAX_DURATION_SECONDS) {
57+
throw new OMException(
58+
"Invalid Value: DurationSeconds must be between " + MIN_DURATION_SECONDS +
59+
" and " + MAX_DURATION_SECONDS + " seconds", INVALID_REQUEST);
60+
}
61+
62+
return durationSeconds;
63+
}
64+
65+
/**
66+
* Validates the role session name.
67+
* @param roleSessionName role session name
68+
* @throws OMException if role session name is invalid
69+
*/
70+
public static void validateRoleSessionName(String roleSessionName) throws OMException {
71+
if (StringUtils.isBlank(roleSessionName)) {
72+
throw new OMException("Missing required parameter: RoleSessionName", INVALID_REQUEST);
73+
}
74+
75+
final int roleSessionNameLength = roleSessionName.length();
76+
if (roleSessionNameLength < ASSUME_ROLE_SESSION_NAME_MIN_LENGTH ||
77+
roleSessionNameLength > ASSUME_ROLE_SESSION_NAME_MAX_LENGTH) {
78+
throw new OMException("Invalid RoleSessionName: must be " + ASSUME_ROLE_SESSION_NAME_MIN_LENGTH + "-" +
79+
ASSUME_ROLE_SESSION_NAME_MAX_LENGTH + " characters long and " +
80+
"contain only alphanumeric characters, +, =, ,, ., @, -", INVALID_REQUEST);
81+
}
82+
83+
// AWS allows: alphanumeric, +, =, ,, ., @, -
84+
// Pattern: [\w+=,.@-]*
85+
// Don't use regex for performance reasons
86+
for (int i = 0; i < roleSessionNameLength; i++) {
87+
final char c = roleSessionName.charAt(i);
88+
if (!isRoleSessionNameChar(c)) {
89+
throw new OMException("Invalid RoleSessionName: must be " + ASSUME_ROLE_SESSION_NAME_MIN_LENGTH + "-" +
90+
ASSUME_ROLE_SESSION_NAME_MAX_LENGTH + " characters long and " +
91+
"contain only alphanumeric characters, +, =, ,, ., @, -", INVALID_REQUEST);
92+
}
93+
}
94+
}
95+
96+
private static boolean isRoleSessionNameChar(char c) {
97+
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') ||
98+
c == '_' || c == '+' || c == '=' || c == ',' || c == '.' || c == '@' || c == '-';
99+
}
100+
101+
/**
102+
* Validates the session policy length.
103+
* @param awsIamSessionPolicy session policy
104+
* @throws OMException if policy length is invalid
105+
*/
106+
public static void validateSessionPolicy(String awsIamSessionPolicy) throws OMException {
107+
if (awsIamSessionPolicy != null && awsIamSessionPolicy.length() > MAX_SESSION_POLICY_LENGTH) {
108+
throw new OMException(
109+
"Policy length exceeded maximum allowed length of " + MAX_SESSION_POLICY_LENGTH, INVALID_REQUEST);
110+
}
111+
}
112+
113+
/**
114+
* Generates the assumed role user ARN.
115+
* @param validRoleArn valid role ARN
116+
* @param roleSessionName role session name
117+
* @return assumed role user ARN
118+
* @throws OMException if role ARN is invalid
119+
*/
120+
public static String toAssumedRoleUserArn(String validRoleArn, String roleSessionName) throws OMException {
121+
// We already know the roleArn is valid, so perform the conversion for assumed role user arn format
122+
// RoleArn format: arn:aws:iam::<account-id>:role/<role-name>
123+
// Assumed role user arn format: arn:aws:sts::<account-id>:assumed-role/<role-name>/<role-session-name>
124+
final String[] parts = splitRoleArnWithoutRegex(validRoleArn);
125+
126+
final String partition = parts[1];
127+
final String accountId = parts[4];
128+
final String resource = parts[5];
129+
final String roleName = resource.substring("role/".length());
130+
131+
final StringBuilder stringBuilder = new StringBuilder("arn:");
132+
stringBuilder.append(partition);
133+
stringBuilder.append(":sts::");
134+
stringBuilder.append(accountId);
135+
stringBuilder.append(":assumed-role/");
136+
stringBuilder.append(roleName);
137+
stringBuilder.append('/');
138+
stringBuilder.append(roleSessionName);
139+
return stringBuilder.toString();
140+
}
141+
142+
private static String[] splitRoleArnWithoutRegex(String roleArn) {
143+
final String[] parts = new String[6];
144+
int start = 0;
145+
for (int i = 0; i < 5; i++) {
146+
final int end = roleArn.indexOf(':', start);
147+
parts[i] = roleArn.substring(start, end);
148+
start = end + 1;
149+
}
150+
parts[5] = roleArn.substring(start);
151+
return parts;
152+
}
153+
}

hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestAwsRoleArnValidator.java renamed to hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestAwsRoleArnValidator.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@
1515
* limitations under the License.
1616
*/
1717

18-
package org.apache.hadoop.ozone.om.request.s3.security;
18+
package org.apache.hadoop.ozone.om.helpers;
1919

2020
import static org.assertj.core.api.Assertions.assertThat;
2121
import static org.junit.jupiter.api.Assertions.assertThrows;
2222

23+
import org.apache.commons.lang3.StringUtils;
2324
import org.apache.hadoop.ozone.om.exceptions.OMException;
2425
import org.junit.jupiter.api.Test;
2526

@@ -38,12 +39,12 @@ public void testValidateAndExtractRoleNameFromArnSuccessCases() throws OMExcepti
3839
assertThat(AwsRoleArnValidator.validateAndExtractRoleNameFromArn(ROLE_ARN_2)).isEqualTo("Role2");
3940

4041
// Path name right at 511-char max boundary
41-
final String arnPrefixLen511 = S3SecurityTestUtils.repeat('p', 510) + "/"; // 510 chars + '/' = 511
42+
final String arnPrefixLen511 = StringUtils.repeat('p', 510) + "/"; // 510 chars + '/' = 511
4243
final String arnMaxPath = "arn:aws:iam::123456789012:role/" + arnPrefixLen511 + "RoleB";
4344
assertThat(AwsRoleArnValidator.validateAndExtractRoleNameFromArn(arnMaxPath)).isEqualTo("RoleB");
4445

4546
// Role name right at 64-char max boundary
46-
final String roleName64 = S3SecurityTestUtils.repeat('A', 64);
47+
final String roleName64 = StringUtils.repeat('A', 64);
4748
final String arn64 = "arn:aws:iam::123456789012:role/" + roleName64;
4849
assertThat(AwsRoleArnValidator.validateAndExtractRoleNameFromArn(arn64)).isEqualTo(roleName64);
4950
}
@@ -105,7 +106,7 @@ public void testValidateAndExtractRoleNameFromArnFailureCases() {
105106
assertThat(e8.getMessage()).isEqualTo("Role ARN is required");
106107

107108
// Path name too long (> 511 characters)
108-
final String arnPrefixLen512 = S3SecurityTestUtils.repeat('q', 511) + "/"; // 511 chars + '/' = 512
109+
final String arnPrefixLen512 = StringUtils.repeat('q', 511) + "/"; // 511 chars + '/' = 512
109110
final String arnTooLongPath = "arn:aws:iam::123456789012:role/" + arnPrefixLen512 + "RoleA";
110111
final OMException e9 = assertThrows(
111112
OMException.class, () -> AwsRoleArnValidator.validateAndExtractRoleNameFromArn(arnTooLongPath));
@@ -120,12 +121,11 @@ public void testValidateAndExtractRoleNameFromArnFailureCases() {
120121
assertThat(e10.getMessage()).isEqualTo("Invalid role ARN: missing role name"); // MyRole/ is considered a path
121122

122123
// 65-char role name
123-
final String roleName65 = S3SecurityTestUtils.repeat('B', 65);
124+
final String roleName65 = StringUtils.repeat('B', 65);
124125
final String roleArn65 = "arn:aws:iam::123456789012:role/" + roleName65;
125126
final OMException e11 = assertThrows(
126127
OMException.class, () -> AwsRoleArnValidator.validateAndExtractRoleNameFromArn(roleArn65));
127128
assertThat(e11.getResult()).isEqualTo(OMException.ResultCodes.INVALID_REQUEST);
128129
assertThat(e11.getMessage()).isEqualTo("Invalid role name: " + roleName65);
129130
}
130131
}
131-

hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3AssumeRoleRequest.java

Lines changed: 11 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
import org.apache.hadoop.ozone.om.OzoneManager;
3434
import org.apache.hadoop.ozone.om.exceptions.OMException;
3535
import org.apache.hadoop.ozone.om.execution.flowcontrol.ExecutionContext;
36+
import org.apache.hadoop.ozone.om.helpers.AwsRoleArnValidator;
37+
import org.apache.hadoop.ozone.om.helpers.S3STSUtils;
3638
import org.apache.hadoop.ozone.om.request.OMClientRequest;
3739
import org.apache.hadoop.ozone.om.request.util.OmResponseUtil;
3840
import org.apache.hadoop.ozone.om.response.OMClientResponse;
@@ -62,14 +64,10 @@ public class S3AssumeRoleRequest extends OMClientRequest {
6264
SECURE_RANDOM = secureRandom;
6365
}
6466

65-
private static final int MIN_TOKEN_EXPIRATION_SECONDS = 900; // 15 minutes in seconds
66-
private static final int MAX_TOKEN_EXPIRATION_SECONDS = 43200; // 12 hours in seconds
6767
private static final int STS_ACCESS_KEY_ID_LENGTH = 20;
6868
private static final int STS_SECRET_ACCESS_KEY_LENGTH = 40;
6969
private static final int STS_ROLE_ID_LENGTH = 16;
7070
private static final String ASSUME_ROLE_ID_PREFIX = "AROA";
71-
private static final int ASSUME_ROLE_SESSION_NAME_MIN_LENGTH = 2;
72-
private static final int ASSUME_ROLE_SESSION_NAME_MAX_LENGTH = 64;
7371
private static final String CHARS_FOR_ACCESS_KEY_IDS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
7472
private static final int CHARS_FOR_ACCESS_KEY_IDS_LENGTH = CHARS_FOR_ACCESS_KEY_IDS.length();
7573
private static final String CHARS_FOR_SECRET_ACCESS_KEYS = CHARS_FOR_ACCESS_KEY_IDS +
@@ -91,19 +89,20 @@ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, Execut
9189
final int durationSeconds = assumeRoleRequest.getDurationSeconds();
9290

9391
// Validate duration
94-
if (durationSeconds < MIN_TOKEN_EXPIRATION_SECONDS || durationSeconds > MAX_TOKEN_EXPIRATION_SECONDS) {
95-
final OMException omException = new OMException(
96-
"Duration must be between " + MIN_TOKEN_EXPIRATION_SECONDS + " and " + MAX_TOKEN_EXPIRATION_SECONDS,
97-
OMException.ResultCodes.INVALID_REQUEST);
92+
try {
93+
S3STSUtils.validateDuration(durationSeconds);
94+
} catch (OMException e) {
9895
return new S3AssumeRoleResponse(
99-
createErrorOMResponse(OmResponseUtil.getOMResponseBuilder(omRequest), omException));
96+
createErrorOMResponse(OmResponseUtil.getOMResponseBuilder(omRequest), e));
10097
}
10198

10299
// Validate role session name
103100
final String roleSessionName = assumeRoleRequest.getRoleSessionName();
104-
final S3AssumeRoleResponse roleSessionNameErrorResponse = validateRoleSessionName(roleSessionName, omRequest);
105-
if (roleSessionNameErrorResponse != null) {
106-
return roleSessionNameErrorResponse;
101+
try {
102+
S3STSUtils.validateRoleSessionName(roleSessionName);
103+
} catch (OMException e) {
104+
return new S3AssumeRoleResponse(
105+
createErrorOMResponse(OmResponseUtil.getOMResponseBuilder(omRequest), e));
107106
}
108107

109108
final String roleArn = assumeRoleRequest.getRoleArn();
@@ -155,27 +154,6 @@ public OMClientResponse validateAndUpdateCache(OzoneManager ozoneManager, Execut
155154
}
156155
}
157156

158-
/**
159-
* Ensures RoleSessionName is valid.
160-
*/
161-
private S3AssumeRoleResponse validateRoleSessionName(String roleSessionName, OMRequest omRequest) {
162-
if (StringUtils.isBlank(roleSessionName)) {
163-
final OMException omException = new OMException(
164-
"RoleSessionName is required", OMException.ResultCodes.INVALID_REQUEST);
165-
return new S3AssumeRoleResponse(
166-
createErrorOMResponse(OmResponseUtil.getOMResponseBuilder(omRequest), omException));
167-
}
168-
if (roleSessionName.length() < ASSUME_ROLE_SESSION_NAME_MIN_LENGTH ||
169-
roleSessionName.length() > ASSUME_ROLE_SESSION_NAME_MAX_LENGTH) {
170-
final OMException omException = new OMException(
171-
"RoleSessionName length must be between " + ASSUME_ROLE_SESSION_NAME_MIN_LENGTH + " and " +
172-
ASSUME_ROLE_SESSION_NAME_MAX_LENGTH, OMException.ResultCodes.INVALID_REQUEST);
173-
return new S3AssumeRoleResponse(
174-
createErrorOMResponse(OmResponseUtil.getOMResponseBuilder(omRequest), omException));
175-
}
176-
return null;
177-
}
178-
179157
/**
180158
* Generates session token using components from the AssumeRoleRequest.
181159
*/

hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/request/s3/security/TestS3AssumeRoleRequest.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ public void testInvalidDurationTooShort() {
142142
final OMResponse omResponse = response.getOMResponse();
143143

144144
assertThat(omResponse.getStatus()).isEqualTo(Status.INVALID_REQUEST);
145-
assertThat(omResponse.getMessage()).isEqualTo("Duration must be between 900 and 43200");
145+
assertThat(omResponse.getMessage()).isEqualTo(
146+
"Invalid Value: DurationSeconds must be between 900 and 43200 seconds");
146147
assertThat(omResponse.hasAssumeRoleResponse()).isFalse();
147148
}
148149

@@ -161,7 +162,8 @@ public void testInvalidDurationTooLong() {
161162
final OMResponse omResponse = response.getOMResponse();
162163

163164
assertThat(omResponse.getStatus()).isEqualTo(Status.INVALID_REQUEST);
164-
assertThat(omResponse.getMessage()).isEqualTo("Duration must be between 900 and 43200");
165+
assertThat(omResponse.getMessage()).isEqualTo(
166+
"Invalid Value: DurationSeconds must be between 900 and 43200 seconds");
165167
assertThat(omResponse.hasAssumeRoleResponse()).isFalse();
166168
}
167169

@@ -326,7 +328,7 @@ public void testAssumeRoleWithEmptySessionName() {
326328
final OMClientResponse response = new S3AssumeRoleRequest(omRequest, CLOCK)
327329
.validateAndUpdateCache(ozoneManager, context);
328330
assertThat(response.getOMResponse().getStatus()).isEqualTo(Status.INVALID_REQUEST);
329-
assertThat(response.getOMResponse().getMessage()).isEqualTo("RoleSessionName is required");
331+
assertThat(response.getOMResponse().getMessage()).isEqualTo("Missing required parameter: RoleSessionName");
330332
}
331333

332334
@Test
@@ -343,7 +345,9 @@ public void testInvalidAssumeRoleSessionNameTooShort() {
343345
final OMResponse omResponse = response.getOMResponse();
344346

345347
assertThat(omResponse.getStatus()).isEqualTo(Status.INVALID_REQUEST);
346-
assertThat(omResponse.getMessage()).isEqualTo("RoleSessionName length must be between 2 and 64");
348+
assertThat(omResponse.getMessage()).isEqualTo(
349+
"Invalid RoleSessionName: must be 2-64 characters long and contain only alphanumeric " +
350+
"characters, +, =, ,, ., @, -");
347351
assertThat(omResponse.hasAssumeRoleResponse()).isFalse();
348352
}
349353

@@ -362,7 +366,10 @@ public void testInvalidRoleSessionNameTooLong() {
362366
final OMResponse omResponse = response.getOMResponse();
363367

364368
assertThat(omResponse.getStatus()).isEqualTo(Status.INVALID_REQUEST);
365-
assertThat(omResponse.getMessage()).isEqualTo("RoleSessionName length must be between 2 and 64");
369+
assertThat(omResponse.getMessage()).isEqualTo(
370+
"Invalid RoleSessionName: must be 2-64 characters long and contain only alphanumeric " +
371+
"characters, +, =, ,, ., @, -"
372+
);
366373
assertThat(omResponse.hasAssumeRoleResponse()).isFalse();
367374
}
368375

0 commit comments

Comments
 (0)