From 5fa9d91903bae3d0e29f5078a5d21f7f66de17b5 Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Fri, 10 Nov 2023 17:56:41 +0300 Subject: [PATCH] [#17] iam: Add converter to native/s3 policy Signed-off-by: Denis Kirillov --- go.mod | 5 +- go.sum | 21 +- iam/converter.go | 339 ++++++----- iam/converter_native.go | 241 ++++++++ iam/converter_s3.go | 153 +++++ iam/converter_test.go | 1216 ++++++++++++++++++++++++++++++++------- iam/policy.go | 4 + iam/policy_test.go | 6 + schema/s3/consts.go | 9 + 9 files changed, 1648 insertions(+), 346 deletions(-) create mode 100644 iam/converter_native.go create mode 100644 iam/converter_s3.go create mode 100644 schema/s3/consts.go diff --git a/go.mod b/go.mod index ffd55cf..6008d15 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,13 @@ module git.frostfs.info/TrueCloudLab/policy-engine go 1.20 -require github.com/stretchr/testify v1.8.1 +require github.com/stretchr/testify v1.8.4 require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/kr/pretty v0.1.0 // indirect + github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 2ec90f7..07efd47 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,18 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/iam/converter.go b/iam/converter.go index b781823..9002d83 100644 --- a/iam/converter.go +++ b/iam/converter.go @@ -1,6 +1,7 @@ package iam import ( + "errors" "fmt" "strconv" "strings" @@ -9,9 +10,7 @@ import ( "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" ) -const ( - RequestOwnerProperty = "Owner" -) +const condKeyAWSPrincipalARN = "aws:PrincipalArn" const ( // String condition operators. @@ -52,168 +51,144 @@ const ( CondArnNotLike string = "ArnNotLike" ) -func (p Policy) ToChain() (*chain.Chain, error) { - if err := p.Validate(GeneralPolicyType); err != nil { +const ( + arnIAMPrefix = "arn:aws:iam::" + s3ResourcePrefix = "arn:aws:s3:::" + s3ActionPrefix = "s3:" +) + +var ( + // ErrInvalidPrincipalFormat occurs when principal has unknown/unsupported format. + ErrInvalidPrincipalFormat = errors.New("invalid principal format") + + // ErrInvalidResourceFormat occurs when resource has unknown/unsupported format. + ErrInvalidResourceFormat = errors.New("invalid resource format") +) + +type formPrincipalConditionFunc func(string) chain.Condition + +type transformConditionFunc func(gr GroupedConditions) (GroupedConditions, error) + +func convertToChainConditions(c Conditions, transformer transformConditionFunc) ([]GroupedConditions, error) { + conditions, err := convertToChainCondition(c) + if err != nil { return nil, err } - var ch chain.Chain - - for _, statement := range p.Statement { - status := chain.AccessDenied - if statement.Effect == AllowEffect { - status = chain.Allow + for i := range conditions { + if conditions[i], err = transformer(conditions[i]); err != nil { + return nil, fmt.Errorf("transform condition: %w", err) } + } - 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 _, principal := range statementPrincipal { - principals = append(principals, principal...) - } + return conditions, nil +} - op = chain.CondStringEquals - if inverted { - op = chain.CondStringNotEquals - } - } +type GroupedConditions struct { + Conditions []chain.Condition + Any bool +} - var conditions []chain.Condition - for _, principal := range principals { - conditions = append(conditions, chain.Condition{ - Op: op, - Object: chain.ObjectRequest, - Key: RequestOwnerProperty, - Value: principal, - }) - } +func convertToChainCondition(c Conditions) ([]GroupedConditions, error) { + var grouped []GroupedConditions - conds, err := statement.Conditions.ToChainCondition() + for op, KVs := range c { + condType, convertValue, err := getConditionTypeAndConverter(op) if err != nil { return nil, err } - conditions = append(conditions, conds...) - - action, actionInverted := statement.action() - ruleAction := chain.Actions{Inverted: actionInverted, Names: action} - - resource, resourceInverted := statement.resource() - ruleResource := chain.Resources{Inverted: resourceInverted, Names: resource} - - r := chain.Rule{ - Status: status, - Actions: ruleAction, - Resources: ruleResource, - Any: true, - Condition: conditions, - } - ch.Rules = append(ch.Rules, r) - } - - return &ch, nil -} - -//nolint:funlen -func (c Conditions) ToChainCondition() ([]chain.Condition, error) { - var conditions []chain.Condition - - var convertValue convertFunction - - for op, KVs := range c { - var condType chain.ConditionType - - switch { - case strings.HasPrefix(op, "String"): - convertValue = noConvertFunction - switch op { - case CondStringEquals: - condType = chain.CondStringEquals - case CondStringNotEquals: - condType = chain.CondStringNotEquals - case CondStringEqualsIgnoreCase: - condType = chain.CondStringEqualsIgnoreCase - case CondStringNotEqualsIgnoreCase: - condType = chain.CondStringNotEqualsIgnoreCase - case CondStringLike: - condType = chain.CondStringLike - case CondStringNotLike: - condType = chain.CondStringNotLike - default: - return nil, fmt.Errorf("unsupported condition operator: '%s'", op) - } - case strings.HasPrefix(op, "Arn"): - convertValue = noConvertFunction - switch op { - case CondArnEquals: - condType = chain.CondStringEquals - case CondArnNotEquals: - condType = chain.CondStringNotEquals - case CondArnLike: - condType = chain.CondStringLike - case CondArnNotLike: - condType = chain.CondStringNotLike - default: - return nil, fmt.Errorf("unsupported condition operator: '%s'", op) - } - case strings.HasPrefix(op, "Numeric"): - // TODO - case strings.HasPrefix(op, "Date"): - convertValue = dateConvertFunction - switch op { - case CondDateEquals: - condType = chain.CondStringEquals - case CondDateNotEquals: - condType = chain.CondStringNotEquals - case CondDateLessThan: - condType = chain.CondStringLessThan - case CondDateLessThanEquals: - condType = chain.CondStringLessThanEquals - case CondDateGreaterThan: - condType = chain.CondStringGreaterThan - case CondDateGreaterThanEquals: - condType = chain.CondStringGreaterThanEquals - default: - return nil, fmt.Errorf("unsupported condition operator: '%s'", op) - } - case op == CondBool: - convertValue = noConvertFunction - condType = chain.CondStringEqualsIgnoreCase - case op == CondIPAddress: - // todo consider using converters - // "203.0.113.0/24" -> "203.0.113.*", - // "2001:DB8:1234:5678::/64" -> "2001:DB8:1234:5678:*" - // or having specific condition type for IP - convertValue = noConvertFunction - condType = chain.CondStringLike - case op == CondNotIPAddress: - convertValue = noConvertFunction - condType = chain.CondStringNotLike - default: - return nil, fmt.Errorf("unsupported condition operator: '%s'", op) - } for key, values := range KVs { - for _, val := range values { + group := GroupedConditions{ + Conditions: make([]chain.Condition, len(values)), + Any: len(values) > 1, + } + + for i, val := range values { converted, err := convertValue(val) if err != nil { return nil, err } - conditions = append(conditions, chain.Condition{ + group.Conditions[i] = chain.Condition{ Op: condType, Object: chain.ObjectRequest, Key: key, Value: converted, - }) + } } + grouped = append(grouped, group) } } - return conditions, nil + return grouped, nil +} + +func getConditionTypeAndConverter(op string) (chain.ConditionType, convertFunction, error) { + switch { + case strings.HasPrefix(op, "String"): + switch op { + case CondStringEquals: + return chain.CondStringEquals, noConvertFunction, nil + case CondStringNotEquals: + return chain.CondStringNotEquals, noConvertFunction, nil + case CondStringEqualsIgnoreCase: + return chain.CondStringEqualsIgnoreCase, noConvertFunction, nil + case CondStringNotEqualsIgnoreCase: + return chain.CondStringNotEqualsIgnoreCase, noConvertFunction, nil + case CondStringLike: + return chain.CondStringLike, noConvertFunction, nil + case CondStringNotLike: + return chain.CondStringNotLike, noConvertFunction, nil + default: + return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op) + } + case strings.HasPrefix(op, "Arn"): + switch op { + case CondArnEquals: + return chain.CondStringEquals, noConvertFunction, nil + case CondArnNotEquals: + return chain.CondStringNotEquals, noConvertFunction, nil + case CondArnLike: + return chain.CondStringLike, noConvertFunction, nil + case CondArnNotLike: + return chain.CondStringNotLike, noConvertFunction, nil + default: + return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op) + } + case strings.HasPrefix(op, "Numeric"): + // TODO + return 0, nil, fmt.Errorf("currently nummeric conditions unsupported: '%s'", op) + case strings.HasPrefix(op, "Date"): + switch op { + case CondDateEquals: + return chain.CondStringEquals, dateConvertFunction, nil + case CondDateNotEquals: + return chain.CondStringNotEquals, dateConvertFunction, nil + case CondDateLessThan: + return chain.CondStringLessThan, dateConvertFunction, nil + case CondDateLessThanEquals: + return chain.CondStringLessThanEquals, dateConvertFunction, nil + case CondDateGreaterThan: + return chain.CondStringGreaterThan, dateConvertFunction, nil + case CondDateGreaterThanEquals: + return chain.CondStringGreaterThanEquals, dateConvertFunction, nil + default: + return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op) + } + case op == CondBool: + return chain.CondStringEqualsIgnoreCase, noConvertFunction, nil + case op == CondIPAddress: + // todo consider using converters + // "203.0.113.0/24" -> "203.0.113.*", + // "2001:DB8:1234:5678::/64" -> "2001:DB8:1234:5678:*" + // or having specific condition type for IP + return chain.CondStringLike, noConvertFunction, nil + case op == CondNotIPAddress: + return chain.CondStringNotLike, noConvertFunction, nil + default: + return 0, nil, fmt.Errorf("unsupported condition operator: '%s'", op) + } } type convertFunction func(string) (string, error) @@ -234,3 +209,89 @@ func dateConvertFunction(val string) (string, error) { return strconv.FormatInt(parsed.UTC().Unix(), 10), nil } + +func parsePrincipalAsIAMUser(principal string) (account string, user string, err error) { + if !strings.HasPrefix(principal, arnIAMPrefix) { + return "", "", ErrInvalidPrincipalFormat + } + + // iam arn format arn:aws:iam:::user/ + iamResource := strings.TrimPrefix(principal, arnIAMPrefix) + sepIndex := strings.Index(iamResource, ":user/") + if sepIndex < 0 { + return "", "", ErrInvalidPrincipalFormat + } + + account = iamResource[:sepIndex] + user = iamResource[sepIndex+6:] + if len(user) == 0 { + return "", "", ErrInvalidPrincipalFormat + } + + userNameIndex := strings.LastIndexByte(user, '/') + if userNameIndex > -1 { + user = user[userNameIndex+1:] + if len(user) == 0 { + return "", "", ErrInvalidPrincipalFormat + } + } + + return account, user, nil +} + +func parseResourceAsS3ARN(resource string) (bucket string, object string, err error) { + if !strings.HasPrefix(resource, s3ResourcePrefix) { + return "", "", ErrInvalidResourceFormat + } + + // iam arn format arn:aws:s3:::/ + s3Resource := strings.TrimPrefix(resource, s3ResourcePrefix) + sepIndex := strings.Index(s3Resource, "/") + if sepIndex < 0 { + return s3Resource, Wildcard, nil + } + + bucket = s3Resource[:sepIndex] + object = s3Resource[sepIndex+1:] + if len(object) == 0 { + return bucket, Wildcard, nil + } + + if bucket == Wildcard && object != Wildcard { + return "", "", ErrInvalidResourceFormat + } + + return bucket, object, nil +} + +func splitGroupedConditions(groupedConditions []GroupedConditions) [][]chain.Condition { + var orConditions []chain.Condition + commonConditions := make([]chain.Condition, 0, len(groupedConditions)) + for _, grouped := range groupedConditions { + if grouped.Any { + orConditions = append(orConditions, grouped.Conditions...) + } else { + commonConditions = append(commonConditions, grouped.Conditions...) + } + } + + if len(orConditions) == 0 { + return [][]chain.Condition{commonConditions} + } + + res := make([][]chain.Condition, len(orConditions)) + for i, condition := range orConditions { + res[i] = append([]chain.Condition{condition}, commonConditions...) + } + + return res +} + +func formStatus(statement Statement) chain.Status { + status := chain.AccessDenied + if statement.Effect == AllowEffect { + status = chain.Allow + } + + return status +} diff --git a/iam/converter_native.go b/iam/converter_native.go new file mode 100644 index 0000000..4399ebe --- /dev/null +++ b/iam/converter_native.go @@ -0,0 +1,241 @@ +package iam + +import ( + "errors" + "fmt" + "strings" + + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" + "git.frostfs.info/TrueCloudLab/policy-engine/schema/native" +) + +const PropertyKeyFilePath = "FilePath" + +// ErrActionsNotApplicable occurs when failed to convert any actions. +var ErrActionsNotApplicable = errors.New("actions not applicable") + +var actionToOpMap = map[string][]string{ + supportedS3ActionDeleteObject: {native.MethodDeleteObject}, + supportedS3ActionGetObject: {native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}, + supportedS3ActionHeadObject: {native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}, + supportedS3ActionPutObject: {native.MethodPutObject}, + supportedS3ActionListBucket: {native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject}, +} + +const ( + supportedS3ActionDeleteObject = "DeleteObject" + supportedS3ActionGetObject = "GetObject" + supportedS3ActionHeadObject = "HeadObject" + supportedS3ActionPutObject = "PutObject" + supportedS3ActionListBucket = "ListBucket" +) + +type NativeResolver interface { + GetUserKey(account, name string) (string, error) + GetBucketCID(bucket string) (string, error) +} + +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) + + action, actionInverted := statement.action() + ruleAction := chain.Actions{Inverted: actionInverted, Names: formNativeActionNames(action)} + if len(ruleAction.Names) == 0 { + continue + } + + resource, resourceInverted := statement.resource() + groupedResources, err := formNativeResourceNamesAndConditions(resource, resolver) + 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 { + ruleConditions := append([]chain.Condition{principalCondFn(principal)}, 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 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 +} + +func formNativeResourceNamesAndConditions(names []string, resolver NativeResolver) ([]GroupedResources, error) { + res := make([]GroupedResources, 0, len(names)) + + var combined []string + + for i := range names { + bkt, obj, err := parseResourceAsS3ARN(names[i]) + if err != nil { + return nil, err + } + + if bkt == Wildcard { + res = res[:0] + return append(res, GroupedResources{Names: []string{native.ResourceFormatAllObjects}}), nil + } + + cnrID, err := resolver.GetBucketCID(bkt) + if err != nil { + return nil, err + } + resource := fmt.Sprintf(native.ResourceFormatRootContainerObjects, cnrID) + + if obj == Wildcard { + combined = append(combined, resource) + continue + } + + res = append(res, GroupedResources{ + Names: []string{resource}, + Conditions: []chain.Condition{ + { + Op: chain.CondStringLike, + Object: chain.ObjectResource, + Key: PropertyKeyFilePath, + Value: obj, + }, + }, + }) + } + + if len(combined) != 0 { + res = append(res, GroupedResources{Names: combined}) + } + + return res, nil +} + +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 { + res := make([]string, 0, len(names)) + + for i := range names { + trimmed := strings.TrimPrefix(names[i], s3ActionPrefix) + if trimmed == Wildcard { + return []string{Wildcard} + } + res = append(res, actionToOpMap[trimmed]...) + } + + return res +} diff --git a/iam/converter_s3.go b/iam/converter_s3.go new file mode 100644 index 0000000..7fed390 --- /dev/null +++ b/iam/converter_s3.go @@ -0,0 +1,153 @@ +package iam + +import ( + "fmt" + "strings" + + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" + "git.frostfs.info/TrueCloudLab/policy-engine/schema/s3" +) + +type S3Resolver interface { + GetUserAddress(account, user string) (string, error) +} + +func ConvertToS3Chain(p Policy, resolver S3Resolver) (*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) + + action, actionInverted := statement.action() + ruleAction := chain.Actions{Inverted: actionInverted, Names: formS3ActionNames(action)} + + resource, resourceInverted := statement.resource() + ruleResource := chain.Resources{Inverted: resourceInverted, Names: formS3ResourceNamesAndConditions(resource)} + + groupedConditions, err := convertToS3ChainCondition(statement.Conditions, resolver) + if err != nil { + return nil, err + } + splitConditions := splitGroupedConditions(groupedConditions) + + principals, principalCondFn, err := getS3PrincipalsAndConditionFunc(statement, resolver) + if err != nil { + return nil, err + } + + for _, principal := range principals { + for _, conditions := range splitConditions { + r := chain.Rule{ + Status: status, + Actions: ruleAction, + Resources: ruleResource, + Condition: append([]chain.Condition{principalCondFn(principal)}, conditions...), + } + engineChain.Rules = append(engineChain.Rules, r) + } + } + } + + return &engineChain, nil +} + +func getS3PrincipalsAndConditionFunc(statement Statement, resolver S3Resolver) ([]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 := formS3Principal(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: s3.PropertyKeyOwner, + Value: principal, + } + }, nil +} + +func convertToS3ChainCondition(c Conditions, resolver S3Resolver) ([]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 = s3.PropertyKeyOwner + val, err := formPrincipalOwner(gr.Conditions[i].Value, resolver) + if err != nil { + return GroupedConditions{}, err + } + gr.Conditions[i].Value = val + } + } + + return gr, nil + }) +} + +func formS3Principal(principal []string, resolver S3Resolver) ([]string, error) { + res := make([]string, len(principal)) + + var err error + for i := range principal { + if res[i], err = formPrincipalOwner(principal[i], resolver); err != nil { + return nil, err + } + } + + return res, nil +} + +func formPrincipalOwner(principal string, resolver S3Resolver) (string, error) { + account, user, err := parsePrincipalAsIAMUser(principal) + if err != nil { + return "", err + } + + address, err := resolver.GetUserAddress(account, user) + if err != nil { + return "", fmt.Errorf("get user address: %w", err) + } + + return address, nil +} + +func formS3ResourceNamesAndConditions(names []string) []string { + res := make([]string, len(names)) + for i := range names { + res[i] = strings.TrimPrefix(names[i], s3ResourcePrefix) + } + + return res +} + +func formS3ActionNames(names []string) []string { + res := make([]string, len(names)) + for i := range names { + res[i] = strings.TrimPrefix(names[i], s3ActionPrefix) + } + + return res +} diff --git a/iam/converter_test.go b/iam/converter_test.go index 27fcc04..1b9d2f7 100644 --- a/iam/converter_test.go +++ b/iam/converter_test.go @@ -1,23 +1,88 @@ package iam import ( + "errors" + "fmt" + "strconv" "testing" - chain "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/inmemory" + "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" ) +type mockUserResolver struct { + users map[string]string + buckets map[string]string +} + +func newMockUserResolver(accountUsers []string, buckets []string) *mockUserResolver { + userMap := make(map[string]string, len(accountUsers)) + for _, user := range accountUsers { + userMap[user] = user + "/resolvedValue" + } + + bucketMap := make(map[string]string, len(buckets)) + for _, bkt := range buckets { + bucketMap[bkt] = bkt + "/resolvedValues" + } + + return &mockUserResolver{users: userMap, buckets: bucketMap} +} + +func (m *mockUserResolver) GetUserAddress(account, user string) (string, error) { + key, ok := m.users[account+"/"+user] + if !ok { + return "", errors.New("not found") + } + + return key, nil +} + +func (m *mockUserResolver) GetUserKey(account, user string) (string, error) { + key, ok := m.users[account+"/"+user] + if !ok { + return "", errors.New("not found") + } + + return key, nil +} + +func (m *mockUserResolver) GetBucketCID(bkt string) (string, error) { + cnrID, ok := m.buckets[bkt] + if !ok { + return "", errors.New("not found") + } + + return cnrID, nil +} + func TestConverters(t *testing.T) { + namespace := "root" + userName := "JohnDoe" + user := namespace + "/" + userName + principal := "arn:aws:iam::" + namespace + ":user/" + userName + bktName := "DOC-EXAMPLE-BUCKET" + objName := "object-name" + resource := bktName + "/*" + action := "PutObject" + + mockResolver := newMockUserResolver([]string{user}, []string{bktName}) + t.Run("valid policy", func(t *testing.T) { p := Policy{ Version: "2012-10-17", Statement: []Statement{{ Principal: map[PrincipalType][]string{ - AWSPrincipalType: {"arn:aws:iam::111122223333:user/JohnDoe"}, + AWSPrincipalType: {principal}, }, Effect: AllowEffect, Action: []string{"s3:PutObject"}, - Resource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"}, + Resource: []string{"arn:aws:s3:::" + resource}, Conditions: map[string]Condition{ CondStringEquals: { "s3:RequestObjectTag/Department": {"Finance"}, @@ -29,15 +94,14 @@ func TestConverters(t *testing.T) { expected := &chain.Chain{Rules: []chain.Rule{ { Status: chain.Allow, - Actions: chain.Actions{Names: p.Statement[0].Action}, - Resources: chain.Resources{Names: p.Statement[0].Resource}, - Any: true, + Actions: chain.Actions{Names: []string{action}}, + Resources: chain.Resources{Names: []string{resource}}, Condition: []chain.Condition{ { Op: chain.CondStringEquals, Object: chain.ObjectRequest, - Key: RequestOwnerProperty, - Value: "arn:aws:iam::111122223333:user/JohnDoe", + Key: s3.PropertyKeyOwner, + Value: mockResolver.users[user], }, { Op: chain.CondStringEquals, @@ -49,9 +113,43 @@ func TestConverters(t *testing.T) { }, }} - chain, err := p.ToChain() + s3Chain, err := ConvertToS3Chain(p, mockResolver) require.NoError(t, err) - require.Equal(t, expected, chain) + require.Equal(t, expected, s3Chain) + }) + + t.Run("valid native policy", func(t *testing.T) { + p := Policy{ + Version: "2012-10-17", + Statement: []Statement{{ + Principal: map[PrincipalType][]string{ + AWSPrincipalType: {principal}, + }, + Effect: AllowEffect, + Action: []string{"s3:PutObject"}, + Resource: []string{"arn:aws:s3:::" + resource}, + }}, + } + + expected := &chain.Chain{Rules: []chain.Rule{ + { + Status: chain.Allow, + Actions: chain.Actions{Names: []string{action}}, + Resources: chain.Resources{Names: []string{fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName])}}, + Condition: []chain.Condition{ + { + Op: chain.CondStringEquals, + Object: chain.ObjectRequest, + Key: native.PropertyKeyActorPublicKey, + Value: mockResolver.users[user], + }, + }, + }, + }} + + nativeChain, err := ConvertToNativeChain(p, mockResolver) + require.NoError(t, err) + require.Equal(t, expected, nativeChain) }) t.Run("valid inverted policy", func(t *testing.T) { @@ -59,34 +157,75 @@ func TestConverters(t *testing.T) { Version: "2012-10-17", Statement: []Statement{{ NotPrincipal: map[PrincipalType][]string{ - AWSPrincipalType: {"arn:aws:iam::111122223333:user/JohnDoe"}, + AWSPrincipalType: {principal}, }, Effect: DenyEffect, NotAction: []string{"s3:PutObject"}, - NotResource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"}, + NotResource: []string{"arn:aws:s3:::" + resource}, }}, } expected := &chain.Chain{Rules: []chain.Rule{ { Status: chain.AccessDenied, - Actions: chain.Actions{Inverted: true, Names: p.Statement[0].NotAction}, - Resources: chain.Resources{Inverted: true, Names: p.Statement[0].NotResource}, - Any: true, + Actions: chain.Actions{Inverted: true, Names: []string{action}}, + Resources: chain.Resources{Inverted: true, Names: []string{resource}}, Condition: []chain.Condition{ { Op: chain.CondStringNotEquals, Object: chain.ObjectRequest, - Key: RequestOwnerProperty, - Value: "arn:aws:iam::111122223333:user/JohnDoe", + Key: s3.PropertyKeyOwner, + Value: mockResolver.users[user], }, }, }, }} - chain, err := p.ToChain() + s3Chain, err := ConvertToS3Chain(p, mockResolver) require.NoError(t, err) - require.Equal(t, expected, chain) + require.Equal(t, expected, s3Chain) + }) + + t.Run("valid policy map get action", func(t *testing.T) { + p := Policy{ + Version: "2012-10-17", + Statement: []Statement{{ + Principal: map[PrincipalType][]string{ + AWSPrincipalType: {principal}, + }, + Effect: DenyEffect, + NotAction: []string{"s3:GetObject"}, + NotResource: []string{"arn:aws:s3:::" + bktName + "/" + objName}, + }}, + } + + expected := &chain.Chain{Rules: []chain.Rule{ + { + Status: chain.AccessDenied, + Actions: chain.Actions{Inverted: true, Names: actionToOpMap["GetObject"]}, + Resources: chain.Resources{Inverted: true, Names: []string{ + fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName]), + }}, + Condition: []chain.Condition{ + { + Op: chain.CondStringEquals, + Object: chain.ObjectRequest, + Key: native.PropertyKeyActorPublicKey, + Value: mockResolver.users[user], + }, + { + Op: chain.CondStringLike, + Object: chain.ObjectResource, + Key: PropertyKeyFilePath, + Value: objName, + }, + }, + }, + }} + + nativeChain, err := ConvertToNativeChain(p, mockResolver) + require.NoError(t, err) + require.Equal(t, expected, nativeChain) }) t.Run("invalid policy (unsupported principal type)", func(t *testing.T) { @@ -94,7 +233,7 @@ func TestConverters(t *testing.T) { Version: "2012-10-17", Statement: []Statement{{ Principal: map[PrincipalType][]string{ - "dummy": {"arn:aws:iam::111122223333:user/JohnDoe"}, + "dummy": {principal}, }, Effect: AllowEffect, Action: []string{"s3:PutObject"}, @@ -102,7 +241,7 @@ func TestConverters(t *testing.T) { }}, } - _, err := p.ToChain() + _, err := ConvertToS3Chain(p, mockResolver) require.Error(t, err) }) @@ -111,196 +250,881 @@ func TestConverters(t *testing.T) { Version: "2012-10-17", Statement: []Statement{{ Principal: map[PrincipalType][]string{ - AWSPrincipalType: {"arn:aws:iam::111122223333:user/JohnDoe"}, + AWSPrincipalType: {principal}, }, Effect: AllowEffect, Action: []string{"s3:PutObject"}, }}, } - _, err := p.ToChain() + _, err := ConvertToS3Chain(p, mockResolver) require.Error(t, err) }) - t.Run("check policy conditions", func(t *testing.T) { + t.Run("invalid policy (not applicable native actions)", func(t *testing.T) { p := Policy{ Version: "2012-10-17", Statement: []Statement{{ - Principal: map[PrincipalType][]string{Wildcard: nil}, - Effect: AllowEffect, - Action: []string{"s3:PutObject"}, - Resource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"}, - Conditions: Conditions{ - CondStringEquals: {"key1": {"val0", "val1"}}, - CondStringNotEquals: {"key2": {"val2"}}, - CondStringEqualsIgnoreCase: {"key3": {"val3"}}, - CondStringNotEqualsIgnoreCase: {"key4": {"val4"}}, - CondStringLike: {"key5": {"val5"}}, - CondStringNotLike: {"key6": {"val6"}}, - CondDateEquals: {"key7": {"2006-01-02T15:04:05+07:00"}}, - CondDateNotEquals: {"key8": {"2006-01-02T15:04:05Z"}}, - CondDateLessThan: {"key9": {"2006-01-02T15:04:05+06:00"}}, - CondDateLessThanEquals: {"key10": {"2006-01-02T15:04:05+03:00"}}, - CondDateGreaterThan: {"key11": {"2006-01-02T15:04:05-01:00"}}, - CondDateGreaterThanEquals: {"key12": {"2006-01-02T15:04:05-03:00"}}, - CondBool: {"key13": {"True"}}, - CondIPAddress: {"key14": {"val14"}}, - CondNotIPAddress: {"key15": {"val15"}}, - CondArnEquals: {"key16": {"val16"}}, - CondArnLike: {"key17": {"val17"}}, - CondArnNotEquals: {"key18": {"val18"}}, - CondArnNotLike: {"key19": {"val19"}}, + Principal: map[PrincipalType][]string{ + AWSPrincipalType: {principal}, }, + Effect: AllowEffect, + Action: []string{"s3:AbortMultipartUpload"}, + Resource: []string{"arn:aws:s3:::" + resource}, }}, } - expected := &chain.Chain{Rules: []chain.Rule{ - { - Status: chain.Allow, - Actions: chain.Actions{Names: p.Statement[0].Action}, - Resources: chain.Resources{Names: p.Statement[0].Resource}, - Any: true, - Condition: []chain.Condition{ - { - Op: chain.CondStringLike, - Object: chain.ObjectRequest, - Key: RequestOwnerProperty, - Value: "*", - }, - { - Op: chain.CondStringEquals, - Object: chain.ObjectRequest, - Key: "key1", - Value: "val0", - }, - { - Op: chain.CondStringEquals, - Object: chain.ObjectRequest, - Key: "key1", - Value: "val1", - }, - { - Op: chain.CondStringNotEquals, - Object: chain.ObjectRequest, - Key: "key2", - Value: "val2", - }, - { - Op: chain.CondStringEqualsIgnoreCase, - Object: chain.ObjectRequest, - Key: "key3", - Value: "val3", - }, - { - Op: chain.CondStringNotEqualsIgnoreCase, - Object: chain.ObjectRequest, - Key: "key4", - Value: "val4", - }, - { - Op: chain.CondStringLike, - Object: chain.ObjectRequest, - Key: "key5", - Value: "val5", - }, - { - Op: chain.CondStringNotLike, - Object: chain.ObjectRequest, - Key: "key6", - Value: "val6", - }, - { - Op: chain.CondStringEquals, - Object: chain.ObjectRequest, - Key: "key7", - Value: "1136189045", - }, - { - Op: chain.CondStringNotEquals, - Object: chain.ObjectRequest, - Key: "key8", - Value: "1136214245", - }, - { - Op: chain.CondStringLessThan, - Object: chain.ObjectRequest, - Key: "key9", - Value: "1136192645", - }, - { - Op: chain.CondStringLessThanEquals, - Object: chain.ObjectRequest, - Key: "key10", - Value: "1136203445", - }, - { - Op: chain.CondStringGreaterThan, - Object: chain.ObjectRequest, - Key: "key11", - Value: "1136217845", - }, - { - Op: chain.CondStringGreaterThanEquals, - Object: chain.ObjectRequest, - Key: "key12", - Value: "1136225045", - }, - { - Op: chain.CondStringEqualsIgnoreCase, - Object: chain.ObjectRequest, - Key: "key13", - Value: "True", - }, - { - Op: chain.CondStringLike, - Object: chain.ObjectRequest, - Key: "key14", - Value: "val14", - }, - { - Op: chain.CondStringNotLike, - Object: chain.ObjectRequest, - Key: "key15", - Value: "val15", - }, - { - Op: chain.CondStringEquals, - Object: chain.ObjectRequest, - Key: "key16", - Value: "val16", - }, - { - Op: chain.CondStringLike, - Object: chain.ObjectRequest, - Key: "key17", - Value: "val17", - }, - { - Op: chain.CondStringNotEquals, - Object: chain.ObjectRequest, - Key: "key18", - Value: "val18", - }, - { - Op: chain.CondStringNotLike, - Object: chain.ObjectRequest, - Key: "key19", - Value: "val19", - }, - }, - }, - }} - - chain, err := p.ToChain() - require.NoError(t, err) - - for i, rule := range chain.Rules { - expectedRule := expected.Rules[i] - require.Equal(t, expectedRule.Actions, rule.Actions) - require.Equal(t, expectedRule.Any, rule.Any) - require.Equal(t, expectedRule.Resources, rule.Resources) - require.Equal(t, expectedRule.Status, rule.Status) - require.ElementsMatch(t, expectedRule.Condition, rule.Condition) - } + _, err := ConvertToNativeChain(p, mockResolver) + require.Error(t, err) }) } + +func TestConvertToChainCondition(t *testing.T) { + principal := "arn:aws:iam::namespace:user/userName" + + conditions := Conditions{ + CondStringEquals: {"key1": {"val0", "val1"}}, + CondStringNotEquals: {"key2": {"val2"}}, + CondStringEqualsIgnoreCase: {"key3": {"val3"}}, + CondStringNotEqualsIgnoreCase: {"key4": {"val4"}}, + CondStringLike: {"key5": {"val5"}}, + CondStringNotLike: {"key6": {"val6"}}, + CondDateEquals: {"key7": {"2006-01-02T15:04:05+07:00"}}, + CondDateNotEquals: {"key8": {"2006-01-02T15:04:05Z"}}, + CondDateLessThan: {"key9": {"2006-01-02T15:04:05+06:00"}}, + CondDateLessThanEquals: {"key10": {"2006-01-02T15:04:05+03:00"}}, + CondDateGreaterThan: {"key11": {"2006-01-02T15:04:05-01:00"}}, + CondDateGreaterThanEquals: {"key12": {"2006-01-02T15:04:05-03:00"}}, + CondBool: {"key13": {"True"}}, + CondIPAddress: {"key14": {"val14"}}, + CondNotIPAddress: {"key15": {"val15"}}, + CondArnEquals: {"key16": {"val16"}}, + CondArnLike: {condKeyAWSPrincipalARN: {principal}}, + CondArnNotEquals: {"key18": {"val18"}}, + CondArnNotLike: {"key19": {"val19"}}, + } + + expectedCondition := []GroupedConditions{ + { + Any: true, + Conditions: []chain.Condition{ + { + Op: chain.CondStringEquals, + Object: chain.ObjectRequest, + Key: "key1", + Value: "val0", + }, + { + Op: chain.CondStringEquals, + Object: chain.ObjectRequest, + Key: "key1", + Value: "val1", + }, + }, + }, + { + Conditions: []chain.Condition{{ + Op: chain.CondStringNotEquals, + Object: chain.ObjectRequest, + Key: "key2", + Value: "val2", + }}, + }, + { + Conditions: []chain.Condition{{ + Op: chain.CondStringEqualsIgnoreCase, + Object: chain.ObjectRequest, + Key: "key3", + Value: "val3", + }}, + }, + { + Conditions: []chain.Condition{{ + Op: chain.CondStringNotEqualsIgnoreCase, + Object: chain.ObjectRequest, + Key: "key4", + Value: "val4", + }}, + }, + { + Conditions: []chain.Condition{{ + Op: chain.CondStringLike, + Object: chain.ObjectRequest, + Key: "key5", + Value: "val5", + }}, + }, + { + Conditions: []chain.Condition{{ + Op: chain.CondStringNotLike, + Object: chain.ObjectRequest, + Key: "key6", + Value: "val6", + }}, + }, + { + Conditions: []chain.Condition{{ + Op: chain.CondStringEquals, + Object: chain.ObjectRequest, + Key: "key7", + Value: "1136189045", + }}, + }, + { + Conditions: []chain.Condition{{ + Op: chain.CondStringNotEquals, + Object: chain.ObjectRequest, + Key: "key8", + Value: "1136214245", + }}, + }, + { + Conditions: []chain.Condition{{ + Op: chain.CondStringLessThan, + Object: chain.ObjectRequest, + Key: "key9", + Value: "1136192645", + }}, + }, + { + Conditions: []chain.Condition{{ + Op: chain.CondStringLessThanEquals, + Object: chain.ObjectRequest, + Key: "key10", + Value: "1136203445", + }}, + }, + { + Conditions: []chain.Condition{{ + Op: chain.CondStringGreaterThan, + Object: chain.ObjectRequest, + Key: "key11", + Value: "1136217845", + }}, + }, + { + Conditions: []chain.Condition{{ + Op: chain.CondStringGreaterThanEquals, + Object: chain.ObjectRequest, + Key: "key12", + Value: "1136225045", + }}, + }, + { + Conditions: []chain.Condition{{ + Op: chain.CondStringEqualsIgnoreCase, + Object: chain.ObjectRequest, + Key: "key13", + Value: "True", + }}, + }, + { + Conditions: []chain.Condition{{ + Op: chain.CondStringLike, + Object: chain.ObjectRequest, + Key: "key14", + Value: "val14", + }}, + }, + { + Conditions: []chain.Condition{{ + Op: chain.CondStringNotLike, + Object: chain.ObjectRequest, + Key: "key15", + Value: "val15", + }}, + }, + { + Conditions: []chain.Condition{{ + Op: chain.CondStringEquals, + Object: chain.ObjectRequest, + Key: "key16", + Value: "val16", + }}, + }, + { + Conditions: []chain.Condition{{ + Op: chain.CondStringLike, + Object: chain.ObjectRequest, + Key: condKeyAWSPrincipalARN, + Value: principal, + }}, + }, + { + Conditions: []chain.Condition{{ + Op: chain.CondStringNotEquals, + Object: chain.ObjectRequest, + Key: "key18", + Value: "val18", + }}, + }, + { + Conditions: []chain.Condition{{ + Op: chain.CondStringNotLike, + Object: chain.ObjectRequest, + Key: "key19", + Value: "val19", + }}, + }, + } + + actualCondition, err := convertToChainCondition(conditions) + require.NoError(t, err) + require.ElementsMatch(t, expectedCondition, actualCondition) +} + +func TestParsePrincipalARN(t *testing.T) { + for i, tc := range []struct { + principal string + account string + user string + error bool + }{ + { + principal: "arn:aws:iam::root:user/user", + account: "root", + user: "user", + error: false, + }, + { + principal: "arn:aws:iam::root:user/path/user/user2", + account: "root", + user: "user2", + error: false, + }, + { + principal: "arn:aws:iam::root:user/", + error: true, + }, + { + principal: "root:user/name", + error: true, + }, + { + principal: "arn:aws:iam::root:user", + error: true, + }, + { + principal: "arn:aws:iam::root:name", + error: true, + }, + { + principal: "arn:aws:iam::root:user/path/user/", + error: true, + }, + } { + t.Run(strconv.Itoa(i), func(t *testing.T) { + account, user, err := parsePrincipalAsIAMUser(tc.principal) + if tc.error { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, tc.account, account) + require.Equal(t, tc.user, user) + }) + } +} + +func TestComplexNativeConditions(t *testing.T) { + namespace := "root" + userName1, userName2 := "user1", "user2" + user1, user2 := namespace+"/"+userName1, namespace+"/"+userName2 + principal1 := "arn:aws:iam::" + namespace + ":user/" + userName1 + principal2 := "arn:aws:iam::" + namespace + ":user/" + userName2 + bktName1, bktName2, bktName3 := "bktName", "bktName2", "bktName3" + objName1 := "objName1" + resource1 := bktName1 + "/" + objName1 + resource2 := bktName2 + "/*" + resource3 := bktName3 + "/*" + action := "PutObject" + + key1, key2 := "key1", "key2" + val0, val1, val2 := "val0", "val1", "val2" + + mockResolver := newMockUserResolver([]string{user1, user2}, []string{bktName1, bktName2, bktName3}) + nativeResource1 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName1]) + nativeResource2 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName2]) + nativeResource3 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName3]) + + p := Policy{ + Version: "2012-10-17", + Statement: []Statement{{ + Principal: map[PrincipalType][]string{ + AWSPrincipalType: {principal1, principal2}, + }, + Effect: DenyEffect, + Action: []string{"s3:" + action}, + Resource: []string{"arn:aws:s3:::" + resource1, "arn:aws:s3:::" + resource2, "arn:aws:s3:::" + resource3}, + Conditions: map[string]Condition{ + CondStringEquals: {key1: {val0, val1}}, + CondStringLike: {key2: {val2}}, + }, + }}, + } + + expectedStatus := chain.AccessDenied + expectedActions := chain.Actions{Names: actionToOpMap[action]} + expectedResource1 := chain.Resources{Names: []string{nativeResource1}} + expectedResource23 := chain.Resources{Names: []string{nativeResource2, nativeResource3}} + + 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]} + objectName1Condition := chain.Condition{Op: chain.CondStringLike, Object: chain.ObjectResource, Key: PropertyKeyFilePath, Value: objName1} + key1val0Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: key1, Value: val0} + key1val1Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: key1, Value: val1} + key2val2Condition := chain.Condition{Op: chain.CondStringLike, Object: chain.ObjectRequest, Key: key2, Value: val2} + + expected := &chain.Chain{Rules: []chain.Rule{ + { + Status: expectedStatus, + Actions: expectedActions, + Resources: expectedResource1, + Condition: []chain.Condition{ + user1Condition, + objectName1Condition, + key1val0Condition, + key2val2Condition, + }, + }, + { + Status: expectedStatus, + Actions: expectedActions, + Resources: expectedResource1, + Condition: []chain.Condition{ + user1Condition, + objectName1Condition, + key1val1Condition, + key2val2Condition, + }, + }, + { + Status: expectedStatus, + Actions: expectedActions, + Resources: expectedResource1, + Condition: []chain.Condition{ + user2Condition, + objectName1Condition, + key1val0Condition, + key2val2Condition, + }, + }, + { + Status: expectedStatus, + Actions: expectedActions, + Resources: expectedResource1, + Condition: []chain.Condition{ + user2Condition, + objectName1Condition, + key1val1Condition, + key2val2Condition, + }, + }, + { + Status: expectedStatus, + Actions: expectedActions, + Resources: expectedResource23, + Condition: []chain.Condition{ + user1Condition, + key1val0Condition, + key2val2Condition, + }, + }, + { + Status: expectedStatus, + Actions: expectedActions, + Resources: expectedResource23, + Condition: []chain.Condition{ + user1Condition, + key1val1Condition, + key2val2Condition, + }, + }, + { + Status: expectedStatus, + Actions: expectedActions, + Resources: expectedResource23, + Condition: []chain.Condition{ + user2Condition, + key1val0Condition, + key2val2Condition, + }, + }, + { + Status: expectedStatus, + Actions: expectedActions, + Resources: expectedResource23, + Condition: []chain.Condition{ + user2Condition, + key1val1Condition, + key2val2Condition, + }, + }, + }} + + nativeChain, err := ConvertToNativeChain(p, mockResolver) + require.NoError(t, err) + requireChainRulesMatch(t, expected.Rules, nativeChain.Rules) + + s := inmemory.NewInMemory() + err = s.MorphRuleChainStorage().AddMorphRuleChain("name", engine.NamespaceTarget("ns"), nativeChain) + require.NoError(t, err) + + for _, tc := range []struct { + name string + action string + resource string + resourceMap map[string]string + requestMap map[string]string + status chain.Status + }{ + { + name: "bucket resource1, all conditions matched", + action: action, + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName2], "some-oid"), + resourceMap: map[string]string{ + PropertyKeyFilePath: "any-object-name", + }, + requestMap: map[string]string{ + native.PropertyKeyActorPublicKey: mockResolver.users[user1], + key1: val0, + key2: val2, + }, + status: chain.AccessDenied, + }, + { + name: "bucket resource3, all conditions matched", + action: action, + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName3], "some-oid"), + resourceMap: map[string]string{ + PropertyKeyFilePath: "any-object-name", + }, + requestMap: map[string]string{ + native.PropertyKeyActorPublicKey: mockResolver.users[user1], + key1: val0, + key2: val2, + }, + status: chain.AccessDenied, + }, + { + name: "bucket resource, user condition mismatched", + action: action, + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName2], "some-oid"), + resourceMap: map[string]string{ + PropertyKeyFilePath: "any-object-name", + }, + requestMap: map[string]string{ + key1: val0, + key2: val2, + }, + status: chain.NoRuleFound, + }, + { + name: "bucket resource, key2 condition mismatched", + action: action, + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName3], "some-oid"), + resourceMap: map[string]string{ + PropertyKeyFilePath: "any-object-name", + }, + requestMap: map[string]string{ + native.PropertyKeyActorPublicKey: mockResolver.users[user1], + key1: val0, + key2: val0, + }, + status: chain.NoRuleFound, + }, + { + name: "bucket resource, key1 condition mismatched", + action: action, + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName2], "some-oid"), + resourceMap: map[string]string{ + PropertyKeyFilePath: "any-object-name", + }, + requestMap: map[string]string{ + native.PropertyKeyActorPublicKey: mockResolver.users[user1], + key2: val2, + }, + status: chain.NoRuleFound, + }, + { + name: "bucket/object resource, all conditions matched", + action: action, + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName1], "some-oid"), + resourceMap: map[string]string{ + PropertyKeyFilePath: objName1, + }, + requestMap: map[string]string{ + native.PropertyKeyActorPublicKey: mockResolver.users[user1], + key1: val0, + key2: val2, + }, + status: chain.AccessDenied, + }, + { + name: "bucket/object resource, user condition mismatched", + action: action, + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName1], "some-oid"), + resourceMap: map[string]string{ + PropertyKeyFilePath: objName1, + }, + requestMap: map[string]string{ + native.PropertyKeyActorPublicKey: "dummy", + key1: val0, + key2: val2, + }, + status: chain.NoRuleFound, + }, + { + name: "bucket/object resource, key1 condition mismatched", + action: action, + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName1], "some-oid"), + resourceMap: map[string]string{ + PropertyKeyFilePath: objName1, + }, + requestMap: map[string]string{ + native.PropertyKeyActorPublicKey: "dummy", + key2: val2, + }, + status: chain.NoRuleFound, + }, + { + name: "bucket/object resource, key2 condition mismatched", + action: action, + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName1], "some-oid"), + resourceMap: map[string]string{ + PropertyKeyFilePath: objName1, + }, + requestMap: map[string]string{ + native.PropertyKeyActorPublicKey: "dummy", + key1: val0, + key2: val0, + }, + status: chain.NoRuleFound, + }, + { + name: "bucket/object resource, object filepath condition mismatched", + action: action, + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName1], "some-oid"), + resourceMap: map[string]string{ + PropertyKeyFilePath: "any-object-name", + }, + requestMap: map[string]string{ + native.PropertyKeyActorPublicKey: mockResolver.users[user1], + key1: val0, + key2: val2, + }, + status: chain.NoRuleFound, + }, + { + name: "resource mismatched", + action: action, + resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, "some-cid", "some-oid"), + resourceMap: map[string]string{ + PropertyKeyFilePath: objName1, + }, + requestMap: map[string]string{ + native.PropertyKeyActorPublicKey: mockResolver.users[user1], + key1: val0, + key2: val2, + }, + status: chain.NoRuleFound, + }, + } { + t.Run(tc.name, func(t *testing.T) { + req := testutil.NewRequest(tc.action, testutil.NewResource(tc.resource, tc.resourceMap), tc.requestMap) + status, _, err := s.IsAllowed("name", "ns", req) + require.NoError(t, err) + require.Equal(t, tc.status.String(), status.String()) + }) + } +} + +func TestComplexS3Conditions(t *testing.T) { + namespace := "root" + userName1, userName2 := "user1", "user2" + user1, user2 := namespace+"/"+userName1, namespace+"/"+userName2 + principal1 := "arn:aws:iam::" + namespace + ":user/" + userName1 + principal2 := "arn:aws:iam::" + namespace + ":user/" + userName2 + bktName1, bktName2, bktName3 := "bktName", "bktName2", "bktName3" + objName1 := "objName1" + resource1 := bktName1 + "/" + objName1 + resource2 := bktName2 + "/*" + resource3 := bktName3 + "/*" + action := "PutObject" + + key1, key2 := "key1", "key2" + val0, val1, val2 := "val0", "val1", "val2" + + mockResolver := newMockUserResolver([]string{user1, user2}, []string{bktName1, bktName2, bktName3}) + + p := Policy{ + Version: "2012-10-17", + Statement: []Statement{{ + Principal: map[PrincipalType][]string{ + AWSPrincipalType: {principal1, principal2}, + }, + Effect: DenyEffect, + Action: []string{"s3:" + action}, + Resource: []string{"arn:aws:s3:::" + resource1, "arn:aws:s3:::" + resource2, "arn:aws:s3:::" + resource3}, + Conditions: map[string]Condition{ + CondStringEquals: {key1: {val0, val1}}, + CondStringLike: {key2: {val2}}, + }, + }}, + } + + expectedStatus := chain.AccessDenied + expectedActions := chain.Actions{Names: actionToOpMap[action]} + expectedResources := chain.Resources{Names: []string{resource1, resource2, resource3}} + + user1Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: s3.PropertyKeyOwner, Value: mockResolver.users[user1]} + user2Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: s3.PropertyKeyOwner, Value: mockResolver.users[user2]} + key1val0Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: key1, Value: val0} + key1val1Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: key1, Value: val1} + key2val2Condition := chain.Condition{Op: chain.CondStringLike, Object: chain.ObjectRequest, Key: key2, Value: val2} + + expected := &chain.Chain{Rules: []chain.Rule{ + { + Status: expectedStatus, + Actions: expectedActions, + Resources: expectedResources, + Condition: []chain.Condition{ + user1Condition, + key1val0Condition, + key2val2Condition, + }, + }, + { + Status: expectedStatus, + Actions: expectedActions, + Resources: expectedResources, + Condition: []chain.Condition{ + user1Condition, + key1val1Condition, + key2val2Condition, + }, + }, + { + Status: expectedStatus, + Actions: expectedActions, + Resources: expectedResources, + Condition: []chain.Condition{ + user2Condition, + key1val0Condition, + key2val2Condition, + }, + }, + { + Status: expectedStatus, + Actions: expectedActions, + Resources: expectedResources, + Condition: []chain.Condition{ + user2Condition, + key1val1Condition, + key2val2Condition, + }, + }, + }} + + s3Chain, err := ConvertToS3Chain(p, mockResolver) + require.NoError(t, err) + requireChainRulesMatch(t, expected.Rules, s3Chain.Rules) + + s := inmemory.NewInMemory() + err = s.MorphRuleChainStorage().AddMorphRuleChain("name", engine.NamespaceTarget("ns"), s3Chain) + require.NoError(t, err) + + for _, tc := range []struct { + name string + action string + resource string + resourceMap map[string]string + requestMap map[string]string + status chain.Status + }{ + { + name: "bucket resource1, all conditions matched", + action: action, + resource: resource1, + requestMap: map[string]string{ + s3.PropertyKeyOwner: mockResolver.users[user1], + key1: val0, + key2: val2, + }, + status: chain.AccessDenied, + }, + { + name: "bucket resource3, all conditions matched", + action: action, + resource: bktName3 + "/some-obj", + requestMap: map[string]string{ + s3.PropertyKeyOwner: mockResolver.users[user1], + key1: val0, + key2: val2, + }, + status: chain.AccessDenied, + }, + { + name: "bucket resource, user condition mismatched", + action: action, + resource: bktName2 + "/some-obj", + requestMap: map[string]string{ + key1: val0, + key2: val2, + }, + status: chain.NoRuleFound, + }, + { + name: "bucket resource, key2 condition mismatched", + action: action, + resource: bktName3 + "/some-obj", + requestMap: map[string]string{ + s3.PropertyKeyOwner: mockResolver.users[user1], + key1: val0, + key2: val0, + }, + status: chain.NoRuleFound, + }, + { + name: "bucket resource, key1 condition mismatched", + action: action, + resource: bktName2 + "/some-obj", + requestMap: map[string]string{ + s3.PropertyKeyOwner: mockResolver.users[user1], + key2: val2, + }, + status: chain.NoRuleFound, + }, + { + name: "bucket/object resource, all conditions matched", + action: action, + resource: resource1, + requestMap: map[string]string{ + s3.PropertyKeyOwner: mockResolver.users[user1], + key1: val0, + key2: val2, + }, + status: chain.AccessDenied, + }, + { + name: "bucket/object resource, resource mismatched", + action: action, + resource: bktName1 + "/some-obj", + requestMap: map[string]string{ + s3.PropertyKeyOwner: mockResolver.users[user1], + key1: val0, + key2: val2, + }, + status: chain.NoRuleFound, + }, + { + name: "bucket/object resource, user condition mismatched", + action: action, + resource: resource1, + requestMap: map[string]string{ + s3.PropertyKeyOwner: "dummy", + key1: val0, + key2: val2, + }, + status: chain.NoRuleFound, + }, + { + name: "bucket/object resource, key1 condition mismatched", + action: action, + resource: resource1, + requestMap: map[string]string{ + s3.PropertyKeyOwner: "dummy", + key2: val2, + }, + status: chain.NoRuleFound, + }, + { + name: "bucket/object resource, key2 condition mismatched", + action: action, + resource: resource1, + requestMap: map[string]string{ + s3.PropertyKeyOwner: "dummy", + key1: val0, + key2: val0, + }, + status: chain.NoRuleFound, + }, + { + name: "resource mismatched", + action: action, + resource: "some-bkt/some-obj", + requestMap: map[string]string{ + s3.PropertyKeyOwner: mockResolver.users[user1], + key1: val0, + key2: val2, + }, + status: chain.NoRuleFound, + }, + } { + t.Run(tc.name, func(t *testing.T) { + req := testutil.NewRequest(tc.action, testutil.NewResource(tc.resource, tc.resourceMap), tc.requestMap) + status, _, err := s.IsAllowed("name", "ns", req) + require.NoError(t, err) + require.Equal(t, tc.status.String(), status.String()) + }) + } +} + +func requireChainRulesMatch(t *testing.T, expected, actual []chain.Rule) { + require.Equal(t, len(expected), len(actual), "length of chain rules differ") + + seen := make(map[int]int) + for i, expRule := range expected { + for j, actRule := range actual { + if _, ok := seen[j]; ok { + continue + } + + if areRulesMatched(expRule, actRule) { + seen[j] = i + break + } + } + } + + require.Len(t, seen, len(expected), "expected unique rules") +} + +func areRulesMatched(rule1, rule2 chain.Rule) bool { + if rule1.Status != rule2.Status || + rule1.Any != rule2.Any || + rule1.Resources.Inverted != rule2.Resources.Inverted || + len(rule1.Resources.Names) != len(rule2.Resources.Names) || + rule1.Actions.Inverted != rule2.Actions.Inverted || + len(rule1.Actions.Names) != len(rule2.Actions.Names) { + 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{}) + for _, cond1 := range rule1.Condition { + for j, cond2 := range rule2.Condition { + if _, ok := seen[j]; ok { + continue + } + if cond1 == cond2 { + seen[j] = struct{}{} + break + } + } + } + + return len(seen) == len(rule1.Condition) +} diff --git a/iam/policy.go b/iam/policy.go index 956b239..d5649fa 100644 --- a/iam/policy.go +++ b/iam/policy.go @@ -222,6 +222,10 @@ func (p Policy) Validate(typ PolicyType) error { } func (p Policy) validate() error { + if len(p.Statement) == 0 { + return errors.New("'Statement' is missing") + } + for _, statement := range p.Statement { if !statement.Effect.IsValid() { return fmt.Errorf("unknown effect: '%s'", statement.Effect) diff --git a/iam/policy_test.go b/iam/policy_test.go index 11d0fcc..6172655 100644 --- a/iam/policy_test.go +++ b/iam/policy_test.go @@ -320,6 +320,12 @@ func TestValidatePolicies(t *testing.T) { typ: GeneralPolicyType, isValid: false, }, + { + name: "missing statement block", + policy: Policy{}, + typ: GeneralPolicyType, + isValid: false, + }, { name: "identity based valid", policy: Policy{ diff --git a/schema/s3/consts.go b/schema/s3/consts.go new file mode 100644 index 0000000..a8de81f --- /dev/null +++ b/schema/s3/consts.go @@ -0,0 +1,9 @@ +package s3 + +const ( + PropertyKeyOwner = "Owner" + + PropertyKeyDelimiter = "s3:delimiter" + PropertyKeyPrefix = "s3:prefix" + PropertyKeyVersionID = "s3:VersionId" +)