Skip to content

fix: enforce integration ownership on Google Sheets export mutation (NES-1489)#8909

Open
jianwei1 wants to merge 10 commits intomainfrom
jianweichong/nes-1489-google-sync-only-works-for-team-creator-not-for-other
Open

fix: enforce integration ownership on Google Sheets export mutation (NES-1489)#8909
jianwei1 wants to merge 10 commits intomainfrom
jianweichong/nes-1489-google-sync-only-works-for-team-creator-not-for-other

Conversation

@jianwei1
Copy link
Copy Markdown
Contributor

@jianwei1 jianwei1 commented Mar 25, 2026

Summary

  • Root cause: The frontend sync dialog calls journeyVisitorExportToGoogleSheet (not googleSheetsSyncCreate). The original PR targeted the wrong mutation.
  • Privacy fix: Added integration ownership (userId) and team-scoping (teamId) checks to journeyVisitorExportToGoogleSheet — the mutation the frontend actually uses. Previously, any authenticated team member could create syncs using another user's Google account via direct API calls.
  • Reverted googleSheetsSyncCreate back to main — its original isIntegrationOwner withAuth guard was already correct. This mutation is not called by any frontend code.
  • Companion PR: fix: filter Google sync dropdown to only show current user's accounts (NES-1492) #8958 filters the frontend dropdown to only show the current user's integrations (NES-1492)

What changed

File Change
googleSheetsSyncCreate.mutation.ts Reverted to main (original auth was correct)
googleSheetsSyncCreate.mutation.spec.ts Reverted to main
journeyVisitorExportToGoogleSheet.mutation.ts Added ownership + team-scoping guards
journeyVisitorExportToGoogleSheet.mutation.spec.ts Added 2 new test cases

Permission model after fix

journeyVisitorExportToGoogleSheet (the mutation the frontend calls):

  • isAuthenticated — must be logged in
  • ability(Action.Export, ...) — must be team manager/member or journey owner
  • integration.teamId === journey.teamId — integration must belong to the journey's team
  • integration.userId === context.user.id — must own the integration (privacy: only use your own Google account)

How it works with PR #8958

Together, these PRs enable any team member to create Google syncs:

  1. Frontend (PR fix: filter Google sync dropdown to only show current user's accounts (NES-1492) #8958): dropdown only shows the user's own integrations + "Add New Google Account" button
  2. Backend (this PR): enforces ownership even against direct API calls

Test plan

  • Verify googleSheetsSyncCreate tests pass (7 tests — reverted to main)
  • Verify journeyVisitorExportToGoogleSheet tests pass (16 tests — 2 new)
  • Verify non-owner gets "You can only create syncs using your own Google account" error
  • Verify cross-team integration gets "Integration does not belong to this journey's team" error
  • With PR fix: filter Google sync dropdown to only show current user's accounts (NES-1492) #8958: verify a non-creator team member can create a sync using their own Google account

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced authorization validation for Google Sheet exports to verify integration ownership and team association before processing requests.
  • Tests

    • Added test coverage for authorization validation scenarios in Google Sheet exports.
  • Documentation

    • Added documentation describing authorization validation updates.

Replace isIntegrationOwner withAuth guard with isAuthenticated + resolver-level
permission check (isIntegrationOwner || isTeamManager), matching the pattern
used by googleSheetsSyncDelete and googleSheetsSyncBackfill.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@stage-branch-merger
Copy link
Copy Markdown
Contributor

I see you added the "on stage" label, I'll get this merged to the stage branch!

@linear
Copy link
Copy Markdown

linear bot commented Mar 25, 2026

@jianwei1 jianwei1 self-assigned this Mar 25, 2026
@nx-cloud
Copy link
Copy Markdown

nx-cloud bot commented Mar 25, 2026

View your CI Pipeline Execution ↗ for commit 265f92d

Command Status Duration Result
nx affected --target=subgraph-check --base=2f46... ✅ Succeeded 2s View ↗
nx affected --target=extract-translations --bas... ✅ Succeeded <1s View ↗
nx affected --target=lint --base=2f46277387999c... ✅ Succeeded 26s View ↗
nx affected --target=type-check --base=2f462773... ✅ Succeeded 21s View ↗
nx run-many --target=codegen --all --parallel=3 ✅ Succeeded 2s View ↗
nx run-many --target=prisma-generate --all --pa... ✅ Succeeded 4s View ↗

☁️ Nx Cloud last updated this comment at 2026-04-14 21:06:10 UTC

@jianwei1 jianwei1 requested a review from mikeallisonJS March 25, 2026 20:23
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 25, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 68ea2bb9-5401-4e87-9e66-76b374a22c44

📥 Commits

Reviewing files that changed from the base of the PR and between 78af4de and c437ba7.

📒 Files selected for processing (3)
  • apis/api-journeys-modern/src/schema/journeyVisitor/journeyVisitorExportToGoogleSheet.mutation.spec.ts
  • apis/api-journeys-modern/src/schema/journeyVisitor/journeyVisitorExportToGoogleSheet.mutation.ts
  • docs/plans/2026-03-25-002-fix-google-sync-permission-team-manager-plan.md

Walkthrough

This pull request adds authorization validation to the journeyVisitorExportToGoogleSheet mutation to ensure a provided Google integration belongs to the requesting user and the journey's team before accessing Google APIs. The implementation includes corresponding test cases and documentation.

Changes

Cohort / File(s) Summary
Authorization Validation
apis/api-journeys-modern/src/schema/journeyVisitor/journeyVisitorExportToGoogleSheet.mutation.ts, apis/api-journeys-modern/src/schema/journeyVisitor/journeyVisitorExportToGoogleSheet.mutation.spec.ts
Added integration ownership and team membership validation checks. Extended integration lookup to select userId and teamId, then reject with FORBIDDEN error if either does not match the requesting context. Added two test cases verifying rejection behavior when integration user or team mismatches occur.
Documentation
docs/plans/2026-03-25-002-fix-google-sync-permission-team-manager-plan.md
Added security fix plan documenting the authorization guardrails added to the mutation, implementation details, affected files, and test coverage for forbidden conditions.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: adding integration ownership enforcement to the Google Sheets export mutation, with the issue identifier confirming relevance.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jianweichong/nes-1489-google-sync-only-works-for-team-creator-not-for-other

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.

Copy link
Copy Markdown
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)
apis/api-journeys-modern/src/schema/googleSheetsSync/googleSheetsSyncCreate.mutation.ts (1)

44-57: Consider extracting the manager-or-owner check into a shared helper.

This permission block now mirrors the same logic used in the other Google Sheets sync mutations, so centralizing it would make future auth changes less likely to drift between create/delete/backfill.

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

In
`@apis/api-journeys-modern/src/schema/googleSheetsSync/googleSheetsSyncCreate.mutation.ts`
around lines 44 - 57, Extract the duplicated permission check into a shared
helper (e.g., isManagerOrOwner(userId, journey, googleIntegration)) that returns
a boolean; replace the inline logic that computes
isTeamManager/isIntegrationOwner in googleSheetsSyncCreate (and mirror in other
Google Sheets sync mutations) with a call to that helper, and keep the same
GraphQLError('Forbidden', { extensions: { code: 'FORBIDDEN' } }) throw when the
helper returns false; ensure the helper checks journey.team?.userTeams for a
userTeam with matching userId and role === 'manager' and verifies
googleIntegration.userId === userId so behavior of the existing variables
(isTeamManager, isIntegrationOwner) is preserved.
apis/api-journeys-modern/src/schema/googleSheetsSync/googleSheetsSyncCreate.mutation.spec.ts (1)

243-300: Consider adding a manager-without-export-permission case.

The new branch introduced here is “team manager but not integration owner,” but the existing export-denied test still only exercises the owner path. Adding the same manager fixture with mockAbility.mockReturnValue(false) would lock down that the new branch still respects the export gate.

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

In
`@apis/api-journeys-modern/src/schema/googleSheetsSync/googleSheetsSyncCreate.mutation.spec.ts`
around lines 243 - 300, Add a new test that covers the "manager without export
permission" path: reuse the same mockJourney and mockIntegration fixtures from
the existing case but set mockAbility.mockReturnValue(false) (or
mockReturnValueOnce) to simulate export permission denied, invoke authClient
with GOOGLE_SHEETS_SYNC_CREATE_MUTATION and the same variables, then assert the
mutation is rejected (e.g., data.googleSheetsSyncCreate is null and errors
contain the permission/authorization message) and verify
prismaMock.googleSheetsSync.create was not called; this ensures the code paths
in the googleSheetsSyncCreate resolver enforce the export gate for managers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@apis/api-journeys-modern/src/schema/googleSheetsSync/googleSheetsSyncCreate.mutation.spec.ts`:
- Around line 243-300: Add a new test that covers the "manager without export
permission" path: reuse the same mockJourney and mockIntegration fixtures from
the existing case but set mockAbility.mockReturnValue(false) (or
mockReturnValueOnce) to simulate export permission denied, invoke authClient
with GOOGLE_SHEETS_SYNC_CREATE_MUTATION and the same variables, then assert the
mutation is rejected (e.g., data.googleSheetsSyncCreate is null and errors
contain the permission/authorization message) and verify
prismaMock.googleSheetsSync.create was not called; this ensures the code paths
in the googleSheetsSyncCreate resolver enforce the export gate for managers.

In
`@apis/api-journeys-modern/src/schema/googleSheetsSync/googleSheetsSyncCreate.mutation.ts`:
- Around line 44-57: Extract the duplicated permission check into a shared
helper (e.g., isManagerOrOwner(userId, journey, googleIntegration)) that returns
a boolean; replace the inline logic that computes
isTeamManager/isIntegrationOwner in googleSheetsSyncCreate (and mirror in other
Google Sheets sync mutations) with a call to that helper, and keep the same
GraphQLError('Forbidden', { extensions: { code: 'FORBIDDEN' } }) throw when the
helper returns false; ensure the helper checks journey.team?.userTeams for a
userTeam with matching userId and role === 'manager' and verifies
googleIntegration.userId === userId so behavior of the existing variables
(isTeamManager, isIntegrationOwner) is preserved.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 250abc16-e81c-4e45-b48b-0245ff2027b4

📥 Commits

Reviewing files that changed from the base of the PR and between 2e47fe9 and 78af4de.

📒 Files selected for processing (2)
  • apis/api-journeys-modern/src/schema/googleSheetsSync/googleSheetsSyncCreate.mutation.spec.ts
  • apis/api-journeys-modern/src/schema/googleSheetsSync/googleSheetsSyncCreate.mutation.ts

@stage-branch-merger
Copy link
Copy Markdown
Contributor

Merge conflict attempting to merge this into stage. Please fix manually.

stage-branch-merger bot added a commit that referenced this pull request Mar 30, 2026
@jianwei1 jianwei1 removed the on stage label Apr 6, 2026
jianwei1 and others added 2 commits April 7, 2026 09:31
…tation (NES-1489)

The frontend uses journeyVisitorExportToGoogleSheet (not googleSheetsSyncCreate),
so the original PR targeted the wrong mutation. This commit:

- Reverts googleSheetsSyncCreate to main (original isIntegrationOwner guard was correct)
- Adds integration ownership + team-scoping checks to journeyVisitorExportToGoogleSheet
- Moves integration validation before Google API token fetch for defense-in-depth
- Updates plan document with revised root cause analysis

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jianwei1 jianwei1 changed the title fix: allow team managers to create Google Sheets syncs (NES-1489) fix: enforce integration ownership on Google Sheets export mutation (NES-1489) Apr 6, 2026
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