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

  1. Send an ownerType that has a registered policy — check the string against the IFileAccessPolicy registration keys in the Files module.
  2. Implement an IFileAccessPolicy for the new owner type and register it in DI so Resolve() finds it.
  3. Centralize ownerType values as shared constants (Contracts) so client and server cannot drift.
  4. 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

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


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)