Skip to content
Open
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 @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@
package org.apache.hadoop.ozone.om.ratis;

import static org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Status.INVALID_REQUEST;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

Expand All @@ -44,6 +46,8 @@
import org.apache.hadoop.ozone.om.request.OMRequestTestUtils;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
import org.apache.hadoop.ozone.protocolPB.OzoneManagerProtocolServerSideTranslatorPB;
import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer;
import org.apache.ratis.protocol.ClientId;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

Expand Down Expand Up @@ -136,4 +140,58 @@ public void testUnknownRequestHandling()

assertEquals(expectedResponse, actualResponse);
}

@Test
public void testAssumeRoleRejectedWhenStsDisabled() {
ozoneManager = mock(OzoneManager.class, CALLS_REAL_METHODS);
when(ozoneManager.isS3STSEnabled()).thenReturn(false);

final OzoneManagerProtocolProtos.OMRequest omRequest =
OzoneManagerProtocolProtos.OMRequest.newBuilder()
.setCmdType(OzoneManagerProtocolProtos.Type.AssumeRole)
.setClientId(ClientId.randomId().toString())
.build();

final OMException omException = assertThrows(OMException.class,
() -> OzoneManagerRatisUtils.createClientRequest(omRequest, ozoneManager));
assertEquals(OMException.ResultCodes.FEATURE_NOT_ENABLED, omException.getResult());
}

@Test
public void testAssumeRoleRejectedWhenStsEnabledButNativeAuthorizerUsed() {
ozoneManager = mock(OzoneManager.class, CALLS_REAL_METHODS);
when(ozoneManager.isS3STSEnabled()).thenReturn(true);

final IAccessAuthorizer authorizer = mock(IAccessAuthorizer.class);
when(authorizer.isNative()).thenReturn(true);
when(ozoneManager.getAccessAuthorizer()).thenReturn(authorizer);

final OzoneManagerProtocolProtos.OMRequest omRequest =
OzoneManagerProtocolProtos.OMRequest.newBuilder()
.setCmdType(OzoneManagerProtocolProtos.Type.AssumeRole)
.setClientId(ClientId.randomId().toString())
.build();

final OMException omException = assertThrows(OMException.class,
() -> OzoneManagerRatisUtils.createClientRequest(omRequest, ozoneManager));
assertEquals(OMException.ResultCodes.FEATURE_NOT_ENABLED, omException.getResult());
}

@Test
public void testAssumeRoleAllowedWhenStsEnabledAndNativeAuthorizerNotUsed() {
ozoneManager = mock(OzoneManager.class, CALLS_REAL_METHODS);
when(ozoneManager.isS3STSEnabled()).thenReturn(true);

final IAccessAuthorizer authorizer = mock(IAccessAuthorizer.class);
when(authorizer.isNative()).thenReturn(false);
when(ozoneManager.getAccessAuthorizer()).thenReturn(authorizer);

final OzoneManagerProtocolProtos.OMRequest omRequest =
OzoneManagerProtocolProtos.OMRequest.newBuilder()
.setCmdType(OzoneManagerProtocolProtos.Type.AssumeRole)
.setClientId(ClientId.randomId().toString())
.build();

assertDoesNotThrow(() -> OzoneManagerRatisUtils.createClientRequest(omRequest, ozoneManager));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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 contains test classes for OM Ratis server implementation.
*/
package org.apache.hadoop.ozone.om.ratis;
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ public void initialization() {
s3Auth = new S3Auth(signatureInfo.getStringToSign(),
signatureInfo.getSignature(),
signatureInfo.getAwsAccessId(), signatureInfo.getAwsAccessId());
if (signatureInfo.getSessionToken() != null &&
!signatureInfo.getSessionToken().isEmpty()) {
s3Auth.setSessionToken(signatureInfo.getSessionToken());
}
LOG.debug("S3 access id: {}", s3Auth.getAccessID());
ClientProtocol clientProtocol =
getClient().getObjectStore().getClientProxy();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import static org.apache.hadoop.ozone.s3sts.S3STSConfigKeys.OZONE_S3G_STS_PAYLOAD_HASH_MAX_VALUE;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
Expand Down Expand Up @@ -68,6 +69,8 @@ public class AWSSignatureProcessor implements SignatureProcessor {
private static final AuditLogger AUDIT =
new AuditLogger(AuditLoggerType.S3GLOGGER);

private static final String X_AMZ_SECURITY_TOKEN = "x-amz-security-token";

@Context
private ContainerRequestContext context;

Expand Down Expand Up @@ -103,13 +106,49 @@ public SignatureInfo parseSignature() throws OS3Exception, IOException, NoSuchAl
if (signatureInfo == null) {
signatureInfo = new SignatureInfo.Builder(Version.NONE).setService("s3").build();
}

// Capture STS session token if present (header-based or query-based).
// - Header-based SigV4: x-amz-security-token
// - Query-based (for presigned URLs): X-Amz-Security-Token
final String sessionToken = extractSessionToken(headers);
if (sessionToken != null && !sessionToken.isEmpty()) {
signatureInfo.setSessionToken(sessionToken);
}

String payloadHash = getPayloadHash(headers, signatureInfo);
signatureInfo.setPayloadHash(payloadHash);
signatureInfo.setUnfilteredURI(
context.getUriInfo().getRequestUri().getPath());
return signatureInfo;
}

private String extractSessionToken(LowerCaseKeyStringMap headers) {
// Header-based token
final String headerToken = headers.get(X_AMZ_SECURITY_TOKEN);
if (headerToken != null && !headerToken.isEmpty()) {
return headerToken;
}

// Query-based token - this would be used for presigned URLs
final MultivaluedMap<String, String> queryParams = context.getUriInfo().getQueryParameters();
if (queryParams == null) {
return null;
}
for (Map.Entry<String, List<String>> entry : queryParams.entrySet()) {
final String key = entry.getKey();
if (Strings.isNullOrEmpty(key)) {
continue;
}
if (key.compareToIgnoreCase(X_AMZ_SECURITY_TOKEN) == 0) {
final List<String> values = entry.getValue();
if (values != null && !values.isEmpty()) {
return values.get(0);
}
}
}
return null;
}

private String getPayloadHash(Map<String, String> headers, SignatureInfo signatureInfo)
throws OS3Exception, NoSuchAlgorithmException, IOException {
if (signatureInfo.getVersion() == Version.V2) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ public class SignatureInfo {

private String service = null;

/**
* Optional AWS session token (x-amz-security-token / X-Amz-Security-Token).
* <p>
* This is required for STS temporary credentials when calling S3 APIs.
*/
private String sessionToken = null;

public SignatureInfo() { }

private SignatureInfo(Builder b) {
Expand All @@ -78,7 +85,8 @@ public void initialize(SignatureInfo signatureInfo) {
.setUnfilteredURI(signatureInfo.getUnfilteredURI())
.setStringToSign(signatureInfo.getStringToSign())
.setPayloadHash(signatureInfo.getPayloadHash())
.setService(signatureInfo.getService()));
.setService(signatureInfo.getService())
.setSessionToken(signatureInfo.getSessionToken()));
}

private void initialize(Builder b) {
Expand All @@ -95,6 +103,7 @@ private void initialize(Builder b) {
this.stringToSign = b.stringToSign;
this.payloadHash = b.payloadHash;
this.service = b.service;
this.sessionToken = b.sessionToken;
}

public String getAwsAccessId() {
Expand Down Expand Up @@ -165,6 +174,14 @@ public void setService(String service) {
this.service = service;
}

public String getSessionToken() {
return sessionToken;
}

public void setSessionToken(String sessionToken) {
this.sessionToken = sessionToken;
}

/**
* Signature version.
*/
Expand All @@ -189,6 +206,7 @@ public static class Builder {
private String stringToSign = null;
private String payloadHash = null;
private String service = null;
private String sessionToken = null;

public Builder(Version version) {
this.version = version;
Expand Down Expand Up @@ -254,6 +272,11 @@ public Builder setService(String service) {
return this;
}

public Builder setSessionToken(String sessionToken) {
this.sessionToken = sessionToken;
return this;
}

public SignatureInfo build() {
return new SignatureInfo(this);
}
Expand Down
Loading
Loading