Passed
Pull Request — master (#1603)
by Tolga
03:57
created

engines.NewEntityFilter   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nop 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
package engines
2
3
import (
4
	"context"
5
	"errors"
6
7
	"golang.org/x/sync/errgroup"
8
9
	"github.com/Permify/permify/internal/schema"
10
	"github.com/Permify/permify/internal/storage"
11
	storageContext "github.com/Permify/permify/internal/storage/context"
12
	"github.com/Permify/permify/pkg/database"
13
	base "github.com/Permify/permify/pkg/pb/base/v1"
14
)
15
16
// EntityFilter is a struct that performs permission checks on a set of entities
17
type EntityFilter struct {
18
	// dataReader is responsible for reading relationship information
19
	dataReader storage.DataReader
20
21
	schema *base.SchemaDefinition
22
}
23
24
// NewEntityFilter creates a new EntityFilter engine
25
func NewEntityFilter(dataReader storage.DataReader, sch *base.SchemaDefinition) *EntityFilter {
26
	return &EntityFilter{
27
		dataReader: dataReader,
28
		schema:     sch,
29
	}
30
}
31
32
// EntityFilter is a method of the EntityFilterEngine struct. It executes a permission request for linked entities.
33
func (engine *EntityFilter) EntityFilter(
34
	ctx context.Context, // A context used for tracing and cancellation.
35
	request *base.PermissionEntityFilterRequest, // A permission request for linked entities.
36
	visits *VisitsMap, // A map that keeps track of visited entities to avoid infinite loops.
37
	publisher *BulkEntityPublisher, // A custom publisher that publishes results in bulk.
38
) (err error) { // Returns an error if one occurs during execution.
39
	// Check if direct result
40
	if request.GetEntrance().GetType() == request.GetSubject().GetType() && request.GetEntrance().GetValue() == request.GetSubject().GetRelation() {
41
		found := &base.Entity{
42
			Type: request.GetSubject().GetType(),
43
			Id:   request.GetSubject().GetId(),
44
		}
45
46
		if !visits.AddPublished(found) { // If the entity and relation has already been visited.
47
			return nil
48
		}
49
50
		// If the entity reference is the same as the subject, publish the result directly and return.
51
		publisher.Publish(found, &base.PermissionCheckRequestMetadata{
52
			SnapToken:     request.GetMetadata().GetSnapToken(),
53
			SchemaVersion: request.GetMetadata().GetSchemaVersion(),
54
			Depth:         request.GetMetadata().GetDepth(),
55
		}, request.GetContext(), base.CheckResult_CHECK_RESULT_UNSPECIFIED)
56
	}
57
58
	// Retrieve linked entrances
59
	cn := schema.NewLinkedGraph(engine.schema) // Create a new linked graph from the schema definition.
60
	var entrances []*schema.LinkedEntrance
61
	entrances, err = cn.LinkedEntrances(
62
		request.GetEntrance(),
63
		&base.Entrance{
64
			Type:  request.GetSubject().GetType(),
65
			Value: request.GetSubject().GetRelation(),
66
		},
67
	) // Retrieve the linked entrances between the entity reference and subject.
68
69
	// Create a new context for executing goroutines and a cancel function.
70
	cctx, cancel := context.WithCancel(ctx)
71
	defer cancel()
72
73
	// Create a new errgroup and a new context that inherits the original context.
74
	g, cont := errgroup.WithContext(cctx)
75
76
	// Loop over each linked entrance.
77
	for _, entrance := range entrances {
78
		// Switch on the kind of linked entrance.
79
		switch entrance.LinkedEntranceKind() {
80
		case schema.RelationLinkedEntrance: // If the linked entrance is a relation entrance.
81
			err = engine.relationEntrance(cont, request, entrance, visits, g, publisher) // Call the relation entrance method.
82
			if err != nil {
83
				return err
84
			}
85
		case schema.ComputedUserSetLinkedEntrance: // If the linked entrance is a computed user set entrance.
86
			err = engine.lt(ctx, request, &base.EntityAndRelation{ // Call the run method with a new entity and relation.
87
				Entity: &base.Entity{
88
					Type: entrance.TargetEntrance.GetType(),
89
					Id:   request.GetSubject().GetId(),
90
				},
91
				Relation: entrance.TargetEntrance.GetValue(),
92
			}, visits, g, publisher)
93
			if err != nil {
94
				return err
95
			}
96
		case schema.AttributeLinkedEntrance: // If the linked entrance is a computed user set entrance.
97
			err = engine.attributeEntrance(cont, request, entrance, visits, g, publisher) // Call the tuple to user set entrance method.
98
			if err != nil {
99
				return err
100
			}
101
		case schema.TupleToUserSetLinkedEntrance: // If the linked entrance is a tuple to user set entrance.
102
			err = engine.tupleToUserSetEntrance(cont, request, entrance, visits, g, publisher) // Call the tuple to user set entrance method.
103
			if err != nil {
104
				return err
105
			}
106
		default:
107
			return errors.New("unknown linked entrance type") // Return an error if the linked entrance is of an unknown type.
108
		}
109
	}
110
111
	return g.Wait() // Wait for all goroutines in the errgroup to complete and return any errors that occur.
112
}
113
114
// relationEntrance is a method of the EntityFilterEngine struct. It handles relation entrances.
115
func (engine *EntityFilter) attributeEntrance(
116
	ctx context.Context, // A context used for tracing and cancellation.
117
	request *base.PermissionEntityFilterRequest, // A permission request for linked entities.
118
	entrance *schema.LinkedEntrance, // A linked entrance.
119
	visits *VisitsMap, // A map that keeps track of visited entities to avoid infinite loops.
120
	g *errgroup.Group, // An errgroup used for executing goroutines.
121
	publisher *BulkEntityPublisher, // A custom publisher that publishes results in bulk.
122
) error { // Returns an error if one occurs during execution.
123
124
	// Retrieve the scope associated with the target entrance type.
125
	// Check if it exists to avoid accessing a nil map entry.
126
	scope, exists := request.GetScope()[entrance.TargetEntrance.GetType()]
127
128
	// Initialize data as an empty slice of strings.
129
	var data []string
130
131
	// If the scope exists, assign its Data field to the data slice.
132
	if exists {
133
		data = scope.GetData()
134
	}
135
136
	// Define a TupleFilter. This specifies which tuples we're interested in.
137
	// We want tuples that match the entity type and ID from the request, and have a specific relation.
138
	filter := &base.AttributeFilter{
139
		Entity: &base.EntityFilter{
140
			Type: entrance.TargetEntrance.GetType(),
141
			Ids:  data,
142
		},
143
		Attributes: []string{entrance.TargetEntrance.GetValue()},
144
	}
145
146
	var (
147
		cti, rit   *database.AttributeIterator
148
		err        error
149
		pagination database.CursorPagination
150
	)
151
152
	// Determine the pagination settings based on the entity type in the request.
153
	// If the entity type matches the target entrance, use cursor pagination with sorting by "entity_id".
154
	// Otherwise, use the default pagination settings.
155
	if request.GetEntrance().GetType() == entrance.TargetEntrance.GetType() {
156
		pagination = database.NewCursorPagination(database.Cursor(request.GetCursor()), database.Sort("entity_id"))
157
	} else {
158
		pagination = database.NewCursorPagination()
159
	}
160
161
	// Query the relationships using the specified pagination settings.
162
	// The context tuples are filtered based on the provided filter.
163
	cti, err = storageContext.NewContextualAttributes(request.GetContext().GetAttributes()...).QueryAttributes(filter, pagination)
164
	if err != nil {
165
		return err
166
	}
167
168
	// Query the relationships for the entity in the request.
169
	// The results are filtered based on the provided filter and pagination settings.
170
	rit, err = engine.dataReader.QueryAttributes(ctx, request.GetTenantId(), filter, request.GetMetadata().GetSnapToken(), pagination)
171
	if err != nil {
172
		return err
173
	}
174
175
	// Create a new UniqueTupleIterator from the two TupleIterators.
176
	// NewUniqueTupleIterator() ensures that the iterator only returns unique tuples.
177
	it := database.NewUniqueAttributeIterator(rit, cti)
178
179
	for it.HasNext() { // Loop over each relationship.
180
		// Get the next tuple's subject.
181
		current, ok := it.GetNext()
182
183
		if !ok {
184
			break
185
		}
186
187
		g.Go(func() error {
188
			return engine.la(ctx, request,
189
				&base.Entity{
190
					Type: entrance.TargetEntrance.GetType(),
191
					Id:   current.GetEntity().GetId(),
192
				}, visits, g, publisher)
193
		})
194
	}
195
196
	return nil
197
}
198
199
// relationEntrance is a method of the EntityFilterEngine struct. It handles relation entrances.
200
func (engine *EntityFilter) relationEntrance(
201
	ctx context.Context, // A context used for tracing and cancellation.
202
	request *base.PermissionEntityFilterRequest, // A permission request for linked entities.
203
	entrance *schema.LinkedEntrance, // A linked entrance.
204
	visits *VisitsMap, // A map that keeps track of visited entities to avoid infinite loops.
205
	g *errgroup.Group, // An errgroup used for executing goroutines.
206
	publisher *BulkEntityPublisher, // A custom publisher that publishes results in bulk.
207
) error { // Returns an error if one occurs during execution.
208
	// Retrieve the scope associated with the target entrance type.
209
	// Check if it exists to avoid accessing a nil map entry.
210
	scope, exists := request.GetScope()[entrance.TargetEntrance.GetType()]
211
212
	// Initialize data as an empty slice of strings.
213
	var data []string
214
215
	// If the scope exists, assign its Data field to the data slice.
216
	if exists {
217
		data = scope.GetData()
218
	}
219
220
	// Define a TupleFilter. This specifies which tuples we're interested in.
221
	// We want tuples that match the entity type and ID from the request, and have a specific relation.
222
	filter := &base.TupleFilter{
223
		Entity: &base.EntityFilter{
224
			Type: entrance.TargetEntrance.GetType(),
225
			Ids:  data,
226
		},
227
		Relation: entrance.TargetEntrance.GetValue(),
228
		Subject: &base.SubjectFilter{
229
			Type:     request.GetSubject().GetType(),
230
			Ids:      []string{request.GetSubject().GetId()},
231
			Relation: request.GetSubject().GetRelation(),
232
		},
233
	}
234
235
	var (
236
		cti, rit   *database.TupleIterator
237
		err        error
238
		pagination database.CursorPagination
239
	)
240
241
	// Determine the pagination settings based on the entity type in the request.
242
	// If the entity type matches the target entrance, use cursor pagination with sorting by "entity_id".
243
	// Otherwise, use the default pagination settings.
244
	if request.GetEntrance().GetType() == entrance.TargetEntrance.GetType() {
245
		pagination = database.NewCursorPagination(database.Cursor(request.GetCursor()), database.Sort("entity_id"))
246
	} else {
247
		pagination = database.NewCursorPagination()
248
	}
249
250
	// Query the relationships using the specified pagination settings.
251
	// The context tuples are filtered based on the provided filter.
252
	cti, err = storageContext.NewContextualTuples(request.GetContext().GetTuples()...).QueryRelationships(filter, pagination)
253
	if err != nil {
254
		return err
255
	}
256
257
	// Query the relationships for the entity in the request.
258
	// The results are filtered based on the provided filter and pagination settings.
259
	rit, err = engine.dataReader.QueryRelationships(ctx, request.GetTenantId(), filter, request.GetMetadata().GetSnapToken(), pagination)
260
	if err != nil {
261
		return err
262
	}
263
264
	// Create a new UniqueTupleIterator from the two TupleIterators.
265
	// NewUniqueTupleIterator() ensures that the iterator only returns unique tuples.
266
	it := database.NewUniqueTupleIterator(rit, cti)
267
268
	for it.HasNext() { // Loop over each relationship.
269
		// Get the next tuple's subject.
270
		current, ok := it.GetNext()
271
		if !ok {
272
			break
273
		}
274
		g.Go(func() error {
275
			return engine.lt(ctx, request, &base.EntityAndRelation{ // Call the run method with a new entity and relation.
276
				Entity: &base.Entity{
277
					Type: current.GetEntity().GetType(),
278
					Id:   current.GetEntity().GetId(),
279
				},
280
				Relation: current.GetRelation(),
281
			}, visits, g, publisher)
282
		})
283
	}
284
	return nil
285
}
286
287
// tupleToUserSetEntrance is a method of the EntityFilterEngine struct. It handles tuple to user set entrances.
288
func (engine *EntityFilter) tupleToUserSetEntrance(
289
	// A context used for tracing and cancellation.
290
	ctx context.Context,
291
	// A permission request for linked entities.
292
	request *base.PermissionEntityFilterRequest,
293
	// A linked entrance.
294
	entrance *schema.LinkedEntrance,
295
	// A map that keeps track of visited entities to avoid infinite loops.
296
	visits *VisitsMap,
297
	// An errgroup used for executing goroutines.
298
	g *errgroup.Group,
299
	// A custom publisher that publishes results in bulk.
300
	publisher *BulkEntityPublisher,
301
) error { // Returns an error if one occurs during execution.
302
	// Retrieve the scope associated with the target entrance type.
303
	// Check if it exists to avoid accessing a nil map entry.
304
	scope, exists := request.GetScope()[entrance.TargetEntrance.GetType()]
305
306
	// Initialize data as an empty slice of strings.
307
	var data []string
308
309
	// If the scope exists, assign its Data field to the data slice.
310
	if exists {
311
		data = scope.GetData()
312
	}
313
314
	// Define a TupleFilter. This specifies which tuples we're interested in.
315
	// We want tuples that match the entity type and ID from the request, and have a specific relation.
316
	filter := &base.TupleFilter{
317
		Entity: &base.EntityFilter{
318
			Type: entrance.TargetEntrance.GetType(),
319
			Ids:  data,
320
		},
321
		Relation: entrance.TupleSetRelation, // Query for relationships that match the tuple set relation.
322
		Subject: &base.SubjectFilter{
323
			Type:     request.GetSubject().GetType(),
324
			Ids:      []string{request.GetSubject().GetId()},
325
			Relation: "",
326
		},
327
	}
328
329
	var (
330
		cti, rit   *database.TupleIterator
331
		err        error
332
		pagination database.CursorPagination
333
	)
334
335
	// Determine the pagination settings based on the entity type in the request.
336
	// If the entity type matches the target entrance, use cursor pagination with sorting by "entity_id".
337
	// Otherwise, use the default pagination settings.
338
	if request.GetEntrance().GetType() == entrance.TargetEntrance.GetType() {
339
		pagination = database.NewCursorPagination(database.Cursor(request.GetCursor()), database.Sort("entity_id"))
340
	} else {
341
		pagination = database.NewCursorPagination()
342
	}
343
344
	// Query the relationships using the specified pagination settings.
345
	// The context tuples are filtered based on the provided filter.
346
	cti, err = storageContext.NewContextualTuples(request.GetContext().GetTuples()...).QueryRelationships(filter, pagination)
347
	if err != nil {
348
		return err
349
	}
350
351
	// Query the relationships for the entity in the request.
352
	// The results are filtered based on the provided filter and pagination settings.
353
	rit, err = engine.dataReader.QueryRelationships(ctx, request.GetTenantId(), filter, request.GetMetadata().GetSnapToken(), pagination)
354
	if err != nil {
355
		return err
356
	}
357
358
	// Create a new UniqueTupleIterator from the two TupleIterators.
359
	// NewUniqueTupleIterator() ensures that the iterator only returns unique tuples.
360
	it := database.NewUniqueTupleIterator(rit, cti)
361
362
	for it.HasNext() { // Loop over each relationship.
363
		// Get the next tuple's subject.
364
		current, ok := it.GetNext()
365
		if !ok {
366
			break
367
		}
368
		g.Go(func() error {
369
			return engine.lt(ctx, request, &base.EntityAndRelation{ // Call the run method with a new entity and relation.
370
				Entity: &base.Entity{
371
					Type: entrance.TargetEntrance.GetType(),
372
					Id:   current.GetEntity().GetId(),
373
				},
374
				Relation: entrance.TargetEntrance.GetValue(),
375
			}, visits, g, publisher)
376
		})
377
	}
378
	return nil
379
}
380
381
// run is a method of the EntityFilterEngine struct. It executes the linked entity engine for a given request.
382
func (engine *EntityFilter) lt(
383
	ctx context.Context, // A context used for tracing and cancellation.
384
	request *base.PermissionEntityFilterRequest, // A permission request for linked entities.
385
	found *base.EntityAndRelation, // An entity and relation that was previously found.
386
	visits *VisitsMap, // A map that keeps track of visited entities to avoid infinite loops.
387
	g *errgroup.Group, // An errgroup used for executing goroutines.
388
	publisher *BulkEntityPublisher, // A custom publisher that publishes results in bulk.
389
) error { // Returns an error if one occurs during execution.
390
	if !visits.AddER(found.GetEntity(), found.GetRelation()) { // If the entity and relation has already been visited.
391
		return nil
392
	}
393
394
	var err error
395
396
	// Retrieve linked entrances
397
	cn := schema.NewLinkedGraph(engine.schema)
398
	var entrances []*schema.LinkedEntrance
399
	entrances, err = cn.LinkedEntrances(
400
		request.GetEntrance(),
401
		&base.Entrance{
402
			Type:  request.GetSubject().GetType(),
403
			Value: request.GetSubject().GetRelation(),
404
		},
405
	) // Retrieve the linked entrances for the request.
406
	if err != nil {
407
		return err
408
	}
409
410
	if entrances == nil { // If there are no linked entrances for the request.
411
		if found.GetEntity().GetType() == request.GetEntrance().GetType() && found.GetRelation() == request.GetEntrance().GetValue() { // Check if the found entity matches the requested entity reference.
412
			if !visits.AddPublished(found.GetEntity()) { // If the entity and relation has already been visited.
413
				return nil
414
			}
415
			publisher.Publish(found.GetEntity(), &base.PermissionCheckRequestMetadata{ // Publish the found entity with the permission check metadata.
416
				SnapToken:     request.GetMetadata().GetSnapToken(),
417
				SchemaVersion: request.GetMetadata().GetSchemaVersion(),
418
				Depth:         request.GetMetadata().GetDepth(),
419
			}, request.GetContext(), base.CheckResult_CHECK_RESULT_UNSPECIFIED)
420
			return nil
421
		}
422
		return nil // Otherwise, return without publishing any results.
423
	}
424
425
	g.Go(func() error {
426
		return engine.EntityFilter(ctx, &base.PermissionEntityFilterRequest{ // Call the Run method recursively with a new permission request.
427
			TenantId: request.GetTenantId(),
428
			Entrance: request.GetEntrance(),
429
			Subject: &base.Subject{
430
				Type:     found.GetEntity().GetType(),
431
				Id:       found.GetEntity().GetId(),
432
				Relation: found.GetRelation(),
433
			},
434
			Scope:    request.GetScope(),
435
			Metadata: request.GetMetadata(),
436
			Context:  request.GetContext(),
437
			Cursor:   request.GetCursor(),
438
		}, visits, publisher)
439
	})
440
	return nil
441
}
442
443
func (engine *EntityFilter) la(
444
	ctx context.Context, // A context used for tracing and cancellation.
445
	request *base.PermissionEntityFilterRequest, // A permission request for linked entities.
446
	found *base.Entity, // An entity and relation that was previously found.
447
	visits *VisitsMap, // A map that keeps track of visited entities to avoid infinite loops.
448
	g *errgroup.Group, // An errgroup used for executing goroutines.
449
	publisher *BulkEntityPublisher, // A custom publisher that publishes results in bulk.
450
) error { // Returns an error if one occurs during execution.
451
	if !visits.AddPublished(found) { // If the entity and relation has already been visited.
452
		return nil
453
	}
454
455
	publisher.Publish(found, &base.PermissionCheckRequestMetadata{ // Publish the found entity with the permission check metadata.
456
		SnapToken:     request.GetMetadata().GetSnapToken(),
457
		SchemaVersion: request.GetMetadata().GetSchemaVersion(),
458
		Depth:         request.GetMetadata().GetDepth(),
459
	}, request.GetContext(), base.CheckResult_CHECK_RESULT_UNSPECIFIED)
460
	return nil
461
}
462