Skip to content

Commit

Permalink
Add Cache.AssignableRelations
Browse files Browse the repository at this point in the history
It can be used to query the model for relations that may exists between
an object and a subject.
  • Loading branch information
ronenh committed Oct 28, 2024
1 parent f798617 commit ea7d738
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 4,863 deletions.
56 changes: 49 additions & 7 deletions cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ import (
"github.com/aserto-dev/azm/model/diff"
stts "github.com/aserto-dev/azm/stats"
dsc "github.com/aserto-dev/go-directory/aserto/directory/common/v3"
"github.com/aserto-dev/go-directory/pkg/derr"
"github.com/samber/lo"
)

type (
ObjectName = model.ObjectName
RelationName = model.RelationName
)

type Cache struct {
Expand Down Expand Up @@ -37,7 +44,7 @@ func (c *Cache) CanUpdate(other *model.Model, stats *stts.Stats) error {
}

// ObjectExists, checks if given object type name exists in the model cache.
func (c *Cache) ObjectExists(on model.ObjectName) bool {
func (c *Cache) ObjectExists(on ObjectName) bool {
c.mtx.RLock()
defer c.mtx.RUnlock()

Expand All @@ -46,7 +53,7 @@ func (c *Cache) ObjectExists(on model.ObjectName) bool {
}

// RelationExists, checks if given relation type, for the given object type, exists in the model cache.
func (c *Cache) RelationExists(on model.ObjectName, rn model.RelationName) bool {
func (c *Cache) RelationExists(on ObjectName, rn RelationName) bool {
c.mtx.RLock()
defer c.mtx.RUnlock()

Expand All @@ -58,7 +65,7 @@ func (c *Cache) RelationExists(on model.ObjectName, rn model.RelationName) bool
}

// PermissionExists, checks if given permission, for the given object type, exists in the model cache.
func (c *Cache) PermissionExists(on model.ObjectName, pn model.RelationName) bool {
func (c *Cache) PermissionExists(on ObjectName, pn RelationName) bool {
c.mtx.RLock()
defer c.mtx.RUnlock()

Expand All @@ -80,11 +87,46 @@ func (c *Cache) ValidateRelation(relation *dsc.Relation) error {
defer c.mtx.RUnlock()

return c.model.ValidateRelation(
model.ObjectName(relation.ObjectType),
ObjectName(relation.ObjectType),
model.ObjectID(relation.ObjectId),
model.RelationName(relation.Relation),
model.ObjectName(relation.SubjectType),
RelationName(relation.Relation),
ObjectName(relation.SubjectType),
model.ObjectID(relation.SubjectId),
model.RelationName(relation.SubjectRelation),
RelationName(relation.SubjectRelation),
)
}

func (c *Cache) AssignableRelations(on, sn ObjectName, sr ...RelationName) ([]RelationName, error) {
if !c.ObjectExists(on) {
return nil, derr.ErrObjectNotFound.Msg(on.String())
}
if !c.ObjectExists(sn) {
return nil, derr.ErrObjectNotFound.Msg(sn.String())
}
for _, srel := range sr {
if !c.RelationExists(sn, srel) {
return nil, derr.ErrRelationNotFound.Msgf("%s#%s", sn, sr)
}
}

matches := lo.PickBy(c.model.Objects[on].Relations, func(rn RelationName, r *model.Relation) bool {
for _, ref := range r.Union {
if ref.Object != sn {
// type mismatch
continue
}

switch {
case ref.IsDirect(), ref.IsWildcard():
if len(sr) == 0 {
return true
}
case ref.IsSubject() && lo.Contains(sr, ref.Relation):
return true
}
}
return false
})

return lo.Keys(matches), nil
}
66 changes: 66 additions & 0 deletions cache/cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package cache_test

import (
"fmt"
"strings"
"testing"

"github.com/aserto-dev/azm/cache"
v3 "github.com/aserto-dev/azm/v3"
"github.com/samber/lo"
"github.com/stretchr/testify/require"
)

type (
ON = cache.ObjectName
RN = cache.RelationName
)

type testCase struct {
on ON
sn ON
sr []RN
expected []RN
}

func (t *testCase) name() string {
name := fmt.Sprintf("%s#?@%s", t.on, t.sn)
switch len(t.sr) {
case 0:
return name
case 1:
return fmt.Sprintf("%s#%s", name, t.sr[0])
default:
srs := strings.Join(lo.Map(t.sr, func(sr RN, _ int) string { return sr.String() }), "|")
return fmt.Sprintf("%s#[%s]", name, srs)
}
}

func TestAssignableRelations(t *testing.T) {
m, err := v3.LoadFile("./cache_test.yaml")
require.NoError(t, err)
require.NotNil(t, m)

c := cache.New(m)

tests := []*testCase{
{"machine", "user", nil, []RN{"owner"}},
{"machine", "tenant", nil, nil},
{"group", "group", []RN{"member"}, []RN{"member", "guest"}},
{"group", "user", nil, []RN{"member", "guest"}},
{"tenant", "user", nil, []RN{"owner", "admin", "viewer"}},
{"tenant", "group", nil, nil},
{"tenant", "machine", nil, nil},
{"tenant", "machine", []RN{"owner"}, []RN{"log_writer", "data_reader"}},
}

for _, test := range tests {
t.Run(test.name(), func(tt *testing.T) {
assert := require.New(tt)
actual, err := c.AssignableRelations(test.on, test.sn, test.sr...)
assert.NoError(err)
assert.Equal(len(test.expected), len(actual))
assert.Subset(test.expected, actual)
})
}
}
27 changes: 27 additions & 0 deletions cache/cache_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# yaml-language-server: $schema=manifest.json
---

# model
model:
version: 3

types:
user: {}

machine:
relations:
owner: user

group:
relations:
member: user | group#member
guest: user | group#member

tenant:
relations:
owner: user
admin: user | group#member
viewer: user | group#member | group#guest

log_writer: machine#owner
data_reader: machine#owner
Loading

0 comments on commit ea7d738

Please sign in to comment.