policy-engine/iam/converter_native.go
Denis Kirillov b6a6816800 [#68] iam: Allow read object on delete operation
We must be able to read s3 multipart object from storage
(to find out the parts it consists of)
to fully delete such multipart object

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2024-04-12 09:57:53 +03:00

405 lines
15 KiB
Go

package iam
import (
"fmt"
"strings"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
)
const PropertyKeyFilePath = "FilePath"
var actionToNativeOpMap = map[string][]string{
s3ActionAbortMultipartUpload: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodHeadObject},
s3ActionCreateBucket: {native.MethodGetContainer, native.MethodPutContainer, native.MethodSetContainerEACL},
s3ActionDeleteBucket: {native.MethodGetContainer, native.MethodDeleteContainer, native.MethodSearchObject, native.MethodHeadObject},
s3ActionDeleteBucketPolicy: {native.MethodGetContainer},
s3ActionDeleteObject: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodPutObject, native.MethodHeadObject, native.MethodGetObject, native.MethodRangeObject},
s3ActionDeleteObjectTagging: {native.MethodGetContainer, native.MethodHeadObject},
s3ActionDeleteObjectVersion: {native.MethodGetContainer, native.MethodDeleteObject, native.MethodPutObject, native.MethodHeadObject, native.MethodGetObject, native.MethodRangeObject},
s3ActionDeleteObjectVersionTagging: {native.MethodGetContainer, native.MethodHeadObject},
s3ActionGetBucketACL: {native.MethodGetContainer, native.MethodGetContainerEACL},
s3ActionGetBucketCORS: {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject},
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{}{
native.MethodPutContainer: {},
native.MethodDeleteContainer: {},
native.MethodGetContainer: {},
native.MethodListContainers: {},
native.MethodSetContainerEACL: {},
native.MethodGetContainerEACL: {},
}
var objectNativeOperations = map[string]struct{}{
native.MethodGetObject: {},
native.MethodPutObject: {},
native.MethodHeadObject: {},
native.MethodDeleteObject: {},
native.MethodSearchObject: {},
native.MethodRangeObject: {},
native.MethodHashObject: {},
}
type NativeResolver interface {
GetUserKey(account, name string) (string, error)
GetBucketInfo(bucket string) (*BucketInfo, error)
}
type BucketInfo struct {
Namespace string
Container string
}
func ConvertToNativeChain(p Policy, resolver NativeResolver) (*chain.Chain, error) {
if err := p.Validate(ResourceBasedPolicyType); err != nil {
return nil, err
}
var engineChain chain.Chain
for _, statement := range p.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()
nativeActions, err := formNativeActionNames(action)
if err != nil {
return nil, err
}
ruleAction := chain.Actions{Inverted: actionInverted, Names: nativeActions}
if len(ruleAction.Names) == 0 {
continue
}
resource, resourceInverted := statement.resource()
groupedResources, err := formNativeResourceNamesAndConditions(resource, resolver, getActionTypes(nativeActions))
if err != nil {
return nil, err
}
groupedConditions, err := convertToNativeChainCondition(statement.Conditions, resolver)
if err != nil {
return nil, err
}
splitConditions := splitGroupedConditions(groupedConditions)
principals, principalCondFn, err := getNativePrincipalsAndConditionFunc(statement, resolver)
if err != nil {
return nil, err
}
for _, groupedResource := range groupedResources {
for _, principal := range principals {
for _, conditions := range splitConditions {
var principalCondition []chain.Condition
if principal != Wildcard {
principalCondition = []chain.Condition{principalCondFn(principal)}
}
ruleConditions := append(principalCondition, groupedResource.Conditions...)
r := chain.Rule{
Status: status,
Actions: ruleAction,
Resources: chain.Resources{
Inverted: resourceInverted,
Names: groupedResource.Names,
},
Condition: append(ruleConditions, conditions...),
}
engineChain.Rules = append(engineChain.Rules, r)
}
}
}
}
if len(engineChain.Rules) == 0 {
return nil, ErrActionsNotApplicable
}
return &engineChain, nil
}
func getActionTypes(nativeActions []string) ActionTypes {
var res ActionTypes
for _, action := range nativeActions {
if res.Object && res.Container {
break
}
_, isObj := objectNativeOperations[action]
_, isCnr := containerNativeOperations[action]
res.Object = res.Object || isObj || action == Wildcard
res.Container = res.Container || isCnr || action == Wildcard
}
return res
}
func getNativePrincipalsAndConditionFunc(statement Statement, resolver NativeResolver) ([]string, formPrincipalConditionFunc, error) {
var principals []string
var op chain.ConditionType
statementPrincipal, inverted := statement.principal()
if _, ok := statementPrincipal[Wildcard]; ok { // this can be true only if 'inverted' false
principals = []string{Wildcard}
op = chain.CondStringLike
} else {
for principalType, principal := range statementPrincipal {
if principalType != AWSPrincipalType {
return nil, nil, fmt.Errorf("unsupported principal type '%s'", principalType)
}
parsedPrincipal, err := formNativePrincipal(principal, resolver)
if err != nil {
return nil, nil, fmt.Errorf("parse principal: %w", err)
}
principals = append(principals, parsedPrincipal...)
}
op = chain.CondStringEquals
if inverted {
op = chain.CondStringNotEquals
}
}
return principals, func(principal string) chain.Condition {
return chain.Condition{
Op: op,
Object: chain.ObjectRequest,
Key: native.PropertyKeyActorPublicKey,
Value: principal,
}
}, nil
}
func convertToNativeChainCondition(c Conditions, resolver NativeResolver) ([]GroupedConditions, error) {
return convertToChainConditions(c, func(gr GroupedConditions) (GroupedConditions, error) {
for i := range gr.Conditions {
if gr.Conditions[i].Key == condKeyAWSPrincipalARN {
gr.Conditions[i].Key = native.PropertyKeyActorPublicKey
val, err := formPrincipalKey(gr.Conditions[i].Value, resolver)
if err != nil {
return GroupedConditions{}, err
}
gr.Conditions[i].Value = val
}
}
return gr, nil
})
}
type GroupedResources struct {
Names []string
Conditions []chain.Condition
}
type ActionTypes struct {
Object bool
Container bool
}
func formNativeResourceNamesAndConditions(names []string, resolver NativeResolver, actionTypes ActionTypes) ([]GroupedResources, error) {
if !actionTypes.Object && !actionTypes.Container {
return nil, ErrActionsNotApplicable
}
res := make([]GroupedResources, 0, len(names))
combined := make(map[string]struct{})
for _, resource := range names {
if err := validateResource(resource); err != nil {
return nil, err
}
if resource == Wildcard {
res = res[:0]
return append(res, formWildcardNativeResource(actionTypes)), nil
}
if !strings.HasPrefix(resource, s3ResourcePrefix) {
continue
}
var bkt, obj string
s3Resource := strings.TrimPrefix(resource, s3ResourcePrefix)
if s3Resource == Wildcard {
res = res[:0]
return append(res, formWildcardNativeResource(actionTypes)), nil
}
if sepIndex := strings.Index(s3Resource, "/"); sepIndex < 0 {
bkt = s3Resource
} else {
bkt = s3Resource[:sepIndex]
obj = s3Resource[sepIndex+1:]
if len(obj) == 0 {
obj = Wildcard
}
}
bktInfo, err := resolver.GetBucketInfo(bkt)
if err != nil {
return nil, err
}
if obj == Wildcard && actionTypes.Object { // this corresponds to arn:aws:s3:::BUCKET/ or arn:aws:s3:::BUCKET/*
combined[fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container)] = struct{}{}
combined[fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container)] = struct{}{}
continue
}
if obj == "" && actionTypes.Container { // this corresponds to arn:aws:s3:::BUCKET
combined[fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container)] = struct{}{}
continue
}
res = append(res, GroupedResources{
Names: []string{
fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container),
fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container),
},
Conditions: []chain.Condition{
{
Op: chain.CondStringLike,
Object: chain.ObjectResource,
Key: PropertyKeyFilePath,
Value: obj,
},
},
})
}
if len(combined) != 0 {
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
}
func formWildcardNativeResource(actionTypes ActionTypes) GroupedResources {
groupedNames := make([]string, 0, 2)
if actionTypes.Object {
groupedNames = append(groupedNames, native.ResourceFormatAllObjects)
}
if actionTypes.Container {
groupedNames = append(groupedNames, native.ResourceFormatAllContainers)
}
return GroupedResources{Names: groupedNames}
}
func formNativePrincipal(principal []string, resolver NativeResolver) ([]string, error) {
res := make([]string, len(principal))
var err error
for i := range principal {
if res[i], err = formPrincipalKey(principal[i], resolver); err != nil {
return nil, err
}
}
return res, nil
}
func formPrincipalKey(principal string, resolver NativeResolver) (string, error) {
account, user, err := parsePrincipalAsIAMUser(principal)
if err != nil {
return "", err
}
key, err := resolver.GetUserKey(account, user)
if err != nil {
return "", fmt.Errorf("get user key: %w", err)
}
return key, nil
}
func formNativeActionNames(names []string) ([]string, error) {
uniqueActions := make(map[string]struct{}, len(names))
for _, action := range names {
if action == Wildcard {
return []string{Wildcard}, nil
}
isIAM, err := validateAction(action)
if err != nil {
return nil, err
}
if isIAM {
continue
}
if action[len(s3ActionPrefix):] == Wildcard {
return []string{Wildcard}, nil
}
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
}