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_response carries error_code, generated from the model rather than hand-written into a protocol header.
  • The live handler refuses with signup_disabled when 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_authorization when 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

  1. Model first: add error_code to signup_response in ores.iam.signup_messages, then regenerate, because a handler that answers with a code needs a field to answer with.
  2. Gate the handler: read system.user_signups and system.signup_requires_authorization in auth_handler::signup before the service is called, and answer with the code and a sentence.
  3. 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.
  4. Test the refusals in the IAM core suite: switch off, authorisation on, and the happy path with the switch on.
  5. 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/signup recipe'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.

Emacs 29.3 (Org mode 9.6.15)