fix(zapier): request base|read_all so the Base dropdown loads - #3
Open
caoxing9 wants to merge 1 commit into
Open
fix(zapier): request base|read_all so the Base dropdown loads#3caoxing9 wants to merge 1 commit into
caoxing9 wants to merge 1 commit into
Conversation
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
force-pushed
the
fix/zapier-base-read-all-scope
branch
from
August 14, 2026 17:07
0688fe1 to
7e8048e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 withbase|read_all. We only ever requestedbase|read.This is a regression from a Teable-side security fix (2026-07-21)
It used to work. The backend unconditionally handed
base|read_allto every OAuth client, regardless of consented scopes. teablecce429cb4removed 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.
cce429cb4removes the implicit `baseEvidence from production logs (
zapier logs --user=ryan@garrisonre.com --detailed)GET /api/auth/userGET /api/base/access/allGET /api/base/access/allGET /api/base/access/all{"message":"Forbidden resource","status":403,"code":"restricted_resource"}Granted token scopes on that connection — note what's missing:
Why it looked healthy, and why reconnecting never helped
authentication.testhitsGET /api/auth/user, which only needsuser|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_alltoSCOPES.base|readis 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@Permissionsand is correctly covered; the removed implicit grant coveredbase|read_allonly, so the blast radius is exactly this one dropdown.The OAuth App (client id
cltmh2wegs4wq0xoq4jonapp.teable.ai) must be grantedbase|read_allin Teable → Settings → OAuth Apps before this version is promoted. Otherwise authorization hard-fails for everyone:base|read_allis a valid OAuth scope (packages/core/src/auth/oauth.ts:29), so granting it is a config change.Deploy order:
base|read_allto the OAuth App in Teable.zapier push, promote.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 --noEmitcleannpx jest test/unit.test.ts— 30/30 pass🤖 Generated with Claude Code