[#17] iam: Add converter to native/s3 policy
All checks were successful
Tests and linters / Tests (1.21) (pull_request) Successful in 1m5s
Tests and linters / Tests (1.20) (pull_request) Successful in 1m19s
DCO action / DCO (pull_request) Successful in 1m4s
Tests and linters / Staticcheck (pull_request) Successful in 1m25s
Tests and linters / Tests with -race (pull_request) Successful in 1m35s
Tests and linters / Lint (pull_request) Successful in 2m14s

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
This commit is contained in:
Denis Kirillov 2023-11-10 17:56:41 +03:00
parent 5db67021e1
commit c9d4d15db6
9 changed files with 1683 additions and 346 deletions

16
go.mod
View file

@ -2,10 +2,24 @@ module git.frostfs.info/TrueCloudLab/policy-engine
go 1.20
require github.com/stretchr/testify v1.8.1
require (
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20231114081800-3787477133f3
github.com/nspcc-dev/neo-go v0.103.1
github.com/stretchr/testify v1.8.4
)
require (
git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.15.1-0.20230802075510-964c3edb3f44 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/hashicorp/golang-lru v0.6.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/nspcc-dev/rfc6979 v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/text v0.13.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

43
go.sum
View file

@ -1,17 +1,40 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.15.1-0.20230802075510-964c3edb3f44 h1:v6JqBD/VzZx3QSxbaXnUwnnJ1KEYheU4LzLGr3IhsAE=
git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.15.1-0.20230802075510-964c3edb3f44/go.mod h1:pKJJRLOChW4zDQsAt1e8k/snWKljJtpkiPfxV53ngjI=
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20231114081800-3787477133f3 h1:Qa35bB58plMb14LIsYzu2ibeYfsY2taFnbZytv9Ao3M=
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20231114081800-3787477133f3/go.mod h1:t1akKcUH7iBrFHX8rSXScYMP17k2kYQXMbZooiL5Juw=
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/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/nspcc-dev/neo-go v0.103.1 h1:BfRBceHUu8jSc1KQy7CzmQ/pa+xzAmgcyteGf0/IGgM=
github.com/nspcc-dev/neo-go v0.103.1/go.mod h1:MD7MPiyshUwrE5n1/LzxeandbItaa/iLW/bJb6gNs/U=
github.com/nspcc-dev/rfc6979 v0.2.0 h1:3e1WNxrN60/6N0DW7+UYisLeZJyfqZTNOjeV/toYvOE=
github.com/nspcc-dev/rfc6979 v0.2.0/go.mod h1:exhIh1PdpDC5vQmyEsGvc4YDM/lyQp/452QxGq/UEso=
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=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -1,17 +1,18 @@
package iam
import (
"errors"
"fmt"
"strconv"
"strings"
"time"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
)
const (
RequestOwnerProperty = "Owner"
)
const condKeyAWSPrincipalARN = "aws:PrincipalArn"
const (
// String condition operators.
@ -52,168 +53,157 @@ 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 UserResolver interface {
GetUserKey(account, user string) (*keys.PublicKey, error)
}
type BucketResolver interface {
GetBucketCID(bucket string) (cid.ID, error)
}
type Resolver interface {
UserResolver
BucketResolver
}
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 +224,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::<account>:user/<user-name-with-path>
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:::<bucket-name>/<object-name>
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
}

236
iam/converter_native.go Normal file
View file

@ -0,0 +1,236 @@
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"
)
func ConvertToNativeChain(p Policy, resolver Resolver) (*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 UserResolver) ([]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 UserResolver) ([]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 BucketResolver) ([]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.EncodeToString())
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 UserResolver) ([]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 UserResolver) (string, error) {
account, user, err := parsePrincipalAsIAMUser(principal)
if err != nil {
return "", err
}
key, err := resolver.GetUserKey(account, user)
if err != nil {
return "", fmt.Errorf("resolve user: %w", err)
}
return string(key.Bytes()), 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
}

149
iam/converter_s3.go Normal file
View file

@ -0,0 +1,149 @@
package iam
import (
"fmt"
"strings"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"git.frostfs.info/TrueCloudLab/policy-engine/schema/s3"
)
func ConvertToS3Chain(p Policy, resolver UserResolver) (*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 UserResolver) ([]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 UserResolver) ([]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 UserResolver) ([]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 UserResolver) (string, error) {
account, user, err := parsePrincipalAsIAMUser(principal)
if err != nil {
return "", err
}
key, err := resolver.GetUserKey(account, user)
if err != nil {
return "", fmt.Errorf("resolve user: %w", err)
}
return key.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
}

File diff suppressed because it is too large Load diff

View file

@ -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)

View file

@ -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{

9
schema/s3/consts.go Normal file
View file

@ -0,0 +1,9 @@
package s3
const (
PropertyKeyOwner = "Owner"
PropertyKeyDelimiter = "s3:delimiter"
PropertyKeyPrefix = "s3:prefix"
PropertyKeyVersionID = "s3:VersionId"
)