Repository navigation
fix(iam): delete what a principal or account owns with it - #923
Open
hitesh96db wants to merge 7 commits into
Open
hitesh96db wants to merge 7 commits into
hitesh96db wants to merge 7 commits into
Conversation
added 7 commits
November 9, 2026 17:13
Inline policies and permissions boundaries are stored by principal name with no foreign key to the principal, and MongoDB and Cassandra link no IAM record to its principal or account. A delete left them behind, and a principal created later with the same name, or an account with the same id, got them and their permissions (#693, #694). - SQLite, PostgreSQL: a user, group or role delete also deletes its inline policies and boundary, in the principal's transaction. - MongoDB: a user delete also deletes its boundary and tags, a group delete its policies and memberships, a role delete its policies, boundary, tags and sessions. Errors are returned, not ignored. - Cassandra: a group delete also deletes its policies and memberships, a user delete its boundary, a role delete its policies, boundary, tags and sessions. - MongoDB, Cassandra: an account delete deletes the account's users, groups, roles and all they own. What a principal or account owns goes before its own record, so a failure leaves less, never more. - Every backend: a user, group or role create, after its own insert, also deletes rows stored under its name, so rows an earlier release left are cleared. MongoDB and Cassandra do the same for an account id. MongoDB now refuses a taken account id on create. - MongoDB, Cassandra: a user, access key, session or role of an account that no longer exists is unknown when signing in or assuming a role. extenddb_storage::iam_leftovers_conformance (feature test-util) checks this against a real database; each backend's suite runs it. Fixes #693. Fixes #694.
Putting an inline policy or a permissions boundary on a user, group or role that does not exist answered 204 and stored it under the name, so it waited for the next principal created with that name. The management API and the console now answer 404 "IAM <type> not found", as IAM answers NoSuchEntity. A create also clears such rows, which covers a put that races a delete.
Each test gives a role, user, group or account everything it can own, deletes it, checks that its credentials stop working, creates it again with the same name or id, and checks that it starts empty. Two more check that a policy or boundary put on a missing principal is refused with 404.
The getting-started and admin guides list what a user, group, role or account delete removes. The upgrade manual has a v0.1.15 entry, the differences document compares the delete and the put with IAM's DeleteConflict and NoSuchEntity, and the storage guide states the cascade rules a backend must follow.
A principal's create or delete returned an error, such as AlreadyExists or NotFound, by dropping its BEGIN IMMEDIATE transaction. Dropping a SQLite transaction only queues its rollback, and a BEGIN IMMEDIATE on the connection before the rollback ran failed with "attempted to call begin_with at non-zero transaction depth", so the next write on that connection answered 500. The creates and deletes now commit or roll back through one helper before they return.
hitesh96db
requested review from
LeeroyHannigan,
amrith,
c33howard,
eliseong,
jcshepherd,
navareamol,
pdf-amzn and
robinnsc
as code owners
November 10, 2026 01:59
| ); | ||
| assert!( | ||
| store | ||
| .verify_iam_user_password(&account, "alice", "correct horse") |
| ); | ||
| assert!( | ||
| !store | ||
| .verify_iam_user_password(&account, "alice", "correct horse") |
| ); | ||
| assert!( | ||
| store | ||
| .verify_iam_user_password(ACCOUNT, "alice", "correct horse") |
| ); | ||
| assert!( | ||
| !store | ||
| .verify_iam_user_password(ACCOUNT, "alice", "correct horse") |
navareamol
reviewed
Nov 10, 2026
navareamol
left a comment
Collaborator
There was a problem hiding this comment.
No blocking findings. Two things to settle:
- CI is red and the branch is behind main.
run-testsfails onpanicking_refresh_does_not_leak_inflight_flagincrates/cache, which this PR does not touch. It passed 100 of 100 runs on main here, so I think it is a flake. Could you update the branch and rerun it? CodeQL also reports 4 "hard-coded cryptographic value" alerts on the test passwords iniam_leftovers.rsandlive_tests.rs. Could you generate the password in the test, or dismiss the alerts with a comment? - The description says a deleted principal leaves nothing behind, but the failed-login history in
login_attemptsis keyed by name and follows it. Measured on SQLite and Cassandra: 6 wrong passwords for a user, delete the user, create the same name again, then the correct password from the same address gets 429 for up to 15 minutes. It is the same on main, so this PR does not cause it. Could you clear that name'slogin_attemptsrows in the delete and the create, or narrow the claim in the description? Filing it as its own issue also works for me.
The wire tests pass on SQLite, PostgreSQL and Cassandra. On main, 8 of the 9 fail on SQLite, as the description says. The two Cassandra direct tests pass too.
I'll approve once 1 is green and 2 is settled.
| let cleared = self | ||
| .delete_principal_dependents(account_id, principal_type, principal_name) | ||
| .await; | ||
| if cleared.is_err() |
Collaborator
There was a problem hiding this comment.
If the clear fails and the delete of the new row fails too, the second error is only logged and the create returns the first. The principal then exists with the old rows still under it. A retry of the create gets 409 and skips the clear, so nothing removes them. Is anything else clearing them in that case? I read this from the code and did not inject the failure.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A deleted user, group, role or account now leaves nothing behind for one created later with the same name or ID, on all four backends.
Inline policies and permissions boundaries are stored by principal name, with no foreign key to the principal. On MongoDB and Cassandra, no IAM record is linked to its principal or account at all. Before this change, each delete path removed a different subset (read from
mainat35edb778):Changes:
delete_usernow returns errors from these deletes; before, it ignored them.accountscollection is unique onaccount_nameonly, socreate_accountnow checks the ID first. Before, a second account with the same ID could be created, and the cleanup on create must never reach a live account.IAM <user|group|role> not foundinstead of 204, as IAM answersNoSuchEntity. The check is in the server: the storage methods stay plain data access, and the cleanup on create covers a put that races a delete.BEGIN IMMEDIATEon that connection before the rollback ran failed withattempted to call begin_with at non-zero transaction depth. That already happened oncreate_user's error paths; the newNotFoundandAlreadyExistsreturns made it frequent. The creates and deletes now commit or roll back explicitly before they return.I kept cascade rather than IAM's
DeleteConflictrefusal: ExtendDB already cascades access keys, tags and memberships, and refusing would break existing callers.docs/differences-from-dynamodb.mdrecords the difference.Why
Closes #693.
Closes #694.
A principal created with a deleted principal's name got its permissions: an inline policy or boundary on every backend, and on MongoDB and Cassandra also its access keys, sessions or group members. On MongoDB and Cassandra, a deleted account's users stayed in the catalog with their password hashes.
On SQLite, on
main(35edb778), I reproduced the role, user and group cases withextenddb manage. A recreated role listed the old policy and boundary, and a new session of it was allowedListTables. A recreated user and a recreated group behaved the same way.Testing done
extenddb_storage::iam_leftovers_conformance(featuretest-util). Each backend's suite runs it against a real database. It checks that a delete leaves no policy or boundary, that a create over rows stored under its name starts empty, that a refused duplicate create keeps the live principal's policies and boundary, and that an account created again with the same ID has no principals or policies.tests/iam_leftovers.rs): pass.main'susers.rs,groups.rsandroles.rson both backends. With only the cleanup in SQLite'screate_userremoved, it fails at "created over rows left under its name".management_store::live_tests, added todevtools/run-mongodb-tests --rust-live) and Cassandra (tests/direct/iam_leftovers.rs): not run locally, because this machine has neither. Both also test that a user and role of a deleted account (only the account record removed) cannot log in or be assumed. The credential stores' new checks have no unit test: MongoDB's hard-codes theextenddb_catalogdatabase, and Cassandra's is private to the crate.tests/test_iam_delete_leftovers.py(9 tests, all backends). For a role, user, group and account, it gives each everything it can own, deletes it, checks that its key or session is refused, creates it again, and checks that it starts empty and denied. It also checks that the account's user can no longer log in through the management API or the console. Two tests check the 404 on a missing principal.main's SQLite build, 8 of the 9 fail. The account test passes there, as expected: the foreign keys already cascade an account delete on SQLite. Its failure would show on MongoDB and Cassandra, which I could not run.mainmerged.cargo +1.97.0 fmt --all -- --check: clean.cargo +1.97.0 clippy --all-targets -- -D warningson the six changed crates, with both feature sets ofclippy.yml: clean. The workspace-wide run stops at an unused variable incrates/core/tests/data_file_decode_peak.rs, which this branch does not touch.cargo +1.88.0 checkon the changed crates: OK.cargo test --libfor storage, storage-sqlite, storage-mongodb, storage-cassandra and server: pass.cargo test -p extenddb-storage-postgres --testsagainst PostgreSQL 17: 395 passed. 4 failed, and they fail the same way onmainhere: the 3migrate_data_db_nametests need a password in the connection string, anda_role_that_does_not_own_the_tables_is_told_what_to_rundepends on the local superuser setup.Checklist
cargo test --workspace)cargo fmt --check)cargo clippy -- -W clippy::pedantic)Storagetrait, auth model, on-diskformat, 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. No schema, trait or wire change. The management API now answers 404 where it answered 204 for a policy or boundary on a missing principal.
Breaking changes
PUT .../users|groups|roles/{name}/policy/{policy}andPUT .../users|roles/{name}/permissions-boundaryon a principal that does not exist answer 404IAM <type> not found, where they answered 204. A script that attaches before it creates must create first.create-accountwith an account ID that is already in use answersAccount already exists.The upgrade manual has a v0.1.15 entry. No catalog migration is required.
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.