Skip to content

Prevent admins from deleting their own account#9139

Merged
ogenstad merged 2 commits into
stablefrom
ai-bug-pipeline-9138-prevent-self-account-delete
May 7, 2026
Merged

Prevent admins from deleting their own account#9139
ogenstad merged 2 commits into
stablefrom
ai-bug-pipeline-9138-prevent-self-account-delete

Conversation

@claude

@claude claude Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Why (from Patrick)

Prevents a user from deleting their own account, i.e. the last remaining admin should not be able to delete their own account.

Fixes #9138.

Changes

  • Initial commit 4a32e02 added a failing test (and created this PR)
  • In the current commit I added the code to enforce the new restriction, the fix itself is quite basic of note is that we also support overriding the context when submitting mutations this is mainly used internally from the Prefect workers so that we can keep the initial user as the user making changes within the system. However a corner case with that is that if we just looked at the account ID an admin would still be able to delete their account if then provided an override the context. It might be a bit convoluted but perhaps there's also some future usecase to storing the original user account even when overriding the context within the requests

AI summary below

Analyst's findings (summary)

Root cause: The generic mutate_delete method in InfrahubMutationMixin performs no check to prevent a user from deleting the CoreAccount node that corresponds to their own authenticated session. CoreAccount is absent from mutation_map in manager.py, so it receives the unguarded default InfrahubMutation base class for all CRUD mutations.
Affected files:

  • backend/infrahub/graphql/mutations/main.py -- lines 503–525: mutate_delete fetches the target object and deletes it with no guard against the target being the caller's own account
  • backend/infrahub/graphql/context.py -- line 39: apply_external_context overwrites graphql_context.active_account_session.account_id in place, destroying the original authenticating account ID
  • backend/infrahub/graphql/initialization.py -- lines 81–86: assigned_user_id reads the (possibly overridden) account_session.account_id
  • backend/infrahub/graphql/manager.py -- lines 525–539: CoreAccount is absent from the mutation_map

Replication test

Test file: backend/tests/component/graphql/test_core_account.py
Test name: test_admin_cannot_delete_own_account

What it tests: Asserts that calling CoreAccountDelete with the authenticated admin's own account ID returns a GraphQL error instead of succeeding silently.

Verification: Test confirmed FAILING on current code.
Failure reason: result.errors is None because mutate_delete has no self-deletion guard and deletes the account without raising any error — the mutation returns ok: True.

Failure output (last 20 lines)
    async def test_admin_cannot_delete_own_account(
        db: InfrahubDatabase,
        default_branch: Branch,
        default_permission_backend: None,
        authentication_base: None,
        session_admin: AccountSession,
        create_test_admin: Node,
    ) -> None:
        default_branch.update_schema_hash()
        gql_params = await prepare_graphql_params(
            db=db,
            branch=default_branch,
            account_session=session_admin,
        )

        result = await graphql(
            schema=gql_params.schema,
            source=CORE_ACCOUNT_DELETE,
            context_value=gql_params.context,
            root_value=None,
            variable_values={"id": create_test_admin.id},
        )

>       assert result.errors
E       AssertionError: assert None
E        +  where None = ExecutionResult(data={'CoreAccountDelete': {'ok': True}}, errors=None).errors

backend/tests/component/graphql/test_core_account.py:174: AssertionError
FAILED backend/tests/component/graphql/test_core_account.py::test_admin_cannot_delete_own_account

Test expectations

The test asserts that when an authenticated admin sends a CoreAccountDelete mutation targeting their own account ID, the GraphQL response contains an error. Specifically, it expects result.errors to be non-empty and the error message to equal "Cannot delete your own account".

The fix must:

  1. Add original_account_id to AccountSession in auth.py and populate it in prepare_graphql_params
  2. Create InfrahubAccountMutation in mutations/account.py that overrides mutate_delete to compare the target node ID against original_account_id and raise ValidationError("Cannot delete your own account") if they match
  3. Register InfrahubKind.ACCOUNT: InfrahubAccountMutation in manager.py

The test does NOT cover the context-override bypass path (that requires a separate test case once the original_account_id field exists).

@github-actions github-actions Bot added the group/backend Issue related to the backend (API Server, Git Agent) label May 5, 2026
@codspeed-hq

codspeed-hq Bot commented May 5, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 12 untouched benchmarks


Comparing ai-bug-pipeline-9138-prevent-self-account-delete (1785f06) with stable (7298f33)1

Open in CodSpeed

Footnotes

  1. No successful run was found on stable (c451bc1) during the generation of this report, so 7298f33 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@claude

claude Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor Author

Verdict: APPROVED

The replication test is realistic, correctly scoped, and confirmed to fail against the buggy code. It follows project testing conventions (no mocking, exact-match GraphQL error assertion, imports at the top, no issue references in test names).

A. Test realism

  • CoreAccountDelete is a real generated mutation (confirmed in frontend/app/src/shared/api/graphql/generated/types.ts) so the operation name and data: {id: $id} shape match what real clients send.
  • session_admin (backend/tests/component/conftest.py:2591) is constructed with account_id=create_test_admin.id, so passing create_test_admin.id as the deletion target genuinely exercises the self-deletion path the analyst described.
  • All fixtures used in the signature (default_permission_backend, authentication_base, session_admin, create_test_admin) resolve to existing fixtures in backend/tests/conftest.py and backend/tests/component/conftest.py.

B. Test correctness

  • The test asserts the expected behavior: result.errors is non-empty and the message equals "Cannot delete your own account". It does not codify the buggy behavior.
  • It exercises the affected path identified by the analyst (InfrahubMutationMixin.mutate_delete in backend/infrahub/graphql/mutations/main.py), since CoreAccount falls through to the unguarded default mutation.
  • The PR description includes a verified failure run against the unpatched code (AssertionError: assert None), confirming the test cannot pass without changing production code.

C. Test quality

  • Imports are at the top (backend/tests/component/graphql/test_core_account.py:1-11), per dev/rules/testing-python.md.
  • No unittest.mock/MagicMock/patch usage — uses real fixtures, consistent with the rest of test_core_account.py.
  • GraphQL error message asserted with == (test_core_account.py:175), per the rule "Assert on the exact message with ==, not substring checks with in".
  • CORE_ACCOUNT_DELETE is a module-level constant, mirroring how test_everyone_can_update_password defines its query inline — fine either way; the constant is reasonable for reuse.
  • Test name test_admin_cannot_delete_own_account is descriptive and contains no issue number.
  • Placement in backend/tests/component/graphql/test_core_account.py is consistent with the existing test_permissions and test_everyone_can_update_password cases targeting CoreAccount-related GraphQL behavior.

D. Alignment with analysis

  • The test matches the analyst's primary root cause: the self-account deletion path through the default mutate_delete.
  • Scope is intentionally narrow — the analyst's secondary finding (the apply_external_context bypass that overwrites account_session.account_id) is not covered. The PR body acknowledges this explicitly: "The test does NOT cover the context-override bypass path (that requires a separate test case once the original_account_id field exists)."
    • This is acceptable for the replication test, but the bypass path is a real gap. A follow-up test asserting that an external-context override cannot be used to bypass the self-delete guard should be added in the fix PR (not blocking).

Suggestions (non-blocking)

  • Consider adding a complementary positive-case assertion (e.g., admin can delete a different account) in the same module, so a future regression that broadly breaks CoreAccountDelete is distinguishable from a regression that only breaks the self-delete guard. This is not required for replication.
  • After the fix lands, add the bypass-path test the PR body alludes to, so the original_account_id/authenticating_account_id plumbing is exercised end-to-end.

Recommended next steps

  • Approve this test and proceed to the fix phase.
  • Track the bypass-path test as a follow-up so it is not forgotten once the fix introduces the new field on AccountSession.

@ogenstad ogenstad changed the title test: failing test for #9138 -- prevent admin self-account deletion Prevent admins from deleting their own account May 7, 2026
@ogenstad ogenstad marked this pull request as ready for review May 7, 2026 07:55
@ogenstad ogenstad requested a review from a team as a code owner May 7, 2026 07:55
@ogenstad ogenstad merged commit 045500f into stable May 7, 2026
51 checks passed
@ogenstad ogenstad deleted the ai-bug-pipeline-9138-prevent-self-account-delete branch May 7, 2026 11:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

group/backend Issue related to the backend (API Server, Git Agent)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: It's possible to delete your own account

2 participants