Task: Answer the registration policy and land the account it describes

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 door reads one policy before it offers a form, and the server cannot answer it yet. iam.v1.auth.registration-policy is a new unauthenticated read that returns the deployment's switch, whether a registration needs authorisation, the tenant a registration lands in, the party and role it receives, and whether the result is usable at once.

Answering it needs four things the tree does not have:

  1. The tenant an address names. The handler must parse the hostname, look the tenant up by it, and fall back to the tenant flagged is_registration_default. Today neither a sign-up nor a sign-in request carries a hostname, and the sign-up path lands every account in the system tenant by omission.
  2. The three defaults. A flag, is_registration_default, on the tenant, the party and the role, each with a partial unique index over the live rows, so a default points at a row that exists and only one row can hold it.
  3. The pending state. A second value beside active on a new account_status lookup, held by an account that has no party yet.
  4. A code on every refusal. login_response gains error_code, and the sign-in path refuses a pending account with its own code before it refuses a party-less one.

The policy target state is registration and onboarding defaults; the interactions are the entry journeys.

The door's screen, the BFF route and the settings write that turns the switch on are not in this task: this is the server the screen will call.

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-30

3. Acceptance

  • iam.v1.auth.registration-policy answers, unauthenticated, with signups_enabled, authorization_required, the resolved tenant, the nominated party and role, usable_now, and a reason when it cannot.
  • The tenant is resolved from the hostname on the request; when the address names no tenant the tenant flagged is_registration_default is used, and when neither exists the read refuses with no_registration_destination rather than naming the system tenant.
  • A registration lands in the resolved tenant, not the system tenant, and carries the nominated role.
  • An account whose tenant nominates no default party is created with account_status pending; one whose tenant nominates the defaults is active and receives the association at once.
  • login_response carries error_code, and the sign-in path refuses a pending account with account_pending before it refuses a party-less one.
  • Every model change is regenerated across the layers its entity drives, and the changed entity suite passes.

4. Plan

  1. Model and SQL first, in the order the references require: the account_status lookup, then is_registration_default on the tenant, the party and the role with their partial unique indexes, then account.account_status. Regenerate each entity with compass codegen entity generate.
  2. Add the hostname to signup_request, and the destination fields, the status and the error_code to signup_response; add error_code to login_response. Regenerate both operation models.
  3. Teach signup_service to take a resolved context and the destination, so the account lands in the tenant the address names with the status the defaults allow.
  4. Write the registration-policy handler over the settings service and the three default reads, and wire it in the registrar.
  5. Give every sign-in refusal a code and refuse the pending state.
  6. Test: the policy read's destination rule, the pending transition, and the refusal codes.

5. Measurements

Every number this task claims about a tree, with the command that produced it and the commit it was measured at. A number without both is not a measurement, because a clean-up record describes a tree and trunk moves under a branch: a consumer count, a gate's component count and a suite's size all change with no code changing, and the reader of a bare number cannot tell which tree it came from. Where the raw output matters, put it in an evidence/ file beside the task and name that file in the Command column.

What Value Command Commit
Entities gaining is_registration_default 3 (tenant, party, role) grep -rln is_registration_default projects/*/modeling/ ae3486b951
New lookup entities 1 (account_status, seeded with 2 rows) grep -c "INSERT 0 2" against the recreate log ae3486b951
Refusal codes added to the vocabulary 7 grep -c "0x00" projects/ores.utility/include/ores.utility/serialization/error_code.hpp 525d95fe07
"Sign-up decision" cases 8 cases, 29 assertions, all pass ores.iam.core.tests "[signup]" 525d95fe07
IAM core suite 163 cases, 417 assertions, all pass ores.iam.core.tests 525d95fe07
IAM shell suite 313 cases, 823 assertions, all pass ores.shell.iam.tests ae3486b951
IAM component drift no drift check_component_drift.py --component iam ae3486b951

6. Notes

  • The policy read is separate from iam.v1.bootstrap.status: one answers whether the deployment has been set up, the other what it offers a person who is not in it. They change for different reasons, so a client that wants one rarely wants the other.
  • The flags are read at system scope, because system.user_signups is the deployment's answer and not a tenant's. The three defaults are read in the resolved tenant's scope, because that is where the flagged rows live.
  • The settings write is a later task. Until it exists the switch can only be turned on by seeding it, which is the setup journeys' job.

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 server can answer the policy the door asks, resolve the tenant an address names, and land a registration in the state the defaults allow. The work came in two commits, model layer first and behaviour second, because the second reads what the first gives a shape to.

The model layer (ae3486b951). The tenant, the party and the role each carry is_registration_default, at most one live row per tenant by a partial unique index. The flag sits on the row it points at, not on the tenant, because row-level security decides who may write that row: the tenant administrator owns their parties and roles, the super administrator owns the tenant table. A new account_status lookup carries active and pending beside the account type pattern, and the account holds one. Two operation models gained the fields a refusal or a confirmation needs, and a third, registration_policy_messages, declares the read itself. The "which row is flagged" reads are paste blocks on the party and role models, so regeneration keeps them, and a hand-written lookup beside the hostname read answers the same question for the tenant.

The behaviour (525d95fe07). iam.v1.auth.registration-policy reads the deployment's switch and approval flag at system scope, resolves the tenant from the address the request carries, and answers the destination and the two defaults. A deployment with no destination refuses with no_registration_destination rather than landing the account in the system tenant; one that nominates no role refuses with no_default_role. Both decisions are functions split from the reads, which is what makes them testable without a database. The live sign-up path resolves the tenant before it creates anything, registers through the service that validates the username, the email and the password, grants the nominated role, and writes the association when one is nominated. Sign-in answers a code on every refusal, and a pending account is refused before a party-less one.

Verified. ores.iam.core.tests builds and its full suite passes, 163 cases and 417 assertions; the eight new decision cases account for 29 of them. ores.shell.iam.tests passes, 313 cases and 823 assertions, including the two command units the new models generated. ores.shell.exe links, which is what proves the two new command units are registered. The iam component dry-runs clean. The protocol twin, shell menu collision and route authentication checkers pass.

The strongest check is the data layer: compass db recreate -y -k rebuilt the environment from the generated SQL and the log shows the new table created and seeded with the two account statuses, which is what the integration half needs to run at all. Before the recreate the IAM service refused to start with column "account_status" does not exist, which is the drift the bearings had already flagged and the reason the previous suite carried failures.

Four things this task did not do, all recorded rather than hidden:

  • The settings write. system.user_signups can still only be turned on by seeding it. The read is real; the permission-gated write pair is the next task, and until it exists the super administrator's kill switch is honoured but not settable through a screen.
  • The BFF and the browser. The policy read has no HTTP route and no client. The route, the hostname forwarding and the dialogs are the door's screen task. The read is reachable over NATS now, which is what the shell command and the core suite exercise.
  • bootstrap_mode as a sign-in refusal. The target state lists it, but no login path refuses on bootstrap mode today; the handler reports it as tenant_bootstrap_mode and admits the sign-in. Rather than claim a code that no refusal produces, the code is not set.
  • An unexpected sign-in failure keeps its sentence and no code. No documented code means "the server failed", and inventing one would put a lie on the wire.

The approval workflow behind system.signup_requires_authorization stays out of scope, as the target state says: the flag closes the door and the read says why, and the approval itself belongs to ores.workflow.

Emacs 29.3 (Org mode 9.6.15)