• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Permify / permify / 18709238527

22 Oct 2025 07:50AM UTC coverage: 85.968%. Remained the same
18709238527

Pull #2560

github

tolgaozen
docs: update API version to v1.4.6 across documentation and code files
Pull Request #2560: docs: update API version to v1.4.6 across documentation and code files

9423 of 10961 relevant lines covered (85.97%)

298.23 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.39
/internal/engines/entity_filter.go
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 {
136✔
26
        return &EntityFilter{
136✔
27
                dataReader: dataReader,
136✔
28
                graph:      schema.NewLinkedGraph(sch),
136✔
29
        }
136✔
30
}
136✔
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.
2,368✔
39
        // Check if direct result
2,368✔
40
        if request.GetEntrance().GetType() == request.GetSubject().GetType() && request.GetEntrance().GetValue() == request.GetSubject().GetRelation() {
3,452✔
41
                found := &base.Entity{
1,084✔
42
                        Type: request.GetSubject().GetType(),
1,084✔
43
                        Id:   request.GetSubject().GetId(),
1,084✔
44
                }
1,084✔
45

1,084✔
46
                if !visits.AddPublished(found) { // If the entity and relation has already been visited.
1,114✔
47
                        return nil
30✔
48
                }
30✔
49

50
                // If the entity reference is the same as the subject, publish the result directly and return.
51
                publisher.Publish(found, &base.PermissionCheckRequestMetadata{
1,054✔
52
                        SnapToken:     request.GetMetadata().GetSnapToken(),
1,054✔
53
                        SchemaVersion: request.GetMetadata().GetSchemaVersion(),
1,054✔
54
                        Depth:         request.GetMetadata().GetDepth(),
1,054✔
55
                }, request.GetContext(), base.CheckResult_CHECK_RESULT_UNSPECIFIED)
1,054✔
56
        }
57

58
        // Retrieve linked entrances
59
        var entrances []*schema.LinkedEntrance
2,338✔
60
        entrances, err = engine.graph.LinkedEntrances(
2,338✔
61
                request.GetEntrance(),
2,338✔
62
                &base.Entrance{
2,338✔
63
                        Type:  request.GetSubject().GetType(),
2,338✔
64
                        Value: request.GetSubject().GetRelation(),
2,338✔
65
                },
2,338✔
66
        ) // Retrieve the linked entrances between the entity reference and subject.
2,338✔
67

2,338✔
68
        if entrances == nil {
3,384✔
69
                return nil
1,046✔
70
        }
1,046✔
71

72
        // Create a new context for executing goroutines and a cancel function.
73
        cctx, cancel := context.WithCancel(ctx)
1,292✔
74
        defer cancel()
1,292✔
75

1,292✔
76
        // Create a new errgroup and a new context that inherits the original context.
1,292✔
77
        g, cont := errgroup.WithContext(cctx)
1,292✔
78

1,292✔
79
        // Loop over each linked entrance.
1,292✔
80
        for _, entrance := range entrances {
2,956✔
81
                // Switch on the kind of linked entrance.
1,664✔
82
                switch entrance.LinkedEntranceKind() {
1,664✔
83
                case schema.RelationLinkedEntrance: // If the linked entrance is a relation entrance.
368✔
84
                        err = engine.relationEntrance(cont, request, entrance, visits, g, publisher) // Call the relation entrance method.
368✔
85
                        if err != nil {
368✔
86
                                return err
×
87
                        }
×
88
                case schema.ComputedUserSetLinkedEntrance: // If the linked entrance is a computed user set entrance.
1,007✔
89
                        err = engine.lt(cont, request, &base.EntityAndRelation{ // Call the run method with a new entity and relation.
1,007✔
90
                                Entity: &base.Entity{
1,007✔
91
                                        Type: entrance.TargetEntrance.GetType(),
1,007✔
92
                                        Id:   request.GetSubject().GetId(),
1,007✔
93
                                },
1,007✔
94
                                Relation: entrance.TargetEntrance.GetValue(),
1,007✔
95
                        }, visits, g, publisher)
1,007✔
96
                        if err != nil {
1,007✔
97
                                return err
×
98
                        }
×
99
                case schema.AttributeLinkedEntrance: // If the linked entrance is a computed user set entrance.
65✔
100
                        err = engine.attributeEntrance(cont, request, entrance, visits, publisher) // Call the tuple to user set entrance method.
65✔
101
                        if err != nil {
65✔
102
                                return err
×
103
                        }
×
104
                case schema.TupleToUserSetLinkedEntrance: // If the linked entrance is a tuple to user set entrance.
133✔
105
                        err = engine.tupleToUserSetEntrance(cont, request, entrance, visits, g, publisher) // Call the tuple to user set entrance method.
133✔
106
                        if err != nil {
133✔
107
                                return err
×
108
                        }
×
109
                case schema.PathChainLinkedEntrance: // If the linked entrance is a path chain entrance.
91✔
110
                        err = engine.pathChainEntrance(cont, request, entrance, visits, publisher) // Call the path chain entrance method.
91✔
111
                        if err != nil {
91✔
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.
1,292✔
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
        publisher *BulkEntityPublisher, // A custom publisher that publishes results in bulk.
129
) error { // Returns an error if one occurs during execution.
65✔
130
        // attributeEntrance only handles direct attribute access
65✔
131
        if !visits.AddEA(entrance.TargetEntrance.GetType(), entrance.TargetEntrance.GetValue()) {
106✔
132
                return nil
41✔
133
        }
41✔
134

135
        // Retrieve the scope associated with the target entrance type
136
        scope, exists := request.GetScope()[entrance.TargetEntrance.GetType()]
24✔
137
        var data []string
24✔
138
        if exists {
24✔
139
                data = scope.GetData()
×
140
        }
×
141

142
        // Query attributes directly
143
        filter := &base.AttributeFilter{
24✔
144
                Entity: &base.EntityFilter{
24✔
145
                        Type: entrance.TargetEntrance.GetType(),
24✔
146
                        Ids:  data,
24✔
147
                },
24✔
148
                Attributes: []string{entrance.TargetEntrance.GetValue()},
24✔
149
        }
24✔
150

24✔
151
        pagination := database.NewCursorPagination(database.Cursor(request.GetCursor()), database.Sort("entity_id"))
24✔
152

24✔
153
        cti, err := storageContext.NewContextualAttributes(request.GetContext().GetAttributes()...).QueryAttributes(filter, pagination)
24✔
154
        if err != nil {
24✔
155
                return err
×
156
        }
×
157

158
        rit, err := engine.dataReader.QueryAttributes(ctx, request.GetTenantId(), filter, request.GetMetadata().GetSnapToken(), pagination)
24✔
159
        if err != nil {
24✔
160
                return err
×
161
        }
×
162

163
        it := database.NewUniqueAttributeIterator(rit, cti)
24✔
164

24✔
165
        // Publish entities directly for regular case
24✔
166
        for it.HasNext() {
131✔
167
                current, ok := it.GetNext()
107✔
168
                if !ok {
107✔
169
                        break
×
170
                }
171

172
                entity := &base.Entity{
107✔
173
                        Type: entrance.TargetEntrance.GetType(),
107✔
174
                        Id:   current.GetEntity().GetId(),
107✔
175
                }
107✔
176

107✔
177
                if !visits.AddPublished(entity) {
173✔
178
                        continue
66✔
179
                }
180

181
                publisher.Publish(entity, &base.PermissionCheckRequestMetadata{
41✔
182
                        SnapToken:     request.GetMetadata().GetSnapToken(),
41✔
183
                        SchemaVersion: request.GetMetadata().GetSchemaVersion(),
41✔
184
                        Depth:         request.GetMetadata().GetDepth(),
41✔
185
                }, request.GetContext(), base.CheckResult_CHECK_RESULT_UNSPECIFIED)
41✔
186
        }
187

188
        return nil
24✔
189
}
190

191
// relationEntrance is a method of the EntityFilterEngine struct. It handles relation entrances.
192
func (engine *EntityFilter) relationEntrance(
193
        ctx context.Context, // A context used for tracing and cancellation.
194
        request *base.PermissionEntityFilterRequest, // A permission request for linked entities.
195
        entrance *schema.LinkedEntrance, // A linked entrance.
196
        visits *VisitsMap, // A map that keeps track of visited entities to avoid infinite loops.
197
        g *errgroup.Group, // An errgroup used for executing goroutines.
198
        publisher *BulkEntityPublisher, // A custom publisher that publishes results in bulk.
199
) error { // Returns an error if one occurs during execution.
368✔
200
        // Retrieve the scope associated with the target entrance type.
368✔
201
        // Check if it exists to avoid accessing a nil map entry.
368✔
202
        scope, exists := request.GetScope()[entrance.TargetEntrance.GetType()]
368✔
203

368✔
204
        // Initialize data as an empty slice of strings.
368✔
205
        var data []string
368✔
206

368✔
207
        // If the scope exists, assign its Data field to the data slice.
368✔
208
        if exists {
375✔
209
                data = scope.GetData()
7✔
210
        }
7✔
211

212
        // Define a TupleFilter. This specifies which tuples we're interested in.
213
        // We want tuples that match the entity type and ID from the request, and have a specific relation.
214
        filter := &base.TupleFilter{
368✔
215
                Entity: &base.EntityFilter{
368✔
216
                        Type: entrance.TargetEntrance.GetType(),
368✔
217
                        Ids:  data,
368✔
218
                },
368✔
219
                Relation: entrance.TargetEntrance.GetValue(),
368✔
220
                Subject: &base.SubjectFilter{
368✔
221
                        Type:     request.GetSubject().GetType(),
368✔
222
                        Ids:      []string{request.GetSubject().GetId()},
368✔
223
                        Relation: request.GetSubject().GetRelation(),
368✔
224
                },
368✔
225
        }
368✔
226

368✔
227
        var (
368✔
228
                cti, rit   *database.TupleIterator
368✔
229
                err        error
368✔
230
                pagination database.CursorPagination
368✔
231
        )
368✔
232

368✔
233
        // Determine the pagination settings based on the entity type in the request.
368✔
234
        // If the entity type matches the target entrance, use cursor pagination with sorting by "entity_id".
368✔
235
        // Otherwise, use the default pagination settings.
368✔
236
        if request.GetEntrance().GetType() == entrance.TargetEntrance.GetType() {
539✔
237
                pagination = database.NewCursorPagination(database.Cursor(request.GetCursor()), database.Sort("entity_id"))
171✔
238
        } else {
368✔
239
                pagination = database.NewCursorPagination()
197✔
240
        }
197✔
241

242
        // Query the relationships using the specified pagination settings.
243
        // The context tuples are filtered based on the provided filter.
244
        cti, err = storageContext.NewContextualTuples(request.GetContext().GetTuples()...).QueryRelationships(filter, pagination)
368✔
245
        if err != nil {
368✔
246
                return err
×
247
        }
×
248

249
        // Query the relationships for the entity in the request.
250
        // The results are filtered based on the provided filter and pagination settings.
251
        rit, err = engine.dataReader.QueryRelationships(ctx, request.GetTenantId(), filter, request.GetMetadata().GetSnapToken(), pagination)
368✔
252
        if err != nil {
368✔
253
                return err
×
254
        }
×
255

256
        // Create a new UniqueTupleIterator from the two TupleIterators.
257
        // NewUniqueTupleIterator() ensures that the iterator only returns unique tuples.
258
        it := database.NewUniqueTupleIterator(rit, cti)
368✔
259

368✔
260
        for it.HasNext() { // Loop over each relationship.
1,470✔
261
                // Get the next tuple's subject.
1,102✔
262
                current, ok := it.GetNext()
1,102✔
263
                if !ok {
1,102✔
264
                        break
×
265
                }
266
                g.Go(func() error {
2,204✔
267
                        return engine.lt(ctx, request, &base.EntityAndRelation{ // Call the run method with a new entity and relation.
1,102✔
268
                                Entity: &base.Entity{
1,102✔
269
                                        Type: current.GetEntity().GetType(),
1,102✔
270
                                        Id:   current.GetEntity().GetId(),
1,102✔
271
                                },
1,102✔
272
                                Relation: current.GetRelation(),
1,102✔
273
                        }, visits, g, publisher)
1,102✔
274
                })
1,102✔
275
        }
276
        return nil
368✔
277
}
278

279
// tupleToUserSetEntrance is a method of the EntityFilterEngine struct. It handles tuple to user set entrances.
280
func (engine *EntityFilter) tupleToUserSetEntrance(
281
        // A context used for tracing and cancellation.
282
        ctx context.Context,
283
        // A permission request for linked entities.
284
        request *base.PermissionEntityFilterRequest,
285
        // A linked entrance.
286
        entrance *schema.LinkedEntrance,
287
        // A map that keeps track of visited entities to avoid infinite loops.
288
        visits *VisitsMap,
289
        // An errgroup used for executing goroutines.
290
        g *errgroup.Group,
291
        // A custom publisher that publishes results in bulk.
292
        publisher *BulkEntityPublisher,
293
) error { // Returns an error if one occurs during execution.
133✔
294
        // Retrieve the scope associated with the target entrance type.
133✔
295
        // Check if it exists to avoid accessing a nil map entry.
133✔
296
        scope, exists := request.GetScope()[entrance.TargetEntrance.GetType()]
133✔
297

133✔
298
        // Initialize data as an empty slice of strings.
133✔
299
        var data []string
133✔
300

133✔
301
        // If the scope exists, assign its Data field to the data slice.
133✔
302
        if exists {
133✔
303
                data = scope.GetData()
×
304
        }
×
305

306
        // Define a TupleFilter. This specifies which tuples we're interested in.
307
        // We want tuples that match the entity type and ID from the request, and have a specific relation.
308
        filter := &base.TupleFilter{
133✔
309
                Entity: &base.EntityFilter{
133✔
310
                        Type: entrance.TargetEntrance.GetType(),
133✔
311
                        Ids:  data,
133✔
312
                },
133✔
313
                Relation: entrance.TupleSetRelation, // Query for relationships that match the tuple set relation.
133✔
314
                Subject: &base.SubjectFilter{
133✔
315
                        Type:     request.GetSubject().GetType(),
133✔
316
                        Ids:      []string{request.GetSubject().GetId()},
133✔
317
                        Relation: "",
133✔
318
                },
133✔
319
        }
133✔
320

133✔
321
        var (
133✔
322
                cti, rit   *database.TupleIterator
133✔
323
                err        error
133✔
324
                pagination database.CursorPagination
133✔
325
        )
133✔
326

133✔
327
        // Determine the pagination settings based on the entity type in the request.
133✔
328
        // If the entity type matches the target entrance, use cursor pagination with sorting by "entity_id".
133✔
329
        // Otherwise, use the default pagination settings.
133✔
330
        if request.GetEntrance().GetType() == entrance.TargetEntrance.GetType() {
233✔
331
                pagination = database.NewCursorPagination(database.Cursor(request.GetCursor()), database.Sort("entity_id"))
100✔
332
        } else {
133✔
333
                pagination = database.NewCursorPagination()
33✔
334
        }
33✔
335

336
        // Query the relationships using the specified pagination settings.
337
        // The context tuples are filtered based on the provided filter.
338
        cti, err = storageContext.NewContextualTuples(request.GetContext().GetTuples()...).QueryRelationships(filter, pagination)
133✔
339
        if err != nil {
133✔
340
                return err
×
341
        }
×
342

343
        // Query the relationships for the entity in the request.
344
        // The results are filtered based on the provided filter and pagination settings.
345
        rit, err = engine.dataReader.QueryRelationships(ctx, request.GetTenantId(), filter, request.GetMetadata().GetSnapToken(), pagination)
133✔
346
        if err != nil {
133✔
347
                return err
×
348
        }
×
349

350
        // Create a new UniqueTupleIterator from the two TupleIterators.
351
        // NewUniqueTupleIterator() ensures that the iterator only returns unique tuples.
352
        it := database.NewUniqueTupleIterator(rit, cti)
133✔
353

133✔
354
        for it.HasNext() { // Loop over each relationship.
332✔
355
                // Get the next tuple's subject.
199✔
356
                current, ok := it.GetNext()
199✔
357
                if !ok {
199✔
358
                        break
×
359
                }
360
                g.Go(func() error {
398✔
361
                        return engine.lt(ctx, request, &base.EntityAndRelation{ // Call the run method with a new entity and relation.
199✔
362
                                Entity: &base.Entity{
199✔
363
                                        Type: entrance.TargetEntrance.GetType(),
199✔
364
                                        Id:   current.GetEntity().GetId(),
199✔
365
                                },
199✔
366
                                Relation: entrance.TargetEntrance.GetValue(),
199✔
367
                        }, visits, g, publisher)
199✔
368
                })
199✔
369
        }
370
        return nil
133✔
371
}
372

373
// run is a method of the EntityFilterEngine struct. It executes the linked entity engine for a given request.
374
func (engine *EntityFilter) lt(
375
        ctx context.Context, // A context used for tracing and cancellation.
376
        request *base.PermissionEntityFilterRequest, // A permission request for linked entities.
377
        found *base.EntityAndRelation, // An entity and relation that was previously found.
378
        visits *VisitsMap, // A map that keeps track of visited entities to avoid infinite loops.
379
        g *errgroup.Group, // An errgroup used for executing goroutines.
380
        publisher *BulkEntityPublisher, // A custom publisher that publishes results in bulk.
381
) error { // Returns an error if one occurs during execution.
2,308✔
382
        if !visits.AddER(found.GetEntity(), found.GetRelation()) { // If the entity and relation has already been visited.
2,384✔
383
                return nil
76✔
384
        }
76✔
385

386
        var err error
2,232✔
387

2,232✔
388
        // Retrieve linked entrances
2,232✔
389
        var entrances []*schema.LinkedEntrance
2,232✔
390
        entrances, err = engine.graph.LinkedEntrances(
2,232✔
391
                request.GetEntrance(),
2,232✔
392
                &base.Entrance{
2,232✔
393
                        Type:  request.GetSubject().GetType(),
2,232✔
394
                        Value: request.GetSubject().GetRelation(),
2,232✔
395
                },
2,232✔
396
        ) // Retrieve the linked entrances for the request.
2,232✔
397
        if err != nil {
2,232✔
398
                return err
×
399
        }
×
400

401
        if entrances == nil { // If there are no linked entrances for the request.
2,232✔
402
                if found.GetEntity().GetType() == request.GetEntrance().GetType() && found.GetRelation() == request.GetEntrance().GetValue() { // Check if the found entity matches the requested entity reference.
×
403
                        if !visits.AddPublished(found.GetEntity()) { // If the entity and relation has already been visited.
×
404
                                return nil
×
405
                        }
×
406
                        publisher.Publish(found.GetEntity(), &base.PermissionCheckRequestMetadata{ // Publish the found entity with the permission check metadata.
×
407
                                SnapToken:     request.GetMetadata().GetSnapToken(),
×
408
                                SchemaVersion: request.GetMetadata().GetSchemaVersion(),
×
409
                                Depth:         request.GetMetadata().GetDepth(),
×
410
                        }, request.GetContext(), base.CheckResult_CHECK_RESULT_UNSPECIFIED)
×
411
                        return nil
×
412
                }
413
                return nil // Otherwise, return without publishing any results.
×
414
        }
415

416
        g.Go(func() error {
4,464✔
417
                return engine.EntityFilter(ctx, &base.PermissionEntityFilterRequest{ // Call the Run method recursively with a new permission request.
2,232✔
418
                        TenantId: request.GetTenantId(),
2,232✔
419
                        Entrance: request.GetEntrance(),
2,232✔
420
                        Subject: &base.Subject{
2,232✔
421
                                Type:     found.GetEntity().GetType(),
2,232✔
422
                                Id:       found.GetEntity().GetId(),
2,232✔
423
                                Relation: found.GetRelation(),
2,232✔
424
                        },
2,232✔
425
                        Scope:    request.GetScope(),
2,232✔
426
                        Metadata: request.GetMetadata(),
2,232✔
427
                        Context:  request.GetContext(),
2,232✔
428
                        Cursor:   request.GetCursor(),
2,232✔
429
                }, visits, publisher)
2,232✔
430
        })
2,232✔
431
        return nil
2,232✔
432
}
433

434
// pathChainEntrance handles multi-hop relation chain traversal for nested attributes
435
//
436
// TODO: This function can be optimized for better performance by implementing smart batching logic:
437
// - Extract unique attributes from path chain entrances to avoid duplicate queries
438
// - Implement batch vs individual processing based on scope and attribute count:
439
//   - Use batch mode when we have scope (limited entity IDs) or few attributes (<=1)
440
//   - Use individual mode when no scope and multiple attributes to avoid loading large result sets
441
//   - Refactor into smaller helper functions: extractUniqueAttributes, getScopeIds, shouldUseBatchMode,
442
//     processBatchMode, processIndividualMode, queryAttributesBatch, processEntranceWithResults
443
//   - Remove debug statements after optimization is tested
444
func (engine *EntityFilter) pathChainEntrance(
445
        ctx context.Context,
446
        request *base.PermissionEntityFilterRequest,
447
        entrance *schema.LinkedEntrance,
448
        visits *VisitsMap,
449
        publisher *BulkEntityPublisher,
450
) error {
91✔
451
        if !visits.AddEA(entrance.TargetEntrance.GetType(), entrance.TargetEntrance.GetValue()) {
137✔
452
                return nil
46✔
453
        }
46✔
454

455
        // 1. Query attributes of the target type with scope optimization
456
        scope, exists := request.GetScope()[entrance.TargetEntrance.GetType()]
45✔
457
        var data []string
45✔
458
        if exists {
45✔
459
                data = scope.GetData()
×
460
        }
×
461

462
        filter := &base.AttributeFilter{
45✔
463
                Entity: &base.EntityFilter{
45✔
464
                        Type: entrance.TargetEntrance.GetType(),
45✔
465
                        Ids:  data,
45✔
466
                },
45✔
467
                Attributes: []string{entrance.TargetEntrance.GetValue()},
45✔
468
        }
45✔
469

45✔
470
        pagination := database.NewCursorPagination()
45✔
471
        cti, err := storageContext.NewContextualAttributes(request.GetContext().GetAttributes()...).QueryAttributes(filter, pagination)
45✔
472
        if err != nil {
45✔
473
                return err
×
474
        }
×
475

476
        rit, err := engine.dataReader.QueryAttributes(ctx, request.GetTenantId(), filter, request.GetMetadata().GetSnapToken(), pagination)
45✔
477
        if err != nil {
45✔
478
                return err
×
479
        }
×
480

481
        it := database.NewUniqueAttributeIterator(rit, cti)
45✔
482

45✔
483
        // 2. Collect all attribute entity IDs first (batch approach)
45✔
484
        var attributeEntityIds []string
45✔
485
        sourceType := request.GetEntrance().GetType()
45✔
486
        targetType := entrance.TargetEntrance.GetType()
45✔
487

45✔
488
        // Collect all entity IDs that have the attribute
45✔
489
        for it.HasNext() {
139✔
490
                current, ok := it.GetNext()
94✔
491
                if !ok {
94✔
492
                        break
×
493
                }
494
                attributeEntityIds = append(attributeEntityIds, current.GetEntity().GetId())
94✔
495
        }
496

497
        if len(attributeEntityIds) == 0 {
45✔
498
                return nil
×
499
        }
×
500

501
        // 3. Use the PathChain from entrance to traverse relation chain
502
        chain := entrance.PathChain
45✔
503
        if len(chain) == 0 {
45✔
504
                return errors.New("PathChainLinkedEntrance missing PathChain")
×
505
        }
×
506

507
        // 4. Fold IDs across the relation chain from attribute type back to source type
508
        currentType := targetType
45✔
509
        currentIds := attributeEntityIds
45✔
510

45✔
511
        for i := len(chain) - 1; i >= 0; i-- {
90✔
512
                walk := chain[i] // walk.Type == left entity type; walk.Relation relates walk.Type -> currentType
45✔
513

45✔
514
                // Apply scope optimization only on the final walk (source type)
45✔
515
                var entIds []string
45✔
516
                if i == 0 {
90✔
517
                        if s, exists := request.GetScope()[sourceType]; exists {
45✔
518
                                entIds = s.GetData()
×
519
                        }
×
520
                }
521

522
                // Determine correct subject relation for complex cases like @group#member
523
                subjectRelation := engine.graph.GetSubjectRelationForPathWalk(walk.GetType(), walk.GetRelation(), currentType)
45✔
524

45✔
525
                relationFilter := &base.TupleFilter{
45✔
526
                        Entity: &base.EntityFilter{
45✔
527
                                Type: walk.GetType(),
45✔
528
                                Ids:  entIds,
45✔
529
                        },
45✔
530
                        Relation: walk.GetRelation(),
45✔
531
                        Subject: &base.SubjectFilter{
45✔
532
                                Type:     currentType,
45✔
533
                                Ids:      currentIds,
45✔
534
                                Relation: subjectRelation, // Fixed: Use correct subject relation for complex cases
45✔
535
                        },
45✔
536
                }
45✔
537

45✔
538
                pagination := database.NewCursorPagination()
45✔
539
                ctiR, err := storageContext.NewContextualTuples(request.GetContext().GetTuples()...).QueryRelationships(relationFilter, pagination)
45✔
540
                if err != nil {
45✔
541
                        return err
×
542
                }
×
543

544
                ritR, err := engine.dataReader.QueryRelationships(ctx, request.GetTenantId(), relationFilter, request.GetMetadata().GetSnapToken(), pagination)
45✔
545
                if err != nil {
45✔
546
                        return err
×
547
                }
×
548

549
                relationIt := database.NewUniqueTupleIterator(ritR, ctiR)
45✔
550

45✔
551
                // collect next frontier (left entity IDs)
45✔
552
                nextIdsSet := make(map[string]struct{})
45✔
553
                for relationIt.HasNext() {
159✔
554
                        tuple, ok := relationIt.GetNext()
114✔
555
                        if !ok {
114✔
556
                                break
×
557
                        }
558
                        nextIdsSet[tuple.GetEntity().GetId()] = struct{}{}
114✔
559
                }
560

561
                var nextIds []string
45✔
562
                for id := range nextIdsSet {
159✔
563
                        nextIds = append(nextIds, id)
114✔
564
                }
114✔
565

566
                if len(nextIdsSet) == 0 {
45✔
567
                        return nil // No path found through this walk
×
568
                }
×
569

570
                // prepare for next walk
571
                currentType = walk.GetType()
45✔
572
                currentIds = nextIds
45✔
573
        }
574

575
        // 5. Publish all resolved source entities
576
        for _, id := range currentIds {
159✔
577
                entity := &base.Entity{Type: sourceType, Id: id}
114✔
578
                if !visits.AddPublished(entity) {
177✔
579
                        continue
63✔
580
                }
581

582
                publisher.Publish(entity, &base.PermissionCheckRequestMetadata{
51✔
583
                        SnapToken:     request.GetMetadata().GetSnapToken(),
51✔
584
                        SchemaVersion: request.GetMetadata().GetSchemaVersion(),
51✔
585
                        Depth:         request.GetMetadata().GetDepth(),
51✔
586
                }, request.GetContext(), base.CheckResult_CHECK_RESULT_UNSPECIFIED)
51✔
587
        }
588

589
        return nil
45✔
590
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc