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 @@ -29,6 +29,8 @@ public class S3Auth {
private String userPrincipal;
// Optional STS session token when using temporary credentials
private String sessionToken;
// S3 action without s3: prefix (e.g. PutObject), set by S3 Gateway for use in finer-grained STS permissions.
private String s3Action;

public S3Auth(final String stringToSign,
final String signature,
Expand Down Expand Up @@ -67,4 +69,12 @@ public String getSessionToken() {
public void setSessionToken(String sessionToken) {
this.sessionToken = sessionToken;
}

public String getS3Action() {
return s3Action;
}

public void setS3Action(String s3Action) {
this.s3Action = s3Action;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,9 @@ private OMResponse submitRequest(OMRequest omRequest)
if (threadLocalS3Auth.get().getSessionToken() != null) {
s3AuthBuilder.setSessionToken(threadLocalS3Auth.get().getSessionToken());
}
if (threadLocalS3Auth.get().getS3Action() != null) {
s3AuthBuilder.setS3Action(threadLocalS3Auth.get().getS3Action());
}

builder.setS3Authentication(s3AuthBuilder.build());
}
Expand Down Expand Up @@ -1703,10 +1706,7 @@ public OmMultipartCommitUploadPartInfo commitMultipartUploadPart(
handleError(submitRequest(omRequest))
.getCommitMultiPartUploadResponse();

OmMultipartCommitUploadPartInfo info = new
OmMultipartCommitUploadPartInfo(response.getPartName(),
response.getETag());
return info;
return new OmMultipartCommitUploadPartInfo(response.getPartName(), response.getETag());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1572,7 +1572,6 @@ message CommitKeyRequest {
}

message CommitKeyResponse {

}

message AllocateBlockRequest {
Expand Down Expand Up @@ -2311,6 +2310,17 @@ message S3Authentication {
// If present, indicates this request uses STS temporary credentials
// and carries the base64-encoded session token for validation.
optional string sessionToken = 4;
// The following fields are resolved from the STS session token by OM.
// They are used to enforce STS session policies during Ratis apply.
// They must be written or cleared by the OM leader when the token is validated.
optional string resolvedStsSessionPolicy = 5;
optional string resolvedStsRoleArn = 6;
optional string resolvedStsOriginalAccessKeyId = 7;
optional string resolvedStsTempAccessKeyId = 8;
optional string resolvedStsSecretKeyId = 9;
// S3 action without the s3: prefix for this request (e.g. GetObject), set by S3 Gateway for use
// in finer-grained STS permissions.
optional string s3Action = 10;
}

message RecoverLeaseRequest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8656,4 +8656,4 @@
}
}
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import org.apache.hadoop.ozone.om.helpers.OzoneFileStatusLight;
import org.apache.hadoop.ozone.om.helpers.S3VolumeContext;
import org.apache.hadoop.ozone.om.protocolPB.grpc.GrpcClientConstants;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.S3Authentication;
import org.apache.hadoop.ozone.security.STSTokenIdentifier;
import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer;
import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLIdentityType;
Expand Down Expand Up @@ -236,9 +237,7 @@ public List<OzoneFileStatus> listStatus(OmKeyArgs args, boolean recursive,
try {
if (isAclEnabled) {
if (isStsS3Request()) {
// We need to be able to tell the difference between being able to download a file and merely seeing the file
// name in a list. Use READ for download ability and LIST (here) for listing.
// When listPrefix is set (original S3 ListObjects prefix), authorize LIST on that prefix for the whole
// When listPrefix is set (original S3 ListObjects prefix), authorize READ on that prefix for the whole
// listing, including FSO traversal where keyName is an internal directory (e.g. userA) under prefix user.
final String listPrefix = args.getListPrefix();
final String keyName = args.getKeyName();
Expand All @@ -258,7 +257,7 @@ public List<OzoneFileStatus> listStatus(OmKeyArgs args, boolean recursive,
} else {
aclKey = "*";
}
checkAcls(ResourceType.KEY, StoreType.OZONE, ACLType.LIST, bucket.realVolume(), bucket.realBucket(), aclKey);
checkAcls(ResourceType.KEY, StoreType.OZONE, ACLType.READ, bucket.realVolume(), bucket.realBucket(), aclKey);
} else {
checkAcls(getResourceType(args), StoreType.OZONE, ACLType.READ,
bucket, args.getKeyName());
Expand Down Expand Up @@ -304,12 +303,7 @@ public OzoneFileStatus getFileStatus(OmKeyArgs args) throws IOException {

try {
if (isAclEnabled) {
if (isStsS3Request()) {
checkAcls(getResourceType(args), StoreType.OZONE, ACLType.LIST, bucket, args.getKeyName());
} else {
checkAcls(getResourceType(args), StoreType.OZONE, ACLType.READ,
bucket, args.getKeyName());
}
checkAcls(getResourceType(args), StoreType.OZONE, ACLType.READ, bucket, args.getKeyName());
}
metrics.incNumGetFileStatus();
return keyManager.getFileStatus(args, getClientAddress());
Expand Down Expand Up @@ -384,7 +378,7 @@ public ListKeysResult listKeys(String volumeName, String bucketName,
final String aclKey = (keyPrefix == null || keyPrefix.isEmpty()) ? "*" : keyPrefix;
captureLatencyNs(
perfMetrics.getListKeysAclCheckLatencyNs(), () -> checkAcls(
ResourceType.KEY, StoreType.OZONE, ACLType.LIST, bucket.realVolume(), bucket.realBucket(), aclKey));
ResourceType.KEY, StoreType.OZONE, ACLType.READ, bucket.realVolume(), bucket.realBucket(), aclKey));
} else {
captureLatencyNs(perfMetrics.getListKeysAclCheckLatencyNs(), () ->
checkAcls(ResourceType.BUCKET, StoreType.OZONE, ACLType.LIST,
Expand Down Expand Up @@ -634,7 +628,8 @@ public boolean checkAcls(ResourceType resType, StoreType storeType,
public boolean checkAcls(OzoneObj obj, RequestContext context,
boolean throwIfPermissionDenied) throws OMException {

final RequestContext normalizedRequestContext = maybeAttachSessionPolicyFromThreadLocal(context);
final RequestContext normalizedRequestContext = maybeAttachS3ActionFromThreadLocal(
maybeAttachSessionPolicyFromThreadLocal(context));

if (!captureLatencyNs(perfMetrics::setCheckAccessLatencyNs,
() -> accessAuthorizer.checkAccess(obj, normalizedRequestContext))) {
Expand Down Expand Up @@ -692,6 +687,22 @@ private RequestContext maybeAttachSessionPolicyFromThreadLocal(RequestContext co
.build();
}

/**
* Attaches s3 action to RequestContext if an S3Authentication is found in the Ozone Manager thread local,
* and it has an s3 action. Otherwise, returns the RequestContext as it was before.
* @param context the original RequestContext
* @return RequestContext as before or with s3 action embedded
*/
private RequestContext maybeAttachS3ActionFromThreadLocal(RequestContext context) {
final S3Authentication s3Authentication = OzoneManager.getS3Auth();
if (s3Authentication == null || !s3Authentication.hasS3Action()) {
return context;
}
return context.toBuilder()
.setS3Action(s3Authentication.getS3Action())
.build();
}

static String getClientAddress() {
String clientMachine = Server.getRemoteAddress();
if (clientMachine == null) { //not a RPC client
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
import org.apache.hadoop.ozone.protocolPB.OzoneManagerRequestHandler;
import org.apache.hadoop.ozone.protocolPB.RequestHandler;
import org.apache.hadoop.ozone.security.STSSecurityUtil;
import org.apache.hadoop.ozone.security.STSTokenIdentifier;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.util.Time;
import org.apache.hadoop.util.concurrent.HadoopExecutors;
Expand Down Expand Up @@ -617,7 +619,34 @@ public void close() {
*/
@VisibleForTesting
OMResponse runCommand(OMRequest request, TermIndex termIndex) {
boolean isS3AuthThreadLocalSet = false;
boolean isStsThreadLocalSet = false;
try {
if (ozoneManager.isSecurityEnabled() && request.hasS3Authentication()) {
// STS token verification runs on the leader RPC path so we don't need to recheck here on the apply
// after the log is committed
STSSecurityUtil.ensureResolvedStsFieldsInvariants(request);

final OzoneManagerProtocolProtos.S3Authentication s3Auth = request.getS3Authentication();
// ThreadLocal carries S3 action for OmMetadataReader.
OzoneManager.setS3Auth(s3Auth);
isS3AuthThreadLocalSet = true;

if (s3Auth.hasSessionToken() && !s3Auth.getSessionToken().isEmpty()) {
// ThreadLocal carries session policy for OmMetadataReader
final STSTokenIdentifier rehydratedTokenIdentifier = new STSTokenIdentifier(
s3Auth.hasResolvedStsTempAccessKeyId() ? s3Auth.getResolvedStsTempAccessKeyId() : "",
s3Auth.hasResolvedStsOriginalAccessKeyId() ? s3Auth.getResolvedStsOriginalAccessKeyId() : "",
s3Auth.hasResolvedStsRoleArn() ? s3Auth.getResolvedStsRoleArn() : "",
java.time.Instant.MAX, // ensure it deterministically is not expired
"", // no secretAccessKey needed
s3Auth.hasResolvedStsSessionPolicy() ? s3Auth.getResolvedStsSessionPolicy() : "",
null // no encryption key needed
);
OzoneManager.setStsTokenIdentifier(rehydratedTokenIdentifier);
isStsThreadLocalSet = true;
}
}
ExecutionContext context = ExecutionContext.of(termIndex.getIndex(), termIndex);
final OMClientResponse omClientResponse = handler.handleWriteRequest(
request, context, ozoneManagerDoubleBuffer);
Expand All @@ -636,6 +665,13 @@ OMResponse runCommand(OMRequest request, TermIndex termIndex) {
// For any Runtime exceptions, terminate OM.
String errorMessage = "Request " + request + " failed with exception";
ExitUtils.terminate(1, errorMessage, e, LOG);
} finally {
if (isS3AuthThreadLocalSet) {
OzoneManager.setS3Auth(null);
}
if (isStsThreadLocalSet) {
OzoneManager.setStsTokenIdentifier(null);
}
}
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.hdds.utils.TransactionInfo;
import org.apache.hadoop.ipc_.ProtobufRpcEngine;
Expand Down Expand Up @@ -54,6 +55,7 @@
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.LayoutVersion;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMRequest;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMResponse;
import org.apache.hadoop.ozone.security.STSTokenIdentifier;
import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer;
import org.apache.hadoop.ozone.security.acl.OzoneObj;
import org.apache.hadoop.ozone.security.acl.OzoneObjInfo;
Expand Down Expand Up @@ -112,15 +114,50 @@ public OMClientRequest(OMRequest omRequest) {
*/
public OMRequest preExecute(OzoneManager ozoneManager)
throws IOException {
LayoutVersion layoutVersion = LayoutVersion.newBuilder()
final LayoutVersion layoutVersion = LayoutVersion.newBuilder()
.setVersion(ozoneManager.getVersionManager().getMetadataLayoutVersion())
.build();
omRequest = getOmRequest().toBuilder()

final OMRequest.Builder requestBuilder = getOmRequest().toBuilder()
.setUserInfo(getUserIfNotExists(ozoneManager))
.setLayoutVersion(layoutVersion).build();
.setLayoutVersion(layoutVersion);

if (requestBuilder.hasS3Authentication()) {
requestBuilder.setS3Authentication(
resolveS3Authentication(requestBuilder.getS3Authentication(), OzoneManager.getStsTokenIdentifier()));
}

omRequest = requestBuilder.build();
return omRequest;
}

private static OzoneManagerProtocolProtos.S3Authentication resolveS3Authentication(
OzoneManagerProtocolProtos.S3Authentication s3Auth, STSTokenIdentifier stsTokenIdentifier) {
final OzoneManagerProtocolProtos.S3Authentication.Builder s3AuthBuilder = s3Auth.toBuilder();

if (s3Auth.hasSessionToken() && !s3Auth.getSessionToken().isEmpty() && stsTokenIdentifier != null) {
s3AuthBuilder.setResolvedStsSessionPolicy(
StringUtils.defaultString(stsTokenIdentifier.getSessionPolicy()));
s3AuthBuilder.setResolvedStsRoleArn(
StringUtils.defaultString(stsTokenIdentifier.getRoleArn()));
s3AuthBuilder.setResolvedStsOriginalAccessKeyId(
StringUtils.defaultString(stsTokenIdentifier.getOriginalAccessKeyId()));
s3AuthBuilder.setResolvedStsTempAccessKeyId(
StringUtils.defaultString(stsTokenIdentifier.getTempAccessKeyId()));
final UUID secretKeyId = stsTokenIdentifier.getSecretKeyId();
s3AuthBuilder.setResolvedStsSecretKeyId(
secretKeyId != null ? secretKeyId.toString() : "");
} else {
s3AuthBuilder.clearResolvedStsSessionPolicy();
s3AuthBuilder.clearResolvedStsRoleArn();
s3AuthBuilder.clearResolvedStsOriginalAccessKeyId();
s3AuthBuilder.clearResolvedStsTempAccessKeyId();
s3AuthBuilder.clearResolvedStsSecretKeyId();
}

return s3AuthBuilder.build();
}

/**
* Performs any request specific failure handling during request
* submission. An example of this would be an undo of any steps
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,15 @@ public class OMBucketSetAclRequest extends OMBucketAclRequest {

@Override
public OMRequest preExecute(OzoneManager ozoneManager) throws IOException {
long modificationTime = Time.now();
OzoneManagerProtocolProtos.SetAclRequest.Builder setAclRequestBuilder =
final long modificationTime = Time.now();
final OzoneManagerProtocolProtos.SetAclRequest.Builder setAclRequestBuilder =
getOmRequest().getSetAclRequest().toBuilder()
.setModificationTime(modificationTime);

return getOmRequest().toBuilder()
// super.preExecute resolves S3Authentication (STS) for Ratis apply. Merge SetAclRequest changes on top.
final OMRequest request = super.preExecute(ozoneManager);
return request.toBuilder()
.setSetAclRequest(setAclRequestBuilder)
.setUserInfo(getUserInfo())
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
import org.apache.hadoop.hdds.security.symmetric.ManagedSecretKey;
import org.apache.hadoop.hdds.security.symmetric.SecretKeyClient;
import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.OMTokenProto;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.S3Authentication;
import org.apache.hadoop.security.token.SecretManager;
import org.apache.hadoop.security.token.Token;

Expand Down Expand Up @@ -179,5 +181,44 @@ static void ensureEssentialFieldsArePresentInToken(STSTokenIdentifier stsTokenId
throw new SecretManager.InvalidToken("Invalid STS token - secretAccessKey is null/empty");
}
}

/**
* Ensures STS-related {@link S3Authentication} fields are structurally consistent on the Ratis
* apply path. Cryptographic validation (signature, expiry, secret key lookup) runs on the leader
* RPC path (e.g. {@code S3SecurityUtil.validateS3Credential}). This method performs no crypto and does
* not contact {@link SecretKeyClient}, keeping the apply thread deterministic and lightweight.
*
* @param request OM request possibly containing S3 authentication
* @throws OMException if resolved fields and session token presence are inconsistent
*/
public static void ensureResolvedStsFieldsInvariants(OzoneManagerProtocolProtos.OMRequest request)
throws OMException {
if (!request.hasS3Authentication()) {
return;
}

final S3Authentication s3Auth = request.getS3Authentication();
final boolean hasSessionToken = s3Auth.hasSessionToken() && !s3Auth.getSessionToken().isEmpty();

if (!hasSessionToken) {
// If sessionToken is missing/empty, resolved fields must be empty.
if (s3Auth.hasResolvedStsSessionPolicy() || s3Auth.hasResolvedStsRoleArn() ||
s3Auth.hasResolvedStsOriginalAccessKeyId() || s3Auth.hasResolvedStsTempAccessKeyId() ||
s3Auth.hasResolvedStsSecretKeyId()) {
throw new OMException("Resolved STS fields must be empty when sessionToken is not present", INVALID_TOKEN);
}
return;
}

ensureResolvedFieldsArePresent(s3Auth);
}

private static void ensureResolvedFieldsArePresent(S3Authentication s3Auth) throws OMException {
if (!s3Auth.hasResolvedStsSessionPolicy() || !s3Auth.hasResolvedStsRoleArn() ||
!s3Auth.hasResolvedStsOriginalAccessKeyId() || !s3Auth.hasResolvedStsTempAccessKeyId() ||
!s3Auth.hasResolvedStsSecretKeyId()) {
throw new OMException("Resolved STS fields must be present when sessionToken is present", INVALID_TOKEN);
}
}
}

Loading