Skip to content

fix(connectors): stop granting act-as-artist authority off the flat account_artist_ids roster edge (P0b)#772

Merged
sweetmantech merged 2 commits into
mainfrom
fix/connector-authority-roster-cut
Jul 9, 2026
Merged

fix(connectors): stop granting act-as-artist authority off the flat account_artist_ids roster edge (P0b)#772
sweetmantech merged 2 commits into
mainfrom
fix/connector-authority-roster-cut

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary

P0b interim cut per the decision callout in recoupable/chat#1860 (sweetman, 2026-07-08): connector paths no longer treat a bare account_artist_ids roster row as act-as-artist authority. Two accounts that both roster canonical artist A can no longer use each other's OAuth connectors for A.

What changed

  • New lib/composio/checkConnectorAuthority.ts — connector-scoped authority check. Grants on: self, RECOUP_ORG staff (admin bypass, kept), org co-membership with the target artist, workspace ownership, or membership in a target organization. It never reads the account_artist_ids roster edge. Fails closed on DB errors.
  • Narrowed call sites to the new check (was checkAccountAccess / checkAccountArtistAccess):
    • lib/composio/toolRouter/getComposioTools.ts (artist tool branch)
    • lib/composio/connectors/validateAuthorizeConnectorRequest.ts
    • lib/composio/connectors/validateExecuteConnectorActionRequest.ts (the account_id override on POST /api/connectors/actions)
    • lib/composio/connectors/validateDisconnectConnectorRequest.ts
  • Unchanged by design (scope constraint from the issue): checkAccountArtistAccess still honors the roster edge — read/analytics paths (posts, fans, socials, artist update/delete, YouTube revenue MCP tool) keep working. Connector read paths (GET /api/connectors, GET /api/connectors/actions listing) also keep the flat check per the issue's authorize/execute/disconnect scope.

Verification

  • TDD: new + updated tests written first and confirmed RED (module missing / old mocks), then implemented to GREEN.
  • pnpm exec vitest run lib/composio lib/artists: 51 files, 272 tests, all passing (includes 8 new tests in lib/composio/__tests__/checkConnectorAuthority.test.ts covering self, roster-only denial, org co-membership, RECOUP_ORG bypass, workspace, org target, and both fail-closed paths).
  • pnpm exec tsc --noEmit: 200 pre-existing errors on this branch and on main baseline (all in unrelated __tests__ files); zero errors introduced by this change.

Preview verification pending — tracked in chat#1860.

🤖 Generated with Claude Code


Summary by cubic

Connector actions and artist tool exposure no longer treat a bare account_artist_ids roster row as act-as-artist authority. Managers who only share a rostered artist can’t use each other’s OAuth connectors for that artist.

  • Bug Fixes
    • Added lib/composio/checkConnectorAuthority.ts to enforce connector authority; fails closed on DB errors.
    • Authority grants only for: self, RECOUP_ORG staff, shared org with the target artist, caller’s workspace, or caller’s organization.
    • Updated call sites to use the new check: validateAuthorizeConnectorRequest, validateExecuteConnectorActionRequest, validateDisconnectConnectorRequest, and getComposioTools (artist branch).
    • Kept checkAccountArtistAccess for read/analytics and connector listings; roster-based access still applies there.

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

Review in cubic

…ccount_artist_ids roster edge (P0b)

Interim cut per chat#1860 decision (2026-07-08): connector authorize,
execute, disconnect, and the getComposioTools artist branch now require
self, org co-membership, workspace ownership, org membership, or
RECOUP_ORG staff - never a bare account_artist_ids roster row.

checkAccountArtistAccess is unchanged; read/analytics paths keep
roster-based access.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@vercel

vercel Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
api Ready Ready Preview Jul 9, 2026 4:13pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@sweetmantech, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 48 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e6b49fc2-e298-401a-b71c-e9b1ba8e435a

📥 Commits

Reviewing files that changed from the base of the PR and between aba6835 and b019813.

⛔ Files ignored due to path filters (5)
  • lib/composio/__tests__/checkConnectorAuthority.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/composio/connectors/__tests__/validateAuthorizeConnectorRequest.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/composio/connectors/__tests__/validateDisconnectConnectorRequest.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/composio/connectors/__tests__/validateExecuteConnectorActionRequest.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/composio/toolRouter/__tests__/getComposioTools.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (5)
  • lib/composio/checkConnectorAuthority.ts
  • lib/composio/connectors/validateAuthorizeConnectorRequest.ts
  • lib/composio/connectors/validateDisconnectConnectorRequest.ts
  • lib/composio/connectors/validateExecuteConnectorActionRequest.ts
  • lib/composio/toolRouter/getComposioTools.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/connector-authority-roster-cut

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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 10 files

Confidence score: 2/5

  • In lib/composio/checkConnectorAuthority.ts, lookup errors returning null can fall through as if no org rows were found, which may incorrectly grant connector authority; merging as-is risks an authorization bypass and unintended access — return false explicitly on artist-org/account-org lookup errors (and add a regression test for the error path) before merging.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lib/composio/checkConnectorAuthority.ts">

<violation number="1" location="lib/composio/checkConnectorAuthority.ts:42">
P2: Connector authority can still be granted after an artist-org or account-org lookup error because `null` is treated like “no rows” and the function falls through to later grants. Returning `false` explicitly on those `null` results keeps the fail-closed contract for connector operations.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Client
    participant API as API Route (authorize/execute/disconnect)
    participant V as validate*Request
    participant CCA as checkConnectorAuthority
    participant GetOrgs as getAccountOrganizations
    participant SelArtOrgs as selectArtistOrganizationIds
    participant SelAccOrgs as selectAccountOrganizationIds
    participant SelWs as selectAccountWorkspaceId
    participant VOA as validateOrganizationAccess
    participant DB

    Note over Client,V: NEW: Connector authority flow (P0b cut)

    Client->>API: POST /api/connectors/authorize (or execute/disconnect) with account_id
    API->>V: validate request
    V->>CCA: checkConnectorAuthority(callerId, targetAccountId)

    CCA->>CCA: target === caller? (self)
    alt Self-access
        CCA-->>V: true
    else Not self
        CCA->>GetOrgs: getAccountOrganizations(callerId)
        GetOrgs->>DB: query caller orgs
        DB-->>GetOrgs: rows
        alt Caller is RECOUP_ORG staff
            CCA-->>V: true
        else Not staff
            CCA->>SelArtOrgs: selectArtistOrganizationIds(target)
            SelArtOrgs->>DB: query target artist orgs
            DB-->>SelArtOrgs: rows or null
            alt DB error (null)
                CCA-->>V: false (fail closed)
            else Has orgs
                CCA->>SelAccOrgs: selectAccountOrganizationIds(caller, orgIds)
                SelAccOrgs->>DB: query caller orgs in set
                DB-->>SelAccOrgs: rows or null
                alt Shared org found
                    CCA-->>V: true
                else No shared org
                    CCA->>SelWs: selectAccountWorkspaceId(caller, target)
                    SelWs->>DB: check workspace ownership
                    DB-->>SelWs: record or null
                    alt Workspace owned
                        CCA-->>V: true
                    else Not workspace
                        CCA->>VOA: validateOrganizationAccess(caller, target)
                        VOA->>DB: check org membership
                        DB-->>VOA: boolean
                        alt Belongs to target org
                            CCA-->>V: true
                        else Denied
                            CCA-->>V: false
                        end
                    end
                end
            else No orgs (target has no orgs)
                CCA->>SelWs: selectAccountWorkspaceId(caller, target)
                SelWs->>DB: ...
                alt Workspace owned
                    CCA-->>V: true
                else Not workspace
                    CCA->>VOA: validateOrganizationAccess(...)
                    alt Org member
                        CCA-->>V: true
                    else Denied
                        CCA-->>V: false
                    end
                end
            end
        end
    end

    alt Authority granted
        V-->>API: validated request payload
        API-->>Client: 200 (authorize/execute/disconnect proceeds)
    else Authority denied
        V-->>API: 403 response
        API-->>Client: 403 Access denied
    end

    Note over Client,V: Also used by tool router (artist tool branch)

    participant TR as getComposioTools
    Note over TR: NEW: Uses checkConnectorAuthority (was checkAccountArtistAccess)

    Client->>TR: getComposioTools(accountId, artistId)
    TR->>CCA: checkConnectorAuthority(accountId, artistId)
    CCA->>CCA: same checks (self, RECOUP_ORG, org co-membership, workspace, org)
    alt Authority granted
        CCA-->>TR: true
        TR->>TR: fetch artist connectors and tools
        TR-->>Client: tool set including artist tools
    else Authority denied (roster-only)
        CCA-->>TR: false
        TR->>TR: skip artist tools (still return customer tools)
        TR-->>Client: tool set without artist tools
    end
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic


// 3. Org co-membership with a target artist
const targetOrgs = await selectArtistOrganizationIds(targetAccountId);
if (targetOrgs?.length) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Connector authority can still be granted after an artist-org or account-org lookup error because null is treated like “no rows” and the function falls through to later grants. Returning false explicitly on those null results keeps the fail-closed contract for connector operations.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/composio/checkConnectorAuthority.ts, line 42:

<comment>Connector authority can still be granted after an artist-org or account-org lookup error because `null` is treated like “no rows” and the function falls through to later grants. Returning `false` explicitly on those `null` results keeps the fail-closed contract for connector operations.</comment>

<file context>
@@ -0,0 +1,59 @@
+
+  // 3. Org co-membership with a target artist
+  const targetOrgs = await selectArtistOrganizationIds(targetAccountId);
+  if (targetOrgs?.length) {
+    const orgIds = targetOrgs.map(o => o.organization_id).filter((id): id is string => Boolean(id));
+    if (orgIds.length) {
</file context>

@sweetmantech

Copy link
Copy Markdown
Contributor Author

Preview verification — P0b connector authority cut

Preview: https://api-a99yibg6h-recoup.vercel.app (deployment 5367875052, built from 6346576 — the PR head)
When: 2026-07-09 15:37 UTC
Credential: Privy Bearer token for account did:privy:cmc5836d201wola0mjf3km45g

⚠️ Scope note on the credential

The only credential available for live testing is a RECOUP_ORG (staff) account — it belongs to org 04e3aba9… (RECOUP_ORG_ID). In checkConnectorAuthority that triggers the admin bypass (grant #2), which short-circuits every target check. So this session verifies that the new check preserves legitimate access across all four narrowed call sites, but it cannot reproduce the roster-only 403 denial live (a staff caller is granted everything). The denial itself is covered by the unit suite — see the last row.

Results

# Surface Request Documented Actual Verdict
1 Auth gate GET /api/connectors no token 401 HTTP 401
2 Auth gate GET /api/connectors bad Bearer 401 HTTP 401 {"message":"Failed to verify authentication token"}
3 Read path (unchanged, checkAccountArtistAccess) GET /api/connectors self 200 HTTP 200, 8 connectors
4 Read path (unchanged) GET /api/connectors?account_id=<rostered artist> 200 HTTP 200 ✅ roster read preserved
5 Authorize (now checkConnectorAuthority) POST /api/connectors {connector:"googlesheets", account_id:<rostered artist>} 200 + redirectUrl HTTP 200 {success:true, redirectUrl:"…lk_8KVoKqXitpK1"} ✅ authority granted (staff)
6 Execute (now checkConnectorAuthority) POST /api/connectors/actions {actionSlug:"NOT_A_REAL_ACTION", account_id:<rostered artist>} past auth → 404 HTTP 404 {"error":"Connector action not found: NOT_A_REAL_ACTION"} ✅ passed authority, failed downstream as expected
7 Disconnect (now checkConnectorAuthority) DELETE /api/connectors {connected_account_id:"ca_bogus…", account_id:<rostered artist>} past auth → not-found HTTP 500 {"error":"Connection not found for this account"} ✅ passed authority (see note)
8 Staff bypass kept POST /api/connectors {account_id:<random UUID, not rostered/not my org>} 200 (admin bypass) HTTP 200 {success:true, redirectUrl:"…lk_RiV9GD8Hibnj"} ✅ RECOUP_ORG bypass intact

Rostered artist used: Ana Bárbara f584e4f5-6d91-409d-a9ad-8d4047b0ad26 (confirmed in caller roster via GET /api/artists).

The core denial (roster-only → 403): not live-reproducible here, covered by tests

The security fix — a non-staff account that rosters artist A but shares no org gets 403/zero tools — can't be exercised with a staff token (bypass). It is covered by lib/composio/__tests__/checkConnectorAuthority.test.ts"denies a roster-only manager (bare account_artist_ids row, no org tie)", which also asserts selectAccountArtistId is never called. Full suite: 272 tests green (pnpm exec vitest run lib/composio lib/artists). The getComposioTools artist branch is a chat-agent tool-router path (not a REST surface), so it's likewise covered by unit tests + review rather than a live curl here. Recommend a follow-up live check with a non-staff, roster-only account to close the loop on the denial before/at rollout.

Non-blocking observation (pre-existing, not from this PR)

Row 7: disconnect of a non-existent connection returns HTTP 500, while route.ts JSDoc documents 404 for "connected account does not exist." This is in disconnectConnectorHandler downstream of the authority check (untouched by this PR), so it's pre-existing doc/behavior drift, not a regression here — worth a separate tidy-up.

Verdict

Authority-preserving behavior verified live across all four narrowed surfaces; auth gates and roster read paths intact; staff bypass intact. The roster-only denial is unit-tested but should get one live pass with a non-staff account. No regressions observed from this change.

🤖 Generated with Claude Code

@sweetmantech

Copy link
Copy Markdown
Contributor Author

Follow-up: roster-only denial verified live (non-staff account)

Closes the gap from the previous comment. Re-ran against the same preview https://api-a99yibg6h-recoup.vercel.app (commit 6346576) with a non-staff credential.

Caller: account did:privy:cmc6o2dpl00h5jv0mocnxy81tzero org memberships (GET /api/organizations[]), roster contains exactly one artist: Lady Gaga a9972289-2bfe-437b-89ff-a2c133681837 (GET /api/artists). So it holds the artist in its roster but has no self / staff / org / workspace tie → checkConnectorAuthority must fail closed. This is precisely the cross-account act-as-artist vector the PR severs.

When: 2026-07-09 15:40 UTC

# Surface Request Expected Actual Verdict
A Read path (unchanged) GET /api/connectors?account_id=<rostered Gaga> 200 (roster read preserved) HTTP 200 ✅ analytics access intact
B Self authorize POST /api/connectors {connector:"googlesheets"} (no account_id) 200 HTTP 200 + redirectUrl ✅ own connectors still work
C Authorize for rostered artist POST /api/connectors {connector:"googlesheets", account_id:<Gaga>} 403 HTTP 403 {"error":"Access denied to this account"} denied
D Execute for rostered artist POST /api/connectors/actions {actionSlug:"GOOGLESHEETS_CREATE_SPREADSHEET", account_id:<Gaga>} 403 HTTP 403 {"error":"Access denied to this account"} denied (before action lookup)
E Disconnect for rostered artist DELETE /api/connectors {connected_account_id:"ca_bogus", account_id:<Gaga>} 403 HTTP 403 {"error":"Access denied to this account"} denied (before lookup)

Result

The roster edge no longer grants act-as-artist connector authority: a non-staff account that rosters an artist is denied 403 on authorize, execute, and disconnect for that artist (C/D/E), while its own connectors (B) and roster read/analytics (A) remain fully functional. Combined with the earlier staff-path run (grants + bypass preserved), all four narrowed call sites are now verified live in both directions — grant preserved and denial enforced. No regressions.

🤖 Generated with Claude Code

@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

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