Passed
Pull Request — master (#1691)
by
unknown
03:16
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
145
	if _, ok := visited[key]; ok {
146
		mu.Unlock()
147
		return nil, nil
148
	}
149
	visited[key] = struct{}{}
150
	mu.Unlock()
151
152
	def, ok := g.schema.EntityDefinitions[target.GetType()]
153
	if !ok {
154
		return nil, errors.New("entity definition not found")
155
	}
156
157
	switch def.References[target.GetValue()] {
158
	case base.EntityDefinition_REFERENCE_PERMISSION:
159
		permission, ok := def.Permissions[target.GetValue()]
160
		if !ok {
161
			return nil, errors.New("permission not found")
162
		}
163
		child := permission.GetChild()
164
		if child.GetRewrite() != nil {
165
			return g.findEntranceRewrite(target, source, child.GetRewrite(), visited, mu)
166
		}
167
		return g.findEntranceLeaf(target, source, child.GetLeaf(), visited, mu)
168
	case base.EntityDefinition_REFERENCE_ATTRIBUTE:
169
		attribute, ok := def.Attributes[target.GetValue()]
170
		if !ok {
171
			return nil, errors.New("attribute not found")
172
		}
173
		return []*LinkedEntrance{
174
			{
175
				Kind: AttributeLinkedEntrance,
176
				TargetEntrance: &base.Entrance{
177
					Type:  target.GetType(),
178
					Value: attribute.GetName(),
179
				},
180
			},
181
		}, nil
182
	case base.EntityDefinition_REFERENCE_RELATION:
183
		return g.findRelationEntrance(target, source, visited, mu)
184
	default:
185
		return nil, ErrUnimplemented
186
	}
187
}
188
189
// findRelationEntrance is a helper function that searches the LinkedSchemaGraph for entry points that can be reached from
190
// the specified target relation through the specified source relation. The function only returns entry points that are directly
191
// related to the target relation (i.e. the relation specified by the source reference is one of the relation's immediate children).
192
// The function recursively searches the children of the target relation and returns all reachable entry points. If the target
193
// or source relation does not exist in the schema graph, the function returns an error.
194
//
195
// Parameters:
196
//   - target: pointer to a base.RelationReference that identifies the target relation
197
//   - source: pointer to a base.RelationReference that identifies the source relation used to reach the target relation
198
//   - visited: map used to track visited nodes and avoid infinite recursion
199
//
200
// Returns:
201
//   - slice of LinkedEntrance objects that represent entry points into the LinkedSchemaGraph, or an error if the target or
202
//     source relation does not exist in the schema graph
203
func (g *LinkedSchemaGraph) findRelationEntrance(target, source *base.Entrance, visited map[string]struct{}, mu *sync.Mutex) ([]*LinkedEntrance, error) {
204
	var res []*LinkedEntrance
205
206
	entity, ok := g.schema.EntityDefinitions[target.GetType()]
207
	if !ok {
208
		return nil, errors.New("entity definition not found")
209
	}
210
211
	relation, ok := entity.Relations[target.GetValue()]
212
	if !ok {
213
		return nil, errors.New("relation definition not found")
214
	}
215
216
	if IsDirectlyRelated(relation, source) {
217
		res = append(res, &LinkedEntrance{
218
			Kind: RelationLinkedEntrance,
219
			TargetEntrance: &base.Entrance{
220
				Type:  target.GetType(),
221
				Value: target.GetValue(),
222
			},
223
		})
224
	}
225
226
	for _, rel := range relation.GetRelationReferences() {
227
		if rel.GetRelation() != "" {
228
			entrances, err := g.findEntrance(&base.Entrance{
229
				Type:  rel.GetType(),
230
				Value: rel.GetRelation(),
231
			}, source, visited, mu)
232
			if err != nil {
233
				return nil, err
234
			}
235
			res = append(res, entrances...)
236
		}
237
	}
238
239
	return res, nil
240
}
241
242
// findEntranceWithLeaf is a helper function that searches the LinkedSchemaGraph for entry points that can be reached from
243
// the specified target relation through an action reference with a leaf child. The function searches for entry points that are
244
// reachable through a tuple-to-user-set or computed-user-set action. If the action child is a tuple-to-user-set action, the
245
// function recursively searches for entry points reachable through the child's tuple set relation and the child's computed user
246
// set relation. If the action child is a computed-user-set action, the function recursively searches for entry points reachable
247
// through the computed user set relation. The function only returns entry points that can be reached from the target relation
248
// using the specified source relation. If the target or source relation does not exist in the schema graph, the function returns
249
// an error.
250
//
251
// Parameters:
252
//   - target: pointer to a base.RelationReference that identifies the target relation
253
//   - source: pointer to a base.RelationReference that identifies the source relation used to reach the target relation
254
//   - leaf: pointer to a base.Leaf object that represents the child of an action reference
255
//   - visited: map used to track visited nodes and avoid infinite recursion
256
//
257
// Returns:
258
//   - slice of LinkedEntrance objects that represent entry points into the LinkedSchemaGraph, or an error if the target or
259
//     source relation does not exist in the schema graph
260
func (g *LinkedSchemaGraph) findEntranceLeaf(target, source *base.Entrance, leaf *base.Leaf, visited map[string]struct{}, mu *sync.Mutex) ([]*LinkedEntrance, error) {
261
	switch t := leaf.GetType().(type) {
262
	case *base.Leaf_TupleToUserSet:
263
		tupleSet := t.TupleToUserSet.GetTupleSet().GetRelation()
264
		computedUserSet := t.TupleToUserSet.GetComputed().GetRelation()
265
266
		var res []*LinkedEntrance
267
		entityDefinitions, exists := g.schema.EntityDefinitions[target.GetType()]
268
		if !exists {
269
			return nil, errors.New("entity definition not found")
270
		}
271
272
		relations, exists := entityDefinitions.Relations[tupleSet]
273
		if !exists {
274
			return nil, errors.New("relation definition not found")
275
		}
276
277
		for _, rel := range relations.GetRelationReferences() {
278
			if rel.GetType() == source.GetType() && source.GetValue() == computedUserSet {
279
				res = append(res, &LinkedEntrance{
280
					Kind:             TupleToUserSetLinkedEntrance,
281
					TargetEntrance:   target,
282
					TupleSetRelation: tupleSet,
283
				})
284
			}
285
286
			results, err := g.findEntrance(
287
				&base.Entrance{
288
					Type:  rel.GetType(),
289
					Value: computedUserSet,
290
				},
291
				source,
292
				visited,
293
				mu,
294
			)
295
			if err != nil {
296
				return nil, err
297
			}
298
			res = append(res, results...)
299
		}
300
		return res, nil
301
	case *base.Leaf_ComputedUserSet:
302
		var entrances []*LinkedEntrance
303
304
		if target.GetType() == source.GetType() && t.ComputedUserSet.GetRelation() == source.GetValue() {
305
			entrances = append(entrances, &LinkedEntrance{
306
				Kind:           ComputedUserSetLinkedEntrance,
307
				TargetEntrance: target,
308
			})
309
		}
310
311
		results, err := g.findEntrance(
312
			&base.Entrance{
313
				Type:  target.GetType(),
314
				Value: t.ComputedUserSet.GetRelation(),
315
			},
316
			source,
317
			visited,
318
			mu,
319
		)
320
		if err != nil {
321
			return nil, err
322
		}
323
324
		entrances = append(
325
			entrances,
326
			results...,
327
		)
328
		return entrances, nil
329
	case *base.Leaf_ComputedAttribute:
330
		var entrances []*LinkedEntrance
331
		entrances = append(entrances, &LinkedEntrance{
332
			Kind: AttributeLinkedEntrance,
333
			TargetEntrance: &base.Entrance{
334
				Type:  target.GetType(),
335
				Value: t.ComputedAttribute.GetName(),
336
			},
337
		})
338
		return entrances, nil
339
	case *base.Leaf_Call:
340
		var entrances []*LinkedEntrance
341
		for _, arg := range t.Call.GetArguments() {
342
			computedAttr := arg.GetComputedAttribute()
343
			if computedAttr != nil {
344
				entrances = append(entrances, &LinkedEntrance{
345
					Kind: AttributeLinkedEntrance,
346
					TargetEntrance: &base.Entrance{
347
						Type:  target.GetType(),
348
						Value: computedAttr.GetName(),
349
					},
350
				})
351
			}
352
		}
353
		return entrances, nil
354
	default:
355
		return nil, ErrUndefinedLeafType
356
	}
357
}
358
359
// findEntranceWithRewrite is a helper function that searches the LinkedSchemaGraph for entry points that can be reached from
360
// the specified target relation through an action reference with a rewrite child. The function recursively searches each child of
361
// the rewrite and calls either findEntranceWithRewrite or findEntranceWithLeaf, depending on the child's type. The function
362
// only returns entry points that can be reached from the target relation using the specified source relation. If the target or
363
// source relation does not exist in the schema graph, the function returns an error.
364
//
365
// Parameters:
366
//   - target: pointer to a base.RelationReference that identifies the target relation
367
//   - source: pointer to a base.RelationReference that identifies the source relation used to reach the target relation
368
//   - rewrite: pointer to a base.Rewrite object that represents the child of an action reference
369
//   - visited: map used to track visited nodes and avoid infinite recursion
370
//
371
// Returns:
372
//   - slice of LinkedEntrance objects that represent entry points into the LinkedSchemaGraph, or an error if the target or
373
//     source relation does not exist in the schema graph
374
func (g *LinkedSchemaGraph) findEntranceRewrite(target, source *base.Entrance, rewrite *base.Rewrite, visited map[string]struct{}, mu *sync.Mutex) (results []*LinkedEntrance, err error) {
375
	var res []*LinkedEntrance
376
	for _, child := range rewrite.GetChildren() {
377
		switch child.GetType().(type) {
378
		case *base.Child_Rewrite:
379
			results, err = g.findEntranceRewrite(target, source, child.GetRewrite(), visited, mu)
380
			if err != nil {
381
				return nil, err
382
			}
383
		case *base.Child_Leaf:
384
			results, err = g.findEntranceLeaf(target, source, child.GetLeaf(), visited, mu)
385
			if err != nil {
386
				return nil, err
387
			}
388
		default:
389
			return nil, errors.New("undefined child type")
390
		}
391
		res = append(res, results...)
392
	}
393
	return res, nil
394
}
395