forked from TrueCloudLab/policy-engine
[#58] iam: Support more s3 actions
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
This commit is contained in:
parent
9040e48504
commit
1d51f2121d
5 changed files with 485 additions and 214 deletions
|
@ -11,6 +11,57 @@ import (
|
||||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
s3ActionAbortMultipartUpload = "s3:AbortMultipartUpload"
|
||||||
|
s3ActionCreateBucket = "s3:CreateBucket"
|
||||||
|
s3ActionDeleteBucket = "s3:DeleteBucket"
|
||||||
|
s3ActionDeleteBucketPolicy = "s3:DeleteBucketPolicy"
|
||||||
|
s3ActionDeleteObject = "s3:DeleteObject"
|
||||||
|
s3ActionDeleteObjectTagging = "s3:DeleteObjectTagging"
|
||||||
|
s3ActionDeleteObjectVersion = "s3:DeleteObjectVersion"
|
||||||
|
s3ActionDeleteObjectVersionTagging = "s3:DeleteObjectVersionTagging"
|
||||||
|
s3ActionGetBucketACL = "s3:GetBucketAcl"
|
||||||
|
s3ActionGetBucketCORS = "s3:GetBucketCORS"
|
||||||
|
s3ActionGetBucketLocation = "s3:GetBucketLocation"
|
||||||
|
s3ActionGetBucketNotification = "s3:GetBucketNotification"
|
||||||
|
s3ActionGetBucketObjectLockConfiguration = "s3:GetBucketObjectLockConfiguration"
|
||||||
|
s3ActionGetBucketPolicy = "s3:GetBucketPolicy"
|
||||||
|
s3ActionGetBucketPolicyStatus = "s3:GetBucketPolicyStatus"
|
||||||
|
s3ActionGetBucketTagging = "s3:GetBucketTagging"
|
||||||
|
s3ActionGetBucketVersioning = "s3:GetBucketVersioning"
|
||||||
|
s3ActionGetLifecycleConfiguration = "s3:GetLifecycleConfiguration"
|
||||||
|
s3ActionGetObject = "s3:GetObject"
|
||||||
|
s3ActionGetObjectACL = "s3:GetObjectAcl"
|
||||||
|
s3ActionGetObjectAttributes = "s3:GetObjectAttributes"
|
||||||
|
s3ActionGetObjectLegalHold = "s3:GetObjectLegalHold"
|
||||||
|
s3ActionGetObjectRetention = "s3:GetObjectRetention"
|
||||||
|
s3ActionGetObjectTagging = "s3:GetObjectTagging"
|
||||||
|
s3ActionGetObjectVersion = "s3:GetObjectVersion"
|
||||||
|
s3ActionGetObjectVersionACL = "s3:GetObjectVersionAcl"
|
||||||
|
s3ActionGetObjectVersionAttributes = "s3:GetObjectVersionAttributes"
|
||||||
|
s3ActionGetObjectVersionTagging = "s3:GetObjectVersionTagging"
|
||||||
|
s3ActionListAllMyBuckets = "s3:ListAllMyBuckets"
|
||||||
|
s3ActionListBucket = "s3:ListBucket"
|
||||||
|
s3ActionListBucketMultipartUploads = "s3:ListBucketMultipartUploads"
|
||||||
|
s3ActionListBucketVersions = "s3:ListBucketVersions"
|
||||||
|
s3ActionListMultipartUploadParts = "s3:ListMultipartUploadParts"
|
||||||
|
s3ActionPutBucketACL = "s3:PutBucketAcl"
|
||||||
|
s3ActionPutBucketCORS = "s3:PutBucketCORS"
|
||||||
|
s3ActionPutBucketNotification = "s3:PutBucketNotification"
|
||||||
|
s3ActionPutBucketObjectLockConfiguration = "s3:PutBucketObjectLockConfiguration"
|
||||||
|
s3ActionPutBucketPolicy = "s3:PutBucketPolicy"
|
||||||
|
s3ActionPutBucketTagging = "s3:PutBucketTagging"
|
||||||
|
s3ActionPutBucketVersioning = "s3:PutBucketVersioning"
|
||||||
|
s3ActionPutLifecycleConfiguration = "s3:PutLifecycleConfiguration"
|
||||||
|
s3ActionPutObject = "s3:PutObject"
|
||||||
|
s3ActionPutObjectACL = "s3:PutObjectAcl"
|
||||||
|
s3ActionPutObjectLegalHold = "s3:PutObjectLegalHold"
|
||||||
|
s3ActionPutObjectRetention = "s3:PutObjectRetention"
|
||||||
|
s3ActionPutObjectTagging = "s3:PutObjectTagging"
|
||||||
|
s3ActionPutObjectVersionACL = "s3:PutObjectVersionAcl"
|
||||||
|
s3ActionPutObjectVersionTagging = "s3:PutObjectVersionTagging"
|
||||||
|
)
|
||||||
|
|
||||||
const condKeyAWSPrincipalARN = "aws:PrincipalArn"
|
const condKeyAWSPrincipalARN = "aws:PrincipalArn"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -269,21 +320,18 @@ func validateResource(resource string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateAction(action string) error {
|
func validateAction(action string) (bool, error) {
|
||||||
if action == Wildcard {
|
isIAM := strings.HasPrefix(action, iamActionPrefix)
|
||||||
return nil
|
if !strings.HasPrefix(action, s3ActionPrefix) && !isIAM {
|
||||||
}
|
return false, ErrInvalidActionFormat
|
||||||
|
|
||||||
if !strings.HasPrefix(action, s3ActionPrefix) && !strings.HasPrefix(action, iamActionPrefix) {
|
|
||||||
return ErrInvalidActionFormat
|
|
||||||
}
|
}
|
||||||
|
|
||||||
index := strings.IndexByte(action, Wildcard[0])
|
index := strings.IndexByte(action, Wildcard[0])
|
||||||
if index != -1 && index != utf8.RuneCountInString(action)-1 {
|
if index != -1 && index != utf8.RuneCountInString(action)-1 {
|
||||||
return ErrInvalidActionFormat
|
return false, ErrInvalidActionFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return isIAM, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func splitGroupedConditions(groupedConditions []GroupedConditions) [][]chain.Condition {
|
func splitGroupedConditions(groupedConditions []GroupedConditions) [][]chain.Condition {
|
||||||
|
|
|
@ -10,18 +10,55 @@ import (
|
||||||
|
|
||||||
const PropertyKeyFilePath = "FilePath"
|
const PropertyKeyFilePath = "FilePath"
|
||||||
|
|
||||||
var supportedActionToNativeOpMap = map[string][]string{
|
var actionToNativeOpMap = map[string][]string{
|
||||||
supportedS3NativeActionDeleteObject: {native.MethodDeleteObject, native.MethodHeadObject},
|
s3ActionAbortMultipartUpload: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodHeadObject},
|
||||||
supportedS3NativeActionHeadObject: {native.MethodHeadObject},
|
s3ActionCreateBucket: {native.MethodGetContainer, native.MethodPutContainer, native.MethodSetContainerEACL},
|
||||||
supportedS3NativeActionGetObject: {native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
|
s3ActionDeleteBucket: {native.MethodGetContainer, native.MethodDeleteContainer, native.MethodSearchObject, native.MethodHeadObject},
|
||||||
supportedS3NativeActionPutObject: {native.MethodPutObject},
|
s3ActionDeleteBucketPolicy: {native.MethodGetContainer},
|
||||||
supportedS3NativeActionListBucket: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
|
s3ActionDeleteObject: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodHeadObject},
|
||||||
|
s3ActionDeleteObjectTagging: {native.MethodGetContainer, native.MethodHeadObject},
|
||||||
supportedS3NativeActionCreateBucket: {native.MethodPutContainer},
|
s3ActionDeleteObjectVersion: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodHeadObject},
|
||||||
supportedS3NativeActionDeleteBucket: {native.MethodDeleteContainer},
|
s3ActionDeleteObjectVersionTagging: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodHeadObject},
|
||||||
supportedS3NativeActionListAllMyBucket: {native.MethodListContainers},
|
s3ActionGetBucketACL: {native.MethodGetContainer, native.MethodGetContainerEACL},
|
||||||
supportedS3NativeActionPutBucketACL: {native.MethodSetContainerEACL},
|
s3ActionGetBucketCORS: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject},
|
||||||
supportedS3NativeActionGetBucketACL: {native.MethodGetContainerEACL},
|
s3ActionGetBucketLocation: {native.MethodGetContainer},
|
||||||
|
s3ActionGetBucketNotification: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject},
|
||||||
|
s3ActionGetBucketObjectLockConfiguration: {native.MethodGetContainer},
|
||||||
|
s3ActionGetBucketPolicy: {native.MethodGetContainer},
|
||||||
|
s3ActionGetBucketPolicyStatus: {native.MethodGetContainer},
|
||||||
|
s3ActionGetBucketTagging: {native.MethodGetContainer},
|
||||||
|
s3ActionGetBucketVersioning: {native.MethodGetContainer},
|
||||||
|
s3ActionGetLifecycleConfiguration: { /*not implemented yet*/ },
|
||||||
|
s3ActionGetObject: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
|
||||||
|
s3ActionGetObjectACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodGetObject, native.MethodHeadObject},
|
||||||
|
s3ActionGetObjectAttributes: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject},
|
||||||
|
s3ActionGetObjectLegalHold: {native.MethodGetContainer, native.MethodHeadObject},
|
||||||
|
s3ActionGetObjectRetention: {native.MethodGetContainer, native.MethodHeadObject},
|
||||||
|
s3ActionGetObjectTagging: {native.MethodGetContainer, native.MethodHeadObject},
|
||||||
|
s3ActionGetObjectVersion: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
|
||||||
|
s3ActionGetObjectVersionACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodGetObject, native.MethodHeadObject},
|
||||||
|
s3ActionGetObjectVersionAttributes: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject},
|
||||||
|
s3ActionGetObjectVersionTagging: {native.MethodGetContainer, native.MethodHeadObject},
|
||||||
|
s3ActionListAllMyBuckets: {native.MethodListContainers, native.MethodGetContainer},
|
||||||
|
s3ActionListBucket: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
|
||||||
|
s3ActionListBucketMultipartUploads: {native.MethodGetContainer},
|
||||||
|
s3ActionListBucketVersions: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
|
||||||
|
s3ActionListMultipartUploadParts: {native.MethodGetContainer},
|
||||||
|
s3ActionPutBucketACL: {native.MethodGetContainer, native.MethodSetContainerEACL},
|
||||||
|
s3ActionPutBucketCORS: {native.MethodGetContainer},
|
||||||
|
s3ActionPutBucketNotification: {native.MethodGetContainer, native.MethodHeadObject, native.MethodDeleteObject, native.MethodHeadObject},
|
||||||
|
s3ActionPutBucketObjectLockConfiguration: {native.MethodGetContainer},
|
||||||
|
s3ActionPutBucketPolicy: {native.MethodGetContainer},
|
||||||
|
s3ActionPutBucketTagging: {native.MethodGetContainer},
|
||||||
|
s3ActionPutBucketVersioning: {native.MethodGetContainer},
|
||||||
|
s3ActionPutLifecycleConfiguration: { /*not implemented yet*/ },
|
||||||
|
s3ActionPutObject: {native.MethodGetContainer, native.MethodPutObject},
|
||||||
|
s3ActionPutObjectACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodHeadObject},
|
||||||
|
s3ActionPutObjectLegalHold: {native.MethodGetContainer, native.MethodHeadObject},
|
||||||
|
s3ActionPutObjectRetention: {native.MethodGetContainer, native.MethodHeadObject},
|
||||||
|
s3ActionPutObjectTagging: {native.MethodGetContainer, native.MethodHeadObject},
|
||||||
|
s3ActionPutObjectVersionACL: {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodHeadObject},
|
||||||
|
s3ActionPutObjectVersionTagging: {native.MethodGetContainer, native.MethodHeadObject},
|
||||||
}
|
}
|
||||||
|
|
||||||
var containerNativeOperations = map[string]struct{}{
|
var containerNativeOperations = map[string]struct{}{
|
||||||
|
@ -43,20 +80,6 @@ var objectNativeOperations = map[string]struct{}{
|
||||||
native.MethodHashObject: {},
|
native.MethodHashObject: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
supportedS3NativeActionDeleteObject = "s3:DeleteObject"
|
|
||||||
supportedS3NativeActionGetObject = "s3:GetObject"
|
|
||||||
supportedS3NativeActionHeadObject = "s3:HeadObject"
|
|
||||||
supportedS3NativeActionPutObject = "s3:PutObject"
|
|
||||||
supportedS3NativeActionListBucket = "s3:ListBucket"
|
|
||||||
|
|
||||||
supportedS3NativeActionCreateBucket = "s3:CreateBucket"
|
|
||||||
supportedS3NativeActionDeleteBucket = "s3:DeleteBucket"
|
|
||||||
supportedS3NativeActionListAllMyBucket = "s3:ListAllMyBuckets"
|
|
||||||
supportedS3NativeActionPutBucketACL = "s3:PutBucketAcl"
|
|
||||||
supportedS3NativeActionGetBucketACL = "s3:GetBucketAcl"
|
|
||||||
)
|
|
||||||
|
|
||||||
type NativeResolver interface {
|
type NativeResolver interface {
|
||||||
GetUserKey(account, name string) (string, error)
|
GetUserKey(account, name string) (string, error)
|
||||||
GetBucketInfo(bucket string) (*BucketInfo, error)
|
GetBucketInfo(bucket string) (*BucketInfo, error)
|
||||||
|
@ -76,6 +99,11 @@ func ConvertToNativeChain(p Policy, resolver NativeResolver) (*chain.Chain, erro
|
||||||
|
|
||||||
for _, statement := range p.Statement {
|
for _, statement := range p.Statement {
|
||||||
status := formStatus(statement)
|
status := formStatus(statement)
|
||||||
|
if status != chain.Allow {
|
||||||
|
// Most s3 methods share the same native operations. Deny rules must not affect shared native operations,
|
||||||
|
// therefore this code skips all deny rules for native protocol. Deny is applied for s3 protocol only, in this case.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
action, actionInverted := statement.action()
|
action, actionInverted := statement.action()
|
||||||
nativeActions, err := formNativeActionNames(action)
|
nativeActions, err := formNativeActionNames(action)
|
||||||
|
@ -146,8 +174,8 @@ func getActionTypes(nativeActions []string) ActionTypes {
|
||||||
_, isObj := objectNativeOperations[action]
|
_, isObj := objectNativeOperations[action]
|
||||||
_, isCnr := containerNativeOperations[action]
|
_, isCnr := containerNativeOperations[action]
|
||||||
|
|
||||||
res.Object = isObj || action == Wildcard
|
res.Object = res.Object || isObj || action == Wildcard
|
||||||
res.Container = isCnr || action == Wildcard
|
res.Container = res.Container || isCnr || action == Wildcard
|
||||||
}
|
}
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
@ -222,7 +250,7 @@ func formNativeResourceNamesAndConditions(names []string, resolver NativeResolve
|
||||||
|
|
||||||
res := make([]GroupedResources, 0, len(names))
|
res := make([]GroupedResources, 0, len(names))
|
||||||
|
|
||||||
var combined []string
|
combined := make(map[string]struct{})
|
||||||
|
|
||||||
for _, resource := range names {
|
for _, resource := range names {
|
||||||
if err := validateResource(resource); err != nil {
|
if err := validateResource(resource); err != nil {
|
||||||
|
@ -261,16 +289,20 @@ func formNativeResourceNamesAndConditions(names []string, resolver NativeResolve
|
||||||
}
|
}
|
||||||
|
|
||||||
if obj == Wildcard && actionTypes.Object { // this corresponds to arn:aws:s3:::BUCKET/ or arn:aws:s3:::BUCKET/*
|
if obj == Wildcard && actionTypes.Object { // this corresponds to arn:aws:s3:::BUCKET/ or arn:aws:s3:::BUCKET/*
|
||||||
combined = append(combined, fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container))
|
combined[fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container)] = struct{}{}
|
||||||
|
combined[fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container)] = struct{}{}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if obj == "" && actionTypes.Container { // this corresponds to arn:aws:s3:::BUCKET
|
if obj == "" && actionTypes.Container { // this corresponds to arn:aws:s3:::BUCKET
|
||||||
combined = append(combined, fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container))
|
combined[fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container)] = struct{}{}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
res = append(res, GroupedResources{
|
res = append(res, GroupedResources{
|
||||||
Names: []string{fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container)},
|
Names: []string{
|
||||||
|
fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container),
|
||||||
|
fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container),
|
||||||
|
},
|
||||||
Conditions: []chain.Condition{
|
Conditions: []chain.Condition{
|
||||||
{
|
{
|
||||||
Op: chain.CondStringLike,
|
Op: chain.CondStringLike,
|
||||||
|
@ -283,7 +315,12 @@ func formNativeResourceNamesAndConditions(names []string, resolver NativeResolve
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(combined) != 0 {
|
if len(combined) != 0 {
|
||||||
res = append(res, GroupedResources{Names: combined})
|
gr := GroupedResources{Names: make([]string, 0, len(combined))}
|
||||||
|
for key := range combined {
|
||||||
|
gr.Names = append(gr.Names, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
res = append(res, gr)
|
||||||
}
|
}
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
|
@ -329,26 +366,39 @@ func formPrincipalKey(principal string, resolver NativeResolver) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func formNativeActionNames(names []string) ([]string, error) {
|
func formNativeActionNames(names []string) ([]string, error) {
|
||||||
res := make([]string, 0, len(names))
|
uniqueActions := make(map[string]struct{}, len(names))
|
||||||
|
|
||||||
for _, action := range names {
|
for _, action := range names {
|
||||||
if err := validateAction(action); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if action == Wildcard {
|
if action == Wildcard {
|
||||||
return []string{Wildcard}, nil
|
return []string{Wildcard}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(action, s3ActionPrefix) {
|
isIAM, err := validateAction(action)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if isIAM {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.TrimPrefix(action, s3ActionPrefix) == Wildcard {
|
if action[len(s3ActionPrefix):] == Wildcard {
|
||||||
return []string{Wildcard}, nil
|
return []string{Wildcard}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
res = append(res, supportedActionToNativeOpMap[action]...)
|
nativeActions := actionToNativeOpMap[action]
|
||||||
|
if len(nativeActions) == 0 {
|
||||||
|
return nil, ErrActionsNotApplicable
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, nativeAction := range nativeActions {
|
||||||
|
uniqueActions[nativeAction] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res := make([]string, 0, len(uniqueActions))
|
||||||
|
for key := range uniqueActions {
|
||||||
|
res = append(res, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
|
|
|
@ -7,76 +7,62 @@ import (
|
||||||
"git.frostfs.info/TrueCloudLab/policy-engine/schema/s3"
|
"git.frostfs.info/TrueCloudLab/policy-engine/schema/s3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var specialActionToS3OpMap = map[string][]string{
|
var actionToS3OpMap = map[string][]string{
|
||||||
specialS3ActionsListAllMyBuckets: {"s3:ListBuckets"},
|
s3ActionAbortMultipartUpload: {s3ActionAbortMultipartUpload},
|
||||||
specialS3ActionsListBucket: {"s3:HeadBucket", "s3:GetBucketLocation", "s3:ListObjectsV1", "s3:ListObjectsV2"},
|
s3ActionCreateBucket: {s3ActionCreateBucket},
|
||||||
specialS3ActionsListBucketVersions: {"s3:ListBucketObjectVersions"},
|
s3ActionDeleteBucket: {s3ActionDeleteBucket},
|
||||||
specialS3ActionsListBucketMultipartUploads: {"s3:ListMultipartUploads"},
|
s3ActionDeleteBucketPolicy: {s3ActionDeleteBucketPolicy},
|
||||||
specialS3ActionsGetBucketObjectLockConfiguration: {"s3:GetBucketObjectLockConfig"},
|
s3ActionDeleteObjectTagging: {s3ActionDeleteObjectTagging},
|
||||||
specialS3ActionsGetEncryptionConfiguration: {"s3:GetBucketEncryption"},
|
s3ActionGetBucketLocation: {s3ActionGetBucketLocation},
|
||||||
specialS3ActionsGetLifecycleConfiguration: {"s3:GetBucketLifecycle"},
|
s3ActionGetBucketNotification: {s3ActionGetBucketNotification},
|
||||||
specialS3ActionsGetBucketACL: {"s3:GetBucketACL"},
|
s3ActionGetBucketPolicy: {s3ActionGetBucketPolicy},
|
||||||
specialS3ActionsGetBucketCORS: {"s3:GetBucketCors"},
|
s3ActionGetBucketPolicyStatus: {s3ActionGetBucketPolicyStatus},
|
||||||
specialS3ActionsPutBucketTagging: {"s3:PutBucketTagging", "s3:DeleteBucketTagging"},
|
s3ActionGetBucketTagging: {s3ActionGetBucketTagging},
|
||||||
specialS3ActionsPutBucketObjectLockConfiguration: {"s3:PutBucketObjectLockConfig"},
|
s3ActionGetBucketVersioning: {s3ActionGetBucketVersioning},
|
||||||
specialS3ActionsPutEncryptionConfiguration: {"s3:PutBucketEncryption", "s3:DeleteBucketEncryption"},
|
s3ActionGetObjectAttributes: {s3ActionGetObjectAttributes},
|
||||||
specialS3ActionsPutLifecycleConfiguration: {"s3:PutBucketLifecycle", "s3:DeleteBucketLifecycle"},
|
s3ActionGetObjectLegalHold: {s3ActionGetObjectLegalHold},
|
||||||
specialS3ActionsPutBucketACL: {"s3:PutBucketACL"},
|
s3ActionGetObjectRetention: {s3ActionGetObjectRetention},
|
||||||
specialS3ActionsPutBucketCORS: {"s3:PutBucketCors", "s3:DeleteBucketCors"},
|
s3ActionGetObjectTagging: {s3ActionGetObjectTagging},
|
||||||
specialS3ActionsDeleteBucketCORS: {"s3:DeleteBucketCors"},
|
s3ActionPutBucketNotification: {s3ActionPutBucketNotification},
|
||||||
|
s3ActionPutBucketPolicy: {s3ActionPutBucketPolicy},
|
||||||
|
s3ActionPutBucketVersioning: {s3ActionPutBucketVersioning},
|
||||||
|
s3ActionPutObjectLegalHold: {s3ActionPutObjectLegalHold},
|
||||||
|
s3ActionPutObjectRetention: {s3ActionPutObjectRetention},
|
||||||
|
s3ActionPutObjectTagging: {s3ActionPutObjectTagging},
|
||||||
|
|
||||||
specialS3ActionsListMultipartUploadParts: {"s3:ListParts"},
|
s3ActionListAllMyBuckets: {"s3:ListBuckets"},
|
||||||
specialS3ActionsGetObjectACL: {"s3:GetObjectACL"},
|
s3ActionListBucket: {"s3:HeadBucket", "s3:GetBucketLocation", "s3:ListObjectsV1", "s3:ListObjectsV2"},
|
||||||
specialS3ActionsGetObject: {"s3:GetObject", "s3:HeadObject"},
|
s3ActionListBucketVersions: {"s3:ListBucketObjectVersions"},
|
||||||
specialS3ActionsGetObjectVersion: {"s3:GetObject", "s3:HeadObject"},
|
s3ActionListBucketMultipartUploads: {"s3:ListMultipartUploads"},
|
||||||
specialS3ActionsGetObjectVersionACL: {"s3:GetObjectACL"},
|
s3ActionGetBucketObjectLockConfiguration: {"s3:GetBucketObjectLockConfig"},
|
||||||
specialS3ActionsGetObjectVersionAttributes: {"s3:GetObjectAttributes"},
|
s3ActionGetLifecycleConfiguration: {"s3:GetBucketLifecycle"},
|
||||||
specialS3ActionsGetObjectVersionTagging: {"s3:GetObjectTagging"},
|
s3ActionGetBucketACL: {"s3:GetBucketACL"},
|
||||||
specialS3ActionsPutObjectACL: {"s3:PutObjectACL"},
|
s3ActionGetBucketCORS: {"s3:GetBucketCors"},
|
||||||
specialS3ActionsPutObjectVersionACL: {"s3:PutObjectACL"},
|
s3ActionPutBucketTagging: {"s3:PutBucketTagging", "s3:DeleteBucketTagging"},
|
||||||
specialS3ActionsPutObjectVersionTagging: {"s3:PutObjectTagging"},
|
s3ActionPutBucketObjectLockConfiguration: {"s3:PutBucketObjectLockConfig"},
|
||||||
specialS3ActionsPutObject: {
|
s3ActionPutLifecycleConfiguration: {"s3:PutBucketLifecycle", "s3:DeleteBucketLifecycle"},
|
||||||
|
s3ActionPutBucketACL: {"s3:PutBucketACL"},
|
||||||
|
s3ActionPutBucketCORS: {"s3:PutBucketCors", "s3:DeleteBucketCors"},
|
||||||
|
|
||||||
|
s3ActionListMultipartUploadParts: {"s3:ListParts"},
|
||||||
|
s3ActionGetObjectACL: {"s3:GetObjectACL"},
|
||||||
|
s3ActionGetObject: {"s3:GetObject", "s3:HeadObject"},
|
||||||
|
s3ActionGetObjectVersion: {"s3:GetObject", "s3:HeadObject"},
|
||||||
|
s3ActionGetObjectVersionACL: {"s3:GetObjectACL"},
|
||||||
|
s3ActionGetObjectVersionAttributes: {"s3:GetObjectAttributes"},
|
||||||
|
s3ActionGetObjectVersionTagging: {"s3:GetObjectTagging"},
|
||||||
|
s3ActionPutObjectACL: {"s3:PutObjectACL"},
|
||||||
|
s3ActionPutObjectVersionACL: {"s3:PutObjectACL"},
|
||||||
|
s3ActionPutObjectVersionTagging: {"s3:PutObjectTagging"},
|
||||||
|
s3ActionPutObject: {
|
||||||
"s3:PutObject", "s3:PostObject", "s3:CopyObject",
|
"s3:PutObject", "s3:PostObject", "s3:CopyObject",
|
||||||
"s3:UploadPart", "s3:UploadPartCopy", "s3:CreateMultipartUpload", "s3:CompleteMultipartUpload",
|
"s3:UploadPart", "s3:UploadPartCopy", "s3:CreateMultipartUpload", "s3:CompleteMultipartUpload",
|
||||||
},
|
},
|
||||||
specialS3ActionsDeleteObjectVersionTagging: {"s3:DeleteObjectTagging"},
|
s3ActionDeleteObjectVersionTagging: {"s3:DeleteObjectTagging"},
|
||||||
specialS3ActionsDeleteObject: {"s3:DeleteObject", "s3:DeleteMultipleObjects"},
|
s3ActionDeleteObject: {"s3:DeleteObject", "s3:DeleteMultipleObjects"},
|
||||||
specialS3ActionsDeleteObjectVersion: {"s3:DeleteObject", "s3:DeleteMultipleObjects"},
|
s3ActionDeleteObjectVersion: {"s3:DeleteObject", "s3:DeleteMultipleObjects"},
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
specialS3ActionsListAllMyBuckets = "s3:ListAllMyBuckets"
|
|
||||||
specialS3ActionsListBucket = "s3:ListBucket"
|
|
||||||
specialS3ActionsListBucketVersions = "s3:ListBucketVersions"
|
|
||||||
specialS3ActionsListBucketMultipartUploads = "s3:ListBucketMultipartUploads"
|
|
||||||
specialS3ActionsGetBucketObjectLockConfiguration = "s3:GetBucketObjectLockConfiguration"
|
|
||||||
specialS3ActionsGetEncryptionConfiguration = "s3:GetEncryptionConfiguration"
|
|
||||||
specialS3ActionsGetLifecycleConfiguration = "s3:GetLifecycleConfiguration"
|
|
||||||
specialS3ActionsGetBucketACL = "s3:GetBucketAcl"
|
|
||||||
specialS3ActionsGetBucketCORS = "s3:GetBucketCORS"
|
|
||||||
specialS3ActionsPutBucketTagging = "s3:PutBucketTagging"
|
|
||||||
specialS3ActionsPutBucketObjectLockConfiguration = "s3:PutBucketObjectLockConfiguration"
|
|
||||||
specialS3ActionsPutEncryptionConfiguration = "s3:PutEncryptionConfiguration"
|
|
||||||
specialS3ActionsPutLifecycleConfiguration = "s3:PutLifecycleConfiguration"
|
|
||||||
specialS3ActionsPutBucketACL = "s3:PutBucketAcl"
|
|
||||||
specialS3ActionsPutBucketCORS = "s3:PutBucketCORS"
|
|
||||||
specialS3ActionsDeleteBucketCORS = "s3:DeleteBucketCORS"
|
|
||||||
specialS3ActionsListMultipartUploadParts = "s3:ListMultipartUploadParts"
|
|
||||||
specialS3ActionsGetObjectACL = "s3:GetObjectAcl"
|
|
||||||
specialS3ActionsGetObject = "s3:GetObject"
|
|
||||||
specialS3ActionsGetObjectVersion = "s3:GetObjectVersion"
|
|
||||||
specialS3ActionsGetObjectVersionACL = "s3:GetObjectVersionAcl"
|
|
||||||
specialS3ActionsGetObjectVersionAttributes = "s3:GetObjectVersionAttributes"
|
|
||||||
specialS3ActionsGetObjectVersionTagging = "s3:GetObjectVersionTagging"
|
|
||||||
specialS3ActionsPutObjectACL = "s3:PutObjectAcl"
|
|
||||||
specialS3ActionsPutObjectVersionACL = "s3:PutObjectVersionAcl"
|
|
||||||
specialS3ActionsPutObjectVersionTagging = "s3:PutObjectVersionTagging"
|
|
||||||
specialS3ActionsPutObject = "s3:PutObject"
|
|
||||||
specialS3ActionsDeleteObjectVersionTagging = "s3:DeleteObjectVersionTagging"
|
|
||||||
specialS3ActionsDeleteObject = "s3:DeleteObject"
|
|
||||||
specialS3ActionsDeleteObjectVersion = "s3:DeleteObjectVersion"
|
|
||||||
)
|
|
||||||
|
|
||||||
type S3Resolver interface {
|
type S3Resolver interface {
|
||||||
GetUserAddress(account, user string) (string, error)
|
GetUserAddress(account, user string) (string, error)
|
||||||
}
|
}
|
||||||
|
@ -233,22 +219,41 @@ func validateS3ResourceNames(names []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func formS3ActionNames(names []string) ([]string, error) {
|
func formS3ActionNames(names []string) ([]string, error) {
|
||||||
res := make([]string, 0, len(names))
|
uniqueActions := make(map[string]struct{}, len(names))
|
||||||
|
|
||||||
for _, action := range names {
|
for _, action := range names {
|
||||||
if err := validateAction(action); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if action == Wildcard {
|
if action == Wildcard {
|
||||||
return []string{Wildcard}, nil
|
return []string{Wildcard}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if actions, ok := specialActionToS3OpMap[action]; ok {
|
isIAM, err := validateAction(action)
|
||||||
res = append(res, actions...)
|
if err != nil {
|
||||||
} else {
|
return nil, err
|
||||||
res = append(res, action)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isIAM {
|
||||||
|
uniqueActions[action] = struct{}{}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if action[len(s3ActionPrefix):] == Wildcard {
|
||||||
|
uniqueActions[action] = struct{}{}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
s3Actions := actionToS3OpMap[action]
|
||||||
|
if len(s3Actions) == 0 {
|
||||||
|
return nil, ErrActionsNotApplicable
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s3Action := range s3Actions {
|
||||||
|
uniqueActions[s3Action] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res := make([]string, 0, len(uniqueActions))
|
||||||
|
for key := range uniqueActions {
|
||||||
|
res = append(res, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
|
|
|
@ -118,7 +118,7 @@ func TestConverters(t *testing.T) {
|
||||||
|
|
||||||
s3Chain, err := ConvertToS3Chain(p, mockResolver)
|
s3Chain, err := ConvertToS3Chain(p, mockResolver)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, expected, s3Chain)
|
assertChainsEqual(t, expected, s3Chain)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("valid native policy", func(t *testing.T) {
|
t.Run("valid native policy", func(t *testing.T) {
|
||||||
|
@ -136,9 +136,12 @@ func TestConverters(t *testing.T) {
|
||||||
|
|
||||||
expected := &chain.Chain{Rules: []chain.Rule{
|
expected := &chain.Chain{Rules: []chain.Rule{
|
||||||
{
|
{
|
||||||
Status: chain.Allow,
|
Status: chain.Allow,
|
||||||
Actions: chain.Actions{Names: []string{native.MethodPutObject}},
|
Actions: chain.Actions{Names: []string{native.MethodGetContainer, native.MethodPutObject}},
|
||||||
Resources: chain.Resources{Names: []string{fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, namespace, mockResolver.containers[bktName])}},
|
Resources: chain.Resources{Names: []string{
|
||||||
|
fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, namespace, mockResolver.containers[bktName]),
|
||||||
|
fmt.Sprintf(native.ResourceFormatNamespaceContainer, namespace, mockResolver.containers[bktName])},
|
||||||
|
},
|
||||||
Condition: []chain.Condition{
|
Condition: []chain.Condition{
|
||||||
{
|
{
|
||||||
Op: chain.CondStringEquals,
|
Op: chain.CondStringEquals,
|
||||||
|
@ -152,7 +155,7 @@ func TestConverters(t *testing.T) {
|
||||||
|
|
||||||
nativeChain, err := ConvertToNativeChain(p, mockResolver)
|
nativeChain, err := ConvertToNativeChain(p, mockResolver)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, expected, nativeChain)
|
assertChainsEqual(t, expected, nativeChain)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("valid inverted policy", func(t *testing.T) {
|
t.Run("valid inverted policy", func(t *testing.T) {
|
||||||
|
@ -186,7 +189,7 @@ func TestConverters(t *testing.T) {
|
||||||
|
|
||||||
s3Chain, err := ConvertToS3Chain(p, mockResolver)
|
s3Chain, err := ConvertToS3Chain(p, mockResolver)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, expected, s3Chain)
|
assertChainsEqual(t, expected, s3Chain)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("valid native policy map action", func(t *testing.T) {
|
t.Run("valid native policy map action", func(t *testing.T) {
|
||||||
|
@ -196,7 +199,7 @@ func TestConverters(t *testing.T) {
|
||||||
Principal: map[PrincipalType][]string{
|
Principal: map[PrincipalType][]string{
|
||||||
AWSPrincipalType: {principal},
|
AWSPrincipalType: {principal},
|
||||||
},
|
},
|
||||||
Effect: DenyEffect,
|
Effect: AllowEffect,
|
||||||
Action: []string{"s3:DeleteObject", "s3:DeleteBucket"},
|
Action: []string{"s3:DeleteObject", "s3:DeleteBucket"},
|
||||||
Resource: []string{
|
Resource: []string{
|
||||||
fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktName, objName),
|
fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktName, objName),
|
||||||
|
@ -207,10 +210,11 @@ func TestConverters(t *testing.T) {
|
||||||
|
|
||||||
expected := &chain.Chain{Rules: []chain.Rule{
|
expected := &chain.Chain{Rules: []chain.Rule{
|
||||||
{
|
{
|
||||||
Status: chain.AccessDenied,
|
Status: chain.Allow,
|
||||||
Actions: chain.Actions{Names: []string{native.MethodDeleteObject, native.MethodHeadObject, native.MethodDeleteContainer}},
|
Actions: chain.Actions{Names: []string{native.MethodGetContainer, native.MethodDeleteContainer, native.MethodSearchObject, native.MethodHeadObject, native.MethodDeleteObject}},
|
||||||
Resources: chain.Resources{Names: []string{
|
Resources: chain.Resources{Names: []string{
|
||||||
fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, namespace, mockResolver.containers[bktName]),
|
fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, namespace, mockResolver.containers[bktName]),
|
||||||
|
fmt.Sprintf(native.ResourceFormatNamespaceContainer, namespace, mockResolver.containers[bktName]),
|
||||||
}},
|
}},
|
||||||
Condition: []chain.Condition{
|
Condition: []chain.Condition{
|
||||||
{
|
{
|
||||||
|
@ -228,8 +232,8 @@ func TestConverters(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Status: chain.AccessDenied,
|
Status: chain.Allow,
|
||||||
Actions: chain.Actions{Names: []string{native.MethodDeleteObject, native.MethodHeadObject, native.MethodDeleteContainer}},
|
Actions: chain.Actions{Names: []string{native.MethodGetContainer, native.MethodDeleteContainer, native.MethodSearchObject, native.MethodHeadObject, native.MethodDeleteObject}},
|
||||||
Resources: chain.Resources{Names: []string{
|
Resources: chain.Resources{Names: []string{
|
||||||
fmt.Sprintf(native.ResourceFormatNamespaceContainer, namespace, mockResolver.containers[bktName]),
|
fmt.Sprintf(native.ResourceFormatNamespaceContainer, namespace, mockResolver.containers[bktName]),
|
||||||
}},
|
}},
|
||||||
|
@ -244,7 +248,7 @@ func TestConverters(t *testing.T) {
|
||||||
|
|
||||||
nativeChain, err := ConvertToNativeChain(p, mockResolver)
|
nativeChain, err := ConvertToNativeChain(p, mockResolver)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, expected, nativeChain)
|
assertChainsEqual(t, expected, nativeChain)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("invalid policy (unsupported principal type)", func(t *testing.T) {
|
t.Run("invalid policy (unsupported principal type)", func(t *testing.T) {
|
||||||
|
@ -338,12 +342,12 @@ func TestConverters(t *testing.T) {
|
||||||
|
|
||||||
s3Chain, err := ConvertToS3Chain(p, mockResolver)
|
s3Chain, err := ConvertToS3Chain(p, mockResolver)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, s3Expected, s3Chain)
|
assertChainsEqual(t, s3Expected, s3Chain)
|
||||||
|
|
||||||
nativeExpected := &chain.Chain{Rules: []chain.Rule{{
|
nativeExpected := &chain.Chain{Rules: []chain.Rule{{
|
||||||
Status: chain.Allow,
|
Status: chain.Allow,
|
||||||
Actions: chain.Actions{Names: []string{native.MethodDeleteObject, native.MethodHeadObject}},
|
Actions: chain.Actions{Names: []string{native.MethodGetContainer, native.MethodDeleteObject, native.MethodHeadObject}},
|
||||||
Resources: chain.Resources{Names: []string{native.ResourceFormatAllObjects}},
|
Resources: chain.Resources{Names: []string{native.ResourceFormatAllObjects, native.ResourceFormatAllContainers}},
|
||||||
Condition: []chain.Condition{{
|
Condition: []chain.Condition{{
|
||||||
Op: chain.CondStringEquals,
|
Op: chain.CondStringEquals,
|
||||||
Object: chain.ObjectRequest,
|
Object: chain.ObjectRequest,
|
||||||
|
@ -354,7 +358,7 @@ func TestConverters(t *testing.T) {
|
||||||
|
|
||||||
nativeChain, err := ConvertToNativeChain(p, mockResolver)
|
nativeChain, err := ConvertToNativeChain(p, mockResolver)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, nativeExpected, nativeChain)
|
assertChainsEqual(t, nativeExpected, nativeChain)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -624,8 +628,11 @@ func TestComplexNativeConditions(t *testing.T) {
|
||||||
|
|
||||||
mockResolver := newMockUserResolver([]string{user1, user2}, []string{bktName1, bktName2, bktName3}, "")
|
mockResolver := newMockUserResolver([]string{user1, user2}, []string{bktName1, bktName2, bktName3}, "")
|
||||||
nativeResource1 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers[bktName1])
|
nativeResource1 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers[bktName1])
|
||||||
|
nativeResource1cnr := fmt.Sprintf(native.ResourceFormatRootContainer, mockResolver.containers[bktName1])
|
||||||
nativeResource2 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers[bktName2])
|
nativeResource2 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers[bktName2])
|
||||||
|
nativeResource2cnr := fmt.Sprintf(native.ResourceFormatRootContainer, mockResolver.containers[bktName2])
|
||||||
nativeResource3 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers[bktName3])
|
nativeResource3 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers[bktName3])
|
||||||
|
nativeResource3cnr := fmt.Sprintf(native.ResourceFormatRootContainer, mockResolver.containers[bktName3])
|
||||||
|
|
||||||
p := Policy{
|
p := Policy{
|
||||||
Version: "2012-10-17",
|
Version: "2012-10-17",
|
||||||
|
@ -633,7 +640,7 @@ func TestComplexNativeConditions(t *testing.T) {
|
||||||
Principal: map[PrincipalType][]string{
|
Principal: map[PrincipalType][]string{
|
||||||
AWSPrincipalType: {principal1, principal2},
|
AWSPrincipalType: {principal1, principal2},
|
||||||
},
|
},
|
||||||
Effect: DenyEffect,
|
Effect: AllowEffect,
|
||||||
Action: []string{"s3:" + action},
|
Action: []string{"s3:" + action},
|
||||||
Resource: []string{"arn:aws:s3:::" + resource1, "arn:aws:s3:::" + resource2, "arn:aws:s3:::" + resource3},
|
Resource: []string{"arn:aws:s3:::" + resource1, "arn:aws:s3:::" + resource2, "arn:aws:s3:::" + resource3},
|
||||||
Conditions: map[string]Condition{
|
Conditions: map[string]Condition{
|
||||||
|
@ -643,10 +650,10 @@ func TestComplexNativeConditions(t *testing.T) {
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedStatus := chain.AccessDenied
|
expectedStatus := chain.Allow
|
||||||
expectedActions := chain.Actions{Names: supportedActionToNativeOpMap["s3:"+action]}
|
expectedActions := chain.Actions{Names: actionToNativeOpMap["s3:"+action]}
|
||||||
expectedResource1 := chain.Resources{Names: []string{nativeResource1}}
|
expectedResource1 := chain.Resources{Names: []string{nativeResource1, nativeResource1cnr}}
|
||||||
expectedResource23 := chain.Resources{Names: []string{nativeResource2, nativeResource3}}
|
expectedResource23 := chain.Resources{Names: []string{nativeResource2, nativeResource2cnr, nativeResource3, nativeResource3cnr}}
|
||||||
|
|
||||||
user1Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: native.PropertyKeyActorPublicKey, Value: mockResolver.users[user1]}
|
user1Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: native.PropertyKeyActorPublicKey, Value: mockResolver.users[user1]}
|
||||||
user2Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: native.PropertyKeyActorPublicKey, Value: mockResolver.users[user2]}
|
user2Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: native.PropertyKeyActorPublicKey, Value: mockResolver.users[user2]}
|
||||||
|
@ -770,7 +777,7 @@ func TestComplexNativeConditions(t *testing.T) {
|
||||||
key1: val0,
|
key1: val0,
|
||||||
key2: val2,
|
key2: val2,
|
||||||
},
|
},
|
||||||
status: chain.AccessDenied,
|
status: chain.Allow,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "bucket resource3, all conditions matched",
|
name: "bucket resource3, all conditions matched",
|
||||||
|
@ -784,7 +791,7 @@ func TestComplexNativeConditions(t *testing.T) {
|
||||||
key1: val0,
|
key1: val0,
|
||||||
key2: val2,
|
key2: val2,
|
||||||
},
|
},
|
||||||
status: chain.AccessDenied,
|
status: chain.Allow,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "bucket resource, user condition mismatched",
|
name: "bucket resource, user condition mismatched",
|
||||||
|
@ -838,7 +845,7 @@ func TestComplexNativeConditions(t *testing.T) {
|
||||||
key1: val0,
|
key1: val0,
|
||||||
key2: val2,
|
key2: val2,
|
||||||
},
|
},
|
||||||
status: chain.AccessDenied,
|
status: chain.Allow,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "bucket/object resource, user condition mismatched",
|
name: "bucket/object resource, user condition mismatched",
|
||||||
|
@ -1164,7 +1171,7 @@ func TestS3BucketResource(t *testing.T) {
|
||||||
{
|
{
|
||||||
Principal: map[PrincipalType][]string{Wildcard: nil},
|
Principal: map[PrincipalType][]string{Wildcard: nil},
|
||||||
Effect: DenyEffect,
|
Effect: DenyEffect,
|
||||||
Action: []string{"s3:HeadBucket"},
|
Action: []string{"s3:ListBucket"},
|
||||||
Resource: []string{fmt.Sprintf(s3.ResourceFormatS3Bucket, bktName1)},
|
Resource: []string{fmt.Sprintf(s3.ResourceFormatS3Bucket, bktName1)},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1203,7 +1210,7 @@ func TestS3BucketResource(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWildcardConverters(t *testing.T) {
|
func TestWildcardConverters(t *testing.T) {
|
||||||
policy := `{"Version":"2012-10-17","Statement":{"Effect":"Allow", "Principal": "*", "Action":"*","Resource":"*"}}`
|
policy := `{"Version":"2012-10-17","Statement":{"Effect":"Allow", "Principal": "*", "Action":"s3:*","Resource":"*"}}`
|
||||||
|
|
||||||
var p Policy
|
var p Policy
|
||||||
err := json.Unmarshal([]byte(policy), &p)
|
err := json.Unmarshal([]byte(policy), &p)
|
||||||
|
@ -1212,7 +1219,7 @@ func TestWildcardConverters(t *testing.T) {
|
||||||
s3Expected := &chain.Chain{
|
s3Expected := &chain.Chain{
|
||||||
Rules: []chain.Rule{{
|
Rules: []chain.Rule{{
|
||||||
Status: chain.Allow,
|
Status: chain.Allow,
|
||||||
Actions: chain.Actions{Names: []string{Wildcard}},
|
Actions: chain.Actions{Names: []string{"s3:*"}},
|
||||||
Resources: chain.Resources{Names: []string{Wildcard}},
|
Resources: chain.Resources{Names: []string{Wildcard}},
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
|
@ -1234,50 +1241,176 @@ func TestWildcardConverters(t *testing.T) {
|
||||||
require.Equal(t, nativeExpected, nativeChain)
|
require.Equal(t, nativeExpected, nativeChain)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestActionParsing(t *testing.T) {
|
func TestWildcardObjectsConverters(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
policy := `{"Version":"2012-10-17","Statement":{"Effect":"Allow", "Principal": "*", "Action":"s3:*","Resource":"arn:aws:s3:::bucket/*"}}`
|
||||||
action string
|
|
||||||
err bool
|
var p Policy
|
||||||
}{
|
err := json.Unmarshal([]byte(policy), &p)
|
||||||
{
|
require.NoError(t, err)
|
||||||
action: "withoutPrefix",
|
|
||||||
err: true,
|
s3Expected := &chain.Chain{
|
||||||
},
|
Rules: []chain.Rule{{
|
||||||
{
|
Status: chain.Allow,
|
||||||
action: "s3:*Object",
|
Actions: chain.Actions{Names: []string{"s3:*"}},
|
||||||
err: true,
|
Resources: chain.Resources{Names: []string{"arn:aws:s3:::bucket/*"}},
|
||||||
},
|
}},
|
||||||
{
|
|
||||||
action: "*",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
action: "s3:PutObject",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
action: "s3:Put*",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
action: "s3:*",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
action: "s3:",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
action: "iam:ListAccessKeys",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
action: "iam:*",
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run("", func(t *testing.T) {
|
|
||||||
err := validateAction(tc.action)
|
|
||||||
if tc.err {
|
|
||||||
require.Error(t, err)
|
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s3Chain, err := ConvertToS3Chain(p, newMockUserResolver(nil, nil, ""))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, s3Expected, s3Chain)
|
||||||
|
|
||||||
|
mockResolver := newMockUserResolver(nil, []string{"bucket"}, "")
|
||||||
|
|
||||||
|
nativeExpected := &chain.Chain{
|
||||||
|
Rules: []chain.Rule{{
|
||||||
|
Status: chain.Allow,
|
||||||
|
Actions: chain.Actions{Names: []string{Wildcard}},
|
||||||
|
Resources: chain.Resources{Names: []string{
|
||||||
|
fmt.Sprintf(native.ResourceFormatRootContainer, mockResolver.containers["bucket"]),
|
||||||
|
fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers["bucket"]),
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
nativeChain, err := ConvertToNativeChain(p, mockResolver)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assertChainsEqual(t, nativeExpected, nativeChain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDisableNativeDeny(t *testing.T) {
|
||||||
|
policy := `
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Effect": "Deny",
|
||||||
|
"Principal": "*",
|
||||||
|
"Action": "s3:*",
|
||||||
|
"Resource": [ "arn:aws:s3:::test-bucket/*" ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
var p Policy
|
||||||
|
err := json.Unmarshal([]byte(policy), &p)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = ConvertToNativeChain(p, newMockUserResolver(nil, nil, ""))
|
||||||
|
require.ErrorIs(t, err, ErrActionsNotApplicable)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFromActions(t *testing.T) {
|
||||||
|
t.Run("s3 actions", func(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
action string
|
||||||
|
res []string
|
||||||
|
err bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
action: "withoutPrefix",
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "s3:*Object",
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "*",
|
||||||
|
res: []string{Wildcard},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "s3:PutObject",
|
||||||
|
res: []string{"s3:PutObject", "s3:PostObject", "s3:CopyObject",
|
||||||
|
"s3:UploadPart", "s3:UploadPartCopy", "s3:CreateMultipartUpload", "s3:CompleteMultipartUpload"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "s3:Put*",
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "s3:*",
|
||||||
|
res: []string{"s3:*"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "s3:",
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "iam:ListAccessKeys",
|
||||||
|
res: []string{"iam:ListAccessKeys"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "iam:*",
|
||||||
|
res: []string{"iam:*"},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run("", func(t *testing.T) {
|
||||||
|
actions, err := formS3ActionNames([]string{tc.action})
|
||||||
|
if tc.err {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.ElementsMatch(t, tc.res, actions)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("native actions", func(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
action string
|
||||||
|
res []string
|
||||||
|
err bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
action: "withoutPrefix",
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "s3:*Object",
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "*",
|
||||||
|
res: []string{Wildcard},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "s3:PutObject",
|
||||||
|
res: []string{native.MethodGetContainer, native.MethodPutObject},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "s3:Put*",
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "s3:*",
|
||||||
|
res: []string{Wildcard},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "s3:",
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "iam:ListAccessKeys",
|
||||||
|
res: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "iam:*",
|
||||||
|
res: []string{},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run("", func(t *testing.T) {
|
||||||
|
actions, err := formNativeActionNames([]string{tc.action})
|
||||||
|
if tc.err {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.ElementsMatch(t, tc.res, actions)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPrincipalParsing(t *testing.T) {
|
func TestPrincipalParsing(t *testing.T) {
|
||||||
|
@ -1397,19 +1530,39 @@ func areRulesMatched(rule1, rule2 chain.Rule) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, name := range rule1.Resources.Names {
|
|
||||||
if name != rule2.Resources.Names[i] {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, name := range rule1.Actions.Names {
|
|
||||||
if name != rule2.Actions.Names[i] {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
seen := make(map[int]struct{})
|
seen := make(map[int]struct{})
|
||||||
|
for _, name1 := range rule1.Resources.Names {
|
||||||
|
for j, name2 := range rule2.Resources.Names {
|
||||||
|
if _, ok := seen[j]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if name1 == name2 {
|
||||||
|
seen[j] = struct{}{}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(seen) != len(rule1.Resources.Names) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
seen = make(map[int]struct{})
|
||||||
|
for _, name1 := range rule1.Actions.Names {
|
||||||
|
for j, name2 := range rule2.Actions.Names {
|
||||||
|
if _, ok := seen[j]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if name1 == name2 {
|
||||||
|
seen[j] = struct{}{}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(seen) != len(rule1.Actions.Names) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
seen = make(map[int]struct{})
|
||||||
for _, cond1 := range rule1.Condition {
|
for _, cond1 := range rule1.Condition {
|
||||||
for j, cond2 := range rule2.Condition {
|
for j, cond2 := range rule2.Condition {
|
||||||
if _, ok := seen[j]; ok {
|
if _, ok := seen[j]; ok {
|
||||||
|
@ -1424,3 +1577,19 @@ func areRulesMatched(rule1, rule2 chain.Rule) bool {
|
||||||
|
|
||||||
return len(seen) == len(rule1.Condition)
|
return len(seen) == len(rule1.Condition)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func assertChainsEqual(t *testing.T, chain1, chain2 *chain.Chain) {
|
||||||
|
require.Equal(t, string(chain1.ID), string(chain2.ID))
|
||||||
|
require.Equal(t, chain1.MatchType, chain2.MatchType)
|
||||||
|
require.Equal(t, len(chain1.Rules), len(chain2.Rules))
|
||||||
|
|
||||||
|
for i, rule := range chain1.Rules {
|
||||||
|
require.Equal(t, rule.Any, chain2.Rules[i].Any)
|
||||||
|
require.Equal(t, rule.Resources.Inverted, chain2.Rules[i].Resources.Inverted)
|
||||||
|
require.ElementsMatch(t, rule.Resources.Names, chain2.Rules[i].Resources.Names)
|
||||||
|
require.Equal(t, rule.Status, chain2.Rules[i].Status)
|
||||||
|
require.ElementsMatch(t, rule.Condition, chain2.Rules[i].Condition)
|
||||||
|
require.Equal(t, rule.Actions.Inverted, chain2.Rules[i].Actions.Inverted)
|
||||||
|
require.ElementsMatch(t, rule.Actions.Names, chain2.Rules[i].Actions.Names)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,14 +2,13 @@ package iam
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
|
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
|
||||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine/inmemory"
|
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine/inmemory"
|
||||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource/testutil"
|
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource/testutil"
|
||||||
"git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
|
"git.frostfs.info/TrueCloudLab/policy-engine/schema/s3"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -525,27 +524,27 @@ func TestProcessDenyFirst(t *testing.T) {
|
||||||
|
|
||||||
mockResolver := newMockUserResolver([]string{"root/user-name"}, []string{"test-bucket"}, "")
|
mockResolver := newMockUserResolver([]string{"root/user-name"}, []string{"test-bucket"}, "")
|
||||||
|
|
||||||
identityNativePolicy, err := ConvertToNativeChain(identityPolicy, mockResolver)
|
identityNativePolicy, err := ConvertToS3Chain(identityPolicy, mockResolver)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
identityNativePolicy.MatchType = chain.MatchTypeFirstMatch
|
identityNativePolicy.MatchType = chain.MatchTypeFirstMatch
|
||||||
|
|
||||||
resourceNativePolicy, err := ConvertToNativeChain(resourcePolicy, mockResolver)
|
resourceNativePolicy, err := ConvertToS3Chain(resourcePolicy, mockResolver)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
s := inmemory.NewInMemory()
|
s := inmemory.NewInMemory()
|
||||||
|
|
||||||
target := engine.NamespaceTarget("ns")
|
target := engine.NamespaceTarget("ns")
|
||||||
|
|
||||||
_, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, target, identityNativePolicy)
|
_, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, target, identityNativePolicy)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, target, resourceNativePolicy)
|
_, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, target, resourceNativePolicy)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
resource := testutil.NewResource(fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers["test-bucket"]), nil)
|
resource := testutil.NewResource("arn:aws:s3:::test-bucket/object", nil)
|
||||||
request := testutil.NewRequest("PutObject", resource, map[string]string{native.PropertyKeyActorPublicKey: mockResolver.users["root/user-name"]})
|
request := testutil.NewRequest("s3:PutObject", resource, map[string]string{s3.PropertyKeyOwner: mockResolver.users["root/user-name"]})
|
||||||
|
|
||||||
status, found, err := s.IsAllowed(chain.Ingress, engine.NewRequestTarget("ns", ""), request)
|
status, found, err := s.IsAllowed(chain.S3, engine.NewRequestTarget("ns", ""), request)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, found)
|
require.True(t, found)
|
||||||
require.Equal(t, chain.AccessDenied, status)
|
require.Equal(t, chain.AccessDenied, status)
|
||||||
|
|
Loading…
Reference in a new issue