This is an automated email from the ASF dual-hosted git repository.
ChenSammi pushed a commit to branch HDDS-13323-sts
in repository https://gitbox.apache.org/repos/asf/ozone.git
The following commit(s) were added to refs/heads/HDDS-13323-sts by this push:
new fd0ae5c7f7e HDDS-15137. [STS] Ensure each S3 API has an associated S3
Action (#10197)
fd0ae5c7f7e is described below
commit fd0ae5c7f7ec626c326a20be1b972a5e8e784771
Author: fmorg-git <[email protected]>
AuthorDate: Fri Jun 12 01:10:31 2026 -0700
HDDS-15137. [STS] Ensure each S3 API has an associated S3 Action (#10197)
---
.../apache/hadoop/ozone/om/protocol/S3Auth.java | 10 ++
...OzoneManagerProtocolClientSideTranslatorPB.java | 8 +-
.../src/main/proto/OmClientProtocol.proto | 3 +
.../apache/hadoop/ozone/om/OmMetadataReader.java | 59 ++++-----
.../org/apache/hadoop/ozone/om/OzoneManager.java | 7 +-
.../ozone/om/ratis/OzoneManagerStateMachine.java | 8 ++
.../hadoop/ozone/om/request/OMClientRequest.java | 2 +-
.../hadoop/ozone/om/TestOMMetadataReader.java | 140 +++++++++++++-------
.../hadoop/ozone/s3/endpoint/BucketEndpoint.java | 17 ++-
.../hadoop/ozone/s3/endpoint/EndpointBase.java | 36 ++++++
.../hadoop/ozone/s3/endpoint/ObjectEndpoint.java | 144 +++++++++++----------
.../hadoop/ozone/s3/endpoint/RootEndpoint.java | 8 +-
.../hadoop/ozone/s3/endpoint/S3RequestContext.java | 2 +
.../hadoop/ozone/s3/util/S3GActionIamMapper.java | 92 +++++++++++++
.../ozone/s3/endpoint/TestCopyActionsAudit.java | 133 +++++++++++++++++++
.../ozone/s3/util/TestS3GActionIamMapper.java | 68 ++++++++++
.../apache/hadoop/ozone/s3/util/package-info.java | 21 +++
17 files changed, 593 insertions(+), 165 deletions(-)
diff --git
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/S3Auth.java
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/S3Auth.java
index fa023dfc811..577339c96ac 100644
---
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/S3Auth.java
+++
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/S3Auth.java
@@ -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,
@@ -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;
+ }
}
diff --git
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
index 3030052d75d..8fdf9712c08 100644
---
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
+++
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
@@ -332,6 +332,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());
}
@@ -1788,10 +1791,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
diff --git
a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
index 5cf503275a2..7674d85ca92 100644
--- a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
+++ b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
@@ -2346,6 +2346,9 @@ message S3Authentication {
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 {
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
index 46573917c88..40caed20554 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataReader.java
@@ -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;
@@ -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();
@@ -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());
@@ -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());
@@ -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,
@@ -612,16 +606,15 @@ public boolean checkAcls(ResourceType resType, StoreType
storeType,
.setVolumeName(vol)
.setBucketName(bucket)
.setKeyName(key).build();
- RequestContext context = RequestContext.newBuilder()
+ RequestContext.Builder contextBuilder = RequestContext.newBuilder()
.setClientUgi(ugi)
.setIp(remoteAddress)
.setHost(hostName)
.setAclType(ACLIdentityType.USER)
.setAclRights(aclType)
- .setOwnerName(owner)
- .build();
+ .setOwnerName(owner);
- return checkAcls(obj, context, throwIfPermissionDenied);
+ return checkAcls(obj, contextBuilder, throwIfPermissionDenied);
}
/**
@@ -631,13 +624,14 @@ public boolean checkAcls(ResourceType resType, StoreType
storeType,
* @throws OMException ResultCodes.PERMISSION_DENIED if permission denied
* and throwOnPermissionDenied set to true.
*/
- public boolean checkAcls(OzoneObj obj, RequestContext context,
+ public boolean checkAcls(OzoneObj obj, RequestContext.Builder contextBuilder,
boolean throwIfPermissionDenied) throws OMException {
- final RequestContext normalizedRequestContext =
maybeAttachSessionPolicyFromThreadLocal(context);
+ maybeAddToContextFromThreadLocal(contextBuilder);
+ final RequestContext context = contextBuilder.build();
if (!captureLatencyNs(perfMetrics::setCheckAccessLatencyNs,
- () -> accessAuthorizer.checkAccess(obj, normalizedRequestContext))) {
+ () -> accessAuthorizer.checkAccess(obj, context))) {
if (throwIfPermissionDenied) {
String volumeName = obj.getVolumeName() != null ?
"Volume:" + obj.getVolumeName() + " " : "";
@@ -647,7 +641,7 @@ public boolean checkAcls(OzoneObj obj, RequestContext
context,
"Key:" + obj.getKeyName() : "";
// For STS tokens, make clear that the user is using an assumed role,
otherwise the access denied
// message could be confusing
- String user =
normalizedRequestContext.getClientUgi().getShortUserName();
+ String user = context.getClientUgi().getShortUserName();
final STSTokenIdentifier stsTokenIdentifier =
OzoneManager.getStsTokenIdentifier();
if (stsTokenIdentifier != null) {
final StringBuilder builder = new StringBuilder(user)
@@ -660,11 +654,11 @@ public boolean checkAcls(OzoneObj obj, RequestContext
context,
}
log.warn("User {} doesn't have {} permission to access {} {}{}{}",
user,
- normalizedRequestContext.getAclRights(),
+ context.getAclRights(),
obj.getResourceType(), volumeName, bucketName, keyName);
throw new OMException(
"User " + user +
- " doesn't have " + normalizedRequestContext.getAclRights() +
+ " doesn't have " + context.getAclRights() +
" permission to access " + obj.getResourceType() + " " +
volumeName + bucketName + keyName, ResultCodes.PERMISSION_DENIED);
}
@@ -675,21 +669,22 @@ public boolean checkAcls(OzoneObj obj, RequestContext
context,
}
/**
- * Attaches session policy to RequestContext if an STSTokenIdentifier is
found in the Ozone Manager thread local
- * (meaning this is an STS request), and the STSTokenIdentifier has a
session policy. Otherwise, returns the
- * RequestContext as it was before.
- * @param context the original RequestContext
- * @return RequestContext as before or with sessionPolicy embedded
+ * Enriches the given {@link RequestContext.Builder} with per-request fields
from the Ozone Manager
+ * thread locals: the session policy from {@link STSTokenIdentifier} (set on
STS requests) and the
+ * S3 action from {@link S3Authentication} (set on S3 requests). Either or
both may be absent, in
+ * which case the corresponding field is left untouched on the builder.
+ * @param contextBuilder the builder to enrich in-place
*/
- private RequestContext
maybeAttachSessionPolicyFromThreadLocal(RequestContext context) {
+ public static void maybeAddToContextFromThreadLocal(RequestContext.Builder
contextBuilder) {
final STSTokenIdentifier stsTokenIdentifier =
OzoneManager.getStsTokenIdentifier();
- if (stsTokenIdentifier == null) {
- return context;
+ if (stsTokenIdentifier != null) {
+ contextBuilder.setSessionPolicy(stsTokenIdentifier.getSessionPolicy());
}
- return context.toBuilder()
- .setSessionPolicy(stsTokenIdentifier.getSessionPolicy())
- .build();
+ final S3Authentication s3Authentication = OzoneManager.getS3Auth();
+ if (s3Authentication != null && s3Authentication.hasS3Action() &&
!s3Authentication.getS3Action().isEmpty()) {
+ contextBuilder.setS3Action(s3Authentication.getS3Action());
+ }
}
static String getClientAddress() {
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
index 1986b34acb3..012f7c25dda 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
@@ -2891,16 +2891,15 @@ public boolean checkAcls(ResourceType resType,
StoreType storeType,
.setVolumeName(vol)
.setBucketName(bucket)
.setKeyName(key).build();
- RequestContext context = RequestContext.newBuilder()
+ RequestContext.Builder contextBuilder = RequestContext.newBuilder()
.setClientUgi(ugi)
.setIp(remoteAddress)
.setHost(hostName)
.setAclType(ACLIdentityType.USER)
.setAclRights(aclType)
- .setOwnerName(owner)
- .build();
+ .setOwnerName(owner);
- return omMetadataReader.checkAcls(obj, context, throwIfPermissionDenied);
+ return omMetadataReader.checkAcls(obj, contextBuilder,
throwIfPermissionDenied);
}
/**
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/OzoneManagerStateMachine.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/OzoneManagerStateMachine.java
index 34c5a485d61..2603cd78949 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/OzoneManagerStateMachine.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/ratis/OzoneManagerStateMachine.java
@@ -664,6 +664,7 @@ public void close() {
*/
@VisibleForTesting
OMResponse runCommand(OMRequest request, TermIndex termIndex) {
+ boolean isS3AuthThreadLocalSet = false;
boolean isStsThreadLocalSet = false;
try {
if (ozoneManager.isSecurityEnabled() && request.hasS3Authentication()) {
@@ -672,6 +673,10 @@ OMResponse runCommand(OMRequest request, TermIndex
termIndex) {
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(
@@ -706,6 +711,9 @@ OMResponse runCommand(OMRequest request, TermIndex
termIndex) {
String errorMessage = "Request " + request + " failed with exception";
ExitUtils.terminate(1, errorMessage, e, LOG);
} finally {
+ if (isS3AuthThreadLocalSet) {
+ OzoneManager.setS3Auth(null);
+ }
if (isStsThreadLocalSet) {
OzoneManager.setStsTokenIdentifier(null);
}
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java
index 81d6925442b..6b9c6698cf9 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/OMClientRequest.java
@@ -385,7 +385,7 @@ protected void checkACLsWithFSO(OzoneManager ozoneManager,
String volumeName,
OmMetadataReader omMetadataReader =
(OmMetadataReader) rcMetadataReader.get();
- omMetadataReader.checkAcls(obj, contextBuilder.build(), true);
+ omMetadataReader.checkAcls(obj, contextBuilder, true);
}
}
}
diff --git
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOMMetadataReader.java
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOMMetadataReader.java
index 8403d2203e0..e35a3952193 100644
---
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOMMetadataReader.java
+++
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOMMetadataReader.java
@@ -23,7 +23,6 @@
import static org.apache.hadoop.ozone.security.acl.OzoneObj.ResourceType.KEY;
import static
org.apache.hadoop.ozone.security.acl.OzoneObj.ResourceType.VOLUME;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -78,8 +77,9 @@ public class TestOMMetadataReader {
private static final long MAX_KEYS = 100L;
@AfterEach
- public void clearStsThreadLocal() {
+ public void clearOmThreadLocals() {
OzoneManager.setStsTokenIdentifier(null);
+ OzoneManager.setS3Auth(null);
}
@Test
@@ -120,10 +120,10 @@ public void
testCheckAclsAttachesSessionPolicyFromThreadLocal() throws Exception
final IAccessAuthorizer accessAuthorizer =
createMockIAccessAuthorizerReturningTrue();
final OmMetadataReader omMetadataReader =
createMetadataReader(accessAuthorizer);
- final RequestContext contextWithoutSessionPolicy =
createTestRequestContext();
+ final RequestContext.Builder contextWithoutSessionPolicyBuilder =
createTestRequestContextBuilder();
final OzoneObj obj = createTestOzoneObj();
- assertTrue(omMetadataReader.checkAcls(obj, contextWithoutSessionPolicy,
true));
+ assertTrue(omMetadataReader.checkAcls(obj,
contextWithoutSessionPolicyBuilder, true));
verifySessionPolicyPassedToAuthorizer(accessAuthorizer, obj,
sessionPolicy);
}
@@ -136,16 +136,67 @@ public void testNoSessionPolicyWhenThreadLocalIsNull()
throws Exception {
final IAccessAuthorizer accessAuthorizer =
createMockIAccessAuthorizerReturningTrue();
final OmMetadataReader omMetadataReader =
createMetadataReader(accessAuthorizer);
- final RequestContext contextWithoutSessionPolicy =
createTestRequestContext();
+ final RequestContext.Builder contextWithoutSessionPolicyBuilder =
createTestRequestContextBuilder();
final OzoneObj obj = createTestOzoneObj();
- assertTrue(omMetadataReader.checkAcls(obj, contextWithoutSessionPolicy,
true));
+ assertTrue(omMetadataReader.checkAcls(obj,
contextWithoutSessionPolicyBuilder, true));
verifySessionPolicyPassedToAuthorizer(accessAuthorizer, obj, null);
}
@Test
- public void testListStatusUsesListAclForStsS3Request() throws Exception {
+ public void testCheckAclsAttachesS3ActionFromThreadLocal() throws Exception {
+ OzoneManager.setS3Auth(S3Authentication.newBuilder()
+ .setAccessId(ACCESS_KEY_ID)
+ .setS3Action("GetObject")
+ .build());
+
+ final IAccessAuthorizer accessAuthorizer =
createMockIAccessAuthorizerReturningTrue();
+ final OmMetadataReader omMetadataReader =
createMetadataReader(accessAuthorizer);
+
+ final RequestContext.Builder contextWithoutS3ActionBuilder =
createTestRequestContextBuilder();
+ final OzoneObj obj = createTestOzoneObj();
+
+ assertTrue(omMetadataReader.checkAcls(obj, contextWithoutS3ActionBuilder,
true));
+
+ verifyS3ActionPassedToAuthorizer(accessAuthorizer, obj, "GetObject");
+ }
+
+ @Test
+ public void testCheckAclsLeavesS3ActionUnsetWhenS3AuthThreadLocalNull()
throws Exception {
+ final IAccessAuthorizer accessAuthorizer =
createMockIAccessAuthorizerReturningTrue();
+ final OmMetadataReader omMetadataReader =
createMetadataReader(accessAuthorizer);
+
+ final RequestContext.Builder contextWithoutS3ActionBuilder =
createTestRequestContextBuilder();
+ final OzoneObj obj = createTestOzoneObj();
+
+ assertTrue(omMetadataReader.checkAcls(obj, contextWithoutS3ActionBuilder,
true));
+
+ verifyS3ActionPassedToAuthorizer(accessAuthorizer, obj, null);
+ }
+
+ @Test
+ public void testCheckAclsAttachesSessionPolicyAndS3ActionFromThreadLocals()
throws Exception {
+ setupStsTokenIdentifier();
+
+ OzoneManager.setS3Auth(S3Authentication.newBuilder()
+ .setAccessId(ACCESS_KEY_ID)
+ .setS3Action("PutObject")
+ .build());
+
+ final IAccessAuthorizer accessAuthorizer =
createMockIAccessAuthorizerReturningTrue();
+ final OmMetadataReader omMetadataReader =
createMetadataReader(accessAuthorizer);
+
+ final RequestContext.Builder baseContextBuilder =
createTestRequestContextBuilder();
+ final OzoneObj obj = createTestOzoneObj();
+
+ assertTrue(omMetadataReader.checkAcls(obj, baseContextBuilder, true));
+
+ verifySessionPolicyAndS3ActionPassedToAuthorizer(accessAuthorizer, obj);
+ }
+
+ @Test
+ public void testListStatusUsesReadAclForStsS3Request() throws Exception {
setupStsS3Request();
final IAccessAuthorizer accessAuthorizer =
createMockIAccessAuthorizerReturningTrue();
@@ -161,10 +212,9 @@ public void testListStatusUsesListAclForStsS3Request()
throws Exception {
// For STS S3 requests, listStatus() performs these checks:
// 1. Volume READ (for volume access)
- // 2) Key LIST (for the specific prefix being listed) - we need LIST
permission for STS in order to tell whether the
- // file should be listed only or downloadable (downloadable would be
READ)
+ // 2) Key READ (for the specific prefix being listed)
assertContainsVolumeReadCheck(checks);
- assertContainsKeyListCheckWithName(checks, KEY_PREFIX);
+ assertContainsKeyReadCheckWithName(checks, KEY_PREFIX);
}
@Test
@@ -186,7 +236,6 @@ public void testListStatusUsesReadAclForNonStsRequest()
throws Exception {
assertContainsVolumeReadCheck(checks);
// We want to ensure the current behavior for non-STS requests remains the
same
assertContainsKeyReadCheckWithName(checks);
- assertDoesNotContainKeyListCheck(checks);
}
@Test
@@ -209,7 +258,7 @@ public void
testListStatusUsesListPrefixForAclWhenKeyNameEmptyAndListPrefixSet()
final List<AclCheck> checks = captureAclChecks(accessAuthorizer, 2);
assertContainsVolumeReadCheck(checks);
- assertContainsKeyListCheckWithName(checks, "userA/");
+ assertContainsKeyReadCheckWithName(checks, "userA/");
}
@Test
@@ -231,7 +280,7 @@ public void
testListStatusUsesWildcardForAclWhenKeyNameAndListPrefixEmpty() thro
final List<AclCheck> checks = captureAclChecks(accessAuthorizer, 2);
assertContainsVolumeReadCheck(checks);
- assertContainsKeyListCheckWithName(checks, "*");
+ assertContainsKeyReadCheckWithName(checks, "*");
}
@Test
@@ -254,7 +303,7 @@ public void
testListStatusUsesListPrefixForAclWhenKeyNameIsDescendantOfListPrefi
final List<AclCheck> checks = captureAclChecks(accessAuthorizer, 2);
assertContainsVolumeReadCheck(checks);
- assertContainsKeyListCheckWithName(checks, "user");
+ assertContainsKeyReadCheckWithName(checks, "user");
}
@Test
@@ -277,7 +326,7 @@ public void
testListStatusUsesListPrefixForAclWhenKeyNameIsAncestorOfListPrefix(
final List<AclCheck> checks = captureAclChecks(accessAuthorizer, 2);
assertContainsVolumeReadCheck(checks);
- assertContainsKeyListCheckWithName(checks, "user/foo");
+ assertContainsKeyReadCheckWithName(checks, "user/foo");
}
@Test
@@ -301,7 +350,7 @@ public void
testListStatusThrowsWhenStsKeyNameNotUnderListPrefix() throws Except
}
@Test
- public void testGetFileStatusUsesListAclForStsS3Request() throws Exception {
+ public void testGetFileStatusUsesReadAclForStsS3Request() throws Exception {
setupStsS3Request();
final IAccessAuthorizer accessAuthorizer =
createMockIAccessAuthorizerReturningTrue();
@@ -314,8 +363,7 @@ public void testGetFileStatusUsesListAclForStsS3Request()
throws Exception {
final List<AclCheck> checks = captureAclChecks(accessAuthorizer, 2);
assertContainsVolumeReadCheck(checks);
- assertContainsKeyListCheckWithName(checks, KEY_PREFIX);
- assertDoesNotContainKeyReadCheck(checks);
+ assertContainsKeyReadCheckWithName(checks, KEY_PREFIX);
}
@Test
@@ -333,7 +381,6 @@ public void
testGetFileStatusUsesReadAclForNonStsS3Request() throws Exception {
final List<AclCheck> checks = captureAclChecks(accessAuthorizer, 2);
assertContainsVolumeReadCheck(checks);
assertContainsKeyReadCheckWithName(checks);
- assertDoesNotContainKeyListCheck(checks);
}
@Test
@@ -350,7 +397,7 @@ public void testListKeysUsesPrefixCheckForStsS3Request()
throws Exception {
List<AclCheck> checks = captureAclChecks(accessAuthorizer, 4);
assertContainsBucketListCheck(checks);
- assertContainsKeyListCheckWithName(checks, "userA/");
+ assertContainsKeyReadCheckWithName(checks, "userA/");
// Reset to make case 2 assertions independent of case 1 captures.
reset(accessAuthorizer);
@@ -361,7 +408,7 @@ public void testListKeysUsesPrefixCheckForStsS3Request()
throws Exception {
checks = captureAclChecks(accessAuthorizer, 4);
assertContainsBucketListCheck(checks);
- assertContainsKeyListCheckWithName(checks, "*");
+ assertContainsKeyReadCheckWithName(checks, "*");
}
private OmMetadataReader createMetadataReader(IAccessAuthorizer
accessAuthorizer) throws IOException {
@@ -418,20 +465,18 @@ private IAccessAuthorizer
createMockIAccessAuthorizerReturningTrue() throws OMEx
}
/**
- * Creates a test RequestContext.
+ * Creates a test RequestContext.Builder.
*
- * @return the constructed RequestContext
+ * @return the constructed RequestContext.Builder
*/
- private RequestContext createTestRequestContext() {
- RequestContext.Builder builder = RequestContext.newBuilder()
+ private RequestContext.Builder createTestRequestContextBuilder() {
+ return RequestContext.newBuilder()
.setClientUgi(UserGroupInformation.createRemoteUser("testUser"))
.setIp(InetAddress.getLoopbackAddress())
.setHost("localhost")
.setAclType(IAccessAuthorizer.ACLIdentityType.USER)
.setAclRights(READ)
.setOwnerName("owner");
-
- return builder.build();
}
/**
@@ -505,6 +550,27 @@ private void
verifySessionPolicyPassedToAuthorizer(IAccessAuthorizer accessAutho
assertEquals(expectedSessionPolicy, captor.getValue().getSessionPolicy());
}
+ /**
+ * Verifies that the accessAuthorizer received a call to checkAccess with
the expected s3 action.
+ * @param accessAuthorizer the mock authorizer to verify
+ * @param expectedObj the expected OzoneObj
+ * @param expectedS3Action the expected s3 action (could be null)
+ */
+ private void verifyS3ActionPassedToAuthorizer(IAccessAuthorizer
accessAuthorizer, OzoneObj expectedObj,
+ String expectedS3Action) throws OMException {
+ final ArgumentCaptor<RequestContext> captor =
ArgumentCaptor.forClass(RequestContext.class);
+ verify(accessAuthorizer).checkAccess(eq(expectedObj), captor.capture());
+ assertEquals(expectedS3Action, captor.getValue().getS3Action());
+ }
+
+ private void
verifySessionPolicyAndS3ActionPassedToAuthorizer(IAccessAuthorizer
accessAuthorizer,
+ OzoneObj expectedObj) throws OMException {
+ final ArgumentCaptor<RequestContext> captor =
ArgumentCaptor.forClass(RequestContext.class);
+ verify(accessAuthorizer).checkAccess(eq(expectedObj), captor.capture());
+ assertEquals("session-policy-from-thread-local",
captor.getValue().getSessionPolicy());
+ assertEquals("PutObject", captor.getValue().getS3Action());
+ }
+
private List<AclCheck> captureAclChecks(IAccessAuthorizer accessAuthorizer,
int expectedCheckCount)
throws OMException {
final ArgumentCaptor<OzoneObj> objCaptor =
ArgumentCaptor.forClass(OzoneObj.class);
@@ -538,12 +604,12 @@ private void assertContainsBucketListCheck(List<AclCheck>
checks) {
"Expected a BUCKET LIST ACL check");
}
- private void assertContainsKeyListCheckWithName(List<AclCheck> checks,
String keyName) {
+ private void assertContainsKeyReadCheckWithName(List<AclCheck> checks,
String keyName) {
assertTrue(
checks.stream().anyMatch(
- check -> check.getObj().getResourceType() == KEY &&
check.getContext().getAclRights() == LIST &&
+ check -> check.getObj().getResourceType() == KEY &&
check.getContext().getAclRights() == READ &&
keyName.equals(check.getObj().getKeyName())),
- "Expected a KEY LIST ACL check for key '" + keyName + "'");
+ "Expected a KEY READ ACL check for key '" + keyName + "'");
}
private void assertContainsKeyReadCheckWithName(List<AclCheck> checks) {
@@ -554,20 +620,6 @@ private void
assertContainsKeyReadCheckWithName(List<AclCheck> checks) {
"Expected a KEY READ ACL check for key '" +
TestOMMetadataReader.KEY_PREFIX + "'");
}
- private void assertDoesNotContainKeyReadCheck(List<AclCheck> checks) {
- assertFalse(
- checks.stream().anyMatch(
- check -> check.getObj().getResourceType() == KEY &&
check.getContext().getAclRights() == READ),
- "Did not expect a KEY READ ACL check");
- }
-
- private void assertDoesNotContainKeyListCheck(List<AclCheck> checks) {
- assertFalse(
- checks.stream().anyMatch(
- check -> check.getObj().getResourceType() == KEY &&
check.getContext().getAclRights() == LIST),
- "Did not expect a KEY LIST ACL check");
- }
-
private static final class AclCheck {
private final OzoneObj obj;
private final RequestContext context;
diff --git
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
index 1e78813e971..66806be8e38 100644
---
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
+++
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
@@ -64,7 +64,6 @@
import org.apache.hadoop.ozone.s3.util.ContinueToken;
import org.apache.hadoop.ozone.s3.util.S3Consts.QueryParams;
import org.apache.hadoop.ozone.s3.util.S3StorageType;
-import org.apache.hadoop.util.Time;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -286,19 +285,19 @@ Response handlePutRequest(S3RequestContext context,
String bucketName, InputStre
@HEAD
public Response head(@PathParam(BUCKET) String bucketName)
throws OS3Exception, IOException {
- long startNanos = Time.monotonicNowNanos();
- S3GAction s3GAction = S3GAction.HEAD_BUCKET;
+ S3RequestContext context = new S3RequestContext(this,
S3GAction.HEAD_BUCKET);
+ long startNanos = context.getStartNanos();
try {
OzoneBucket bucket = getVolume().getBucket(bucketName);
S3Owner.verifyBucketOwnerCondition(getHeaders(), bucketName,
bucket.getOwner());
- auditReadSuccess(s3GAction);
+ auditReadSuccess(context.getAction());
getMetrics().updateHeadBucketSuccessStats(startNanos);
return Response.ok().build();
} catch (OMException e) {
- auditReadFailure(s3GAction, e);
+ auditReadFailure(context.getAction(), e);
throw newError(bucketName, e);
} catch (Exception e) {
- auditReadFailure(s3GAction, e);
+ auditReadFailure(context.getAction(), e);
throw e;
}
}
@@ -338,7 +337,7 @@ public MultiDeleteResponse multiDelete(
@QueryParam(QueryParams.DELETE) String delete,
MultiDeleteRequest request
) throws OS3Exception, IOException {
- S3GAction s3GAction = S3GAction.MULTI_DELETE;
+ S3RequestContext context = new S3RequestContext(this,
S3GAction.MULTI_DELETE);
OzoneBucket bucket = getVolume().getBucket(bucketName);
MultiDeleteResponse result = new MultiDeleteResponse();
@@ -349,7 +348,7 @@ public MultiDeleteResponse multiDelete(
for (DeleteObject keyToDelete : request.getObjects()) {
deleteKeys.add(keyToDelete.getKey());
}
- long startNanos = Time.monotonicNowNanos();
+ long startNanos = context.getStartNanos();
try {
S3Owner.verifyBucketOwnerCondition(getHeaders(), bucketName,
bucket.getOwner());
undeletedKeyResultMap = bucket.deleteKeys(deleteKeys, true);
@@ -377,7 +376,7 @@ public MultiDeleteResponse multiDelete(
}
}
- AuditMessage.Builder message = auditMessageFor(s3GAction);
+ AuditMessage.Builder message = auditMessageFor(context.getAction());
message.getParams().put("failedDeletes", deleteKeys.toString());
if (!result.getErrors().isEmpty()) {
diff --git
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
index 0d6fbe1657b..fd6ef9c3c0b 100644
---
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
+++
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/EndpointBase.java
@@ -85,6 +85,7 @@
import org.apache.hadoop.ozone.audit.AuditLogger.PerformanceStringBuilder;
import org.apache.hadoop.ozone.audit.AuditLoggerType;
import org.apache.hadoop.ozone.audit.AuditMessage;
+import org.apache.hadoop.ozone.audit.S3GAction;
import org.apache.hadoop.ozone.client.OzoneBucket;
import org.apache.hadoop.ozone.client.OzoneClient;
import org.apache.hadoop.ozone.client.OzoneClientUtils;
@@ -104,9 +105,11 @@
import org.apache.hadoop.ozone.s3.metrics.S3GatewayMetrics;
import org.apache.hadoop.ozone.s3.signature.SignatureInfo;
import org.apache.hadoop.ozone.s3.util.AuditUtils;
+import org.apache.hadoop.ozone.s3.util.S3GActionIamMapper;
import org.apache.hadoop.ozone.s3.util.S3Utils;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
+import org.apache.ratis.util.function.CheckedSupplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -225,6 +228,39 @@ protected void init() {
// hook method
}
+ /**
+ * Sets the IAM S3 action on thread-local {@link S3Auth} for fine-grained
STS authorization.
+ * Called when the handler resolves the {@link S3GAction}.
+ */
+ protected void applyS3Action(S3GAction action) {
+ if (s3Auth != null) {
+ s3Auth.setS3Action(S3GActionIamMapper.toS3ActionString(action));
+ }
+ }
+
+ /**
+ * Temporarily override the S3 action string set on {@link S3Auth} for
authorization.
+ * <p>
+ * This does not change S3G auditing (which is based on {@link S3GAction}).
+ * The action string is the IAM-style S3 action name without the {@code s3:}
prefix (for example
+ * {@code GetObject}, {@code PutObject}, {@code GetObjectTagging}).
+ * This is used for special case APIs like CopyObject that don't have a 1-1
s3 action mapping, but
+ * requires GetObject on the source file and PutObject on the destination
file.
+ */
+ protected <T, E extends Exception> T runWithS3ActionString(String s3Action,
CheckedSupplier<T, E> checkedSupplier)
+ throws E {
+ if (s3Auth == null) {
+ return checkedSupplier.get();
+ }
+ final String originalS3Action = s3Auth.getS3Action();
+ s3Auth.setS3Action(s3Action);
+ try {
+ return checkedSupplier.get();
+ } finally {
+ s3Auth.setS3Action(originalS3Action);
+ }
+ }
+
protected OzoneVolume getVolume() throws IOException {
return client.getObjectStore().getS3Volume();
}
diff --git
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java
index dd09bff5a41..bfa3c3d5c79 100644
---
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java
+++
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java
@@ -535,8 +535,8 @@ static void addEntityTagHeader(ResponseBuilder
responseBuilder, OzoneKey key) {
public Response head(
@PathParam(BUCKET) String bucketName,
@PathParam(PATH) String keyPath) throws IOException, OS3Exception {
- long startNanos = Time.monotonicNowNanos();
- S3GAction s3GAction = S3GAction.HEAD_KEY;
+ ObjectRequestContext context = new
ObjectRequestContext(S3GAction.HEAD_KEY, bucketName);
+ long startNanos = context.getStartNanos();
OzoneKey key;
try {
@@ -551,12 +551,12 @@ public Response head(
getHeaders(), keyPath, key,
S3ConditionalRequest.PreconditionContext.READ);
if (conditionalResponse != null) {
getMetrics().updateHeadKeySuccessStats(startNanos);
- auditReadSuccess(s3GAction);
+ auditReadSuccess(context.getAction());
return conditionalResponse;
}
// TODO: return the specified range bytes of this object.
} catch (OMException ex) {
- auditReadFailure(s3GAction, ex);
+ auditReadFailure(context.getAction(), ex);
getMetrics().updateHeadKeyFailureStats(startNanos);
if (ex.getResult() == ResultCodes.KEY_NOT_FOUND) {
// Just return 404 with no content
@@ -571,7 +571,7 @@ public Response head(
throw newError(bucketName, keyPath, ex);
}
} catch (Exception ex) {
- auditReadFailure(s3GAction, ex);
+ auditReadFailure(context.getAction(), ex);
throw ex;
}
@@ -588,7 +588,7 @@ public Response head(
addLastModifiedDate(response, key);
addCustomMetadataHeaders(response, key);
getMetrics().updateHeadKeySuccessStats(startNanos);
- auditReadSuccess(s3GAction);
+ auditReadSuccess(context.getAction());
return response.build();
}
@@ -674,8 +674,8 @@ public Response initializeMultipartUpload(
@PathParam(BUCKET) String bucket,
@PathParam(PATH) String key
) throws IOException, OS3Exception {
- long startNanos = Time.monotonicNowNanos();
- S3GAction s3GAction = S3GAction.INIT_MULTIPART_UPLOAD;
+ ObjectRequestContext context = new
ObjectRequestContext(S3GAction.INIT_MULTIPART_UPLOAD, bucket);
+ long startNanos = context.getStartNanos();
try {
OzoneBucket ozoneBucket = getVolume().getBucket(bucket);
@@ -698,16 +698,16 @@ public Response initializeMultipartUpload(
multipartUploadInitiateResponse.setKey(key);
multipartUploadInitiateResponse.setUploadID(multipartInfo.getUploadID());
- auditWriteSuccess(s3GAction);
+ auditWriteSuccess(context.getAction());
getMetrics().updateInitMultipartUploadSuccessStats(startNanos);
return Response.status(Status.OK).entity(
multipartUploadInitiateResponse).build();
} catch (OMException ex) {
- auditWriteFailure(s3GAction, ex);
+ auditWriteFailure(context.getAction(), ex);
getMetrics().updateInitMultipartUploadFailureStats(startNanos);
throw newError(bucket, key, ex);
} catch (Exception ex) {
- auditWriteFailure(s3GAction, ex);
+ auditWriteFailure(context.getAction(), ex);
getMetrics().updateInitMultipartUploadFailureStats(startNanos);
throw ex;
}
@@ -723,9 +723,9 @@ public Response completeMultipartUpload(
@PathParam(PATH) String key,
CompleteMultipartUploadRequest multipartUploadRequest
) throws IOException, OS3Exception {
+ ObjectRequestContext context = new
ObjectRequestContext(S3GAction.COMPLETE_MULTIPART_UPLOAD, bucket);
final String uploadID = queryParams().get(QueryParams.UPLOAD_ID, "");
- long startNanos = Time.monotonicNowNanos();
- S3GAction s3GAction = S3GAction.COMPLETE_MULTIPART_UPLOAD;
+ long startNanos = context.getStartNanos();
// Using LinkedHashMap to preserve ordering of parts list.
Map<Integer, String> partsMap = new LinkedHashMap<>();
List<CompleteMultipartUploadRequest.Part> partList =
@@ -765,12 +765,12 @@ public Response completeMultipartUpload(
wrapInQuotes(omMultipartUploadCompleteInfo.getHash()));
// Location also setting as bucket name.
completeMultipartUploadResponse.setLocation(bucket);
- auditWriteSuccess(s3GAction);
+ auditWriteSuccess(context.getAction());
getMetrics().updateCompleteMultipartUploadSuccessStats(startNanos);
return Response.status(Status.OK).entity(completeMultipartUploadResponse)
.build();
} catch (OMException ex) {
- auditWriteFailure(s3GAction, ex);
+ auditWriteFailure(context.getAction(), ex);
getMetrics().updateCompleteMultipartUploadFailureStats(startNanos);
if (ex.getResult() == ResultCodes.NO_SUCH_MULTIPART_UPLOAD_ERROR) {
throw newError(NO_SUCH_UPLOAD, uploadID, ex);
@@ -798,7 +798,7 @@ public Response completeMultipartUpload(
}
throw newError(bucket, key, ex);
} catch (Exception ex) {
- auditWriteFailure(s3GAction, ex);
+ auditWriteFailure(context.getAction(), ex);
getMetrics().updateCompleteMultipartUploadFailureStats(startNanos);
throw ex;
}
@@ -839,8 +839,8 @@ private Response createMultipartKey(OzoneVolume volume,
OzoneBucket ozoneBucket,
uploadID, getChunkSize(), multiDigestInputStream, perf,
getHeaders());
}
// OmMultipartCommitUploadPartInfo can only be gotten after the
- // OzoneOutputStream is closed, so we need to save the OzoneOutputStream
- final OzoneOutputStream outputStream;
+ // OzoneOutputStream is closed, so we need to get and save the commit
info.
+ final OmMultipartCommitUploadPartInfo omMultipartCommitUploadPartInfo;
long metadataLatencyNs;
if (copyHeader != null) {
Pair<String, String> result = parseSourceHeader(copyHeader);
@@ -852,8 +852,8 @@ private Response createMultipartKey(OzoneVolume volume,
OzoneBucket ozoneBucket,
ozoneBucket.getOwner());
}
- OzoneKeyDetails sourceKeyDetails = getClientProtocol().getKeyDetails(
- volume.getName(), sourceBucket, sourceKey);
+ final OzoneKeyDetails sourceKeyDetails = runWithS3ActionString(
+ "GetObject", () ->
getClientProtocol().getKeyDetails(volume.getName(), sourceBucket, sourceKey));
String range =
getHeaders().getHeaderString(COPY_SOURCE_HEADER_RANGE);
RangeHeader rangeHeader = null;
@@ -879,7 +879,8 @@ private Response createMultipartKey(OzoneVolume volume,
OzoneBucket ozoneBucket,
}
try (OzoneInputStream sourceObject = sourceKeyDetails.getContent()) {
- long copyLength;
+ final long[] copyLengthHolder = new long[1];
+ final long[] metadataLatencyHolder = new long[1];
if (range != null) {
final long skipped =
sourceObject.skip(rangeHeader.getStartOffset());
@@ -889,52 +890,60 @@ private Response createMultipartKey(OzoneVolume volume,
OzoneBucket ozoneBucket,
+ rangeHeader.getStartOffset() + " actual: " + skipped);
}
}
- try (OzoneOutputStream ozoneOutputStream = getClientProtocol()
- .createMultipartKey(volume.getName(), bucketName, key, length,
- partNumber, uploadID)) {
- metadataLatencyNs =
- getMetrics().updateCopyKeyMetadataStats(startNanos);
- copyLength = IOUtils.copyLarge(sourceObject, ozoneOutputStream, 0,
length,
- new byte[getIOBufferSize(length)]);
- ozoneOutputStream.getMetadata()
- .putAll(sourceKeyDetails.getMetadata());
- String raw = ozoneOutputStream.getMetadata().get(OzoneConsts.ETAG);
- if (raw != null) {
- ozoneOutputStream.getMetadata().put(OzoneConsts.ETAG,
stripQuotes(raw));
+ final long finalLength = length;
+ final long bytesToCopy = length;
+ omMultipartCommitUploadPartInfo = runWithS3ActionString("PutObject",
() -> {
+ final OzoneOutputStream ozoneOutputStream =
getClientProtocol().createMultipartKey(
+ volume.getName(), bucketName, key, finalLength, partNumber,
uploadID);
+ try (OzoneOutputStream ignored = ozoneOutputStream) {
+ metadataLatencyHolder[0] =
getMetrics().updateCopyKeyMetadataStats(startNanos);
+ copyLengthHolder[0] = IOUtils.copyLarge(
+ sourceObject, ozoneOutputStream, 0, bytesToCopy, new
byte[getIOBufferSize(bytesToCopy)]);
+ ozoneOutputStream.getMetadata()
+ .putAll(sourceKeyDetails.getMetadata());
+ final String raw =
ozoneOutputStream.getMetadata().get(OzoneConsts.ETAG);
+ if (raw != null) {
+ ozoneOutputStream.getMetadata().put(OzoneConsts.ETAG,
stripQuotes(raw));
+ }
}
- outputStream = ozoneOutputStream;
- }
- getMetrics().incCopyObjectSuccessLength(copyLength);
- perf.appendSizeBytes(copyLength);
+ return ozoneOutputStream.getCommitUploadPartInfo();
+ });
+ metadataLatencyNs = metadataLatencyHolder[0];
+ getMetrics().incCopyObjectSuccessLength(copyLengthHolder[0]);
+ perf.appendSizeBytes(copyLengthHolder[0]);
}
} else {
- long putLength;
- try (OzoneOutputStream ozoneOutputStream = getClientProtocol()
+ final long putLength;
+ // We don't need runWithS3ActionString("PutObject"...) here because
the action in this else branch is
+ // S3GAction.CREATE_MULTIPART_KEY and this has a mapping in
S3GActionIamMapper to "PutObject", so it's covered.
+ // In the if branch of the code, the request action is set to
S3GAction.CREATE_MULTIPART_KEY_BY_COPY which is
+ // mapped to null in S3GActionIamMapper (by design, since it needs
"GetObject" on the source and "PutObject"
+ // on the destination).
+ final OzoneOutputStream ozoneOutputStream = getClientProtocol()
.createMultipartKey(volume.getName(), bucketName, key, length,
- partNumber, uploadID)) {
+ partNumber, uploadID);
+ try (OzoneOutputStream ignored = ozoneOutputStream) {
metadataLatencyNs =
getMetrics().updatePutKeyMetadataStats(startNanos);
putLength = IOUtils.copyLarge(multiDigestInputStream,
ozoneOutputStream, 0, length,
new byte[getIOBufferSize(length)]);
- byte[] digest =
multiDigestInputStream.getMessageDigest(OzoneConsts.MD5_HASH).digest();
- String md5Hash =
DatatypeConverter.printHexBinary(digest).toLowerCase();
- String clientContentMD5 =
getHeaders().getHeaderString(S3Consts.CHECKSUM_HEADER);
+ final byte[] digest =
multiDigestInputStream.getMessageDigest(OzoneConsts.MD5_HASH).digest();
+ final String md5Hash =
DatatypeConverter.printHexBinary(digest).toLowerCase();
+ final String clientContentMD5 =
getHeaders().getHeaderString(S3Consts.CHECKSUM_HEADER);
if (clientContentMD5 != null) {
- CheckedRunnable<IOException> checkContentMD5Hook = () -> {
+ final CheckedRunnable<IOException> checkContentMD5Hook = () -> {
S3Utils.validateContentMD5(clientContentMD5, md5Hash, key);
};
ozoneOutputStream.getKeyOutputStream().setPreCommits(Collections.singletonList(checkContentMD5Hook));
}
ozoneOutputStream.getMetadata().put(OzoneConsts.ETAG, md5Hash);
- outputStream = ozoneOutputStream;
}
+ omMultipartCommitUploadPartInfo =
ozoneOutputStream.getCommitUploadPartInfo();
getMetrics().incPutKeySuccessLength(putLength);
perf.appendSizeBytes(putLength);
}
perf.appendMetaLatencyNanos(metadataLatencyNs);
- OmMultipartCommitUploadPartInfo omMultipartCommitUploadPartInfo =
- outputStream.getCommitUploadPartInfo();
String eTag = omMultipartCommitUploadPartInfo.getETag();
// If the OmMultipartCommitUploadPartInfo does not contain eTag,
// fall back to MPU part name for compatibility in case the (old) OM
@@ -1003,7 +1012,7 @@ srcKeyLen > getDatastreamMinLength()) {
getMetrics().updateCopyKeyMetadataStats(startNanos);
perf.appendMetaLatencyNanos(metadataLatencyNs);
copyLength = IOUtils.copyLarge(src, dest, 0, srcKeyLen, new
byte[getIOBufferSize(srcKeyLen)]);
- String md5Hash =
DatatypeConverter.printHexBinary(src.getMessageDigest().digest()).toLowerCase();
+ final String md5Hash =
DatatypeConverter.printHexBinary(src.getMessageDigest().digest()).toLowerCase();
dest.getMetadata().put(OzoneConsts.ETAG, md5Hash);
}
}
@@ -1024,7 +1033,7 @@ private CopyObjectResponse copyObject(OzoneVolume volume,
String sourceBucket = result.getLeft();
String sourceKey = result.getRight();
- DigestInputStream sourceDigestInputStream = null;
+ final MessageDigest md5Digest = getMD5DigestInstance();
if (S3Owner.hasBucketOwnershipVerificationConditions(getHeaders())) {
String sourceBucketOwner = volume.getBucket(sourceBucket).getOwner();
@@ -1032,8 +1041,8 @@ private CopyObjectResponse copyObject(OzoneVolume volume,
S3Owner.verifyBucketOwnerConditionOnCopyOperation(getHeaders(),
sourceBucket, sourceBucketOwner, null, null);
}
try {
- OzoneKeyDetails sourceKeyDetails = getClientProtocol().getKeyDetails(
- volume.getName(), sourceBucket, sourceKey);
+ final OzoneKeyDetails sourceKeyDetails = runWithS3ActionString(
+ "GetObject", () ->
getClientProtocol().getKeyDetails(volume.getName(), sourceBucket, sourceKey));
// Checking whether we trying to copying to it self.
if (sourceBucket.equals(destBucket) && sourceKey
.equals(destkey)) {
@@ -1103,22 +1112,25 @@ private CopyObjectResponse copyObject(OzoneVolume
volume,
throw ex;
}
- try (OzoneInputStream src = getClientProtocol().getKey(volume.getName(),
- sourceBucket, sourceKey)) {
+ try (OzoneInputStream src = runWithS3ActionString(
+ "GetObject", () -> getClientProtocol().getKey(volume.getName(),
sourceBucket, sourceKey));
+ DigestInputStream sourceDigestInputStream = new
DigestInputStream(src, md5Digest)) {
getMetrics().updateCopyKeyMetadataStats(startNanos);
- sourceDigestInputStream = new DigestInputStream(src,
getMD5DigestInstance());
- copy(volume, sourceDigestInputStream, sourceKeyLen, destkey,
destBucket, replicationConfig,
- customMetadata, perf, startNanos, tags, writeConditions);
- }
+ runWithS3ActionString("PutObject", () -> {
+ copy(volume, sourceDigestInputStream, sourceKeyLen, destkey,
destBucket,
+ replicationConfig, customMetadata, perf, startNanos, tags,
writeConditions);
+ return null;
+ });
- final OzoneKeyDetails destKeyDetails = getClientProtocol().getKeyDetails(
- volume.getName(), destBucket, destkey);
+ final OzoneKeyDetails destKeyDetails =
getClientProtocol().getKeyDetails(
+ volume.getName(), destBucket, destkey);
- getMetrics().updateCopyObjectSuccessStats(startNanos);
- CopyObjectResponse copyObjectResponse = new CopyObjectResponse();
-
copyObjectResponse.setETag(wrapInQuotes(destKeyDetails.getMetadata().get(OzoneConsts.ETAG)));
- copyObjectResponse.setLastModified(destKeyDetails.getModificationTime());
- return copyObjectResponse;
+ getMetrics().updateCopyObjectSuccessStats(startNanos);
+ CopyObjectResponse copyObjectResponse = new CopyObjectResponse();
+
copyObjectResponse.setETag(wrapInQuotes(destKeyDetails.getMetadata().get(OzoneConsts.ETAG)));
+
copyObjectResponse.setLastModified(destKeyDetails.getModificationTime());
+ return copyObjectResponse;
+ }
} catch (OMException ex) {
if (ex.getResult() == ResultCodes.KEY_NOT_FOUND) {
if (getHeaders().getHeaderString(S3Consts.IF_MATCH_HEADER) != null) {
@@ -1137,9 +1149,7 @@ private CopyObjectResponse copyObject(OzoneVolume volume,
} finally {
// Reset the thread-local message digest instance in case of exception
// and MessageDigest#digest is never called
- if (sourceDigestInputStream != null) {
- sourceDigestInputStream.getMessageDigest().reset();
- }
+ md5Digest.reset();
}
}
diff --git
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/RootEndpoint.java
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/RootEndpoint.java
index 9e638a112a7..8fdb80d9a48 100644
---
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/RootEndpoint.java
+++
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/RootEndpoint.java
@@ -26,7 +26,6 @@
import org.apache.hadoop.ozone.client.OzoneBucket;
import org.apache.hadoop.ozone.s3.commontypes.BucketMetadata;
import org.apache.hadoop.ozone.s3.exception.OS3Exception;
-import org.apache.hadoop.util.Time;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -48,7 +47,8 @@ public class RootEndpoint extends EndpointBase {
@GET
public Response get()
throws OS3Exception, IOException {
- long startNanos = Time.monotonicNowNanos();
+ S3RequestContext context = new S3RequestContext(this,
S3GAction.LIST_S3_BUCKETS);
+ long startNanos = context.getStartNanos();
boolean auditSuccess = true;
try {
ListBucketResponse response = new ListBucketResponse();
@@ -73,11 +73,11 @@ public Response get()
return Response.ok(response).build();
} catch (Exception ex) {
auditSuccess = false;
- auditReadFailure(S3GAction.LIST_S3_BUCKETS, ex);
+ auditReadFailure(context.getAction(), ex);
throw ex;
} finally {
if (auditSuccess) {
- auditReadSuccess(S3GAction.LIST_S3_BUCKETS);
+ auditReadSuccess(context.getAction());
}
}
}
diff --git
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3RequestContext.java
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3RequestContext.java
index 4130feaf6fd..ebcb773cef4 100644
---
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3RequestContext.java
+++
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3RequestContext.java
@@ -36,6 +36,7 @@ class S3RequestContext {
this.startNanos = Time.monotonicNowNanos();
this.perf = new PerformanceStringBuilder();
this.action = action;
+ endpoint.applyS3Action(action);
}
long getStartNanos() {
@@ -59,6 +60,7 @@ S3GAction getAction() {
void setAction(S3GAction action) {
this.action = action;
+ endpoint.applyS3Action(action);
}
/**
diff --git
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3GActionIamMapper.java
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3GActionIamMapper.java
new file mode 100644
index 00000000000..9953ebe2020
--- /dev/null
+++
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3GActionIamMapper.java
@@ -0,0 +1,92 @@
+/*
+ * 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.s3.util;
+
+import jakarta.annotation.Nullable;
+import org.apache.hadoop.ozone.audit.S3GAction;
+
+/**
+ * Maps S3 Gateway operations to AWS IAM S3 action names. Values align with
+ * {@code org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver}
so STS session
+ * policies and Ranger policy conditions use the same vocabulary.
+ */
+public final class S3GActionIamMapper {
+
+ private S3GActionIamMapper() {
+ }
+
+ /**
+ * @return S3 action string, or null if not applicable to IAM S3
+ */
+ public static @Nullable String toS3ActionString(@Nullable S3GAction action) {
+ if (action == null) {
+ return null;
+ }
+ switch (action) {
+ case GET_BUCKET:
+ case HEAD_BUCKET:
+ return "ListBucket";
+ case CREATE_BUCKET:
+ return "CreateBucket";
+ case DELETE_BUCKET:
+ return "DeleteBucket";
+ case GET_ACL:
+ return "GetBucketAcl";
+ case PUT_ACL:
+ return "PutBucketAcl";
+ case LIST_MULTIPART_UPLOAD:
+ return "ListBucketMultipartUploads";
+ case MULTI_DELETE:
+ case DELETE_KEY:
+ return "DeleteObject";
+ case LIST_S3_BUCKETS:
+ return "ListAllMyBuckets";
+ case CREATE_MULTIPART_KEY:
+ case CREATE_KEY:
+ case INIT_MULTIPART_UPLOAD:
+ case COMPLETE_MULTIPART_UPLOAD:
+ case CREATE_DIRECTORY:
+ return "PutObject";
+ case LIST_PARTS:
+ return "ListMultipartUploadParts";
+ case GET_KEY:
+ case HEAD_KEY:
+ return "GetObject";
+ case ABORT_MULTIPART_UPLOAD:
+ return "AbortMultipartUpload";
+ case GET_OBJECT_TAGGING:
+ return "GetObjectTagging";
+ case PUT_OBJECT_TAGGING:
+ return "PutObjectTagging";
+ case DELETE_OBJECT_TAGGING:
+ return "DeleteObjectTagging";
+ case PUT_OBJECT_ACL:
+ return "PutObjectAcl";
+ case COPY_OBJECT:
+ case CREATE_MULTIPART_KEY_BY_COPY:
+ // CopyObject / UploadPartCopy require distinct source (GetObject) and
destination (PutObject)
+ // authorization. The endpoint code explicitly sets the IAM action
string for each phase.
+ return null;
+ case GENERATE_SECRET:
+ case REVOKE_SECRET:
+ case ASSUME_ROLE:
+ default:
+ return null;
+ }
+ }
+}
diff --git
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestCopyActionsAudit.java
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestCopyActionsAudit.java
new file mode 100644
index 00000000000..380a7780e9e
--- /dev/null
+++
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestCopyActionsAudit.java
@@ -0,0 +1,133 @@
+/*
+ * 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.s3.endpoint;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.hadoop.ozone.s3.util.S3Consts.COPY_SOURCE_HEADER;
+import static org.apache.hadoop.ozone.s3.util.S3Consts.STORAGE_CLASS_HEADER;
+import static org.apache.hadoop.ozone.s3.util.S3Consts.X_AMZ_CONTENT_SHA256;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayInputStream;
+import java.io.OutputStream;
+import java.util.HashMap;
+import javax.ws.rs.core.HttpHeaders;
+import org.apache.hadoop.hdds.client.ReplicationConfig;
+import org.apache.hadoop.hdds.client.ReplicationFactor;
+import org.apache.hadoop.hdds.client.ReplicationType;
+import org.apache.hadoop.ozone.OzoneConsts;
+import org.apache.hadoop.ozone.audit.AuditLogger.PerformanceStringBuilder;
+import org.apache.hadoop.ozone.audit.S3GAction;
+import org.apache.hadoop.ozone.client.OzoneBucket;
+import org.apache.hadoop.ozone.client.OzoneClient;
+import org.apache.hadoop.ozone.client.OzoneClientStub;
+import org.apache.hadoop.ozone.s3.endpoint.ObjectEndpoint.ObjectRequestContext;
+import org.apache.hadoop.ozone.s3.util.S3Consts;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Verifies audit logging action for copy operations even if S3 action
authorization strings are overridden internally.
+ * For example, S3G.COPY_OBJECT must use S3G.COPY_OBJECT as the audit action,
even though internally the S3 actions
+ * checked are GetObject and PutObject.
+ */
+public class TestCopyActionsAudit {
+
+ @Test
+ public void testCopyObjectAuditActionRemainsCopyObject() throws Exception {
+ final String bucketName = OzoneConsts.S3_BUCKET;
+ final String srcKey = "src.txt";
+ final String destKey = "dest.txt";
+
+ final OzoneClient client = new OzoneClientStub();
+ client.getObjectStore().createS3Bucket(bucketName);
+ final OzoneBucket bucket = client.getObjectStore().getS3Bucket(bucketName);
+
+ try (OutputStream out = bucket.createKey(
+ srcKey, 3, ReplicationConfig.fromTypeAndFactor(ReplicationType.RATIS,
ReplicationFactor.ONE),
+ new HashMap<>())) {
+ out.write("src".getBytes(UTF_8));
+ }
+
+ final HttpHeaders headers = mock(HttpHeaders.class);
+ when(headers.getHeaderString(STORAGE_CLASS_HEADER)).thenReturn("STANDARD");
+
when(headers.getHeaderString(X_AMZ_CONTENT_SHA256)).thenReturn("mockSignature");
+ when(headers.getHeaderString(COPY_SOURCE_HEADER)).thenReturn(bucketName +
"/" + srcKey);
+ when(headers.getHeaderString(HttpHeaders.CONTENT_LENGTH)).thenReturn("0");
+
+ final ObjectEndpoint endpoint = newEndpoint(client, headers);
+ final AuditingObjectOperationHandler auditing = spy(new
AuditingObjectOperationHandler(endpoint));
+
+ final ObjectRequestContext requestContext = endpoint.new
ObjectRequestContext(S3GAction.CREATE_KEY, bucketName);
+
+ auditing.handlePutRequest(requestContext, destKey, new
ByteArrayInputStream(new byte[0]));
+
+ verify(auditing).auditWriteSuccess(eq(S3GAction.COPY_OBJECT),
any(PerformanceStringBuilder.class));
+ }
+
+ @Test
+ public void testUploadPartCopyAuditActionRemainsCreateMultipartKeyByCopy()
throws Exception {
+ final String bucketName = OzoneConsts.S3_BUCKET;
+ final String srcKey = "src-part.txt";
+ final String destKey = "dest-mpu.txt";
+
+ final OzoneClient client = new OzoneClientStub();
+ client.getObjectStore().createS3Bucket(bucketName);
+ final OzoneBucket bucket = client.getObjectStore().getS3Bucket(bucketName);
+
+ try (OutputStream out = bucket.createKey(
+ srcKey, 4, ReplicationConfig.fromTypeAndFactor(ReplicationType.RATIS,
ReplicationFactor.ONE),
+ new HashMap<>())) {
+ out.write("part".getBytes(UTF_8));
+ }
+
+ final HttpHeaders headers = mock(HttpHeaders.class);
+ when(headers.getHeaderString(STORAGE_CLASS_HEADER)).thenReturn("STANDARD");
+
when(headers.getHeaderString(X_AMZ_CONTENT_SHA256)).thenReturn("mockSignature");
+ when(headers.getHeaderString(COPY_SOURCE_HEADER)).thenReturn(bucketName +
"/" + srcKey);
+ when(headers.getHeaderString(HttpHeaders.CONTENT_LENGTH)).thenReturn("0");
+
+ final ObjectEndpoint endpoint = newEndpoint(client, headers);
+
+ final String uploadId =
EndpointTestUtils.initiateMultipartUpload(endpoint, bucketName, destKey);
+ assertNotNull(uploadId);
+
+ endpoint.queryParamsForTest().set(S3Consts.QueryParams.UPLOAD_ID,
uploadId);
+ endpoint.queryParamsForTest().setInt(S3Consts.QueryParams.PART_NUMBER, 1);
+
+ final AuditingObjectOperationHandler auditing = spy(new
AuditingObjectOperationHandler(endpoint));
+ final ObjectRequestContext requestContext = endpoint.new
ObjectRequestContext(S3GAction.CREATE_KEY, bucketName);
+
+ auditing.handlePutRequest(requestContext, destKey, new
ByteArrayInputStream(new byte[0]));
+
+
verify(auditing).auditWriteSuccess(eq(S3GAction.CREATE_MULTIPART_KEY_BY_COPY),
any(PerformanceStringBuilder.class));
+ }
+
+ private static ObjectEndpoint newEndpoint(OzoneClient client, HttpHeaders
headers) {
+ return EndpointBuilder.newObjectEndpointBuilder()
+ .setClient(client)
+ .setHeaders(headers)
+ .build();
+ }
+}
+
diff --git
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestS3GActionIamMapper.java
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestS3GActionIamMapper.java
new file mode 100644
index 00000000000..c7ae9e4e924
--- /dev/null
+++
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/TestS3GActionIamMapper.java
@@ -0,0 +1,68 @@
+/*
+ * 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.s3.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.apache.hadoop.ozone.audit.S3GAction;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for {@link S3GActionIamMapper}. */
+public class TestS3GActionIamMapper {
+
+ @Test
+ public void mapsCoreObjectActions() {
+ assertEquals("ListBucket",
S3GActionIamMapper.toS3ActionString(S3GAction.GET_BUCKET));
+ assertEquals("ListBucket",
S3GActionIamMapper.toS3ActionString(S3GAction.HEAD_BUCKET));
+ assertEquals("CreateBucket",
S3GActionIamMapper.toS3ActionString(S3GAction.CREATE_BUCKET));
+ assertEquals("DeleteBucket",
S3GActionIamMapper.toS3ActionString(S3GAction.DELETE_BUCKET));
+ assertEquals("GetBucketAcl",
S3GActionIamMapper.toS3ActionString(S3GAction.GET_ACL));
+ assertEquals("PutBucketAcl",
S3GActionIamMapper.toS3ActionString(S3GAction.PUT_ACL));
+ assertEquals("ListBucketMultipartUploads",
S3GActionIamMapper.toS3ActionString(S3GAction.LIST_MULTIPART_UPLOAD));
+ assertEquals("DeleteObject",
S3GActionIamMapper.toS3ActionString(S3GAction.MULTI_DELETE));
+ assertEquals("DeleteObject",
S3GActionIamMapper.toS3ActionString(S3GAction.DELETE_KEY));
+ assertEquals("ListAllMyBuckets",
S3GActionIamMapper.toS3ActionString(S3GAction.LIST_S3_BUCKETS));
+ assertEquals("PutObject",
S3GActionIamMapper.toS3ActionString(S3GAction.CREATE_MULTIPART_KEY));
+ assertEquals("PutObject",
S3GActionIamMapper.toS3ActionString(S3GAction.CREATE_KEY));
+ assertEquals("PutObject",
S3GActionIamMapper.toS3ActionString(S3GAction.INIT_MULTIPART_UPLOAD));
+ assertEquals("PutObject",
S3GActionIamMapper.toS3ActionString(S3GAction.COMPLETE_MULTIPART_UPLOAD));
+ assertEquals("PutObject",
S3GActionIamMapper.toS3ActionString(S3GAction.CREATE_DIRECTORY));
+ assertEquals("ListMultipartUploadParts",
S3GActionIamMapper.toS3ActionString(S3GAction.LIST_PARTS));
+ assertEquals("GetObject",
S3GActionIamMapper.toS3ActionString(S3GAction.GET_KEY));
+ assertEquals("GetObject",
S3GActionIamMapper.toS3ActionString(S3GAction.HEAD_KEY));
+ assertEquals("AbortMultipartUpload",
S3GActionIamMapper.toS3ActionString(S3GAction.ABORT_MULTIPART_UPLOAD));
+ assertEquals("GetObjectTagging",
S3GActionIamMapper.toS3ActionString(S3GAction.GET_OBJECT_TAGGING));
+ assertEquals("PutObjectTagging",
S3GActionIamMapper.toS3ActionString(S3GAction.PUT_OBJECT_TAGGING));
+ assertEquals("DeleteObjectTagging",
S3GActionIamMapper.toS3ActionString(S3GAction.DELETE_OBJECT_TAGGING));
+ assertEquals("PutObjectAcl",
S3GActionIamMapper.toS3ActionString(S3GAction.PUT_OBJECT_ACL));
+ }
+
+ @Test
+ public void copyActionsReturnNull() {
+ assertNull(S3GActionIamMapper.toS3ActionString(S3GAction.COPY_OBJECT));
+
assertNull(S3GActionIamMapper.toS3ActionString(S3GAction.CREATE_MULTIPART_KEY_BY_COPY));
+ }
+
+ @Test
+ public void nonIamActionsReturnNull() {
+ assertNull(S3GActionIamMapper.toS3ActionString(S3GAction.ASSUME_ROLE));
+ assertNull(S3GActionIamMapper.toS3ActionString(S3GAction.GENERATE_SECRET));
+ assertNull(S3GActionIamMapper.toS3ActionString(S3GAction.REVOKE_SECRET));
+ }
+}
diff --git
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/package-info.java
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/package-info.java
new file mode 100644
index 00000000000..4b7b37a574a
--- /dev/null
+++
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/util/package-info.java
@@ -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.
+ */
+
+/**
+ * Unit tests for s3 utilities.
+ */
+package org.apache.hadoop.ozone.s3.util;
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]