This is an automated email from the ASF dual-hosted git repository.
smengcl pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git
The following commit(s) were added to refs/heads/master by this push:
new a6332fb5987 HDDS-16724. Add support for missing S3 apis to STS (#11413)
a6332fb5987 is described below
commit a6332fb59879b80615b9233e953d6813d03ae7a8
Author: fmorg-git <[email protected]>
AuthorDate: Thu Oct 8 01:12:24 2026 -0700
HDDS-16724. Add support for missing S3 apis to STS (#11413)
Co-authored-by: Fabian Morgan <[email protected]>
---
.../security/acl/iam/IamSessionPolicyResolver.java | 10 ++
.../acl/iam/TestIamSessionPolicyResolver.java | 140 +++++++++++----
.../main/smoketest/security/ozone-secure-sts.robot | 200 ++++++++++++++++++++-
.../apache/hadoop/ozone/om/OmMetadataReader.java | 34 +++-
.../hadoop/ozone/om/TestOMMetadataReader.java | 73 ++++++++
.../hadoop/ozone/s3/endpoint/EndpointBase.java | 1 +
.../ozone/s3/endpoint/ObjectAttributesHandler.java | 2 +
.../hadoop/ozone/s3/util/S3GActionIamMapper.java | 13 ++
.../TestS3ActionOverrideForOwnerVerification.java | 143 ++++++++++++++-
.../ozone/s3/util/TestS3GActionIamMapper.java | 7 +
10 files changed, 578 insertions(+), 45 deletions(-)
diff --git
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/security/acl/iam/IamSessionPolicyResolver.java
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/security/acl/iam/IamSessionPolicyResolver.java
index 6dcf468f6b7..50ea6f3107a 100644
---
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/security/acl/iam/IamSessionPolicyResolver.java
+++
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/security/acl/iam/IamSessionPolicyResolver.java
@@ -1140,6 +1140,14 @@ enum S3Action {
EnumSet.of(READ, LIST), EnumSet.noneOf(ACLType.class)),
PUT_BUCKET_ACL("s3:PutBucketAcl", ActionKind.BUCKET, EnumSet.of(READ),
EnumSet.of(READ, READ_ACL, WRITE_ACL),
EnumSet.noneOf(ACLType.class)),
+ GET_BUCKET_TAGGING("s3:GetBucketTagging", ActionKind.BUCKET,
EnumSet.of(READ), EnumSet.of(READ),
+ EnumSet.noneOf(ACLType.class)),
+ PUT_BUCKET_TAGGING("s3:PutBucketTagging", ActionKind.BUCKET,
EnumSet.of(READ), EnumSet.of(READ, WRITE),
+ EnumSet.noneOf(ACLType.class)),
+ GET_BUCKET_LIFECYCLE("s3:GetLifecycleConfiguration", ActionKind.BUCKET,
EnumSet.of(READ), EnumSet.of(READ),
+ EnumSet.noneOf(ACLType.class)),
+ PUT_BUCKET_LIFECYCLE("s3:PutLifecycleConfiguration", ActionKind.BUCKET,
EnumSet.of(READ), EnumSet.of(READ, WRITE),
+ EnumSet.noneOf(ACLType.class)),
// Object-scope
ABORT_MULTIPART_UPLOAD("s3:AbortMultipartUpload", ActionKind.OBJECT,
EnumSet.of(READ), EnumSet.of(READ),
@@ -1151,6 +1159,8 @@ enum S3Action {
EnumSet.of(WRITE)),
// Used for HeadObject, CopyObject (for source bucket), GetObject (without
versionId parameter) APIs
GET_OBJECT("s3:GetObject", ActionKind.OBJECT, EnumSet.of(READ),
EnumSet.of(READ), EnumSet.of(READ)),
+ GET_OBJECT_ATTRIBUTES("s3:GetObjectAttributes", ActionKind.OBJECT,
EnumSet.of(READ), EnumSet.of(READ),
+ EnumSet.of(READ)),
GET_OBJECT_TAGGING("s3:GetObjectTagging", ActionKind.OBJECT,
EnumSet.of(READ), EnumSet.of(READ), EnumSet.of(READ)),
// Used for ListParts API
LIST_MULTIPART_UPLOAD_PARTS("s3:ListMultipartUploadParts",
ActionKind.OBJECT, EnumSet.of(READ), EnumSet.of(READ),
diff --git
a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/security/acl/iam/TestIamSessionPolicyResolver.java
b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/security/acl/iam/TestIamSessionPolicyResolver.java
index 54e3be080ea..4b32cab1d73 100644
---
a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/security/acl/iam/TestIamSessionPolicyResolver.java
+++
b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/security/acl/iam/TestIamSessionPolicyResolver.java
@@ -37,13 +37,18 @@
import static
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.DELETE_OBJECT;
import static
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.DELETE_OBJECT_TAGGING;
import static
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.GET_BUCKET_ACL;
+import static
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.GET_BUCKET_LIFECYCLE;
+import static
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.GET_BUCKET_TAGGING;
import static
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.GET_OBJECT;
+import static
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.GET_OBJECT_ATTRIBUTES;
import static
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.GET_OBJECT_TAGGING;
import static
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.LIST_ALL_MY_BUCKETS;
import static
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.LIST_BUCKET;
import static
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.LIST_BUCKET_MULTIPART_UPLOADS;
import static
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.LIST_MULTIPART_UPLOAD_PARTS;
import static
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.PUT_BUCKET_ACL;
+import static
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.PUT_BUCKET_LIFECYCLE;
+import static
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.PUT_BUCKET_TAGGING;
import static
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.PUT_OBJECT;
import static
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3Action.PUT_OBJECT_TAGGING;
import static
org.apache.hadoop.ozone.security.acl.iam.IamSessionPolicyResolver.S3ResourceType;
@@ -77,11 +82,12 @@ public class TestIamSessionPolicyResolver {
private static final String VOLUME = "s3v";
private static final Set<String> ALL_OBJECT_ACTIONS = strSet(
- "AbortMultipartUpload", "DeleteObject", "DeleteObjectTagging",
"GetObject", "GetObjectTagging",
- "ListMultipartUploadParts", "PutObject", "PutObjectTagging");
+ "AbortMultipartUpload", "DeleteObject", "DeleteObjectTagging",
"GetObject", "GetObjectAttributes",
+ "GetObjectTagging", "ListMultipartUploadParts", "PutObject",
"PutObjectTagging");
private static final Set<String> ALL_BUCKET_ACTIONS = strSet(
- "CreateBucket", "DeleteBucket", "GetBucketAcl", "ListBucket",
"ListBucketMultipartUploads", "PutBucketAcl");
+ "CreateBucket", "DeleteBucket", "GetBucketAcl", "GetBucketTagging",
"GetLifecycleConfiguration", "ListBucket",
+ "ListBucketMultipartUploads", "PutBucketAcl", "PutBucketTagging",
"PutLifecycleConfiguration");
private static final Set<String> ALL_BUCKET_ACTIONS_WITH_LIST_ALL_MY_BUCKETS;
@@ -797,11 +803,14 @@ public void testBuildCaseInsensitiveS3ActionMap() {
// Verify s3:Get* contains Get actions
final Set<S3Action> getActions = caseInsensitiveS3ActionMap.get("s3:get*");
- assertThat(getActions).containsOnly(GET_OBJECT, GET_BUCKET_ACL,
GET_OBJECT_TAGGING);
+ assertThat(getActions).containsOnly(
+ GET_OBJECT, GET_OBJECT_ATTRIBUTES, GET_BUCKET_ACL, GET_OBJECT_TAGGING,
GET_BUCKET_TAGGING,
+ GET_BUCKET_LIFECYCLE);
// Verify s3:Put* contains Put actions
final Set<S3Action> putActions = caseInsensitiveS3ActionMap.get("s3:put*");
- assertThat(putActions).containsOnly(PUT_OBJECT, PUT_OBJECT_TAGGING,
PUT_BUCKET_ACL);
+ assertThat(putActions).containsOnly(
+ PUT_OBJECT, PUT_OBJECT_TAGGING, PUT_BUCKET_ACL, PUT_BUCKET_TAGGING,
PUT_BUCKET_LIFECYCLE);
// Verify s3:List* contains List actions
final Set<S3Action> listActions =
caseInsensitiveS3ActionMap.get("s3:list*");
@@ -867,20 +876,21 @@ public void
testMapPolicyActionsToS3ActionsWithMultipleActionsMapAllCorrectly()
@Test
public void testMapPolicyActionsToS3ActionsWithWildcardExpansion() {
final Set<S3Action> result =
mapPolicyActionsToS3Actions(Collections.singleton("s3:Get*"));
- assertThat(result).containsOnly(GET_OBJECT, GET_BUCKET_ACL,
GET_OBJECT_TAGGING);
+ assertThat(result).containsOnly(
+ GET_OBJECT, GET_OBJECT_ATTRIBUTES, GET_BUCKET_ACL, GET_OBJECT_TAGGING,
GET_BUCKET_TAGGING,
+ GET_BUCKET_LIFECYCLE);
// Ensure it is case-insensitive
final Set<S3Action> resultCi =
mapPolicyActionsToS3Actions(Collections.singleton("s3:gET*"));
- assertThat(resultCi).containsOnly(GET_OBJECT, GET_BUCKET_ACL,
GET_OBJECT_TAGGING);
+ assertThat(resultCi).containsOnly(
+ GET_OBJECT, GET_OBJECT_ATTRIBUTES, GET_BUCKET_ACL, GET_OBJECT_TAGGING,
GET_BUCKET_TAGGING,
+ GET_BUCKET_LIFECYCLE);
}
@Test
public void testMapPolicyActionsToS3ActionsWithS3StarReturnsAll() {
final Set<S3Action> result =
mapPolicyActionsToS3Actions(Collections.singleton("s3:*"));
- assertThat(result).containsOnly(
- LIST_ALL_MY_BUCKETS, CREATE_BUCKET, DELETE_BUCKET, GET_BUCKET_ACL,
LIST_BUCKET, LIST_BUCKET_MULTIPART_UPLOADS,
- PUT_BUCKET_ACL, ABORT_MULTIPART_UPLOAD, DELETE_OBJECT,
DELETE_OBJECT_TAGGING, GET_OBJECT, GET_OBJECT_TAGGING,
- LIST_MULTIPART_UPLOAD_PARTS, PUT_OBJECT, PUT_OBJECT_TAGGING);
+ assertThat(result).containsOnly(S3Action.values());
final Set<S3Action> resultCi =
mapPolicyActionsToS3Actions(Collections.singleton("S3:*"));
assertThat(resultCi).isEqualTo(result);
@@ -903,24 +913,25 @@ public void
testMapPolicyActionsToS3ActionsWithOnlyUnsupportedActionsReturnsEmpt
@Test
public void testMapPolicyActionsToS3ActionsDeduplicatesResults() {
final Set<S3Action> result = mapPolicyActionsToS3Actions(strSet("s3:Get*",
"s3:GetObject", "s3:GetBucketAcl"));
- assertThat(result).containsOnly(GET_OBJECT, GET_BUCKET_ACL,
GET_OBJECT_TAGGING);
+ assertThat(result).containsOnly(
+ GET_OBJECT, GET_OBJECT_ATTRIBUTES, GET_BUCKET_ACL, GET_OBJECT_TAGGING,
GET_BUCKET_TAGGING,
+ GET_BUCKET_LIFECYCLE);
}
@Test
public void testMapPolicyActionsToS3ActionsHandlesMultipleWildcards() {
final Set<S3Action> result = mapPolicyActionsToS3Actions(strSet("s3:Get*",
"s3:Put*"));
assertThat(result).containsOnly(
- GET_OBJECT, GET_BUCKET_ACL, GET_OBJECT_TAGGING, PUT_OBJECT,
PUT_OBJECT_TAGGING, PUT_BUCKET_ACL);
+ GET_OBJECT, GET_OBJECT_ATTRIBUTES, GET_BUCKET_ACL, GET_OBJECT_TAGGING,
GET_BUCKET_TAGGING,
+ GET_BUCKET_LIFECYCLE, PUT_OBJECT, PUT_OBJECT_TAGGING, PUT_BUCKET_ACL,
PUT_BUCKET_TAGGING,
+ PUT_BUCKET_LIFECYCLE);
}
@Test
public void testMapPolicyActionsToS3ActionsWithS3StarIgnoresOtherActions() {
final Set<S3Action> result = mapPolicyActionsToS3Actions(strSet("s3:*",
"s3:GetObject", "s3:PutObject"));
// When s3:* is present, it should return all supported concrete actions
- assertThat(result).containsOnly(
- LIST_ALL_MY_BUCKETS, CREATE_BUCKET, DELETE_BUCKET, GET_BUCKET_ACL,
LIST_BUCKET, LIST_BUCKET_MULTIPART_UPLOADS,
- PUT_BUCKET_ACL, ABORT_MULTIPART_UPLOAD, DELETE_OBJECT,
DELETE_OBJECT_TAGGING, GET_OBJECT, GET_OBJECT_TAGGING,
- LIST_MULTIPART_UPLOAD_PARTS, PUT_OBJECT, PUT_OBJECT_TAGGING);
+ assertThat(result).containsOnly(S3Action.values());
}
@Test
@@ -1620,7 +1631,8 @@ public void
testCreatePathsAndPermissionsWithAllConcreteS3Actions() {
new OzoneGrant(objSet(bucket("bucket1")), acls(READ),
ALL_OBJECT_ACTIONS),
new OzoneGrant(objSet(volume()), acls(READ),
ALL_BUCKET_AND_OBJECT_ACTIONS),
new OzoneGrant(
- objSet(bucket("bucket2")), acls(READ, LIST, CREATE, DELETE,
READ_ACL, WRITE_ACL), ALL_BUCKET_ACTIONS),
+ objSet(bucket("bucket2")), acls(READ, LIST, CREATE, DELETE,
READ_ACL, WRITE_ACL, WRITE),
+ ALL_BUCKET_ACTIONS),
new OzoneGrant(objSet(prefix("bucket2", "")), acls(READ),
strSet("ListBucket")));
final Map<IOzoneObj, Set<ACLType>> objToAclsMapRanger = new
LinkedHashMap<>();
@@ -1632,7 +1644,8 @@ public void
testCreatePathsAndPermissionsWithAllConcreteS3Actions() {
new OzoneGrant(objSet(bucket("bucket1")), acls(READ),
ALL_OBJECT_ACTIONS),
new OzoneGrant(objSet(volume()), acls(READ),
ALL_BUCKET_AND_OBJECT_ACTIONS),
new OzoneGrant(
- objSet(bucket("bucket2")), acls(READ, LIST, CREATE, DELETE,
READ_ACL, WRITE_ACL), ALL_BUCKET_ACTIONS),
+ objSet(bucket("bucket2")), acls(READ, LIST, CREATE, DELETE,
READ_ACL, WRITE_ACL, WRITE),
+ ALL_BUCKET_ACTIONS),
new OzoneGrant(objSet(key("bucket2", "*")), acls(READ),
strSet("ListBucket")));
}
@@ -1823,7 +1836,7 @@ public void
testDeduplicatesAcrossMultipleStatementsWhenAllActionPresent() throw
final Set<OzoneGrant> expectedResolvedNative = new LinkedHashSet<>();
// Expected for native: bucket union of supported bucket ACLs; volume
READ; prefix "" READ (from ListBucket)
final Set<IOzoneObj> bucketSet = objSet(bucket("my-bucket"));
- final Set<ACLType> bucketAcls = acls(READ, LIST, CREATE, DELETE, READ_ACL,
WRITE_ACL);
+ final Set<ACLType> bucketAcls = acls(READ, LIST, CREATE, DELETE, READ_ACL,
WRITE_ACL, WRITE);
expectedResolvedNative.add(new OzoneGrant(bucketSet, bucketAcls,
ALL_BUCKET_ACTIONS));
expectedResolvedNative.add(new OzoneGrant(objSet(prefix("my-bucket", "")),
acls(READ), strSet("ListBucket")));
expectedResolvedNative.add(new OzoneGrant(objSet(volume()), acls(READ),
ALL_BUCKET_ACTIONS));
@@ -1936,7 +1949,7 @@ public void testAllActionsForBucket() throws OMException {
final Set<OzoneGrant> expectedResolvedNative = new LinkedHashSet<>();
// Expected for native: union of supported bucket ACLs for bucket; volume
READ; prefix "" READ (from ListBucket)
final Set<IOzoneObj> bucketSet = objSet(bucket("my-bucket"));
- final Set<ACLType> allBucketAcls = acls(READ, LIST, CREATE, DELETE,
READ_ACL, WRITE_ACL);
+ final Set<ACLType> allBucketAcls = acls(READ, LIST, CREATE, DELETE,
READ_ACL, WRITE_ACL, WRITE);
expectedResolvedNative.add(new OzoneGrant(bucketSet, allBucketAcls,
ALL_BUCKET_ACTIONS));
expectedResolvedNative.add(new OzoneGrant(objSet(prefix("my-bucket", "")),
acls(READ), strSet("ListBucket")));
expectedResolvedNative.add(new OzoneGrant(objSet(volume()), acls(READ),
ALL_BUCKET_ACTIONS));
@@ -2070,7 +2083,7 @@ public void testMultipleResourcesInOneStatement() throws
OMException {
final Set<IOzoneObj> bucketSet = objSet(bucket("my-bucket"));
expectedResolvedNative.add(
new OzoneGrant(
- bucketSet, acls(READ, LIST, CREATE, DELETE, READ_ACL, WRITE_ACL),
ALL_BUCKET_AND_OBJECT_ACTIONS));
+ bucketSet, acls(READ, LIST, CREATE, DELETE, READ_ACL, WRITE_ACL,
WRITE), ALL_BUCKET_AND_OBJECT_ACTIONS));
expectedResolvedNative.add(
new OzoneGrant(
objSet(prefix("my-bucket", "")), acls(READ, CREATE, WRITE,
DELETE), ALL_OBJECT_ACTIONS_WITH_LIST_BUCKET));
@@ -2081,7 +2094,7 @@ public void testMultipleResourcesInOneStatement() throws
OMException {
// Expected for Ranger: bucket union of supported bucket ACLs; key "*"
union of supported object ACLs; volume READ
expectedResolvedRanger.add(
new OzoneGrant(
- bucketSet, acls(READ, LIST, CREATE, DELETE, READ_ACL, WRITE_ACL),
ALL_BUCKET_AND_OBJECT_ACTIONS));
+ bucketSet, acls(READ, LIST, CREATE, DELETE, READ_ACL, WRITE_ACL,
WRITE), ALL_BUCKET_AND_OBJECT_ACTIONS));
expectedResolvedRanger.add(
new OzoneGrant(
objSet(key("my-bucket", "*")), acls(READ, CREATE, WRITE, DELETE),
ALL_OBJECT_ACTIONS_WITH_LIST_BUCKET));
@@ -2506,7 +2519,8 @@ public void testAllActionsOnAllResources() throws
OMException {
expectedResolvedRanger.add(new OzoneGrant(objSet(volume()), acls(READ,
LIST), emptySet()));
expectedResolvedRanger.add(
new OzoneGrant(
- objSet(bucket("*")), acls(READ, LIST, CREATE, DELETE, READ_ACL,
WRITE_ACL), ALL_BUCKET_AND_OBJECT_ACTIONS));
+ objSet(bucket("*")), acls(READ, LIST, CREATE, DELETE, READ_ACL,
WRITE_ACL, WRITE),
+ ALL_BUCKET_AND_OBJECT_ACTIONS));
expectedResolvedRanger.add(
new OzoneGrant(objSet(key("*", "*")), acls(READ, CREATE, WRITE,
DELETE), ALL_OBJECT_ACTIONS_WITH_LIST_BUCKET));
assertThat(resolvedFromRangerAuthorizer).isEqualTo(expectedResolvedRanger);
@@ -2530,7 +2544,7 @@ public void testAllActionsOnAllBucketResources() throws
OMException {
final Set<OzoneGrant> expectedResolvedRanger = new LinkedHashSet<>();
// Expected for Ranger: union of supported bucket ACLs on wildcard bucket;
volume READ, LIST; key "*" READ
final Set<IOzoneObj> bucketSet = objSet(bucket("*"));
- final Set<ACLType> bucketAcls = acls(READ, LIST, CREATE, DELETE, READ_ACL,
WRITE_ACL);
+ final Set<ACLType> bucketAcls = acls(READ, LIST, CREATE, DELETE, READ_ACL,
WRITE_ACL, WRITE);
expectedResolvedRanger.add(new OzoneGrant(bucketSet, bucketAcls,
ALL_BUCKET_ACTIONS));
expectedResolvedRanger.add(new OzoneGrant(objSet(key("*", "*")),
acls(READ), strSet("ListBucket")));
expectedResolvedRanger.add(
@@ -2584,23 +2598,38 @@ public void testWildcardActionGroupGetStar() throws
OMException {
final Set<IOzoneObj> bucketSet = objSet(bucket("my-bucket"));
final Set<ACLType> bucketAcls = acls(READ, READ_ACL);
expectedResolvedNative.add(
- new OzoneGrant(bucketSet, bucketAcls, strSet("GetBucketAcl",
"GetObject", "GetObjectTagging")));
+ new OzoneGrant(
+ bucketSet, bucketAcls, strSet(
+ "GetBucketAcl", "GetBucketTagging",
"GetLifecycleConfiguration", "GetObject", "GetObjectAttributes",
+ "GetObjectTagging")));
// Expected for native: READ acl on prefix "" under bucket; volume READ
expectedResolvedNative.add(
- new OzoneGrant(objSet(prefix("my-bucket", "")), acls(READ),
strSet("GetObject", "GetObjectTagging")));
+ new OzoneGrant(
+ objSet(prefix("my-bucket", "")), acls(READ), strSet(
+ "GetObject", "GetObjectAttributes", "GetObjectTagging")));
expectedResolvedNative.add(
- new OzoneGrant(objSet(volume()), acls(READ), strSet("GetBucketAcl",
"GetObject", "GetObjectTagging")));
+ new OzoneGrant(
+ objSet(volume()), acls(READ), strSet(
+ "GetBucketAcl", "GetBucketTagging",
"GetLifecycleConfiguration", "GetObject", "GetObjectAttributes",
+ "GetObjectTagging")));
assertThat(resolvedFromNativeAuthorizer).isEqualTo(expectedResolvedNative);
final Set<OzoneGrant> expectedResolvedRanger = new LinkedHashSet<>();
// Expected for Ranger: bucket READ, READ_ACL acls
expectedResolvedRanger.add(
- new OzoneGrant(bucketSet, bucketAcls, strSet("GetBucketAcl",
"GetObject", "GetObjectTagging")));
+ new OzoneGrant(
+ bucketSet, bucketAcls, strSet(
+ "GetBucketAcl", "GetBucketTagging",
"GetLifecycleConfiguration", "GetObject", "GetObjectAttributes",
+ "GetObjectTagging")));
// Expected for Ranger: READ key acl for resource type KEY with key name
"*"; volume READ
expectedResolvedRanger.add(
- new OzoneGrant(objSet(key("my-bucket", "*")), acls(READ),
strSet("GetObject", "GetObjectTagging")));
+ new OzoneGrant(
+ objSet(key("my-bucket", "*")), acls(READ), strSet("GetObject",
"GetObjectAttributes", "GetObjectTagging")));
expectedResolvedRanger.add(
- new OzoneGrant(objSet(volume()), acls(READ), strSet("GetBucketAcl",
"GetObject", "GetObjectTagging")));
+ new OzoneGrant(
+ objSet(volume()), acls(READ), strSet(
+ "GetBucketAcl", "GetBucketTagging",
"GetLifecycleConfiguration", "GetObject", "GetObjectAttributes",
+ "GetObjectTagging")));
assertThat(resolvedFromRangerAuthorizer).isEqualTo(expectedResolvedRanger);
}
@@ -2670,33 +2699,68 @@ public void testWildcardActionGroupPutStar() throws
OMException {
// Ensure what we got is what we expected
final Set<OzoneGrant> expectedResolvedNative = new LinkedHashSet<>();
- // Expected for native: bucket READ, READ_ACL, WRITE_ACL acl
+ // Expected for native: bucket READ, READ_ACL, WRITE_ACL, WRITE acl
final Set<IOzoneObj> bucketSet = objSet(bucket("my-bucket"));
- final Set<ACLType> bucketAcl = acls(READ, READ_ACL, WRITE_ACL);
+ final Set<ACLType> bucketAcl = acls(READ, READ_ACL, WRITE_ACL, WRITE);
expectedResolvedNative.add(
- new OzoneGrant(bucketSet, bucketAcl, strSet("PutBucketAcl",
"PutObject", "PutObjectTagging")));
+ new OzoneGrant(
+ bucketSet, bucketAcl, strSet(
+ "PutBucketAcl", "PutObject", "PutObjectTagging",
"PutLifecycleConfiguration", "PutBucketTagging")));
// Expected for native: CREATE, WRITE acls on prefix "" under bucket
final Set<IOzoneObj> keyPrefixSet = objSet(prefix("my-bucket", ""));
final Set<ACLType> keyAcls = acls(CREATE, WRITE);
expectedResolvedNative.add(new OzoneGrant(keyPrefixSet, keyAcls,
strSet("PutObject", "PutObjectTagging")));
// Expected for native: volume READ
expectedResolvedNative.add(
- new OzoneGrant(objSet(volume()), acls(READ), strSet("PutBucketAcl",
"PutObject", "PutObjectTagging")));
+ new OzoneGrant(
+ objSet(volume()), acls(READ), strSet(
+ "PutBucketAcl", "PutObject", "PutObjectTagging",
"PutLifecycleConfiguration", "PutBucketTagging")));
assertThat(resolvedFromNativeAuthorizer).isEqualTo(expectedResolvedNative);
final Set<OzoneGrant> expectedResolvedRanger = new LinkedHashSet<>();
- // Expected for Ranger: bucket READ, READ_ACL, WRITE_ACL acl
+ // Expected for Ranger: bucket READ, READ_ACL, WRITE_ACL, WRITE acl
expectedResolvedRanger.add(
- new OzoneGrant(bucketSet, bucketAcl, strSet("PutBucketAcl",
"PutObject", "PutObjectTagging")));
+ new OzoneGrant(
+ bucketSet, bucketAcl, strSet(
+ "PutBucketAcl", "PutObject", "PutObjectTagging",
"PutLifecycleConfiguration", "PutBucketTagging")));
// Expected for Ranger: CREATE, WRITE key acls for resource type KEY with
key name "*"
final Set<IOzoneObj> rangerKeySet = objSet(key("my-bucket", "*"));
expectedResolvedRanger.add(new OzoneGrant(rangerKeySet, keyAcls,
strSet("PutObject", "PutObjectTagging")));
// Expected for Ranger: volume READ
expectedResolvedRanger.add(
- new OzoneGrant(objSet(volume()), acls(READ), strSet("PutBucketAcl",
"PutObject", "PutObjectTagging")));
+ new OzoneGrant(
+ objSet(volume()), acls(READ), strSet(
+ "PutBucketAcl", "PutObject", "PutObjectTagging",
"PutLifecycleConfiguration", "PutBucketTagging")));
assertThat(resolvedFromRangerAuthorizer).isEqualTo(expectedResolvedRanger);
}
+ @Test
+ public void
testPutBucketTaggingAndPutLifecycleConfigurationRequireBucketReadAndWrite()
throws OMException {
+ final String json = "{\n" +
+ " \"Statement\": [{\n" +
+ " \"Effect\": \"Allow\",\n" +
+ " \"Action\": [\n" +
+ " \"s3:PutBucketTagging\",\n" +
+ " \"s3:PutLifecycleConfiguration\"\n" +
+ " ],\n" +
+ " \"Resource\": \"arn:aws:s3:::my-bucket\"\n" +
+ " }]\n" +
+ "}";
+
+ final Set<OzoneGrant> resolvedFromNativeAuthorizer = resolve(json, VOLUME,
NATIVE);
+ final Set<OzoneGrant> resolvedFromRangerAuthorizer = resolve(json, VOLUME,
RANGER);
+
+ // Ensure what we got is what we expected
+ final Set<OzoneGrant> expected = new LinkedHashSet<>();
+ final Set<IOzoneObj> bucketSet = objSet(bucket("my-bucket"));
+ final Set<ACLType> bucketAcl = acls(READ, WRITE);
+ expected.add(new OzoneGrant(bucketSet, bucketAcl,
strSet("PutBucketTagging", "PutLifecycleConfiguration")));
+ expected.add(new OzoneGrant(objSet(volume()), acls(READ),
strSet("PutBucketTagging", "PutLifecycleConfiguration")));
+
+ assertThat(resolvedFromNativeAuthorizer).isEqualTo(expected);
+ assertThat(resolvedFromRangerAuthorizer).isEqualTo(expected);
+ }
+
@Test
public void testWildcardActionGroupDeleteStar() throws OMException {
final String json = "{\n" +
diff --git
a/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot
b/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot
index 407cf60f105..81c784382c8 100644
--- a/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot
+++ b/hadoop-ozone/dist/src/main/smoketest/security/ozone-secure-sts.robot
@@ -45,6 +45,7 @@ ${PARTIAL_LIST_ALL_BUCKETS_VOL_LIST_ROLE}
partial-list-all-buckets-vol-lis
${PARTIAL_BUCKET_READ_ROLE} partial-bucket-read
${PARTIAL_BUCKET_READ_UPLOAD_PREFIX_ROLE}
partial-bucket-read-upload-prefix
${PARTIAL_BUCKET_LIST_ROLE} partial-bucket-list
+${PARTIAL_BUCKET_WRITE_ROLE} partial-bucket-write
${PARTIAL_BUCKET_READ_ACL_ROLE} partial-bucket-read-acl
${PARTIAL_PUT_OBJECT_KEY_CREATE_ROLE} partial-put-object-key-create
${PARTIAL_PUT_OBJECT_KEY_WRITE_ROLE} partial-put-object-key-write
@@ -58,6 +59,7 @@ ${PARTIAL_LIST_ALL_BUCKETS_VOL_LIST_ROLE_ARN}
arn:aws:iam::123456789012:role/$
${PARTIAL_BUCKET_READ_ROLE_ARN}
arn:aws:iam::123456789012:role/${PARTIAL_BUCKET_READ_ROLE}
${PARTIAL_BUCKET_READ_UPLOAD_PREFIX_ROLE_ARN}
arn:aws:iam::123456789012:role/${PARTIAL_BUCKET_READ_UPLOAD_PREFIX_ROLE}
${PARTIAL_BUCKET_LIST_ROLE_ARN}
arn:aws:iam::123456789012:role/${PARTIAL_BUCKET_LIST_ROLE}
+${PARTIAL_BUCKET_WRITE_ROLE_ARN}
arn:aws:iam::123456789012:role/${PARTIAL_BUCKET_WRITE_ROLE}
${PARTIAL_BUCKET_READ_ACL_ROLE_ARN}
arn:aws:iam::123456789012:role/${PARTIAL_BUCKET_READ_ACL_ROLE}
${PARTIAL_PUT_OBJECT_KEY_CREATE_ROLE_ARN}
arn:aws:iam::123456789012:role/${PARTIAL_PUT_OBJECT_KEY_CREATE_ROLE}
${PARTIAL_PUT_OBJECT_KEY_WRITE_ROLE_ARN}
arn:aws:iam::123456789012:role/${PARTIAL_PUT_OBJECT_KEY_WRITE_ROLE}
@@ -86,6 +88,8 @@ ${TEST_USER_NON_ADMIN} testuser2
@{ICEBERG_PREFIX_USERA_STAR_FSO} userA/ userA/userA.txt
@{ICEBERG_PREFIX_USER_OBS} userA/userA.txt userB/userB.txt
userAfile.txt
@{ICEBERG_PREFIX_USER_FSO} userA/ userA/userA.txt userB/
userB/userB.txt userAfile.txt
+${STS_BUCKET_TAGGING_JSON}
{"TagSet":[{"Key":"sts-tag","Value":"sts-value"}]}
+${STS_LIFECYCLE_JSON}
{"Rules":[{"ID":"sts-lifecycle-rule","Prefix":"sts-lifecycle-unused/","Status":"Enabled","Expiration":{"Days":1}}]}
*** Keywords ***
Populate Iceberg Bucket
@@ -393,24 +397,24 @@ Create STS Link Bucket Access Policies
Refresh Ranger Policy Cache
Create Partial Access Roles in Ranger
- FOR ${role} IN ${PARTIAL_LIST_ALL_BUCKETS_VOL_READ_ROLE}
${PARTIAL_LIST_ALL_BUCKETS_VOL_LIST_ROLE} ${PARTIAL_BUCKET_READ_ROLE}
${PARTIAL_BUCKET_READ_UPLOAD_PREFIX_ROLE} ${PARTIAL_BUCKET_LIST_ROLE}
${PARTIAL_BUCKET_READ_ACL_ROLE} ${PARTIAL_PUT_OBJECT_KEY_CREATE_ROLE}
${PARTIAL_PUT_OBJECT_KEY_WRITE_ROLE}
+ FOR ${role} IN ${PARTIAL_LIST_ALL_BUCKETS_VOL_READ_ROLE}
${PARTIAL_LIST_ALL_BUCKETS_VOL_LIST_ROLE} ${PARTIAL_BUCKET_READ_ROLE}
${PARTIAL_BUCKET_READ_UPLOAD_PREFIX_ROLE} ${PARTIAL_BUCKET_LIST_ROLE}
${PARTIAL_BUCKET_WRITE_ROLE} ${PARTIAL_BUCKET_READ_ACL_ROLE}
${PARTIAL_PUT_OBJECT_KEY_CREATE_ROLE} ${PARTIAL_PUT_OBJECT_KEY_WRITE_ROLE}
${role_json} = Set Variable { "name":
"${role}", "description": "Partial access role" }
Create Ranger Role ${role_json}
END
Create Partial Access Assume Role Policies
- FOR ${role} IN ${PARTIAL_LIST_ALL_BUCKETS_VOL_READ_ROLE}
${PARTIAL_LIST_ALL_BUCKETS_VOL_LIST_ROLE} ${PARTIAL_BUCKET_READ_ROLE}
${PARTIAL_BUCKET_READ_UPLOAD_PREFIX_ROLE} ${PARTIAL_BUCKET_LIST_ROLE}
${PARTIAL_BUCKET_READ_ACL_ROLE} ${PARTIAL_PUT_OBJECT_KEY_CREATE_ROLE}
${PARTIAL_PUT_OBJECT_KEY_WRITE_ROLE}
+ FOR ${role} IN ${PARTIAL_LIST_ALL_BUCKETS_VOL_READ_ROLE}
${PARTIAL_LIST_ALL_BUCKETS_VOL_LIST_ROLE} ${PARTIAL_BUCKET_READ_ROLE}
${PARTIAL_BUCKET_READ_UPLOAD_PREFIX_ROLE} ${PARTIAL_BUCKET_LIST_ROLE}
${PARTIAL_BUCKET_WRITE_ROLE} ${PARTIAL_BUCKET_READ_ACL_ROLE}
${PARTIAL_PUT_OBJECT_KEY_CREATE_ROLE} ${PARTIAL_PUT_OBJECT_KEY_WRITE_ROLE}
Create Ranger Assume Role Policy ${role}
${ICEBERG_SVC_CATALOG_USER}
END
Create Partial Access Volume Policies
# Append partial-role items to existing "iceberg volume access" policy to
avoid duplicate resourceSignature conflicts.
- ${policy_items} = Set Variable [ {
"accesses": [ { "type": "read", "isAllowed": true } ], "roles": [
"${PARTIAL_LIST_ALL_BUCKETS_VOL_READ_ROLE}" ], "delegateAdmin": false }, {
"accesses": [ { "type": "list", "isAllowed": true } ], "roles": [
"${PARTIAL_LIST_ALL_BUCKETS_VOL_LIST_ROLE}" ], "delegateAdmin": false }, {
"accesses": [ { "type": "read", "isAllowed": true } ], "roles": [
"${PARTIAL_BUCKET_READ_ROLE}", "${PARTIAL_BUCKET_READ_UPLOAD_PREFIX_ROLE}",
[...]
+ ${policy_items} = Set Variable [ {
"accesses": [ { "type": "read", "isAllowed": true } ], "roles": [
"${PARTIAL_LIST_ALL_BUCKETS_VOL_READ_ROLE}" ], "delegateAdmin": false }, {
"accesses": [ { "type": "list", "isAllowed": true } ], "roles": [
"${PARTIAL_LIST_ALL_BUCKETS_VOL_LIST_ROLE}" ], "delegateAdmin": false }, {
"accesses": [ { "type": "read", "isAllowed": true } ], "roles": [
"${PARTIAL_BUCKET_READ_ROLE}", "${PARTIAL_BUCKET_READ_UPLOAD_PREFIX_ROLE}",
[...]
Update Ranger Policy Items iceberg volume access ${policy_items}
Create Partial Access Bucket Policies
# Append partial-role items to existing "iceberg ${ICEBERG_BUCKET_OBS}
bucket access" policy.
- ${policy_items} = Set Variable [ {
"accesses": [ { "type": "read", "isAllowed": true } ], "roles": [
"${PARTIAL_BUCKET_READ_ROLE}" ], "delegateAdmin": false }, { "accesses": [ {
"type": "read", "isAllowed": true } ], "roles": [
"${PARTIAL_BUCKET_READ_UPLOAD_PREFIX_ROLE}" ], "delegateAdmin": false }, {
"accesses": [ { "type": "list", "isAllowed": true } ], "roles": [
"${PARTIAL_BUCKET_LIST_ROLE}" ], "delegateAdmin": false }, { "accesses": [ {
"type": "read [...]
+ ${policy_items} = Set Variable [ {
"accesses": [ { "type": "read", "isAllowed": true } ], "roles": [
"${PARTIAL_BUCKET_READ_ROLE}" ], "delegateAdmin": false }, { "accesses": [ {
"type": "read", "isAllowed": true } ], "roles": [
"${PARTIAL_BUCKET_READ_UPLOAD_PREFIX_ROLE}" ], "delegateAdmin": false }, {
"accesses": [ { "type": "list", "isAllowed": true } ], "roles": [
"${PARTIAL_BUCKET_LIST_ROLE}" ], "delegateAdmin": false }, { "accesses": [ {
"type": "writ [...]
Update Ranger Policy Items iceberg ${ICEBERG_BUCKET_OBS} bucket access
${policy_items}
Create Partial Access Table Policies
@@ -950,6 +954,154 @@ STS session policy PutBucketAcl must require bucket
WRITE_ACL
${output} = Execute And Ignore Error aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} put-bucket-acl --bucket
${ICEBERG_BUCKET_OBS} --grant-read "" --profile sts
Should Contain ${output} AccessDenied
+STS session policy GetBucketTagging must require bucket READ
+ ${put_tag_policy} = Set Variable
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutBucketTagging","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+ ${get_tag_policy} = Set Variable
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetBucketTagging","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+ ${delete_tag_policy} = Set Variable
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutBucketTagging","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+
+ Assume Role And Configure STS Profile
policy_json=${put_tag_policy} perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+ ${output} = Execute aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} put-bucket-tagging --bucket
${ICEBERG_BUCKET_OBS} --tagging '${STS_BUCKET_TAGGING_JSON}' --profile sts
+ Should Not Contain ${output} AccessDenied
+
+ # Positive control
+ Assume Role And Configure STS Profile
policy_json=${get_tag_policy} perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+ ${output} = Execute aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} get-bucket-tagging --bucket
${ICEBERG_BUCKET_OBS} --profile sts
+ Should Contain ${output} sts-tag
+
+ # Negative: missing READ
+ Assume Role And Configure STS Profile
policy_json=${get_tag_policy} perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${PARTIAL_BUCKET_LIST_ROLE_ARN}
+ ${output} = Execute And Ignore Error aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} get-bucket-tagging --bucket
${ICEBERG_BUCKET_OBS} --profile sts
+ Should Contain ${output} AccessDenied
+
+ Assume Role And Configure STS Profile
policy_json=${delete_tag_policy} perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+ ${output} = Execute aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} delete-bucket-tagging --bucket
${ICEBERG_BUCKET_OBS} --profile sts
+ Should Not Contain ${output} AccessDenied
+
+STS session policy PutBucketTagging must require bucket READ and WRITE
+ ${put_tag_policy} = Set Variable
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutBucketTagging","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+ ${delete_tag_policy} = Set Variable
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutBucketTagging","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+
+ # Positive control
+ Assume Role And Configure STS Profile
policy_json=${put_tag_policy} perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+ ${output} = Execute aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} put-bucket-tagging --bucket
${ICEBERG_BUCKET_OBS} --tagging '${STS_BUCKET_TAGGING_JSON}' --profile sts
+ Should Not Contain ${output} AccessDenied
+
+ # Negative A: missing WRITE
+ Assume Role And Configure STS Profile
policy_json=${put_tag_policy} perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${PARTIAL_BUCKET_READ_ROLE_ARN}
+ ${output} = Execute And Ignore Error aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} put-bucket-tagging --bucket
${ICEBERG_BUCKET_OBS} --tagging '${STS_BUCKET_TAGGING_JSON}' --profile sts
+ Should Contain ${output} AccessDenied
+
+ # Negative B: missing READ
+ Assume Role And Configure STS Profile
policy_json=${put_tag_policy} perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${PARTIAL_BUCKET_WRITE_ROLE_ARN}
+ ${output} = Execute And Ignore Error aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} put-bucket-tagging --bucket
${ICEBERG_BUCKET_OBS} --tagging '${STS_BUCKET_TAGGING_JSON}' --profile sts
+ Should Contain ${output} AccessDenied
+
+ Assume Role And Configure STS Profile
policy_json=${delete_tag_policy} perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+ ${output} = Execute aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} delete-bucket-tagging --bucket
${ICEBERG_BUCKET_OBS} --profile sts
+ Should Not Contain ${output} AccessDenied
+
+STS session policy PutBucketTagging must authorize DeleteBucketTagging with
bucket READ and WRITE
+ ${put_tag_policy} = Set Variable
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutBucketTagging","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+ ${delete_tag_policy} = Set Variable
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutBucketTagging","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+ ${get_tag_policy} = Set Variable
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetBucketTagging","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+
+ Assume Role And Configure STS Profile
policy_json=${put_tag_policy} perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+ ${output} = Execute aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} put-bucket-tagging --bucket
${ICEBERG_BUCKET_OBS} --tagging '${STS_BUCKET_TAGGING_JSON}' --profile sts
+ Should Not Contain ${output} AccessDenied
+
+ # Negative A: missing WRITE
+ Assume Role And Configure STS Profile
policy_json=${delete_tag_policy} perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${PARTIAL_BUCKET_READ_ROLE_ARN}
+ ${output} = Execute And Ignore Error aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} delete-bucket-tagging --bucket
${ICEBERG_BUCKET_OBS} --profile sts
+ Should Contain ${output} AccessDenied
+
+ # Negative B: missing READ
+ Assume Role And Configure STS Profile
policy_json=${delete_tag_policy} perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${PARTIAL_BUCKET_WRITE_ROLE_ARN}
+ ${output} = Execute And Ignore Error aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} delete-bucket-tagging --bucket
${ICEBERG_BUCKET_OBS} --profile sts
+ Should Contain ${output} AccessDenied
+
+ # Positive control
+ Assume Role And Configure STS Profile
policy_json=${delete_tag_policy} perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+ ${output} = Execute aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} delete-bucket-tagging --bucket
${ICEBERG_BUCKET_OBS} --profile sts
+ Should Not Contain ${output} AccessDenied
+
+ Assume Role And Configure STS Profile
policy_json=${get_tag_policy} perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+ ${output} = Execute And Ignore Error aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} get-bucket-tagging --bucket
${ICEBERG_BUCKET_OBS} --profile sts
+ Should Contain ${output} NoSuchTagSet
+
+STS session policy GetLifecycleConfiguration must require bucket READ
+ ${put_lifecycle_policy} = Set Variable
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutLifecycleConfiguration","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+ ${get_lifecycle_policy} = Set Variable
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetLifecycleConfiguration","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+ ${delete_lifecycle_policy} = Set Variable
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutLifecycleConfiguration","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+
+ Assume Role And Configure STS Profile
policy_json=${put_lifecycle_policy}
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+ ${output} = Execute aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} put-bucket-lifecycle-configuration --bucket
${ICEBERG_BUCKET_OBS} --lifecycle-configuration '${STS_LIFECYCLE_JSON}'
--profile sts
+ Should Not Contain ${output} AccessDenied
+
+ # Positive control
+ Assume Role And Configure STS Profile
policy_json=${get_lifecycle_policy}
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+ ${output} = Execute aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} get-bucket-lifecycle-configuration --bucket
${ICEBERG_BUCKET_OBS} --profile sts
+ Should Contain ${output}
sts-lifecycle-rule
+
+ # Negative: missing READ
+ Assume Role And Configure STS Profile
policy_json=${get_lifecycle_policy}
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${PARTIAL_BUCKET_LIST_ROLE_ARN}
+ ${output} = Execute And Ignore Error aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} get-bucket-lifecycle-configuration --bucket
${ICEBERG_BUCKET_OBS} --profile sts
+ Should Contain ${output} AccessDenied
+
+ Assume Role And Configure STS Profile
policy_json=${delete_lifecycle_policy}
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+ ${output} = Execute aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} delete-bucket-lifecycle --bucket
${ICEBERG_BUCKET_OBS} --profile sts
+ Should Not Contain ${output} AccessDenied
+
+STS session policy PutLifecycleConfiguration must require bucket READ and WRITE
+ ${put_lifecycle_policy} = Set Variable
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutLifecycleConfiguration","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+ ${delete_lifecycle_policy} = Set Variable
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutLifecycleConfiguration","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+
+ # Positive control
+ Assume Role And Configure STS Profile
policy_json=${put_lifecycle_policy}
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+ ${output} = Execute aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} put-bucket-lifecycle-configuration --bucket
${ICEBERG_BUCKET_OBS} --lifecycle-configuration '${STS_LIFECYCLE_JSON}'
--profile sts
+ Should Not Contain ${output} AccessDenied
+
+ # Negative A: missing WRITE
+ Assume Role And Configure STS Profile
policy_json=${put_lifecycle_policy}
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${PARTIAL_BUCKET_READ_ROLE_ARN}
+ ${output} = Execute And Ignore Error aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} put-bucket-lifecycle-configuration --bucket
${ICEBERG_BUCKET_OBS} --lifecycle-configuration '${STS_LIFECYCLE_JSON}'
--profile sts
+ Should Contain ${output} AccessDenied
+
+ # Negative B: missing READ
+ Assume Role And Configure STS Profile
policy_json=${put_lifecycle_policy}
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${PARTIAL_BUCKET_WRITE_ROLE_ARN}
+ ${output} = Execute And Ignore Error aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} put-bucket-lifecycle-configuration --bucket
${ICEBERG_BUCKET_OBS} --lifecycle-configuration '${STS_LIFECYCLE_JSON}'
--profile sts
+ Should Contain ${output} AccessDenied
+
+ Assume Role And Configure STS Profile
policy_json=${delete_lifecycle_policy}
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+ ${output} = Execute aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} delete-bucket-lifecycle --bucket
${ICEBERG_BUCKET_OBS} --profile sts
+ Should Not Contain ${output} AccessDenied
+
+STS session policy PutLifecycleConfiguration must authorize
DeleteBucketLifecycle with bucket READ and WRITE
+ ${put_lifecycle_policy} = Set Variable
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutLifecycleConfiguration","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+ ${delete_lifecycle_policy} = Set Variable
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutLifecycleConfiguration","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+ ${get_lifecycle_policy} = Set Variable
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetLifecycleConfiguration","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}"}]}
+
+ Assume Role And Configure STS Profile
policy_json=${put_lifecycle_policy}
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+ ${output} = Execute aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} put-bucket-lifecycle-configuration --bucket
${ICEBERG_BUCKET_OBS} --lifecycle-configuration '${STS_LIFECYCLE_JSON}'
--profile sts
+ Should Not Contain ${output} AccessDenied
+
+ # Negative A: missing WRITE
+ Assume Role And Configure STS Profile
policy_json=${delete_lifecycle_policy}
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${PARTIAL_BUCKET_READ_ROLE_ARN}
+ ${output} = Execute And Ignore Error aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} delete-bucket-lifecycle --bucket
${ICEBERG_BUCKET_OBS} --profile sts
+ Should Contain ${output} AccessDenied
+
+ # Negative B: missing READ
+ Assume Role And Configure STS Profile
policy_json=${delete_lifecycle_policy}
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${PARTIAL_BUCKET_WRITE_ROLE_ARN}
+ ${output} = Execute And Ignore Error aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} delete-bucket-lifecycle --bucket
${ICEBERG_BUCKET_OBS} --profile sts
+ Should Contain ${output} AccessDenied
+
+ # Positive control
+ Assume Role And Configure STS Profile
policy_json=${delete_lifecycle_policy}
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+ ${output} = Execute aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} delete-bucket-lifecycle --bucket
${ICEBERG_BUCKET_OBS} --profile sts
+ Should Not Contain ${output} AccessDenied
+
+ Assume Role And Configure STS Profile
policy_json=${get_lifecycle_policy}
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+ ${output} = Execute And Ignore Error aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} get-bucket-lifecycle-configuration --bucket
${ICEBERG_BUCKET_OBS} --profile sts
+ Should Contain ${output}
NoSuchLifecycleConfiguration
+
STS session policy GetObjectTagging must require key READ
${key_suffix} = Generate Random String 8 [LOWER]
${key} = Set Variable
sts-object-${key_suffix}.txt
@@ -1109,6 +1261,39 @@ STS session policy AbortMultipartUpload must require key
WRITE
${output} = Execute aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} abort-multipart-upload --bucket
${ICEBERG_BUCKET_OBS} --key ${key} --upload-id ${upload_id} --profile sts
Should Not Contain ${output} AccessDenied
+STS session policy GetObjectAttributes must require GetObject and
GetObjectAttributes with key READ
+ ${key_suffix} = Generate Random String 8 [LOWER]
+ ${key} = Set Variable
sts-object-${key_suffix}.txt
+ ${local_path} = Set Variable
${TEMP_DIR}/${key}
+ Create File ${local_path}
get-object-attributes content
+ ${put_policy} = Set Variable
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutObject","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}/${key}"}]}
+ ${get_attrs_policy} = Set Variable
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:GetObject","s3:GetObjectAttributes"],"Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}/${key}"}]}
+ ${get_object_only_policy} = Set Variable
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}/${key}"}]}
+ ${get_attrs_only_policy} = Set Variable
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObjectAttributes","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}/${key}"}]}
+
+ Assume Role And Configure STS Profile
policy_json=${put_policy} perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+ Put Object Should Succeed ${ICEBERG_BUCKET_OBS} ${key} ${local_path}
+
+ # Positive control
+ Assume Role And Configure STS Profile
policy_json=${get_attrs_policy} perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+ ${output} = Execute aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} get-object-attributes --bucket
${ICEBERG_BUCKET_OBS} --key ${key} --object-attributes ETag --profile sts
+ Should Contain ${output} ETag
+
+ # Negative A: session policy with only GetObjectAttributes
+ Assume Role And Configure STS Profile
policy_json=${get_attrs_only_policy}
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+ ${output} = Execute And Ignore Error aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} get-object-attributes --bucket
${ICEBERG_BUCKET_OBS} --key ${key} --object-attributes ETag --profile sts
+ Should Contain ${output} AccessDenied
+
+ # Negative B: session policy with only GetObject
+ Assume Role And Configure STS Profile
policy_json=${get_object_only_policy}
perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
+ ${output} = Execute And Ignore Error aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} get-object-attributes --bucket
${ICEBERG_BUCKET_OBS} --key ${key} --object-attributes ETag --profile sts
+ Should Contain ${output} AccessDenied
+
+ # Negative C: missing key READ
+ Assume Role And Configure STS Profile
policy_json=${get_attrs_policy} perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${PARTIAL_BUCKET_READ_ROLE_ARN}
+ ${output} = Execute And Ignore Error aws s3api
--endpoint-url ${S3G_ENDPOINT_URL} get-object-attributes --bucket
${ICEBERG_BUCKET_OBS} --key ${key} --object-attributes ETag --profile sts
+ Should Contain ${output} AccessDenied
+
STS session policy containing only GetObject must deny GetObjectTagging
${session_policy} = Set Variable
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}/${ICEBERG_BUCKET_TESTFILE}"}]}
Assume Role And Configure STS Profile
policy_json=${session_policy} perm_access_key_id=${PERMANENT_ACCESS_KEY_ID}
perm_secret_key=${PERMANENT_SECRET_KEY}
role_arn=${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
@@ -1446,6 +1631,13 @@ Expired STS temporary credentials must return
ExpiredToken on S3 APIs
Execute S3api Expect Expired Token
get-object-tagging --bucket ${ICEBERG_BUCKET_OBS} --key
${ICEBERG_BUCKET_TESTFILE}
Execute S3api Expect Expired Token
put-object-tagging --bucket ${ICEBERG_BUCKET_OBS} --key
${ICEBERG_BUCKET_TESTFILE} --tagging
'{"TagSet":[{"Key":"tag-key-expired-sts-token","Value":"tag-value-expired-sts-token"}]}'
Execute S3api Expect Expired Token
delete-object-tagging --bucket ${ICEBERG_BUCKET_OBS} --key
${ICEBERG_BUCKET_TESTFILE}
+ Execute S3api Expect Expired Token
get-bucket-tagging --bucket ${ICEBERG_BUCKET_OBS}
+ Execute S3api Expect Expired Token
put-bucket-tagging --bucket ${ICEBERG_BUCKET_OBS} --tagging
'${STS_BUCKET_TAGGING_JSON}'
+ Execute S3api Expect Expired Token
delete-bucket-tagging --bucket ${ICEBERG_BUCKET_OBS}
+ Execute S3api Expect Expired Token
get-bucket-lifecycle-configuration --bucket ${ICEBERG_BUCKET_OBS}
+ Execute S3api Expect Expired Token
put-bucket-lifecycle-configuration --bucket ${ICEBERG_BUCKET_OBS}
--lifecycle-configuration '${STS_LIFECYCLE_JSON}'
+ Execute S3api Expect Expired Token
delete-bucket-lifecycle --bucket ${ICEBERG_BUCKET_OBS}
+ Execute S3api Expect Expired Token
get-object-attributes --bucket ${ICEBERG_BUCKET_OBS} --key
${ICEBERG_BUCKET_TESTFILE} --object-attributes ETag
Revoking Permanent User Must Revoke Existing Session Token
# Create session tokens for both buckets, verify they work, then revoke
permanent user secret and verify both fail.
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 c3121599566..e93219ae64a 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
@@ -29,6 +29,7 @@
import java.io.IOException;
import java.net.InetAddress;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -83,6 +84,13 @@
* from a rocksDb instance, for both the OM and OM snapshots.
*/
public class OmMetadataReader implements IOmMetadataReader, Auditor {
+ /**
+ * S3 actions that additionally require other S3 actions to be authorized
within the same request.
+ * AWS requires both s3:GetObject and s3:GetObjectAttributes for
GetObjectAttributes.
+ */
+ private static final Map<String, List<String>> DEPENDENT_S3_ACTIONS =
+ Collections.singletonMap("GetObjectAttributes",
Collections.singletonList("GetObject"));
+
private final KeyManager keyManager;
private final PrefixManager prefixManager;
private final VolumeManager volumeManager;
@@ -717,7 +725,7 @@ public boolean checkAcls(OzoneObj obj,
RequestContext.Builder contextBuilder,
final RequestContext context = contextBuilder.build();
if (!captureLatencyNs(perfMetrics::setCheckAccessLatencyNs,
- () -> accessAuthorizer.checkAccess(obj, context))) {
+ () -> checkAccessIncludingDependentS3Actions(obj, context))) {
if (throwIfPermissionDenied) {
String volumeName = obj.getVolumeName() != null ?
"Volume:" + obj.getVolumeName() + " " : "";
@@ -754,6 +762,30 @@ public boolean checkAcls(OzoneObj obj,
RequestContext.Builder contextBuilder,
}
}
+ /**
+ * Checks access for the S3 action in the context and then for each S3
action it depends on (see
+ * {@link #DEPENDENT_S3_ACTIONS}), so a single request does not need a
separate RPC per action.
+ * Each {@link IAccessAuthorizer#checkAccess} call carries exactly one S3
action.
+ *
+ * @return true only if access is granted for the S3 action and all of its
dependent S3 actions.
+ */
+ private boolean checkAccessIncludingDependentS3Actions(OzoneObj obj,
RequestContext context)
+ throws OMException {
+ if (!accessAuthorizer.checkAccess(obj, context)) {
+ return false;
+ }
+ final String s3Action = context.getS3Action();
+ if (s3Action == null) {
+ return true;
+ }
+ for (String dependentAction : DEPENDENT_S3_ACTIONS.getOrDefault(s3Action,
Collections.emptyList())) {
+ if (!accessAuthorizer.checkAccess(obj,
context.toBuilder().setS3Action(dependentAction).build())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
/**
* 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
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 99b4c861c19..5defb87760f 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,6 +23,7 @@
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;
@@ -166,6 +167,78 @@ public void testCheckAclsAttachesS3ActionFromThreadLocal()
throws Exception {
verifyS3ActionPassedToAuthorizer(accessAuthorizer, obj, "GetObject");
}
+ @Test
+ public void testCheckAclsAlsoChecksGetObjectForGetObjectAttributes() throws
Exception {
+ OzoneManager.setS3Auth(S3Authentication.newBuilder()
+ .setAccessId(ACCESS_KEY_ID)
+ .setS3Action("GetObjectAttributes")
+ .build());
+
+ final IAccessAuthorizer accessAuthorizer =
createMockIAccessAuthorizerReturningTrue();
+ final OmMetadataReader omMetadataReader =
createMetadataReader(accessAuthorizer);
+ final OzoneObj obj = createTestOzoneObj();
+
+ assertTrue(omMetadataReader.checkAcls(obj,
createTestRequestContextBuilder(), true));
+
+ final ArgumentCaptor<RequestContext> captor =
ArgumentCaptor.forClass(RequestContext.class);
+ verify(accessAuthorizer, times(2)).checkAccess(eq(obj), captor.capture());
+ assertEquals("GetObjectAttributes",
captor.getAllValues().get(0).getS3Action());
+ assertEquals("GetObject", captor.getAllValues().get(1).getS3Action());
+ }
+
+ @Test
+ public void testCheckAclsDeniedWhenDependentGetObjectActionDenied() throws
Exception {
+ OzoneManager.setS3Auth(S3Authentication.newBuilder()
+ .setAccessId(ACCESS_KEY_ID)
+ .setS3Action("GetObjectAttributes")
+ .build());
+
+ final IAccessAuthorizer accessAuthorizer =
createMockIAccessAuthorizerReturningTrue();
+ final OzoneObj obj = createTestOzoneObj();
+ when(accessAuthorizer.checkAccess(eq(obj),
any(RequestContext.class))).thenAnswer(
+ invocation ->
!"GetObject".equals(invocation.<RequestContext>getArgument(1).getS3Action()));
+ final OmMetadataReader omMetadataReader =
createMetadataReader(accessAuthorizer);
+
+ final OMException ex = assertThrows(
+ OMException.class, () -> omMetadataReader.checkAcls(obj,
createTestRequestContextBuilder(), true));
+
+ assertEquals(ResultCodes.PERMISSION_DENIED, ex.getResult());
+ verify(accessAuthorizer, times(2)).checkAccess(eq(obj),
any(RequestContext.class));
+ }
+
+ @Test
+ public void testCheckAclsSkipsDependentActionWhenGetObjectAttributesDenied()
throws Exception {
+ OzoneManager.setS3Auth(S3Authentication.newBuilder()
+ .setAccessId(ACCESS_KEY_ID)
+ .setS3Action("GetObjectAttributes")
+ .build());
+
+ final IAccessAuthorizer accessAuthorizer =
createMockIAccessAuthorizerReturningTrue();
+ final OzoneObj obj = createTestOzoneObj();
+ when(accessAuthorizer.checkAccess(eq(obj),
any(RequestContext.class))).thenReturn(false);
+ final OmMetadataReader omMetadataReader =
createMetadataReader(accessAuthorizer);
+
+ assertFalse(omMetadataReader.checkAcls(obj,
createTestRequestContextBuilder(), false));
+
+ verify(accessAuthorizer).checkAccess(eq(obj), any(RequestContext.class));
+ }
+
+ @Test
+ public void testCheckAclsDoesNotCheckDependentActionWhenStsFeatureDisabled()
throws Exception {
+ OzoneManager.setS3Auth(S3Authentication.newBuilder()
+ .setAccessId(ACCESS_KEY_ID)
+ .setS3Action("GetObjectAttributes")
+ .build());
+
+ final IAccessAuthorizer accessAuthorizer =
createMockIAccessAuthorizerReturningTrue();
+ final OmMetadataReader omMetadataReader =
createMetadataReader(accessAuthorizer, mock(KeyManager.class), false);
+ final OzoneObj obj = createTestOzoneObj();
+
+ assertTrue(omMetadataReader.checkAcls(obj,
createTestRequestContextBuilder(), true));
+
+ verifyS3ActionPassedToAuthorizer(accessAuthorizer, obj, null);
+ }
+
@Test
public void testCheckAclsDoesNotAttachS3ActionWhenStsFeatureDisabled()
throws Exception {
OzoneManager.setS3Auth(S3Authentication.newBuilder()
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 349d3765ad2..b7b02b5fc53 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
@@ -716,6 +716,7 @@ protected void copyDependenciesTo(EndpointBase target) {
}
target.queryParams = queryParams;
target.s3Auth = s3Auth;
+ target.s3StsEnabled = s3StsEnabled;
target.setClient(this.client);
target.setOzoneConfiguration(this.ozoneConfiguration);
target.setContext(this.context);
diff --git
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectAttributesHandler.java
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectAttributesHandler.java
index 45755f22de4..011005223e4 100644
---
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectAttributesHandler.java
+++
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectAttributesHandler.java
@@ -134,6 +134,8 @@ Response handleGetRequest(ObjectRequestContext context,
String keyPath)
NavigableMap<Integer, Long> completedPartSizes = null;
boolean directoryBucketLayout = false;
try {
+ // AWS requires the credentials to support both s3:GetObject and
s3:GetObjectAttributes actions. OM checks
+ // both while authorizing this single request, which is sent with the
GetObjectAttributes action.
if (requestedAttributes.contains(ATTR_OBJECT_PARTS)) {
S3HeadObjectAttributes headAttributes =
getClientProtocol().headS3ObjectAttributes(bucketName, keyPath);
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
index 7615828d422..259325eb8be 100644
---
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
@@ -49,6 +49,16 @@ private S3GActionIamMapper() {
return "GetBucketAcl";
case PUT_ACL:
return "PutBucketAcl";
+ case GET_BUCKET_TAGGING:
+ return "GetBucketTagging";
+ case PUT_BUCKET_TAGGING:
+ case DELETE_BUCKET_TAGGING:
+ return "PutBucketTagging";
+ case GET_BUCKET_LIFECYCLE:
+ return "GetLifecycleConfiguration";
+ case PUT_BUCKET_LIFECYCLE:
+ case DELETE_BUCKET_LIFECYCLE:
+ return "PutLifecycleConfiguration";
case LIST_MULTIPART_UPLOAD:
return "ListBucketMultipartUploads";
case MULTI_DELETE:
@@ -67,6 +77,9 @@ private S3GActionIamMapper() {
case GET_KEY:
case HEAD_KEY:
return "GetObject";
+ case GET_OBJECT_ATTRIBUTES:
+ // OM also enforces the dependent GetObject action when authorizing this
request.
+ return "GetObjectAttributes";
case ABORT_MULTIPART_UPLOAD:
return "AbortMultipartUpload";
case GET_OBJECT_TAGGING:
diff --git
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3ActionOverrideForOwnerVerification.java
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3ActionOverrideForOwnerVerification.java
index c8bf7570cdc..d4f4befbe73 100644
---
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3ActionOverrideForOwnerVerification.java
+++
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3ActionOverrideForOwnerVerification.java
@@ -17,6 +17,7 @@
package org.apache.hadoop.ozone.s3.endpoint;
+import static
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.getObjectAttributes;
import static org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.put;
import static org.apache.hadoop.ozone.s3.util.S3Consts.COPY_SOURCE_HEADER;
import static
org.apache.hadoop.ozone.s3.util.S3Consts.EXPECTED_BUCKET_OWNER_HEADER;
@@ -33,26 +34,40 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.ozone.OzoneConfigKeys;
import org.apache.hadoop.ozone.client.ObjectStore;
import org.apache.hadoop.ozone.client.OzoneBucket;
import org.apache.hadoop.ozone.client.OzoneClient;
+import org.apache.hadoop.ozone.client.OzoneKey;
import org.apache.hadoop.ozone.client.OzoneVolume;
+import org.apache.hadoop.ozone.client.S3HeadObjectAttributes;
import org.apache.hadoop.ozone.client.protocol.ClientProtocol;
import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes;
+import org.apache.hadoop.ozone.om.helpers.BucketLayout;
import org.apache.hadoop.ozone.om.protocol.S3Auth;
+import org.apache.hadoop.ozone.s3.exception.OS3Exception;
+import org.apache.hadoop.ozone.s3.metrics.S3GatewayMetrics;
import org.apache.hadoop.ozone.s3.signature.SignatureInfo;
+import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
/**
- * Verifies bucket-owner-condition verification (source bucket owner lookup)
runs under the correct IAM S3 action
- * string for copy-style operations.
+ * Verifies temporary IAM S3 action overrides used by composite operations.
*/
public class TestS3ActionOverrideForOwnerVerification {
@@ -63,6 +78,11 @@ public class TestS3ActionOverrideForOwnerVerification {
private static final String SOURCE_OWNER = "source-owner";
private static final String DEST_OWNER = "dest-owner";
+ @BeforeAll
+ public static void setUp() {
+ S3GatewayMetrics.create(new OzoneConfiguration());
+ }
+
@Test
public void
testUploadPartCopyUsesGetObjectActionForSourceBucketOwnerLookup() throws
Exception {
final AtomicReference<String> actionAtSourceBucketOwnerLookup = new
AtomicReference<>();
@@ -109,6 +129,60 @@ public void
testCopyObjectDoesNotSetActionWhenStsDisabled() throws Exception {
assertNull(actionAtSourceBucketOwnerLookup.get());
}
+ @ParameterizedTest
+ @ValueSource(strings = {"ETag", "ObjectParts"})
+ public void
testGetObjectAttributesSendsSingleOmRequestWithGetObjectAttributesAction(String
attribute)
+ throws Exception {
+ final List<String> actions = new ArrayList<>();
+ final ClientProtocol clientProtocol = mock(ClientProtocol.class);
+ final ObjectEndpoint endpoint = newGetObjectAttributesEndpoint(actions,
true, clientProtocol, false);
+
+ final Response response = getObjectAttributes(endpoint, DEST_BUCKET,
DEST_KEY, attribute);
+
+ assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+ // OM enforces the dependent GetObject action itself, so S3 Gateway makes
exactly one OM request.
+ assertEquals(Collections.singletonList("GetObjectAttributes"), actions);
+ verifySingleMetadataLookup(clientProtocol, attribute);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"ETag", "ObjectParts"})
+ public void
testGetObjectAttributesSkipsAdditionalCheckWhenStsDisabled(String attribute)
throws Exception {
+ final List<String> actions = new ArrayList<>();
+ final ObjectEndpoint endpoint = newGetObjectAttributesEndpoint(actions,
false);
+
+ final Response response = getObjectAttributes(endpoint, DEST_BUCKET,
DEST_KEY, attribute);
+
+ assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+ // With STS disabled, no S3 action is sent to OM.
+ assertEquals(Collections.singletonList(null), actions);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"ETag", "ObjectParts"})
+ public void
testGetObjectAttributesPermissionDeniedReturnsAccessDenied(String attribute)
throws Exception {
+ final List<String> actions = new ArrayList<>();
+ final ClientProtocol clientProtocol = mock(ClientProtocol.class);
+ final ObjectEndpoint endpoint = newGetObjectAttributesEndpoint(actions,
true, clientProtocol, true);
+
+ final OS3Exception ex = assertThrows(
+ OS3Exception.class, () -> getObjectAttributes(endpoint, DEST_BUCKET,
DEST_KEY, attribute));
+
+ assertEquals("AccessDenied", ex.getCode());
+ assertEquals(Collections.singletonList("GetObjectAttributes"), actions);
+ verifySingleMetadataLookup(clientProtocol, attribute);
+ }
+
+ private static void verifySingleMetadataLookup(ClientProtocol
clientProtocol, String attribute) throws Exception {
+ if ("ObjectParts".equals(attribute)) {
+ verify(clientProtocol).headS3ObjectAttributes(DEST_BUCKET, DEST_KEY);
+ verify(clientProtocol, never()).headS3Object(DEST_BUCKET, DEST_KEY);
+ } else {
+ verify(clientProtocol).headS3Object(DEST_BUCKET, DEST_KEY);
+ verify(clientProtocol, never()).headS3ObjectAttributes(DEST_BUCKET,
DEST_KEY);
+ }
+ }
+
private static ObjectEndpoint newEndpoint(AtomicReference<String>
actionAtSourceBucketOwnerLookup,
boolean isStsEnabled) throws Exception {
final HttpHeaders headers = mock(HttpHeaders.class);
@@ -170,5 +244,70 @@ private static ObjectEndpoint
newEndpoint(AtomicReference<String> actionAtSource
.setSignatureInfo(signatureInfo)
.build();
}
+
+ private static ObjectEndpoint newGetObjectAttributesEndpoint(List<String>
actions, boolean isStsEnabled)
+ throws Exception {
+ return newGetObjectAttributesEndpoint(actions, isStsEnabled,
mock(ClientProtocol.class), false);
+ }
+
+ private static ObjectEndpoint newGetObjectAttributesEndpoint(List<String>
actions, boolean isStsEnabled,
+ ClientProtocol clientProtocol, boolean denyAccess) throws Exception {
+ final HttpHeaders headers = mock(HttpHeaders.class);
+ final SignatureInfo signatureInfo = mock(SignatureInfo.class);
+ when(signatureInfo.isSignPayload()).thenReturn(true);
+ when(signatureInfo.getStringToSign()).thenReturn("string-to-sign");
+ when(signatureInfo.getSignature()).thenReturn("signature");
+ when(signatureInfo.getAwsAccessId()).thenReturn("access-id");
+
+ final OzoneClient client = mock(OzoneClient.class);
+ final ObjectStore objectStore = mock(ObjectStore.class);
+ final AtomicReference<S3Auth> s3AuthRef = new AtomicReference<>();
+ doAnswer(invocationOnMock -> {
+ s3AuthRef.set(invocationOnMock.getArgument(0));
+ return null;
+ }).when(clientProtocol).setThreadLocalS3Auth(any(S3Auth.class));
+
+ when(client.getObjectStore()).thenReturn(objectStore);
+ when(client.getProxy()).thenReturn(clientProtocol);
+ when(objectStore.getClientProxy()).thenReturn(clientProtocol);
+
+ final OzoneKey key = mock(OzoneKey.class);
+ when(key.isFile()).thenReturn(true);
+ when(key.getModificationTime()).thenReturn(Instant.now());
+ final S3HeadObjectAttributes headAttributes =
mock(S3HeadObjectAttributes.class);
+ when(headAttributes.getKey()).thenReturn(key);
+
when(headAttributes.getBucketLayout()).thenReturn(BucketLayout.OBJECT_STORE);
+
+ when(clientProtocol.headS3Object(DEST_BUCKET,
DEST_KEY)).thenAnswer(invocationOnMock -> {
+ recordS3Action(s3AuthRef, actions);
+ return returnOrDeny(key, denyAccess);
+ });
+ when(clientProtocol.headS3ObjectAttributes(DEST_BUCKET,
DEST_KEY)).thenAnswer(invocationOnMock -> {
+ recordS3Action(s3AuthRef, actions);
+ return returnOrDeny(headAttributes, denyAccess);
+ });
+
+ final OzoneConfiguration conf = new OzoneConfiguration();
+ conf.setBoolean(OzoneConfigKeys.OZONE_S3G_STS_HTTP_ENABLED_KEY,
isStsEnabled);
+ return EndpointBuilder.newObjectEndpointBuilder()
+ .setClient(client)
+ .setConfig(conf)
+ .setHeaders(headers)
+ .setSignatureInfo(signatureInfo)
+ .build();
+ }
+
+ private static <T> T returnOrDeny(T result, boolean denyAccess) throws
OMException {
+ if (denyAccess) {
+ throw new OMException("Permission denied",
ResultCodes.PERMISSION_DENIED);
+ }
+ return result;
+ }
+
+ private static void recordS3Action(AtomicReference<S3Auth> s3AuthRef,
List<String> actions) {
+ final S3Auth s3Auth = s3AuthRef.get();
+ assertNotNull(s3Auth, "S3Auth must be initialized before metadata lookup");
+ actions.add(s3Auth.getS3Action());
+ }
}
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
index b2d1625a185..338dcabfe11 100644
---
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
@@ -52,6 +52,13 @@ public void mapsCoreObjectActions() {
assertEquals("DeleteObjectTagging",
S3GActionIamMapper.toS3ActionString(S3GAction.DELETE_OBJECT_TAGGING));
assertEquals("GetObjectAcl",
S3GActionIamMapper.toS3ActionString(S3GAction.GET_OBJECT_ACL));
assertEquals("PutObjectAcl",
S3GActionIamMapper.toS3ActionString(S3GAction.PUT_OBJECT_ACL));
+ assertEquals("GetObjectAttributes",
S3GActionIamMapper.toS3ActionString(S3GAction.GET_OBJECT_ATTRIBUTES));
+ assertEquals("GetBucketTagging",
S3GActionIamMapper.toS3ActionString(S3GAction.GET_BUCKET_TAGGING));
+ assertEquals("PutBucketTagging",
S3GActionIamMapper.toS3ActionString(S3GAction.PUT_BUCKET_TAGGING));
+ assertEquals("PutBucketTagging",
S3GActionIamMapper.toS3ActionString(S3GAction.DELETE_BUCKET_TAGGING));
+ assertEquals("GetLifecycleConfiguration",
S3GActionIamMapper.toS3ActionString(S3GAction.GET_BUCKET_LIFECYCLE));
+ assertEquals("PutLifecycleConfiguration",
S3GActionIamMapper.toS3ActionString(S3GAction.PUT_BUCKET_LIFECYCLE));
+ assertEquals("PutLifecycleConfiguration",
S3GActionIamMapper.toS3ActionString(S3GAction.DELETE_BUCKET_LIFECYCLE));
}
@Test
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]