Repository navigation
fix(api): authorize product and membership edits before reading the body - #678
Merged
Merged
Conversation
updateProduct, deleteProduct and updateMembership parsed their input before checking whether the caller could act, so a signed-in stranger who sent a bad body got field errors instead of a 403. Each now refuses first. updateMembership still checks the requested role after parsing it, since that check depends on the input, but only once the caller has passed the check on the membership's current role. changeMembership takes one `decide` callback that answers the change or a failure, in place of a refusal callback and a fixed change, so the change can come from input parsed after authorization. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
|
Claude finished @alukach's task in 14s —— View job ✅ No blocking issues — safe to merge. I read the diff and the
Simplify (ponytail)
DocsThe description names docs.source.coop, 💰 Estimated review cost: $0.11 · 0m13s · 5 turns |
…oduct's empty key The refusal tests send an invalid body, so they fail if parsing moves back above authorization, and the not-found tests do the same for the lookup. deleteProduct answers an empty ID as not found without asking DynamoDB, as updateProduct does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#679 lets PATCH /memberships/{id} take `state` as well as `role`, and each needs a different right: a role change the manager's, acceptance the invitee's, revocation either. This branch checked only UpdateMembershipRole before parsing, which would refuse an invitee accepting their own invitation. updateMembership now refuses, before reading the body, a caller who holds none of UpdateMembershipRole, AcceptMembership or RevokeMembership over the membership, so a stranger still learns nothing from validation. Once the body is parsed it checks the right that change needs. accept and revoke are named decisions shared by acceptInvitation, revokeMembership and updateMembership. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
updateProduct,deleteProductandupdateMembershipparsed their input before checking whether the caller could act. A signed-in stranger who sentPATCH /products/{a}/{p}with a bad body gotfield_errorsback instead of a 403, which tells them the product exists and what its schema accepts. Found in review of #676, whose guide says to authorize before validating wherever the permission doesn't depend on the input.What
updateProductanddeleteProductfetch the product and authorize first, then parse the body or query. An unknown product is nownot_foundwhatever the body, and a caller who may not act isforbiddenwhatever the body.deleteProductanswers an empty account or product ID asnot_foundwithout asking DynamoDB, asupdateProductalready does.updateMembershiprefuses, before reading the body, a caller who may make no change at all to the membership: one holding none ofUpdateMembershipRole,AcceptMembershiporRevokeMembershipover it. Since feat(api): one memberships resource at /memberships #679 the body is{ role }or{ state }, and each needs a different right, so the right that change needs is checked once the body is parsed: the manager's for a role (against the current role and the requested one), the invitee's to accept, the member's or a manager's to revoke. A caller who may change a membership can still be told its body is invalid.changeMembershiptakes onedecide(m)callback that answersok(change)or a failure, in place of arefusecallback plus a fixedchange. That letsupdateMembershipbuild its change from input it parses after authorization.acceptInvitation,rejectInvitationandrevokeMembershipmove onto it with the same behavior; the accept and revoke decisions are shared withupdateMembership.createProductandinviteMemberstay as they are: their permission depends on the parsed input.withApiSessionstill answers a body that isn't JSON at all with 400 before any operation runs; that says nothing about the resource, and moving it would change every route, so it is out of scope here.Docs, ADRs and
llms.txtNo user-facing flow or endpoint changes, so docs.source.coop and
llms.txtstill hold. The data-proxy ADRs describe who may act, not the order of checks, so none change.API.mdin #676 already states this convention; once this merges, the matching line in #676's "Where the code doesn't follow the guide yet" list can go.Testing
updateMembershiptest gives the caller one right at a time. It fails if the pre-parse check goes back to the role right alone, which would refuse an invitee accepting.main(with feat(api): one memberships resource at /memberships #679):jest src/lib/operations src/app/api src/lib/actions/memberships.test.ts, 16 suites, 201 tests pass.npm run type-checkpasses.🤖 Generated with Claude Code