Skip to content

Commit b5d6241

Browse files
committed
feat: comment and refactor rewrite checks
1 parent 0d90a1e commit b5d6241

File tree

2 files changed

+138
-121
lines changed

2 files changed

+138
-121
lines changed

internal/check/engine.go

+111-110
Original file line numberDiff line numberDiff line change
@@ -35,153 +35,146 @@ type (
3535
Query = relationtuple.RelationQuery
3636
)
3737

38+
const WildcardRelation = "..."
39+
3840
func NewEngine(d EngineDependencies) *Engine {
3941
return &Engine{
4042
d: d,
4143
}
4244
}
4345

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

55-
// This is the same as the graph problem "can requested.Subject be reached
56-
// from requested.Object through the first outgoing edge requested.Relation"
57-
//
58-
// We implement recursive depth-first search here.
59-
// TODO replace by more performant algorithm:
60-
// https://github.com/ory/keto/issues/483
61-
62-
ctx = graph.InitVisited(ctx)
63-
g := checkgroup.New(ctx)
64-
65-
for _, sr := range rels {
66-
var wasAlreadyVisited bool
67-
ctx, wasAlreadyVisited = graph.CheckAndAddVisited(ctx, sr.Subject)
68-
if wasAlreadyVisited {
69-
continue
70-
}
71-
72-
// we only have to check Subject here as we know that sr was reached
73-
// from requested.ObjectID, requested.Relation through 0...n
74-
// indirections
75-
if requested.Subject.Equals(sr.Subject) {
76-
// found the requested relation
77-
g.SetIsMember()
78-
break
79-
}
80-
81-
sub, isSubjectSet := sr.Subject.(*relationtuple.SubjectSet)
82-
if !isSubjectSet {
83-
continue
84-
}
57+
// CheckRelationTuple checks if the relation tuple's subject has the relation on
58+
// the object in the namespace either directly or indirectly and returns a check
59+
// result.
60+
func (e *Engine) CheckRelationTuple(ctx context.Context, r *RelationTuple, restDepth int) checkgroup.Result {
61+
// global max-depth takes precedence when it is the lesser or if the request
62+
// max-depth is less than or equal to 0
63+
if globalMaxDepth := e.d.Config(ctx).MaxReadDepth(); restDepth <= 0 || globalMaxDepth < restDepth {
64+
restDepth = globalMaxDepth
65+
}
8566

86-
g.Add(e.subQuery(
87-
requested,
88-
&Query{Object: sub.Object, Relation: sub.Relation, Namespace: sub.Namespace},
89-
restDepth,
90-
))
67+
resultCh := make(chan checkgroup.Result)
68+
go e.checkIsAllowed(ctx, r, restDepth)(ctx, resultCh)
69+
select {
70+
case result := <-resultCh:
71+
return result
72+
case <-ctx.Done():
73+
return checkgroup.Result{Err: errors.WithStack(ctx.Err())}
9174
}
92-
return checkgroup.WithEdge(checkgroup.Edge{
93-
Tuple: *requested,
94-
Type: expand.Union,
95-
}, g.CheckFunc())
9675
}
9776

98-
func (e *Engine) subQuery(
99-
requested *RelationTuple,
100-
query *Query,
101-
restDepth int,
102-
) checkgroup.CheckFunc {
77+
// checkExpandSubject checks the expansions of the subject set of the tuple.
78+
//
79+
// For a relation tuple n:obj#rel@user, checkExpandSubject first queries for all
80+
// subjects that match n:obj#rel@* (arbirary subjects), and then for each
81+
// subject checks subject@user.
82+
func (e *Engine) checkExpandSubject(ctx context.Context, r *RelationTuple, restDepth int) checkgroup.CheckFunc {
10383
if restDepth < 0 {
10484
e.d.Logger().
105-
WithFields(requested.ToLoggerFields()).
85+
WithFields(r.ToLoggerFields()).
10686
Debug("reached max-depth, therefore this query will not be further expanded")
10787
return checkgroup.UnknownMemberFunc
10888
}
109-
11089
return func(ctx context.Context, resultCh chan<- checkgroup.Result) {
11190
e.d.Logger().
112-
WithField("request", requested.String()).
113-
WithField("query", query.String()).
114-
Trace("check one indirection further")
115-
116-
// an empty page token denotes the first page (as tokens are opaque)
117-
var prevPage string
118-
119-
// Special case: check if we can find the subject id directly
120-
if rels, _, err := e.d.RelationTupleManager().GetRelationTuples(ctx, requested.ToQuery()); err == nil && len(rels) > 0 {
121-
resultCh <- checkgroup.Result{
122-
Membership: checkgroup.IsMember,
123-
Tree: &expand.Tree{
124-
Type: expand.Leaf,
125-
Tuple: requested,
126-
},
127-
}
128-
return
129-
}
91+
WithField("request", r.String()).
92+
Trace("check expand subject")
13093

13194
g := checkgroup.New(ctx)
13295

96+
var (
97+
subjects []*RelationTuple
98+
pageToken string
99+
err error
100+
visited bool
101+
innerCtx = graph.InitVisited(ctx)
102+
query = &Query{Namespace: r.Namespace, Object: r.Object, Relation: r.Relation}
103+
)
133104
for {
134-
nextRels, nextPage, err := e.d.RelationTupleManager().GetRelationTuples(ctx, query, x.WithToken(prevPage))
135-
// herodot.ErrNotFound occurs when the namespace is unknown
105+
subjects, pageToken, err = e.d.RelationTupleManager().GetRelationTuples(innerCtx, query, x.WithToken(pageToken))
136106
if errors.Is(err, herodot.ErrNotFound) {
137107
g.Add(checkgroup.NotMemberFunc)
138108
break
139109
} else if err != nil {
140110
g.Add(checkgroup.ErrorFunc(err))
141111
break
142112
}
143-
144-
g.Add(e.isIncluded(ctx, requested, nextRels, restDepth-1))
145-
146-
// loop through pages until either allowed, end of pages, or an error occurred
147-
if nextPage == "" || g.Done() {
113+
for _, s := range subjects {
114+
innerCtx, visited = graph.CheckAndAddVisited(innerCtx, s.Subject)
115+
if visited {
116+
continue
117+
}
118+
if s.Subject.SubjectSet() == nil || s.Subject.SubjectSet().Relation == WildcardRelation {
119+
continue
120+
}
121+
g.Add(e.checkIsAllowed(
122+
innerCtx,
123+
&RelationTuple{
124+
Namespace: s.Subject.SubjectSet().Namespace,
125+
Object: s.Subject.SubjectSet().Object,
126+
Relation: s.Subject.SubjectSet().Relation,
127+
Subject: r.Subject,
128+
},
129+
restDepth-1,
130+
))
131+
}
132+
if pageToken == "" || g.Done() {
148133
break
149134
}
150-
prevPage = nextPage
151135
}
152136

153137
resultCh <- g.Result()
154138
}
155139
}
156140

157-
func (e *Engine) CheckIsMember(ctx context.Context, r *RelationTuple, restDepth int) (bool, error) {
158-
result := e.CheckRelationTuple(ctx, r, restDepth)
159-
if result.Err != nil {
160-
return false, result.Err
161-
}
162-
return result.Membership == checkgroup.IsMember, nil
163-
}
164-
165-
func (e *Engine) CheckRelationTuple(ctx context.Context, r *RelationTuple, restDepth int) checkgroup.Result {
166-
// global max-depth takes precedence when it is the lesser or if the request
167-
// max-depth is less than or equal to 0
168-
if globalMaxDepth := e.d.Config(ctx).MaxReadDepth(); restDepth <= 0 || globalMaxDepth < restDepth {
169-
restDepth = globalMaxDepth
141+
// checkDirect checks if the relation tuple is in the database directly.
142+
func (e *Engine) checkDirect(ctx context.Context, r *RelationTuple, restDepth int) checkgroup.CheckFunc {
143+
if restDepth < 0 {
144+
e.d.Logger().
145+
WithField("method", "checkDirect").
146+
Debug("reached max-depth, therefore this query will not be further expanded")
147+
return checkgroup.UnknownMemberFunc
170148
}
171-
172-
resultCh := make(chan checkgroup.Result)
173-
go e.checkIsAllowed(ctx, r, restDepth)(ctx, resultCh)
174-
select {
175-
case result := <-resultCh:
176-
return result
177-
case <-ctx.Done():
178-
return checkgroup.Result{Err: errors.WithStack(ctx.Err())}
149+
return func(ctx context.Context, resultCh chan<- checkgroup.Result) {
150+
e.d.Logger().
151+
WithField("request", r.String()).
152+
Trace("check direct")
153+
if rels, _, err := e.d.RelationTupleManager().GetRelationTuples(ctx, r.ToQuery()); err == nil && len(rels) > 0 {
154+
resultCh <- checkgroup.Result{
155+
Membership: checkgroup.IsMember,
156+
Tree: &expand.Tree{
157+
Type: expand.Leaf,
158+
Tuple: r,
159+
},
160+
}
161+
} else {
162+
resultCh <- checkgroup.Result{
163+
Membership: checkgroup.NotMember,
164+
}
165+
}
179166
}
180167
}
181168

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

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

192185
g := checkgroup.New(ctx)
193-
g.Add(e.subQuery(r,
194-
&Query{
195-
Object: r.Object,
196-
Relation: r.Relation,
197-
Namespace: r.Namespace,
198-
}, restDepth),
199-
)
186+
187+
// OLD
188+
// g.Add(e.subQuery(r,
189+
// &Query{
190+
// Object: r.Object,
191+
// Relation: r.Relation,
192+
// Namespace: r.Namespace,
193+
// }, restDepth),
194+
// )
195+
// OLD
196+
197+
// NEW
198+
g.Add(e.checkDirect(ctx, r, restDepth-1))
199+
g.Add(e.checkExpandSubject(ctx, r, restDepth))
200+
// NEW
200201

201202
relation, err := e.astRelationFor(ctx, r)
202203
if err != nil {

internal/check/rewrites.go

+27-11
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,12 @@ func (e *Engine) checkInverted(
157157
}
158158
}
159159

160+
// checkComputedUserset rewrites the relation tuple to use the userset relation
161+
// instead of the the relation from the tuple.
162+
//
163+
// A relation tuple n:obj#original_rel@user is rewritten to
164+
// n:obj#userset@user, where the 'userset' relation is taken from the
165+
// userset.Relation.
160166
func (e *Engine) checkComputedUserset(
161167
ctx context.Context,
162168
r *RelationTuple,
@@ -185,8 +191,18 @@ func (e *Engine) checkComputedUserset(
185191
)
186192
}
187193

194+
// checkTupleToUserset rewrites the relation tuple to use the userset relation.
195+
//
196+
// Given a relation tuple like docs:readme#editor@user, and a tuple-to-userset
197+
// rewrite with the relation "parent" and the computed userset relation
198+
// "owner", the following checks will be performed:
199+
//
200+
// * query for all tuples like docs:readme#parent@??? to get a list of subjects
201+
// that have the parent relation on docs:readme
202+
//
203+
// * For each matching subject, then check if subject#owner@user.
188204
func (e *Engine) checkTupleToUserset(
189-
r *RelationTuple,
205+
tuple *RelationTuple,
190206
userset *ast.TupleToUserset,
191207
restDepth int,
192208
) checkgroup.CheckFunc {
@@ -196,24 +212,24 @@ func (e *Engine) checkTupleToUserset(
196212
}
197213

198214
e.d.Logger().
199-
WithField("request", r.String()).
215+
WithField("request", tuple.String()).
200216
WithField("tuple to userset relation", userset.Relation).
201217
WithField("tuple to userset computed", userset.ComputedUsersetRelation).
202218
Trace("check tuple to userset")
203219

204220
return func(ctx context.Context, resultCh chan<- checkgroup.Result) {
205221
var (
206222
prevPage, nextPage string
207-
rts []*RelationTuple
223+
tuples []*RelationTuple
208224
err error
209225
)
210226
g := checkgroup.New(ctx)
211227
for nextPage = "x"; nextPage != "" && !g.Done(); prevPage = nextPage {
212-
rts, nextPage, err = e.d.RelationTupleManager().GetRelationTuples(
228+
tuples, nextPage, err = e.d.RelationTupleManager().GetRelationTuples(
213229
ctx,
214230
&Query{
215-
Namespace: r.Namespace,
216-
Object: r.Object,
231+
Namespace: tuple.Namespace,
232+
Object: tuple.Object,
217233
Relation: userset.Relation,
218234
},
219235
x.WithToken(prevPage))
@@ -222,17 +238,17 @@ func (e *Engine) checkTupleToUserset(
222238
return
223239
}
224240

225-
for _, rt := range rts {
226-
if rt.Subject.SubjectSet() == nil {
241+
for _, t := range tuples {
242+
if t.Subject.SubjectSet() == nil {
227243
continue
228244
}
229245
g.Add(e.checkIsAllowed(
230246
ctx,
231247
&RelationTuple{
232-
Namespace: rt.Subject.SubjectSet().Namespace,
233-
Object: rt.Subject.SubjectSet().Object,
248+
Namespace: t.Subject.SubjectSet().Namespace,
249+
Object: t.Subject.SubjectSet().Object,
234250
Relation: userset.ComputedUsersetRelation,
235-
Subject: r.Subject,
251+
Subject: tuple.Subject,
236252
},
237253
restDepth-1,
238254
))

0 commit comments

Comments
 (0)