Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: adding functionality to handle cycles in expand and check engine #623

Merged
merged 5 commits into from
Jun 16, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions internal/check/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package check
import (
"context"
"errors"
"github.com/ory/keto/internal/utils"

"github.com/ory/herodot"

Expand All @@ -16,6 +17,7 @@ type (
}
Engine struct {
d EngineDependencies
u utils.EngineUtils
}
EngineDependencies interface {
relationtuple.ManagerProvider
Expand All @@ -25,6 +27,7 @@ type (
func NewEngine(d EngineDependencies) *Engine {
return &Engine{
d: d,
u: &utils.EngineUtilsProvider{},
}
}

Expand All @@ -35,6 +38,11 @@ func (e *Engine) subjectIsAllowed(ctx context.Context, requested *relationtuple.
// TODO replace by more performant algorithm: https://github.com/ory/keto/issues/483

for _, sr := range rels {
ctx, wasAlreadyVisited := e.u.CheckVisited(ctx, sr.String())
zepatrik marked this conversation as resolved.
Show resolved Hide resolved
if wasAlreadyVisited {
continue
}

// we only have to check Subject here as we know that sr was reached from requested.ObjectID, requested.Relation through 0...n indirections
if requested.Subject.Equals(sr.Subject) {
// found the requested relation
Expand Down
53 changes: 53 additions & 0 deletions internal/check/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,4 +438,57 @@ func TestEngine(t *testing.T) {
assert.True(t, allowed, req.String())
}
})

t.Run("case=circular tuples", func(t *testing.T) {
sendlingerTor, odeonsplatz, centralStation, connected, namesp := "Sendlinger Tor", "Odeonsplatz", "Central Station", "connected", "munich transport"

reg := newDepsProvider(t, []*namespace.Namespace{{Name: namesp}})

require.NoError(t, reg.RelationTupleManager().WriteRelationTuples(context.Background(), []*relationtuple.InternalRelationTuple{
{
Namespace: namesp,
Object: sendlingerTor,
Relation: connected,
Subject: &relationtuple.SubjectSet{
Namespace: namesp,
Object: odeonsplatz,
Relation: connected,
},
},
{
Namespace: namesp,
Object: odeonsplatz,
Relation: connected,
Subject: &relationtuple.SubjectSet{
Namespace: namesp,
Object: centralStation,
Relation: connected,
},
},
{
Namespace: namesp,
Object: centralStation,
Relation: connected,
Subject: &relationtuple.SubjectSet{
Namespace: namesp,
Object: sendlingerTor,
Relation: connected,
},
},
}...))

e := check.NewEngine(reg)

stations := []string{sendlingerTor, odeonsplatz, centralStation}
res, err := e.SubjectIsAllowed(context.Background(), &relationtuple.InternalRelationTuple{
Namespace: namesp,
Object: stations[0],
Relation: connected,
Subject: &relationtuple.SubjectID{
ID: stations[2],
},
})
require.NoError(t, err)
assert.False(t, res)
})
}
12 changes: 11 additions & 1 deletion internal/expand/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package expand

import (
"context"
"github.com/ory/keto/internal/utils"

"github.com/ory/keto/internal/x"

Expand All @@ -14,21 +15,30 @@ type (
}
Engine struct {
d EngineDependencies
u utils.EngineUtils
}
EngineProvider interface {
ExpandEngine() *Engine
}
)

func NewEngine(d EngineDependencies) *Engine {
return &Engine{d: d}
return &Engine{
d: d,
u: &utils.EngineUtilsProvider{},
}
}

func (e *Engine) BuildTree(ctx context.Context, subject relationtuple.Subject, restDepth int) (*Tree, error) {
if restDepth <= 0 {
return nil, nil
}

ctx, wasAlreadyVisited := e.u.CheckVisited(ctx, subject.String())
if wasAlreadyVisited {
return nil, nil
}

if us, isUserSet := subject.(*relationtuple.SubjectSet); isUserSet {
subTree := &Tree{
Type: Union,
Expand Down
73 changes: 73 additions & 0 deletions internal/expand/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,4 +287,77 @@ func TestEngine(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, expectedTree, tree)
})

t.Run("case=circular tuples", func(t *testing.T) {
sendlingerTor, odeonsplatz, centralStation, connected, namesp := "Sendlinger Tor", "Odeonsplatz", "Central Station", "connected", "munich transport"

sendlingerTorSS, odeonsplatzSS, centralStationSS := &relationtuple.SubjectSet{
Namespace: namesp,
Object: sendlingerTor,
Relation: connected,
}, &relationtuple.SubjectSet{
Namespace: namesp,
Object: odeonsplatz,
Relation: connected,
}, &relationtuple.SubjectSet{
Namespace: namesp,
Object: centralStation,
Relation: connected,
}

reg, e := newTestEngine(t, []*namespace.Namespace{{Name: namesp}})

expectedTree := &expand.Tree{
Type: expand.Union,
Subject: sendlingerTorSS,
Children: []*expand.Tree{
{
Type: expand.Union,
Subject: odeonsplatzSS,
Children: []*expand.Tree{
{
Type: expand.Union,
Subject: centralStationSS,
Children: []*expand.Tree{
{
Type: expand.Leaf,
Subject: sendlingerTorSS,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if that was on purpose, but I like that we have can see the circle still in the tree. It makes it possible for the client to see circular references as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it was on purpose, or it will be confusing for the client and might be flagged as an unexpected behaviour.

Children: nil,
},
},
},
},
},
},
}

require.NoError(t, reg.RelationTupleManager().WriteRelationTuples(context.Background(), []*relationtuple.InternalRelationTuple{
{
Namespace: namesp,
Object: sendlingerTor,
Relation: connected,
Subject: odeonsplatzSS,
},
{
Namespace: namesp,
Object: odeonsplatz,
Relation: connected,
Subject: centralStationSS,
},
{
Namespace: namesp,
Object: centralStation,
Relation: connected,
Subject: sendlingerTorSS,
},
}...))

tree, err := e.BuildTree(context.Background(), &relationtuple.SubjectSet{
Namespace: namesp,
Object: sendlingerTor,
Relation: connected,
}, 100)
require.NoError(t, err)
assert.Equal(t, expectedTree, tree)
})
}
38 changes: 38 additions & 0 deletions internal/utils/graph_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package utils
zepatrik marked this conversation as resolved.
Show resolved Hide resolved

import (
"context"
)

type (
EngineUtils interface {
CheckVisited(ctx context.Context, current string) (context.Context, bool)
}
EngineUtilsProvider struct {
EngineUtils
}
zepatrik marked this conversation as resolved.
Show resolved Hide resolved
)

func (*EngineUtilsProvider) CheckVisited(ctx context.Context, current string) (context.Context, bool) {
zepatrik marked this conversation as resolved.
Show resolved Hide resolved
visitedMap, ok := ctx.Value("visitedMap").(map[string]bool)
zepatrik marked this conversation as resolved.
Show resolved Hide resolved
if !ok {
// for the first time initialize the map
visitedMap = make(map[string]bool)
zepatrik marked this conversation as resolved.
Show resolved Hide resolved
visitedMap[current] = true
return context.WithValue(ctx, "visitedMap", visitedMap), false
}

// check if current node was already visited
if visitedMap[current] {
return ctx, true
}

// set current entry to visited
visitedMap[current] = true

return context.WithValue(
ctx,
"visitedMap",
visitedMap,
), false
}
43 changes: 43 additions & 0 deletions internal/utils/graph_utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package utils

import (
"context"
"github.com/stretchr/testify/assert"
"testing"
)

func TestEngineUtilsProvider_CheckVisited(t *testing.T) {
t.Run("case=finds cycle", func(t *testing.T) {
ep := &EngineUtilsProvider{}

linkedList := []string{"A", "B", "C", "B", "D"}

ctx := context.Background()
var isThereACycle bool
for _, n := range linkedList {
ctx, isThereACycle = ep.CheckVisited(ctx, n)
if isThereACycle {
break
}
}

assert.Equal(t, isThereACycle, true)
})

t.Run("case=ignores if no cycle", func(t *testing.T) {
ep := &EngineUtilsProvider{}

list := []string{"A", "B", "C", "D"}

ctx := context.Background()
var isThereACycle bool
for _, n := range list {
ctx, isThereACycle = ep.CheckVisited(ctx, n)
if isThereACycle {
break
}
}

assert.Equal(t, isThereACycle, false)
})
}