Skip to content

BE-538: HashQL: Permission system integration#8882

Open
indietyp wants to merge 34 commits into
bm/be-71-hashql-consider-merging-ast-stagesfrom
bm/be-538-hashql-permission-system-integration
Open

BE-538: HashQL: Permission system integration#8882
indietyp wants to merge 34 commits into
bm/be-71-hashql-consider-merging-ast-stagesfrom
bm/be-538-hashql-permission-system-integration

Conversation

@indietyp

@indietyp indietyp commented Jun 18, 2026

Copy link
Copy Markdown
Member

🌟 What is the purpose of this PR?

Integrates the authorization policy system into HashQL query execution. Compiled queries are now patched at runtime with actor-specific permit/forbid conditions (WHERE clause) and property masking (CASE WHEN projections), achieving parity with the existing PolicyComponents + Filter::for_policies system used by the REST entity endpoints.

Size note: This PR is large (75 files, ~4.8k added). If review is difficult, it can be split into 4 stacked PRs:

Production Tests Snapshots HTTP
PR1: Prerequisites (patch system, projections, shared test infra) ~800 ~500 ~30
PR2: Policy (permit/forbid WHERE clause) ~400 ~600 ~200
PR3: Protection (property masking graft) ~250 ~700 ~500
PR4: API wiring (REST integration, diagnostics) ~350 ~69

🚫 Blocked by

These PRs rework the underlying entity tables and query compilation that the authorization graft targets. Once they land, this PR will need to undergo some adjustments.

🔍 What does this change?

Authorization patching system (eval/src/postgres/authorization/)

  • Policy admission: translates Cedar-style permit/forbid policies into SQL WHERE predicates, with two-phase lowering (blank permit/forbid detection before expression allocation to avoid dead parameters)
  • Property masking: generates CASE WHEN expressions grafted into the entity_editions LATERAL subquery, stripping protected property keys from properties and property_metadata columns
  • HList/CPS pipeline (PreparedQueryPatch): typed layer composition where each layer receives a next continuation, registers join demands before materialization, and can rewrite the FROM tree after
  • Auxiliary parameters: Vec<Box<dyn ToSql + Sync>> sidepiece on PreparedQuery, chained as borrowed refs during encoding, separate from the compiler's Parameters system

REST API wiring (graph/api/src/rest/hashql/)

  • PolicyComponents built per-request from the authenticated actor and query-derived actions
  • PropertyProtectionFilterConfig read from the store pool settings (respects HASH_GRAPH_SKIP_FILTER_PROTECTION)
  • Proper diagnostics: actor_not_found (ERROR/400) vs authorization_context_failed (BUG/500), with DetermineActor checking inner cause to distinguish store errors from genuine not-found

Projection system (eval/src/postgres/projections.rs)

  • AuxiliaryProjections: tracks compiled join aliases, detects existing joins (punch-through), registers new join demands
  • build_joins: materializes auth joins into the FROM tree via mem::replace on CrossJoin right spine, maintaining LATERAL ordering (non-LATERAL before LATERAL)
  • find_from_by_alias: recursive FROM tree walker for locating graft targets

Test infrastructure

  • Shared CompilationFixture in postgres/tests.rs used by both filter and authorization tests
  • MockStore implementing PrincipalStore + PolicyStore with strict assertions (actor UUID, action, principal context)
  • 45 authorization unit tests (policy algebra, protection lowering, projection building, 5 E2E integration)
  • HTTP tests updated: all HashQL requests now authenticate, plus new tests for missing header and invalid actor

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • does not modify any publishable blocks or libraries, or modifications do not need publishing

📜 Does this require a change to the docs?

The changes in this PR:

  • are internal and do not require a docs change

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • do not affect the execution graph

⚠️ Known issues

  • PropertyProtectionFilterConfig uses HashMap internally, so SQL generation order for multiple protected properties is non-deterministic. Semantics are order-independent (concatenated mask expression), but query plans could theoretically differ between requests. The multi-property test uses structural assertions instead of snapshots to avoid flakiness.

🛡 What tests cover this?

  • 45 authorization unit tests: policy algebra (7), protection lowering (14), projection building (7), side-effect assertions (5), E2E integration (5), structural assertions (7)
  • 24 filter tests (migrated to shared infrastructure)
  • HTTP integration tests: missing auth header (400), invalid actor with diagnostic (400), authenticated queries (200)
  • All existing HashQL HTTP tests updated to pass actor ID

❓ How to test this?

  1. Start the stack (yarn dev:backend)
  2. Run HTTP tests: cd tests/graph/http && yarn reset-database && yarn httpyac send --all tests/hashql.http
  3. Verify: 10/10 pass, including auth header rejection and invalid actor diagnostic
  4. Run Rust tests: cargo test --lib --package hashql-eval -- postgres

@vercel

vercel Bot commented Jun 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hash Ready Ready Preview, Comment Jun 23, 2026 10:13am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
hashdotdesign-tokens Ignored Ignored Preview Jun 23, 2026 10:13am
petrinaut Skipped Skipped Jun 23, 2026 10:13am

@vercel vercel Bot temporarily deployed to Preview – petrinaut June 18, 2026 15:18 Inactive
@github-actions github-actions Bot added area/apps > hash* Affects HASH (a `hash-*` app) area/libs Relates to first-party libraries/crates/packages (area) labels Jun 18, 2026
@vercel vercel Bot temporarily deployed to Preview – petrinaut June 22, 2026 12:48 Inactive
@indietyp indietyp marked this pull request as ready for review June 22, 2026 13:47
Copilot AI review requested due to automatic review settings June 22, 2026 13:47
@cursor

cursor Bot commented Jun 22, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes authentication, authorization SQL, and property masking on the HashQL read path—security-critical behavior with a breaking API requirement (mandatory actor header).

Overview
HashQL execution now requires the X-Authenticated-User-Actor-Id header (OpenAPI updated). After compile, the server resolves PolicyComponents for that actor and the query’s inferred actions (e.g. ViewEntity), then patches each prepared Postgres query before the orchestrator runs.

Runtime authorization graft (AuthorizationPatch via PreparedQueryPatch): permit/forbid policies become extra WHERE predicates; PropertyProtectionFilterConfig strips protected keys by rewriting the entity_editions LATERAL so properties / property_metadata are (column - mask). Auxiliary bind parameters are appended and passed through query_raw alongside compiled params.

Server/router wiring: drops a dedicated PostgresStorePool extension for HashQL; uses the main store pool (AsRef<PostgresClient> + policy/principal stores) and threads filter_protection from pool settings. HashQL routes are merged with other API resources under the same generic store bounds.

Supporting changes: property-protection filters use a narrower PropertyFilterEntityQueryPath; Postgres query builder gains array_positions and uuid typing; compile-time with_property_mask on the compiler is removed in favor of the patch pipeline. Large unit/snapshot coverage for policy algebra, protection lowering, and end-to-end patched SQL.

Reviewed by Cursor Bugbot for commit d74c496. Bugbot is set up for automated code reviews on this repo. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 81f4f86. Configure here.

Comment thread libs/@local/graph/api/src/rest/hashql/error.rs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Integrates the graph authorization policy system into HashQL execution by patching compiled Postgres queries at runtime with actor-specific policy admission (permit/forbid WHERE predicates) and property protection masking (CASE WHEN–driven JSONB key removal inside the entity_editions LATERAL subquery). This also wires HashQL REST execution to require an authenticated actor header, builds PolicyComponents per request, and expands/updates the test infrastructure and snapshots to cover the new behavior.

Changes:

  • Add a typed, continuation-passing prepared-query patch pipeline (PreparedQueryPatch) and an AuthorizationPatch layer to graft policy predicates + property masking into compiled SQL.
  • Rework Postgres projections to expose entity_editions as a CROSS JOIN LATERAL (SELECT ee.<col> AS <col> ...) so masking can target properties/property_metadata, and add auxiliary join planning for auth needs.
  • Update REST HashQL endpoint to require X-Authenticated-User-Actor-Id, build policy context per request, propagate property protection config, and add/adjust unit + HTTP + snapshot tests.

Reviewed changes

Copilot reviewed 78 out of 78 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/graph/integration/postgres/email_filter_protection.rs Update protection config helpers to return Arc<PropertyProtectionFilterConfig> and use the new property-filter path enum.
tests/graph/http/tests/hashql.http Add negative auth tests (missing header / invalid actor) and update existing requests to send actor header.
tests/graph/benches/graph/scenario/runner.rs Update benchmark settings to use Arc<PropertyProtectionFilterConfig>.
libs/@local/hashql/eval/tests/ui/postgres/filter/provides_drives_select_and_joins.snap Snapshot updates reflecting entity_editions as a LATERAL subquery.
libs/@local/hashql/eval/tests/ui/postgres/filter/data_island_provides_without_lateral.snap Snapshot updates reflecting entity_editions as a LATERAL subquery.
libs/@local/hashql/eval/tests/ui/postgres/authorization/protection/transpile_hash_default.snap New snapshot for default protection mask SQL + auxiliary params.
libs/@local/hashql/eval/tests/ui/postgres/authorization/protection/resolve_path_uuid.snap New snapshot for protection path resolution (Uuid).
libs/@local/hashql/eval/tests/ui/postgres/authorization/protection/resolve_path_type_base_urls.snap New snapshot for protection path resolution (TypeBaseUrls).
libs/@local/hashql/eval/tests/ui/postgres/authorization/protection/resolve_expression_text.snap New snapshot for protection parameter lowering.
libs/@local/hashql/eval/tests/ui/postgres/authorization/protection/resolve_expression_actor_id.snap New snapshot for protection actor-id lowering.
libs/@local/hashql/eval/tests/ui/postgres/authorization/protection/lower_property_filter_case_when.snap New snapshot for per-property CASE mask fragment.
libs/@local/hashql/eval/tests/ui/postgres/authorization/protection/lower_filter_not_equal.snap New snapshot for protection NotEqual lowering.
libs/@local/hashql/eval/tests/ui/postgres/authorization/protection/lower_filter_nested_all.snap New snapshot for nested All lowering.
libs/@local/hashql/eval/tests/ui/postgres/authorization/protection/lower_filter_in.snap New snapshot for protection In lowering.
libs/@local/hashql/eval/tests/ui/postgres/authorization/protection/lower_filter_equal.snap New snapshot for protection Equal lowering.
libs/@local/hashql/eval/tests/ui/postgres/authorization/protection/lower_filter_any_disjunction.snap New snapshot for protection Any lowering.
libs/@local/hashql/eval/tests/ui/postgres/authorization/projections/build_joins_no_laterals_with_auth.snap New snapshot for auxiliary-join insertion (no laterals, auth joins present).
libs/@local/hashql/eval/tests/ui/postgres/authorization/projections/build_joins_inserts_before_laterals.snap New snapshot for inserting auth joins before continuation laterals.
libs/@local/hashql/eval/tests/ui/postgres/authorization/projections/build_from_with_entity_ids_and_editions.snap New snapshot validating FROM assembly with ids + editions lateral.
libs/@local/hashql/eval/tests/ui/postgres/authorization/projections/build_from_with_entity_editions.snap New snapshot validating FROM assembly with editions lateral.
libs/@local/hashql/eval/tests/ui/postgres/authorization/projections/build_from_editions_before_continuations.snap New snapshot validating editions lateral precedes continuation laterals.
libs/@local/hashql/eval/tests/ui/postgres/authorization/projections/build_from_base_only.snap New snapshot for base-only FROM.
libs/@local/hashql/eval/tests/ui/postgres/authorization/policy/optimize_single_web.snap New snapshot for policy optimization (single web id).
libs/@local/hashql/eval/tests/ui/postgres/authorization/policy/optimize_single_entity.snap New snapshot for policy optimization (single entity uuid).
libs/@local/hashql/eval/tests/ui/postgres/authorization/policy/optimize_multiple_webs.snap New snapshot for policy optimization (batched web ids).
libs/@local/hashql/eval/tests/ui/postgres/authorization/policy/optimize_multiple_entities.snap New snapshot for policy optimization (batched entity uuids).
libs/@local/hashql/eval/tests/ui/postgres/authorization/policy/is_of_type_overlap.snap New snapshot for (base_url, version) pairing check via array_positions overlap.
libs/@local/hashql/eval/tests/ui/postgres/authorization/policy/is_of_base_type_any.snap New snapshot for base-url membership via ANY(base_urls).
libs/@local/hashql/eval/tests/ui/postgres/authorization/policy/filter_not_negation.snap New snapshot for NOT policy filter lowering.
libs/@local/hashql/eval/tests/ui/postgres/authorization/policy/filter_any_disjunction.snap New snapshot for Any policy filter lowering.
libs/@local/hashql/eval/tests/ui/postgres/authorization/policy/filter_all_conjunction.snap New snapshot for All policy filter lowering.
libs/@local/hashql/eval/tests/ui/postgres/authorization/policy/created_by_principal_with_actor.snap New snapshot for created-by principal check with actor.
libs/@local/hashql/eval/tests/ui/postgres/authorization/policy/created_by_principal_anonymous.snap New snapshot for created-by principal check with anonymous/public actor.
libs/@local/hashql/eval/tests/ui/postgres/authorization/policy/constraint_web.snap New snapshot for web-scoped constraint lowering.
libs/@local/hashql/eval/tests/ui/postgres/authorization/policy/constraint_web_with_created_by.snap New snapshot for web constraint + created-by filter lowering.
libs/@local/hashql/eval/tests/ui/postgres/authorization/policy/constraint_exact_entity.snap New snapshot for exact-entity constraint lowering.
libs/@local/hashql/eval/tests/ui/postgres/authorization/policy/constraint_any_with_type_filter.snap New snapshot for any-entity constraint with type filter lowering.
libs/@local/hashql/eval/tests/ui/postgres/authorization/policy/algebra_permits_and_forbids.snap New snapshot for combined permit + forbid algebra result.
libs/@local/hashql/eval/tests/ui/postgres/authorization/policy/algebra_constrained_permits_only.snap New snapshot for constrained-permits-only algebra result.
libs/@local/hashql/eval/tests/ui/postgres/authorization/policy/algebra_blank_permit_with_forbids.snap New snapshot for blank-permit + forbids algebra result.
libs/@local/hashql/eval/tests/ui/postgres/authorization/integration/patch_with_policy_and_protection.snap New integration snapshot showing both WHERE graft + property masking.
libs/@local/hashql/eval/tests/ui/postgres/authorization/integration/patch_protection_with_type_base_urls.snap New integration snapshot ensuring auth joins are in-scope for masking.
libs/@local/hashql/eval/tests/ui/postgres/authorization/integration/patch_instance_admin_bypasses_protection.snap New integration snapshot for instance-admin bypass of masking.
libs/@local/hashql/eval/tests/ui/postgres/authorization/integration/patch_blank_permit_no_protection.snap New integration snapshot for minimal patching on allow-all/no mask.
libs/@local/hashql/eval/tests/ui/postgres/authorization/integration/patch_blank_forbid_denies_all.snap New integration snapshot for deny-all blank forbid with defense-in-depth masking.
libs/@local/hashql/eval/tests/ui/postgres/authorization/.spec.toml Mark authorization UI snapshot suite as skipped and name the suite.
libs/@local/hashql/eval/src/postgres/tests.rs Introduce shared Postgres compiler test fixture + SQL lint helper.
libs/@local/hashql/eval/src/postgres/projections.rs Rework entity_editions join into LATERAL subquery + add AuxiliaryProjections and auth join materialization.
libs/@local/hashql/eval/src/postgres/prepared.rs Add prepared query representation with patch-layer pipeline (HList/CPS) and query registry iteration helpers.
libs/@local/hashql/eval/src/postgres/parameters.rs Add AuxiliaryParameters for runtime policy/protection parameter binding.
libs/@local/hashql/eval/src/postgres/mod.rs Export new authorization + prepared query plumbing, and remove prior property-mask-on-compiler approach.
libs/@local/hashql/eval/src/postgres/filter/tests.rs Migrate filter tests to shared fixture and remove old property-mask test path.
libs/@local/hashql/eval/src/postgres/authorization/tests.rs Add mock authorization store + end-to-end patching tests and reports.
libs/@local/hashql/eval/src/postgres/authorization/protection/tests.rs Add unit tests and snapshots for protection lowering.
libs/@local/hashql/eval/src/postgres/authorization/protection/mod.rs Add property protection lowering into SQL mask expressions + join demands.
libs/@local/hashql/eval/src/postgres/authorization/policy/tests.rs Add unit tests and snapshots for policy translation + optimizations.
libs/@local/hashql/eval/src/postgres/authorization/policy/mod.rs Implement policy constraint/filter translation to SQL plus optimization batching.
libs/@local/hashql/eval/src/postgres/authorization/mod.rs Implement AuthorizationPatch patch layer (WHERE admission + projection masking graft).
libs/@local/hashql/eval/src/orchestrator/request/mod.rs Add iterator module for request parameter iteration.
libs/@local/hashql/eval/src/orchestrator/request/iter.rs Add ExactSizeIterator adapter for chaining compiled + auxiliary params.
libs/@local/hashql/eval/src/orchestrator/request/graph_read.rs Execute SQL with chained compiled + auxiliary parameters via query_raw.
libs/@local/hashql/eval/package.json Move Rust workspace deps from devDependencies to dependencies for eval package.
libs/@local/hashql/eval/Cargo.toml Adjust dependency grouping/ordering; ensure auth/store deps are available where needed.
libs/@local/graph/type-fetcher/src/store.rs Add AsRef<T> forwarding impl to satisfy new REST store bounds.
libs/@local/graph/store/src/filter/protection.rs Introduce PropertyFilterEntityQueryPath and narrow protection filter path types.
libs/@local/graph/store/src/filter/parameter.rs Update conversions to handle new PropertyFilterExpressionList type.
libs/@local/graph/store/src/filter/mod.rs Update property-filter-to-entity-filter conversion to use new path type.
libs/@local/graph/postgres-store/src/store/postgres/query/table.rs Add EntityEditions::ALL for explicit projection selection in LATERAL editions subquery.
libs/@local/graph/postgres-store/src/store/postgres/query/expression/table_reference.rs Add TableName::from_table helper for const construction.
libs/@local/graph/postgres-store/src/store/postgres/query/expression/conditional.rs Add array_positions() SQL function support and uuid PostgresType.
libs/@local/graph/postgres-store/src/store/postgres/mod.rs Make filter_protection an Arc and export a PostgresClient alias.
libs/@local/graph/postgres-store/src/lib.rs Enable additional nightly features used by new const helpers/arrays.
libs/@local/graph/api/src/rest/mod.rs Wire HashQL routes into REST router and pass filter_protection via extensions; update store bounds to provide Postgres client access.
libs/@local/graph/api/src/rest/hashql/mod.rs Require authenticated actor header, build PolicyComponents, apply AuthorizationPatch, and execute via store pool.
libs/@local/graph/api/src/rest/hashql/error.rs Improve diagnostics for store acquire and auth-context creation (actor-not-found vs infra).
libs/@local/graph/api/src/rest/hashql/compile.rs Collect per-query action permissions from compiled artifacts to drive policy resolution.
libs/@local/graph/api/openapi/openapi.json Add required X-Authenticated-User-Actor-Id header to HashQL endpoint spec.
apps/hash-graph/src/subcommand/server.rs Pass filter_protection into REST dependencies and update store bounds/types accordingly.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread libs/@local/hashql/eval/src/orchestrator/request/iter.rs
Comment thread libs/@local/hashql/eval/src/postgres/projections.rs
Comment thread libs/@local/hashql/eval/src/postgres/projections.rs
Comment on lines +599 to +602
/// This traverses the right spine of `CrossJoin` nodes to find the
/// insertion point (the innermost non-LATERAL join tree), appends
/// authorization joins there, and reassembles the LATERAL chain on top.
pub(crate) fn build_joins(&self, mut from: FromItem<'static>) -> FromItem<'static> {
Comment thread libs/@local/graph/store/src/filter/protection.rs Outdated
@indietyp indietyp force-pushed the bm/be-71-hashql-consider-merging-ast-stages branch from 7a01031 to df982ed Compare June 23, 2026 09:41
@indietyp indietyp force-pushed the bm/be-538-hashql-permission-system-integration branch from 81f4f86 to 0f60303 Compare June 23, 2026 09:41
Copilot AI review requested due to automatic review settings June 23, 2026 09:52
@indietyp indietyp force-pushed the bm/be-71-hashql-consider-merging-ast-stages branch from df982ed to ea95719 Compare June 23, 2026 09:52
@indietyp indietyp force-pushed the bm/be-538-hashql-permission-system-integration branch from 0f60303 to 5a0812f Compare June 23, 2026 09:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 78 out of 78 changed files in this pull request and generated 5 comments.

Comment thread libs/@local/hashql/eval/src/postgres/projections.rs
Comment thread libs/@local/hashql/eval/src/postgres/projections.rs
Comment thread libs/@local/hashql/eval/src/postgres/projections.rs
Comment thread libs/@local/hashql/eval/src/postgres/projections.rs
Comment thread libs/@local/graph/store/src/filter/protection.rs Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/apps > hash* Affects HASH (a `hash-*` app) area/apps > hash-graph area/apps area/libs Relates to first-party libraries/crates/packages (area) area/tests New or updated tests type/eng > backend Owned by the @backend team

Development

Successfully merging this pull request may close these issues.

3 participants