engines.*EntityFilter.EntityFilter   F
last analyzed

Complexity

Conditions 15

Size

Total Lines 83
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Importance

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