fullstackhero/dotnet-starter-kit · error · ForbiddenException

The root tenant administrator cannot be demoted.

Error message

The root tenant administrator cannot be demoted.

What it means

Thrown as a ForbiddenException when the target of a role change is the root tenant's seed administrator. That account is the framework's last-resort recovery login, so demoting it is unconditionally disallowed regardless of who is asking.

Solutions

  1. Exclude the root tenant's seed admin from any role-removal payloads.
  2. If you truly need to change that account, do it via a database/seed change, not the role API.
  3. Filter it out in the admin UI's editable-user list.
  4. Ensure bulk operations fetch the root admin id and skip it.

Example fix

// before: bulk payload includes root admin
await userRoleService.AssignRolesAsync(rootAdminId, rolesWithoutAdmin, ct);
// after
if (userRoleService.IsRootTenantAdminUser(rootAdminId)) return; // never demote seed admin
Defensive patterns

Strategy: validation

Validate before calling

if (isRootTenantSeedAdmin(userId)) throw new Error("The root tenant seed admin cannot be modified via the roles API.");

Try / catch

try { await assignRoles(userId, roles); } catch (e) {
  if (e.status === 403 && /root tenant administrator/i.test(e.message)) notify("Seed admin is protected");
  else throw e;
}

Prevention

When it happens

Trigger: AssignRolesAsync called against the root-tenant seed admin's userId with any payload that removes its Admin role; Verify IsRootTenantAdmin matched (root tenant + seed admin identity).

Common situations: Bulk user-management operations sweeping the seed admin into a role change; cleanup scripts deleting/degrading 'unused' admin accounts in the root tenant.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15). Data as JSON: /api/errors/7263306161cef90a. Report an issue: GitHub.

Appendix: source

Thrown at src/Modules/Identity/Modules.Identity/Services/UserRoleService.cs:99

        {
            return;
        }

        // Administrators cannot demote themselves — they would lose access immediately on the next request,
        // and would need another admin to restore them.
        var actorId = currentUser.GetUserId();
        if (actorId != Guid.Empty && string.Equals(actorId.ToString(), user.Id, StringComparison.Ordinal))
        {
            throw new CustomException(
                "Administrators cannot remove their own admin role.",
                Array.Empty<string>(),
                HttpStatusCode.BadRequest);
        }

        // The root tenant's seed admin is the framework's last-resort recovery account.
        if (IsRootTenantAdmin(user))
        {
            throw new ForbiddenException("The root tenant administrator cannot be demoted.");
        }

        // After this removal, at least one admin must remain in the tenant — matches
        // the "at least one active administrator" invariant enforced on user deactivation.
        await EnsureMinimumAdminCountAsync();
    }

    private bool IsRootTenantAdmin(FshUser user)
    {
        return user.Email == MultitenancyConstants.Root.EmailAddress
            && multiTenantContextAccessor?.MultiTenantContext?.TenantInfo?.Id == MultitenancyConstants.Root.Id;
    }

    private async Task EnsureMinimumAdminCountAsync()
    {
        int adminCount = (await userManager.GetUsersInRoleAsync(RoleConstants.Admin)).Count;
        if (adminCount <= 1)
        {

View on GitHub (pinned to 3f2959e683)