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

schema.NewLinkedGraph   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
package schema
2
3
import (
4
	"errors"
5
	"sync"
6
7
	"github.com/Permify/permify/pkg/dsl/utils"
8
	base "github.com/Permify/permify/pkg/pb/base/v1"
9
)
10
11
// LinkedSchemaGraph represents a graph of linked schema objects. The schema object contains definitions for entities,
12
// relationships, and permissions, and the graph is constructed by linking objects together based on their dependencies. The
13
// graph is used by the PermissionEngine to resolve permissions and expand user sets for a given request.
14
//
15
// Fields:
16
//   - schema: pointer to the base.SchemaDefinition that defines the schema objects in the graph
17
type LinkedSchemaGraph struct {
18
	schema *base.SchemaDefinition
19
}
20
21
// NewLinkedGraph returns a new instance of LinkedSchemaGraph with the specified base.SchemaDefinition as its schema.
22
// The schema object contains definitions for entities, relationships, and permissions, and is used to construct a graph of
23
// linked schema objects. The graph is used by the PermissionEngine to resolve permissions and expand user sets for a
24
// given request.
25
//
26
// Parameters:
27
//   - schema: pointer to the base.SchemaDefinition that defines the schema objects in the graph
28
//
29
// Returns:
30
//   - pointer to a new instance of LinkedSchemaGraph with the specified schema object
31
func NewLinkedGraph(schema *base.SchemaDefinition) *LinkedSchemaGraph {
32
	return &LinkedSchemaGraph{
33
		schema: schema,
34
	}
35
}
36
37
// LinkedEntranceKind is a string type that represents the kind of LinkedEntrance object. An LinkedEntrance object defines an entry point
38
// into the LinkedSchemaGraph, which is used to resolve permissions and expand user sets for a given request.
39
//
40
// Values:
41
//   - RelationLinkedEntrance: represents an entry point into a relationship object in the schema graph
42
//   - TupleToUserSetLinkedEntrance: represents an entry point into a tuple-to-user-set object in the schema graph
43
//   - ComputedUserSetLinkedEntrance: represents an entry point into a computed user set object in the schema graph
44
type LinkedEntranceKind string
45
46
const (
47
	RelationLinkedEntrance        LinkedEntranceKind = "relation"
48
	TupleToUserSetLinkedEntrance  LinkedEntranceKind = "tuple_to_user_set"
49
	ComputedUserSetLinkedEntrance LinkedEntranceKind = "computed_user_set"
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.RelationReference
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) RelationshipLinkedEntrances(targets []*base.RelationReference, source *base.RelationReference) ([]*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.RelationReference) {
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.RelationReference, visited map[string]struct{}, mu *sync.Mutex) ([]*LinkedEntrance, error) {
142
	key := utils.Key(target.GetType(), target.GetRelation())
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.GetRelation()] {
159
	case base.EntityDefinition_REFERENCE_PERMISSION:
160
		permission, ok := def.Permissions[target.GetRelation()]
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
		return nil, ErrUnimplemented
171
	case base.EntityDefinition_REFERENCE_RELATION:
172
		return g.findRelationEntrance(target, source, visited, mu)
173
	default:
174
		return nil, ErrUnimplemented
175
	}
176
}
177
178
// findRelationEntrance is a helper function that searches the LinkedSchemaGraph for entry points that can be reached from
179
// the specified target relation through the specified source relation. The function only returns entry points that are directly
180
// related to the target relation (i.e. the relation specified by the source reference is one of the relation's immediate children).
181
// The function recursively searches the children of the target relation and returns all reachable entry points. If the target
182
// or source relation does not exist in the schema graph, the function returns an error.
183
//
184
// Parameters:
185
//   - target: pointer to a base.RelationReference that identifies the target relation
186
//   - source: pointer to a base.RelationReference that identifies the source relation used to reach the target relation
187
//   - visited: map used to track visited nodes and avoid infinite recursion
188
//
189
// Returns:
190
//   - slice of LinkedEntrance objects that represent entry points into the LinkedSchemaGraph, or an error if the target or
191
//     source relation does not exist in the schema graph
192
func (g *LinkedSchemaGraph) findRelationEntrance(target, source *base.RelationReference, visited map[string]struct{}, mu *sync.Mutex) ([]*LinkedEntrance, error) {
193
	var res []*LinkedEntrance
194
195
	entity, ok := g.schema.EntityDefinitions[target.GetType()]
196
	if !ok {
197
		return nil, errors.New("entity definition not found")
198
	}
199
200
	relation, ok := entity.Relations[target.GetRelation()]
201
	if !ok {
202
		return nil, errors.New("relation definition not found")
203
	}
204
205
	if IsDirectlyRelated(relation, source) {
206
		res = append(res, &LinkedEntrance{
207
			Kind: RelationLinkedEntrance,
208
			TargetEntrance: &base.RelationReference{
209
				Type:     target.GetType(),
210
				Relation: target.GetRelation(),
211
			},
212
		})
213
	}
214
215
	for _, rel := range relation.GetRelationReferences() {
216
		if rel.GetRelation() != "" {
217
			entrances, err := g.findEntrance(rel, source, visited, mu)
218
			if err != nil {
219
				return nil, err
220
			}
221
			res = append(res, entrances...)
222
		}
223
	}
224
225
	return res, nil
226
}
227
228
// findEntranceWithLeaf is a helper function that searches the LinkedSchemaGraph for entry points that can be reached from
229
// the specified target relation through an action reference with a leaf child. The function searches for entry points that are
230
// reachable through a tuple-to-user-set or computed-user-set action. If the action child is a tuple-to-user-set action, the
231
// function recursively searches for entry points reachable through the child's tuple set relation and the child's computed user
232
// set relation. If the action child is a computed-user-set action, the function recursively searches for entry points reachable
233
// through the computed user set relation. The function only returns entry points that can be reached from the target relation
234
// using the specified source relation. If the target or source relation does not exist in the schema graph, the function returns
235
// an error.
236
//
237
// Parameters:
238
//   - target: pointer to a base.RelationReference that identifies the target relation
239
//   - source: pointer to a base.RelationReference that identifies the source relation used to reach the target relation
240
//   - leaf: pointer to a base.Leaf object that represents the child of an action reference
241
//   - visited: map used to track visited nodes and avoid infinite recursion
242
//
243
// Returns:
244
//   - slice of LinkedEntrance objects that represent entry points into the LinkedSchemaGraph, or an error if the target or
245
//     source relation does not exist in the schema graph
246
func (g *LinkedSchemaGraph) findEntranceLeaf(target, source *base.RelationReference, leaf *base.Leaf, visited map[string]struct{}, mu *sync.Mutex) ([]*LinkedEntrance, error) {
247
	switch t := leaf.GetType().(type) {
248
	case *base.Leaf_TupleToUserSet:
249
		tupleSet := t.TupleToUserSet.GetTupleSet().GetRelation()
250
		computedUserSet := t.TupleToUserSet.GetComputed().GetRelation()
251
252
		var res []*LinkedEntrance
253
		entityDefinitions, exists := g.schema.EntityDefinitions[target.GetType()]
254
		if !exists {
255
			return nil, errors.New("entity definition not found")
256
		}
257
258
		relations, exists := entityDefinitions.Relations[tupleSet]
259
		if !exists {
260
			return nil, errors.New("relation definition not found")
261
		}
262
263
		for _, rel := range relations.GetRelationReferences() {
264
			if rel.GetType() == source.GetType() && source.GetRelation() == computedUserSet {
265
				res = append(res, &LinkedEntrance{
266
					Kind:             TupleToUserSetLinkedEntrance,
267
					TargetEntrance:   target,
268
					TupleSetRelation: tupleSet,
269
				})
270
			}
271
272
			results, err := g.findEntrance(
273
				&base.RelationReference{
274
					Type:     rel.GetType(),
275
					Relation: computedUserSet,
276
				},
277
				source,
278
				visited,
279
				mu,
280
			)
281
			if err != nil {
282
				return nil, err
283
			}
284
			res = append(res, results...)
285
		}
286
		return res, nil
287
	case *base.Leaf_ComputedUserSet:
288
289
		var entrances []*LinkedEntrance
290
291
		if target.GetType() == source.GetType() && t.ComputedUserSet.GetRelation() == source.GetRelation() {
292
			entrances = append(entrances, &LinkedEntrance{
293
				Kind:           ComputedUserSetLinkedEntrance,
294
				TargetEntrance: target,
295
			})
296
		}
297
298
		results, err := g.findEntrance(
299
			&base.RelationReference{
300
				Type:     target.GetType(),
301
				Relation: t.ComputedUserSet.GetRelation(),
302
			},
303
			source,
304
			visited,
305
			mu,
306
		)
307
		if err != nil {
308
			return nil, err
309
		}
310
311
		entrances = append(
312
			entrances,
313
			results...,
314
		)
315
		return entrances, nil
316
	case *base.Leaf_ComputedAttribute:
317
		return nil, ErrUnimplemented
318
	case *base.Leaf_Call:
319
		return nil, ErrUnimplemented
320
	default:
321
		return nil, ErrUndefinedLeafType
322
	}
323
}
324
325
// findEntranceWithRewrite is a helper function that searches the LinkedSchemaGraph for entry points that can be reached from
326
// the specified target relation through an action reference with a rewrite child. The function recursively searches each child of
327
// the rewrite and calls either findEntranceWithRewrite or findEntranceWithLeaf, depending on the child's type. The function
328
// only returns entry points that can be reached from the target relation using the specified source relation. If the target or
329
// source relation does not exist in the schema graph, the function returns an error.
330
//
331
// Parameters:
332
//   - target: pointer to a base.RelationReference that identifies the target relation
333
//   - source: pointer to a base.RelationReference that identifies the source relation used to reach the target relation
334
//   - rewrite: pointer to a base.Rewrite object that represents the child of an action reference
335
//   - visited: map used to track visited nodes and avoid infinite recursion
336
//
337
// Returns:
338
//   - slice of LinkedEntrance objects that represent entry points into the LinkedSchemaGraph, or an error if the target or
339
//     source relation does not exist in the schema graph
340
func (g *LinkedSchemaGraph) findEntranceRewrite(target, source *base.RelationReference, rewrite *base.Rewrite, visited map[string]struct{}, mu *sync.Mutex) (results []*LinkedEntrance, err error) {
341
	var res []*LinkedEntrance
342
	for _, child := range rewrite.GetChildren() {
343
		switch child.GetType().(type) {
344
		case *base.Child_Rewrite:
345
			results, err = g.findEntranceRewrite(target, source, child.GetRewrite(), visited, mu)
346
			if err != nil {
347
				return nil, err
348
			}
349
		case *base.Child_Leaf:
350
			results, err = g.findEntranceLeaf(target, source, child.GetLeaf(), visited, mu)
351
			if err != nil {
352
				return nil, err
353
			}
354
		default:
355
			return nil, errors.New("undefined child type")
356
		}
357
		res = append(res, results...)
358
	}
359
	return res, nil
360
}
361