Skip to content

fix: enforce team scoping for campaign, contacts, and invites#356

Merged
KMKoushik merged 2 commits intomainfrom
fix/team-scope-authorization
Feb 23, 2026
Merged

fix: enforce team scoping for campaign, contacts, and invites#356
KMKoushik merged 2 commits intomainfrom
fix/team-scope-authorization

Conversation

@KMKoushik
Copy link
Member

@KMKoushik KMKoushik commented Feb 22, 2026

Summary

  • enforce team ownership checks when assigning and reading campaign contact books in the campaign router
  • scope public get-contact lookups to the validated contact book so cross-book contact IDs are not returned
  • scope resendTeamInvite by teamId and add focused regression tests for campaign, contacts API, and team invite flows
  • make Stripe webhook API test deterministic by mocking STRIPE_WEBHOOK_SECRET as undefined in that test file

Verification

  • pnpm exec vitest run -c vitest.trpc.config.ts src/server/api/routers/campaign-security.trpc.test.ts src/server/api/routers/team-security.trpc.test.ts
  • pnpm exec vitest run -c vitest.api.config.ts src/server/public-api/api/contacts/get-contact.api.test.ts src/app/api/webhook/stripe/route.api.test.ts

Summary by cubic

Enforces team scoping for campaigns, public contact lookups, and invite resends to prevent cross-team access. Adds focused authorization tests and makes the Stripe webhook test deterministic.

  • Bug Fixes
    • Campaign: validate contactBook belongs to the current team on update and when reading details.
    • Public API: scope GET /v1/contactBooks/{contactBookId}/contacts/{contactId} to the contact book (use findFirst with contactBookId).
    • Team invites: require teamId and scope resendTeamInvite by team to block cross-team resends.
    • Tests: mock STRIPE_WEBHOOK_SECRET and domain validation in campaign security tests for deterministic behavior.

Written for commit ce4eec8. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Enforced team-level scoping for contacts and contact books to prevent cross-team access
    • Tightened resend-invite behavior to ensure invites are validated within the correct team
  • Tests

    • Added authorization tests for campaign contact-book assignments and team invite resending
    • Added API tests for contact retrieval and a Stripe webhook test exercising the "missing webhook secret" path

@vercel
Copy link

vercel bot commented Feb 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
unsend-marketing Ready Ready Preview, Comment Feb 23, 2026 0:28am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 22, 2026

No actionable comments were generated in the recent review. 🎉


Walkthrough

Database lookups and service calls are scoped to team or contact-book context across multiple routes and services. Contact and contactBook queries now include contactBookId or teamId filters; team invite resend now requires (teamId, inviteId, teamName) and uses a team-scoped findFirst; tests were added to assert authorization behavior for campaign updates, team invite resending, contact retrieval, and Stripe webhook secret handling.

Possibly related PRs

  • fix: enforce contact book ownership #341: Enforces contact-book/team scoping in contact-related lookups and updates, aligning with this PR's changes to require ownership filters and prevent cross-team/contact-book access.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: enforce team scoping for campaign, contacts, and invites' directly and comprehensively summarizes the main changes: adding team ownership checks and scoping for campaigns, contacts, and invites across multiple router files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Feb 22, 2026

Deploying usesend with  Cloudflare Pages  Cloudflare Pages

Latest commit: ce4eec8
Status: ✅  Deploy successful!
Preview URL: https://26462adc.usesend.pages.dev
Branch Preview URL: https://fix-team-scope-authorization.usesend.pages.dev

View logs

@greptile-apps
Copy link

greptile-apps bot commented Feb 22, 2026

Greptile Summary

This PR adds critical team-scoping security checks across campaign, contact, and team invite operations to prevent unauthorized cross-team data access.

Key security improvements:

  • Campaign contact book assignment now validates teamId in both updateCampaign and getCampaign endpoints (campaign.ts:131, 194)
  • Public API contact lookup changed from findUnique to findFirst with explicit contactBookId scoping to prevent cross-book contact access (get-contact.ts:59-62)
  • Team invite resend operation now requires teamId match, preventing admins from resending invites belonging to other teams (team-service.ts:315-321)

Test coverage:

  • Three new security-focused test files validate the authorization fixes with focused regression tests
  • Stripe webhook test made deterministic by mocking STRIPE_WEBHOOK_SECRET as undefined

All changes follow the repository's testing conventions and use proper mocking patterns.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk - it closes security vulnerabilities without breaking changes
  • The PR addresses critical authorization vulnerabilities by adding team-scoping checks across multiple endpoints. All changes are focused security fixes with comprehensive test coverage. The implementation follows established patterns in the codebase (using composite where clauses with teamId). Tests validate the exact security scenarios being fixed. No breaking changes to API contracts.
  • No files require special attention

Last reviewed commit: 57852f7

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

8 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
apps/web/src/server/service/team-service.ts (1)

310-322: Team-scoped invite lookup is correct and consistent with deleteTeamInvite.

The switch from findUnique (global by id) to findFirst filtered by both teamId and id properly prevents cross-team invite resends. One minor nit: id: { equals: inviteId } can be simplified to id: inviteId — Prisma treats them equivalently in findFirst.

🔧 Optional: simplify the where clause
     const invite = await db.teamInvite.findFirst({
       where: {
         teamId,
-        id: {
-          equals: inviteId,
-        },
+        id: inviteId,
       },
     });

Same simplification could apply to deleteTeamInvite at Line 339–344 for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/server/service/team-service.ts` around lines 310 - 322, Simplify
the Prisma where clauses by replacing the verbose id: { equals: inviteId } with
the shorthand id: inviteId in the team-scoped lookup in resendTeamInvite (the
db.teamInvite.findFirst call) and apply the same simplification to the
corresponding deleteTeamInvite lookup (where db.teamInvite is queried) for
consistency.
apps/web/src/server/api/routers/team-security.trpc.test.ts (1)

28-28: Note: ~/env is not mocked — works only for the error path.

The resendTeamInvite service uses env.NEXTAUTH_URL after finding an invite. Since the current test only covers the null (not found) path, this works fine. If you later add a happy-path test, you'll need to mock ~/env as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/server/api/routers/team-security.trpc.test.ts` at line 28, The
test only mocks the webhook service but not the environment, so future
happy-path tests for the resendTeamInvite flow will fail because
resendTeamInvite reads env.NEXTAUTH_URL; update the test
(apps/web/src/server/api/routers/team-security.trpc.test.ts) to mock ~/env
before importing or invoking resendTeamInvite (use vi.mock for '~/env' to
provide a NEXTAUTH_URL value) so the service sees a valid NEXTAUTH_URL during
happy-path tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/web/src/server/api/routers/team-security.trpc.test.ts`:
- Line 28: The test only mocks the webhook service but not the environment, so
future happy-path tests for the resendTeamInvite flow will fail because
resendTeamInvite reads env.NEXTAUTH_URL; update the test
(apps/web/src/server/api/routers/team-security.trpc.test.ts) to mock ~/env
before importing or invoking resendTeamInvite (use vi.mock for '~/env' to
provide a NEXTAUTH_URL value) so the service sees a valid NEXTAUTH_URL during
happy-path tests.

In `@apps/web/src/server/service/team-service.ts`:
- Around line 310-322: Simplify the Prisma where clauses by replacing the
verbose id: { equals: inviteId } with the shorthand id: inviteId in the
team-scoped lookup in resendTeamInvite (the db.teamInvite.findFirst call) and
apply the same simplification to the corresponding deleteTeamInvite lookup
(where db.teamInvite is queried) for consistency.

@KMKoushik KMKoushik merged commit 61dfcee into main Feb 23, 2026
6 checks passed
@KMKoushik KMKoushik deleted the fix/team-scope-authorization branch February 23, 2026 00:30
KMKoushik added a commit that referenced this pull request Feb 24, 2026
* fix: enforce team-scoped lookups for campaign contacts and invites

* fix(test): mock domain service in campaign security test
Sign up for free to 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.

1 participant