Passed
Pull Request — master (#2556)
by Tolga
02:53
created

schema.*LinkedSchemaGraph.GetInverseRelation   A

Complexity

Conditions 5

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 9
nop 2
dl 0
loc 15
rs 9.3333
c 0
b 0
f 0
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
)
48
49
// LinkedEntrance represents an entry point into the LinkedSchemaGraph, which is used to resolve permissions and expand user
50
// sets for a given request. The object contains a kind that specifies the type of entry point (e.g. relation, tuple-to-user-set),
51
// an entry point reference that identifies the specific entry point in the graph, and a tuple set relation reference that
52
// specifies the relation to use when expanding user sets for the entry point.
53
//
54
// Fields:
55
//   - Kind: LinkedEntranceKind representing the type of entry point
56
//   - TargetEntrance: pointer to a base.Entrance that identifies the entry point in the schema graph
57
//   - TupleSetRelation: string that specifies the relation to use when expanding user sets for the entry point
58
//   - Path: single base.RelationReference that represents the path for nested attributes
59
type LinkedEntrance struct {
60
	Kind             LinkedEntranceKind
61
	TargetEntrance   *base.Entrance
62
	TupleSetRelation string
63
	Path             *base.RelationReference // Single relation reference for nested attributes
64
}
65
66
// LinkedEntranceKind returns the kind of the LinkedEntrance object. The kind specifies the type of entry point (e.g. relation,
67
// tuple-to-user-set, computed user set).
68
//
69
// Returns:
70
//   - LinkedEntranceKind representing the type of entry point
71
func (re LinkedEntrance) LinkedEntranceKind() LinkedEntranceKind {
72
	return re.Kind
73
}
74
75
// RelationshipLinkedEntrances returns a slice of LinkedEntrance objects that represent entry points into the LinkedSchemaGraph
76
// for the specified target and source relations. The function recursively searches the graph for all entry points that can
77
// be reached from the target relation through the specified source relation. The resulting entry points contain a reference
78
// to the relation object in the schema graph and the relation used to expand user sets for the entry point. If the target or
79
// source relation does not exist in the schema graph, the function returns an error.
80
//
81
// Parameters:
82
//   - target: pointer to a base.RelationReference that identifies the target relation
83
//   - source: pointer to a base.RelationReference that identifies the source relation used to reach the target relation
84
//
85
// Returns:
86
//   - slice of LinkedEntrance objects that represent entry points into the LinkedSchemaGraph, or an error if the target or
87
//     source relation does not exist in the schema graph
88
func (g *LinkedSchemaGraph) LinkedEntrances(target, source *base.Entrance) ([]*LinkedEntrance, error) {
89
	entries, err := g.findEntrance(target, source, map[string]struct{}{})
90
	if err != nil {
91
		return nil, err
92
	}
93
94
	return entries, nil
95
}
96
97
// findEntrance is a recursive helper function that searches the LinkedSchemaGraph for all entry points that can be reached
98
// from the specified target relation through the specified source relation. The function uses a depth-first search to traverse
99
// the schema graph and identify entry points, marking visited nodes in a map to avoid infinite recursion. If the target or
100
// source relation does not exist in the schema graph, the function returns an error. If the source relation is an action
101
// reference, the function recursively searches the graph for entry points reachable from the action child. If the source
102
// relation is a regular relational reference, the function delegates to findRelationEntrance to search for entry points.
103
//
104
// Parameters:
105
//   - target: pointer to a base.RelationReference that identifies the target relation
106
//   - source: pointer to a base.RelationReference that identifies the source relation used to reach the target relation
107
//   - visited: map used to track visited nodes and avoid infinite recursion
108
//
109
// Returns:
110
//   - slice of LinkedEntrance objects that represent entry points into the LinkedSchemaGraph, or an error if the target or
111
//     source relation does not exist in the schema graph
112
func (g *LinkedSchemaGraph) findEntrance(target, source *base.Entrance, visited map[string]struct{}) ([]*LinkedEntrance, error) {
113
	key := utils.Key(target.GetType(), target.GetValue())
114
	if _, ok := visited[key]; ok {
115
		return nil, nil
116
	}
117
	visited[key] = struct{}{}
118
119
	def, ok := g.schema.EntityDefinitions[target.GetType()]
120
	if !ok {
121
		return nil, errors.New("entity definition not found")
122
	}
123
124
	switch def.References[target.GetValue()] {
125
	case base.EntityDefinition_REFERENCE_PERMISSION:
126
		permission, ok := def.Permissions[target.GetValue()]
127
		if !ok {
128
			return nil, errors.New("permission not found")
129
		}
130
		child := permission.GetChild()
131
		if child.GetRewrite() != nil {
132
			return g.findEntranceRewrite(target, source, child.GetRewrite(), visited)
133
		}
134
		return g.findEntranceLeaf(target, source, child.GetLeaf(), visited)
135
	case base.EntityDefinition_REFERENCE_ATTRIBUTE:
136
		attribute, ok := def.Attributes[target.GetValue()]
137
		if !ok {
138
			return nil, errors.New("attribute not found")
139
		}
140
		return []*LinkedEntrance{
141
			{
142
				Kind: AttributeLinkedEntrance,
143
				TargetEntrance: &base.Entrance{
144
					Type:  target.GetType(),
145
					Value: attribute.GetName(),
146
				},
147
			},
148
		}, nil
149
	case base.EntityDefinition_REFERENCE_RELATION:
150
		return g.findRelationEntrance(target, source, visited)
151
	default:
152
		return nil, ErrUnimplemented
153
	}
154
}
155
156
// findRelationEntrance is a helper function that searches the LinkedSchemaGraph for entry points that can be reached from
157
// the specified target relation through the specified source relation. The function only returns entry points that are directly
158
// related to the target relation (i.e. the relation specified by the source reference is one of the relation's immediate children).
159
// The function recursively searches the children of the target relation and returns all reachable entry points. If the target
160
// or source relation does not exist in the schema graph, the function returns an error.
161
//
162
// Parameters:
163
//   - target: pointer to a base.RelationReference that identifies the target relation
164
//   - source: pointer to a base.RelationReference that identifies the source relation used to reach the target relation
165
//   - visited: map used to track visited nodes and avoid infinite recursion
166
//
167
// Returns:
168
//   - slice of LinkedEntrance objects that represent entry points into the LinkedSchemaGraph, or an error if the target or
169
//     source relation does not exist in the schema graph
170
func (g *LinkedSchemaGraph) findRelationEntrance(target, source *base.Entrance, visited map[string]struct{}) ([]*LinkedEntrance, error) {
171
	var res []*LinkedEntrance
172
173
	entity, ok := g.schema.EntityDefinitions[target.GetType()]
174
	if !ok {
175
		return nil, errors.New("entity definition not found")
176
	}
177
178
	relation, ok := entity.Relations[target.GetValue()]
179
	if !ok {
180
		return nil, errors.New("relation definition not found")
181
	}
182
183
	if IsDirectlyRelated(relation, source) {
184
		res = append(res, &LinkedEntrance{
185
			Kind: RelationLinkedEntrance,
186
			TargetEntrance: &base.Entrance{
187
				Type:  target.GetType(),
188
				Value: target.GetValue(),
189
			},
190
		})
191
	}
192
193
	for _, rel := range relation.GetRelationReferences() {
194
		if rel.GetRelation() != "" {
195
			entrances, err := g.findEntrance(&base.Entrance{
196
				Type:  rel.GetType(),
197
				Value: rel.GetRelation(),
198
			}, source, visited)
199
			if err != nil {
200
				return nil, err
201
			}
202
			res = append(res, entrances...)
203
		}
204
	}
205
206
	return res, nil
207
}
208
209
// findEntranceWithLeaf is a helper function that searches the LinkedSchemaGraph for entry points that can be reached from
210
// the specified target relation through an action reference with a leaf child. The function searches for entry points that are
211
// reachable through a tuple-to-user-set or computed-user-set action. If the action child is a tuple-to-user-set action, the
212
// function recursively searches for entry points reachable through the child's tuple set relation and the child's computed user
213
// set relation. If the action child is a computed-user-set action, the function recursively searches for entry points reachable
214
// through the computed user set relation. The function only returns entry points that can be reached from the target relation
215
// using the specified source relation. If the target or source relation does not exist in the schema graph, the function returns
216
// an error.
217
//
218
// Parameters:
219
//   - target: pointer to a base.RelationReference that identifies the target relation
220
//   - source: pointer to a base.RelationReference that identifies the source relation used to reach the target relation
221
//   - leaf: pointer to a base.Leaf object that represents the child of an action reference
222
//   - visited: map used to track visited nodes and avoid infinite recursion
223
//
224
// Returns:
225
//   - slice of LinkedEntrance objects that represent entry points into the LinkedSchemaGraph, or an error if the target or
226
//     source relation does not exist in the schema graph
227
func (g *LinkedSchemaGraph) findEntranceLeaf(target, source *base.Entrance, leaf *base.Leaf, visited map[string]struct{}) ([]*LinkedEntrance, error) {
228
	switch t := leaf.GetType().(type) {
229
	case *base.Leaf_TupleToUserSet:
230
		tupleSet := t.TupleToUserSet.GetTupleSet().GetRelation()
231
		computedUserSet := t.TupleToUserSet.GetComputed().GetRelation()
232
233
		var res []*LinkedEntrance
234
		entityDefinitions, exists := g.schema.EntityDefinitions[target.GetType()]
235
		if !exists {
236
			return nil, errors.New("entity definition not found")
237
		}
238
239
		relations, exists := entityDefinitions.Relations[tupleSet]
240
		if !exists {
241
			return nil, errors.New("relation definition not found")
242
		}
243
244
		// Cache computed relation paths to avoid duplicate BuildRelationPath calls
245
		relationPathCache := make(map[string]*base.RelationReference)
246
247
		for _, rel := range relations.GetRelationReferences() {
248
			if rel.GetType() == source.GetType() && source.GetValue() == computedUserSet {
249
				res = append(res, &LinkedEntrance{
250
					Kind:             TupleToUserSetLinkedEntrance,
251
					TargetEntrance:   target,
252
					TupleSetRelation: tupleSet,
253
				})
254
			}
255
256
			results, err := g.findEntrance(
257
				&base.Entrance{
258
					Type:  rel.GetType(),
259
					Value: computedUserSet,
260
				},
261
				source,
262
				visited,
263
			)
264
			if err != nil {
265
				return nil, err
266
			}
267
268
			// Populate relation path for nested attributes with caching
269
			for _, result := range results {
270
				if result.Kind == AttributeLinkedEntrance && target.GetType() != rel.GetType() {
271
					cacheKey := target.GetType() + "->" + rel.GetType()
272
					if relationPath, exists := relationPathCache[cacheKey]; exists {
273
						result.Path = relationPath
274
					} else {
275
						relationPath, err := g.BuildRelationPath(target.GetType(), rel.GetType())
276
						if err == nil {
277
							relationPathCache[cacheKey] = relationPath
278
							result.Path = relationPath
279
						}
280
					}
281
				}
282
			}
283
284
			res = append(res, results...)
285
		}
286
		return res, nil
287
	case *base.Leaf_ComputedUserSet:
288
		var entrances []*LinkedEntrance
289
290
		if target.GetType() == source.GetType() && t.ComputedUserSet.GetRelation() == source.GetValue() {
291
			entrances = append(entrances, &LinkedEntrance{
292
				Kind:           ComputedUserSetLinkedEntrance,
293
				TargetEntrance: target,
294
			})
295
		}
296
297
		results, err := g.findEntrance(
298
			&base.Entrance{
299
				Type:  target.GetType(),
300
				Value: t.ComputedUserSet.GetRelation(),
301
			},
302
			source,
303
			visited,
304
		)
305
		if err != nil {
306
			return nil, err
307
		}
308
309
		entrances = append(
310
			entrances,
311
			results...,
312
		)
313
		return entrances, nil
314
	case *base.Leaf_ComputedAttribute:
315
		var entrances []*LinkedEntrance
316
		entrances = append(entrances, &LinkedEntrance{
317
			Kind: AttributeLinkedEntrance,
318
			TargetEntrance: &base.Entrance{
319
				Type:  target.GetType(),
320
				Value: t.ComputedAttribute.GetName(),
321
			},
322
		})
323
		return entrances, nil
324
	case *base.Leaf_Call:
325
		var entrances []*LinkedEntrance
326
		for _, arg := range t.Call.GetArguments() {
327
			computedAttr := arg.GetComputedAttribute()
328
			if computedAttr != nil {
329
				entrances = append(entrances, &LinkedEntrance{
330
					Kind: AttributeLinkedEntrance,
331
					TargetEntrance: &base.Entrance{
332
						Type:  target.GetType(),
333
						Value: computedAttr.GetName(),
334
					},
335
				})
336
			}
337
		}
338
		return entrances, nil
339
	default:
340
		return nil, ErrUndefinedLeafType
341
	}
342
}
343
344
// findEntranceWithRewrite is a helper function that searches the LinkedSchemaGraph for entry points that can be reached from
345
// the specified target relation through an action reference with a rewrite child. The function recursively searches each child of
346
// the rewrite and calls either findEntranceWithRewrite or findEntranceWithLeaf, depending on the child's type. The function
347
// only returns entry points that can be reached from the target relation using the specified source relation. If the target or
348
// source relation does not exist in the schema graph, the function returns an error.
349
//
350
// Parameters:
351
//   - target: pointer to a base.RelationReference that identifies the target relation
352
//   - source: pointer to a base.RelationReference that identifies the source relation used to reach the target relation
353
//   - rewrite: pointer to a base.Rewrite object that represents the child of an action reference
354
//   - visited: map used to track visited nodes and avoid infinite recursion
355
//
356
// Returns:
357
//   - slice of LinkedEntrance objects that represent entry points into the LinkedSchemaGraph, or an error if the target or
358
//     source relation does not exist in the schema graph
359
func (g *LinkedSchemaGraph) findEntranceRewrite(target, source *base.Entrance, rewrite *base.Rewrite, visited map[string]struct{}) (results []*LinkedEntrance, err error) {
360
	var res []*LinkedEntrance
361
	for _, child := range rewrite.GetChildren() {
362
		switch child.GetType().(type) {
363
		case *base.Child_Rewrite:
364
			results, err = g.findEntranceRewrite(target, source, child.GetRewrite(), visited)
365
			if err != nil {
366
				return nil, err
367
			}
368
		case *base.Child_Leaf:
369
			results, err = g.findEntranceLeaf(target, source, child.GetLeaf(), visited)
370
			if err != nil {
371
				return nil, err
372
			}
373
		default:
374
			return nil, errors.New("undefined child type")
375
		}
376
		res = append(res, results...)
377
	}
378
	return res, nil
379
}
380
381
// GetInverseRelation finds which relation connects the given entity types.
382
// Returns the relation name that connects sourceEntityType to targetEntityType.
383
func (g *LinkedSchemaGraph) GetInverseRelation(sourceEntityType, targetEntityType string) (string, error) {
384
	entityDef, exists := g.schema.EntityDefinitions[sourceEntityType]
385
	if !exists {
386
		return "", errors.New("source entity definition not found")
387
	}
388
389
	for relationName, relationDef := range entityDef.Relations {
390
		for _, relRef := range relationDef.RelationReferences {
391
			if relRef.GetType() == targetEntityType {
392
				return relationName, nil
393
			}
394
		}
395
	}
396
397
	return "", errors.New("no relation found connecting source to target entity type")
398
}
399
400
// BuildRelationPath builds the relation path for nested attributes
401
func (g *LinkedSchemaGraph) BuildRelationPath(sourceEntityType, targetEntityType string) (*base.RelationReference, error) {
402
	relationName, err := g.GetInverseRelation(sourceEntityType, targetEntityType)
403
	if err != nil {
404
		return nil, err
405
	}
406
407
	return &base.RelationReference{
408
		Type:     sourceEntityType,
409
		Relation: relationName,
410
	}, nil
411
}
412