Skip to content

fix(zapier): request base|read_all so the Base dropdown loads - #3

Open
caoxing9 wants to merge 1 commit into
mainfrom
fix/zapier-base-read-all-scope
Open

fix(zapier): request base|read_all so the Base dropdown loads#3
caoxing9 wants to merge 1 commit into
mainfrom
fix/zapier-base-read-all-scope

Conversation

@caoxing9

@caoxing9 caoxing9 commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Problem

Reported by a user (ryan@garrisonre.com): "Zapier not seeing my Bases… everything appears connected correctly. I have deleted connections, cleared cache, tried again — nothing changes."

The Base dropdown is powered by GET /api/base/access/all, which the backend guards with base|read_all. We only ever requested base|read.

This is a regression from a Teable-side security fix (2026-07-21)

It used to work. The backend unconditionally handed base|read_all to every OAuth client, regardless of consented scopes. teable cce429cb4 removed that:

  // apps/nestjs-backend/src/features/auth/permission.service.ts
    if (clientId && clientId.startsWith(IdPrefix.OAuthClient)) {
+     // Only expose base|read_all when the user actually consented to it.
+     // Previously it was concatenated unconditionally, granting third-party
+     // OAuth apps broader read access than the scopes they were approved for.
      return {
-       scopes: scopes.concat('base|read_all'),
+       scopes,

That's the fix for GHSA-c57x (OAuth scope escalation) — correct on their side. It just exposed that this integration had been silently relying on the escalation.

Date Event
2026-06-24 Integration ships requesting `base
2026-07-21 cce429cb4 removes the implicit `base
2026-08-13 User reports the Base dropdown is empty

Evidence from production logs (zapier logs --user=ryan@garrisonre.com --detailed)

Time (CDT) Endpoint Status
17:53:21 GET /api/auth/user 200
17:53:27 GET /api/base/access/all 403
17:54:53 GET /api/base/access/all 403
17:56:56 GET /api/base/access/all 403
{"message":"Forbidden resource","status":403,"code":"restricted_resource"}

Granted token scopes on that connection — note what's missing:

["base|read","table|read","field|read","view|read",
 "record|read","record|create","record|update","record|delete","user|email_read"]

Why it looked healthy, and why reconnecting never helped

authentication.test hits GET /api/auth/user, which only needs user|email_read — 200 every time. So Zapier shows a green, correctly-labelled connection while the integration is unusable. Reconnecting can't fix it: the new authorization re-requests the same insufficient scope.

Already-configured Zaps keep running, since they only touch table/record endpoints whose scopes we request explicitly. That's why this surfaced as user complaints rather than as failing Zaps — and why it went unnoticed for three weeks.

Fix

Add base|read_all to SCOPES. base|read is kept for the per-base endpoints.

Also adds a unit test pinning each scope to the endpoint that needs it. A missing scope fails silently — the request 403s and the dropdown just renders empty — and this is the second time that's bitten us (see d0e7c56, view|read). Every other endpoint the integration calls was audited against its controller @Permissions and is correctly covered; the removed implicit grant covered base|read_all only, so the blast radius is exactly this one dropdown.

⚠️ Required before release — Teable side first

The OAuth App (client id cltmh2wegs4wq0xoq4j on app.teable.ai) must be granted base|read_all in Teable → Settings → OAuth Apps before this version is promoted. Otherwise authorization hard-fails for everyone:

// apps/nestjs-backend/src/features/oauth/oauth-server.service.ts:137
const invalidScopes = difference(queryScopes, scopes);
if (invalidScopes.length) return done(new BadRequestException('Invalid scopes: ' + ...));

base|read_all is a valid OAuth scope (packages/core/src/auth/oauth.ts:29), so granting it is a config change.

Deploy order:

  1. Grant base|read_all to the OAuth App in Teable.
  2. Merge, zapier push, promote.
  3. Every existing user reconnects once — not just the reporter. Scopes are fixed at grant time, so every connection created before this ships is stuck with the old set.

Worth noting for the consent screen: users will now see "read all bases" listed explicitly, where previously they got it without being asked. That's the point of GHSA-c57x, but it is a visible change at authorization time.

Test plan

  • npx tsc --noEmit clean
  • npx jest test/unit.test.ts — 30/30 pass
  • After the OAuth App is updated: reconnect an account and confirm the Base dropdown populates

🤖 Generated with Claude Code

The Base dropdown is powered by GET /api/base/access/all, which the
backend guards with `base|read_all`. We only ever requested `base|read`.

This worked until 2026-07-21. Before then the backend unconditionally
concatenated `base|read_all` onto every OAuth client's scopes:

    scopes: scopes.concat('base|read_all'),

teable cce429cb4 removed that implicit grant as a security fix for
GHSA-c57x (OAuth scope escalation — third-party apps were getting broader
read access than the user approved). Correct fix on their side; it just
exposed that we were relying on the escalation without knowing it.

Since then GET /api/base/access/all returns

    403 {"message":"Forbidden resource","code":"restricted_resource"}

so the Base dropdown renders empty and no new Zap can be configured.
Already-configured Zaps keep running — they only touch table/record
endpoints, whose scopes we request explicitly — which is why this
surfaced as user reports rather than as broken Zaps.

The connection itself still tests fine (authentication.test hits
GET /api/auth/user, which only needs `user|email_read`), so the
integration looks healthy while being unusable, and reconnecting never
helps because it re-requests the same insufficient scope.

Adds a unit test pinning every scope to the endpoint that needs it. All
other endpoints were audited against their controller `@Permissions` and
are correctly covered — the removed implicit grant covered `base|read_all`
only, so the blast radius is exactly this one dropdown.

Requires a matching change on the Teable side BEFORE release: the OAuth
App (client id cltmh2wegs4wq0xoq4j on app.teable.ai) must be granted
`base|read_all`, otherwise authorize rejects the request with
"Invalid scopes" (oauth-server.service.ts:137). Every existing user must
reconnect once, since scopes are fixed at grant time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@caoxing9
caoxing9 force-pushed the fix/zapier-base-read-all-scope branch from 0688fe1 to 7e8048e Compare August 14, 2026 17:07
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