feature/11-support_notprincipal #12

Merged
dkirillov merged 2 commits from dkirillov/policy-engine:feature/11-support_notprincipal into master 2024-09-04 19:51:23 +00:00
7 changed files with 590 additions and 82 deletions

View file

@ -37,15 +37,25 @@ func (c *Chain) DecodeBytes(b []byte) error {
type Rule struct { type Rule struct {
Status Status Status Status
// Actions the operation is applied to. // Actions the operation is applied to.
Action []string Actions Actions
// List of the resources the operation is applied to. // List of the resources the operation is applied to.
Resource []string Resources Resources
// True iff individual conditions must be combined with the logical OR. // True iff individual conditions must be combined with the logical OR.
// By default AND is used, so _each_ condition must pass. // By default AND is used, so _each_ condition must pass.
Any bool Any bool
Condition []Condition Condition []Condition
} }
type Actions struct {
Inverted bool
Names []string
}
type Resources struct {
Inverted bool
Names []string
}
type Condition struct { type Condition struct {
Op ConditionType Op ConditionType
Object ObjectType Object ObjectType
@ -88,6 +98,45 @@ const (
CondNumericGreaterThanEquals CondNumericGreaterThanEquals
) )
func (c ConditionType) String() string {
switch c {
case CondStringEquals:
return "StringEquals"
case CondStringNotEquals:
return "StringNotEquals"
case CondStringEqualsIgnoreCase:
return "StringEqualsIgnoreCase"
case CondStringNotEqualsIgnoreCase:
return "StringNotEqualsIgnoreCase"
case CondStringLike:
return "StringLike"
case CondStringNotLike:
return "StringNotLike"
case CondStringLessThan:
return "StringLessThan"
case CondStringLessThanEquals:
return "StringLessThanEquals"
case CondStringGreaterThan:
return "StringGreaterThan"
case CondStringGreaterThanEquals:
return "StringGreaterThanEquals"
case CondNumericEquals:
return "NumericEquals"
case CondNumericNotEquals:
return "NumericNotEquals"
case CondNumericLessThan:
return "NumericLessThan"
case CondNumericLessThanEquals:
return "NumericLessThanEquals"
case CondNumericGreaterThan:
aarifullin marked this conversation as resolved Outdated

Didn't you consdier to panic here? I am afraid that this can be taken for fine invocation and may cause many problems for the side that invokes this convertation

Didn't you consdier to _panic_ here? I am afraid that this can be taken for fine invocation and may cause many problems for the side that invokes this convertation

I supposed in our case it's enough to have panic here

I supposed in our case it's enough to have panic [here](https://git.frostfs.info/dkirillov/policy-engine/src/commit/08deadaa4cae569609540c58491ddb3d47d76922/chain.go#L148)
return "NumericGreaterThan"
case CondNumericGreaterThanEquals:
return "NumericGreaterThanEquals"
default:
return "unknown condition type"
}
}
func (c *Condition) Match(req Request) bool { func (c *Condition) Match(req Request) bool {
var val string var val string
switch c.Object { switch c.Object {
@ -126,9 +175,9 @@ func (c *Condition) Match(req Request) bool {
} }
func (r *Rule) Match(req Request) (status Status, matched bool) { func (r *Rule) Match(req Request) (status Status, matched bool) {
found := len(r.Resource) == 0 found := len(r.Resources.Names) == 0
for i := range r.Resource { for i := range r.Resources.Names {
if globMatch(req.Resource().Name(), r.Resource[i]) { if globMatch(req.Resource().Name(), r.Resources.Names[i]) != r.Resources.Inverted {
found = true found = true
break break
} }
@ -136,8 +185,8 @@ func (r *Rule) Match(req Request) (status Status, matched bool) {
if !found { if !found {
return NoRuleFound, false return NoRuleFound, false
} }
for i := range r.Action { for i := range r.Actions.Names {
if globMatch(req.Operation(), r.Action[i]) { if globMatch(req.Operation(), r.Actions.Names[i]) != r.Actions.Inverted {
return r.matchCondition(req) return r.matchCondition(req)
} }
} }

View file

@ -11,10 +11,10 @@ func TestEncodeDecode(t *testing.T) {
Rules: []Rule{ Rules: []Rule{
{ {
Status: Allow, Status: Allow,
Action: []string{ Actions: Actions{Names: []string{
"native::PutObject", "native::PutObject",
}, }},
Resource: []string{"*"}, Resources: Resources{Names: []string{"*"}},
Condition: []Condition{ Condition: []Condition{
{ {
Op: CondStringEquals, Op: CondStringEquals,

View file

@ -1,7 +1,6 @@
package iam package iam
import ( import (
"errors"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@ -11,8 +10,6 @@ import (
) )
const ( const (
FrostFSPrincipal = "FrostFS"
RequestOwnerProperty = "Owner" RequestOwnerProperty = "Owner"
) )
@ -56,6 +53,10 @@ const (
) )
func (p Policy) ToChain() (*policyengine.Chain, error) { func (p Policy) ToChain() (*policyengine.Chain, error) {
if err := p.Validate(GeneralPolicyType); err != nil {
return nil, err
}
var chain policyengine.Chain var chain policyengine.Chain
for _, statement := range p.Statement { for _, statement := range p.Statement {
@ -64,20 +65,21 @@ func (p Policy) ToChain() (*policyengine.Chain, error) {
status = policyengine.Allow status = policyengine.Allow
} }
if len(statement.Principal) != 1 {
return nil, errors.New("currently supported exactly one principal type")
}
var principals []string var principals []string
var op policyengine.ConditionType var op policyengine.ConditionType
if _, ok := statement.Principal[Wildcard]; ok { statementPrincipal, inverted := statement.principal()
if _, ok := statementPrincipal[Wildcard]; ok { // this can be true only if 'inverted' false
dstepanov-yadro marked this conversation as resolved Outdated

I didn't catch the thought. Please explain.

I didn't catch the thought. Please explain.

Only Principal can be *:

{
	"Statement": {
		"Principal": "*",
	}
}

The following json is invalid according to spec

{
	"Statement": {
		"NotPrincipal": "*",
	}
}
Only `Principal` can be `*`: ```json { "Statement": { "Principal": "*", } } ``` The following json is invalid according to [spec](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_notprincipal.html) ```json { "Statement": { "NotPrincipal": "*", } } ```
principals = []string{Wildcard} principals = []string{Wildcard}
op = policyengine.CondStringLike op = policyengine.CondStringLike
} else if frostfsPrincipals, ok := statement.Principal[FrostFSPrincipal]; ok {
principals = frostfsPrincipals
op = policyengine.CondStringEquals
} else { } else {
return nil, errors.New("currently supported only FrostFS or all (wildcard) principals") for _, principal := range statementPrincipal {
principals = append(principals, principal...)
}
op = policyengine.CondStringEquals
if inverted {
op = policyengine.CondStringNotEquals
}
} }
var conditions []policyengine.Condition var conditions []policyengine.Condition
@ -96,10 +98,16 @@ func (p Policy) ToChain() (*policyengine.Chain, error) {
} }
conditions = append(conditions, conds...) conditions = append(conditions, conds...)
action, actionInverted := statement.action()
ruleAction := policyengine.Actions{Inverted: actionInverted, Names: action}
resource, resourceInverted := statement.resource()
ruleResource := policyengine.Resources{Inverted: resourceInverted, Names: resource}
r := policyengine.Rule{ r := policyengine.Rule{
Status: status, Status: status,
Action: statement.Action, Actions: ruleAction,
Resource: statement.Resource, Resources: ruleResource,
Any: true, Any: true,
Condition: conditions, Condition: conditions,
} }

View file

@ -12,8 +12,8 @@ func TestConverters(t *testing.T) {
p := Policy{ p := Policy{
Version: "2012-10-17", Version: "2012-10-17",
Statement: []Statement{{ Statement: []Statement{{
Principal: map[string][]string{ Principal: map[PrincipalType][]string{
FrostFSPrincipal: {"arn:aws:iam::111122223333:user/JohnDoe"}, AWSPrincipalType: {"arn:aws:iam::111122223333:user/JohnDoe"},
}, },
Effect: AllowEffect, Effect: AllowEffect,
Action: []string{"s3:PutObject"}, Action: []string{"s3:PutObject"},
@ -28,10 +28,10 @@ func TestConverters(t *testing.T) {
expected := &policyengine.Chain{Rules: []policyengine.Rule{ expected := &policyengine.Chain{Rules: []policyengine.Rule{
{ {
Status: policyengine.Allow, Status: policyengine.Allow,
Action: p.Statement[0].Action, Actions: policyengine.Actions{Names: p.Statement[0].Action},
Resource: p.Statement[0].Resource, Resources: policyengine.Resources{Names: p.Statement[0].Resource},
Any: true, Any: true,
Condition: []policyengine.Condition{ Condition: []policyengine.Condition{
{ {
Op: policyengine.CondStringEquals, Op: policyengine.CondStringEquals,
@ -54,12 +54,47 @@ func TestConverters(t *testing.T) {
require.Equal(t, expected, chain) require.Equal(t, expected, chain)
}) })
t.Run("valid inverted policy", func(t *testing.T) {
p := Policy{
Version: "2012-10-17",
Statement: []Statement{{
NotPrincipal: map[PrincipalType][]string{
AWSPrincipalType: {"arn:aws:iam::111122223333:user/JohnDoe"},
},
Effect: DenyEffect,
NotAction: []string{"s3:PutObject"},
NotResource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
}},
}
expected := &policyengine.Chain{Rules: []policyengine.Rule{
{
Status: policyengine.AccessDenied,
Actions: policyengine.Actions{Inverted: true, Names: p.Statement[0].NotAction},
Resources: policyengine.Resources{Inverted: true, Names: p.Statement[0].NotResource},
Any: true,
Condition: []policyengine.Condition{
{
Op: policyengine.CondStringNotEquals,
Object: policyengine.ObjectRequest,
Key: RequestOwnerProperty,
Value: "arn:aws:iam::111122223333:user/JohnDoe",
},
},
},
}}
chain, err := p.ToChain()
require.NoError(t, err)
require.Equal(t, expected, chain)
})
t.Run("invalid policy (unsupported principal type)", func(t *testing.T) { t.Run("invalid policy (unsupported principal type)", func(t *testing.T) {
p := Policy{ p := Policy{
Version: "2012-10-17", Version: "2012-10-17",
Statement: []Statement{{ Statement: []Statement{{
Principal: map[string][]string{ Principal: map[PrincipalType][]string{
"AWS": {"arn:aws:iam::111122223333:user/JohnDoe"}, "dummy": {"arn:aws:iam::111122223333:user/JohnDoe"},
}, },
Effect: AllowEffect, Effect: AllowEffect,
Action: []string{"s3:PutObject"}, Action: []string{"s3:PutObject"},
@ -71,14 +106,15 @@ func TestConverters(t *testing.T) {
require.Error(t, err) require.Error(t, err)
}) })
t.Run("invalid policy (missing principal)", func(t *testing.T) { t.Run("invalid policy (missing resource)", func(t *testing.T) {
p := Policy{ p := Policy{
Version: "2012-10-17", Version: "2012-10-17",
Statement: []Statement{{ Statement: []Statement{{
Principal: map[string][]string{}, Principal: map[PrincipalType][]string{
Effect: AllowEffect, AWSPrincipalType: {"arn:aws:iam::111122223333:user/JohnDoe"},
Action: []string{"s3:PutObject"}, },
Resource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"}, Effect: AllowEffect,
Action: []string{"s3:PutObject"},
}}, }},
} }
@ -90,7 +126,7 @@ func TestConverters(t *testing.T) {
p := Policy{ p := Policy{
Version: "2012-10-17", Version: "2012-10-17",
Statement: []Statement{{ Statement: []Statement{{
Principal: map[string][]string{"*": nil}, Principal: map[PrincipalType][]string{Wildcard: nil},
Effect: AllowEffect, Effect: AllowEffect,
Action: []string{"s3:PutObject"}, Action: []string{"s3:PutObject"},
Resource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"}, Resource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
@ -120,10 +156,10 @@ func TestConverters(t *testing.T) {
expected := &policyengine.Chain{Rules: []policyengine.Rule{ expected := &policyengine.Chain{Rules: []policyengine.Rule{
{ {
Status: policyengine.Allow, Status: policyengine.Allow,
Action: p.Statement[0].Action, Actions: policyengine.Actions{Names: p.Statement[0].Action},
Resource: p.Statement[0].Resource, Resources: policyengine.Resources{Names: p.Statement[0].Resource},
Any: true, Any: true,
Condition: []policyengine.Condition{ Condition: []policyengine.Condition{
{ {
Op: policyengine.CondStringLike, Op: policyengine.CondStringLike,
@ -260,9 +296,9 @@ func TestConverters(t *testing.T) {
for i, rule := range chain.Rules { for i, rule := range chain.Rules {
expectedRule := expected.Rules[i] expectedRule := expected.Rules[i]
require.Equal(t, expectedRule.Action, rule.Action) require.Equal(t, expectedRule.Actions, rule.Actions)
require.Equal(t, expectedRule.Any, rule.Any) require.Equal(t, expectedRule.Any, rule.Any)
require.Equal(t, expectedRule.Resource, rule.Resource) require.Equal(t, expectedRule.Resources, rule.Resources)
require.Equal(t, expectedRule.Status, rule.Status) require.Equal(t, expectedRule.Status, rule.Status)
require.ElementsMatch(t, expectedRule.Condition, rule.Condition) require.ElementsMatch(t, expectedRule.Condition, rule.Condition)
} }

View file

@ -3,11 +3,11 @@ package iam
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
) )
type ( type (
// Policy grammar https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_grammar.html // Policy grammar https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_grammar.html
// Currently 'NotPrincipal', 'NotAction' and 'NotResource' are not supported (so cannot be unmarshalled).
Policy struct { Policy struct {
Version string `json:"Version,omitempty"` Version string `json:"Version,omitempty"`
ID string `json:"Id,omitempty"` ID string `json:"Id,omitempty"`
@ -17,15 +17,19 @@ type (
Statements []Statement Statements []Statement
Statement struct { Statement struct {
SID string `json:"Sid,omitempty"` ID string `json:"Id,omitempty"`
Principal Principal `json:"Principal,omitempty"` SID string `json:"Sid,omitempty"`
Effect Effect `json:"Effect"` Principal Principal `json:"Principal,omitempty"`
Action Action `json:"Action"` NotPrincipal Principal `json:"NotPrincipal,omitempty"`
Resource Resource `json:"Resource"` Effect Effect `json:"Effect"`
Conditions Conditions `json:"Condition,omitempty"` Action Action `json:"Action,omitempty"`
NotAction Action `json:"NotAction,omitempty"`
Resource Resource `json:"Resource,omitempty"`
NotResource Resource `json:"NotResource,omitempty"`
Conditions Conditions `json:"Condition,omitempty"`
} }
Principal map[string][]string Principal map[PrincipalType][]string
Effect string Effect string
@ -36,6 +40,16 @@ type (
Conditions map[string]Condition Conditions map[string]Condition
Condition map[string][]string Condition map[string][]string
PolicyType int
PrincipalType string
)
const (
GeneralPolicyType PolicyType = iota
IdentityBasedPolicyType
ResourceBasedPolicyType
) )
const Wildcard = "*" const Wildcard = "*"
@ -45,6 +59,22 @@ const (
DenyEffect Effect = "Deny" DenyEffect Effect = "Deny"
) )
func (e Effect) IsValid() bool {
return e == AllowEffect || e == DenyEffect
}
const (
AWSPrincipalType PrincipalType = "AWS"
FederatedPrincipalType PrincipalType = "Federated"
ServicePrincipalType PrincipalType = "Service"
CanonicalUserPrincipalType PrincipalType = "CanonicalUser"
)
func (p PrincipalType) IsValid() bool {
return p == AWSPrincipalType || p == FederatedPrincipalType ||
p == ServicePrincipalType || p == CanonicalUserPrincipalType
}
func (s *Statements) UnmarshalJSON(data []byte) error { func (s *Statements) UnmarshalJSON(data []byte) error {
var list []Statement var list []Statement
if err := json.Unmarshal(data, &list); err == nil { if err := json.Unmarshal(data, &list); err == nil {
@ -75,7 +105,7 @@ func (p *Principal) UnmarshalJSON(data []byte) error {
return nil return nil
} }
m := make(map[string]interface{}) m := make(map[PrincipalType]any)
if err := json.Unmarshal(data, &m); err != nil { if err := json.Unmarshal(data, &m); err != nil {
return err return err
} }
@ -87,7 +117,7 @@ func (p *Principal) UnmarshalJSON(data []byte) error {
continue continue
} }
list, ok := val.([]interface{}) list, ok := val.([]any)
if !ok { if !ok {
return errors.New("invalid principal format") return errors.New("invalid principal format")
} }
@ -144,7 +174,7 @@ func (r *Resource) UnmarshalJSON(data []byte) error {
func (c *Condition) UnmarshalJSON(data []byte) error { func (c *Condition) UnmarshalJSON(data []byte) error {
*c = make(Condition) *c = make(Condition)
m := make(map[string]interface{}) m := make(map[string]any)
if err := json.Unmarshal(data, &m); err != nil { if err := json.Unmarshal(data, &m); err != nil {
return err return err
} }
@ -156,7 +186,7 @@ func (c *Condition) UnmarshalJSON(data []byte) error {
continue continue
} }
list, ok := val.([]interface{}) list, ok := val.([]any)
if !ok { if !ok {
return errors.New("invalid principal format") return errors.New("invalid principal format")
} }
@ -175,3 +205,110 @@ func (c *Condition) UnmarshalJSON(data []byte) error {
return nil return nil
} }
func (p Policy) Validate(typ PolicyType) error {
if err := p.validate(); err != nil {
return err
}
switch typ {
case IdentityBasedPolicyType:
return p.validateIdentityBased()
case ResourceBasedPolicyType:
return p.validateResourceBased()
default:
return nil
}
}
func (p Policy) validate() error {
for _, statement := range p.Statement {
if !statement.Effect.IsValid() {
return fmt.Errorf("unknown effect: '%s'", statement.Effect)
}
if len(statement.Action) != 0 && len(statement.NotAction) != 0 {
return errors.New("'Actions' and 'NotAction' are mutually exclusive")
}
if statement.Resource != nil && statement.NotResource != nil {
aarifullin marked this conversation as resolved Outdated

It is good that you intend to check mutual exclusion of these fields and this semantically correct but:

  1. I would insist on statement.Resource != nil check instead length checking. If these types weren't be slices or maps you would define a pointer to a type and use check for nil-ness
  2. Is it right that "one of" must surely defined? Are there situtation when neither "Resource" nor "NotResource" are defined?
It is good that you intend to check mutual exclusion of these fields and this semantically correct but: 1. I would insist on `statement.Resource != nil` check instead length checking. If these types weren't be slices or maps you would define a pointer to a type and use check for `nil`-ness 2. Is it right that "one of" must surely defined? Are there situtation when neither "Resource" nor "NotResource" are defined?
  1. Ok, I'll fix this
  2. Yes. No, there isn't such situation
1. Ok, I'll fix this 2. Yes. No, there isn't such situation
return errors.New("'Resources' and 'NotResource' are mutually exclusive")
}
if len(statement.Resource) == 0 && len(statement.NotResource) == 0 {
return errors.New("one of 'Resources'/'NotResource' must be provided")
}
if len(statement.Principal) != 0 && len(statement.NotPrincipal) != 0 {
return errors.New("'Principal' and 'NotPrincipal' are mutually exclusive")
}
if len(statement.NotPrincipal) != 0 && statement.Effect != DenyEffect {
return errors.New("using 'NotPrincipal' with effect 'Allow' is not supported")
}
principal, _ := statement.principal()
if err := principal.validate(); err != nil {
return err
}
}
return nil
}
func (p Policy) validateIdentityBased() error {
if len(p.ID) != 0 {
return errors.New("'Id' is not allowed for identity-based policy")
}
for _, statement := range p.Statement {
if len(statement.Principal) != 0 || len(statement.NotPrincipal) != 0 {
return errors.New("'Principal' and 'NotPrincipal' are not allowed for identity-based policy")
}
}
return nil
}
func (p Policy) validateResourceBased() error {
for _, statement := range p.Statement {
if len(statement.Principal) == 0 && len(statement.NotPrincipal) == 0 {
return errors.New("'Principal' or 'NotPrincipal' must be provided for resource-based policy")
}
}
return nil
}
func (s Statement) principal() (Principal, bool) {
if len(s.NotPrincipal) != 0 {
return s.NotPrincipal, true
}
return s.Principal, false
}
func (s Statement) action() (Action, bool) {
if len(s.NotAction) != 0 {
return s.NotAction, true
}
return s.Action, false
}
func (s Statement) resource() (Resource, bool) {
if len(s.NotResource) != 0 {
return s.NotResource, true
}
return s.Resource, false
}
func (p Principal) validate() error {
if _, ok := p[Wildcard]; ok && len(p) == 1 {
return nil
}
for key := range p {
if !key.IsValid() {
return fmt.Errorf("unknown principal type: '%s'", key)
}
}
return nil
}

View file

@ -31,7 +31,7 @@ func TestUnmarshalIAMPolicy(t *testing.T) {
ID: "PutObjPolicy", ID: "PutObjPolicy",
Statement: []Statement{{ Statement: []Statement{{
SID: "DenyObjectsThatAreNotSSEKMS", SID: "DenyObjectsThatAreNotSSEKMS",
Principal: map[string][]string{ Principal: map[PrincipalType][]string{
"*": nil, "*": nil,
}, },
Effect: DenyEffect, Effect: DenyEffect,
@ -78,8 +78,8 @@ func TestUnmarshalIAMPolicy(t *testing.T) {
expected := Policy{ expected := Policy{
Version: "2012-10-17", Version: "2012-10-17",
Statement: []Statement{{ Statement: []Statement{{
Principal: map[string][]string{ Principal: map[PrincipalType][]string{
"AWS": {"arn:aws:iam::111122223333:user/JohnDoe"}, AWSPrincipalType: {"arn:aws:iam::111122223333:user/JohnDoe"},
}, },
Effect: AllowEffect, Effect: AllowEffect,
Action: []string{"s3:PutObject"}, Action: []string{"s3:PutObject"},
@ -113,8 +113,8 @@ func TestUnmarshalIAMPolicy(t *testing.T) {
expected := Policy{ expected := Policy{
Statement: []Statement{{ Statement: []Statement{{
Principal: map[string][]string{ Principal: map[PrincipalType][]string{
"AWS": {"arn:aws:iam::111122223333:user/JohnDoe"}, AWSPrincipalType: {"arn:aws:iam::111122223333:user/JohnDoe"},
}, },
}}, }},
} }
@ -170,4 +170,255 @@ func TestUnmarshalIAMPolicy(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expected, p) require.Equal(t, expected, p)
}) })
t.Run("'Not*' fields", func(t *testing.T) {
policy := `
{
"Id": "PutObjPolicy",
"Statement": [{
"NotPrincipal": {"AWS":["arn:aws:iam::111122223333:user/Alice"]},
"Effect": "Deny",
"NotAction": "s3:PutObject",
"NotResource": "arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"
}]
}`
expected := Policy{
ID: "PutObjPolicy",
Statement: []Statement{{
NotPrincipal: map[PrincipalType][]string{
AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"},
},
Effect: DenyEffect,
NotAction: []string{"s3:PutObject"},
NotResource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
}},
}
var p Policy
err := json.Unmarshal([]byte(policy), &p)
require.NoError(t, err)
require.Equal(t, expected, p)
})
}
func TestValidatePolicies(t *testing.T) {
for _, tc := range []struct {
name string
policy Policy
typ PolicyType
isValid bool
}{
{
name: "valid permission boundaries",
policy: Policy{
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
Resource: []string{Wildcard},
}},
},
typ: GeneralPolicyType,
isValid: true,
},
{
name: "general invalid effect",
policy: Policy{
Statement: []Statement{{
Effect: "dummy",
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
Resource: []string{Wildcard},
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "general invalid principal block",
policy: Policy{
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
Resource: []string{Wildcard},
Principal: map[PrincipalType][]string{Wildcard: nil},
NotPrincipal: map[PrincipalType][]string{Wildcard: nil},
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "general invalid not principal",
policy: Policy{
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
Resource: []string{Wildcard},
NotPrincipal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}},
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "general invalid principal type",
policy: Policy{
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
Resource: []string{Wildcard},
NotPrincipal: map[PrincipalType][]string{"dummy": {"arn:aws:iam::111122223333:user/Alice"}},
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "general invalid action block",
policy: Policy{
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
NotAction: []string{"iam:*"},
Resource: []string{Wildcard},
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "general invalid resource block",
policy: Policy{
Statement: []Statement{{
Effect: AllowEffect,
Resource: []string{Wildcard},
NotResource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "invalid resource block",
policy: Policy{
Statement: []Statement{{
Effect: AllowEffect,
Resource: []string{},
NotResource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "missing resource block",
policy: Policy{
Statement: []Statement{{
Effect: AllowEffect,
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "identity based valid",
policy: Policy{
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:PutObject"},
Resource: []string{Wildcard},
}},
},
typ: IdentityBasedPolicyType,
isValid: true,
},
{
name: "identity based invalid because of id presence",
policy: Policy{
ID: "some-id",
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:PutObject"},
Resource: []string{Wildcard},
}},
},
typ: IdentityBasedPolicyType,
isValid: false,
},
{
name: "identity based invalid because of principal presence",
policy: Policy{
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:PutObject"},
Resource: []string{Wildcard},
Principal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}},
}},
},
typ: IdentityBasedPolicyType,
isValid: false,
},
{
name: "identity based invalid because of not principal presence",
policy: Policy{
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:PutObject"},
Resource: []string{Wildcard},
NotPrincipal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}},
}},
},
typ: IdentityBasedPolicyType,
isValid: false,
},
{
name: "resource based valid principal",
policy: Policy{
Statement: []Statement{{
Effect: DenyEffect,
Action: []string{"s3:PutObject"},
Resource: []string{Wildcard},
Principal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}},
}},
},
typ: ResourceBasedPolicyType,
isValid: true,
},
{
name: "resource based valid not principal",
policy: Policy{
ID: "some-id",
Statement: []Statement{{
Effect: DenyEffect,
Action: []string{"s3:PutObject"},
Resource: []string{Wildcard},
NotPrincipal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}},
}},
},
typ: ResourceBasedPolicyType,
isValid: true,
},
{
name: "resource based invalid missing principal",
policy: Policy{
ID: "some-id",
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:PutObject"},
Resource: []string{Wildcard},
}},
},
typ: ResourceBasedPolicyType,
isValid: false,
},
} {
t.Run(tc.name, func(t *testing.T) {
err := tc.policy.Validate(tc.typ)
if tc.isValid {
require.NoError(t, err)
} else {
require.Error(t, err)
}
})
}
} }

View file

@ -8,11 +8,12 @@ import (
func TestInmemory(t *testing.T) { func TestInmemory(t *testing.T) {
const ( const (
object = "native::object::abc/xyz" object = "native::object::abc/xyz"
container = "native::object::abc/*" container = "native::object::abc/*"
namespace = "Tenant1" namespace = "Tenant1"
actor1 = "owner1" namespace2 = "Tenant2"
actor2 = "owner2" actor1 = "owner1"
actor2 = "owner2"
) )
s := NewInMemory() s := NewInMemory()
@ -32,15 +33,15 @@ func TestInmemory(t *testing.T) {
s.AddNameSpaceChain(Ingress, namespace, &Chain{ s.AddNameSpaceChain(Ingress, namespace, &Chain{
Rules: []Rule{ Rules: []Rule{
{ // Restrict to remove ANY object from the namespace. { // Restrict to remove ANY object from the namespace.
Status: AccessDenied, Status: AccessDenied,
Action: []string{"native::object::delete"}, Actions: Actions{Names: []string{"native::object::delete"}},
Resource: []string{"native::object::*"}, Resources: Resources{Names: []string{"native::object::*"}},
}, },
{ // Allow to put object only from the trusted subnet AND trusted actor, deny otherwise. { // Allow to put object only from the trusted subnet AND trusted actor, deny otherwise.
Status: AccessDenied, Status: AccessDenied,
Action: []string{"native::object::put"}, Actions: Actions{Names: []string{"native::object::put"}},
Resource: []string{"native::object::*"}, Resources: Resources{Names: []string{"native::object::*"}},
Any: true, Any: true,
Condition: []Condition{ Condition: []Condition{
{ {
Op: CondStringNotLike, Op: CondStringNotLike,
@ -59,12 +60,22 @@ func TestInmemory(t *testing.T) {
}, },
}) })
s.AddNameSpaceChain(Ingress, namespace2, &Chain{
Rules: []Rule{
{ // Deny all expect "native::object::get" for all objects expect "native::object::abc/xyz".
Status: AccessDenied,
Actions: Actions{Inverted: true, Names: []string{"native::object::get"}},
Resources: Resources{Inverted: true, Names: []string{object}},
},
},
})
s.AddResourceChain(Ingress, container, &Chain{ s.AddResourceChain(Ingress, container, &Chain{
Rules: []Rule{ Rules: []Rule{
{ // Allow to actor2 to get objects from the specific container only if they have `Department=HR` attribute. { // Allow to actor2 to get objects from the specific container only if they have `Department=HR` attribute.
Status: Allow, Status: Allow,
Action: []string{"native::object::get"}, Actions: Actions{Names: []string{"native::object::get"}},
Resource: []string{"native::object::abc/*"}, Resources: Resources{Names: []string{"native::object::abc/*"}},
Condition: []Condition{ Condition: []Condition{
{ {
Op: CondStringEquals, Op: CondStringEquals,
@ -131,6 +142,22 @@ func TestInmemory(t *testing.T) {
require.Equal(t, AccessDenied, status) require.Equal(t, AccessDenied, status)
require.True(t, ok) require.True(t, ok)
}) })
t.Run("inverted rules", func(t *testing.T) {
req := newRequest("native::object::put", newResource(object, nil), nil)
status, ok = s.IsAllowed(Ingress, namespace2, req)
require.Equal(t, NoRuleFound, status)
require.False(t, ok)
req = newRequest("native::object::put", newResource("native::object::cba/def", nil), nil)
status, ok = s.IsAllowed(Ingress, namespace2, req)
require.Equal(t, AccessDenied, status)
require.True(t, ok)
req = newRequest("native::object::get", newResource("native::object::cba/def", nil), nil)
status, ok = s.IsAllowed(Ingress, namespace2, req)
require.Equal(t, NoRuleFound, status)
require.False(t, ok)
})
t.Run("good", func(t *testing.T) { t.Run("good", func(t *testing.T) {
status, ok = s.IsAllowed(Ingress, namespace, reqGood) status, ok = s.IsAllowed(Ingress, namespace, reqGood)
require.Equal(t, NoRuleFound, status) require.Equal(t, NoRuleFound, status)
@ -139,9 +166,9 @@ func TestInmemory(t *testing.T) {
t.Run("quota on a different container", func(t *testing.T) { t.Run("quota on a different container", func(t *testing.T) {
s.AddOverride(Ingress, &Chain{ s.AddOverride(Ingress, &Chain{
Rules: []Rule{{ Rules: []Rule{{
Status: QuotaLimitReached, Status: QuotaLimitReached,
Action: []string{"native::object::put"}, Actions: Actions{Names: []string{"native::object::put"}},
Resource: []string{"native::object::cba/*"}, Resources: Resources{Names: []string{"native::object::cba/*"}},
}}, }},
}) })
@ -152,9 +179,9 @@ func TestInmemory(t *testing.T) {
t.Run("quota on the request container", func(t *testing.T) { t.Run("quota on the request container", func(t *testing.T) {
s.AddOverride(Ingress, &Chain{ s.AddOverride(Ingress, &Chain{
Rules: []Rule{{ Rules: []Rule{{
Status: QuotaLimitReached, Status: QuotaLimitReached,
Action: []string{"native::object::put"}, Actions: Actions{Names: []string{"native::object::put"}},
Resource: []string{"native::object::abc/*"}, Resources: Resources{Names: []string{"native::object::abc/*"}},
}}, }},
}) })