Passed
Pull Request — master (#2556)
by Tolga
03:34
created

engines.*EntityFilter.handleNestedAttribute   F

Complexity

Conditions 16

Size

Total Lines 128
Code Lines 81

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 16
eloc 81
nop 5
dl 0
loc 128
rs 2.3018
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like engines.*EntityFilter.handleNestedAttribute often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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
	graph *schema.LinkedSchemaGraph
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
		graph:      schema.NewLinkedGraph(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
	var entrances []*schema.LinkedEntrance
60
	entrances, err = engine.graph.LinkedEntrances(
61
		request.GetEntrance(),
62
		&base.Entrance{
63
			Type:  request.GetSubject().GetType(),
64
			Value: request.GetSubject().GetRelation(),
65
		},
66
	) // Retrieve the linked entrances between the entity reference and subject.
67
68
	if entrances == nil {
69
		return nil
70
	}
71
72
	// Create a new context for executing goroutines and a cancel function.
73
	cctx, cancel := context.WithCancel(ctx)
74
	defer cancel()
75
76
	// Create a new errgroup and a new context that inherits the original context.
77
	g, cont := errgroup.WithContext(cctx)
78
79
	// Loop over each linked entrance.
80
	for _, entrance := range entrances {
81
		// Switch on the kind of linked entrance.
82
		switch entrance.LinkedEntranceKind() {
83
		case schema.RelationLinkedEntrance: // If the linked entrance is a relation entrance.
84
			err = engine.relationEntrance(cont, request, entrance, visits, g, publisher) // Call the relation entrance method.
85
			if err != nil {
86
				return err
87
			}
88
		case schema.ComputedUserSetLinkedEntrance: // If the linked entrance is a computed user set entrance.
89
			err = engine.lt(cont, request, &base.EntityAndRelation{ // Call the run method with a new entity and relation.
90
				Entity: &base.Entity{
91
					Type: entrance.TargetEntrance.GetType(),
92
					Id:   request.GetSubject().GetId(),
93
				},
94
				Relation: entrance.TargetEntrance.GetValue(),
95
			}, visits, g, publisher)
96
			if err != nil {
97
				return err
98
			}
99
		case schema.AttributeLinkedEntrance: // If the linked entrance is a computed user set entrance.
100
			err = engine.attributeEntrance(cont, request, entrance, visits, publisher) // Call the tuple to user set entrance method.
101
			if err != nil {
102
				return err
103
			}
104
		case schema.TupleToUserSetLinkedEntrance: // If the linked entrance is a tuple to user set entrance.
105
			err = engine.tupleToUserSetEntrance(cont, request, entrance, visits, g, publisher) // Call the tuple to user set entrance method.
106
			if err != nil {
107
				return err
108
			}
109
		default:
110
			return errors.New("unknown linked entrance type") // Return an error if the linked entrance is of an unknown type.
111
		}
112
	}
113
114
	return g.Wait() // Wait for all goroutines in the errgroup to complete and return any errors that occur.
115
}
116
117
// relationEntrance is a method of the EntityFilterEngine struct. It handles relation entrances.
118
func (engine *EntityFilter) attributeEntrance(
119
	ctx context.Context, // A context used for tracing and cancellation.
120
	request *base.PermissionEntityFilterRequest, // A permission request for linked entities.
121
	entrance *schema.LinkedEntrance, // A linked entrance.
122
	visits *VisitsMap, // A map that keeps track of visited entities to avoid infinite loops.
123
	publisher *BulkEntityPublisher, // A custom publisher that publishes results in bulk.
124
) error { // Returns an error if one occurs during execution.
125
	// Check if this is a nested attribute case (different entity types)
126
	if request.GetEntrance().GetType() != entrance.TargetEntrance.GetType() {
127
		// This is a nested attribute case - use native recursive approach
128
		return engine.handleNestedAttribute(ctx, request, entrance, visits, publisher)
129
	}
130
131
	// Regular case: same entity type, direct attribute access
132
	if !visits.AddEA(entrance.TargetEntrance.GetType(), entrance.TargetEntrance.GetValue()) {
133
		return nil
134
	}
135
136
	// Retrieve the scope associated with the target entrance type
137
	scope, exists := request.GetScope()[entrance.TargetEntrance.GetType()]
138
	var data []string
139
	if exists {
140
		data = scope.GetData()
141
	}
142
143
	// Query attributes directly
144
	filter := &base.AttributeFilter{
145
		Entity: &base.EntityFilter{
146
			Type: entrance.TargetEntrance.GetType(),
147
			Ids:  data,
148
		},
149
		Attributes: []string{entrance.TargetEntrance.GetValue()},
150
	}
151
152
	pagination := database.NewCursorPagination(database.Cursor(request.GetCursor()), database.Sort("entity_id"))
153
154
	cti, err := storageContext.NewContextualAttributes(request.GetContext().GetAttributes()...).QueryAttributes(filter, pagination)
155
	if err != nil {
156
		return err
157
	}
158
159
	rit, err := engine.dataReader.QueryAttributes(ctx, request.GetTenantId(), filter, request.GetMetadata().GetSnapToken(), pagination)
160
	if err != nil {
161
		return err
162
	}
163
164
	it := database.NewUniqueAttributeIterator(rit, cti)
165
166
	// Publish entities directly for regular case
167
	for it.HasNext() {
168
		current, ok := it.GetNext()
169
		if !ok {
170
			break
171
		}
172
173
		entity := &base.Entity{
174
			Type: entrance.TargetEntrance.GetType(),
175
			Id:   current.GetEntity().GetId(),
176
		}
177
178
		if !visits.AddPublished(entity) {
179
			continue
180
		}
181
182
		publisher.Publish(entity, &base.PermissionCheckRequestMetadata{
183
			SnapToken:     request.GetMetadata().GetSnapToken(),
184
			SchemaVersion: request.GetMetadata().GetSchemaVersion(),
185
			Depth:         request.GetMetadata().GetDepth(),
186
		}, request.GetContext(), base.CheckResult_CHECK_RESULT_UNSPECIFIED)
187
	}
188
189
	return nil
190
}
191
192
// handleNestedAttribute handles nested attribute cases using native recursive approach
193
func (engine *EntityFilter) handleNestedAttribute(
194
	ctx context.Context,
195
	request *base.PermissionEntityFilterRequest,
196
	entrance *schema.LinkedEntrance,
197
	visits *VisitsMap,
198
	publisher *BulkEntityPublisher,
199
) error {
200
	if !visits.AddEA(entrance.TargetEntrance.GetType(), entrance.TargetEntrance.GetValue()) {
201
		return nil
202
	}
203
204
	// 1. Query attributes of the target type with scope optimization
205
	scope, exists := request.GetScope()[entrance.TargetEntrance.GetType()]
206
	var data []string
207
	if exists {
208
		data = scope.GetData()
209
	}
210
	// For nested case, we still need to respect scope if provided
211
212
	filter := &base.AttributeFilter{
213
		Entity: &base.EntityFilter{
214
			Type: entrance.TargetEntrance.GetType(),
215
			Ids:  data, // Use scope data for optimization
216
		},
217
		Attributes: []string{entrance.TargetEntrance.GetValue()},
218
	}
219
220
	pagination := database.NewCursorPagination()
221
	cti, err := storageContext.NewContextualAttributes(request.GetContext().GetAttributes()...).QueryAttributes(filter, pagination)
222
	if err != nil {
223
		return err
224
	}
225
226
	rit, err := engine.dataReader.QueryAttributes(ctx, request.GetTenantId(), filter, request.GetMetadata().GetSnapToken(), pagination)
227
	if err != nil {
228
		return err
229
	}
230
231
	it := database.NewUniqueAttributeIterator(rit, cti)
232
233
	// 2. Collect all attribute entity IDs first (batch approach)
234
	var attributeEntityIds []string
235
	sourceType := request.GetEntrance().GetType()
236
	targetType := entrance.TargetEntrance.GetType()
237
238
	// Use pre-computed relation path if available, otherwise compute dynamically
239
	var relationName string
240
	if len(entrance.RelationPath) > 0 {
241
		relationName = entrance.RelationPath[0].GetRelation()
242
	} else {
243
		// Fallback: compute relation path dynamically
244
		relationPath, err := engine.graph.BuildRelationPath(sourceType, targetType)
245
		if err != nil {
246
			return err // Cannot determine relation path
247
		}
248
		entrance.RelationPath = relationPath
249
		relationName = relationPath[0].GetRelation()
250
	}
251
252
	// Collect all entity IDs that have the attribute
253
	for it.HasNext() {
254
		current, ok := it.GetNext()
255
		if !ok {
256
			break
257
		}
258
		attributeEntityIds = append(attributeEntityIds, current.GetEntity().GetId())
259
	}
260
261
	if len(attributeEntityIds) == 0 {
262
		return nil
263
	}
264
265
	// 3. Single batch query to find all related entities with scope optimization
266
	sourceScope, sourceExists := request.GetScope()[sourceType]
267
	var sourceData []string
268
	if sourceExists {
269
		sourceData = sourceScope.GetData()
270
	}
271
272
	relationFilter := &base.TupleFilter{
273
		Entity: &base.EntityFilter{
274
			Type: sourceType,
275
			Ids:  sourceData, // Use source scope for optimization
276
		},
277
		Relation: relationName,
278
		Subject: &base.SubjectFilter{
279
			Type:     targetType,
280
			Ids:      attributeEntityIds, // Batch all attribute entity IDs
281
			Relation: "",
282
		},
283
	}
284
285
	pagination = database.NewCursorPagination()
286
	ctiRelations, err := storageContext.NewContextualTuples(request.GetContext().GetTuples()...).QueryRelationships(relationFilter, pagination)
287
	if err != nil {
288
		return err
289
	}
290
291
	ritRelations, err := engine.dataReader.QueryRelationships(ctx, request.GetTenantId(), relationFilter, request.GetMetadata().GetSnapToken(), pagination)
292
	if err != nil {
293
		return err
294
	}
295
296
	relationIt := database.NewUniqueTupleIterator(ritRelations, ctiRelations)
297
	for relationIt.HasNext() {
298
		tuple, ok := relationIt.GetNext()
299
		if !ok {
300
			break
301
		}
302
303
		relatedEntity := &base.Entity{
304
			Type: sourceType,
305
			Id:   tuple.GetEntity().GetId(),
306
		}
307
308
		if !visits.AddPublished(relatedEntity) {
309
			continue
310
		}
311
312
		// BulkEntityPublisher handles bulk processing - just publish entity ID
313
		publisher.Publish(relatedEntity, &base.PermissionCheckRequestMetadata{
314
			SnapToken:     request.GetMetadata().GetSnapToken(),
315
			SchemaVersion: request.GetMetadata().GetSchemaVersion(),
316
			Depth:         request.GetMetadata().GetDepth(),
317
		}, request.GetContext(), base.CheckResult_CHECK_RESULT_UNSPECIFIED)
318
	}
319
320
	return nil
321
}
322
323
// relationEntrance is a method of the EntityFilterEngine struct. It handles relation entrances.
324
func (engine *EntityFilter) relationEntrance(
325
	ctx context.Context, // A context used for tracing and cancellation.
326
	request *base.PermissionEntityFilterRequest, // A permission request for linked entities.
327
	entrance *schema.LinkedEntrance, // A linked entrance.
328
	visits *VisitsMap, // A map that keeps track of visited entities to avoid infinite loops.
329
	g *errgroup.Group, // An errgroup used for executing goroutines.
330
	publisher *BulkEntityPublisher, // A custom publisher that publishes results in bulk.
331
) error { // Returns an error if one occurs during execution.
332
	// Retrieve the scope associated with the target entrance type.
333
	// Check if it exists to avoid accessing a nil map entry.
334
	scope, exists := request.GetScope()[entrance.TargetEntrance.GetType()]
335
336
	// Initialize data as an empty slice of strings.
337
	var data []string
338
339
	// If the scope exists, assign its Data field to the data slice.
340
	if exists {
341
		data = scope.GetData()
342
	}
343
344
	// Define a TupleFilter. This specifies which tuples we're interested in.
345
	// We want tuples that match the entity type and ID from the request, and have a specific relation.
346
	filter := &base.TupleFilter{
347
		Entity: &base.EntityFilter{
348
			Type: entrance.TargetEntrance.GetType(),
349
			Ids:  data,
350
		},
351
		Relation: entrance.TargetEntrance.GetValue(),
352
		Subject: &base.SubjectFilter{
353
			Type:     request.GetSubject().GetType(),
354
			Ids:      []string{request.GetSubject().GetId()},
355
			Relation: request.GetSubject().GetRelation(),
356
		},
357
	}
358
359
	var (
360
		cti, rit   *database.TupleIterator
361
		err        error
362
		pagination database.CursorPagination
363
	)
364
365
	// Determine the pagination settings based on the entity type in the request.
366
	// If the entity type matches the target entrance, use cursor pagination with sorting by "entity_id".
367
	// Otherwise, use the default pagination settings.
368
	if request.GetEntrance().GetType() == entrance.TargetEntrance.GetType() {
369
		pagination = database.NewCursorPagination(database.Cursor(request.GetCursor()), database.Sort("entity_id"))
370
	} else {
371
		pagination = database.NewCursorPagination()
372
	}
373
374
	// Query the relationships using the specified pagination settings.
375
	// The context tuples are filtered based on the provided filter.
376
	cti, err = storageContext.NewContextualTuples(request.GetContext().GetTuples()...).QueryRelationships(filter, pagination)
377
	if err != nil {
378
		return err
379
	}
380
381
	// Query the relationships for the entity in the request.
382
	// The results are filtered based on the provided filter and pagination settings.
383
	rit, err = engine.dataReader.QueryRelationships(ctx, request.GetTenantId(), filter, request.GetMetadata().GetSnapToken(), pagination)
384
	if err != nil {
385
		return err
386
	}
387
388
	// Create a new UniqueTupleIterator from the two TupleIterators.
389
	// NewUniqueTupleIterator() ensures that the iterator only returns unique tuples.
390
	it := database.NewUniqueTupleIterator(rit, cti)
391
392
	for it.HasNext() { // Loop over each relationship.
393
		// Get the next tuple's subject.
394
		current, ok := it.GetNext()
395
		if !ok {
396
			break
397
		}
398
		g.Go(func() error {
399
			return engine.lt(ctx, request, &base.EntityAndRelation{ // Call the run method with a new entity and relation.
400
				Entity: &base.Entity{
401
					Type: current.GetEntity().GetType(),
402
					Id:   current.GetEntity().GetId(),
403
				},
404
				Relation: current.GetRelation(),
405
			}, visits, g, publisher)
406
		})
407
	}
408
	return nil
409
}
410
411
// tupleToUserSetEntrance is a method of the EntityFilterEngine struct. It handles tuple to user set entrances.
412
func (engine *EntityFilter) tupleToUserSetEntrance(
413
	// A context used for tracing and cancellation.
414
	ctx context.Context,
415
	// A permission request for linked entities.
416
	request *base.PermissionEntityFilterRequest,
417
	// A linked entrance.
418
	entrance *schema.LinkedEntrance,
419
	// A map that keeps track of visited entities to avoid infinite loops.
420
	visits *VisitsMap,
421
	// An errgroup used for executing goroutines.
422
	g *errgroup.Group,
423
	// A custom publisher that publishes results in bulk.
424
	publisher *BulkEntityPublisher,
425
) error { // Returns an error if one occurs during execution.
426
	// Retrieve the scope associated with the target entrance type.
427
	// Check if it exists to avoid accessing a nil map entry.
428
	scope, exists := request.GetScope()[entrance.TargetEntrance.GetType()]
429
430
	// Initialize data as an empty slice of strings.
431
	var data []string
432
433
	// If the scope exists, assign its Data field to the data slice.
434
	if exists {
435
		data = scope.GetData()
436
	}
437
438
	// Define a TupleFilter. This specifies which tuples we're interested in.
439
	// We want tuples that match the entity type and ID from the request, and have a specific relation.
440
	filter := &base.TupleFilter{
441
		Entity: &base.EntityFilter{
442
			Type: entrance.TargetEntrance.GetType(),
443
			Ids:  data,
444
		},
445
		Relation: entrance.TupleSetRelation, // Query for relationships that match the tuple set relation.
446
		Subject: &base.SubjectFilter{
447
			Type:     request.GetSubject().GetType(),
448
			Ids:      []string{request.GetSubject().GetId()},
449
			Relation: "",
450
		},
451
	}
452
453
	var (
454
		cti, rit   *database.TupleIterator
455
		err        error
456
		pagination database.CursorPagination
457
	)
458
459
	// Determine the pagination settings based on the entity type in the request.
460
	// If the entity type matches the target entrance, use cursor pagination with sorting by "entity_id".
461
	// Otherwise, use the default pagination settings.
462
	if request.GetEntrance().GetType() == entrance.TargetEntrance.GetType() {
463
		pagination = database.NewCursorPagination(database.Cursor(request.GetCursor()), database.Sort("entity_id"))
464
	} else {
465
		pagination = database.NewCursorPagination()
466
	}
467
468
	// Query the relationships using the specified pagination settings.
469
	// The context tuples are filtered based on the provided filter.
470
	cti, err = storageContext.NewContextualTuples(request.GetContext().GetTuples()...).QueryRelationships(filter, pagination)
471
	if err != nil {
472
		return err
473
	}
474
475
	// Query the relationships for the entity in the request.
476
	// The results are filtered based on the provided filter and pagination settings.
477
	rit, err = engine.dataReader.QueryRelationships(ctx, request.GetTenantId(), filter, request.GetMetadata().GetSnapToken(), pagination)
478
	if err != nil {
479
		return err
480
	}
481
482
	// Create a new UniqueTupleIterator from the two TupleIterators.
483
	// NewUniqueTupleIterator() ensures that the iterator only returns unique tuples.
484
	it := database.NewUniqueTupleIterator(rit, cti)
485
486
	for it.HasNext() { // Loop over each relationship.
487
		// Get the next tuple's subject.
488
		current, ok := it.GetNext()
489
		if !ok {
490
			break
491
		}
492
		g.Go(func() error {
493
			return engine.lt(ctx, request, &base.EntityAndRelation{ // Call the run method with a new entity and relation.
494
				Entity: &base.Entity{
495
					Type: entrance.TargetEntrance.GetType(),
496
					Id:   current.GetEntity().GetId(),
497
				},
498
				Relation: entrance.TargetEntrance.GetValue(),
499
			}, visits, g, publisher)
500
		})
501
	}
502
	return nil
503
}
504
505
// run is a method of the EntityFilterEngine struct. It executes the linked entity engine for a given request.
506
func (engine *EntityFilter) lt(
507
	ctx context.Context, // A context used for tracing and cancellation.
508
	request *base.PermissionEntityFilterRequest, // A permission request for linked entities.
509
	found *base.EntityAndRelation, // An entity and relation that was previously found.
510
	visits *VisitsMap, // A map that keeps track of visited entities to avoid infinite loops.
511
	g *errgroup.Group, // An errgroup used for executing goroutines.
512
	publisher *BulkEntityPublisher, // A custom publisher that publishes results in bulk.
513
) error { // Returns an error if one occurs during execution.
514
	if !visits.AddER(found.GetEntity(), found.GetRelation()) { // If the entity and relation has already been visited.
515
		return nil
516
	}
517
518
	var err error
519
520
	// Retrieve linked entrances
521
	var entrances []*schema.LinkedEntrance
522
	entrances, err = engine.graph.LinkedEntrances(
523
		request.GetEntrance(),
524
		&base.Entrance{
525
			Type:  request.GetSubject().GetType(),
526
			Value: request.GetSubject().GetRelation(),
527
		},
528
	) // Retrieve the linked entrances for the request.
529
	if err != nil {
530
		return err
531
	}
532
533
	if entrances == nil { // If there are no linked entrances for the request.
534
		if found.GetEntity().GetType() == request.GetEntrance().GetType() && found.GetRelation() == request.GetEntrance().GetValue() { // Check if the found entity matches the requested entity reference.
535
			if !visits.AddPublished(found.GetEntity()) { // If the entity and relation has already been visited.
536
				return nil
537
			}
538
			publisher.Publish(found.GetEntity(), &base.PermissionCheckRequestMetadata{ // Publish the found entity with the permission check metadata.
539
				SnapToken:     request.GetMetadata().GetSnapToken(),
540
				SchemaVersion: request.GetMetadata().GetSchemaVersion(),
541
				Depth:         request.GetMetadata().GetDepth(),
542
			}, request.GetContext(), base.CheckResult_CHECK_RESULT_UNSPECIFIED)
543
			return nil
544
		}
545
		return nil // Otherwise, return without publishing any results.
546
	}
547
548
	g.Go(func() error {
549
		return engine.EntityFilter(ctx, &base.PermissionEntityFilterRequest{ // Call the Run method recursively with a new permission request.
550
			TenantId: request.GetTenantId(),
551
			Entrance: request.GetEntrance(),
552
			Subject: &base.Subject{
553
				Type:     found.GetEntity().GetType(),
554
				Id:       found.GetEntity().GetId(),
555
				Relation: found.GetRelation(),
556
			},
557
			Scope:    request.GetScope(),
558
			Metadata: request.GetMetadata(),
559
			Context:  request.GetContext(),
560
			Cursor:   request.GetCursor(),
561
		}, visits, publisher)
562
	})
563
	return nil
564
}
565