Skip to content

Commit

Permalink
feat: expand API (#285)
Browse files Browse the repository at this point in the history
  • Loading branch information
zepatrik authored Nov 10, 2020
1 parent 396c1ae commit a3ca0b8
Show file tree
Hide file tree
Showing 9 changed files with 540 additions and 16 deletions.
4 changes: 4 additions & 0 deletions cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import (
"os"
"sync"

"github.com/ory/keto/expand"

"github.com/ory/keto/check"

"github.com/julienschmidt/httprouter"
Expand Down Expand Up @@ -83,6 +85,8 @@ on configuration options, open the configuration documentation:
rh.RegisterPublicRoutes(router)
ch := check.NewHandler(reg)
ch.RegisterPublicRoutes(router)
eh := expand.NewHandler(reg)
eh.RegisterPublicRoutes(router)

server := graceful.WithDefaults(&http.Server{
Addr: ":4466",
Expand Down
24 changes: 17 additions & 7 deletions driver/registry_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"github.com/ory/herodot"
"github.com/ory/x/logrusx"

"github.com/ory/keto/expand"

"github.com/ory/keto/check"

"github.com/ory/keto/persistence/memory"
Expand All @@ -16,10 +18,11 @@ var _ x.WriterProvider = &RegistryDefault{}
var _ x.LoggerProvider = &RegistryDefault{}

type RegistryDefault struct {
p *memory.Persister
l *logrusx.Logger
w herodot.Writer
e *check.Engine
p *memory.Persister
l *logrusx.Logger
w herodot.Writer
ce *check.Engine
ee *expand.Engine
}

func (r *RegistryDefault) Logger() *logrusx.Logger {
Expand All @@ -44,8 +47,15 @@ func (r *RegistryDefault) RelationTupleManager() relationtuple.Manager {
}

func (r *RegistryDefault) PermissionEngine() *check.Engine {
if r.e == nil {
r.e = check.NewEngine(r)
if r.ce == nil {
r.ce = check.NewEngine(r)
}
return r.ce
}

func (r *RegistryDefault) ExpandEngine() *expand.Engine {
if r.ee == nil {
r.ee = expand.NewEngine(r)
}
return r.e
return r.ee
}
68 changes: 68 additions & 0 deletions expand/engine.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package expand

import (
"context"

"github.com/ory/keto/models"
"github.com/ory/keto/relationtuple"
)

type (
engineDependencies interface {
relationtuple.ManagerProvider
}
Engine struct {
d engineDependencies
}
EngineProvider interface {
ExpandEngine() *Engine
}
)

func NewEngine(d engineDependencies) *Engine {
return &Engine{d: d}
}

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

if us, isUserSet := subject.(*models.UserSet); isUserSet {
subTree := &Tree{
Type: Union,
Subject: subject,
}

rels, err := e.d.RelationTupleManager().GetRelationTuples(ctx, []*models.RelationQuery{{
Relation: us.Relation,
Object: us.Object,
}})
if err != nil {
// TODO error handling
return nil, err
}

if restDepth <= 1 {
subTree.Type = Leaf
return subTree, nil
}

subTree.Children = make([]*Tree, len(rels))
for ri, r := range rels {
subTree.Children[ri], err = e.BuildTree(ctx, r.Subject, restDepth-1)
if err != nil {
// TODO error handling
return nil, err
}
}

return subTree, nil
}

// is UserID
return &Tree{
Type: Leaf,
Subject: subject,
}, nil
}
214 changes: 214 additions & 0 deletions expand/engine_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
package expand_test

import (
"context"
"testing"

"github.com/ory/keto/expand"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/ory/keto/driver"
"github.com/ory/keto/models"
)

func newTestEngine(_ *testing.T) (*driver.RegistryDefault, *expand.Engine) {
reg := &driver.RegistryDefault{}
e := expand.NewEngine(reg)
return reg, e
}

func TestEngine(t *testing.T) {
t.Run("case=returns UserID on expand", func(t *testing.T) {
user := &models.UserID{ID: "user"}
_, e := newTestEngine(t)

tree, err := e.BuildTree(context.Background(), user, 100)
require.NoError(t, err)
assert.Equal(t, &expand.Tree{
Type: expand.Leaf,
Subject: user,
}, tree)
})

t.Run("case=expands one level", func(t *testing.T) {
tommy := &models.UserID{ID: "Tommy"}
paul := &models.UserID{ID: "Paul"}
boulderGroup := &models.Object{
ID: "boulder group",
Namespace: "default",
}
bouldererUserSet := &models.UserSet{
Relation: "member",
Object: boulderGroup,
}
boulderers := []*models.InternalRelationTuple{
{
Relation: bouldererUserSet.Relation,
Object: boulderGroup,
Subject: tommy,
},
{
Relation: bouldererUserSet.Relation,
Object: boulderGroup,
Subject: paul,
},
}
reg, e := newTestEngine(t)

require.NoError(t, reg.RelationTupleManager().WriteRelationTuples(context.Background(), boulderers...))

tree, err := e.BuildTree(context.Background(), bouldererUserSet, 100)
require.NoError(t, err)
assert.Equal(t, &expand.Tree{
Type: expand.Union,
Subject: bouldererUserSet,
Children: []*expand.Tree{
{
Type: expand.Leaf,
Subject: tommy,
},
{
Type: expand.Leaf,
Subject: paul,
},
},
}, tree)
})

t.Run("case=expands two levels", func(t *testing.T) {
reg, e := newTestEngine(t)
expectedTree := &expand.Tree{
Type: expand.Union,
Subject: &models.UserSet{
Object: &models.Object{ID: "z"},
Relation: "transitive member",
},
Children: []*expand.Tree{
{
Type: expand.Union,
Subject: &models.UserSet{
Object: &models.Object{ID: "x"},
Relation: "member",
},
Children: []*expand.Tree{
{
Type: expand.Leaf,
Subject: &models.UserID{ID: "a"},
},
{
Type: expand.Leaf,
Subject: &models.UserID{ID: "b"},
},
{
Type: expand.Leaf,
Subject: &models.UserID{ID: "c"},
},
},
},
{
Type: expand.Union,
Subject: &models.UserSet{
Object: &models.Object{ID: "y"},
Relation: "member",
},
Children: []*expand.Tree{
{
Type: expand.Leaf,
Subject: &models.UserID{ID: "d"},
},
{
Type: expand.Leaf,
Subject: &models.UserID{ID: "e"},
},
{
Type: expand.Leaf,
Subject: &models.UserID{ID: "f"},
},
},
},
},
}

for _, group := range expectedTree.Children {
require.NoError(t, reg.RelationTupleManager().WriteRelationTuples(context.Background(), &models.InternalRelationTuple{
Object: expectedTree.Subject.(*models.UserSet).Object,
Relation: "transitive member",
Subject: &models.UserSet{
Object: group.Subject.(*models.UserSet).Object,
Relation: "member",
},
}))

for _, user := range group.Children {
require.NoError(t, reg.RelationTupleManager().WriteRelationTuples(context.Background(), &models.InternalRelationTuple{
Object: group.Subject.(*models.UserSet).Object,
Relation: "member",
Subject: user.Subject.(*models.UserID),
}))
}
}

actualTree, err := e.BuildTree(context.Background(), expectedTree.Subject, 100)
require.NoError(t, err)
assert.Equal(t, expectedTree, actualTree, "%+v", actualTree.Children[0].Children)
})

t.Run("case=respects max depth", func(t *testing.T) {
reg, e := newTestEngine(t)
root := &models.Object{ID: "root"}
prev := root
for _, sub := range []string{"0", "1", "2", "3"} {
require.NoError(t, reg.RelationTupleManager().WriteRelationTuples(context.Background(), &models.InternalRelationTuple{
Object: prev,
Relation: "child",
Subject: &models.UserSet{
Object: &models.Object{ID: sub},
Relation: "child",
},
}))
prev = &models.Object{ID: sub}
}

expectedTree := &expand.Tree{
Type: expand.Union,
Subject: &models.UserSet{
Object: root,
Relation: "child",
},
Children: []*expand.Tree{
{
Type: expand.Union,
Subject: &models.UserSet{
Object: &models.Object{ID: "0"},
Relation: "child",
},
Children: []*expand.Tree{
{
Type: expand.Union,
Subject: &models.UserSet{
Object: &models.Object{ID: "1"},
Relation: "child",
},
Children: []*expand.Tree{
{
Type: expand.Leaf,
Subject: &models.UserSet{
Object: &models.Object{ID: "2"},
Relation: "child",
},
},
},
},
},
},
},
}

actualTree, err := e.BuildTree(context.Background(), expectedTree.Subject, 4)
require.NoError(t, err)

assert.Equal(t, expectedTree, actualTree)
})
}
53 changes: 53 additions & 0 deletions expand/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package expand

import (
"net/http"
"strconv"

"github.com/julienschmidt/httprouter"

"github.com/ory/keto/models"
"github.com/ory/keto/x"
)

type (
handlerDependencies interface {
EngineProvider
x.LoggerProvider
x.WriterProvider
}
handler struct {
d handlerDependencies
}
)

const routeBase = "/expand"

func NewHandler(d handlerDependencies) *handler {
return &handler{d: d}
}

func (h *handler) RegisterPublicRoutes(router *httprouter.Router) {
router.GET(routeBase, h.getCheck)
}

func (h *handler) getCheck(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
objectID := r.URL.Query().Get("object-id")
relationName := r.URL.Query().Get("relation-name")
depth, err := strconv.ParseInt(r.URL.Query().Get("depth"), 0, 0)
if err != nil {
h.d.Writer().WriteError(w, r, err)
return
}

res, err := h.d.ExpandEngine().BuildTree(r.Context(), &models.UserSet{
Relation: relationName,
Object: (&models.Object{}).FromString(objectID),
}, int(depth))
if err != nil {
h.d.Writer().WriteError(w, r, err)
return
}

h.d.Writer().Write(w, r, res)
}
Loading

0 comments on commit a3ca0b8

Please sign in to comment.