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

  1. Invoke the endpoint with the root operator account / root tenant context.
  2. Hide the generate-invoices action for non-root users in the UI.
  3. If the intent is per-tenant invoicing, use the tenant-scoped invoice endpoint instead.
  4. 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

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


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)