fullstackhero/dotnet-starter-kit · error · ForbiddenException

Cross-tenant audit access requires…

Error message

Cross-tenant audit access requires Permissions.AuditTrails.ViewCrossTenant.

What it means

GetAuditsQueryHandler.BuildBaseQueryAsync throws ForbiddenException when a cross-tenant audit listing is requested without the AuditingPermissions.AuditTrails.ViewCrossTenant permission. Cross-tenant listing uses IgnoreQueryFilters(), gated behind this explicit permission check. Maps to HTTP 403.

Solutions

  1. Assign AuditingPermissions.AuditTrails.ViewCrossTenant to the required role.
  2. Restrict the UI tenant filter to the caller's tenant when the permission is absent.
  3. Re-run seeding to refresh role permissions.
  4. Log in as a root operator for cross-tenant audit views.
Defensive patterns

Strategy: validation

Validate before calling

if (requestedTenant != currentTenant && !user.HasPermission(AuditingPermissions.AuditTrails.ViewCrossTenant)) return Forbid();

Try / catch

try { return await api.GetAudits(filter); } catch (ForbiddenAccessException) { // render 'insufficient permissions' state }

Prevention

When it happens

Trigger: Calling the audit list endpoint with a tenant filter selecting a tenant other than the caller's (or 'all') while lacking ViewCrossTenant.

Common situations: Tenant admin filtering the audit grid by another tenant; UI exposing a tenant dropdown the user is not entitled to use; roles not re-seeded after the permission was introduced.

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/24a95cb890f6c5e1. Report an issue: GitHub.

Appendix: source

Thrown at src/Modules/Auditing/Modules.Auditing/Features/v1/GetAudits/GetAuditsQueryHandler.cs:160

        var currentTenant = _currentUser.GetTenant();
        var requested = string.IsNullOrWhiteSpace(query.TenantId) ? null : query.TenantId;

        bool wantsCrossTenant =
            requested is not null
            && !string.Equals(requested, currentTenant, StringComparison.OrdinalIgnoreCase);

        if (!wantsCrossTenant)
        {
            return _dbContext.AuditRecords.AsNoTracking();
        }

        var userId = _currentUser.GetUserId().ToString();
        var allowed = await _permissions
            .HasPermissionAsync(userId, AuditingPermissions.AuditTrails.ViewCrossTenant, ct)
            .ConfigureAwait(false);
        if (!allowed)
        {
            throw new ForbiddenException("Cross-tenant audit access requires Permissions.AuditTrails.ViewCrossTenant.");
        }

        return _dbContext.AuditRecords
            .AsNoTracking()
            .IgnoreQueryFilters()
            .Where(a => a.TenantId == requested);
    }

    /// <summary>
    /// Clamps the supplied window to <see cref="MaxWindow"/> and supplies a
    /// <see cref="DefaultWindow"/> when both endpoints are missing. The
    /// validator catches obvious misuse (from &gt; to); this method handles
    /// the open-ended "no range" case so the SQL is always bounded.
    /// </summary>
    private (DateTime FromUtc, DateTime ToUtc) ResolveWindow(DateTime? from, DateTime? to)
    {
        var now = _timeProvider.GetUtcNow().UtcDateTime;
        var resolvedTo = to ?? now;

View on GitHub (pinned to 3f2959e683)