Passed
Pull Request — master (#1694)
by Tolga
03:52
created

engines.*EntityFilter.la   A

Complexity

Conditions 2

Size

Total Lines 18
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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