feat: complete per-user API keys — ready for main (v5.0.0)#25
Merged
Conversation
Replaces the single shared API key with a keystore: 'api_keys' block in
the credentials YAML mapping sha256-hex(token) -> {id: <contributor>}.
The legacy single 'api_key' still works (folds to id "owner") for
back-compat. Auth disabled only when no keys at all are configured
(dev mode preserved). Malformed keystore fails closed (server refuses
to start).
The auth middleware resolves the bearer token to a contributor id
(sha256 + constant lookup; never returns "unknown") and injects it into
the ASGI scope.
POST /events stamps the authenticated id into the event as 'created_by',
OVERWRITING any client-supplied value (anti-spoofing), persisted into
the durable queue line.
The graph write stamps 'created_by' WRITE-ONCE via ON CREATE SET on both
node-write paths (Session inline MERGE and the universal _NODE_MERGE_CYPHER),
never as part of the MERGE key — node identity/uniqueness constraints
unchanged. Adds a non-unique idx_session_created_by index.
Authenticate-and-stamp only (no roles in v1); workspace remains a client
label. Zero client change. Historical nodes keep null created_by.
Verified end-to-end against an isolated instance: a peer's lying
'created_by' is overwritten by the server (anti-spoof), created_by is
immutable across a second writer (write-once), and a bogus token gets 401
with nothing written. 1353 unit tests pass; ruff/pyright clean.
🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…-once, edge provenance, doc-driven setup
Completes the per-user-API-keys feature (stacks on the keystore foundation) so it
is mergeable and trustworthy end-to-end:
- Auth fail-closed: reject empty `api_keys: {}` (only omitted/null disables auth);
validate digests are 64 lowercase hex (normalize uppercase); reject blank ids.
- Write-once made structural: `created_by` is excluded from node props so
`SET n += row.props` can never clobber the `ON CREATE SET` stamp.
- Edge/relationship provenance (v1): write-once `ON CREATE SET r.created_by`;
bare endpoint placeholder nodes stay null (no cross-session mis-attribution);
intentionally not indexed in v1.
- Session-ownership invariant (one contributor per session) made observable:
a conflicting created_by on worker reuse logs ERROR and preserves the bound id
(never silently overwrites, never crashes ingest); test-enforced.
- Removed the `init` subcommand (it emitted the legacy single-key format); the
Docker auto-bootstrap now emits an `api_keys` keystore and prints the raw token
once. Legacy single `api_key` still authenticates (back-compat).
- Documentation: new docs/managing-api-keys.md and docs/designs/per-user-api-keys.md;
fixed the misleading "per-peer keys (future)" note; updated README, example
config, peer-onboarding, service-setup, and the repo AGENTS.md.
- Tests: real (isolated, ephemeral) Neo4j gates for anti-spoof, node write-once,
and edge first-asserter-wins; real-path crash-recovery test (replaces the inline
reimplementation) with truncated/garbage-line robustness; fixed a pre-existing
test spy whose arity broke when created_by was added to _write_batch.
- Bump server version 4.0.2 -> 5.0.0 (breaking: init removed, empty api_keys errors).
Verified: full non-Docker suite 1409 passed; isolated-Neo4j suite 79 passed;
ruff + pyright clean.
Generated with [Amplifier](https://github.com/microsoft/amplifier)
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Complete per-user API keys — ready for
main(v5.0.0)This PR carries @bkrabach's keystore foundation commit (
7854776, from #24) plus the completion work required to make the feature mergeable, trustworthy end-to-end, and operable by humans and agents. It supersedes #24.What the feature does
Replaces the single shared API key with a keystore (
api_keys: { sha256_hex(token) -> { id: <contributor> } }) so every graph write carries authenticated, spoof-proof contributor attribution. The server stores only token digests; peers sendAuthorization: Bearer <raw-token>. Legacy singleapi_keystill authenticates (folds toowner) for back-compat.Completion changes (on top of #24)
api_keys: {}is now a hard startup error (only omitted/nulldisables auth); digests validated as 64 lowercase hex (uppercase normalized); blank contributor ids rejected.created_byis excluded from node props, soSET n += row.propscan never clobber theON CREATE SETstamp.ON CREATE SET r.created_by; bare endpoint placeholder nodes intentionally left null (avoids cross-session mis-attribution); not indexed in v1.session_idis load-bearing; a conflictingcreated_byon worker reuse logsERRORand preserves the bound id (never silently overwrites, never crashes ingest). Documented + test-enforced.initsubcommand (it emitted the legacy single-key format); the Docker auto-bootstrap now emits anapi_keyskeystore and prints the raw token once. Setup is doc-driven for humans and agents.docs/managing-api-keys.mdanddocs/designs/per-user-api-keys.md; corrected the "per-peer keys (future)" note (it's shipped); updated README, example config, peer-onboarding, service-setup, and the repoAGENTS.md.4.0.2 → 5.0.0(breaking:initremoved, emptyapi_keyserrors).Note: a pre-existing bug fixed here
The new real (isolated, ephemeral) Neo4j gates surfaced that
7854776's_write_batchsignature change (addingcreated_by) wasn't propagated to two test spies intests/neo4j/test_flush_lock_ordering.py— they fail with an arity error under Docker-backed CI. Fixed; that suite is green.Verification
ruff+pyrightclean.🤖 Generated with Amplifier