{"record":{"id":"7263306161cef90a","repo":"fullstackhero/dotnet-starter-kit","slug":"the-root-tenant-administrator-cannot-be-demoted","errorCode":null,"errorMessage":"The root tenant administrator cannot be demoted.","messagePattern":"The root tenant administrator cannot be demoted\\.","errorType":"exception","errorClass":"ForbiddenException","httpStatus":403,"severity":"error","filePath":"src/Modules/Identity/Modules.Identity/Services/UserRoleService.cs","lineNumber":99,"sourceCode":"        {\n            return;\n        }\n\n        // Administrators cannot demote themselves — they would lose access immediately on the next request,\n        // and would need another admin to restore them.\n        var actorId = currentUser.GetUserId();\n        if (actorId != Guid.Empty && string.Equals(actorId.ToString(), user.Id, StringComparison.Ordinal))\n        {\n            throw new CustomException(\n                \"Administrators cannot remove their own admin role.\",\n                Array.Empty<string>(),\n                HttpStatusCode.BadRequest);\n        }\n\n        // The root tenant's seed admin is the framework's last-resort recovery account.\n        if (IsRootTenantAdmin(user))\n        {\n            throw new ForbiddenException(\"The root tenant administrator cannot be demoted.\");\n        }\n\n        // After this removal, at least one admin must remain in the tenant — matches\n        // the \"at least one active administrator\" invariant enforced on user deactivation.\n        await EnsureMinimumAdminCountAsync();\n    }\n\n    private bool IsRootTenantAdmin(FshUser user)\n    {\n        return user.Email == MultitenancyConstants.Root.EmailAddress\n            && multiTenantContextAccessor?.MultiTenantContext?.TenantInfo?.Id == MultitenancyConstants.Root.Id;\n    }\n\n    private async Task EnsureMinimumAdminCountAsync()\n    {\n        int adminCount = (await userManager.GetUsersInRoleAsync(RoleConstants.Admin)).Count;\n        if (adminCount <= 1)\n        {","sourceCodeStart":81,"sourceCodeEnd":117,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Identity/Modules.Identity/Services/UserRoleService.cs#L81-L117","documentation":"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.","triggerScenarios":"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).","commonSituations":"Bulk user-management operations sweeping the seed admin into a role change; cleanup scripts deleting/degrading 'unused' admin accounts in the root tenant.","solutions":["Exclude the root tenant's seed admin from any role-removal payloads.","If you truly need to change that account, do it via a database/seed change, not the role API.","Filter it out in the admin UI's editable-user list.","Ensure bulk operations fetch the root admin id and skip it."],"exampleFix":"// before: bulk payload includes root admin\nawait userRoleService.AssignRolesAsync(rootAdminId, rolesWithoutAdmin, ct);\n// after\nif (userRoleService.IsRootTenantAdminUser(rootAdminId)) return; // never demote seed admin","handlingStrategy":"validation","validationCode":"if (isRootTenantSeedAdmin(userId)) throw new Error(\"The root tenant seed admin cannot be modified via the roles API.\");","typeGuard":null,"tryCatchPattern":"try { await assignRoles(userId, roles); } catch (e) {\n  if (e.status === 403 && /root tenant administrator/i.test(e.message)) notify(\"Seed admin is protected\");\n  else throw e;\n}","preventionTips":["Hard-code an exclusion for the root seed admin id in bulk operations.","Hide the seed admin from editable user lists.","Treat changes to that account as database/seed-level operations only."],"tags":["identity","roles","authorization","root-tenant"],"backgroundTag":"permission-denied","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}