fix: resolve collaborator role server-side instead of trusting the request body - #1378
Merged
SheyeJDev merged 4 commits intoSep 27, 2026
Merged
Conversation
…src/services/collaboration/permissions.ts
…src/middleware/project-permission.ts
…src/services/collaboration/__tests__/permissions-enforcement.test.ts
…src/routes/collaboration.ts
|
@hi-bit-ke Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
Contributor
|
LGTM |
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.
Overview
assertPermissionenforces the role it is given, but nothing resolved whichrole a requester actually holds:
POST /projects/:projectId/deleteread therole straight out of the request body and defaulted it to
owner:Any caller could therefore send
{"role":"owner"}and delete a project. Thematrix was defined and tested; it was not enforced server-side, which is what
the issue asks for.
This PR resolves the role from the authenticated requester address against the
project's stored collaboration record, and denies by default when that record
cannot be established.
Related Issue
Closes #1320
Changes
backend/src/services/collaboration/permissions.ts— addsProjectRoleContext(a project'sownerplus collaborators with optionalroles) and two functions built on the existing matrix:
resolveRequesterRole()maps an address to its role (owner wins, unlisted →null, missing role →viewer), andassertProjectPermission()resolvesfirst and then asserts, so a role can never be supplied by the caller. It
fails closed: an unknown requester or an unavailable record throws
permission_deniedwithrole: nullandreason: "project_role_unknown".backend/src/middleware/project-permission.ts(new) —createProjectPermissionMiddleware(permission, resolveContext)composes afterthe existing
requireStellarAddress(which verifies theX-Stellar-Addressheader), resolves the role, publishes it onres.locals.collaboratorRole, and answers401when unauthenticated,400without a project id, and
403when the role is missing or insufficient.A resolver that throws is treated as "no record" rather than as access.
backend/src/routes/collaboration.ts— the delete route now runsrequireStellarAddress+ the permission middleware, and therolefield isremoved from its body schema. The response echoes the resolved role for audit.
A
setProjectRoleContextResolver()registration point supplies theauthoritative record at startup; while unregistered, the route fails closed
instead of trusting the caller.
Verification Results
The new suite covers role resolution (owner precedence, stored roles, unlisted
addresses, unusable input), fail-closed behaviour (unknown requester, unavailable
record, resolver throwing), and — the regression this fixes — that the
middleware ignores a
role: "owner"in the request body.ROLE_PERMISSIONS,getPermissionsMatrix); unchangedassertProjectPermission()+createProjectPermissionMiddleware(), applied to the delete routerolefield is removed and a claimedrole: "owner"is asserted to be ignored403 permission_denied