Skip to content

feat(api): users can check and delete their own account - #2031

Merged
AmanGIT07 merged 3 commits into
mainfrom
feat/delete-current-user
Oct 29, 2026
Merged

AmanGIT07 merged 3 commits into
mainfrom
feat/delete-current-user

Conversation

@AmanGIT07

@AmanGIT07 AmanGIT07 commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #2028, which is stacked on #2027. Merge those first. This branch also carries #2026's proto commit (blockers on OrganizationToLeave). Merge #2026 first; that commit drops out when this branch is rebased.

Summary

Two new FrontierService RPCs act on the logged-in user. CheckCurrentUserDelete reports what deleting the account would do and what blocks it, without changing anything. DeleteCurrentUser runs the same check and refuses with every blocker. Otherwise it deletes the account the same way the admin DeleteUser does, then clears the session cookie.

A user delete, admin or self, now leaves each org with the guarded member removal, so it can no longer remove an org's last owner. Force removal is removed.

Changes

  • internal/api/v1beta1connect/user.go:
    • CheckCurrentUserDelete returns can_delete, the user blockers, the orgs deleted with the user (each with its own can_delete and blockers), the orgs the user leaves (role title and blockers), and the PAT count.
    • DeleteCurrentUser deletes the logged-in user and clears the session cookie with clearSessionCookie, like AuthLogout.
    • DeleteUser and DeleteCurrentUser share one helper. A blocked delete returns FAILED_PRECONDITION with one violation per blocker, and each blocker of an org it would delete starts with organization[<name>]: (feat(deleter): user delete checks for blockers before deleting anything #2027). A failed check or a failed delete is logged. A successful delete writes the app.user.deleted audit log.
  • internal/api/v1beta1connect/authenticate.go: getLoggedInUser returns the principal only when it is a user. Service users and PATs get PERMISSION_DENIED. CreateCurrentUserPAT uses it too. clearSessionCookie sets the header that tells the session interceptor to clear the cookie. AuthLogout and DeleteCurrentUser use it.
  • pkg/server/connect_interceptors/authorization.go: both RPCs are on the authorization skip list, because they need only a session, like GetCurrentUser. Both are on the PAT deny list.
  • core/membership/org.go: ForceRemoveOrganizationMember is removed. RemoveOrganizationMemberOnDelete is RemoveOrganizationMember for a user delete: the same member and last-owner guards, and it also works on a disabled org.
  • core/deleter/service.go: RemoveUsersFromOrg is removed. DeleteUser leaves each org with RemoveOrganizationMemberOnDelete:
    • a user who is no longer a member, or an org that is gone, counts as done;
    • an org that lost its other owner since the check stops the delete with SOLE_OWNER_WITH_MEMBERS.
  • core/membership/org.go, core/resource/service.go: removing a member, or a principal's custom-resource access, skips a policy that is already gone.
  • Mocks regenerated for the changed interfaces.

Technical Details

  • Force removal came in with refactor(deleter): delegate org removal cascade to membership #1696. It kept deleting a user who was an org's only owner possible, because the user delete had no check then. feat(deleter): user delete checks for blockers before deleting anything #2027's check refuses that case before anything is deleted, so both deletes now use the guarded removal. The admin DeleteUser behaves differently in one case only: when an owner leaves at the same moment, it is refused instead of leaving the org without an owner.
  • An org where the user is the only human member is deleted with the user, whatever the user's role. A user who is not an owner gets there only when the org has no human owner, for example when its owners are service users.
  • The only owner of a disabled org that has other members is refused. The members API refuses disabled orgs, so the user cannot fix this alone. A superuser enables the org, so the user can make another member an owner, or deletes the org.
  • Anything removed by someone else during the delete counts as done: an org, a membership, or a policy. A user that does not exist still returns NOT_FOUND.
  • The old session cookie stops working because the user delete ends the user's sessions just before the user row (fix(user): user delete ends the user's sessions #2024).
  • can_delete comes from the local invoice rows, which sync from the billing provider on a timer. The delete asks the provider again, so it can still be refused for an unpaid invoice that hasn't synced yet (feat(deleter): user delete checks for blockers before deleting anything #2027).
  • DeleteUser stays superuser only. A non-superuser still gets UNAVAILABLE.
  • Like the other current-user RPCs, the delete needs only a valid session. A superuser can delete their own account; the bootstrap client in the config can still grant platform admin.

Test Plan

  • core/deleter/service_test.go TestDeleteUser: the steps run in order with the guarded removal. A user removed from an org since the check, or an org deleted since the check, does not stop the delete. An org that lost its other owner since the check refuses with SOLE_OWNER_WITH_MEMBERS.
  • core/membership/org_test.go TestService_RemoveOrganizationMemberOnDelete: removes an owner from a disabled org through the owner guard, refuses the last owner, returns ErrNotMember, skips policies that are already gone, and passes up a failed org lookup.
  • core/resource/service_test.go: a policy that is already gone counts as deleted.
  • internal/api/v1beta1connect/user_test.go:
    • TestConnectHandler_CheckCurrentUserDelete: the full response mapping, nothing blocking, a failed check, no session, a service user, and a PAT.
    • TestConnectHandler_DeleteCurrentUser: success clears the cookie; a blocked delete returns FAILED_PRECONDITION; also a failed delete, no session, a service user, and a PAT.
    • TestConnectHandler_DeleteUser: the admin delete leaves the admin's own cookie alone.
  • Breaking any one rule makes a test fail. For example: letting a PAT through, not clearing the cookie, dropping the last-owner guard from the removal, stopping on a policy that is already gone, or not writing the audit log.
  • e2e, test/e2e/regression/api_test.go TestDeleteCurrentUser, in order:
    • The check lists the org that goes with the user and the orgs it leaves.
    • The delete is refused while the user is the only owner of an org with another member, and nothing changes.
    • The user makes the other member an owner, and the delete goes through and clears the cookie.
    • The old cookie gets UNAUTHENTICATED.
    • CheckFederatedResourcePermission is false on every org and project the user had.
    • ListPolicies and the policies table hold no live policy for the user.
    • The user is in neither users nor role_pairs of the orgs it left.
    • The delete wrote one app.user.deleted audit log and one user.deleted audit record.
    • The same email signs up again as a new user with a new id. The old id stays not found, and the new account has no orgs.
    • A service user cannot call either RPC.
  • e2e, test/e2e/regression/pat_test.go TestPATCannotCheckOrDeleteItsOwnersAccount: a PAT is refused on both RPCs. The check counts the PAT. The PAT stops working after its owner deletes the account.
  • go test ./test/e2e/... passes: regression (all suites) and smoke.
  • Every non-e2e unit package passes, and make lint reports no issues.
  • Manual end-to-end run against a local server built from this branch, with Postgres and SpiceDB in Docker and a deletion rule that makes one resource type block deletes: 12 scenarios, 57 checks, all pass.
Scenario What was checked Result
No session check, delete UNAUTHENTICATED on both
User with no orgs check, delete, sign up again with the same email can delete, deleted, new user with a new id
Alone in one org, only owner of an org with another member, member of a third org check, refused delete, fix, delete, cookie, access, policies, org users, orgs, sessions, audit see the full list below
Every blocker at once check, delete negative balance, a blocking resource and the only owner rule in one answer; nothing touched
Disabled orgs check, delete an org where the user is alone is deleted; the user leaves a disabled org as a member and as one of two owners
Only owner of a disabled org with a member check, delete, promote while disabled, admin step refused until a superuser enables the org and the user promotes the member
Disabled member check still counts, so the only owner is blocked
Org whose other member is a service user check, delete the org goes with the user
Service user as the caller check, delete PERMISSION_DENIED on both; the service user keeps working
PAT as the caller check, delete, PAT count PERMISSION_DENIED on both; the session check counts the PAT; the PAT stops working after the delete
Admin DeleteUser only owner with a member, member of a disabled org, non-superuser caller refused, deleted, UNAVAILABLE
Superuser check on its own account works, read only
All 57 checks
# Scenario Check Result
1.1 No session Check UNAUTHENTICATED
1.2 No session Delete UNAUTHENTICATED
2.1 User with no orgs Check can delete, nothing listed, 0 PATs
2.2 User with no orgs Delete ok
2.3 User with no orgs GetUser as a superuser after the delete NOT_FOUND
2.4 User with no orgs Same email signs up again new user, new id
3.1 Main flow Access before the delete 6 of 6 (3 orgs, 3 projects)
3.2 Main flow Check cannot delete. The alone org is listed to delete. The member org is listed to leave as "Member". The shared org is listed to leave as "Owner", blocked by SOLE_OWNER_WITH_MEMBERS
3.3 Main flow Check is read only same answer twice, live policies unchanged
3.4 Main flow Delete while blocked FAILED_PRECONDITION, SOLE_OWNER_WITH_MEMBERS on the shared org
3.5 Main flow Error message starts with "user cannot be deleted yet"
3.6 Main flow After the refused delete still signed in, alone org live, access 6 of 6
3.7 Main flow The user makes the other member an owner, then checks can delete
3.8 Main flow In users and role_pairs of the shared and member orgs, before yes, yes
3.9 Main flow Delete ok
3.10 Main flow Response cookie sid emptied with Max-Age=0
3.11 Main flow Old cookie on GetCurrentUser, check and delete UNAUTHENTICATED on all three
3.12 Main flow GetUser as a superuser NOT_FOUND
3.13 Main flow Access after the delete 0 of 6
3.14 Main flow Live policies for the user, in the table and in ListPolicies 0 and 0
3.15 Main flow In users and role_pairs, after no, no
3.16 Main flow Orgs: alone, shared, member deleted, enabled, enabled
3.17 Main flow Owners left in the shared org 1
3.18 Main flow Session rows for the user 0
3.19 Main flow app.user.deleted audit log and user.deleted audit record 1 and 1
4.1 Every blocker at once Check the org in debt has NEGATIVE_TOKEN_BALANCE; the org with a blocking resource has RESOURCES_EXIST; the shared org has SOLE_OWNER_WITH_MEMBERS
4.2 Every blocker at once Delete FAILED_PRECONDITION with all three in one answer
4.3 Every blocker at once Nothing touched user ok, three orgs enabled, the resource still there
4.4 Every blocker at once Check after deleting the resource only the balance and owner blockers remain
5.1 Disabled orgs Members API removing a member from a disabled org FAILED_PRECONDITION, unchanged
5.2 Disabled orgs Check can delete. The alone org is listed to delete. The user leaves the co-owned org as "Owner" and the other org as "Member"
5.3 Disabled orgs Delete ok
5.4 Disabled orgs Orgs after alone org deleted, the other two still disabled
5.5 Disabled orgs Live policies for the user, owners left in the co-owned org 0, 1
6.1 Only owner of a disabled org with a member Check blocked by SOLE_OWNER_WITH_MEMBERS
6.2 Only owner of a disabled org with a member Delete FAILED_PRECONDITION, SOLE_OWNER_WITH_MEMBERS
6.3 Only owner of a disabled org with a member The user makes the member an owner while the org is disabled FAILED_PRECONDITION
6.4 Only owner of a disabled org with a member A superuser enables the org, the user makes the member an owner, delete ok
7.1 Disabled member Check blocked by SOLE_OWNER_WITH_MEMBERS
8.1 Org whose other member is a service user Check the org goes with the user
8.2 Org whose other member is a service user Delete, then the org ok, deleted
9.1 Service user as the caller GetCurrentUser ok
9.2 Service user as the caller Check, delete PERMISSION_DENIED, PERMISSION_DENIED
9.3 Service user as the caller Still works after ok
10.1 PAT as the caller GetCurrentUser with the PAT ok
10.2 PAT as the caller Check with the PAT PERMISSION_DENIED, "not allowed with a personal access token"
10.3 PAT as the caller Delete with the PAT PERMISSION_DENIED, "not allowed with a personal access token"
10.4 PAT as the caller Check with the session pat_count 1
10.5 PAT as the caller Delete with the session ok
10.6 PAT as the caller The PAT after the delete, live PAT rows UNAUTHENTICATED, 0
11.1 Admin DeleteUser Only owner of an org with a member FAILED_PRECONDITION, SOLE_OWNER_WITH_MEMBERS
11.2 Admin DeleteUser That user after the refusal still there
11.3 Admin DeleteUser Member of a disabled org ok
11.4 Admin DeleteUser User, live policies and org after NOT_FOUND, 0, still disabled
11.5 Admin DeleteUser A non-superuser calls it for someone else UNAVAILABLE, unchanged
11.6 Admin DeleteUser That user after the call still there
12.1 Superuser Check on its own account works, read only

SQL Safety (if your PR touches *_repository.go or goqu.*)

Not applicable: no repository or goqu code changes. The new e2e checks read counts with $1 parameters.

@vercel

vercel Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
frontier Ready Ready Preview Oct 29, 2026 7:41am UTC

@coderabbitai

coderabbitai Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 0ab717b and c6135ac.

Files selected for processing: 17
  • core/deleter/deleter.go
  • core/deleter/mocks/membership_service.go
  • core/deleter/service.go
  • core/deleter/service_test.go
  • core/membership/org.go
  • core/membership/org_test.go
  • core/resource/service.go
  • core/resource/service_test.go
  • internal/api/v1beta1connect/authenticate.go
  • internal/api/v1beta1connect/interfaces.go
  • internal/api/v1beta1connect/mocks/cascade_deleter.go
  • internal/api/v1beta1connect/user.go
  • internal/api/v1beta1connect/user_pat.go
  • internal/api/v1beta1connect/user_test.go
  • pkg/server/connect_interceptors/authorization.go
  • test/e2e/regression/api_test.go
  • test/e2e/regression/pat_test.go

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 0 remain after this review.

📝 Summary

Summary by CodeRabbit

  • New Features
    • Users can check whether their account is eligible for deletion and review potential blockers, personal access token counts, and organization impacts.
    • Users can delete their own account. Successful deletion clears the session, and the account can be recreated using the same email address.
  • Bug Fixes
    • Account deletion now handles already-missing organization or resource policies without failing.
    • Deletion is blocked when it would leave an organization without an owner.
    • Service users and personal access tokens cannot check or delete a user’s account.

The change adds current-user deletion checks and deletion endpoints. It updates organization membership cleanup to enforce last-owner checks and tolerate missing memberships and policies. Tests cover API responses, session invalidation, cleanup results, and restrictions for service users and personal access tokens.

Changes: User deletion flow

Layer / File(s) Summary
Deletion cleanup and membership rules
core/deleter/*, core/membership/*, core/resource/*
User deletion uses a guarded membership-removal path and maps last-owner errors to blockers. The cleanup ignores missing memberships, organizations, and policies.
Current-user deletion endpoints
internal/api/v1beta1connect/*, pkg/server/connect_interceptors/authorization.go
The API adds current-user deletion checks and deletion. It maps check results and errors, clears the session cookie after successful self-deletion, and denies PAT calls to both endpoints.
Deletion regression coverage
test/e2e/regression/api_test.go, test/e2e/regression/pat_test.go
End-to-end tests cover deletion previews, blockers, successful deletion, session invalidation, audit records, and restrictions on service-user and PAT calls.

Priority: Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Suggested reviewers: rohilsurana, whoabhisheksah

Merge Risk: Minimal · up to c6135

The identified deletion-cleanup path does not leave access policies behind. No issue requiring a fix before merge was established.

Pre-merge checks: Passed 2
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coveralls

coveralls commented Oct 27, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 40836980855

Coverage increased (+0.07%) to 55.992%

Details

  • Coverage increased (+0.07%) from the base build.
  • Patch coverage: 3 uncovered changes across 2 files (108 of 111 lines covered, 97.3%).
  • 1 coverage regression across 1 file.

Uncovered Changes

File Changed Covered %
internal/api/v1beta1connect/authenticate.go 12 10 83.33%
internal/api/v1beta1connect/user.go 62 61 98.39%
Total (6 files) 111 108 97.3%

Coverage Regressions

1 previously-covered line in 1 file lost coverage.

File Lines Losing Coverage Coverage
core/membership/org.go 1 85.56%

Coverage Stats

Coverage Status
Relevant Lines: 42145
Covered Lines: 23598
Line Coverage: 55.99%
Coverage Strength: 19.68 hits per line

💛 - Coveralls

@rohilsurana rohilsurana left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: the PR body lists core/membership/service.go changes that already landed in #2022.

Comment thread core/deleter/service.go Outdated
Comment thread internal/api/v1beta1connect/authenticate.go Outdated
Comment thread internal/api/v1beta1connect/user.go Outdated
Comment thread internal/api/v1beta1connect/user_test.go Outdated
Comment thread core/deleter/service_test.go Outdated
Comment thread core/deleter/service.go
Comment thread internal/api/v1beta1connect/user.go
Comment thread internal/api/v1beta1connect/user.go
Comment thread internal/api/v1beta1connect/authenticate.go
Comment thread test/e2e/regression/api_test.go
@AmanGIT07

Copy link
Copy Markdown
Contributor Author

@rohilsurana Updated the description: it no longer lists core/membership/service.go, whose change landed in #2022.

@AmanGIT07
AmanGIT07 force-pushed the test/user-delete-check branch from 16df0ba to 8bdebc5 Compare October 29, 2026 07:21
@AmanGIT07
AmanGIT07 force-pushed the feat/delete-current-user branch from 6d4daa2 to d698fea Compare October 29, 2026 07:21
Base automatically changed from test/user-delete-check to main October 29, 2026 07:40
CheckCurrentUserDelete reports what deleting the logged-in user's account would do and what blocks it, without changing anything. DeleteCurrentUser runs the same check, refuses with every blocker, and otherwise deletes the account like the admin DeleteUser, then clears the session cookie. Both refuse service users and PATs. A user delete now leaves each org with the guarded member removal, which also works on a disabled org, so it can no longer remove an org's last owner, and force removal is removed. Anything removed by someone else during the delete counts as done.
@AmanGIT07
AmanGIT07 force-pushed the feat/delete-current-user branch from d698fea to c6135ac Compare October 29, 2026 07:40
@AmanGIT07
AmanGIT07 merged commit 8190c1f into main Oct 29, 2026
8 checks passed
@AmanGIT07
AmanGIT07 deleted the feat/delete-current-user branch October 29, 2026 07:52

This branch was successfully deployed

1 active deployment
Preview — c6135acb Deployed Oct 29, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants