Task: Make the registration gate real
Table of Contents
This page documents a task in the Implement the entry journeys in ores.web story. It captures the goal, current status, acceptance, and any notes or results.
1. Goal
The live sign-up path reads the deployment's switch. Today iam.v1.auth.signup
serves through account_setup_service, which reads neither system.user_signups
nor system.signup_requires_authorization; the service that does read them has no
caller outside its own two files. A deployment that has turned self-registration
off therefore still accepts registrations, over NATS and over the HTTP gateway,
and the refusal a screen receives is a sentence with no code because the response
has no field to carry one.
This is the first implementation task of the story because it closes an open door. The rest of the change table in registration and onboarding defaults — the tenant resolution, the defaults, the waiting state — is built on a path that has stopped admitting people.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Implement the entry journeys in ores.web |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-09-29 |
3. Acceptance
signup_responsecarrieserror_code, generated from the model rather than hand-written into a protocol header.- The live handler refuses with
signup_disabledwhen the deployment's switch is off, before it creates anything, and the refusal is asserted by a test that turns the switch off. - It refuses with
signup_requires_authorizationwhen the authorisation flag is on, because nothing performs that approval yet, and the refusal says so. - The switch is read at system scope, and the registered default stays
false, so a deployment nobody has asked refuses registration. - The HTTP gateway's answer carries the same code, since it forwards the same message.
- Every address of the changed model is regenerated: the C++ protocol, the generated TypeScript, and the HTTP route and its OpenAPI entry.
- The flags' seeded descriptions no longer say the behaviour is unimplemented, unless it still is.
4. Plan
- Model first: add
error_codetosignup_responsein ores.iam.signup_messages, then regenerate, because a handler that answers with a code needs a field to answer with. - Gate the handler: read
system.user_signupsandsystem.signup_requires_authorizationinauth_handler::signupbefore the service is called, and answer with the code and a sentence. - Decide the fate of the dead flag-aware service: either it becomes the live path, or it is deleted. Leaving it is what made the door look closed.
- Test the refusals in the IAM core suite: switch off, authorisation on, and the happy path with the switch on.
- Update the seeded descriptions, and the HTTP recipe that captures the refusal, so the record matches what the code does.
5. Measurements
| What | Value | Command | Commit |
|---|---|---|---|
| Callers of the flag-aware sign-up service | 0, outside its own header and source | grep -rln signup_service projects/ --include=*.cpp --include=*.hpp |
67493fae8e |
| Refusal codes the sign-up response carries | 0 | projects/ores.iam/modeling/ores.iam.signup_messages.org |
67493fae8e |
| Addresses of the sign-up response to regenerate | to be counted when the model changes | grep -rln "signup_response" projects/ --include=*.hpp --include=*.ts --include=*.cpp |
67493fae8e |
6. Notes
- Why the gate comes before the rest: every other change in the target state adds a refusal or a state to a path that is currently open to anyone. Closing it first means the later work is built on a path that refuses by default.
- The authorisation flag is a deliberate dead end for now. Nothing performs the approval, so the honest behaviour is to close the door and say so, which is what the target state records; the workflow itself belongs to
ores.workflow.
7. Test Scenarios
Manual QA scenarios (scaffolded via compass add test_scenario)
that verify this task. Link
new ones here as they're created; the scenario doc itself links back
via its "Verifies task" field.
| Scenario | State | Notes |
|---|---|---|
8. PRs
| PR | Title |
|---|---|
| #2423 | [iam,web] Implement the entry journeys' registration door |
9. Review
| Comment summary | File | Decision | Notes |
|---|---|---|---|
10. Result
The gate is real. iam.v1.auth.signup reads system.user_signups and
system.signup_requires_authorization at system scope before it creates anything,
and refuses with signup_disabled or signup_requires_authorization and the code
the response now carries. A settings read that fails closes the door, because a
deployment that cannot say whether it accepts registrations does not accept them.
The refusal is recorded in the auth event log like every other sign-up failure.
The decision is a function of the two flags, and the read is a second overload
that supplies them. That split is what makes the test possible: the seeded default
is false, so a test against the read could only ever see one branch, and nothing
can turn the switch on until the settings write exists.
Verified. ores.iam.core.tests builds, and the four cases in
messaging_auth_registration_gate_tests.cpp pass: the switch off, the switch off
with the authorisation flag on, the door open, and that the authorisation flag
cannot change an answer the switch has already given. The test file is in the
generated build list, and the reachability gate counts it: 7743 cases in 953
sources.
Two things this task did not do, both recorded rather than hidden:
- A refusal from the account service still answers
invalid_request. The service throws sentences, so the validation refusals cannot carry their real codes until it returns them; that belongs with the sign-up screen, which is what needs to branch on them. - The
http/http/signuprecipe's captured answer predates the gate and names a message nothing produces. Re-capturing it is the HTTP recipe harness's job, and hand-writing a capture would be a record of a call nobody made.
The dead flag-aware service is no longer dead. The plan left open whether it
should become the live path or be deleted, and the decision is adoption: it is
the only thing in the tree that checks the username and the email for uniqueness
and the password against the policy, and the administrator path the sign-up path
used checks none of them. The live path now registers through it, so a validation
refusal answers with its own code rather than invalid_request.
That leaves the administrator path alone, deliberately. Creating an administrator is a different act with a different policy, and adopting the self-registration service there would change it.