Skip to content

fix: guarantee root admin and Administrator role are non-deletable#119

Merged
andregoepel merged 1 commit into
mainfrom
bugfix/root-admin-non-deletable
Jul 19, 2026
Merged

fix: guarantee root admin and Administrator role are non-deletable#119
andregoepel merged 1 commit into
mainfrom
bugfix/root-admin-non-deletable

Conversation

@andregoepel

Copy link
Copy Markdown
Owner

Problem

The first-run setup produced a deletable root administrator and a deletable Administrator role. An administrator could therefore delete the main admin account and/or the Administrator role from the admin UI, orphaning all administrative access and leaving the app in an inconsistent, unrecoverable state. The setup UI even promises an "un-deletable root administrator" — the implementation didn't deliver it.

One defect, two layers. Closes #117 (sample) and #118 (library hardening).

Root cause

Setup.razor bypassed the library's root-user mechanism:

  • It created the first user without RootUser = true. So none of the library's root guards applied, and User.Deletable (default true) was propagated verbatim by CreateAsync — it did not force false for root the way UpdateAsync does.
  • It created the Administrator role manually (Role.Deletable defaults to true). Because RootUser was never set, the library path that creates the role as non-deletable never ran.

Fix

Library (defence in depth — the guarantee no longer depends on host code):

  • UserStore.CreateAsync now forces Deletable = false for root users, mirroring the existing UpdateAsync guard.
  • EnsureAdministratorRoleAsync now hardens a pre-existing deletable Administrator role to non-deletable via a RoleChanged event, so the guarantee holds regardless of how the role first came to exist.

Sample (canonical library usage):

  • Setup.razor creates the first admin with RootUser = true and lets the library own the whole "un-deletable root administrator" guarantee (protected role creation + assignment + non-deletability) in one transaction. The manual role creation and AddToRole that left both deletable are removed.

Tests

  • Integration (UserStoreTests): CreateAsync_RootUser_IsNonDeletable and CreateAsync_RootUser_HardensExistingDeletableAdministratorRole. Verified locally against Postgres (Podman) — green, with the full user/role store suite (60/60) passing, no regressions.
  • E2E (AdministrationTests.Admin_RootAccount_IsNonDeletable): asserts the /Setup-created admin surfaces as Deletable = "No" in the Users grid, covering the sample fix end-to-end. ProvisionAdminAsync drives the real /Setup page, so every admin E2E test also regression-guards that setup still works with RootUser = true. Built locally; runs in CI (local E2E is blocked by the Aspire HTTPS dev cert).

Verification

  • dotnet build (library, sample, both test projects): 0 errors, 0 warnings.
  • Integration tests: 60/60 green locally.
  • csharpier: compliant.

The first-run setup produced a *deletable* root admin and a *deletable*
Administrator role, so an administrator could delete the main admin
account and/or the Administrator role and orphan all admin access.

Two layers, one defect:

- Library: CreateAsync now forces Deletable=false for root users
  (mirroring the existing UpdateAsync guard) so the default
  User.Deletable=true can no longer mint a deletable root.
  EnsureAdministratorRoleAsync now hardens a pre-existing deletable
  Administrator role to non-deletable via a RoleChanged event, so the
  guarantee holds regardless of how the role first came to exist.
- Sample: Setup.razor creates the first admin with RootUser=true and
  lets the library own the "un-deletable root administrator" guarantee
  (protected role creation + assignment + non-deletability) in one
  transaction. The manual role creation and AddToRole that left both
  deletable are removed.

Tests: integration coverage for both invariants (root created
non-deletable; existing deletable Administrator role hardened) and an
E2E assertion that the /Setup-created admin surfaces as non-deletable.

Closes #117
Closes #118
@andregoepel andregoepel added bug Something isn't working security Security vulnerability or hardening labels Jul 19, 2026
@andregoepel
andregoepel merged commit 7c7ff63 into main Jul 19, 2026
4 checks passed
@andregoepel
andregoepel deleted the bugfix/root-admin-non-deletable branch July 23, 2026 16:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

First-run setup creates a deletable root admin and a deletable Administrator role

1 participant