BE-332: Add fail-closed request authentication to the Graph REST API - #9218
BE-332: Add fail-closed request authentication to the Graph REST API#9218TimDiekmann wants to merge 4 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #9218 +/- ##
==========================================
+ Coverage 59.63% 59.73% +0.09%
==========================================
Files 1420 1423 +3
Lines 138767 139067 +300
Branches 6555 6561 +6
==========================================
+ Hits 82753 83067 +314
+ Misses 54950 54934 -16
- Partials 1064 1066 +2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Merging this PR will degrade performance by 15.38%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ❌ | bit_matrix/dense/iter_row[64] |
140.8 ns | 170 ns | -17.16% |
| ❌ | bit_matrix/dense/iter_row[200] |
185.8 ns | 215 ns | -13.57% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing t/be-332-move-authentication-to-the-graph (0466c8f) with main (e28fe6f)1
Footnotes
PR SummaryCursor Bugbot is generating a summary for commit a766c47. Configure here. |
There was a problem hiding this comment.
Pull request overview
Adds centralized, fail-closed authentication to the Graph REST API through a pluggable provider abstraction.
Changes:
- Adds authentication providers, credential resolution, middleware, and actor extraction.
- Protects REST and HashQL routes while exempting bootstrap endpoints.
- Updates tests, dependency wiring, and dependency diagrams.
Reviewed changes
Copilot reviewed 34 out of 36 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
yarn.lock |
Updates workspace dependency resolution. |
tests/graph/test-data/rust/docs/dependency-diagram.mmd |
Updates dependency fixture. |
tests/graph/http/tests/hashql.http |
Tests HashQL authentication. |
libs/@local/temporal-client/docs/dependency-diagram.mmd |
Updates dependency diagram. |
libs/@local/status/rust/docs/dependency-diagram.mmd |
Updates dependency diagram. |
libs/@local/harpc/wire-protocol/docs/dependency-diagram.mmd |
Updates dependency diagram. |
libs/@local/harpc/types/docs/dependency-diagram.mmd |
Updates dependency diagram. |
libs/@local/graph/types/docs/dependency-diagram.mmd |
Updates dependency diagram. |
libs/@local/graph/temporal-versioning/docs/dependency-diagram.mmd |
Updates dependency diagram. |
libs/@local/graph/store/docs/dependency-diagram.mmd |
Updates dependency diagram. |
libs/@local/graph/authorization/docs/dependency-diagram.mmd |
Updates dependency diagram. |
libs/@local/graph/authentication/src/request.rs |
Implements credential resolution and errors. |
libs/@local/graph/authentication/src/provider.rs |
Defines authentication provider interface. |
libs/@local/graph/authentication/src/lib.rs |
Exposes authentication modules. |
libs/@local/graph/authentication/package.json |
Adds workspace dependencies. |
libs/@local/graph/authentication/docs/dependency-diagram.mmd |
Documents crate dependencies. |
libs/@local/graph/authentication/Cargo.toml |
Adds Rust dependencies. |
libs/@local/graph/api/src/rest/property_type.rs |
Uses resolved actor extraction. |
libs/@local/graph/api/src/rest/principal.rs |
Uses resolved actor extraction. |
libs/@local/graph/api/src/rest/permissions.rs |
Uses resolved actor extraction. |
libs/@local/graph/api/src/rest/mod.rs |
Installs global authentication middleware. |
libs/@local/graph/api/src/rest/http_tracing_layer.rs |
Records middleware-resolved actors. |
libs/@local/graph/api/src/rest/entity/query/mod.rs |
Uses resolved actor extraction. |
libs/@local/graph/api/src/rest/entity/mod.rs |
Uses resolved actor extraction. |
libs/@local/graph/api/src/rest/entity_type.rs |
Uses resolved actor extraction. |
libs/@local/graph/api/src/rest/data_type.rs |
Uses resolved actor extraction. |
libs/@local/graph/api/src/rest/auth.rs |
Adds Axum authentication bindings. |
libs/@local/graph/api/src/rest/admin.rs |
Adds admin actor-header middleware. |
libs/@local/graph/api/package.json |
Adds authentication workspace dependency. |
libs/@local/graph/api/docs/dependency-diagram.mmd |
Updates API dependency diagram. |
libs/@local/graph/api/Cargo.toml |
Adds authentication and test dependencies. |
libs/@local/codegen/docs/dependency-diagram.mmd |
Updates dependency diagram. |
libs/@local/codec/docs/dependency-diagram.mmd |
Updates dependency diagram. |
libs/@blockprotocol/type-system/rust/docs/dependency-diagram.mmd |
Updates dependency diagram. |
Cargo.lock |
Updates Rust dependency lock data. |
apps/hash-graph/docs/dependency-diagram.mmd |
Updates application dependency diagram. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 34 out of 36 changed files in this pull request and generated no new comments.
Suppressed comments (1)
libs/@local/graph/api/src/rest/admin.rs:126
- In JWT mode this middleware does not know the authenticated admin actor: it records only the unverified actor-ID header.
AdminActorIdlater resolves the real user atadmin.rs:179-192but never updates the span, so a normal JWT-only request has noactor_entity_uuid, while a JWT request with a spoofed header is attributed to that spoofed UUID. Record the resolveduser_idin the JWT branch (overwriting any header-derived value), or move span recording to the point whereAdminActorIdis finalized.
.layer(axum::middleware::from_fn(auth::actor_id_header_middleware))
f022ce4 to
0466c8f
Compare
Benchmark results
|
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 2002 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 1002 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 3314 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 1527 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 2078 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 1033 | Flame Graph |
policy_resolution_medium
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 102 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 52 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 269 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 108 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 133 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 63 | Flame Graph |
policy_resolution_none
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 2 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 2 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 8 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 3 | Flame Graph |
policy_resolution_small
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 52 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 26 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 94 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 27 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 66 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 29 | Flame Graph |
read_scaling_complete
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id;one_depth | 1 entities | Flame Graph | |
| entity_by_id;one_depth | 10 entities | Flame Graph | |
| entity_by_id;one_depth | 25 entities | Flame Graph | |
| entity_by_id;one_depth | 5 entities | Flame Graph | |
| entity_by_id;one_depth | 50 entities | Flame Graph | |
| entity_by_id;two_depth | 1 entities | Flame Graph | |
| entity_by_id;two_depth | 10 entities | Flame Graph | |
| entity_by_id;two_depth | 25 entities | Flame Graph | |
| entity_by_id;two_depth | 5 entities | Flame Graph | |
| entity_by_id;two_depth | 50 entities | Flame Graph | |
| entity_by_id;zero_depth | 1 entities | Flame Graph | |
| entity_by_id;zero_depth | 10 entities | Flame Graph | |
| entity_by_id;zero_depth | 25 entities | Flame Graph | |
| entity_by_id;zero_depth | 5 entities | Flame Graph | |
| entity_by_id;zero_depth | 50 entities | Flame Graph |
read_scaling_linkless
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id | 1 entities | Flame Graph | |
| entity_by_id | 10 entities | Flame Graph | |
| entity_by_id | 100 entities | Flame Graph | |
| entity_by_id | 1000 entities | Flame Graph | |
| entity_by_id | 10000 entities | Flame Graph |
representative_read_entity
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/block/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/book/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/building/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/organization/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/page/v/2
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/person/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/playlist/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/song/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/uk-address/v/1
|
Flame Graph |
representative_read_entity_type
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| get_entity_type_by_id | Account ID: bf5a9ef5-dc3b-43cf-a291-6210c0321eba
|
Flame Graph |
representative_read_multiple_entities
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_property | traversal_paths=0 | 0 | |
| entity_by_property | traversal_paths=255 | 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=0 | 0 | |
| link_by_source_by_property | traversal_paths=255 | 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true |
scenarios
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| full_test | query-limited | Flame Graph | |
| full_test | query-unlimited | Flame Graph | |
| linked_queries | query-limited | Flame Graph | |
| linked_queries | query-unlimited | Flame Graph |
🌟 What is the purpose of this PR?
Makes authentication at the Graph REST API fail-closed and pluggable. Every route now rejects requests without credentials by default instead of each handler opting in, and credential verification is abstracted behind an
AuthenticationProvidertrait in thehash-graph-authenticationcrate so further providers (Kratos sessions, OAuth tokens) plug into the same pipeline in follow-up PRs.🔗 Related links
hash-graph-authenticationcrate #9209 set up thehash-graph-authenticationcrate this PR builds on🚫 Blocked by
Nothing.
🔍 What does this change?
hash-graph-authenticationgainsprovider::AuthenticationProvider— a provider owns both the recognition of its credentials in the request headers and their verification, returningAuthentication::{NotRecognized, Verified(ActorId), Rejected}— plusrequest::resolve_request_actor, which resolves a request to the acting principal: provider credential first, theX-Authenticated-User-Actor-Idheader second, and a recognized-but-rejected credential never falls back to the headerStaticAuthenticationProviderserves as the wired provider (NotRecognized, so all traffic authenticates via the actor-ID header exactly as before) and as the test doubleAuthenticatedActorIdextractor (renamed fromAuthenticatedUserHeader)POST /hashqlpreviously enforced no authentication and now requires it; the HTTP test suite pins this with a 401 test/policies/seed,/actors/machine/identifier/system/{identifier}) stay reachable without credentials — they run before any actor existsactor_entity_uuidon the request span for admin requestsPre-Merge Checklist 🚀
🚢 Has this modified a publishable library?
This PR:
📜 Does this require a change to the docs?
The changes in this PR:
🕸️ Does this require a change to the Turbo Graph?
The changes in this PR:
turbo.json's have been updated to reflect this@rust/hash-graph-api→@rust/hash-graph-authenticationedge is carried by the generatedpackage.jsondependencies (sync:turborepo); noturbo.jsonchanges are neededX-Authenticated-User-Actor-Idheader remains the transitional authentication path for internal services; its hardening is tracked in BE-714 (internal). Until then the Graph must not be exposed beyond the internal network, which matches the current deployment.POST /hashqlanymore (its spec declares no credential parameter). No caller exists today; the session provider follow-up supersedes this, so the client is left as is.route_layer).🐾 Next steps
AuthenticationProviderimplementation, prepared as a follow-up to keep this diff reviewable)🛡 What tests cover this?
hash-graph-authenticationunit tests: credential precedence, no-fallback on rejected credentials, header parsinghash-graph-apimiddleware tests (tower::oneshotharness): fail-closed rejection, bootstrap allowlist, extractor behavior with and without middleware, admin header middlewarehashql.httpadds an explicit 401 test for credential-less requests❓ How to test this?
yarn start:graphorcargo run --bin hash-graph --all-features -- server)curl -i http://127.0.0.1:4000/actors/machine/identifier/system/h→ 200 (bootstrap route)curl -i http://127.0.0.1:4000/actors/machine/identifier/h→ 401 (no credentials)-H "X-Authenticated-User-Actor-Id: <id from step 2>"→ 200cd tests/graph/http && sh test.shfor the full suite