[#11] Support inverted action and resource

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
This commit is contained in:
Denis Kirillov 2023-10-30 16:34:11 +03:00
parent 5eee1a7334
commit 8d291039d8
3 changed files with 107 additions and 31 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:
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

@ -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,
@ -139,9 +150,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 +163,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/*"}},
}}, }},
}) })
@ -163,4 +174,20 @@ 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)
})
} }