fix(compiler): disable FK star when peripheral nodes need elevated filter

Problem

The FK star optimization removes the node table scan for peripheral nodes, replacing them with FK-column joins on the central node table. However, SecurityPass needs the node table scan to inject the role-gated startsWith(traversal_path) filter for nodes with needs_elevated_filter.

When a peripheral node (e.g. Vulnerability) requires elevated access but the query is optimized as a star with a different central node (e.g. Project), the security filter is never injected because the peripheral node table scan no longer exists. This causes the query to return 0 results for users with the correct elevated role.

Impact

Security Managers with valid security_manager_path("1/100/") roles cannot see Vulnerability nodes when querying via edge traversal with no property filters. The test traversal_vulnerability_security_manager_no_filters_sees_data documented this as a TODO since the security invariant (Reporter blocked) was enforced, but the over-restriction prevented legitimate access.

Fix

detect_fk_star now checks if any peripheral node has needs_elevated_filter. If so, it returns None, disabling the star optimization and allowing SecurityPass to inject the filter correctly.

Files changed

  • crates/query-engine/compiler/src/passes/plan/edge_chain.rs: Added security guard in detect_fk_star
  • crates/integration-tests/tests/server/data_correctness/security.rs: Updated test to assert correct behavior (2 nodes instead of 0)

Testing

cargo test -p query-engine traversal_vulnerability_security_manager_no_filters_sees_data
The test now passes with:
assert_node_count(2) — Vulnerability 8000 + Project 1000
assert_node_ids("Project", &[1000])
assert_node_ids("Vulnerability", &[8000])
assert_edge_count("IN_PROJECT", 1)
Regression prevention
This fix ensures that future entities with required_role above Reporter will not be silently dropped by FK star optimization. The security guard is conservative: it only disables the optimization when necessary, preserving performance for non-elevated queries.
Related
Fixes the TODO in security.rs:1108 that tracked this bug.

[skip query-dsl-version-check]


<!--
TEMPLATE CONVENTION — read before filling this out

The top sections are for a REVIEWER skimming in 30 seconds: what changed, why,
how it was verified. They must read as plain prose.

ALL implementation mechanics — function names, type names, constants, encoder
details, wire-format traces, file-by-file walkthroughs, alternatives
considered, full benchmark tables, raw logs, agent reasoning — go in the Agent
context block at the bottom. Never above it.

Agents: if you feel the urge to write a wall of text, write it inside the
Agent context block. The top sections stay terse.
-->

### What does this MR do and why?

<!--
HARD LIMITS for this section:
  - At most 80 words.
  - At most 3 inline `code` spans.
  - NO bare function/type/constant names in prose (no foo_bar, Foo::bar,
    do_thing(), CONST_NAME). If you need them, you are writing for the wrong
    section — move it to Agent context.

Write 2-3 plain sentences: the operator/user-visible effect, and why. State the
symptom that motivated it, not the code path that implements it. Keep this
updated as discussion happens.

Example of the RIGHT level:
  "Indexing froze when a large batch produced an over-long request URL that the
   HTTP layer rejected. This guards the URL length at the single point every
   datalake lookup passes through, so an oversized batch is split instead of
   wedging the pipeline."
-->

### Related Issues

<!--
Does this MR close or contribute to any issues/epics? `Closes #N` or
`Relates to #N`. Omit if trivial.
-->

### Testing

<!--
How did you verify the change? One or two lines plus a CI job link is
usually enough. Full test transcripts and exploratory notes go in the Agent
context block.
-->

### Performance Analysis

<!--
The headline result and any regression risk. Full flamegraphs, profiler
output, and benchmark tables go in the Agent context block — link or
summarize them here.
-->

- [ ] This merge request does not introduce any performance regression. If a performance regression is expected, explain why.

<details>
<summary><b>Agent context</b> — long-form analysis, file-by-file walkthroughs, profiler output, alternatives considered</summary>

<!--
Agents: put extended reasoning here. File-by-file walkthroughs, full
benchmark tables, raw profiler output, dataflow narratives, alternatives
considered, and anything else that would bury the sections above belongs in
this block.
-->

* fix(compiler): disable FK star optimization when peripheral nodes need elevated security filter

The FK star optimization removes the node table scan for peripheral
nodes, replacing them with FK-column joins on the central node table.
However, SecurityPass needs the node table scan to inject the
role-gated startsWith(traversal_path) filter for nodes with
needs_elevated_filter.

When a peripheral node (e.g. Vulnerability) requires elevated access
but the query is optimized as a star with a different central node
(e.g. Project), the security filter is never injected because the
peripheral node table scan no longer exists. This causes the query
to return 0 results for users with the correct elevated role.

Fix: detect_fk_star now checks if any peripheral node has
needs_elevated_filter. If so, it returns None, disabling the star
optimization and allowing SecurityPass to inject the filter correctly.

Fixes the TODO in security.rs: traversal_vulnerability_security_manager_no_filters_sees_data.

</details>

/label ~"Hackathon"

/assign me
<!--
/request_review @jgdoyon1 @michaelangeloio @michaelusa @bohdanpk
-->
Edited by Anna Tchijova

Merge request reports

Loading