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