{"record":{"id":"ac69aa29e1364882","repo":"invoke-ai/InvokeAI","slug":"not-authorized-to-modify-this-image","errorCode":null,"errorMessage":"Not authorized to modify this image","messagePattern":"Not authorized to modify this image","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"invokeai/app/api/routers/_access.py","lineNumber":54,"sourceCode":"        # aggregates — five extra queries and five extra ways to fail per name.\n        #\n        # Only a board positively known to be gone falls through to the 403. A storage error\n        # propagates instead of being caught here: `board_records.get` deliberately does not\n        # translate sqlite errors into not-found, and a caller that cannot decide ownership\n        # must not report the name as an ordinary permission denial — the batch loops treat a\n        # 403 as a silent auth skip, which turned a locked database into images dropped from\n        # the response with no failure reported at all.\n        try:\n            board = ApiDependencies.invoker.services.board_records.get(board_id)\n        except BoardRecordNotFoundException:\n            pass\n        else:\n            if board.user_id == current_user.user_id:\n                return\n            if board.board_visibility == BoardVisibility.Public:\n                return\n\n    raise HTTPException(status_code=403, detail=\"Not authorized to modify this image\")\n\n\ndef _assert_image_record_exists(image_name: str) -> None:\n    \"\"\"Turn a refusal into a 404 when the image is positively gone.\n\n    The two refusals mean opposite things to a client holding a reference to the image — a\n    workflow's image field, a reference image on a canvas layer. Gone is permanent, and the\n    reference should be dropped. Denied is a permission decision that can be reversed (a board\n    flipped back to Shared, an owner re-granting access), and dropping the reference over one\n    destroys work the user cannot get back by restoring the permission.\n\n    Nothing above can tell them apart: the ownership test rests on `images.user_id`, which is\n    gone with the row, so a deleted image reaches that same 403 as a foreign one. So the\n    distinction is made here, on the refusal path only — the happy path pays nothing for it.\n\n    A storage error propagates rather than answering either, so an unreadable database cannot\n    present as a deleted image and take the user's references down with it. `exists` is a bare\n    row probe rather than `get` for the same reason from the other side: `get` deserializes, so","sourceCodeStart":36,"sourceCodeEnd":72,"githubUrl":"https://github.com/invoke-ai/InvokeAI/blob/0b6a024f2ff6a86bfb953dcdb9cc504ef7397a06/invokeai/app/api/routers/_access.py#L36-L72","documentation":"`assert_image_owner` raises this 403 when a non-admin user attempts to mutate an image they do not own. Ownership passes only if the user owns the image row, owns the image's board, or the image sits on a Public board; otherwise the mutation is refused.","triggerScenarios":"PATCH/DELETE on an image (e.g. /v1/images/i/{image_name}, star, update board) where current_user is not admin, not the image owner, does not own the image's board, and the board is not Public. Also fires if the image's board record is missing (BoardRecordNotFoundException falls through).","commonSituations":"A multiuser instance where a collaborator tries to delete or update another user's canvas image; an image whose board was deleted so ownership cannot be established; stale UI state referencing an image moved between boards.","solutions":["Log in as the image's owner or an admin user to perform the mutation","Move the image to a Public board (as its owner) so other users gain mutation rights on it","As the board owner, take ownership of the image's board so you qualify as the owner","Verify the image still exists and its board record is intact; if the board was deleted, re-create/restore it"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Before mutating, check read access/ownership if the API exposes it\nconst img = await fetch(`/v1/images/i/${imageName}/metadata`, {headers: authHeaders});\nif (!img.ok && img.status === 403) console.warn('Skipping image (not owned):', imageName);","typeGuard":"function canModifyImage(image, user) {\n  return user.is_admin || image.user_id === user.user_id || image.board_visibility === 'public';\n}","tryCatchPattern":"try {\n  await fetch(`/v1/images/i/${imageName}`, { method: 'DELETE', headers: authHeaders });\n} catch (e) {\n  if (e.status === 403) { /* per-image skip, continue batch */ }\n  else if (e.status === 404) { /* drop stale reference */ }\n  else throw e;\n}","preventionTips":["Only batch-modify images you created, or run the batch as an admin","Prefer operating on boards you own rather than individual foreign images","Handle 403 per item in batch loops instead of aborting the whole batch","Check board visibility (Public boards grant mutation) before attempting cross-user edits"],"tags":["auth","forbidden","image","multiuser","ownership"],"backgroundTag":"insufficient-permissions-403","analyzedSha":"0b6a024f2ff6a86bfb953dcdb9cc504ef7397a06","analyzedAt":"2026-08-29T04:46:49.967Z","schemaVersion":2},"datasetVersion":"2026-08-29T07:17:48.351Z"}