{"record":{"id":"dcbb6985c15f33e7","repo":"fullstackhero/dotnet-starter-kit","slug":"only-the-root-tenant-can-set-the-default-theme","errorCode":null,"errorMessage":"Only the root tenant can set the default theme","messagePattern":"Only the root tenant can set the default theme","errorType":"exception","errorClass":"ForbiddenException","httpStatus":403,"severity":"error","filePath":"src/Modules/Multitenancy/Modules.Multitenancy/Services/TenantThemeService.cs","lineNumber":223,"sourceCode":"        }\n\n        await InvalidateCacheAsync(tenantId, ct).ConfigureAwait(false);\n\n        if (_logger.IsEnabled(LogLevel.Information))\n        {\n            _logger.LogInformation(\"Reset theme to defaults for tenant {TenantId}\", tenantId);\n        }\n    }\n\n    public async Task SetAsDefaultThemeAsync(string tenantId, CancellationToken ct = default)\n    {\n        ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);\n\n        // Ensure only root tenant can set default theme\n        var currentTenantId = _tenantAccessor.MultiTenantContext?.TenantInfo?.Id;\n        if (currentTenantId != MultitenancyConstants.Root.Id)\n        {\n            throw new ForbiddenException(\"Only the root tenant can set the default theme\");\n        }\n\n        // Clear existing default\n        var existingDefault = await _dbContext.TenantThemes\n            .FirstOrDefaultAsync(t => t.IsDefault, ct)\n            .ConfigureAwait(false);\n\n        if (existingDefault is not null)\n        {\n            existingDefault.IsDefault = false;\n        }\n\n        // Set new default\n        var entity = await _dbContext.TenantThemes\n            .FirstOrDefaultAsync(t => t.TenantId == tenantId, ct)\n            .ConfigureAwait(false);\n\n        if (entity is null)","sourceCodeStart":205,"sourceCodeEnd":241,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Multitenancy/Modules.Multitenancy/Services/TenantThemeService.cs#L205-L241","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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."],"exampleFix":"// before\n// called from a normal tenant's session\nawait themeClient.SetAsDefaultThemeAsync(themeId);\n\n// after\nvar currentTenant = httpContext.GetMultiTenantContext<TenantInfo>()?.TenantInfo?.Id;\nif (currentTenant != MultitenancyConstants.Root.Id)\n{\n    throw new ForbiddenException(\"Switch to the root tenant (admin app) to set the default theme.\");\n}\nawait themeClient.SetAsDefaultThemeAsync(themeId);","handlingStrategy":"validation","validationCode":"var currentTenantId = httpContext.GetMultiTenantContext<TenantInfo>()?.TenantInfo?.Id;\nif (currentTenantId != MultitenancyConstants.Root.Id)\n    throw new ForbiddenException(\"Default theme can only be set by the root tenant.\");","typeGuard":"bool IsRootContext(TenantInfo? ti) => ti?.Id == MultitenancyConstants.Root.Id;","tryCatchPattern":"try\n{\n    await themeService.SetAsDefaultThemeAsync(tenantId, themeId, ct);\n}\ncatch (ForbiddenException)\n{\n    logger.LogWarning(\"Non-root tenant {TenantId} attempted to set default theme\", currentTenantId);\n}","preventionTips":["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."],"tags":["multitenancy","authorization","tenant-theme","forbidden"],"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"}