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:
- 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. - 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. - The pending state. A second value beside
activeon a newaccount_statuslookup, held by an account that has no party yet. - A code on every refusal.
login_responsegainserror_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-policyanswers, unauthenticated, withsignups_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_defaultis used, and when neither exists the read refuses withno_registration_destinationrather 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_statuspending; one whose tenant nominates the defaults isactiveand receives the association at once. login_responsecarrieserror_code, and the sign-in path refuses a pending account withaccount_pendingbefore 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
- Model and SQL first, in the order the references require: the
account_statuslookup, thenis_registration_defaulton the tenant, the party and the role with their partial unique indexes, thenaccount.account_status. Regenerate each entity withcompass codegen entity generate. - Add the hostname to
signup_request, and the destination fields, the status and theerror_codetosignup_response; adderror_codetologin_response. Regenerate both operation models. - Teach
signup_serviceto take a resolved context and the destination, so the account lands in the tenant the address names with the status the defaults allow. - Write the registration-policy handler over the settings service and the three default reads, and wire it in the registrar.
- Give every sign-in refusal a code and refuse the pending state.
- 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_signupsis 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_signupscan 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_modeas a sign-in refusal. The target state lists it, but no login path refuses on bootstrap mode today; the handler reports it astenant_bootstrap_modeand 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.