BE-765: Tighten the authentication provider types and split the Kratos module - #9234
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 #9234 +/- ##
==========================================
+ Coverage 59.94% 59.96% +0.02%
==========================================
Files 1427 1428 +1
Lines 139949 139981 +32
Branches 6595 6597 +2
==========================================
+ Hits 83898 83946 +48
+ Misses 54977 54961 -16
Partials 1074 1074
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:
|
PR SummaryMedium Risk Overview
Actor resolution is centralized in The Kratos code splits into
Reviewed by Cursor Bugbot for commit 7565604. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Pull request overview
Refactors Graph authentication provider types and separates Kratos session handling without changing credential precedence or authentication behavior.
Changes:
- Replaces the provider decision enum with
ControlFlow<Result<…>>. - Moves user-actor resolution into
actor.rsand splits Kratos response handling from session verification. - Gates the static test provider behind
test-utilsand expands consolidated error-path tests.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
libs/@local/graph/authentication/src/request.rs |
Adapts provider outcomes and adds client-message safety tests. |
libs/@local/graph/authentication/src/provider.rs |
Simplifies the provider contract and gates test utilities. |
libs/@local/graph/authentication/src/kratos/session.rs |
Focuses the provider on session verification and typed user resolution. |
libs/@local/graph/authentication/src/kratos/mod.rs |
Adds shared Kratos metadata and response handling. |
libs/@local/graph/authentication/src/delegation.rs |
Adopts the simplified provider result type. |
libs/@local/graph/authentication/src/actor.rs |
Adds reusable user-actor resolution. |
libs/@local/graph/authentication/Cargo.toml |
Adds rstest and the test-utils feature. |
libs/@local/graph/api/Cargo.toml |
Enables authentication test utilities for API tests. |
Cargo.lock |
Records the authentication crate’s rstest dependency. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
a32f68f to
4b430c6
Compare
4b430c6 to
97543a9
Compare
97543a9 to
782bb1a
Compare
782bb1a to
ffb6ef7
Compare
…s module The providers carried types and a module layout from when there was only one of them. This lands the cleanup separately from the Cloudflare Access provider so both stay reviewable. `AuthenticationProvider` now breaks with a `Result` instead of a dedicated two-variant enum, which turns each provider's tail into a `map`. `resolve_user_actor` returns the `UserId` it already checked for, and moves to `actor.rs` since it holds no Kratos knowledge. The Kratos module splits into `mod` and `session`, with the HTTP status handling extracted into `read_response_body`. The session provider keeps its 401/403 check ahead of that call, so an expired session still reports as an invalid session rather than as a provider fault. Note one behaviour change: the identity metadata deserializes `graph_actor_id` as a `UserId`, so a value that is not an RFC 4122 UUID now fails the whoami parse instead of failing later at actor resolution.
The `test-utils` dev-dependency adds an edge from the API crate to the authentication crate, which the committed diagrams did not carry.
Each diagram is generated with the root's dependencies *and* dependents, so the new dev-dependency edge shows up in every crate whose diagram contains both the API and the authentication crate — not just in the two at its ends.
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?
The Graph's authentication providers carry types and a module layout from when there was only one provider. The next provider (Cloudflare Access, BE-761) touches the same places, so this lands the cleanup on its own to keep both changes reviewable apart.
No new capability, and no change to which credentials authenticate — with one deliberate exception noted under Known issues.
🔗 Related links
🚫 Blocked by
🔍 What does this change?
AuthenticationProviderbreaks with aResultrather than a dedicated two-variant enum.Continuestill means "no credential of mine here"; both outcomes still stop the chain, so a rejected credential never falls through to another provider. Each provider's tail collapses from a match to amap.resolve_user_actorreturns theUserIdit already checked for, instead of the widerActorId, and moves toactor.rs— it holds no Kratos knowledge.modandsession. The HTTP status handling, previously written out inline, becomesread_response_bodyinmod.rs, which makes it testable without spawning a server. The session provider keeps its 401/403 check ahead of that call, so an expired session still reports as an invalid session rather than as a provider fault.rstestcases, and the shared test helpers move next to the types they exercise.StaticAuthenticationProvidersits behind atest-utilsfeature, so the fake provider can no longer be constructed in a production build.Pre-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:
The identity metadata deserializes
graph_actor_idas aUserId, which validates the RFC 4122 version and variant. A value that fails that check now aborts the whoami parse and is reported as an invalid provider response, where it previously parsed and failed later at actor resolution as an unauthenticated request. The two differ in status code and log level.Actor IDs are generated as version 4 UUIDs and the nil UUID is explicitly still accepted, so this only reaches records provisioned by hand. Keeping the stricter type is deliberate: the same struct is what the Graph will write once identity provisioning moves over. If the error class turns out to matter operationally, it is a one-line type change.
🐾 Next steps
🛡 What tests cover this?
Existing coverage is preserved; the suite grew rather than shrank.
kratos/mod.rs, one case per rung, without a fake server.❓ How to test this?
cargo nextest run --package hash-graph-authentication --package hash-graph-api --all-featurescargo clippy --package hash-graph-authentication --package hash-graph-api --all-features --all-targetsAuthenticationErrorand observe that the crate stops compiling until it is handled and listed.