{"record":{"id":"e98972f5f870eed8","repo":"fullstackhero/dotnet-starter-kit","slug":"not-allowed-to-attach-files-to-this-owner","errorCode":null,"errorMessage":"Not allowed to attach files to this owner.","messagePattern":"Not allowed to attach files to this owner\\.","errorType":"exception","errorClass":"ForbiddenException","httpStatus":403,"severity":"error","filePath":"src/Modules/Files/Modules.Files/Features/v1/RequestUploadUrl/RequestUploadUrlCommandHandler.cs","lineNumber":67,"sourceCode":"                $\"Extension '{extension}' not allowed for category '{cmd.Category}'.\",\n                (IEnumerable<string>?)null,\n                HttpStatusCode.BadRequest);\n        }\n\n        if (cmd.SizeBytes > category.MaxBytes)\n        {\n            throw new CustomException(\n                $\"File exceeds max size of {category.MaxBytes} bytes for category '{cmd.Category}'.\",\n                (IEnumerable<string>?)null,\n                HttpStatusCode.BadRequest);\n        }\n\n        // Authorization: policy must exist and allow the attach.\n        var policy = policies.Resolve(cmd.OwnerType)\n            ?? throw new ForbiddenException($\"No file access policy registered for owner type '{cmd.OwnerType}'.\");\n        if (!await policy.CanAttachAsync(cmd.OwnerId, userId.ToString(), cancellationToken).ConfigureAwait(false))\n        {\n            throw new ForbiddenException(\"Not allowed to attach files to this owner.\");\n        }\n\n        // Quota pre-check (no debit yet — debit happens on finalize with actual bytes).\n        var quotaCheck = await quotas.CheckAsync(tenantId, QuotaResource.StorageBytes, cmd.SizeBytes, cancellationToken).ConfigureAwait(false);\n        if (!quotaCheck.Allowed)\n        {\n            throw new CustomException(\n                $\"Storage quota exceeded ({quotaCheck.CurrentUsage}/{quotaCheck.Limit} bytes).\",\n                (IEnumerable<string>?)null,\n                (HttpStatusCode)507);\n        }\n\n        // Generate id + storage key + presigned URL.\n        var id = Guid.CreateVersion7();\n        var storageKey = StorageKeyBuilder.Build(tenantId, cmd.OwnerType, id, cmd.FileName, DateTimeOffset.UtcNow);\n        var ttl = TimeSpan.FromMinutes(options.Value.UploadUrlTtlMinutes);\n        var presigned = await storage.GenerateUploadUrlAsync(storageKey, cmd.ContentType, category.MaxBytes, ttl, cancellationToken).ConfigureAwait(false);\n","sourceCodeStart":49,"sourceCodeEnd":85,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Files/Modules.Files/Features/v1/RequestUploadUrl/RequestUploadUrlCommandHandler.cs#L49-L85","documentation":"ForbiddenException thrown when the resolved IFileAccessPolicy's CanAttachAsync(cmd.OwnerId, userId, ...) returns false — the policy itself decided this user may not attach files to that owner object. This is a deliberate authorization denial, distinct from a missing policy.","triggerScenarios":"Attaching a file to an entity the caller cannot modify: not the ticket's assignee/creator, not a member of the project that owns the object, an owner id from another tenant, or passing a nonexistent/mistyped ownerId so ownership checks fail.","commonSituations":"Frontend letting users attach files to items they can only view; copying an ownerId from another record; cross-tenant access attempts; role restrictions where only certain roles may modify attachments; tests using another user's credentials.","solutions":["Use an account with permission to modify the target owner entity, or attach the file to an owner the current user controls.","Verify the ownerId is correct and belongs to the caller's tenant.","Adjust the owner entity's access policy implementation only if the business rules are wrong — not to bypass authorization.","Check the UI hides or disables attachment actions the current user cannot perform."],"exampleFix":"// before\nawait requestUploadUrl({ ownerType: \"ticket\", ownerId: otherTeamsTicketId, ... }); // CanAttachAsync false\n// after\nawait requestUploadUrl({ ownerType: \"ticket\", ownerId: myTicketId, ... }); // caller can modify this ticket","handlingStrategy":"try-catch","validationCode":"// check the user can modify the owner entity first\nconst perms = await getOwnerPermissions(ownerType, ownerId);\nif (!perms.canEdit) throw new Error(\"You cannot attach files to this item\");","typeGuard":null,"tryCatchPattern":"try { await requestUploadUrl(cmd); } catch (e) { if (e.status === 403 && e.message === \"Not allowed to attach files to this owner.\") { showToast(\"You don't have permission to attach files here\"); } else throw e; }","preventionTips":["Hide/disable attachment UI when the user lacks edit rights on the owner entity.","Pass the ownerId from the loaded entity object, never from ad-hoc user input.","Keep per-owner-type access rules documented and covered by tests."],"tags":["authorization","files","permissions"],"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"}