scope read endpoints to project membership/admins - #301
Open
shreeyaadhikari wants to merge 2 commits into
Open
Conversation
shreeyaadhikari
marked this pull request as ready for review
July 31, 2026 00:17
nourshoreibah
requested changes
Aug 12, 2026
nourshoreibah
left a comment
Collaborator
There was a problem hiding this comment.
Sorry im just getting to reviewing now, but we recently changed the names of the roles/the db enum to comply with Ashley's requests. can you confirm this is still ok given the new roles?
Collaborator
|
We also have way better context on what each role can see (see my comments on figma https://www.figma.com/design/K3ygUDFaj1b7lyLmag6Dtc/BRANCH-Designs?node-id=415-2422&p=f&m=dev) |
nourshoreibah
added a commit
that referenced
this pull request
Aug 12, 2026
* fix: address audit findings across lambdas, frontend and infra Fixes the actionable findings from a full-repo bug scan. Findings already covered by open PRs (#301-#307) are deliberately untouched. Security / data exposure: - Lock down the generated-reports S3 bucket. All four block_public_* were false and a bucket policy granted s3:GetObject to Principal "*", so reports (member emails, donor contacts, expenditure amounts) were world-readable at predictable keys. Reports are now served only through a presigned GET. - Grant the lambda role s3:PutObject/s3:GetObject on the reports bucket. It had no S3 permissions at all, so POST /reports/generate was failing AccessDenied; GetObject is additionally required because a presigned URL carries the signer's permissions. - Validate objectUrl on POST /reports against the bucket's own host, so an arbitrary (e.g. javascript:) URL can no longer be stored and rendered. - Sanitize fileName before interpolating it into an S3 key in GET /reports/upload-url. - Stop logging every user row (emails, admin flags) to CloudWatch. Correctness: - Lowercase emails in UserValidationUtils.validateEmail. POST /users stored them verbatim while POST /auth/register looks up email.toLowerCase(), so any invite containing uppercase was permanently unclaimable (403 INVITATION_REQUIRED). - POST /auth/register now checks numUpdatedRows on the invitation claim. A no-op claim previously still returned 201, leaving a Cognito user whose sub referenced no row, which broke every later login. Same check on the auto-link path. - DELETE /users/{userId} deletes the Cognito user too, so the address can be re-invited. - PATCH /users/{userId} rejects email changes. Email is the Cognito username and nothing synced it, so a change silently broke sign-in and reset. - POST /donations returns 404 for a missing donor/project instead of 500, and accepts numeric strings for amount and ids. - GET /projects/{id}/donors selects explicit columns; selectAll() over a 3-table join collided project_id and leaked the whole project row. - Reject non-numeric path ids on the users and projects {id} routes, which reached Postgres as NaN and surfaced as 500s. Features that were half-built: - Add GET /reports/{id}/download returning a presigned URL. Reports could be generated but never retrieved; the frontend used object_url only for a format label. - Wire up bulk delete on the reports page. It was stubbed behind a stale comment claiming no DELETE endpoint existed, though DELETE /reports/{id} has been there all along. - Switch the reports page to server-side pagination and surface transient delete/download failures in a non-blocking banner. - Pass report_type through POST /reports/generate and return file_type. Cleanup: - Single canonical region-qualified S3 URL helper; the two call sites disagreed and the region-less form only resolved via a redirect. - Drop the unused DonationValidationUtils import. - Reconcile the duplicated auth DTOs (isAdmin is now required in both). Full dedup needs a packaging change, since neither shared package can resolve the other without a new cross-dependency. - Gitignore lambda.zip. Users-lambda tests now mock the Cognito SDK: CI injects a real user pool id, and the DELETE tests target seeded ashley@branch.org, so they would otherwise have issued live AdminDeleteUser calls against production. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: auto-format terraform and update documentation - Auto-formatted .tf files with terraform fmt - Updated README.md with terraform-docs Co-authored-by: nourshoreibah <nourshoreibah@users.noreply.github.com> * chore: regenerate lambda READMEs * refactor(types): make @branch/types the single source of the auth DTOs The auth DTOs were declared twice, in shared/types/auth-types.d.ts and shared/lambda-auth/src/types.ts, and had already drifted (isAdmin was optional in one and required in the other). The previous commit only reconciled the two copies and left a "keep these in sync" comment, which is just documented duplication. shared/lambda-auth now takes a file: dependency on @branch/types and re-exports the DTOs from it, so there is exactly one declaration. @branch/types stays a dependency-free leaf, which is what keeps the edge acyclic; lambdas are unaffected because they already depend on both packages, and the types are erased at compile time so nothing reaches the bundle. Adding a dependency to shared/lambda-auth changes the resolved tree for every lambda, so all six package-lock.json files are regenerated. They were already stale: none recorded lambda-auth's devDependencies. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(reports): bind report object keys to their project, and review fixes Addresses the four review comments on #310. The significant one: POST /reports only checked that objectUrl pointed at the reports bucket, not that the key belonged to the project being written. A caller with access to project A could register a row with project_id A and an objectUrl under reports/B/, then read project B's report back through GET /reports/{id}/download, which authorizes off report.project_id. That defeats the access control this PR set out to add. Keys are now bound to their project via a single reportKeyPrefix() helper used for both construction and validation: POST /reports rejects a key outside the project's prefix, and the download route re-checks the stored key against report.project_id before presigning. The prefix check runs after the access check so 403/404 still take precedence. All existing rows match the prefix, since both key-construction paths already used it. Also: - Clear the reports-page selection on page change. With server-side pagination selectedIds could retain rows from a previous page and bulk delete would remove them unseen. - Skip the Cognito delete when COGNITO_USER_POOL_ID is unset instead of making a call that cannot succeed, and log it as a configuration error. cognitoDeleted already reported false in this case via the InvalidParameterException path. - Re-indent the POST /donations membership and try/catch blocks, whose closing braces read as though they closed the route. One regression test covers the cross-project key rejection. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
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.
ℹ️ Issue
Closes #246
📝 Description
Add membership + admin scoping to read endpoints so authenticated non-admins only see rows for projects they belong to; admins keep full visibility. Also fixed TypeScript/runtime issues found while applying scoping and updated tests to match the new security behavior.
Briefly list the changes made to the code:
✔️ Verification
Ran the test suite locally to verify the changes and updated the tests so they now expect the new project-scoped results for non-admin users.