Skip to content
Draft
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 @@ -693,6 +693,10 @@ public final class OzoneConfigKeys {
public static final String OZONE_CLIENT_ELASTIC_BYTE_BUFFER_POOL_MAX_SIZE =
"ozone.client.elastic.byte.buffer.pool.max.size";
public static final String OZONE_CLIENT_ELASTIC_BYTE_BUFFER_POOL_MAX_SIZE_DEFAULT = "16GB";

public static final String OZONE_S3G_STS_HTTP_ENABLED_KEY =
"ozone.s3g.sts.http.enabled";
public static final boolean OZONE_S3G_STS_HTTP_ENABLED_DEFAULT = false;

/**
* There is no need to instantiate this class.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* 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 org.apache.hadoop.ozone.om.exceptions.OMException;
Expand Down Expand Up @@ -125,7 +125,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 +134,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
@@ -0,0 +1,153 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.hadoop.ozone.om.helpers;

import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.INVALID_REQUEST;

import net.jcip.annotations.Immutable;
import org.apache.commons.lang3.StringUtils;
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() {
}

/**
* 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 (StringUtils.isBlank(roleSessionName)) {
throw new OMException("Missing required parameter: RoleSessionName", 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: must be " + ASSUME_ROLE_SESSION_NAME_MIN_LENGTH + "-" +
ASSUME_ROLE_SESSION_NAME_MAX_LENGTH + " characters long and " +
"contain only alphanumeric characters, +, =, ,, ., @, -", 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 RoleSessionName: must be " + ASSUME_ROLE_SESSION_NAME_MIN_LENGTH + "-" +
ASSUME_ROLE_SESSION_NAME_MAX_LENGTH + " characters long and " +
"contain only alphanumeric characters, +, =, ,, ., @, -", 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(
"Policy length exceeded maximum allowed length of " + 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
* @throws OMException if role ARN is invalid
*/
public static String toAssumedRoleUserArn(String validRoleArn, String roleSessionName) throws OMException {
// 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());

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 Down Expand Up @@ -105,7 +106,7 @@ public void testValidateAndExtractRoleNameFromArnFailureCases() {
assertThat(e8.getMessage()).isEqualTo("Role ARN is required");

// Path name too long (> 511 characters)
final String arnPrefixLen512 = S3SecurityTestUtils.repeat('q', 511) + "/"; // 511 chars + '/' = 512
final String 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 +121,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 @@ -476,6 +476,7 @@ public final class OzoneManager extends ServiceRuntimeInfoImpl

private final boolean isS3MultiTenancyEnabled;
private final boolean isStrictS3;
private final boolean isS3STSEnabled;
private ExitManager exitManager;

private OzoneManagerPrepareState prepareState;
Expand Down Expand Up @@ -685,6 +686,11 @@ private OzoneManager(OzoneConfiguration conf, StartupOption startupOption)
this.isS3MultiTenancyEnabled =
OMMultiTenantManager.checkAndEnableMultiTenancy(this, conf);

// Enable S3 STS if config key is set
this.isS3STSEnabled = conf.getBoolean(
OzoneConfigKeys.OZONE_S3G_STS_HTTP_ENABLED_KEY,
OzoneConfigKeys.OZONE_S3G_STS_HTTP_ENABLED_DEFAULT);

metrics = OMMetrics.create();
omSnapshotIntMetrics = OmSnapshotInternalMetrics.create();
perfMetrics = OMPerformanceMetrics.register();
Expand Down Expand Up @@ -1137,6 +1143,13 @@ public boolean isStrictS3() {
return isStrictS3;
}

/**
* Returns true if S3 STS is enabled; false otherwise.
*/
public boolean isS3STSEnabled() {
return isS3STSEnabled;
}

/**
* Throws OMException FEATURE_NOT_ENABLED if S3 multi-tenancy is not enabled.
*/
Expand All @@ -1150,6 +1163,21 @@ public void checkS3MultiTenancyEnabled() throws OMException {
FEATURE_NOT_ENABLED);
}

/**
* Throws OMException FEATURE_NOT_ENABLED if S3 STS (AssumeRole) is not enabled.
*/
public void checkS3STSEnabled() throws OMException {
if (isS3STSEnabled()) {
if (getAccessAuthorizer().isNative()) {
throw new OMException("S3 STS is not enabled for Ozone Native Authorizer", FEATURE_NOT_ENABLED);
}
return;
}

throw new OMException("S3 STS is not enabled. Please set " + OzoneConfigKeys.OZONE_S3G_STS_HTTP_ENABLED_KEY +
" to true and restart all OMs.", FEATURE_NOT_ENABLED);
}

/**
* Return config value of {@link OzoneConfigKeys#OZONE_SECURITY_ENABLED_KEY}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Clock;
import java.time.ZoneOffset;
import org.apache.hadoop.hdds.conf.ConfigurationSource;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.hdds.security.SecurityConfig;
Expand Down Expand Up @@ -68,6 +70,7 @@
import org.apache.hadoop.ozone.om.request.key.acl.prefix.OMPrefixSetAclRequest;
import org.apache.hadoop.ozone.om.request.s3.multipart.S3ExpiredMultipartUploadsAbortRequest;
import org.apache.hadoop.ozone.om.request.s3.security.OMSetSecretRequest;
import org.apache.hadoop.ozone.om.request.s3.security.S3AssumeRoleRequest;
import org.apache.hadoop.ozone.om.request.s3.security.S3DeleteRevokedSTSTokensRequest;
import org.apache.hadoop.ozone.om.request.s3.security.S3GetSecretRequest;
import org.apache.hadoop.ozone.om.request.s3.security.S3RevokeSTSTokenRequest;
Expand Down Expand Up @@ -119,6 +122,8 @@ public final class OzoneManagerRatisUtils {
private static final Logger LOG = LoggerFactory
.getLogger(OzoneManagerRatisUtils.class);

private static final Clock CLOCK = Clock.system(ZoneOffset.UTC);

private OzoneManagerRatisUtils() {
}

Expand Down Expand Up @@ -198,6 +203,9 @@ public static OMClientRequest createClientRequest(OMRequest omRequest,
return new OMSetSecretRequest(omRequest);
case RevokeS3Secret:
return new S3RevokeSecretRequest(omRequest);
case AssumeRole:
ozoneManager.checkS3STSEnabled();
return new S3AssumeRoleRequest(omRequest, CLOCK);
case RevokeSTSToken:
return new S3RevokeSTSTokenRequest(omRequest);
case DeleteRevokedSTSTokens:
Expand Down
Loading