Investigation: The routes and operations the credentials journeys need

Table of Contents

A point-in-time record, written while the story that commissioned it was open, of an investigation into what each credentials step can reach today.

It is a snapshot, not a description of the system. Whatever it concludes that the code must keep doing belongs on the page that owns the subject; cite that page from code, recipes and models, not this one.

1. Context

The three Credentials journeys are documented and prototyped. Before their build tasks can be sized, the group needs one answer: what can each step reach today? The prototypes answered that screen by screen, and each screen's answer mixed three different things — a subject that does not exist, a route the browser cannot use, and a handler that answers with nothing. Those three have different owners and different costs, so this page separates them.

It is a snapshot: written on 2026-09-30 against commit dbbde296d0. The counts move with the tree.

2. Methodology

Every claim below comes from the tree, not from a design document:

What How
Routes the browser can use =grep -c "server\.\(get\ post\ put\ delete\ patch\)(" projects/ores.web/packages/bff/src/server.ts= — 23 registrations
Routes external clients can use =grep -rn "router->\(get\ post\ put\ delete\)(\"/api/v1/iam" projects/ores.http/iam/src/routes/iam/*.cpp= — 50 across 12 files
Subjects that exist the generated protocol: projects/ores.web/packages/wire-protocol/src/generated/iam/protocol/*.ts
Permissions the catalogue grants grep -n "iam::" projects/ores.sql/populate/iam/iam_permissions_populate.sql
Guards the handlers enforce grep -rn has_permission projects/ores.iam/core/include/ores.iam.core/messaging/
Handlers that answer with nothing session_operations_handler.hpp

3. Paths taken

  • The BFF alone was read first, and it misled. Its 23 routes hold no session, account or login-record read, so the first conclusion was that nothing serves the credentials data. That is wrong: ores.http serves almost all of it, and the browser never reaches ores.http.
  • Then the route the documents named was followed. The journey documents claimed the BFF answers /api/v1/sessions/active. The path exists, on ores.http, so the handler behind it was read: it replies with {.success = true} and no sessions. The claim was corrected in the journey documents under Correct the claims the credentials prototypes disproved.
  • The permission column was checked against the handlers, not the catalogue. The two disagree in two places, and both are recorded below: one as a gap and one as a defect.

4. Conclusions

4.1. The boundary that decides everything

The browser reaches the BFF and nothing else. ores.http forwards each IAM route to the matching subject, so a route there proves the subject answers; it does not prove the browser can reach it. Every credentials screen therefore needs a BFF route per read and per write, even where the operation is complete on the server.

4.2. The gap kinds

Kind Meaning Cost
Route The subject and its handler work; no BFF route forwards to them One hand-written route, its schema and its component-specification entry
Handler The route and the subject exist; the handler answers with no rows A change in ores.iam, then the route
Subject Nothing anywhere carries the operation A design decision, then a subject, a handler and a route
Scope The operation works but cannot express what the screen asks A change to the request shape

4.3. Protect my account

Step Subject ores.http BFF Permission
Read my account iam.v1.accounts.get GET /api/v1/iam/accounts/{username} none iam::accounts:read
Read my login record iam.v1.login_info.get GET /api/v1/iam/login_info/{account_id} none iam::login_info:read
Change my password iam.v1.accounts.change-password POST /api/v1/iam/accounts/change-password POST /api/account/password own account
List my active sessions iam.v1.sessions.active POST /api/v1/iam/sessions/active none iam::sessions:read
Sign out iam.v1.auth.logout POST /api/v1/iam/auth/logout DELETE /api/session own session
End one other sign-in none none none subject gap
Enrol two-factor none none none subject gap
Session statistics none none none subject gap

Three route gaps, one handler gap, three subject gaps.

4.4. Rescue access

Step Subject ores.http BFF Permission
Find the colleague iam.v1.accounts.list GET /api/v1/iam/accounts none iam::accounts:read
Find the colleague iam.v1.accounts.get GET /api/v1/iam/accounts/{username} none iam::accounts:read
Read the security state iam.v1.login_info.get GET /api/v1/iam/login_info/{account_id} none iam::login_info:read
Set a new password iam.v1.accounts.reset-password POST /api/v1/iam/accounts/reset-password none iam::accounts:reset_password
Lock the account iam.v1.accounts.lock POST /api/v1/iam/accounts/lock none iam::accounts:lock
Unlock the account iam.v1.accounts.unlock POST /api/v1/iam/accounts/unlock none iam::accounts:unlock
Force a change at the next sign-in none none none subject gap
Activate or deactivate none none none subject gap
Self-service recovery none none none subject gap

Six route gaps, no handler gap, three subject gaps. The forced-change gap has a half-built shape: set_password_reset_required exists and nothing calls it, so the work is a subject and a handler over a service method that is already there.

4.5. Audit sign-ins

Step Subject ores.http BFF Permission
List every session iam.v1.sessions.list GET /api/v1/iam/sessions none iam::sessions:read
List the active sessions iam.v1.sessions.active POST /api/v1/iam/sessions/active none iam::sessions:read
Read the samples iam.v1.sessions.samples none none iam::sessions:read
List the login records iam.v1.login_info.list GET /api/v1/iam/login_info none iam::login_info:read
Read the auth events none none none subject gap
See the statistics none none none subject gap
End another account's session none none none subject gap
Delete the session row iam.v1.sessions.delete POST /api/v1/iam/sessions/delete-many none permission gap

Three route gaps, one handler gap, four subject gaps, and one scope gap: sessions.list pages with offset, limit and order and takes no account filter, so the screen cannot ask for one account's sessions.

4.6. The two handler gaps are one-line stubs

session_operations_handler.hpp answers both sessions.active and sessions.samples with {.success = true} and no rows. The ores.http route for the first forwards to that same handler, so the HTTP surface inherits the empty answer.

4.7. The permission gap

session_handler.hpp guards its delete verbs with iam::sessions:delete, and no such permission exists in the catalogue: it grants iam::sessions:read and iam::sessions:write only. Only a holder of the iam::* wildcard can delete a session row, so the operation is not the administrator action the screen needs.

4.8. A defect the exercise found

The same file guards its write verbs and does not guard its reads: list_sessions, get_session and get_many_sessions check no permission, while the account reads check iam::accounts:read and the login-record reads check iam::login_info:read. The catalogue grants iam::sessions:read for exactly this purpose, so the reads are unguarded by oversight and not by design. It is filed as its own capture, because it is a defect and not a journey gap.

4.9. The total

Gap Count
BFF routes to add 10 distinct: accounts.get, accounts.list, login_info.get, login_info.list, sessions.list, sessions.active, sessions.samples, accounts.reset-password, accounts.lock, accounts.unlock
Handlers to finish 2 (sessions.active, sessions.samples)
Subjects to design 6 (end one session, two-factor enrolment, statistics, forced change, activate or deactivate, recovery, auth events)
Request shapes to widen 1 (sessions.list needs an account filter)
Defects to fix 1 (unguarded session reads)

Protect my account needs three routes, Rescue access six and Audit sign-ins four; three of those routes serve two journeys each.

5. Recommendations

  • Size the build tasks by gap kind. The ten route gaps are mechanical, and each one forwards to a subject that already exists. The two handler gaps need an owner in ores.iam. The six subject gaps need a decision before a route.
  • Fix the unguarded session reads before the Audit screen ships. It is a security defect, and the screen would be built on it.
  • Widen sessions.list with an account filter as part of the Audit task, since the prototype already draws that filter as unavailable.
  • Give the six subject gaps one owner each, and decide them together: they share one question, which credential operations a tenant administrator may perform and which the member performs on their own account.
  • Do not treat an ores.http route as evidence that the browser can reach it. The next investigation of this shape should start at the BFF and widen.

6. See also

Emacs 29.3 (Org mode 9.6.15)