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.httpserves almost all of it, and the browser never reachesores.http. - Then the route the documents named was followed. The journey documents
claimed the BFF answers
/api/v1/sessions/active. The path exists, onores.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.listwith 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.httproute as evidence that the browser can reach it. The next investigation of this shape should start at the BFF and widen.
6. See also
- Implement the credentials journeys in ores.web — the story this record answers.
- Correct the claims the credentials prototypes disproved — the earlier correction, which this census widens.
- User Journey Extraction Standard — the I03 and I04 checks this census serves.
- The journey documents: Protect my account, Rescue access and Audit sign-ins.