From 8d291039d8604a93b84514d02888d255512783f3 Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Mon, 30 Oct 2023 16:34:11 +0300 Subject: [PATCH] [#11] Support inverted action and resource Signed-off-by: Denis Kirillov --- chain.go | 63 ++++++++++++++++++++++++++++++++++++++----- chain_test.go | 6 ++--- inmemory_test.go | 69 +++++++++++++++++++++++++++++++++--------------- 3 files changed, 107 insertions(+), 31 deletions(-) diff --git a/chain.go b/chain.go index 6bb263d..2a57e44 100644 --- a/chain.go +++ b/chain.go @@ -37,15 +37,25 @@ func (c *Chain) DecodeBytes(b []byte) error { type Rule struct { Status Status // Actions the operation is applied to. - Action []string + Actions Actions // List of the resources the operation is applied to. - Resource []string + Resources Resources // True iff individual conditions must be combined with the logical OR. // By default AND is used, so _each_ condition must pass. Any bool Condition []Condition } +type Actions struct { + Inverted bool + Names []string +} + +type Resources struct { + Inverted bool + Names []string +} + type Condition struct { Op ConditionType Object ObjectType @@ -88,6 +98,45 @@ const ( 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: + return "NumericGreaterThan" + case CondNumericGreaterThanEquals: + return "NumericGreaterThanEquals" + default: + return "unknown condition type" + } +} + func (c *Condition) Match(req Request) bool { var val string switch c.Object { @@ -126,9 +175,9 @@ func (c *Condition) Match(req Request) bool { } func (r *Rule) Match(req Request) (status Status, matched bool) { - found := len(r.Resource) == 0 - for i := range r.Resource { - if globMatch(req.Resource().Name(), r.Resource[i]) { + found := len(r.Resources.Names) == 0 + for i := range r.Resources.Names { + if globMatch(req.Resource().Name(), r.Resources.Names[i]) != r.Resources.Inverted { found = true break } @@ -136,8 +185,8 @@ func (r *Rule) Match(req Request) (status Status, matched bool) { if !found { return NoRuleFound, false } - for i := range r.Action { - if globMatch(req.Operation(), r.Action[i]) { + for i := range r.Actions.Names { + if globMatch(req.Operation(), r.Actions.Names[i]) != r.Actions.Inverted { return r.matchCondition(req) } } diff --git a/chain_test.go b/chain_test.go index 204ddf0..690ec37 100644 --- a/chain_test.go +++ b/chain_test.go @@ -11,10 +11,10 @@ func TestEncodeDecode(t *testing.T) { Rules: []Rule{ { Status: Allow, - Action: []string{ + Actions: Actions{Names: []string{ "native::PutObject", - }, - Resource: []string{"*"}, + }}, + Resources: Resources{Names: []string{"*"}}, Condition: []Condition{ { Op: CondStringEquals, diff --git a/inmemory_test.go b/inmemory_test.go index 8a63621..ef70492 100644 --- a/inmemory_test.go +++ b/inmemory_test.go @@ -8,11 +8,12 @@ import ( func TestInmemory(t *testing.T) { const ( - object = "native::object::abc/xyz" - container = "native::object::abc/*" - namespace = "Tenant1" - actor1 = "owner1" - actor2 = "owner2" + object = "native::object::abc/xyz" + container = "native::object::abc/*" + namespace = "Tenant1" + namespace2 = "Tenant2" + actor1 = "owner1" + actor2 = "owner2" ) s := NewInMemory() @@ -32,15 +33,15 @@ func TestInmemory(t *testing.T) { s.AddNameSpaceChain(Ingress, namespace, &Chain{ Rules: []Rule{ { // Restrict to remove ANY object from the namespace. - Status: AccessDenied, - Action: []string{"native::object::delete"}, - Resource: []string{"native::object::*"}, + Status: AccessDenied, + Actions: Actions{Names: []string{"native::object::delete"}}, + Resources: Resources{Names: []string{"native::object::*"}}, }, { // Allow to put object only from the trusted subnet AND trusted actor, deny otherwise. - Status: AccessDenied, - Action: []string{"native::object::put"}, - Resource: []string{"native::object::*"}, - Any: true, + Status: AccessDenied, + Actions: Actions{Names: []string{"native::object::put"}}, + Resources: Resources{Names: []string{"native::object::*"}}, + Any: true, Condition: []Condition{ { 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{ Rules: []Rule{ { // Allow to actor2 to get objects from the specific container only if they have `Department=HR` attribute. - Status: Allow, - Action: []string{"native::object::get"}, - Resource: []string{"native::object::abc/*"}, + Status: Allow, + Actions: Actions{Names: []string{"native::object::get"}}, + Resources: Resources{Names: []string{"native::object::abc/*"}}, Condition: []Condition{ { Op: CondStringEquals, @@ -139,9 +150,9 @@ func TestInmemory(t *testing.T) { t.Run("quota on a different container", func(t *testing.T) { s.AddOverride(Ingress, &Chain{ Rules: []Rule{{ - Status: QuotaLimitReached, - Action: []string{"native::object::put"}, - Resource: []string{"native::object::cba/*"}, + Status: QuotaLimitReached, + Actions: Actions{Names: []string{"native::object::put"}}, + Resources: Resources{Names: []string{"native::object::cba/*"}}, }}, }) @@ -152,9 +163,9 @@ func TestInmemory(t *testing.T) { t.Run("quota on the request container", func(t *testing.T) { s.AddOverride(Ingress, &Chain{ Rules: []Rule{{ - Status: QuotaLimitReached, - Action: []string{"native::object::put"}, - Resource: []string{"native::object::abc/*"}, + Status: QuotaLimitReached, + Actions: Actions{Names: []string{"native::object::put"}}, + Resources: Resources{Names: []string{"native::object::abc/*"}}, }}, }) @@ -163,4 +174,20 @@ func TestInmemory(t *testing.T) { 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) + }) }