fix(graphsec): handle ontology-defined redaction specs in engine

What does this MR do and why?

Problem

This MR addresses the issue (first identified in !222 (closed)) where Definition nodes (and a few others like File, Branch) were being authorized against their own hash-based id, when they should use project_id. The ontology fixture had the right config — RedactionConfig.id_column = "project_id" — but the query engine was ignoring it and always falling back to "id". The root issue was that the server was re-deriving redaction config from the ontology at request time, which created two problems: the fallback logic could silently ignore the configured column, and every stage in the pipeline needed the ontology passed around to do its job.

Solution

The fix is to do that derivation once, at compile time, inside the query engine. normalize.rs now builds a full entity_auth map over every ontology entity that has a RedactionConfig, and stores it on Input. enforce.rs copies that map into ResultContext, so by the time a compiled query reaches the server, the auth metadata travels with it. As a consequence, InputNode.redaction_id_column is also set unconditionally during normalization — it's a String, not an Option<String>, and nothing downstream needs a fallback.

Solution Details

RedactionExtractor is gone. Its job was to look up ontology config at request time to figure out which resource type maps to which entity, and which column to use as the auth ID. That job now happens at compile time, so the extractor had nothing left to do.

The intermediate stage structs (ExtractionOutput, AuthorizationOutput, RedactionOutput) were carrying fields that were either already derivable from query_result or just pass-through values unchanged across every stage. Specifically:

  • result_context was re-extracted from query_result.ctx() at the end anyway
  • generated_sql never changed between stages — it comes from the compiler and goes straight to the formatter
  • resources_to_check was computed from query_result and immediately consumed by the next stage, so there was no reason to store it on a struct

ExtractionOutput is gone entirely — ExtractionStage::execute just returns a QueryResult now. AuthorizationStage calls .resource_checks() itself. generated_sql lives as a local in run_query. result_context comes off query_result.ctx() once, right before it's needed.

Testing

All existing tests pass, with no change to assertions, only setup. Two tests built ResultContext manually without going through compile(), so entity_auth was empty and every row was getting redacted. Fixed by extracting the entity auth building logic into a public build_entity_auth(ontology) function that the tests can call directly.

Performance Analysis

  • This merge request does not introduce any performance regression. If a performance regression is expected, explain why.
Edited by Michael Usachenko

Merge request reports

Loading