Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,20 @@ public static Set<AssumeRoleRequest.OzoneGrant> resolve(String policyJson, Strin
continue;
}

// s3:prefix is only applicable to the ListBucket action because we don't support ListBucketVersions
// (see https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazons3.html and search for
// s3:prefix). If a statement carries a Condition, non-ListBucket actions (ex GetObject, PutObject,
// ListBucketMultipartUploads, etc.) in that statement do not apply.
final Set<S3Action> filteredS3Actions = filterActionsWhenConditionPresent(mappedS3Actions, condition);
if (filteredS3Actions.isEmpty()) {
continue;
}

// Categorize resources according to bucket resource, object resource, etc
final Set<ResourceSpec> resourceSpecs = validateAndCategorizeResources(authorizerType, resources);

// For each action, map to Ozone objects (paths) and acls based on resource specs and prefixes
createPathsAndPermissions(volumeName, authorizerType, mappedS3Actions, resourceSpecs, condition, objToAclsMap);
createPathsAndPermissions(volumeName, authorizerType, filteredS3Actions, resourceSpecs, condition, objToAclsMap);
}

// Group accumulated objects by their ACL sets to create final result
Expand Down Expand Up @@ -359,6 +368,23 @@ static Set<S3Action> mapPolicyActionsToS3Actions(Set<String> actions) {
return mappedActions;
}

/**
* Filters out actions when a Condition is present if the action is not ListBucket.
*/
private static Set<S3Action> filterActionsWhenConditionPresent(Set<S3Action> mappedS3Actions, Condition condition) {
if (condition == null) {
return mappedS3Actions;
}

if (mappedS3Actions.contains(S3Action.LIST_BUCKET) || mappedS3Actions.contains(S3Action.ALL_S3)) {
final Set<S3Action> filteredActions = new HashSet<>();
filteredActions.add(S3Action.LIST_BUCKET);
return filteredActions;
}

return Collections.emptySet();
}

/**
* Validates that wildcard bucket patterns are not used with native authorizer.
*/
Expand Down Expand Up @@ -474,7 +500,7 @@ private static void processResourceSpecWithActions(String volumeName, Authorizer
Preconditions.checkArgument(
authorizerType != AuthorizerType.NATIVE,
"ResourceSpec type ANY not supported for OzoneNativeAuthorizer");
processResourceTypeAny(volumeName, mappedS3Actions, objToAclsMap);
processResourceTypeAny(volumeName, authorizerType, mappedS3Actions, condition, objToAclsMap);
break;
case BUCKET:
processBucketResource(volumeName, mappedS3Actions, resourceSpec, condition, authorizerType, objToAclsMap);
Expand Down Expand Up @@ -509,12 +535,24 @@ private static void processResourceSpecWithActions(String volumeName, Authorizer
* Handles ResourceType.ANY (*).
* Example: "Resource": "*"
*/
private static void processResourceTypeAny(String volumeName, Set<S3Action> mappedS3Actions,
Map<IOzoneObj, Set<ACLType>> objToAclsMap) {
private static void processResourceTypeAny(String volumeName, AuthorizerType authorizerType,
Set<S3Action> mappedS3Actions, Condition condition, Map<IOzoneObj, Set<ACLType>> objToAclsMap) {
for (S3Action action : mappedS3Actions) {
addAclsForObj(objToAclsMap, volumeObj(volumeName), action.volumePerms);
addAclsForObj(objToAclsMap, bucketObj(volumeName, "*"), action.bucketPerms);
addAclsForObj(objToAclsMap, keyObj(volumeName, "*", "*"), action.objectPerms);
if (condition != null && condition.prefixes != null && !condition.prefixes.isEmpty() &&
(action == S3Action.LIST_BUCKET || action == S3Action.ALL_S3)) {
for (String prefix : condition.prefixes) {
// If operator is StringEquals, ignore wildcard prefixes - this is AWS behavior
if (STRING_EQUALS.equals(condition.operator) && hasWildcard(prefix)) {
continue;
}
createObjectResourcesFromConditionPrefix(
volumeName, authorizerType, ResourceSpec.any(), prefix, objToAclsMap, EnumSet.of(READ));
}
} else {
addAclsForObj(objToAclsMap, keyObj(volumeName, "*", "*"), action.objectPerms);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1258,6 +1258,30 @@ public void testAllActionsForKey() throws OMException {
assertThat(resolvedFromRangerAuthorizer).isEqualTo(expectedResolvedRanger);
}

@Test
public void testAllActionsForKeyWithPrefixCondition() throws OMException {
final String json = "{\n" +
" \"Statement\": [{\n" +
" \"Effect\": \"Allow\",\n" +
" \"Action\": \"s3:*\",\n" +
" \"Resource\": \"arn:aws:s3:::my-bucket/*\",\n" +
" \"Condition\": {\n" +
" \"StringLike\": {\n" +
" \"s3:prefix\": [ \"team/folder\", \"team/folder/*\" ]\n" +
" }\n" +
" }\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 - only ListBucket supports s3:prefix and that is a bucket action,
// not object action
assertThat(resolvedFromNativeAuthorizer).isEmpty();
assertThat(resolvedFromRangerAuthorizer).isEmpty();
}

@Test
public void testAllActionsForBucket() throws OMException {
final String json = "{\n" +
Expand Down Expand Up @@ -1287,6 +1311,46 @@ public void testAllActionsForBucket() throws OMException {
assertThat(resolvedFromRangerAuthorizer).isEqualTo(expectedResolvedRanger);
}

@Test
public void testAllActionsForBucketWithPrefixCondition() throws OMException {
final String json = "{\n" +
" \"Statement\": [{\n" +
" \"Effect\": \"Allow\",\n" +
" \"Action\": \"s3:*\",\n" +
" \"Resource\": \"arn:aws:s3:::my-bucket\",\n" +
" \"Condition\": {\n" +
" \"StringLike\": {\n" +
" \"s3:prefix\": [ \"team/folder\", \"team/folder/*\" ]\n" +
" }\n" +
" }\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> expectedResolvedNative = new LinkedHashSet<>();
// Expected for native: READ, LIST ACLs for bucket (only ListBucket supports s3:prefix); volume READ;
// prefix "team/folder", "team/folder/" READ
final Set<IOzoneObj> bucketSet = objSet(bucket("my-bucket"));
final Set<ACLType> bucketAcls = acls(READ, LIST);
expectedResolvedNative.add(
new OzoneGrant(objSet(volume(), prefix("my-bucket", "team/folder"), prefix("my-bucket", "team/folder/")),
acls(READ)));
expectedResolvedNative.add(new OzoneGrant(bucketSet, bucketAcls));
assertThat(resolvedFromNativeAuthorizer).isEqualTo(expectedResolvedNative);

// Expected for Ranger: READ, LIST ACLs for bucket (only ListBucket supports s3:prefix); volume READ,
// key "team/folder", "team/folder/*" READ
final Set<OzoneGrant> expectedResolvedRanger = new LinkedHashSet<>();
expectedResolvedRanger.add(
new OzoneGrant(objSet(volume(), key("my-bucket", "team/folder"), key("my-bucket", "team/folder/*")),
acls(READ)));
expectedResolvedRanger.add(new OzoneGrant(bucketSet, bucketAcls));
assertThat(resolvedFromRangerAuthorizer).isEqualTo(expectedResolvedRanger);
}

@Test
public void testMultipleResourcesInSeparateStatements() throws OMException {
final String json = "{\n" +
Expand Down Expand Up @@ -1517,11 +1581,11 @@ public void testIgnoresUnsupportedActionsWhenSupportedActionsAreIncluded() throw
" \"Effect\": \"Allow\",\n" +
" \"Action\": [\n" +
" \"s3:GetAccelerateConfiguration\",\n" + // unsupported action
" \"s3:GetBucketAcl\",\n" +
" \"s3:GetBucketAcl\",\n" + // ignored because it doesn't support s3:prefix condition
" \"s3:GetObject\",\n" + // object-level action not applied for bucket
" \"s3:GetObjectAcl\",\n" + // unsupported action
" \"s3:ListBucket\",\n" +
" \"s3:ListBucketMultipartUploads\"\n" +
" \"s3:ListBucketMultipartUploads\"\n" + // ignored because it doesn't support s3:prefix condition
" ],\n" +
" \"Resource\": \"arn:aws:s3:::bucket1\",\n" +
" \"Condition\": {\n" +
Expand All @@ -1539,16 +1603,16 @@ public void testIgnoresUnsupportedActionsWhenSupportedActionsAreIncluded() throw
// Ensure what we got is what we expected
final Set<OzoneGrant> expectedResolvedNative = new LinkedHashSet<>();

// Expected for native: READ, LIST, READ_ACL bucket acls; volume and prefixes "team/folder", "team/folder/" READ
// Expected for native: READ, LIST bucket acls; volume and prefixes "team/folder", "team/folder/" READ
final Set<IOzoneObj> bucketSet = objSet(bucket("bucket1"));
final Set<ACLType> bucketAcls = acls(READ, LIST, READ_ACL);
final Set<ACLType> bucketAcls = acls(READ, LIST);
expectedResolvedNative.add(new OzoneGrant(bucketSet, bucketAcls));
expectedResolvedNative.add(new OzoneGrant(
objSet(volume(), prefix("bucket1", "team/folder"), prefix("bucket1", "team/folder/")), acls(READ)));
assertThat(resolvedFromNativeAuthorizer).isEqualTo(expectedResolvedNative);

final Set<OzoneGrant> expectedResolvedRanger = new LinkedHashSet<>();
// Expected for Ranger: READ, LIST, READ_ACL bucket acls; volume and keys "team/folder" and "team/folder/*" READ
// Expected for Ranger: READ, LIST bucket acls; volume and keys "team/folder" and "team/folder/*" READ
expectedResolvedRanger.add(new OzoneGrant(bucketSet, bucketAcls));
expectedResolvedRanger.add(new OzoneGrant(
objSet(volume(), key("bucket1", "team/folder"), key("bucket1", "team/folder/*")), acls(READ)));
Expand All @@ -1569,17 +1633,37 @@ public void testMultiplePrefixesWithWildcards() throws OMException {
final Set<OzoneGrant> resolvedFromNativeAuthorizer = resolve(json, VOLUME, NATIVE);
final Set<OzoneGrant> resolvedFromRangerAuthorizer = resolve(json, VOLUME, RANGER);

// Ensure what we got is what we expected
// s3:prefix conditions do not apply to object actions like s3:GetObject.
assertThat(resolvedFromNativeAuthorizer).isEmpty();
assertThat(resolvedFromRangerAuthorizer).isEmpty();
}

@Test
public void testListAndGetWithPrefixConditionSkipsObjectAction() throws OMException {
final String json = "{\n" +
" \"Statement\": [{\n" +
" \"Effect\": \"Allow\",\n" +
" \"Action\": [\"s3:ListBucket\", \"s3:GetObject\"],\n" +
" \"Resource\": [\"arn:aws:s3:::logs\", \"arn:aws:s3:::logs/*\"],\n" +
" \"Condition\": { \"StringLike\": { \"s3:prefix\": \"team/*\" } }\n" +
" }]\n" +
"}";

final Set<OzoneGrant> resolvedFromNativeAuthorizer = resolve(json, VOLUME, NATIVE);
final Set<OzoneGrant> resolvedFromRangerAuthorizer = resolve(json, VOLUME, RANGER);

// Expected for native (GetObject is ignored because s3:prefix is present): READ, LIST bucket acls; volume READ;
// prefix "log/team" READ
final Set<OzoneGrant> expectedResolvedNative = new LinkedHashSet<>();
// Expected for native: READ acl on prefix "" (condition prefixes are ignored); bucket READ; volume READ;
final Set<IOzoneObj> readObjectsNative = objSet(prefix("logs", ""), bucket("logs"), volume());
expectedResolvedNative.add(new OzoneGrant(readObjectsNative, acls(READ)));
expectedResolvedNative.add(new OzoneGrant(objSet(bucket("logs")), acls(READ, LIST)));
expectedResolvedNative.add(new OzoneGrant(objSet(volume(), prefix("logs", "team/")), acls(READ)));
assertThat(resolvedFromNativeAuthorizer).isEqualTo(expectedResolvedNative);

// Expected for Ranger (GetObject is ignored because s3:prefix is present): READ, LIST bucket acls; volume READ;
// key "log/team/*" READ
final Set<OzoneGrant> expectedResolvedRanger = new LinkedHashSet<>();
// Expected for Ranger: READ acl on key "*" (condition prefixes are ignored)
final Set<IOzoneObj> keySet = objSet(key("logs", "*"), bucket("logs"), volume());
expectedResolvedRanger.add(new OzoneGrant(keySet, acls(READ)));
expectedResolvedRanger.add(new OzoneGrant(objSet(bucket("logs")), acls(READ, LIST)));
expectedResolvedRanger.add(new OzoneGrant(objSet(volume(), key("logs", "team/*")), acls(READ)));
assertThat(resolvedFromRangerAuthorizer).isEqualTo(expectedResolvedRanger);
}

Expand Down Expand Up @@ -1701,6 +1785,35 @@ public void testObjectActionOnAllResources() throws OMException {
assertThat(resolvedFromRangerAuthorizer).isEqualTo(expectedResolvedRanger);
}

@Test
public void testAllActionsOnAllResourcesWithPrefixCondition() throws OMException {
final String json = "{\n" +
" \"Statement\": [{\n" +
" \"Effect\": \"Allow\",\n" +
" \"Action\": \"s3:*\",\n" +
" \"Resource\": \"*\",\n" +
" \"Condition\": {\n" +
" \"StringLike\": {\n" +
" \"s3:prefix\": [ \"team/folder\", \"team/folder/*\" ]\n" +
" }\n" +
" }\n" +
" }]\n" +
"}";

// Wildcards on bucket are not supported for Native authorizer
expectBucketWildcardUnsupportedExceptionForNativeAuthorizer(json);

final Set<OzoneGrant> resolvedFromRangerAuthorizer = resolve(json, VOLUME, RANGER);
// Ensure what we got is what we expected
final Set<OzoneGrant> expectedResolvedRanger = new LinkedHashSet<>();
// Expected for Ranger: (only ListBucket supports s3:prefix) READ volume; READ, LIST acl on bucket;
// READ on key "team/folder", "team/folder/*"
expectedResolvedRanger.add(new OzoneGrant(objSet(bucket("*")), acls(READ, LIST)));
expectedResolvedRanger.add(
new OzoneGrant(objSet(volume(), key("*", "team/folder"), key("*", "team/folder/*")), acls(READ)));
assertThat(resolvedFromRangerAuthorizer).isEqualTo(expectedResolvedRanger);
}

@Test
public void testAllActionsOnAllResources() throws OMException {
final String json = "{\n" +
Expand Down
Loading