M1 acceptance gate: persisted client_credentials client gets a token (#25) - #113
Merged
Merged
Conversation
…urvives a process restart Closes #25. Closes #18. Two OS processes, not two logical phases in one: `app/sms-gateway/tests/m1_acceptance_gate_live_postgres.rs` spawns the real `sms-gateway serve` binary via `CARGO_BIN_EXE_sms-gateway`, provisions a service account through the real `provision_app_client` procedure against the same live Postgres, SIGKILLs and reaps that process, then spawns a genuinely fresh one and proves a real private_key_jwt token exchange plus a real sendMessage call both work against it — the only way to actually falsify the GrantType bug #25 exists to guard against, per its own text ("An in-memory-only test passes with the GrantType bug present and is therefore worthless here"). Separately asserts a hand-signed, test-only token carrying the developer role's real perms (no provider:* permission) is refused on PATCH /providers/{id}, naming provider:update — the RBAC half of the gate. Deviates from the ticket's literal wording in one place, documented in the test file's own module doc: provisioning is a direct Procedures::provision_app_client call rather than an HTTP request to process #1, because provisionAppClient's own @Allow requires hasRole('owner')/hasRole('admin') and GatewayAuth never mints either role for a real token in this deployment (no human-login path exists yet) — the same conclusion rbac_layer2_live_postgres.rs already reached for PATCH /providers/{id}. What the gate actually cares about — persistence surviving a real process boundary, and a real token exchange plus a real protected call against the restarted process — is still exercised for real over HTTP. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
3 tasks
stephane-segning
added a commit
that referenced
this pull request
Aug 3, 2026
The doc still read "M1 is still open: #23, #24, #25" after all three merged and closed epic #18. Replaced the stale line with a writeup matching the file's existing density: what each PR actually built, the client-secret-vs-private_key_jwt schema conflict caught and resolved in permission-literal bug Lightbridge caught in #24, and #25's two-genuinely- separate-OS-processes acceptance gate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
stephane-segning
added a commit
that referenced
this pull request
Aug 3, 2026
…) (#115) The doc still read "M1 is still open: #23, #24, #25" after all three merged and closed epic #18. Replaced the stale line with a writeup matching the file's existing density: what each PR actually built, the client-secret-vs-private_key_jwt schema conflict caught and resolved in permission-literal bug Lightbridge caught in #24, and #25's two-genuinely- separate-OS-processes acceptance gate. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.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.
Summary
app/sms-gateway/tests/m1_acceptance_gate_live_postgres.rs— Gate: a persisted client_credentials client gets a token #25's own acceptance gate: provisions a service account through the realprovision_app_clientprocedure, kills the OS process that observed the provisioning, spawns a genuinely fresh one, and proves a realprivate_key_jwttoken exchange plus a real protected route call both work against it. Separately proves a hand-signed, test-onlydeveloper-perms token is refused onPATCH /providers/{id}.app/sms-gateway/Cargo.toml— new[dev-dependencies](base64,chrono,jsonwebtoken,rand,reqwest,rsa,serde,serde_json) this test needs; all already pinned versions used elsewhere in the workspace, no new dependency trees.Intent
Closes #25 ("Gate: a persisted
client_credentialsclient gets a token"), the closing ticket of epic #18 (M1 — sms-auth). Closes #18: its own "Stories" checklist is #19–#25, and #19/#20/#21/#22/#23/#24 are all already closed (#23 as #111, #24 as #112) — #25 was the last one open, and the epic's stated blocker, #87, closed earlier.Source of truth: #25 (quoting its own text): "Provision a service account through the real provisioning path so it is persisted, restart the process so nothing is served from memory, then exchange
client_id+ [credential] for a token and call a route with it. An in-memory-only test passes with theGrantTypebug present and is therefore worthless here. Second half: assert adeveloper-role token is refused on aprovider:writeroute."Scope
sms-gateway serveitself requires to start (an activeOauthSigningKey; aProviderrow keyedorange_cm,resolve_provider_row_id's own requirement) plus whatsendMessageneeds downstream (an approvedSenderId).sms-gateway serveas process Add milestone 0 crates: sms-encoding and sms-msisdn #1 viaenv!("CARGO_BIN_EXE_sms-gateway")(only available to an integration test inside thesms-gatewaypackage itself, which is why this test lives here rather than alongside this workspace's other live suites insms-api/sms-auth).Procedures::provision_app_client— real RSA keygen, real transactional Postgres writes (AppClient+OauthClient), against the same live database process Add milestone 0 crates: sms-encoding and sms-msisdn #1 is itself serving from.Child::kill()(SIGKILL on Unix, not a graceful shutdown) thenChild::wait(), blocking until the OS has genuinely reaped it.sms-gateway serveagain as process Fix mermaid diagrams CI job: pass --no-sandbox to mmdc #2 — a different PID, zero shared memory with process Add milestone 0 crates: sms-encoding and sms-msisdn #1, same database, same port.private_key_jwtclient assertion with the persisted private key, exchanges it at process Fix mermaid diagrams CI job: pass --no-sandbox to mmdc #2's own/token, and callssendMessagewith the resulting access token — succeeds only because the client row is actually in Postgres.developerrole's realperms(§5.2 of the design doc:app:read,webhook:manage,message:read,message:send— noprovider:*permission), signed with the same active OP signing key soGatewayAuth's real RS256/JWKS validation accepts it structurally, and assertsPATCH /providers/{id}returns403namingprovider:update.Procedures::provision_app_clientdirectly rather than making an HTTP request to process Add milestone 0 crates: sms-encoding and sms-msisdn #1.provisionAppClient's own@allowinschema.cstackishasRole('owner') || hasRole('admin'), andGatewayAuth::authenticateunconditionally setsrole: "app"for every realclient_credentialstoken this deployment ever issues — there is no human-login path to obtain anowner/admin-role token from at all.rbac_layer2_live_postgres.rs's own module doc already reaches the identical conclusion forPATCH /providers/{id}, andprovision_app_client_live_postgres.rsalready made the same choice for the same reason. What the ticket's own stated purpose actually needs — persistence surviving a real process boundary, and a real token exchange plus a real protected call against the restarted process — is still exercised for real over HTTP, against a process that never observed the provisioning call.perms-bearing token (sms_auth::opnever sets one), so there is no real OAuth flow to obtain adeveloper-shaped token from either.Verification
Then the actual gate, live, twice in a row against a fresh
postgres:16container (migrated withci/apply-migrations.sh):Both runs passed. Confirmed from the test's own PID/exit-status logging that it genuinely spawns and kills two separate OS processes, not two logical phases in one:
unix_wait_status(9)on both is SIGKILL, confirmingkill_and_waitgenuinely hard-kills rather than requesting a graceful shutdown. A second full run reused the idempotentorange_cmProviderfixture cleanly and passed identically, confirming the test is safe to re-run against a non-reset database (this workspace's existing live-suite convention).Screenshots/Evidence
N/A — a backend integration test with no UI surface. The PID/exit-status transcript above is the evidence that matters here (the two-process requirement), reproduced in full in this PR's own description above rather than a screenshot.
Risk Assessment
Low. Additive only: one new
#[ignore]d test file (never runs in a normalcargo test/CI unit-test pass, only under an explicit-- --ignoredlive run against a real Postgres) and new[dev-dependencies]entries scoped toapp/sms-gateway's own test target. No production code path changes —app/sms-gateway/src/main.rs,op.rs, and everycrates/file are untouched.AI Usage Declaration
This PR was implemented by Claude Code (Claude Sonnet 5) under the repo owner's direction, working from the ticket's own text (issue #25) plus the epic's accumulated findings (#18 and its linked PRs).
postgres:16Docker container (not simulated, not assumed) — twice in a row — and confirmed both runs pass, including reading the PID/exit-status output to confirm two distinct OS processes were genuinely spawned and SIGKILLed, not approximated within one process.developer-perms token in step 7 is a deliberate stand-in for a future human-login path, not a real issuance route, and (b) provisioning in step 3 is a direct procedure call rather than a literal HTTP request to process Add milestone 0 crates: sms-encoding and sms-msisdn #1, for the documented Layer-1-policy reason above — this is a correction to the ticket's own wording, not a silent weakening of what it asks for.Reviewer Focus
Procedures::provision_app_clientcall instead of HTTP to process Add milestone 0 crates: sms-encoding and sms-msisdn #1" deviation is an acceptable reading of Gate: a persisted client_credentials client gets a token #25's intent, given no owner/admin-role token is reachable over real HTTP in this deployment today (same conclusionrbac_layer2_live_postgres.rsalready documented for the RBAC negative case).sign_developer_stand_in_token's framing (hand-signed, explicitly not a real issuance path) is honest and clear enough, or should be hardened further (e.g. an even louder in-code warning) given it's the one piece of this test that doesn't go through a real OP flow.