Skip to content

[permissions] Fix the access holes and status codes found by the spec review - #1327

Merged
frankrousseau merged 44 commits into
cgwire:mainfrom
frankrousseau:main
Oct 29, 2026
Merged

frankrousseau merged 44 commits into
cgwire:mainfrom
frankrousseau:main

Conversation

@frankrousseau

Copy link
Copy Markdown
Contributor

Problem

  • Several routes read the global role instead of the project role, so per-project vendors, clients, managers and supervisors got the wrong access
  • Some writes were badly guarded: task creation, share link passwords and guest names, guest annotations, sequence and entity deletion, budgets, previews and working files for clients, breakdown links, parent ids on PUT, comment replies and attachments, shared filters
  • Managers' CSV export leaked the daily salary, and the test event route was open to all
  • Wrong status codes: 500 on bad dates, 400 instead of 403 on Kitsu imports, raw database errors returned by imports
  • The OpenAPI specs did not follow these fixes

Solution

  • Resolve the project role before each role check, and scope listings to the projects the caller manages or supervises
  • Add the missing permission and parent checks, and protect the parent ids on PUT
  • Keep the daily salary and the test event route to administrators
  • Answer 400 on wrong dates, 403 on Kitsu imports, and sanitize import database errors
  • Update the OpenAPI specs to match

frankrousseau and others added 30 commits October 28, 2026 19:18
POST /data/tasks ran no permission check. It now requires an admin or a
manager of the entity project, and the task project_id follows the
entity instead of the body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only four shared routes read the password query parameter: a protected
link answered 404 to guest creation, media and comment edition even with
the right password. The password check moves out of validate_share_token,
which the services call again without it, into the route decorator.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without a valid guest_id, guest creation returned the existing guest
carrying the same name on the link: typing a reviewer's name took their
identity. A guest is now reused through its id only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A guest could update or delete every drawing object of a preview,
including those of the team. Guest additions are now stamped with the
guest id and guest updates and deletions are kept to the objects stamped
with it, keyed by time and id.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Any team member who was not the creator could delete a sequence, and the
creator needed manager access.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Return 200 instead of 201 when deleting a time spent
- Let admins handle the time of a person no longer assigned to the task
- List every time spent of a person when no date is given

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gins

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The eight entity deletion routes ignored the result of
check_belong_to_project, so the creator of an asset, shot, scene,
episode, sequence, concept or edit deleted it after leaving the project.
The rule now lives in check_entity_delete_access, which raises.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The fix itself (no KeyError when the body has no data) landed in
b9fed7a.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ProductionBudgetResource PUT and DELETE took a budget of any project,
and ProductionBudgetEntryResource an entry of any budget. get_budget and
get_budget_entry now take the parent id and answer 404 on a mismatch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
join(Task) after join(Project) joined the tasks on the project: a vendor
got every entity of the project.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
check_task_action_access lets a client through, as a client comments on
a task. The routes adding, removing, uploading or repositioning previews
and the working file updates now go through check_task_work_access,
which also blocks the client role resolved on the project.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tances

A client or a vendor of the project could link or unlink shots and
scenes, with a shot of another project, and create or remove asset
instances. These writes now go through check_project_edition_access and
the shot must belong to the scene project.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…omments

A PUT moved a preview file to another task, an asset instance to another
asset, scene or target asset, and a comment to another task: the update
checks read the project off the stored parent. The asset instance update
also took any member of the project, clients included.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
AttachmentFile GET checked the access to the task, not to the comment:
a client read the attachments of the internal comments.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Acknowledging a comment and deleting a reply took any comment id under
any task. Both now load the comment through get_task_comment, and the
reply deletion checks the task access, which it never did.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The author of a comment skipped every check when replying to it, out of
the team included. The reply now checks the task access first.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The persons export is open to the managers and carried the daily salary
column. The column now shows to the admins only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A vendor got every shot of an episode and every scene of a sequence, and
read the preview of an assigned task without being on the team. The two
listings now drop the entities the vendor holds no task on, the episode
shots the way the episode sequences already did, and the preview read
checks the project access first.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
generate_fixture_project moves self.project_id onto the new project, so
the client landed in the other team and the 403 held for another reason.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Removing a filter group deleted every filter it held, those of the other
members of a shared group included. Theirs are now detached from the
group, only the owner's go with it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
frankrousseau and others added 14 commits October 28, 2026 20:00
A shared filter or group reached every open project, not only the ones
the caller is on the team of. The listings now keep them to the caller's
teams, or to everyone when they name no project; admins are not scoped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
NewWorkingFile took any person_id as the author. Only a manager of the
project may file on behalf of someone else now.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
default_preview_background_file_id was checked against the project
backgrounds on creation only. The update now checks both background
fields, reading the stored project when the body carries no list.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
/status/test-event asked for no authentication and broadcast an event to
every connected client.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The listing read the global role: a vendor on one project only got every
entity of it. Each entity now follows the role held on its own project,
through the new get_project_ids_with_roles helper.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The vendor filter and the client blanking read the global role. Each
task now follows the role held on its own project, and a vendor must be
on the team as everywhere else.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
OutputFilesResource blocked the global vendor role only. The listing now
leaves out the projects the user is a vendor on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The share link routes checked the global manager role before resolving
the project, so a manager on the project only got a 403.
check_manager_project_access already covers the rule.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The time spent routes read the global role and opened every project of
the caller. They now keep to the projects where the caller is a manager
or a supervisor, read per project.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The to-check scope read the global role. It now keeps to the projects
where the caller is a manager or a supervisor, read per project.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nking

A client comment is blanked for an artist, but both roles were read
globally: a client on the project only kept their text visible, and an
artist on the project only was not covered. Both are read on the project
of the notification now.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A supervisor of one project who also manages another had the department
filter of their supervision applied to every project, so they missed the
other departments of the projects they manage, in the time spents of a
person and in the tasks to check.

user_service.build_supervision_scope_filter builds one task filter from
the roles held per project: every task of a managed project, the tasks of
the supervisor's departments on a supervised one. The person time spent
routes and the tasks to check pass this filter instead of a flat list of
departments, the explicit department_id query parameter keeps its own
filter.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frankrousseau
frankrousseau merged commit 530f8a0 into cgwire:main Oct 29, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant