forked from TrueCloudLab/policy-engine
[#XX] iam: Support native converter
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
This commit is contained in:
parent
0566a2b058
commit
dfba3eb75b
4 changed files with 143 additions and 9 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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{
|
||||
|
|
Loading…
Reference in a new issue