fix: support recursive attributes in lookup-entity#2763
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds cursor-aware recursive expansion for self-referential attribute permissions, self-cycle detection in the linked schema, path-chain composition/caching for nested attribute entrances, and tests covering recursive lookups, pagination, and cursor decoding. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant EF as EntityFilter
participant LS as LinkedSchema
participant DB as Storage
participant BP as BulkEntityPublisher
Client->>EF: Check/Lookup request
EF->>LS: SelfCycleRelationsForPermission(entityType, permission)
LS-->>EF: selfCycleRelations
EF->>EF: attributeEntrance(..., selfCycleRelations)
EF->>DB: query direct attribute relations (with cursor)
DB-->>EF: direct entities (+ cursor)
EF->>BP: publish direct entities
alt selfCycleRelations exist
loop per relation
EF->>EF: expandRecursiveRelation(relation, seedIDs)
EF->>DB: queryRelations(relation, seedIDs, cursor)
DB-->>EF: related entities (+ nextCursor)
EF->>BP: publish recursive entities
end
end
BP-->>Client: aggregated results
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/engines/entity_filter.go (1)
130-210:⚠️ Potential issue | 🟠 MajorCursor-filtered seeds can drop recursive results.
When recursion is enabled, the attribute query is cursor-paginated and the seed list only reflects the current page. Descendants with IDs after the cursor but reachable solely via pre-cursor attributes will be missed. Consider collecting seeds without the cursor and applying the cursor only at publish time.
✅ Suggested fix (collect full seeds, filter publish in-memory)
- pagination := database.NewCursorPagination(database.Cursor(request.GetCursor()), database.Sort("entity_id")) + needsRecursive := request.GetEntrance().GetType() == entrance.TargetEntrance.GetType() && len(selfCycleRelations) > 0 + pagination := database.NewCursorPagination(database.Cursor(request.GetCursor()), database.Sort("entity_id")) + cursorValue := "" + if needsRecursive { + // For recursive expansion, collect full seed set and apply cursor in-memory. + pagination = database.NewCursorPagination(database.Sort("entity_id")) + if request.GetCursor() != "" { + var err error + cursorValue, err = decodeCursorValue(request.GetCursor()) + if err != nil { + return err + } + } + } ... - if !visits.AddPublished(entity) { - continue - } - publisher.Publish(entity, &base.PermissionCheckRequestMetadata{ - SnapToken: request.GetMetadata().GetSnapToken(), - SchemaVersion: request.GetMetadata().GetSchemaVersion(), - Depth: request.GetMetadata().GetDepth(), - }, request.GetContext(), base.CheckResult_CHECK_RESULT_UNSPECIFIED) - - attributeEntityIDs = append(attributeEntityIDs, entity.GetId()) + attributeEntityIDs = append(attributeEntityIDs, entity.GetId()) + if cursorValue == "" || entity.GetId() >= cursorValue { + if !visits.AddPublished(entity) { + continue + } + publisher.Publish(entity, &base.PermissionCheckRequestMetadata{ + SnapToken: request.GetMetadata().GetSnapToken(), + SchemaVersion: request.GetMetadata().GetSchemaVersion(), + Depth: request.GetMetadata().GetDepth(), + }, request.GetContext(), base.CheckResult_CHECK_RESULT_UNSPECIFIED) + } ... - if request.GetEntrance().GetType() == entrance.TargetEntrance.GetType() && - len(selfCycleRelations) > 0 && - len(attributeEntityIDs) > 0 { + if needsRecursive && len(attributeEntityIDs) > 0 {
🤖 Fix all issues with AI agents
In `@internal/engines/entity_filter.go`:
- Around line 270-285: The loop builds a TupleFilter but leaves Subject.Relation
empty, which breaks recursive traversal for self-referential relations; update
the code that constructs the &base.TupleFilter so Subject.Relation is set by
calling the schema helper to derive the correct subject relation for the given
relation and entityType (e.g., obtain subjectRel :=
schemaHelper.DeriveSubjectRelation(relation, entityType) or the equivalent
helper method in your schema package) and assign Subject.Relation = subjectRel
(keep using EntityFilter.Type, Ids=data and Subject.Ids=currentIDs as before).
| for len(queue) > 0 { | ||
| currentIDs := queue | ||
| queue = nil | ||
|
|
||
| filter := &base.TupleFilter{ | ||
| Entity: &base.EntityFilter{ | ||
| Type: entityType, | ||
| Ids: data, | ||
| }, | ||
| Relation: relation, | ||
| Subject: &base.SubjectFilter{ | ||
| Type: entityType, | ||
| Ids: currentIDs, | ||
| Relation: "", | ||
| }, | ||
| } |
There was a problem hiding this comment.
Recursive traversal ignores subject relation.
For relations that point back to the same entity type with a subject relation (e.g., @group#member), leaving Subject.Relation empty can miss edges or over-include. Use the schema helper to derive the correct subject relation.
✅ Suggested fix (respect subject relation)
- filter := &base.TupleFilter{
+ subjectRelation := engine.graph.GetSubjectRelationForPathWalk(entityType, relation, entityType)
+ filter := &base.TupleFilter{
Entity: &base.EntityFilter{
Type: entityType,
Ids: data,
},
Relation: relation,
Subject: &base.SubjectFilter{
Type: entityType,
Ids: currentIDs,
- Relation: "",
+ Relation: subjectRelation,
},
}🤖 Prompt for AI Agents
In `@internal/engines/entity_filter.go` around lines 270 - 285, The loop builds a
TupleFilter but leaves Subject.Relation empty, which breaks recursive traversal
for self-referential relations; update the code that constructs the
&base.TupleFilter so Subject.Relation is set by calling the schema helper to
derive the correct subject relation for the given relation and entityType (e.g.,
obtain subjectRel := schemaHelper.DeriveSubjectRelation(relation, entityType) or
the equivalent helper method in your schema package) and assign Subject.Relation
= subjectRel (keep using EntityFilter.Type, Ids=data and Subject.Ids=currentIDs
as before).
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #2763 +/- ##
==========================================
- Coverage 82.64% 82.58% -0.06%
==========================================
Files 74 74
Lines 8125 8314 +189
==========================================
+ Hits 6714 6865 +151
- Misses 892 910 +18
- Partials 519 539 +20 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
Note Docstrings generation - SUCCESS |
Docstrings generation was requested by @omer-topal. * #2763 (comment) The following files were modified: * `internal/engines/entity_filter.go`
| return res | ||
| } | ||
|
|
||
| func (g *LinkedSchemaGraph) collectSelfCycleRelations(entityType, permission string, child *base.Child, seen map[string]struct{}, res *[]string) { |
There was a problem hiding this comment.
I'm not sure I fully understand the graph, but could we end up in a situation where we have a stack overflow from the recursion here? If seen is protecting us from that, can you explain how real quick? Thanks.
There was a problem hiding this comment.
The seen map in SelfCycleRelationsForPermission is only for deduping schema-level relation names. It detects which tuple-set relations in the schema cause a permission to reference itself. That's a static schema analysis step.
Runtime cycle protection is in expandRecursiveRelation. It uses an iterative BFS with a seen set seeded from initial IDs, and only enqueues unseen entity IDs. In a data cycle (1 -> 2 -> 1), 1 is already seen on revisit, so it is not re-enqueued and and the queue eventually drains.
| })) | ||
| }) | ||
|
|
||
| It("Case 32: Nested PathChain preserves tuple-set relation", func() { |
There was a problem hiding this comment.
Can you tell me which tests are ensuring we don't regress existing code with the fix, vs. tests that demonstrate the bug and fail (without the fix)?
There was a problem hiding this comment.
Cases 1–23 cover the existing schema graph logic and would catch any breakage.
Cases 32–36 are new schema-layer tests targeting the new SelfCycleRelationsForPermission method and corrected PathChain output.
The actual runtime behavior is tested in lookup_test.go under the "Recursive Attribute Lookup" context. These set up actual data:
resource:r1#parent@resource:default with is_public=true on default and assert that LookupEntity returns both r1 and default for view.
Those are the ones that directly demonstrate the bug and fail without the fix.
| })) | ||
| }) | ||
|
|
||
| It("Case 33: SelfCycleRelationsForPermission returns only same-type relations", func() { |
There was a problem hiding this comment.
Do these case numbers (32, 33, etc.) trace back to anything in particular?
There was a problem hiding this comment.
Nothing particular, I followed the existing numbering convention.
|
apologize for the over-zealous AI review, but did raise a couple good points that I think are worth looking into at least |
|
fix: #2745 |
Summary by CodeRabbit