{"record":{"id":"24a95cb890f6c5e1","repo":"fullstackhero/dotnet-starter-kit","slug":"cross-tenant-audit-access-requires-permissions-audittrails","errorCode":null,"errorMessage":"Cross-tenant audit access requires Permissions.AuditTrails.ViewCrossTenant.","messagePattern":"Cross-tenant audit access requires Permissions\\.AuditTrails\\.ViewCrossTenant\\.","errorType":"exception","errorClass":"ForbiddenException","httpStatus":403,"severity":"error","filePath":"src/Modules/Auditing/Modules.Auditing/Features/v1/GetAudits/GetAuditsQueryHandler.cs","lineNumber":160,"sourceCode":"        var currentTenant = _currentUser.GetTenant();\n        var requested = string.IsNullOrWhiteSpace(query.TenantId) ? null : query.TenantId;\n\n        bool wantsCrossTenant =\n            requested is not null\n            && !string.Equals(requested, currentTenant, StringComparison.OrdinalIgnoreCase);\n\n        if (!wantsCrossTenant)\n        {\n            return _dbContext.AuditRecords.AsNoTracking();\n        }\n\n        var userId = _currentUser.GetUserId().ToString();\n        var allowed = await _permissions\n            .HasPermissionAsync(userId, AuditingPermissions.AuditTrails.ViewCrossTenant, ct)\n            .ConfigureAwait(false);\n        if (!allowed)\n        {\n            throw new ForbiddenException(\"Cross-tenant audit access requires Permissions.AuditTrails.ViewCrossTenant.\");\n        }\n\n        return _dbContext.AuditRecords\n            .AsNoTracking()\n            .IgnoreQueryFilters()\n            .Where(a => a.TenantId == requested);\n    }\n\n    /// <summary>\n    /// Clamps the supplied window to <see cref=\"MaxWindow\"/> and supplies a\n    /// <see cref=\"DefaultWindow\"/> when both endpoints are missing. The\n    /// validator catches obvious misuse (from &gt; to); this method handles\n    /// the open-ended \"no range\" case so the SQL is always bounded.\n    /// </summary>\n    private (DateTime FromUtc, DateTime ToUtc) ResolveWindow(DateTime? from, DateTime? to)\n    {\n        var now = _timeProvider.GetUtcNow().UtcDateTime;\n        var resolvedTo = to ?? now;","sourceCodeStart":142,"sourceCodeEnd":178,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Auditing/Modules.Auditing/Features/v1/GetAudits/GetAuditsQueryHandler.cs#L142-L178","documentation":"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.","triggerScenarios":"Calling the audit list endpoint with a tenant filter selecting a tenant other than the caller's (or 'all') while lacking ViewCrossTenant.","commonSituations":"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.","solutions":["Assign AuditingPermissions.AuditTrails.ViewCrossTenant to the required role.","Restrict the UI tenant filter to the caller's tenant when the permission is absent.","Re-run seeding to refresh role permissions.","Log in as a root operator for cross-tenant audit views."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"if (requestedTenant != currentTenant && !user.HasPermission(AuditingPermissions.AuditTrails.ViewCrossTenant)) return Forbid();","typeGuard":null,"tryCatchPattern":"try { return await api.GetAudits(filter); } catch (ForbiddenAccessException) { // render 'insufficient permissions' state }","preventionTips":["Only show the all-tenants filter to users with ViewCrossTenant","Keep role permission catalogs in sync with server permission constants","Audit role assignments periodically for least privilege"],"tags":["authorization","multitenancy","permissions","auditing"],"backgroundTag":"permission-denied","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}