Skip to content

Commit

Permalink
feat: comment and refactor rewrite checks
Browse files Browse the repository at this point in the history
  • Loading branch information
hperl committed Jul 27, 2022
1 parent 0d90a1e commit 2183cd8
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 121 deletions.
208 changes: 98 additions & 110 deletions internal/check/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,153 +35,146 @@ type (
Query = relationtuple.RelationQuery
)

const WildcardRelation = "..."

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

func (e *Engine) isIncluded(
ctx context.Context,
requested *RelationTuple,
rels []*RelationTuple,
restDepth int,
) checkgroup.CheckFunc {
if restDepth < 0 {
e.d.Logger().Debug("reached max-depth, therefore this query will not be further expanded")
return checkgroup.UnknownMemberFunc
// CheckIsMember checks if the relation tuple's subject has the relation on the
// object in the namespace either directly or indirectly and returns a boolean
// result.
func (e *Engine) CheckIsMember(ctx context.Context, r *RelationTuple, restDepth int) (bool, error) {
result := e.CheckRelationTuple(ctx, r, restDepth)
if result.Err != nil {
return false, result.Err
}
return result.Membership == checkgroup.IsMember, nil
}

// This is the same as the graph problem "can requested.Subject be reached
// from requested.Object through the first outgoing edge requested.Relation"
//
// We implement recursive depth-first search here.
// TODO replace by more performant algorithm:
// https://github.com/ory/keto/issues/483

ctx = graph.InitVisited(ctx)
g := checkgroup.New(ctx)

for _, sr := range rels {
var wasAlreadyVisited bool
ctx, wasAlreadyVisited = graph.CheckAndAddVisited(ctx, sr.Subject)
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
g.SetIsMember()
break
}

sub, isSubjectSet := sr.Subject.(*relationtuple.SubjectSet)
if !isSubjectSet {
continue
}
// CheckRelationTuple checks if the relation tuple's subject has the relation on
// the object in the namespace either directly or indirectly and returns a check
// result.
func (e *Engine) CheckRelationTuple(ctx context.Context, r *RelationTuple, restDepth int) checkgroup.Result {
// global max-depth takes precedence when it is the lesser or if the request
// max-depth is less than or equal to 0
if globalMaxDepth := e.d.Config(ctx).MaxReadDepth(); restDepth <= 0 || globalMaxDepth < restDepth {
restDepth = globalMaxDepth
}

g.Add(e.subQuery(
requested,
&Query{Object: sub.Object, Relation: sub.Relation, Namespace: sub.Namespace},
restDepth,
))
resultCh := make(chan checkgroup.Result)
go e.checkIsAllowed(ctx, r, restDepth)(ctx, resultCh)
select {
case result := <-resultCh:
return result
case <-ctx.Done():
return checkgroup.Result{Err: errors.WithStack(ctx.Err())}
}
return checkgroup.WithEdge(checkgroup.Edge{
Tuple: *requested,
Type: expand.Union,
}, g.CheckFunc())
}

func (e *Engine) subQuery(
requested *RelationTuple,
query *Query,
restDepth int,
) checkgroup.CheckFunc {
// checkExpandSubject checks the expansions of the subject set of the tuple.
//
// For a relation tuple n:obj#rel@user, checkExpandSubject first queries for all
// subjects that match n:obj#rel@* (arbirary subjects), and then for each
// subject checks subject@user.
func (e *Engine) checkExpandSubject(ctx context.Context, r *RelationTuple, restDepth int) checkgroup.CheckFunc {
if restDepth < 0 {
e.d.Logger().
WithFields(requested.ToLoggerFields()).
WithFields(r.ToLoggerFields()).
Debug("reached max-depth, therefore this query will not be further expanded")
return checkgroup.UnknownMemberFunc
}

return func(ctx context.Context, resultCh chan<- checkgroup.Result) {
e.d.Logger().
WithField("request", requested.String()).
WithField("query", query.String()).
Trace("check one indirection further")

// an empty page token denotes the first page (as tokens are opaque)
var prevPage string

// Special case: check if we can find the subject id directly
if rels, _, err := e.d.RelationTupleManager().GetRelationTuples(ctx, requested.ToQuery()); err == nil && len(rels) > 0 {
resultCh <- checkgroup.Result{
Membership: checkgroup.IsMember,
Tree: &expand.Tree{
Type: expand.Leaf,
Tuple: requested,
},
}
return
}
WithField("request", r.String()).
Trace("check expand subject")

g := checkgroup.New(ctx)

var (
subjects []*RelationTuple
pageToken string
err error
visited bool
innerCtx = graph.InitVisited(ctx)
query = &Query{Namespace: r.Namespace, Object: r.Object, Relation: r.Relation}
)
for {
nextRels, nextPage, err := e.d.RelationTupleManager().GetRelationTuples(ctx, query, x.WithToken(prevPage))
// herodot.ErrNotFound occurs when the namespace is unknown
subjects, pageToken, err = e.d.RelationTupleManager().GetRelationTuples(innerCtx, query, x.WithToken(pageToken))
if errors.Is(err, herodot.ErrNotFound) {
g.Add(checkgroup.NotMemberFunc)
break
} else if err != nil {
g.Add(checkgroup.ErrorFunc(err))
break
}

g.Add(e.isIncluded(ctx, requested, nextRels, restDepth-1))

// loop through pages until either allowed, end of pages, or an error occurred
if nextPage == "" || g.Done() {
for _, s := range subjects {
innerCtx, visited = graph.CheckAndAddVisited(innerCtx, s.Subject)
if visited {
continue
}
if s.Subject.SubjectSet() == nil || s.Subject.SubjectSet().Relation == WildcardRelation {
continue
}
g.Add(e.checkIsAllowed(
innerCtx,
&RelationTuple{
Namespace: s.Subject.SubjectSet().Namespace,
Object: s.Subject.SubjectSet().Object,
Relation: s.Subject.SubjectSet().Relation,
Subject: r.Subject,
},
restDepth-1,
))
}
if pageToken == "" || g.Done() {
break
}
prevPage = nextPage
}

resultCh <- g.Result()
}
}

func (e *Engine) CheckIsMember(ctx context.Context, r *RelationTuple, restDepth int) (bool, error) {
result := e.CheckRelationTuple(ctx, r, restDepth)
if result.Err != nil {
return false, result.Err
}
return result.Membership == checkgroup.IsMember, nil
}

func (e *Engine) CheckRelationTuple(ctx context.Context, r *RelationTuple, restDepth int) checkgroup.Result {
// global max-depth takes precedence when it is the lesser or if the request
// max-depth is less than or equal to 0
if globalMaxDepth := e.d.Config(ctx).MaxReadDepth(); restDepth <= 0 || globalMaxDepth < restDepth {
restDepth = globalMaxDepth
// checkDirect checks if the relation tuple is in the database directly.
func (e *Engine) checkDirect(ctx context.Context, r *RelationTuple, restDepth int) checkgroup.CheckFunc {
if restDepth < 0 {
e.d.Logger().
WithField("method", "checkDirect").
Debug("reached max-depth, therefore this query will not be further expanded")
return checkgroup.UnknownMemberFunc
}

resultCh := make(chan checkgroup.Result)
go e.checkIsAllowed(ctx, r, restDepth)(ctx, resultCh)
select {
case result := <-resultCh:
return result
case <-ctx.Done():
return checkgroup.Result{Err: errors.WithStack(ctx.Err())}
return func(ctx context.Context, resultCh chan<- checkgroup.Result) {
e.d.Logger().
WithField("request", r.String()).
Trace("check direct")
if rels, _, err := e.d.RelationTupleManager().GetRelationTuples(ctx, r.ToQuery()); err == nil && len(rels) > 0 {
resultCh <- checkgroup.Result{
Membership: checkgroup.IsMember,
Tree: &expand.Tree{
Type: expand.Leaf,
Tuple: r,
},
}
} else {
resultCh <- checkgroup.Result{
Membership: checkgroup.NotMember,
}
}
}
}

// checkIsAllowed checks if the relation tuple is allowed (there is a path from
// the relation tuple subject to the namespace, object and relation) either
// directly (in the database), or through subject-set expansions, or through
// user-set rewrites.
func (e *Engine) checkIsAllowed(ctx context.Context, r *RelationTuple, restDepth int) checkgroup.CheckFunc {
if restDepth < 0 {
e.d.Logger().Debug("reached max-depth, therefore this query will not be further expanded")
e.d.Logger().
WithField("method", "checkIsAllowed").
Debug("reached max-depth, therefore this query will not be further expanded")
return checkgroup.UnknownMemberFunc
}

Expand All @@ -190,13 +183,8 @@ func (e *Engine) checkIsAllowed(ctx context.Context, r *RelationTuple, restDepth
Trace("check is allowed")

g := checkgroup.New(ctx)
g.Add(e.subQuery(r,
&Query{
Object: r.Object,
Relation: r.Relation,
Namespace: r.Namespace,
}, restDepth),
)
g.Add(e.checkDirect(ctx, r, restDepth-1))
g.Add(e.checkExpandSubject(ctx, r, restDepth))

relation, err := e.astRelationFor(ctx, r)
if err != nil {
Expand Down
38 changes: 27 additions & 11 deletions internal/check/rewrites.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,12 @@ func (e *Engine) checkInverted(
}
}

// checkComputedUserset rewrites the relation tuple to use the userset relation
// instead of the the relation from the tuple.
//
// A relation tuple n:obj#original_rel@user is rewritten to
// n:obj#userset@user, where the 'userset' relation is taken from the
// userset.Relation.
func (e *Engine) checkComputedUserset(
ctx context.Context,
r *RelationTuple,
Expand Down Expand Up @@ -185,8 +191,18 @@ func (e *Engine) checkComputedUserset(
)
}

// checkTupleToUserset rewrites the relation tuple to use the userset relation.
//
// Given a relation tuple like docs:readme#editor@user, and a tuple-to-userset
// rewrite with the relation "parent" and the computed userset relation
// "owner", the following checks will be performed:
//
// * query for all tuples like docs:readme#parent@??? to get a list of subjects
// that have the parent relation on docs:readme
//
// * For each matching subject, then check if subject#owner@user.
func (e *Engine) checkTupleToUserset(
r *RelationTuple,
tuple *RelationTuple,
userset *ast.TupleToUserset,
restDepth int,
) checkgroup.CheckFunc {
Expand All @@ -196,24 +212,24 @@ func (e *Engine) checkTupleToUserset(
}

e.d.Logger().
WithField("request", r.String()).
WithField("request", tuple.String()).
WithField("tuple to userset relation", userset.Relation).
WithField("tuple to userset computed", userset.ComputedUsersetRelation).
Trace("check tuple to userset")

return func(ctx context.Context, resultCh chan<- checkgroup.Result) {
var (
prevPage, nextPage string
rts []*RelationTuple
tuples []*RelationTuple
err error
)
g := checkgroup.New(ctx)
for nextPage = "x"; nextPage != "" && !g.Done(); prevPage = nextPage {
rts, nextPage, err = e.d.RelationTupleManager().GetRelationTuples(
tuples, nextPage, err = e.d.RelationTupleManager().GetRelationTuples(
ctx,
&Query{
Namespace: r.Namespace,
Object: r.Object,
Namespace: tuple.Namespace,
Object: tuple.Object,
Relation: userset.Relation,
},
x.WithToken(prevPage))
Expand All @@ -222,17 +238,17 @@ func (e *Engine) checkTupleToUserset(
return
}

for _, rt := range rts {
if rt.Subject.SubjectSet() == nil {
for _, t := range tuples {
if t.Subject.SubjectSet() == nil {
continue
}
g.Add(e.checkIsAllowed(
ctx,
&RelationTuple{
Namespace: rt.Subject.SubjectSet().Namespace,
Object: rt.Subject.SubjectSet().Object,
Namespace: t.Subject.SubjectSet().Namespace,
Object: t.Subject.SubjectSet().Object,
Relation: userset.ComputedUsersetRelation,
Subject: r.Subject,
Subject: tuple.Subject,
},
restDepth-1,
))
Expand Down

0 comments on commit 2183cd8

Please sign in to comment.