Skip to content

fix(console): serve an IAM user its own user page and access keys only - #844

Open
LeeroyHannigan wants to merge 5 commits into
mainfrom
fix/console-iam-user-own-pages
Open

LeeroyHannigan wants to merge 5 commits into
mainfrom
fix/console-iam-user-own-pages

Conversation

@LeeroyHannigan

@LeeroyHannigan LeeroyHannigan commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

What

An IAM-user console session now reads only its own user page and its own access keys in its account. Every other page and form under /console/accounts redirects the session to /console. Admin sessions see every page as before.

  • The account page, the group pages, the role pages and the account list at /console/accounts are for admin sessions only.
  • A user page opens only when the account ID and the user name in the path are the session's own. The new require_user_session in account_scope.rs makes this check. The management API uses the same rule for the access keys of an IAM user. Another user's name and a name that no user has get the same answer.
  • The user's own page lists its access keys, with the forms that create and delete them. It leaves out the user's inline policies, groups and tags. The management API does not give the user these reads either: GET .../users/{self}/policies and GET .../users/{self}/tags answer 403, and no route lists the groups of a user.
  • The admin-only forms and POST handlers redirect an IAM user to /console. These are the forms that change users, groups, roles, members and policies, the delete account form and the new account form. Before, some of them sent the user to the account page, the group page or /console/accounts.
  • Each handler makes its check before it reads the account, user, group or role in the path.
  • The navigation bar shows the Accounts link to admin sessions only. The dashboard card of an IAM user links to its own user page, and its breadcrumbs are Dashboard and its own user. The pages, navigation bar and breadcrumbs of an admin session are the same as before, byte for byte.
  • Docs: getting-started section 8, the architecture guide, the admin guide, the security model, and a v0.1.15 entry in the upgrade manual. The earlier v0.1.15 entry for console account scoping no longer says that the home page links an IAM user to its own account.

Why

Closes #731.

An IAM-user console session read every page of its own account. It saw every user, group and role name. It also saw the access key IDs, inline policy names, groups and tags of every user. Group pages showed the members and policy names, and role pages showed the trust policy and policy names. The management API refuses the same reads to the same user with HTTP 403. docs/getting-started.md section 8 said that IAM users view their own details and manage their own access keys.

Testing done

  • Rust: 34 console unit tests pass. They cover the own-user check, the user page, the breadcrumbs and the dashboard card. The new account_scope_reads_tests.rs uses a store that counts calls. For an IAM-user session, the account page, a group page, a role page and another user's page answer 303 to /console. Each makes one password hash read, for the session check, and no other store call. The user's own page and each admin page make 1 other call.
  • Python: tests/test_console_iam_user_pages.py (53 tests) logs in as an IAM user that holds only the self-service policy that every user with a password gets. It checks these things:
    • 15 refused pages and 21 refused forms answer 303 to /console and change nothing.
    • No page that the user opens names another user, group or role, or links to a page that redirects it. The user's own group and tag do not show on its page.
    • The management API refuses the same reads.
    • The user creates and deletes its own keys and logs out. The two key forms answer 403 to an empty or wrong CSRF token, and the keys do not change.
    • An admin still reads every page.
  • tests/test_console_*.py and tests/test_cross_account_isolation.py: 168 passed on a SQLite build.
  • cargo fmt --check and cargo clippy -p extenddb-server --all-targets -- -D warnings pass.
  • Negative controls: I disabled each new check, one at a time, and a test failed each time. The 18 controls cover the account, group, role, user and account list checks, and the read order of four pages. They also cover both CSRF checks, the redirect targets, the dashboard card, the breadcrumbs and the Accounts link.
  • After the main merge (e5cd478):
    • cargo fmt --check and CI's clippy command pass. Rust 1.88.0 builds the server tests.
    • Server: 351 of 356 tests pass, including all 57 console tests. The 5 that fail are backup store tests. They need parent directories owned by root, and on the test host those directories show uid 65534. The merge does not change that code.
    • tests/test_console_*.py and tests/test_cross_account_isolation.py: 184 passed and 1 skipped on a SQLite build.
    • Negative controls on the merged code each made a test fail. They showed the Accounts link to every session, added a catalog read before the check in account_detail or group_detail, and added a second password hash read in group_detail.

Merging with main

Commit e5cd478 merges main into this branch. On main, #727, #704, #773 and #875 change the console code that this PR changes. The merge does what this section planned:

Checklist

  • I have read CONTRIBUTING.md
  • All tests pass (cargo test --workspace)
  • Code is formatted (cargo fmt --check)
  • Clippy is clean (cargo clippy -- -W clippy::pedantic)
  • I have added or updated tests for new functionality
  • I have updated documentation if behavior changed
  • Breaking changes are noted below (if any)
  • If this changes the wire protocol, Storage trait, auth model, on-disk
    format, or public CLI surface, an RFC has been accepted or is linked
    below. Otherwise, an ADR captures the decision (link below).

ADR / RFC: n/a. The change limits what an IAM-user console session reads to what the management API already gives that user. The API, the storage and the policy evaluation do not change.

Breaking changes

In the console, an IAM user can no longer open its account page or the pages of other users. It can no longer open the group and role pages or the account list either. Its own user page no longer shows its inline policies, groups and tags. It still creates and deletes its own access keys there.


By submitting this pull request, I confirm that my contribution is made under
the terms of the Apache License 2.0 and I agree to the Developer Certificate of
Origin (DCO). See CONTRIBUTING.md for details.

An IAM-user console session could open every page of its own account. The
account page named every user, group and role. A user page showed that
user's access key IDs, inline policy names, groups and tags. A group page
showed its members and inline policy names, and a role page showed its trust
policy, inline policy names and tags. The handlers checked only that the
account in the path was the session's own. The management API answers the
same reads to the same user with HTTP 403.

An IAM-user session now opens only its own user page and the forms that
create and delete its own access keys. These are the access key reads and
writes the management API gives the same user. The console has no form to
change the password or import a key. require_user_session lets an IAM-user
session through when the account ID and the user name in the path are its
own. The management API applies the same rule to an IAM user's access keys.
Any other user's pages redirect it to /console before the handler reads
anything about that user. The account page, the group and role pages and
the account list are for admins. Their handlers redirect an IAM-user session
to /console before they read what the page shows. The admin-only forms under
the account also redirect it to /console. Before, some of them redirected to
the account page, the group page or the account list.

The user's own page lists its access keys and leaves out its inline
policies, groups and tags. The management API does not give the user those
either. The dashboard links an IAM-user session to its own user page, and
the navigation bar shows the Accounts link to admins only. The breadcrumbs
of the user page and the new access key page leave out the pages the
session may not open. Admin sessions get the same pages as before.

The tests that read an IAM user's own account page, its dashboard link and
its account list now expect the redirects.
…d keys

tests/test_console_iam_user_pages.py logs in as an IAM user that holds only
the self-service policy every user created with a console password gets. The
account also holds two other users, a group and a role, each with an inline
policy, and one user with an access key and a tag.

The tests check that the account list, the account forms and every page and
form under the user's account other than its own user page and access key
forms redirect it to /console, and that no refused form changes the catalog.
They check that no page it opens names another user, group or role of the
account or links to a page that redirects it, and that the management API
refuses the user the same reads. The user still reads, creates and
deletes its own access keys and logs out, and an admin session still reads
every page.
The getting started guide, the admin guide, the architecture guide and the
security model now say that an IAM-user session opens its own user page and
manages its own access keys, and that every other page under its account
redirects it to the dashboard. The upgrade manual has a v0.1.15 entry for
the change.

The earlier v0.1.15 entry for console account scoping no longer says
that the home page links an IAM user to its own account, or that the
pages of the user's own account are unchanged.
account_scope_reads_tests.rs runs the account, group, role and user page
handlers against a catalog store that holds no data and counts every call
to it. An IAM-user session that requests the account page, a group page, a
role page or another user's page gets a redirect to /console (HTTP 303).
The handler makes no call to the store. An admin request and the IAM
user's own user page each make one call.

The integration test posts the create and delete access key forms as the
IAM user. It posts each form twice, once with an empty CSRF token and once
with a wrong token of the same length. Each answer is HTTP 403, and the
user's access keys, as the management API lists them, do not change.

The IAM user in the integration test now belongs to a second group and has
a tag. The test checks that its own page leaves out that group and tag,
and that the admin's view of the same page shows them.
@hitesh96db hitesh96db self-assigned this Nov 5, 2026
@hitesh96db hitesh96db added security release-blocker Must ship for its milestone's release labels Nov 5, 2026

@hitesh96db hitesh96db left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes #731 as far as I can see, but it needs a rebase before it can merge. #727 and #704 both merged after this branch was cut, and the PR now conflicts with main. The inline comments cover the conflict resolution and two test breaks that the merge does not flag as conflicts.

What I checked

All on this PR's head (1fc6c234), with Rust 1.97.0 (the CI toolchain):

  • Every route has a guard. There are 30 routes under /console/accounts. Three use require_user_session: user_detail, create_access_key and delete_access_key. The other 27 redirect an IAM-user session to /console before their first catalog call. The nine policy routes get this from policy_form, put_policy and delete_policy, which this PR does not change and which were already admin-only.
  • user_scope fails closed. It compares the account ID and user name in the path with the session's own, exactly. It reads nothing, so another user and a name no user has get the same answer. This is the rule the management API applies in authorize_self_service (management/iam_user_self.rs).
  • Key deletion is scoped to the path's user. The access key forms trust the {name} in the path. That is safe because delete_access_key matches on access key ID, account ID and user name on all four backends: the SQL WHERE on SQLite and PostgreSQL, the delete_one filter on MongoDB, and the LWT IF on Cassandra. An IAM user cannot delete another user's key by putting that key's ID under its own user.
  • The own user page shows keys only. It ends after the access keys section for an IAM-user session, so the policies, groups and tags sections are not rendered.
  • cargo test -p extenddb-server --lib console: 34 passed.
  • Against a SQLite release build of this PR on 127.0.0.1, tests/test_console_iam_user_pages.py, test_console_account_scope.py, test_console_integration.py and test_cross_account_isolation.py: 159 passed.

Rebase onto main

A trial merge of origin/main conflicts in crates/server/src/console/html.rs, crates/server/src/console/pages/mod.rs and docs/manuals/10-security-model.md. These are the conflicts the description predicts:

  • pages/mod.rs: #727 added the same identity_label -> html::NavIdentity change, so this conflict is the import lines only.
  • 10-security-model.md: take this PR's paragraph without the "Until pull request #727 merges, ..." sentence, and keep #727's admin-only metrics bullet.
  • html.rs: see the inline comment.
  • #704 test breaks: after the merge, account_scope_reads_tests.rs does not compile because of #704's API changes. See the two inline comments there.

Not blocking

  • IAM users lose some reads. An IAM user can no longer see its own inline policies, groups and tags in the console. That matches the management API, and the upgrade manual says so. It would be good for a second reviewer to agree to this explicitly.
  • CodeQL alerts. The four "Cleartext logging of sensitive information" alerts flag account IDs in redirect handling and test messages. They look like false positives.

Not checked

  • The rebased branch, which does not build yet.
  • Backends other than SQLite for the Python suites.

This resolves the conflicts with #727 and #704. The navigation bar puts
Accounts with the admin-only links of #727, so the bar of an IAM user
shows Dashboard, its name and Logout. The security model keeps the
paragraph of this branch without its sentence about #727, and keeps the
metrics bullet of #727.

On main, #704 checks each console request against the password hash of
the session's principal. The read-count tests answer that read, count it
apart from the other calls, create their sessions with a stamp and set
`secure_cookies`. The docs of account_scope.rs say that the account
check runs right after the session check. The security model and the
upgrade manual say that the session check is the only read before the
user check. The navigation bar test of #727 now expects no Accounts
link for an IAM user.

Signed-off-by: Lee Hannigan <29015211+LeeroyHannigan@users.noreply.github.com>
@LeeroyHannigan

Copy link
Copy Markdown
Collaborator Author

@hitesh96db thanks for the review. I merged main into this branch as e5cd478, with the resolution that your summary and the description give. Your review mentions inline comments, but none reached the PR, so I worked from the summary.

The "Merging with main" section of the description lists each change, with the two test breaks from #704. Could you take another look?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-blocker Must ship for its milestone's release security v1 Needed for the 1.0 release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Console: an IAM user reads every user, group and role page of its own account, which the management API refuses it

3 participants