Repository navigation
feat(storage): optional databaseId on uploadFile selects the file row's database - #1975
Conversation
…'s database The tenant lane's upload carries the row's scope like every other unprefixed operation. Omitted, it is the session's database claim; named, it picks that database's bucket on a shared database-scope plane under the request role, so the plane's membership RLS decides access and a non-member is refused with DATABASE_ACCESS_DENIED. Identity stays on the claims.
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
|
Review complete. 🟡 1 medium 💬 Inline comments (1)
🧹 Nitpicks (3) — 🟢 3 low
This PR lets callers pass an explicit
Reviewed commit: bd01587 |
There was a problem hiding this comment.
Adds an optional databaseId input to the presigned upload mutations so storage buckets can be scoped per database, with a new shared-db-scope seed fixture and integration tests covering allowed, denied, and omitted-database paths.
Key findings
- 🟡 byDatabase filter breaks claim-scoped default-bucket lookup — plugin.ts:299
| resolveUploadBucket( | ||
| pgClient, storageConfig, databaseId, | ||
| vals.bucketKey ?? null, vals.ownerId ?? null, vals.isPublic === true, | ||
| hasDatabaseId, vals.databaseId != null, |
There was a problem hiding this comment.
🟡 bug · medium
byDatabase filter breaks claim-scoped default-bucket lookup
resolveUploadBucket is called with byDatabase = hasDatabaseId at every call site (graphile/graphile-presigned-url-plugin/src/plugin.ts:299, also lines 117 and 428), so on any plane whose buckets table has a database_id column, getBucketConfig appends AND database_id = $2 even when the caller omitted databaseId and the lookup is claim-scoped. On a shared database-keyed plane the session claim is the storage host's database, while bucket rows carry per-tenant database_id values, so the resolved default key cannot match its own database_id filter and the mutation fails with BUCKET_NOT_FOUND where the pre-PR key-only (RLS-filtered) lookup succeeded. Existing deployments with database_id-bearing bucket rows and NULL or differently-scoped values hit the same regression.
📋 Prompt for AI Agents
In graphile/graphile-presigned-url-plugin/src/plugin.ts, at the three resolveUploadBucket(...) call sites (lines ~117, ~299, ~428), change the byDatabase argument from hasDatabaseId to hasDatabaseId && vals.databaseId != null so the AND database_id = $N filter in getBucketConfig (storage-module-cache.ts:345) applies only when the caller explicitly supplied a databaseId; the claim-scoped default path must keep the pre-PR unscoped bucket lookup for backward compatibility.
Summary
Tenant-lane
uploadFile/uploadFilesgain an optionaldatabaseId, matching the unprefixed-op convention (sdk-scope-syntax): the input carries the row's scope, identity stays on the JWT/GUC claims.Why: a shared database-scope plane (one
buckets/filespair hosted by the storage host's database, rows keyed bydatabase_id) had no way to land a file in any database but the claim's. On the local stack, dashboard branding for a tenant site failed withBUCKET_NOT_FOUNDbecausestorage.localhostalways runs with the platform database's claim.database_idcolumn (StorageRegistryEntry.hasDatabaseId), so platform-planeuploadPlatformFileis unchanged.getBucketConfig(..., byDatabase)addsdatabase_id = $n, so same-key buckets of different databases on one plane can't be confused. The cache key already included the database.SECURITY DEFINER, role switch orBYPASSRLS.Tests:
upload-database-selector.integration.test.tsruns over HTTP with fixture session tokens resolved by the API'sauthenticate(theshared-db-scope-storagefixture is a shared plane with membership RLS):filesrow hasdatabase_id = A;DATABASE_ACCESS_DENIEDand no row is written;DATABASE_ACCESS_DENIED.Related: the constructive-db default buckets PR (shared-plane
default/default-publicper tenant, plus a cascade on database delete), and dashboard#318, which passes the site'sdatabaseId.Link to Devin session: https://app.devin.ai/sessions/24d6f72094de4ff7aba0abc96289c44b
Open in Devin Desktop: https://app.devin.ai/desktop/session/24d6f72094de4ff7aba0abc96289c44b?variant=devin
Requested by: @pyramation