diff --git a/iam/converter.go b/iam/converter.go index 4704dcd..84ee292 100644 --- a/iam/converter.go +++ b/iam/converter.go @@ -66,10 +66,34 @@ const ( const ( arnIAMPrefix = "arn:aws:iam::" s3ResourcePrefix = "arn:aws:s3:::" + s3ActionPrefix = "s3:" ) -// ErrInvalidPrincipalFormat occurs when principal has unknown/unsupported format. -var ErrInvalidPrincipalFormat = errors.New("invalid principal format") +var actionToOpMap = map[string][]string{ + // todo use constants after https://git.frostfs.info/TrueCloudLab/policy-engine/issues/16 + + actionDeleteObject: {"native:DeleteObject"}, + actionGetObject: {"native:GetObject", "native:HeadObject", "native:SearchObject", "native:RangeObject", "native:HashObject"}, + actionHeadObject: {"native:HeadObject", "native:SearchObject", "native:RangeObject", "native:HashObject"}, + actionPutObject: {"native:PutObject"}, + actionListBucket: {"native:GetObject", "native:HeadObject", "native:SearchObject", "native:RangeObject", "native:HashObject"}, +} + +const ( + actionDeleteObject = "DeleteObject" + actionGetObject = "GetObject" + actionHeadObject = "HeadObject" + actionPutObject = "PutObject" + actionListBucket = "ListBucket" +) + +var ( + // ErrInvalidPrincipalFormat occurs when principal has unknown/unsupported format. + ErrInvalidPrincipalFormat = errors.New("invalid principal format") + + // ErrActionsNotApplicable occurs when failed to convert any actions. + ErrActionsNotApplicable = errors.New("actions not applicable") +) type ChainType string @@ -87,7 +111,7 @@ func (p Policy) ToChain(typ ChainType, resolver UserResolver) (*policyengine.Cha return nil, fmt.Errorf("unknown chain type '%s'", typ) } - if err := p.Validate(GeneralPolicyType); err != nil { + if err := p.Validate(ResourceBasedPolicyType); err != nil { return nil, err } @@ -140,7 +164,10 @@ func (p Policy) ToChain(typ ChainType, resolver UserResolver) (*policyengine.Cha conditions = append(conditions, conds...) action, actionInverted := statement.action() - ruleAction := policyengine.Actions{Inverted: actionInverted, Names: action} + ruleAction := policyengine.Actions{Inverted: actionInverted, Names: formActionNames(typ, action)} + if len(ruleAction.Names) == 0 { + continue + } resource, resourceInverted := statement.resource() names, extraConditions := formResourceNamesAndConditions(typ, resource) @@ -157,6 +184,10 @@ func (p Policy) ToChain(typ ChainType, resolver UserResolver) (*policyengine.Cha chain.Rules = append(chain.Rules, r) } + if len(chain.Rules) == 0 { + return nil, ErrActionsNotApplicable + } + return &chain, nil } @@ -384,3 +415,37 @@ func formS3ResourceNamesAndConditions(names []string) ([]string, []policyengine. return res, nil } + +func formActionNames(chainType ChainType, names []string) []string { + switch chainType { + case S3ChainType: + return formS3ActionNames(names) + case NativeChainType: + return formNativeActionNames(names) + } + + panic("unknown chain type") // this must not ever happen +} + +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 +} + +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 8940601..413ef0c 100644 --- a/iam/converter_test.go +++ b/iam/converter_test.go @@ -40,6 +40,7 @@ func TestConverters(t *testing.T) { user := namespace + "/" + userName principal := "arn:aws:iam::" + namespace + ":user/" + userName resource := "DOC-EXAMPLE-BUCKET/*" + action := "PutObject" mockResolver := newMockUserResolver(t, []string{user}) @@ -64,7 +65,7 @@ func TestConverters(t *testing.T) { expected := &policyengine.Chain{Rules: []policyengine.Rule{ { Status: policyengine.Allow, - Actions: policyengine.Actions{Names: p.Statement[0].Action}, + Actions: policyengine.Actions{Names: []string{action}}, Resources: policyengine.Resources{Names: []string{resource}}, Any: true, Condition: []policyengine.Condition{ @@ -105,7 +106,7 @@ func TestConverters(t *testing.T) { expected := &policyengine.Chain{Rules: []policyengine.Rule{ { Status: policyengine.Allow, - Actions: policyengine.Actions{Names: p.Statement[0].Action}, + Actions: policyengine.Actions{Names: []string{"native:" + action}}, Resources: policyengine.Resources{Names: []string{"native:::object/*"}}, Any: true, Condition: []policyengine.Condition{ @@ -146,7 +147,7 @@ func TestConverters(t *testing.T) { expected := &policyengine.Chain{Rules: []policyengine.Rule{ { Status: policyengine.AccessDenied, - Actions: policyengine.Actions{Inverted: true, Names: p.Statement[0].NotAction}, + Actions: policyengine.Actions{Inverted: true, Names: []string{action}}, Resources: policyengine.Resources{Inverted: true, Names: []string{resource}}, Any: true, Condition: []policyengine.Condition{ @@ -165,6 +166,47 @@ func TestConverters(t *testing.T) { require.Equal(t, expected, chain) }) + 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:::" + resource}, + }}, + } + + expected := &policyengine.Chain{Rules: []policyengine.Rule{ + { + Status: policyengine.AccessDenied, + Actions: policyengine.Actions{Inverted: true, Names: actionToOpMap["GetObject"]}, + Resources: policyengine.Resources{Inverted: true, Names: []string{"native:::object/*"}}, + Any: true, + Condition: []policyengine.Condition{ + { + Op: policyengine.CondStringEquals, + Object: policyengine.ObjectRequest, + Key: RequestOwnerProperty, + Value: mockResolver.users[user].Address(), + }, + { + Op: policyengine.CondStringLike, + Object: policyengine.ObjectResource, + Key: ResourceFullPathProperty, + Value: resource, + }, + }, + }, + }} + + chain, err := p.ToChain(NativeChainType, mockResolver) + require.NoError(t, err) + require.Equal(t, expected, chain) + }) + t.Run("invalid policy (unsupported principal type)", func(t *testing.T) { p := Policy{ Version: "2012-10-17", @@ -198,6 +240,23 @@ func TestConverters(t *testing.T) { require.Error(t, err) }) + t.Run("invalid policy (not applicable native actions)", func(t *testing.T) { + p := Policy{ + Version: "2012-10-17", + Statement: []Statement{{ + Principal: map[PrincipalType][]string{ + AWSPrincipalType: {principal}, + }, + Effect: AllowEffect, + Action: []string{"s3:AbortMultipartUpload"}, + Resource: []string{"arn:aws:s3:::" + resource}, + }}, + } + + _, err := p.ToChain(NativeChainType, mockResolver) + require.Error(t, err) + }) + t.Run("check policy conditions", func(t *testing.T) { p := Policy{ Version: "2012-10-17", @@ -233,7 +292,7 @@ func TestConverters(t *testing.T) { expected := &policyengine.Chain{Rules: []policyengine.Rule{ { Status: policyengine.Allow, - Actions: policyengine.Actions{Names: p.Statement[0].Action}, + Actions: policyengine.Actions{Names: []string{action}}, Resources: policyengine.Resources{Names: []string{resource}}, Any: true, Condition: []policyengine.Condition{ @@ -381,7 +440,7 @@ func TestConverters(t *testing.T) { }) } -func TestName(t *testing.T) { +func TestParsePrincipalARN(t *testing.T) { for i, tc := range []struct { principal string account string diff --git a/iam/policy.go b/iam/policy.go index 956b239..775bcd8 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' 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{