forked from TrueCloudLab/policy-engine
[#11] iam: Support 'NotPrincipal', 'NotAction', 'NotResource'
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
This commit is contained in:
parent
8d291039d8
commit
63ecf63a08
5 changed files with 499 additions and 67 deletions
|
@ -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
|
||||||
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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
161
iam/policy.go
161
iam/policy.go
|
@ -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 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,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)
|
||||||
|
@ -174,20 +190,4 @@ func TestInmemory(t *testing.T) {
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue