scope read endpoints to project membership/admins - #301

Open
shreeyaadhikari wants to merge 2 commits into
mainfrom
ownership-membership-check
Open

scope read endpoints to project membership/admins#301
shreeyaadhikari wants to merge 2 commits into
mainfrom
ownership-membership-check

Conversation

@shreeyaadhikari

@shreeyaadhikarishreeyaadhikari commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

ℹ️ 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:

  • Added membership/admin scoping to read endpoints: donors/handler.ts for GET /donors, GET /donations
  • enforce project-membership checks for GET /expenditures in expenditures/handler.ts
  • reports/handler.ts: scope GET /reports.
  • Tests: Updated expectations to reflect scoped results for non-admin users in donors.test.ts

✔️ 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.

imageimage

@shreeyaadhikarishreeyaadhikari self-assigned this Jul 30, 2026
@shreeyaadhikari
shreeyaadhikari marked this pull request as ready for review July 31, 2026 00:17
@github-actions
github-actionsBot requested a review from Rayna-YuJuly 31, 2026 00:17
github-actionsBot added a commit that referenced this pull request Jul 31, 2026

@nourshoreibahnourshoreibah left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@nourshoreibah

Copy link
Copy Markdown
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>
Sign up for freeto 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.

[Security] List endpoints leak data across users (no ownership/membership check)

2 participants

@shreeyaadhikari@nourshoreibah
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

scope read endpoints to project membership/admins - #301

Open
shreeyaadhikari wants to merge 2 commits into
mainfrom
ownership-membership-check
Open

scope read endpoints to project membership/admins#301
shreeyaadhikari wants to merge 2 commits into
mainfrom
ownership-membership-check

Conversation

@shreeyaadhikari

@shreeyaadhikarishreeyaadhikari commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

ℹ️ 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:

  • Added membership/admin scoping to read endpoints: donors/handler.ts for GET /donors, GET /donations
  • enforce project-membership checks for GET /expenditures in expenditures/handler.ts
  • reports/handler.ts: scope GET /reports.
  • Tests: Updated expectations to reflect scoped results for non-admin users in donors.test.ts

✔️ 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.

imageimage

@shreeyaadhikarishreeyaadhikari self-assigned this Jul 30, 2026
@shreeyaadhikari
shreeyaadhikari marked this pull request as ready for review July 31, 2026 00:17
@github-actions
github-actionsBot requested a review from Rayna-YuJuly 31, 2026 00:17
github-actionsBot added a commit that referenced this pull request Jul 31, 2026

@nourshoreibahnourshoreibah left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@nourshoreibah

Copy link
Copy Markdown
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>
Sign up for freeto 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.

[Security] List endpoints leak data across users (no ownership/membership check)

2 participants

@shreeyaadhikari@nourshoreibah
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

scope read endpoints to project membership/admins - #301

Open
shreeyaadhikari wants to merge 2 commits into
mainfrom
ownership-membership-check
Open

scope read endpoints to project membership/admins#301
shreeyaadhikari wants to merge 2 commits into
mainfrom
ownership-membership-check

Conversation

@shreeyaadhikari

@shreeyaadhikarishreeyaadhikari commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

ℹ️ 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:

  • Added membership/admin scoping to read endpoints: donors/handler.ts for GET /donors, GET /donations
  • enforce project-membership checks for GET /expenditures in expenditures/handler.ts
  • reports/handler.ts: scope GET /reports.
  • Tests: Updated expectations to reflect scoped results for non-admin users in donors.test.ts

✔️ 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.

imageimage

@shreeyaadhikarishreeyaadhikari self-assigned this Jul 30, 2026
@shreeyaadhikari
shreeyaadhikari marked this pull request as ready for review July 31, 2026 00:17
@github-actions
github-actionsBot requested a review from Rayna-YuJuly 31, 2026 00:17
github-actionsBot added a commit that referenced this pull request Jul 31, 2026

@nourshoreibahnourshoreibah left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@nourshoreibah

Copy link
Copy Markdown
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>
Sign up for freeto 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.

[Security] List endpoints leak data across users (no ownership/membership check)

2 participants

@shreeyaadhikari@nourshoreibah
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

scope read endpoints to project membership/admins - #301

Open
shreeyaadhikari wants to merge 2 commits into
mainfrom
ownership-membership-check
Open

scope read endpoints to project membership/admins#301
shreeyaadhikari wants to merge 2 commits into
mainfrom
ownership-membership-check

Conversation

@shreeyaadhikari

@shreeyaadhikarishreeyaadhikari commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

ℹ️ 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:

  • Added membership/admin scoping to read endpoints: donors/handler.ts for GET /donors, GET /donations
  • enforce project-membership checks for GET /expenditures in expenditures/handler.ts
  • reports/handler.ts: scope GET /reports.
  • Tests: Updated expectations to reflect scoped results for non-admin users in donors.test.ts

✔️ 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.

imageimage

@shreeyaadhikarishreeyaadhikari self-assigned this Jul 30, 2026
@shreeyaadhikari
shreeyaadhikari marked this pull request as ready for review July 31, 2026 00:17
@github-actions
github-actionsBot requested a review from Rayna-YuJuly 31, 2026 00:17
github-actionsBot added a commit that referenced this pull request Jul 31, 2026

@nourshoreibahnourshoreibah left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@nourshoreibah

Copy link
Copy Markdown
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>
Sign up for freeto 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.

[Security] List endpoints leak data across users (no ownership/membership check)

2 participants

@shreeyaadhikari@nourshoreibah
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

scope read endpoints to project membership/admins - #301

Open
shreeyaadhikari wants to merge 2 commits into
mainfrom
ownership-membership-check
Open

scope read endpoints to project membership/admins#301
shreeyaadhikari wants to merge 2 commits into
mainfrom
ownership-membership-check

Conversation

@shreeyaadhikari

@shreeyaadhikarishreeyaadhikari commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

ℹ️ 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:

  • Added membership/admin scoping to read endpoints: donors/handler.ts for GET /donors, GET /donations
  • enforce project-membership checks for GET /expenditures in expenditures/handler.ts
  • reports/handler.ts: scope GET /reports.
  • Tests: Updated expectations to reflect scoped results for non-admin users in donors.test.ts

✔️ 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.

imageimage

@shreeyaadhikarishreeyaadhikari self-assigned this Jul 30, 2026
@shreeyaadhikari
shreeyaadhikari marked this pull request as ready for review July 31, 2026 00:17
@github-actions
github-actionsBot requested a review from Rayna-YuJuly 31, 2026 00:17
github-actionsBot added a commit that referenced this pull request Jul 31, 2026

@nourshoreibahnourshoreibah left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@nourshoreibah

Copy link
Copy Markdown
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>
Sign up for freeto 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.

[Security] List endpoints leak data across users (no ownership/membership check)

2 participants

@shreeyaadhikari@nourshoreibah
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

scope read endpoints to project membership/admins - #301

Open
shreeyaadhikari wants to merge 2 commits into
mainfrom
ownership-membership-check
Open

scope read endpoints to project membership/admins#301
shreeyaadhikari wants to merge 2 commits into
mainfrom
ownership-membership-check

Conversation

@shreeyaadhikari

@shreeyaadhikarishreeyaadhikari commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

ℹ️ 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:

  • Added membership/admin scoping to read endpoints: donors/handler.ts for GET /donors, GET /donations
  • enforce project-membership checks for GET /expenditures in expenditures/handler.ts
  • reports/handler.ts: scope GET /reports.
  • Tests: Updated expectations to reflect scoped results for non-admin users in donors.test.ts

✔️ 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.

imageimage

@shreeyaadhikarishreeyaadhikari self-assigned this Jul 30, 2026
@shreeyaadhikari
shreeyaadhikari marked this pull request as ready for review July 31, 2026 00:17
@github-actions
github-actionsBot requested a review from Rayna-YuJuly 31, 2026 00:17
github-actionsBot added a commit that referenced this pull request Jul 31, 2026

@nourshoreibahnourshoreibah left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@nourshoreibah

Copy link
Copy Markdown
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>
Sign up for freeto 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.

[Security] List endpoints leak data across users (no ownership/membership check)

2 participants

@shreeyaadhikari@nourshoreibah
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

scope read endpoints to project membership/admins - #301

Open
shreeyaadhikari wants to merge 2 commits into
mainfrom
ownership-membership-check
Open

scope read endpoints to project membership/admins#301
shreeyaadhikari wants to merge 2 commits into
mainfrom
ownership-membership-check

Conversation

@shreeyaadhikari

@shreeyaadhikarishreeyaadhikari commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

ℹ️ 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:

  • Added membership/admin scoping to read endpoints: donors/handler.ts for GET /donors, GET /donations
  • enforce project-membership checks for GET /expenditures in expenditures/handler.ts
  • reports/handler.ts: scope GET /reports.
  • Tests: Updated expectations to reflect scoped results for non-admin users in donors.test.ts

✔️ 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.

imageimage

@shreeyaadhikarishreeyaadhikari self-assigned this Jul 30, 2026
@shreeyaadhikari
shreeyaadhikari marked this pull request as ready for review July 31, 2026 00:17
@github-actions
github-actionsBot requested a review from Rayna-YuJuly 31, 2026 00:17
github-actionsBot added a commit that referenced this pull request Jul 31, 2026

@nourshoreibahnourshoreibah left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@nourshoreibah

Copy link
Copy Markdown
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>
Sign up for freeto 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.

[Security] List endpoints leak data across users (no ownership/membership check)

2 participants

@shreeyaadhikari@nourshoreibah
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

scope read endpoints to project membership/admins - #301

Open
shreeyaadhikari wants to merge 2 commits into
mainfrom
ownership-membership-check
Open

scope read endpoints to project membership/admins#301
shreeyaadhikari wants to merge 2 commits into
mainfrom
ownership-membership-check

Conversation

@shreeyaadhikari

@shreeyaadhikarishreeyaadhikari commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

ℹ️ 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:

  • Added membership/admin scoping to read endpoints: donors/handler.ts for GET /donors, GET /donations
  • enforce project-membership checks for GET /expenditures in expenditures/handler.ts
  • reports/handler.ts: scope GET /reports.
  • Tests: Updated expectations to reflect scoped results for non-admin users in donors.test.ts

✔️ 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.

imageimage

@shreeyaadhikarishreeyaadhikari self-assigned this Jul 30, 2026
@shreeyaadhikari
shreeyaadhikari marked this pull request as ready for review July 31, 2026 00:17
@github-actions
github-actionsBot requested a review from Rayna-YuJuly 31, 2026 00:17
github-actionsBot added a commit that referenced this pull request Jul 31, 2026

@nourshoreibahnourshoreibah left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@nourshoreibah

Copy link
Copy Markdown
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>
Sign up for freeto 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.

[Security] List endpoints leak data across users (no ownership/membership check)

2 participants

@shreeyaadhikari@nourshoreibah