[#36] iam: Support iam actions

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
This commit is contained in:
Denis Kirillov 2023-12-18 17:00:31 +03:00 committed by Alexey Vanin
parent e57d213595
commit ec39d8371a
3 changed files with 80 additions and 31 deletions

View file

@ -56,6 +56,7 @@ const (
arnIAMPrefix = "arn:aws:iam::" arnIAMPrefix = "arn:aws:iam::"
s3ResourcePrefix = "arn:aws:s3:::" s3ResourcePrefix = "arn:aws:s3:::"
s3ActionPrefix = "s3:" s3ActionPrefix = "s3:"
iamActionPrefix = "iam:"
) )
var ( var (
@ -277,19 +278,16 @@ func parseActionAsS3Action(action string) (string, error) {
return Wildcard, nil return Wildcard, nil
} }
if !strings.HasPrefix(action, s3ActionPrefix) { if !strings.HasPrefix(action, s3ActionPrefix) && !strings.HasPrefix(action, iamActionPrefix) {
return "", ErrInvalidActionFormat return "", ErrInvalidActionFormat
} }
// iam arn format :s3:<action-name> index := strings.IndexByte(action, Wildcard[0])
s3Action := strings.TrimPrefix(action, s3ActionPrefix) if index != -1 && index != utf8.RuneCountInString(action)-1 {
index := strings.IndexByte(s3Action, Wildcard[0])
if index != -1 && index != utf8.RuneCountInString(s3Action)-1 {
return "", ErrInvalidActionFormat return "", ErrInvalidActionFormat
} }
return s3Action, nil return action, nil
} }
func splitGroupedConditions(groupedConditions []GroupedConditions) [][]chain.Condition { func splitGroupedConditions(groupedConditions []GroupedConditions) [][]chain.Condition {

View file

@ -3,6 +3,7 @@ package iam
import ( import (
"errors" "errors"
"fmt" "fmt"
"strings"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"git.frostfs.info/TrueCloudLab/policy-engine/schema/native" "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
@ -22,11 +23,11 @@ var actionToOpMap = map[string][]string{
} }
const ( const (
supportedS3ActionDeleteObject = "DeleteObject" supportedS3ActionDeleteObject = "s3:DeleteObject"
supportedS3ActionGetObject = "GetObject" supportedS3ActionGetObject = "s3:GetObject"
supportedS3ActionHeadObject = "HeadObject" supportedS3ActionHeadObject = "s3:HeadObject"
supportedS3ActionPutObject = "PutObject" supportedS3ActionPutObject = "s3:PutObject"
supportedS3ActionListBucket = "ListBucket" supportedS3ActionListBucket = "s3:ListBucket"
) )
type NativeResolver interface { type NativeResolver interface {
@ -237,7 +238,7 @@ func formNativeActionNames(names []string) ([]string, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if action == Wildcard { if action == Wildcard || strings.TrimPrefix(action, s3ActionPrefix) == Wildcard {
return []string{Wildcard}, nil return []string{Wildcard}, nil
} }
res = append(res, actionToOpMap[action]...) res = append(res, actionToOpMap[action]...)

View file

@ -70,7 +70,7 @@ func TestConverters(t *testing.T) {
bktName := "DOC-EXAMPLE-BUCKET" bktName := "DOC-EXAMPLE-BUCKET"
objName := "object-name" objName := "object-name"
resource := bktName + "/*" resource := bktName + "/*"
action := "PutObject" s3action := "s3:PutObject"
mockResolver := newMockUserResolver([]string{user}, []string{bktName}) mockResolver := newMockUserResolver([]string{user}, []string{bktName})
@ -95,7 +95,7 @@ func TestConverters(t *testing.T) {
expected := &chain.Chain{Rules: []chain.Rule{ expected := &chain.Chain{Rules: []chain.Rule{
{ {
Status: chain.Allow, Status: chain.Allow,
Actions: chain.Actions{Names: []string{action}}, Actions: chain.Actions{Names: []string{s3action}},
Resources: chain.Resources{Names: []string{resource}}, Resources: chain.Resources{Names: []string{resource}},
Condition: []chain.Condition{ Condition: []chain.Condition{
{ {
@ -135,7 +135,7 @@ func TestConverters(t *testing.T) {
expected := &chain.Chain{Rules: []chain.Rule{ expected := &chain.Chain{Rules: []chain.Rule{
{ {
Status: chain.Allow, Status: chain.Allow,
Actions: chain.Actions{Names: []string{action}}, Actions: chain.Actions{Names: []string{native.MethodPutObject}},
Resources: chain.Resources{Names: []string{fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName])}}, Resources: chain.Resources{Names: []string{fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName])}},
Condition: []chain.Condition{ Condition: []chain.Condition{
{ {
@ -169,7 +169,7 @@ func TestConverters(t *testing.T) {
expected := &chain.Chain{Rules: []chain.Rule{ expected := &chain.Chain{Rules: []chain.Rule{
{ {
Status: chain.AccessDenied, Status: chain.AccessDenied,
Actions: chain.Actions{Inverted: true, Names: []string{action}}, Actions: chain.Actions{Inverted: true, Names: []string{s3action}},
Resources: chain.Resources{Inverted: true, Names: []string{resource}}, Resources: chain.Resources{Inverted: true, Names: []string{resource}},
Condition: []chain.Condition{ Condition: []chain.Condition{
{ {
@ -203,7 +203,7 @@ func TestConverters(t *testing.T) {
expected := &chain.Chain{Rules: []chain.Rule{ expected := &chain.Chain{Rules: []chain.Rule{
{ {
Status: chain.AccessDenied, Status: chain.AccessDenied,
Actions: chain.Actions{Inverted: true, Names: actionToOpMap["GetObject"]}, Actions: chain.Actions{Inverted: true, Names: actionToOpMap["s3:GetObject"]},
Resources: chain.Resources{Inverted: true, Names: []string{ Resources: chain.Resources{Inverted: true, Names: []string{
fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName]), fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName]),
}}, }},
@ -278,6 +278,49 @@ func TestConverters(t *testing.T) {
_, err := ConvertToNativeChain(p, mockResolver) _, err := ConvertToNativeChain(p, mockResolver)
require.Error(t, err) require.Error(t, err)
}) })
t.Run("valid mixed iam/s3 actions", func(t *testing.T) {
p := Policy{
Statement: []Statement{{
Principal: map[PrincipalType][]string{AWSPrincipalType: {principal}},
Effect: AllowEffect,
Action: []string{"s3:PutObject", "iam:*"},
Resource: []string{"*"},
}},
}
s3Expected := &chain.Chain{Rules: []chain.Rule{{
Status: chain.Allow,
Actions: chain.Actions{Names: []string{"s3:PutObject", "iam:*"}},
Resources: chain.Resources{Names: []string{"*"}},
Condition: []chain.Condition{{
Op: chain.CondStringEquals,
Object: chain.ObjectRequest,
Key: s3.PropertyKeyOwner,
Value: mockResolver.users[user],
}},
}}}
s3Chain, err := ConvertToS3Chain(p, mockResolver)
require.NoError(t, err)
require.Equal(t, s3Expected, s3Chain)
nativeExpected := &chain.Chain{Rules: []chain.Rule{{
Status: chain.Allow,
Actions: chain.Actions{Names: []string{native.MethodPutObject}},
Resources: chain.Resources{Names: []string{native.ResourceFormatAllObjects}},
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, nativeExpected, nativeChain)
})
} }
func TestConvertToChainCondition(t *testing.T) { func TestConvertToChainCondition(t *testing.T) {
@ -566,7 +609,7 @@ func TestComplexNativeConditions(t *testing.T) {
} }
expectedStatus := chain.AccessDenied expectedStatus := chain.AccessDenied
expectedActions := chain.Actions{Names: actionToOpMap[action]} expectedActions := chain.Actions{Names: actionToOpMap["s3:"+action]}
expectedResource1 := chain.Resources{Names: []string{nativeResource1}} expectedResource1 := chain.Resources{Names: []string{nativeResource1}}
expectedResource23 := chain.Resources{Names: []string{nativeResource2, nativeResource3}} expectedResource23 := chain.Resources{Names: []string{nativeResource2, nativeResource3}}
@ -852,7 +895,7 @@ func TestComplexS3Conditions(t *testing.T) {
resource1 := bktName1 + "/" + objName1 resource1 := bktName1 + "/" + objName1
resource2 := bktName2 + "/*" resource2 := bktName2 + "/*"
resource3 := bktName3 + "/*" resource3 := bktName3 + "/*"
action := "PutObject" action := "s3:PutObject"
key1, key2 := "key1", "key2" key1, key2 := "key1", "key2"
val0, val1, val2 := "val0", "val1", "val2" val0, val1, val2 := "val0", "val1", "val2"
@ -866,7 +909,7 @@ func TestComplexS3Conditions(t *testing.T) {
AWSPrincipalType: {principal1, principal2}, AWSPrincipalType: {principal1, principal2},
}, },
Effect: DenyEffect, Effect: DenyEffect,
Action: []string{"s3:" + action}, Action: []string{action},
Resource: []string{"arn:aws:s3:::" + resource1, "arn:aws:s3:::" + resource2, "arn:aws:s3:::" + resource3}, Resource: []string{"arn:aws:s3:::" + resource1, "arn:aws:s3:::" + resource2, "arn:aws:s3:::" + resource3},
Conditions: map[string]Condition{ Conditions: map[string]Condition{
CondStringEquals: {key1: {val0, val1}}, CondStringEquals: {key1: {val0, val1}},
@ -876,7 +919,7 @@ func TestComplexS3Conditions(t *testing.T) {
} }
expectedStatus := chain.AccessDenied expectedStatus := chain.AccessDenied
expectedActions := chain.Actions{Names: actionToOpMap[action]} expectedActions := chain.Actions{Names: []string{action}}
expectedResources := chain.Resources{Names: []string{resource1, resource2, resource3}} expectedResources := chain.Resources{Names: []string{resource1, resource2, resource3}}
user1Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: s3.PropertyKeyOwner, Value: mockResolver.users[user1]} user1Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: s3.PropertyKeyOwner, Value: mockResolver.users[user1]}
@ -1105,19 +1148,19 @@ func TestS3BucketResource(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// check we match just "bucket1" resource // check we match just "bucket1" resource
req := testutil.NewRequest("HeadBucket", testutil.NewResource(bktName1, nil), nil) req := testutil.NewRequest("s3:HeadBucket", testutil.NewResource(bktName1, nil), nil)
status, _, err := s.IsAllowed(chainName, engine.NewRequestTargetWithNamespace(namespace), req) status, _, err := s.IsAllowed(chainName, engine.NewRequestTargetWithNamespace(namespace), req)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, chain.AccessDenied.String(), status.String()) require.Equal(t, chain.AccessDenied.String(), status.String())
// check we match just "bucket2" resource // check we match just "bucket2" resource
req = testutil.NewRequest("HeadBucket", testutil.NewResource(bktName2, nil), nil) req = testutil.NewRequest("s3:HeadBucket", testutil.NewResource(bktName2, nil), nil)
status, _, err = s.IsAllowed(chainName, engine.NewRequestTargetWithNamespace(namespace), req) status, _, err = s.IsAllowed(chainName, engine.NewRequestTargetWithNamespace(namespace), req)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, chain.Allow.String(), status.String()) require.Equal(t, chain.Allow.String(), status.String())
// check we also match "bucket2/object" resource // check we also match "bucket2/object" resource
req = testutil.NewRequest("PutObject", testutil.NewResource(bktName2+"/object", nil), nil) req = testutil.NewRequest("s3:PutObject", testutil.NewResource(bktName2+"/object", nil), nil)
status, _, err = s.IsAllowed(chainName, engine.NewRequestTargetWithNamespace(namespace), req) status, _, err = s.IsAllowed(chainName, engine.NewRequestTargetWithNamespace(namespace), req)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, chain.Allow.String(), status.String()) require.Equal(t, chain.Allow.String(), status.String())
@ -1159,20 +1202,27 @@ func TestActionParsing(t *testing.T) {
}, },
{ {
action: "s3:PutObject", action: "s3:PutObject",
expected: "PutObject", expected: "s3:PutObject",
}, },
{ {
action: "s3:Put*", action: "s3:Put*",
expected: "Put*", expected: "s3:Put*",
}, },
{ {
action: "s3:*", action: "s3:*",
expected: "*", expected: "s3:*",
}, },
{ {
action: "s3:", action: "s3:",
expected: "s3:",
expected: "", },
{
action: "iam:ListAccessKeys",
expected: "iam:ListAccessKeys",
},
{
action: "iam:*",
expected: "iam:*",
}, },
} { } {
t.Run("", func(t *testing.T) { t.Run("", func(t *testing.T) {