Passed
Pull Request — master (#2556)
by Tolga
03:23 queued 11s
created

internal/schema/linked_schema.go   F

Size/Duplication

Total Lines 482
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
cc 64
eloc 235
dl 0
loc 482
rs 3.28
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A schema.NewLinkedGraph 0 3 1
A schema.*LinkedSchemaGraph.LinkedEntrances 0 7 2
B schema.*LinkedSchemaGraph.findEntranceRewrite 0 20 7
A schema.LinkedEntrance.LinkedEntranceKind 0 2 1
C schema.*LinkedSchemaGraph.findEntrance 0 41 10
F schema.*LinkedSchemaGraph.findEntranceLeaf 0 135 23
A schema.*LinkedSchemaGraph.GetInverseRelation 0 15 5
B schema.*LinkedSchemaGraph.BuildRelationPathChain 0 60 8
B schema.*LinkedSchemaGraph.findRelationEntrance 0 37 7
1
package schema
2
3
import (
4
	"errors"
5
6
	"github.com/Permify/permify/pkg/dsl/utils"
7
	base "github.com/Permify/permify/pkg/pb/base/v1"
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
type LinkedSchemaGraph struct {
14
	schema *base.SchemaDefinition
15
}
16
17
// NewLinkedGraph returns a new instance of LinkedSchemaGraph with the specified base.SchemaDefinition as its schema.
18
// The schema object contains definitions for entities, relationships, and permissions, and is used to construct a graph of
19
// linked schema objects. The graph is used by the PermissionEngine to resolve permissions and expand user sets for a
20
// given request.
21
//
22
// Parameters:
23
//   - schema: pointer to the base.SchemaDefinition that defines the schema objects in the graph
24
//
25
// Returns:
26
//   - pointer to a new instance of LinkedSchemaGraph with the specified schema object
27
func NewLinkedGraph(schema *base.SchemaDefinition) *LinkedSchemaGraph {
28
	return &LinkedSchemaGraph{
29
		schema: schema,
30
	}
31
}
32
33
// LinkedEntranceKind is a string type that represents the kind of LinkedEntrance object. An LinkedEntrance object defines an entry point
34
// into the LinkedSchemaGraph, which is used to resolve permissions and expand user sets for a given request.
35
//
36
// Values:
37
//   - RelationLinkedEntrance: represents an entry point into a relationship object in the schema graph
38
//   - TupleToUserSetLinkedEntrance: represents an entry point into a tuple-to-user-set object in the schema graph
39
//   - ComputedUserSetLinkedEntrance: represents an entry point into a computed user set object in the schema graph
40
type LinkedEntranceKind string
41
42
const (
43
	RelationLinkedEntrance        LinkedEntranceKind = "relation"
44
	TupleToUserSetLinkedEntrance  LinkedEntranceKind = "tuple_to_user_set"
45
	ComputedUserSetLinkedEntrance LinkedEntranceKind = "computed_user_set"
46
	AttributeLinkedEntrance       LinkedEntranceKind = "attribute"
47
	PathChainLinkedEntrance       LinkedEntranceKind = "path_chain"
48
)
49
50
// LinkedEntrance represents an entry point into the LinkedSchemaGraph, which is used to resolve permissions and expand user
51
// sets for a given request. The object contains a kind that specifies the type of entry point (e.g. relation, tuple-to-user-set),
52
// an entry point reference that identifies the specific entry point in the graph, and a tuple set relation reference that
53
// specifies the relation to use when expanding user sets for the entry point.
54
//
55
// Fields:
56
//   - Kind: LinkedEntranceKind representing the type of entry point
57
//   - TargetEntrance: pointer to a base.Entrance that identifies the entry point in the schema graph
58
//   - TupleSetRelation: string that specifies the relation to use when expanding user sets for the entry point
59
//   - PathChain: complete chain of relation references for multi-hop nested attributes
60
type LinkedEntrance struct {
61
	Kind             LinkedEntranceKind
62
	TargetEntrance   *base.Entrance
63
	TupleSetRelation string
64
	PathChain        []*base.RelationReference // Complete chain for multi-hop nested attributes
65
}
66
67
// LinkedEntranceKind returns the kind of the LinkedEntrance object. The kind specifies the type of entry point (e.g. relation,
68
// tuple-to-user-set, computed user set).
69
//
70
// Returns:
71
//   - LinkedEntranceKind representing the type of entry point
72
func (re LinkedEntrance) LinkedEntranceKind() LinkedEntranceKind {
73
	return re.Kind
74
}
75
76
// RelationshipLinkedEntrances returns a slice of LinkedEntrance objects that represent entry points into the LinkedSchemaGraph
77
// for the specified target and source relations. The function recursively searches the graph for all entry points that can
78
// be reached from the target relation through the specified source relation. The resulting entry points contain a reference
79
// to the relation object in the schema graph and the relation used to expand user sets for the entry point. If the target or
80
// source relation does not exist in the schema graph, the function returns an error.
81
//
82
// Parameters:
83
//   - target: pointer to a base.RelationReference that identifies the target relation
84
//   - source: pointer to a base.RelationReference that identifies the source relation used to reach the target relation
85
//
86
// Returns:
87
//   - slice of LinkedEntrance objects that represent entry points into the LinkedSchemaGraph, or an error if the target or
88
//     source relation does not exist in the schema graph
89
func (g *LinkedSchemaGraph) LinkedEntrances(target, source *base.Entrance) ([]*LinkedEntrance, error) {
90
	entries, err := g.findEntrance(target, source, map[string]struct{}{})
91
	if err != nil {
92
		return nil, err
93
	}
94
95
	return entries, nil
96
}
97
98
// findEntrance is a recursive helper function that searches the LinkedSchemaGraph for all entry points that can be reached
99
// from the specified target relation through the specified source relation. The function uses a depth-first search to traverse
100
// the schema graph and identify entry points, marking visited nodes in a map to avoid infinite recursion. If the target or
101
// source relation does not exist in the schema graph, the function returns an error. If the source relation is an action
102
// reference, the function recursively searches the graph for entry points reachable from the action child. If the source
103
// relation is a regular relational reference, the function delegates to findRelationEntrance to search for entry points.
104
//
105
// Parameters:
106
//   - target: pointer to a base.RelationReference that identifies the target relation
107
//   - source: pointer to a base.RelationReference that identifies the source relation used to reach the target relation
108
//   - visited: map used to track visited nodes and avoid infinite recursion
109
//
110
// Returns:
111
//   - slice of LinkedEntrance objects that represent entry points into the LinkedSchemaGraph, or an error if the target or
112
//     source relation does not exist in the schema graph
113
func (g *LinkedSchemaGraph) findEntrance(target, source *base.Entrance, visited map[string]struct{}) ([]*LinkedEntrance, error) {
114
	key := utils.Key(target.GetType(), target.GetValue())
115
	if _, ok := visited[key]; ok {
116
		return nil, nil
117
	}
118
	visited[key] = struct{}{}
119
120
	def, ok := g.schema.EntityDefinitions[target.GetType()]
121
	if !ok {
122
		return nil, errors.New("entity definition not found")
123
	}
124
125
	switch def.References[target.GetValue()] {
126
	case base.EntityDefinition_REFERENCE_PERMISSION:
127
		permission, ok := def.Permissions[target.GetValue()]
128
		if !ok {
129
			return nil, errors.New("permission not found")
130
		}
131
		child := permission.GetChild()
132
		if child.GetRewrite() != nil {
133
			return g.findEntranceRewrite(target, source, child.GetRewrite(), visited)
134
		}
135
		return g.findEntranceLeaf(target, source, child.GetLeaf(), visited)
136
	case base.EntityDefinition_REFERENCE_ATTRIBUTE:
137
		attribute, ok := def.Attributes[target.GetValue()]
138
		if !ok {
139
			return nil, errors.New("attribute not found")
140
		}
141
		return []*LinkedEntrance{
142
			{
143
				Kind: AttributeLinkedEntrance,
144
				TargetEntrance: &base.Entrance{
145
					Type:  target.GetType(),
146
					Value: attribute.GetName(),
147
				},
148
			},
149
		}, nil
150
	case base.EntityDefinition_REFERENCE_RELATION:
151
		return g.findRelationEntrance(target, source, visited)
152
	default:
153
		return nil, ErrUnimplemented
154
	}
155
}
156
157
// findRelationEntrance is a helper function that searches the LinkedSchemaGraph for entry points that can be reached from
158
// the specified target relation through the specified source relation. The function only returns entry points that are directly
159
// related to the target relation (i.e. the relation specified by the source reference is one of the relation's immediate children).
160
// The function recursively searches the children of the target relation and returns all reachable entry points. If the target
161
// or source relation does not exist in the schema graph, the function returns an error.
162
//
163
// Parameters:
164
//   - target: pointer to a base.RelationReference that identifies the target relation
165
//   - source: pointer to a base.RelationReference that identifies the source relation used to reach the target relation
166
//   - visited: map used to track visited nodes and avoid infinite recursion
167
//
168
// Returns:
169
//   - slice of LinkedEntrance objects that represent entry points into the LinkedSchemaGraph, or an error if the target or
170
//     source relation does not exist in the schema graph
171
func (g *LinkedSchemaGraph) findRelationEntrance(target, source *base.Entrance, visited map[string]struct{}) ([]*LinkedEntrance, error) {
172
	var res []*LinkedEntrance
173
174
	entity, ok := g.schema.EntityDefinitions[target.GetType()]
175
	if !ok {
176
		return nil, errors.New("entity definition not found")
177
	}
178
179
	relation, ok := entity.Relations[target.GetValue()]
180
	if !ok {
181
		return nil, errors.New("relation definition not found")
182
	}
183
184
	if IsDirectlyRelated(relation, source) {
185
		res = append(res, &LinkedEntrance{
186
			Kind: RelationLinkedEntrance,
187
			TargetEntrance: &base.Entrance{
188
				Type:  target.GetType(),
189
				Value: target.GetValue(),
190
			},
191
		})
192
	}
193
194
	for _, rel := range relation.GetRelationReferences() {
195
		if rel.GetRelation() != "" {
196
			entrances, err := g.findEntrance(&base.Entrance{
197
				Type:  rel.GetType(),
198
				Value: rel.GetRelation(),
199
			}, source, visited)
200
			if err != nil {
201
				return nil, err
202
			}
203
			res = append(res, entrances...)
204
		}
205
	}
206
207
	return res, nil
208
}
209
210
// findEntranceWithLeaf is a helper function that searches the LinkedSchemaGraph for entry points that can be reached from
211
// the specified target relation through an action reference with a leaf child. The function searches for entry points that are
212
// reachable through a tuple-to-user-set or computed-user-set action. If the action child is a tuple-to-user-set action, the
213
// function recursively searches for entry points reachable through the child's tuple set relation and the child's computed user
214
// set relation. If the action child is a computed-user-set action, the function recursively searches for entry points reachable
215
// through the computed user set relation. The function only returns entry points that can be reached from the target relation
216
// using the specified source relation. If the target or source relation does not exist in the schema graph, the function returns
217
// an error.
218
//
219
// Parameters:
220
//   - target: pointer to a base.RelationReference that identifies the target relation
221
//   - source: pointer to a base.RelationReference that identifies the source relation used to reach the target relation
222
//   - leaf: pointer to a base.Leaf object that represents the child of an action reference
223
//   - visited: map used to track visited nodes and avoid infinite recursion
224
//
225
// Returns:
226
//   - slice of LinkedEntrance objects that represent entry points into the LinkedSchemaGraph, or an error if the target or
227
//     source relation does not exist in the schema graph
228
func (g *LinkedSchemaGraph) findEntranceLeaf(target, source *base.Entrance, leaf *base.Leaf, visited map[string]struct{}) ([]*LinkedEntrance, error) {
229
	switch t := leaf.GetType().(type) {
230
	case *base.Leaf_TupleToUserSet:
231
		tupleSet := t.TupleToUserSet.GetTupleSet().GetRelation()
232
		computedUserSet := t.TupleToUserSet.GetComputed().GetRelation()
233
234
		var res []*LinkedEntrance
235
		entityDefinitions, exists := g.schema.EntityDefinitions[target.GetType()]
236
		if !exists {
237
			return nil, errors.New("entity definition not found")
238
		}
239
240
		relations, exists := entityDefinitions.Relations[tupleSet]
241
		if !exists {
242
			return nil, errors.New("relation definition not found")
243
		}
244
245
		// Cache computed relation path chains to avoid duplicate BuildRelationPathChain calls
246
		relationPathChainCache := make(map[string][]*base.RelationReference)
247
248
		for _, rel := range relations.GetRelationReferences() {
249
			if rel.GetType() == source.GetType() && source.GetValue() == computedUserSet {
250
				res = append(res, &LinkedEntrance{
251
					Kind:             TupleToUserSetLinkedEntrance,
252
					TargetEntrance:   target,
253
					TupleSetRelation: tupleSet,
254
				})
255
			}
256
257
			results, err := g.findEntrance(
258
				&base.Entrance{
259
					Type:  rel.GetType(),
260
					Value: computedUserSet,
261
				},
262
				source,
263
				visited,
264
			)
265
			if err != nil {
266
				return nil, err
267
			}
268
269
			// Handle nested attributes: create PathChainLinkedEntrance for cases with PathChain
270
			var filteredResults []*LinkedEntrance
271
272
			for _, result := range results {
273
				if result.Kind == AttributeLinkedEntrance && target.GetType() != result.TargetEntrance.GetType() {
274
					cacheKey := target.GetType() + "->" + result.TargetEntrance.GetType()
275
276
					var pathChain []*base.RelationReference
277
					var exists bool
278
279
					if pathChain, exists = relationPathChainCache[cacheKey]; !exists {
280
						var err error
281
						pathChain, err = g.BuildRelationPathChain(target.GetType(), result.TargetEntrance.GetType())
282
						if err == nil {
283
							relationPathChainCache[cacheKey] = pathChain
284
						}
285
					}
286
287
					if len(pathChain) > 0 {
288
						// Create PathChainLinkedEntrance for cases with PathChain
289
						res = append(res, &LinkedEntrance{
290
							Kind:             PathChainLinkedEntrance,
291
							TargetEntrance:   result.TargetEntrance,
292
							TupleSetRelation: "",
293
							PathChain:        pathChain,
294
						})
295
						// Skip adding AttributeLinkedEntrance for cases with PathChain
296
					} else {
297
						// No PathChain, keep AttributeLinkedEntrance
298
						filteredResults = append(filteredResults, result)
299
					}
300
				} else {
301
					// Non-nested or other types: keep as-is
302
					filteredResults = append(filteredResults, result)
303
				}
304
			}
305
306
			res = append(res, filteredResults...)
307
		}
308
		return res, nil
309
	case *base.Leaf_ComputedUserSet:
310
		var entrances []*LinkedEntrance
311
312
		if target.GetType() == source.GetType() && t.ComputedUserSet.GetRelation() == source.GetValue() {
313
			entrances = append(entrances, &LinkedEntrance{
314
				Kind:           ComputedUserSetLinkedEntrance,
315
				TargetEntrance: target,
316
			})
317
		}
318
319
		results, err := g.findEntrance(
320
			&base.Entrance{
321
				Type:  target.GetType(),
322
				Value: t.ComputedUserSet.GetRelation(),
323
			},
324
			source,
325
			visited,
326
		)
327
		if err != nil {
328
			return nil, err
329
		}
330
331
		entrances = append(
332
			entrances,
333
			results...,
334
		)
335
		return entrances, nil
336
	case *base.Leaf_ComputedAttribute:
337
		var entrances []*LinkedEntrance
338
		entrances = append(entrances, &LinkedEntrance{
339
			Kind: AttributeLinkedEntrance,
340
			TargetEntrance: &base.Entrance{
341
				Type:  target.GetType(),
342
				Value: t.ComputedAttribute.GetName(),
343
			},
344
		})
345
		return entrances, nil
346
	case *base.Leaf_Call:
347
		var entrances []*LinkedEntrance
348
		for _, arg := range t.Call.GetArguments() {
349
			computedAttr := arg.GetComputedAttribute()
350
			if computedAttr != nil {
351
				entrances = append(entrances, &LinkedEntrance{
352
					Kind: AttributeLinkedEntrance,
353
					TargetEntrance: &base.Entrance{
354
						Type:  target.GetType(),
355
						Value: computedAttr.GetName(),
356
					},
357
				})
358
			}
359
		}
360
		return entrances, nil
361
	default:
362
		return nil, ErrUndefinedLeafType
363
	}
364
}
365
366
// findEntranceWithRewrite is a helper function that searches the LinkedSchemaGraph for entry points that can be reached from
367
// the specified target relation through an action reference with a rewrite child. The function recursively searches each child of
368
// the rewrite and calls either findEntranceWithRewrite or findEntranceWithLeaf, depending on the child's type. The function
369
// only returns entry points that can be reached from the target relation using the specified source relation. If the target or
370
// source relation does not exist in the schema graph, the function returns an error.
371
//
372
// Parameters:
373
//   - target: pointer to a base.RelationReference that identifies the target relation
374
//   - source: pointer to a base.RelationReference that identifies the source relation used to reach the target relation
375
//   - rewrite: pointer to a base.Rewrite object that represents the child of an action reference
376
//   - visited: map used to track visited nodes and avoid infinite recursion
377
//
378
// Returns:
379
//   - slice of LinkedEntrance objects that represent entry points into the LinkedSchemaGraph, or an error if the target or
380
//     source relation does not exist in the schema graph
381
func (g *LinkedSchemaGraph) findEntranceRewrite(target, source *base.Entrance, rewrite *base.Rewrite, visited map[string]struct{}) (results []*LinkedEntrance, err error) {
382
	var res []*LinkedEntrance
383
	for _, child := range rewrite.GetChildren() {
384
		switch child.GetType().(type) {
385
		case *base.Child_Rewrite:
386
			results, err = g.findEntranceRewrite(target, source, child.GetRewrite(), visited)
387
			if err != nil {
388
				return nil, err
389
			}
390
		case *base.Child_Leaf:
391
			results, err = g.findEntranceLeaf(target, source, child.GetLeaf(), visited)
392
			if err != nil {
393
				return nil, err
394
			}
395
		default:
396
			return nil, errors.New("undefined child type")
397
		}
398
		res = append(res, results...)
399
	}
400
	return res, nil
401
}
402
403
// GetInverseRelation finds which relation connects the given entity types.
404
// Returns the relation name that connects sourceEntityType to targetEntityType.
405
func (g *LinkedSchemaGraph) GetInverseRelation(sourceEntityType, targetEntityType string) (string, error) {
406
	entityDef, exists := g.schema.EntityDefinitions[sourceEntityType]
407
	if !exists {
408
		return "", errors.New("source entity definition not found")
409
	}
410
411
	for relationName, relationDef := range entityDef.Relations {
412
		for _, relRef := range relationDef.RelationReferences {
413
			if relRef.GetType() == targetEntityType {
414
				return relationName, nil
415
			}
416
		}
417
	}
418
419
	return "", errors.New("no relation found connecting source to target entity type")
420
}
421
422
// BuildRelationPathChain builds the complete relation path chain for multi-hop nested attributes
423
func (g *LinkedSchemaGraph) BuildRelationPathChain(sourceEntityType, targetEntityType string) ([]*base.RelationReference, error) {
424
	// Try direct relation first
425
	relationName, err := g.GetInverseRelation(sourceEntityType, targetEntityType)
426
	if err == nil {
427
		// Direct relation exists, return single hop
428
		return []*base.RelationReference{
429
			{
430
				Type:     sourceEntityType,
431
				Relation: relationName,
432
			},
433
		}, nil
434
	}
435
436
	// Use BFS to find multi-hop path
437
	visited := make(map[string]bool)
438
	queue := []struct {
439
		entityType string
440
		path       []*base.RelationReference
441
	}{{sourceEntityType, []*base.RelationReference{}}}
442
443
	visited[sourceEntityType] = true
444
445
	for len(queue) > 0 {
446
		current := queue[0]
447
		queue = queue[1:]
448
449
		if current.entityType == targetEntityType {
450
			return current.path, nil
451
		}
452
453
		entityDef, exists := g.schema.EntityDefinitions[current.entityType]
454
		if !exists {
455
			continue
456
		}
457
458
		// Explore all relations from current entity
459
		for relationName, relationDef := range entityDef.Relations {
460
			for _, relRef := range relationDef.RelationReferences {
461
				nextEntityType := relRef.GetType()
462
				if !visited[nextEntityType] {
463
					visited[nextEntityType] = true
464
465
					// Build new path
466
					newPath := make([]*base.RelationReference, len(current.path)+1)
467
					copy(newPath, current.path)
468
					newPath[len(current.path)] = &base.RelationReference{
469
						Type:     current.entityType,
470
						Relation: relationName,
471
					}
472
473
					queue = append(queue, struct {
474
						entityType string
475
						path       []*base.RelationReference
476
					}{nextEntityType, newPath})
477
				}
478
			}
479
		}
480
	}
481
482
	return nil, errors.New("no path found between entity types")
483
}
484