fullstackhero/dotnet-starter-kit · error · ForbiddenException
Only the root operator may generate invoices across tenants.
Error message
Only the root operator may generate invoices across tenants.
What it means
GenerateInvoicesCommandHandler throws ForbiddenException when the caller's resolved tenant is not the root tenant. Even users holding Billing.Manage in a normal tenant must not trigger platform-wide invoice generation across all tenants; only the root operator may.
Solutions
- Invoke the endpoint with the root operator account / root tenant context.
- Hide the generate-invoices action for non-root users in the UI.
- If the intent is per-tenant invoicing, use the tenant-scoped invoice endpoint instead.
- Verify tenant resolution so the intended root caller is not misresolved to another tenant.
Example fix
// before // tenant admin calls generate-invoices -> 403 // after // authenticate as root operator (tenant id == MultitenancyConstants.Root.Id) or use per-tenant invoicing
Defensive patterns
Strategy: validation
Validate before calling
if (tenantAccessor.MultiTenantContext?.TenantInfo?.Id != MultitenancyConstants.Root.Id) return Forbid();
Try / catch
try { await api.GenerateInvoices(cmd); } catch (ForbiddenAccessException) { // hide action / inform user root required } Prevention
- Expose platform-wide invoicing only to root operator accounts
- Hide the UI action for non-root tenants
- Use tenant-scoped invoicing endpoints for per-tenant needs
When it happens
Trigger: Calling the generate-invoices endpoint while authenticated under a regular tenant id (callerTenantId != MultitenancyConstants.Root.Id), regardless of permissions.
Common situations: A tenant admin with Billing.Manage attempting the operation; testing with a non-root tenant profile; production calls accidentally pointed at a tenant-scoped service account.
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
- Tenant context is required.
- Tenant context is required.
- Tenant context is required.
- Tenant context is required.
- Tenant context is required.
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/f1ba21dc8e61e27e.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Billing/Modules.Billing/Features/v1/Invoices/GenerateInvoices/GenerateInvoicesCommandHandler.cs:24
using Mediator;
namespace FSH.Modules.Billing.Features.v1.Invoices.GenerateInvoices;
public sealed class GenerateInvoicesCommandHandler(
IBillingService billing,
IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor)
: ICommandHandler<GenerateInvoicesCommand, int>
{
public async ValueTask<int> Handle(GenerateInvoicesCommand command, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(command);
// Platform-wide invoice generation runs across EVERY tenant — it is a root-operator action.
// A tenant admin (who also holds Billing.Manage) must not be able to trigger it.
var callerTenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id
?? throw new UnauthorizedException("Tenant context is required.");
if (callerTenantId != MultitenancyConstants.Root.Id)
{
throw new ForbiddenException("Only the root operator may generate invoices across tenants.");
}
return await billing.GenerateInvoicesForAllTenantsAsync(command.PeriodYear, command.PeriodMonth, cancellationToken).ConfigureAwait(false);
}
}
View on GitHub (pinned to 3f2959e683)