fullstackhero/dotnet-starter-kit · error · ForbiddenException
Only the root tenant can set the default theme
Error message
Only the root tenant can set the default theme
What it means
TenantThemeService.SetAsDefaultThemeAsync requires the caller's current tenant (from the Finbuckle multitenant context) to be the root tenant; otherwise it throws ForbiddenException. Only the root/host tenant may designate a theme as the tenant-wide default.
Solutions
- Call the endpoint as the root tenant (correct host/tenant header so Finbuckle resolves Root.Id).
- Use the admin app / root-tenant credentials for default-theme management.
- Ensure the request carries tenant resolution info (identifier header/host) so MultiTenantContext.TenantInfo is populated.
- If a tenant wants custom branding, use per-tenant theme APIs instead of SetAsDefaultThemeAsync.
Example fix
// before
// called from a normal tenant's session
await themeClient.SetAsDefaultThemeAsync(themeId);
// after
var currentTenant = httpContext.GetMultiTenantContext<TenantInfo>()?.TenantInfo?.Id;
if (currentTenant != MultitenancyConstants.Root.Id)
{
throw new ForbiddenException("Switch to the root tenant (admin app) to set the default theme.");
}
await themeClient.SetAsDefaultThemeAsync(themeId); Defensive patterns
Strategy: validation
Validate before calling
var currentTenantId = httpContext.GetMultiTenantContext<TenantInfo>()?.TenantInfo?.Id;
if (currentTenantId != MultitenancyConstants.Root.Id)
throw new ForbiddenException("Default theme can only be set by the root tenant."); Type guard
bool IsRootContext(TenantInfo? ti) => ti?.Id == MultitenancyConstants.Root.Id;
Try / catch
try
{
await themeService.SetAsDefaultThemeAsync(tenantId, themeId, ct);
}
catch (ForbiddenException)
{
logger.LogWarning("Non-root tenant {TenantId} attempted to set default theme", currentTenantId);
} Prevention
- Perform default-theme management only from the admin app under the root tenant context.
- Verify tenant resolution (host/header mapping) so MultiTenantContext.TenantInfo is not null.
- Gate the UI action on the user's tenant being root before calling the API.
- Offer per-tenant theming as the alternative for non-root tenants.
When it happens
Trigger: Calling SetAsDefaultThemeAsync while authenticated under a non-root tenant context (MultiTenantContext.TenantInfo.Id != Root.Id); calling from the dashboard app of a normal tenant; calling without tenant resolution at all so TenantInfo (or its Id) is null and the != comparison fails.
Common situations: Tenant admin attempting to force their branding on all tenants; integration scripts not resolving into a tenant context (no tenant header/host mapping, so currentTenantId is null); using the wrong API client (dashboard instead of admin/root).
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
- Channel admin role required.
- Cross-tenant audit access requires…
- Cross-tenant audit summary requires…
- cross-tenant impersonation is restricted to platform…
- invalid tenant
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/dcbb6985c15f33e7.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Multitenancy/Modules.Multitenancy/Services/TenantThemeService.cs:223
}
await InvalidateCacheAsync(tenantId, ct).ConfigureAwait(false);
if (_logger.IsEnabled(LogLevel.Information))
{
_logger.LogInformation("Reset theme to defaults for tenant {TenantId}", tenantId);
}
}
public async Task SetAsDefaultThemeAsync(string tenantId, CancellationToken ct = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
// Ensure only root tenant can set default theme
var currentTenantId = _tenantAccessor.MultiTenantContext?.TenantInfo?.Id;
if (currentTenantId != MultitenancyConstants.Root.Id)
{
throw new ForbiddenException("Only the root tenant can set the default theme");
}
// Clear existing default
var existingDefault = await _dbContext.TenantThemes
.FirstOrDefaultAsync(t => t.IsDefault, ct)
.ConfigureAwait(false);
if (existingDefault is not null)
{
existingDefault.IsDefault = false;
}
// Set new default
var entity = await _dbContext.TenantThemes
.FirstOrDefaultAsync(t => t.TenantId == tenantId, ct)
.ConfigureAwait(false);
if (entity is null)View on GitHub (pinned to 3f2959e683)