Task: Say which roles may be asked for

Table of Contents

This page documents a task in the Land the role request journey story. It captures the goal, current status, acceptance, and any notes or results.

1. Goal

A tenant administrator can say which roles a member may ask for. Today every role in the tenant is on offer: the screens filter the list and the ask operation refuses what it should, so the rule lives in two places and the tenant can state it in neither. Add is_requestable to ores.iam.role, in the shape of is_registration_default, and let the ask operation and the screens read it.

2. Status

Field Value
State DONE
Parent story Land the role request journey
Now Nothing.
Waiting on Nothing.
Next Nothing.
Last touched 2026-10-07

3. Acceptance

  • ores.iam.role carries is_requestable, in the shape and with the defaults of is_registration_default.
  • The field is projected across the whole stack: the model, the SQL, the C++ domain and repositories, the protocol twins, the BFF and the web screens.
  • A role the tenant has not made requestable cannot be asked for: the ask operation refuses it and the screen does not offer it.
  • The role screens let a tenant administrator set the flag.

4. Plan

The flag goes on the role row, in the shape of is_registration_default, because row-level security already decides who may write a role row and the tenant administrator is that person. Nothing new is needed to guard it.

It defaults true, so a role a tenant administrator creates is offered to the members without further ceremony, and the rule is stated by exception: the seed clears it on the service roles and on the two that grant everything, which are the roles the screens used to filter out by name. Defaulting false was rejected: it would silently withdraw every role in every existing tenant until an administrator opted each one back in, which is a different change from the one the journey asks for.

ores_iam_roles_upsert_fn takes the flag, so the seed names it in one place. create or replace does not replace a function whose argument list changed, so an existing database would keep the old three-argument version; the drop script covers it, because it drops by name and so removes every overload.

The rule leaves the screens. The picker filters on the flag rather than on a name ending in Service and a permission code of *, and the ask operation refuses a role the tenant has not offered with its own outcome code, so the refusal is a value rather than a sentence to match.

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
Roles the seed makes unaskable, the call sites it states that at 22 of 30 grep -c ", false);" projects/ores.sql/populate/iam/iam_roles_populate.sql (22), against 30 ores_iam_roles_upsert_fn calls 36f1396b9d
ACME roles the flag survives provisioning on 30, 22 false and 8 true compass sql -- -f .../evidence/requestable_roles.sql, kept as evidence/requestable_roles.txt 36f1396b9d
Roles the BFF offers a member over HTTP 8 of 30 curl ... /api/roles, kept as evidence/requestable_screen.txt 36f1396b9d
Files the entity projection touches 36 changed, 282 insertions git show --stat 36f1396b9d 36f1396b9d
Suites in the full run 94, of which 93 passed compass test run --preset linux-clang-debug-make 36f1396b9d
The one failure ores.database.tests, a pre-existing flake Re-run alone passes; the same binary passed 94 of 94 earlier the same day. Captured in the inbox. 36f1396b9d

6. Notes

create or replace function does not replace a function whose argument list changed: it adds an overload. ores_iam_roles_upsert_fn gained a fourth parameter, so an existing database would have kept the three-argument version alongside the new one and resolved a three-argument call to the old one, which writes no flag. Nothing in the tree can hit that in practice, because the seed runs on a database recreated from scratch, and drop/seed/upsert_functions_drop.sql drops the function by name and so removes every overload. The drop needed no change for that reason.

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
Verify a member may ask only for the roles the tenant offers BACKLOG The walk was run as evidence; the scenario is written for a tester to repeat it.

8. PRs

PR Title
#2754 [iam] Let a tenant say which roles a member may ask for

9. Review

Comment summary File Decision Notes
       

10. Result

Built and measured. The role carries is_requestable, the seed states it at twenty-two of the thirty roles it creates, and the tenant provisioner carries it to every tenant it makes. The rule left the screens: the picker reads the flag and the ask operation refuses a role the tenant has not offered with outcome denied and code not_requestable, so the refusal is a value rather than a sentence to match.

Proved on a database recreated from scratch and provisioned with ACME, in four walks kept beside this page. The member ashley.moore is refused IamService and allowed Operations, and the refusal leaves no request behind: evidence/requestable_ask.txt. The flag survives provisioning across all thirty of the tenant's roles, the twenty-two the seed clears it on and the eight it does not: evidence/requestable_roles.txt. The BFF passes the flag through to the browser and offers a member the same eight roles: evidence/requestable_screen.txt.

The fourth walk is the one a seed cannot show. The tenant administrator took Operations away through the browser's own route, and the member was then refused the role that had been accepted minutes earlier; it was put back, and the tenant was as it was: evidence/requestable_tenant_control.txt. The answer is the tenant's rather than the platform's.

C++ and the web both hold the behaviour: a repository round trip of a role written not askable, a BFF case that the flag reaches the wire write, and a web case that a role marked not askable is not offered even when its name and its permissions would have passed the old filter.

The suite ran 94 suites and 93 passed. The one failure is ores.database.tests, in a component this change does not touch, and it is a flake in the test's own design: the listener loss-window test kills the listener's backend and then asserts the listener has not yet reconnected, which nothing enforces. It passes on an immediate re-run and the same binary passed all 94 earlier the same day. It is captured in the inbox rather than worked around here.

Emacs 29.3 (Org mode 9.6.15)