Passed
Pull Request — master (#1465)
by
unknown
02:47
created

schema.*LinkedSchemaGraph.LinkedEntrances   B

Complexity

Conditions 7

Size

Total Lines 33
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 26
nop 2
dl 0
loc 33
rs 7.856
c 0
b 0
f 0
1
package schema
2
3
import (
4
	"errors"
5
	"github.com/Permify/permify/pkg/dsl/utils"
6
	base "github.com/Permify/permify/pkg/pb/base/v1"
7
	"sync"
8
)
9
10
// LinkedSchemaGraph represents a graph of linked schema objects. The schema object contains definitions for entities,
11
// relationships, and permissions, and the graph is constructed by linking objects together based on their dependencies. The
12
// graph is used by the PermissionEngine to resolve permissions and expand user sets for a given request.
13
//
14
// Fields:
15
//   - schema: pointer to the base.SchemaDefinition that defines the schema objects in the graph
16
type LinkedSchemaGraph struct {
17
	schema *base.SchemaDefinition
18
}
19
20
// NewLinkedGraph returns a new instance of LinkedSchemaGraph with the specified base.SchemaDefinition as its schema.
21
// The schema object contains definitions for entities, relationships, and permissions, and is used to construct a graph of
22
// linked schema objects. The graph is used by the PermissionEngine to resolve permissions and expand user sets for a
23
// given request.
24
//
25
// Parameters:
26
//   - schema: pointer to the base.SchemaDefinition that defines the schema objects in the graph
27
//
28
// Returns:
29
//   - pointer to a new instance of LinkedSchemaGraph with the specified schema object
30
func NewLinkedGraph(schema *base.SchemaDefinition) *LinkedSchemaGraph {
31
	return &LinkedSchemaGraph{
32
		schema: schema,
33
	}
34
}
35
36
// LinkedEntranceKind is a string type that represents the kind of LinkedEntrance object. An LinkedEntrance object defines an entry point
37
// into the LinkedSchemaGraph, which is used to resolve permissions and expand user sets for a given request.
38
//
39
// Values:
40
//   - RelationLinkedEntrance: represents an entry point into a relationship object in the schema graph
41
//   - TupleToUserSetLinkedEntrance: represents an entry point into a tuple-to-user-set object in the schema graph
42
//   - ComputedUserSetLinkedEntrance: represents an entry point into a computed user set object in the schema graph
43
type LinkedEntranceKind string
44
45
const (
46
	RelationLinkedEntrance        LinkedEntranceKind = "relation"
47
	TupleToUserSetLinkedEntrance  LinkedEntranceKind = "tuple_to_user_set"
48
	ComputedUserSetLinkedEntrance LinkedEntranceKind = "computed_user_set"
49
	AttributeLinkedEntrance       LinkedEntranceKind = "attribute"
50
)
51
52
// LinkedEntrance represents an entry point into the LinkedSchemaGraph, which is used to resolve permissions and expand user
53
// sets for a given request. The object contains a kind that specifies the type of entry point (e.g. relation, tuple-to-user-set),
54
// an entry point reference that identifies the specific entry point in the graph, and a tuple set relation reference that
55
// specifies the relation to use when expanding user sets for the entry point.
56
//
57
// Fields:
58
//   - Kind: LinkedEntranceKind representing the type of entry point
59
//   - LinkedEntrance: pointer to a base.RelationReference that identifies the entry point in the schema graph
60
//   - TupleSetRelation: pointer to a base.RelationReference that specifies the relation to use when expanding user sets
61
//     for the entry point
62
type LinkedEntrance struct {
63
	Kind             LinkedEntranceKind
64
	TargetEntrance   *base.Entrance
65
	TupleSetRelation string
66
}
67
68
// LinkedEntranceKind returns the kind of the LinkedEntrance object. The kind specifies the type of entry point (e.g. relation,
69
// tuple-to-user-set, computed user set).
70
//
71
// Returns:
72
//   - LinkedEntranceKind representing the type of entry point
73
func (re LinkedEntrance) LinkedEntranceKind() LinkedEntranceKind {
74
	return re.Kind
75
}
76
77
// RelationshipLinkedEntrances returns a slice of LinkedEntrance objects that represent entry points into the LinkedSchemaGraph
78
// for the specified target and source relations. The function recursively searches the graph for all entry points that can
79
// be reached from the target relation through the specified source relation. The resulting entry points contain a reference
80
// to the relation object in the schema graph and the relation used to expand user sets for the entry point. If the target or
81
// source relation does not exist in the schema graph, the function returns an error.
82
//
83
// Parameters:
84
//   - target: pointer to a base.RelationReference that identifies the target relation
85
//   - source: pointer to a base.RelationReference that identifies the source relation used to reach the target relation
86
//
87
// Returns:
88
//   - slice of LinkedEntrance objects that represent entry points into the LinkedSchemaGraph, or an error if the target or
89
//     source relation does not exist in the schema graph
90
func (g *LinkedSchemaGraph) LinkedEntrances(targets []*base.Entrance, source *base.Entrance) ([]*LinkedEntrance, error) {
91
	visited := map[string]struct{}{}
92
	var mu sync.Mutex
93
	var wg sync.WaitGroup
94
	errChan := make(chan error, len(targets))
95
	done := make(chan struct{})
96
	totalEntries := make([]*LinkedEntrance, 0)
97
	for _, target := range targets {
98
		wg.Add(1)
99
		go func(target *base.Entrance) {
100
			defer wg.Done()
101
			entries, err := g.findEntrance(target, source, visited, &mu)
102
			if err != nil {
103
				errChan <- err
104
			}
105
			mu.Lock()
106
			defer mu.Unlock()
107
			totalEntries = append(totalEntries, entries...)
108
		}(target)
109
	}
110
111
	go func() {
112
		wg.Wait()
113
		close(done)
114
	}()
115
116
	select {
117
	case <-done:
118
		// All goroutines finished
119
		return totalEntries, nil
120
	case err := <-errChan:
121
		// Return on the first error encountered
122
		return nil, err
123
	}
124
}
125
126
// findEntrance is a recursive helper function that searches the LinkedSchemaGraph for all entry points that can be reached
127
// from the specified target relation through the specified source relation. The function uses a depth-first search to traverse
128
// the schema graph and identify entry points, marking visited nodes in a map to avoid infinite recursion. If the target or
129
// source relation does not exist in the schema graph, the function returns an error. If the source relation is an action
130
// reference, the function recursively searches the graph for entry points reachable from the action child. If the source
131
// relation is a regular relational reference, the function delegates to findRelationEntrance to search for entry points.
132
//
133
// Parameters:
134
//   - target: pointer to a base.RelationReference that identifies the target relation
135
//   - source: pointer to a base.RelationReference that identifies the source relation used to reach the target relation
136
//   - visited: map used to track visited nodes and avoid infinite recursion
137
//
138
// Returns:
139
//   - slice of LinkedEntrance objects that represent entry points into the LinkedSchemaGraph, or an error if the target or
140
//     source relation does not exist in the schema graph
141
func (g *LinkedSchemaGraph) findEntrance(target, source *base.Entrance, visited map[string]struct{}, mu *sync.Mutex) ([]*LinkedEntrance, error) {
142
	key := utils.Key(target.GetType(), target.GetValue())
143
	mu.Lock()
144
	mu.Lock()
145
146
	if _, ok := visited[key]; ok {
147
		mu.Unlock()
148
		return nil, nil
149
	}
150
	visited[key] = struct{}{}
151
	mu.Unlock()
152
153
	def, ok := g.schema.EntityDefinitions[target.GetType()]
154
	if !ok {
155
		return nil, errors.New("entity definition not found")
156
	}
157
158
	switch def.References[target.GetValue()] {
159
	case base.EntityDefinition_REFERENCE_PERMISSION:
160
		permission, ok := def.Permissions[target.GetValue()]
161
		if !ok {
162
			return nil, errors.New("permission not found")
163
		}
164
		child := permission.GetChild()
165
		if child.GetRewrite() != nil {
166
			return g.findEntranceRewrite(target, source, child.GetRewrite(), visited, mu)
167
		}
168
		return g.findEntranceLeaf(target, source, child.GetLeaf(), visited, mu)
169
	case base.EntityDefinition_REFERENCE_ATTRIBUTE:
170
		attribute, ok := def.Attributes[target.GetValue()]
171
		if !ok {
172
			return nil, errors.New("attribute not found")
173
		}
174
		return []*LinkedEntrance{
175
			{
176
				Kind: AttributeLinkedEntrance,
177
				TargetEntrance: &base.Entrance{
178
					Type:  target.GetType(),
179
					Value: attribute.GetName(),
180
				},
181
			},
182
		}, nil
183
	case base.EntityDefinition_REFERENCE_RELATION:
184
		return g.findRelationEntrance(target, source, visited, mu)
185
	default:
186
		return nil, ErrUnimplemented
187
	}
188
}
189
190
// findRelationEntrance is a helper function that searches the LinkedSchemaGraph for entry points that can be reached from
191
// the specified target relation through the specified source relation. The function only returns entry points that are directly
192
// related to the target relation (i.e. the relation specified by the source reference is one of the relation's immediate children).
193
// The function recursively searches the children of the target relation and returns all reachable entry points. If the target
194
// or source relation does not exist in the schema graph, the function returns an error.
195
//
196
// Parameters:
197
//   - target: pointer to a base.RelationReference that identifies the target relation
198
//   - source: pointer to a base.RelationReference that identifies the source relation used to reach the target relation
199
//   - visited: map used to track visited nodes and avoid infinite recursion
200
//
201
// Returns:
202
//   - slice of LinkedEntrance objects that represent entry points into the LinkedSchemaGraph, or an error if the target or
203
//     source relation does not exist in the schema graph
204
func (g *LinkedSchemaGraph) findRelationEntrance(target, source *base.Entrance, visited map[string]struct{}, mu *sync.Mutex) ([]*LinkedEntrance, error) {
205
	var res []*LinkedEntrance
206
207
	entity, ok := g.schema.EntityDefinitions[target.GetType()]
208
	if !ok {
209
		return nil, errors.New("entity definition not found")
210
	}
211
212
	relation, ok := entity.Relations[target.GetValue()]
213
	if !ok {
214
		return nil, errors.New("relation definition not found")
215
	}
216
217
	if IsDirectlyRelated(relation, source) {
218
		res = append(res, &LinkedEntrance{
219
			Kind: RelationLinkedEntrance,
220
			TargetEntrance: &base.Entrance{
221
				Type:  target.GetType(),
222
				Value: target.GetValue(),
223
			},
224
		})
225
	}
226
227
	for _, rel := range relation.GetRelationReferences() {
228
		if rel.GetRelation() != "" {
229
			entrances, err := g.findEntrance(&base.Entrance{
230
				Type:  rel.GetType(),
231
				Value: rel.GetRelation(),
232
			}, source, visited, mu)
233
			if err != nil {
234
				return nil, err
235
			}
236
			res = append(res, entrances...)
237
		}
238
	}
239
240
	return res, nil
241
}
242
243
// findEntranceWithLeaf is a helper function that searches the LinkedSchemaGraph for entry points that can be reached from
244
// the specified target relation through an action reference with a leaf child. The function searches for entry points that are
245
// reachable through a tuple-to-user-set or computed-user-set action. If the action child is a tuple-to-user-set action, the
246
// function recursively searches for entry points reachable through the child's tuple set relation and the child's computed user
247
// set relation. If the action child is a computed-user-set action, the function recursively searches for entry points reachable
248
// through the computed user set relation. The function only returns entry points that can be reached from the target relation
249
// using the specified source relation. If the target or source relation does not exist in the schema graph, the function returns
250
// an error.
251
//
252
// Parameters:
253
//   - target: pointer to a base.RelationReference that identifies the target relation
254
//   - source: pointer to a base.RelationReference that identifies the source relation used to reach the target relation
255
//   - leaf: pointer to a base.Leaf object that represents the child of an action reference
256
//   - visited: map used to track visited nodes and avoid infinite recursion
257
//
258
// Returns:
259
//   - slice of LinkedEntrance objects that represent entry points into the LinkedSchemaGraph, or an error if the target or
260
//     source relation does not exist in the schema graph
261
func (g *LinkedSchemaGraph) findEntranceLeaf(target, source *base.Entrance, leaf *base.Leaf, visited map[string]struct{}, mu *sync.Mutex) ([]*LinkedEntrance, error) {
262
	switch t := leaf.GetType().(type) {
263
	case *base.Leaf_TupleToUserSet:
264
		tupleSet := t.TupleToUserSet.GetTupleSet().GetRelation()
265
		computedUserSet := t.TupleToUserSet.GetComputed().GetRelation()
266
267
		var res []*LinkedEntrance
268
		entityDefinitions, exists := g.schema.EntityDefinitions[target.GetType()]
269
		if !exists {
270
			return nil, errors.New("entity definition not found")
271
		}
272
273
		relations, exists := entityDefinitions.Relations[tupleSet]
274
		if !exists {
275
			return nil, errors.New("relation definition not found")
276
		}
277
278
		for _, rel := range relations.GetRelationReferences() {
279
			if rel.GetType() == source.GetType() && source.GetValue() == computedUserSet {
280
				res = append(res, &LinkedEntrance{
281
					Kind:             TupleToUserSetLinkedEntrance,
282
					TargetEntrance:   target,
283
					TupleSetRelation: tupleSet,
284
				})
285
			}
286
287
			results, err := g.findEntrance(
288
				&base.Entrance{
289
					Type:  rel.GetType(),
290
					Value: computedUserSet,
291
				},
292
				source,
293
				visited,
294
				mu,
295
			)
296
			if err != nil {
297
				return nil, err
298
			}
299
			res = append(res, results...)
300
		}
301
		return res, nil
302
	case *base.Leaf_ComputedUserSet:
303
		var entrances []*LinkedEntrance
304
305
		if target.GetType() == source.GetType() && t.ComputedUserSet.GetRelation() == source.GetValue() {
306
			entrances = append(entrances, &LinkedEntrance{
307
				Kind:           ComputedUserSetLinkedEntrance,
308
				TargetEntrance: target,
309
			})
310
		}
311
312
		results, err := g.findEntrance(
313
			&base.Entrance{
314
				Type:  target.GetType(),
315
				Value: t.ComputedUserSet.GetRelation(),
316
			},
317
			source,
318
			visited,
319
			mu,
320
		)
321
		if err != nil {
322
			return nil, err
323
		}
324
325
		entrances = append(
326
			entrances,
327
			results...,
328
		)
329
		return entrances, nil
330
	case *base.Leaf_ComputedAttribute:
331
		var entrances []*LinkedEntrance
332
		entrances = append(entrances, &LinkedEntrance{
333
			Kind: AttributeLinkedEntrance,
334
			TargetEntrance: &base.Entrance{
335
				Type:  target.GetType(),
336
				Value: t.ComputedAttribute.GetName(),
337
			},
338
		})
339
		return entrances, nil
340
	case *base.Leaf_Call:
341
		var entrances []*LinkedEntrance
342
		for _, arg := range t.Call.GetArguments() {
343
			computedAttr := arg.GetComputedAttribute()
344
			if computedAttr != nil {
345
				entrances = append(entrances, &LinkedEntrance{
346
					Kind: AttributeLinkedEntrance,
347
					TargetEntrance: &base.Entrance{
348
						Type:  target.GetType(),
349
						Value: computedAttr.GetName(),
350
					},
351
				})
352
			}
353
		}
354
		return entrances, nil
355
	default:
356
		return nil, ErrUndefinedLeafType
357
	}
358
}
359
360
// findEntranceWithRewrite is a helper function that searches the LinkedSchemaGraph for entry points that can be reached from
361
// the specified target relation through an action reference with a rewrite child. The function recursively searches each child of
362
// the rewrite and calls either findEntranceWithRewrite or findEntranceWithLeaf, depending on the child's type. The function
363
// only returns entry points that can be reached from the target relation using the specified source relation. If the target or
364
// source relation does not exist in the schema graph, the function returns an error.
365
//
366
// Parameters:
367
//   - target: pointer to a base.RelationReference that identifies the target relation
368
//   - source: pointer to a base.RelationReference that identifies the source relation used to reach the target relation
369
//   - rewrite: pointer to a base.Rewrite object that represents the child of an action reference
370
//   - visited: map used to track visited nodes and avoid infinite recursion
371
//
372
// Returns:
373
//   - slice of LinkedEntrance objects that represent entry points into the LinkedSchemaGraph, or an error if the target or
374
//     source relation does not exist in the schema graph
375
func (g *LinkedSchemaGraph) findEntranceRewrite(target, source *base.Entrance, rewrite *base.Rewrite, visited map[string]struct{}, mu *sync.Mutex) (results []*LinkedEntrance, err error) {
376
	var res []*LinkedEntrance
377
	for _, child := range rewrite.GetChildren() {
378
		switch child.GetType().(type) {
379
		case *base.Child_Rewrite:
380
			results, err = g.findEntranceRewrite(target, source, child.GetRewrite(), visited, mu)
381
			if err != nil {
382
				return nil, err
383
			}
384
		case *base.Child_Leaf:
385
			results, err = g.findEntranceLeaf(target, source, child.GetLeaf(), visited, mu)
386
			if err != nil {
387
				return nil, err
388
			}
389
		default:
390
			return nil, errors.New("undefined child type")
391
		}
392
		res = append(res, results...)
393
	}
394
	return res, nil
395
}
396