[#XX] iam: Support native converter

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
This commit is contained in:
Denis Kirillov 2023-11-13 16:55:42 +03:00
parent 0566a2b058
commit dfba3eb75b
4 changed files with 143 additions and 9 deletions

View file

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

View file

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

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