Passed
Pull Request — master (#1691)
by
unknown
03:16
created

internal/engines/entityFilter.go   C

Size/Duplication

Total Lines 482
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
cc 55
eloc 284
dl 0
loc 482
rs 6
c 0
b 0
f 0

7 Methods

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