mirror of
https://github.com/makeplane/plane.git
synced 2026-09-02 12:09:14 +02:00
urls/project.py maps "put": "update", but ProjectViewSet never defined update, so PUT fell through to DRF's ModelViewSet.update. The class sets no permission_classes, so that ran under the default IsAuthenticated while partial_update on the identical URL requires workspace or project admin. A workspace MEMBER with no membership of a Secret project could PUT it to network=2 (Public) and then join via the public-project API — read plus member-level write on a project they were never in. Confirmed: an unpatched non-member PUT returns 200 and flips network. PUT now routes through partial_update so both verbs enforce the same rule; a full PUT-replace is not a meaningful operation for projects. Contract tests cover the denied PUT, that a denial writes nothing, that PUT and PATCH reach the same verdict (the defect was that they disagreed), and a positive control that an admin PUT still works. Fail-before verified: 3 failed / 1 passed unpatched, 4 passed patched. Note the payload must be complete — DRF's generic update runs the serializer with partial=False, so an incomplete body 400s on validation before authorization differs, and the test would pass either way. Co-authored-by: Plane AI <noreply@plane.so>