fullstackhero/dotnet-starter-kit · error · ForbiddenException
No file access policy registered for owner type
Error message
No file access policy registered for owner type '{cmd.OwnerType}'. What it means
ForbiddenException thrown when policies.Resolve(cmd.OwnerType) returns null — i.e. no IFileAccessPolicy is registered for the owner type supplied in the command. The Files module delegates attach authorization to per-owner-type policies (ticket, catalog item, etc.); an unregistered owner type is treated as forbidden, not as an open door.
Solutions
- Send an ownerType that has a registered policy — check the string against the IFileAccessPolicy registration keys in the Files module.
- Implement an IFileAccessPolicy for the new owner type and register it in DI so Resolve() finds it.
- Centralize ownerType values as shared constants (Contracts) so client and server cannot drift.
- Add a validator rule listing the supported owner types to fail fast with a helpful message.
Example fix
// before
await requestUploadUrl({ ownerType: "Tickets", ownerId: id, ... }); // no policy registered for "Tickets"
// after
await requestUploadUrl({ ownerType: "ticket", ownerId: id, ... }); // matches registered FileAccessPolicy key Defensive patterns
Strategy: validation
Validate before calling
const SUPPORTED_OWNER_TYPES = ["ticket", "catalogitem"]; // mirrors registered IFileAccessPolicy keys
if (!SUPPORTED_OWNER_TYPES.includes(ownerType)) throw new Error(`No policy for owner type '${ownerType}'`); Type guard
function isSupportedOwnerType(t) { return SUPPORTED_OWNER_TYPES.includes(t); } Try / catch
try { await requestUploadUrl(cmd); } catch (e) { if (e.status === 403 && e.message?.includes("No file access policy")) { console.error("ownerType not registered:", cmd.ownerType); } else throw e; } Prevention
- Reference owner types from shared constants in module Contracts instead of raw strings.
- When adding a new owner entity, implement and register its IFileAccessPolicy as part of the feature checklist.
- Add a unit test asserting every owner type the client uses resolves to a policy.
When it happens
Trigger: RequestUploadUrlCommand with cmd.OwnerType spelled incorrectly ('Tickets' vs 'ticket'), a new owner type introduced by a caller without implementing/registering an IFileAccessPolicy for it in DI, or a policy registered under a different key string than the client sends.
Common situations: New business entity added with file attachments but the developer forgot to implement and register its access policy; frontends hardcoding an ownerType string that drifted from backend constants; renaming an entity on one side only.
Understand the failure class
Background: "You do not have permission" / 403 Forbidden errors: authenticated but not allowed — causes and fixes across open-source libraries — this error's family across 31 libraries.
Related errors
- no policy
- not allowed to change this file's visibility
- no policy
- not allowed to delete this file
- invalid tenant
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/84981283f90be1d7.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Files/Modules.Files/Features/v1/RequestUploadUrl/RequestUploadUrlCommandHandler.cs:64
!category.AllowedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{
throw new CustomException(
$"Extension '{extension}' not allowed for category '{cmd.Category}'.",
(IEnumerable<string>?)null,
HttpStatusCode.BadRequest);
}
if (cmd.SizeBytes > category.MaxBytes)
{
throw new CustomException(
$"File exceeds max size of {category.MaxBytes} bytes for category '{cmd.Category}'.",
(IEnumerable<string>?)null,
HttpStatusCode.BadRequest);
}
// Authorization: policy must exist and allow the attach.
var policy = policies.Resolve(cmd.OwnerType)
?? throw new ForbiddenException($"No file access policy registered for owner type '{cmd.OwnerType}'.");
if (!await policy.CanAttachAsync(cmd.OwnerId, userId.ToString(), cancellationToken).ConfigureAwait(false))
{
throw new ForbiddenException("Not allowed to attach files to this owner.");
}
// Quota pre-check (no debit yet — debit happens on finalize with actual bytes).
var quotaCheck = await quotas.CheckAsync(tenantId, QuotaResource.StorageBytes, cmd.SizeBytes, cancellationToken).ConfigureAwait(false);
if (!quotaCheck.Allowed)
{
throw new CustomException(
$"Storage quota exceeded ({quotaCheck.CurrentUsage}/{quotaCheck.Limit} bytes).",
(IEnumerable<string>?)null,
(HttpStatusCode)507);
}
// Generate id + storage key + presigned URL.
var id = Guid.CreateVersion7();
var storageKey = StorageKeyBuilder.Build(tenantId, cmd.OwnerType, id, cmd.FileName, DateTimeOffset.UtcNow);View on GitHub (pinned to 3f2959e683)