Two parallel reviews of the Tier 0–6 work surfaced one CRITICAL regression and a handful of remaining cross-tenant gaps that the original audit didn't enumerate. All fixed here: CRITICAL * document-reminders.processReminderQueue — the new bulk-fetch leftJoin to documentTemplates was scoped on `templateType` alone. Templates of the same type exist in every port; the cartesian explosion would have fired one Documenso reminder PER matching template-row per cron tick (a 5-port deploy = 5 reminders to the same signer per cycle). Added eq(documentTemplates.portId, portId) to the join. * All five remaining Documenso webhook handlers (RecipientSigned / Completed / Opened / Rejected / Cancelled) accept and require an optional portId now, with a shared resolveWebhookDocument() helper that refuses to mutate when the lookup is ambiguous across tenants without a resolved port. Tier 5's port-scoping was applied only to Expired; the route now forwards the matched portId to every handler. Tightens the WHERE clauses on subsequent UPDATEs to (id, portId) for defense-in-depth. HIGH * verifyDocumensoSecret rejects when `expected` is empty — timingSafeEqual(0-bytes, 0-bytes) was returning true, so a dev env with a blank DOCUMENSO_WEBHOOK_SECRET would accept a request whose X-Documenso-Secret header was also missing/empty. listDocumensoWebhookSecrets skips the env entry when blank. * /api/public/health — the website-intake-secret comparison was a string `===` (not constant-time). Switched to timingSafeEqual via Buffer.from(). MEDIUM * server.ts SIGTERM ordering — Socket.io closes BEFORE the HTTP drain so long-poll websockets stop holding the server open past the compose stop_grace_period. * /api/v1/me PATCH preferences merge — allow-list filter on the merged JSONB so legacy rows from the old .passthrough() era stop silently re-shipping their bloat to disk. Migration fixes (deploy-blocking) * 0041 referenced `port_role_overrides.permissions` (column is `permission_overrides`) — overrides are partial JSONB and don't need backfilling at all (deepMerge resolves edit from the base role). Removed the override UPDATEs entirely. * 0042 switched all FK + CHECK adds to NOT VALID + VALIDATE so the brief table-lock phase is decoupled from the row-scan validation, giving a cleaner abort-and-restart story if a constraint catches dirty production data. Added a pre-cleanup UPDATE for invoices.billing_entity_id = '' rows (backfills from clientName, falls back to the row id) so the new non-empty CHECK passes on a dirty table. Test status: 1175/1175 vitest, tsc clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
43 lines
1.7 KiB
SQL
43 lines
1.7 KiB
SQL
-- Backfill the new `documents.edit` and `files.edit` permission keys on
|
|
-- every existing row in `roles.permissions`. The schema (RolePermissions
|
|
-- in src/lib/db/schema/users.ts) added these keys to close the silent-403
|
|
-- traps on PATCH /api/v1/documents/[id], /cancel, /remind, /watchers, and
|
|
-- PATCH /api/v1/files/[id] — each used a permission key that did not exist
|
|
-- in the schema, so withPermission()'s `resourcePerms[action]` returned
|
|
-- undefined and 403'd every non-superadmin call.
|
|
--
|
|
-- Backfill rule:
|
|
-- documents.edit ← documents.create (anyone who can create can edit)
|
|
-- files.edit ← files.upload (same rationale)
|
|
--
|
|
-- jsonb_set with create_missing=true (the default) inserts the key only
|
|
-- when it's absent, so re-runs are idempotent and the migration is safe
|
|
-- against a partial run.
|
|
--
|
|
-- Note: per-port overrides live in `port_role_overrides.permission_overrides`
|
|
-- and are PARTIAL — they only contain the keys a port flipped from the
|
|
-- base role. The deepMerge resolver fills in `documents.edit` from the
|
|
-- base role for any port that didn't override it, so we deliberately do
|
|
-- NOT touch `port_role_overrides` here. Backfilling there would synthesize
|
|
-- override entries that the operator never intended.
|
|
|
|
UPDATE roles
|
|
SET permissions = jsonb_set(
|
|
permissions,
|
|
'{documents,edit}',
|
|
COALESCE(permissions->'documents'->'create', 'false'::jsonb),
|
|
true
|
|
)
|
|
WHERE permissions->'documents' IS NOT NULL
|
|
AND NOT (permissions->'documents' ? 'edit');
|
|
|
|
UPDATE roles
|
|
SET permissions = jsonb_set(
|
|
permissions,
|
|
'{files,edit}',
|
|
COALESCE(permissions->'files'->'upload', 'false'::jsonb),
|
|
true
|
|
)
|
|
WHERE permissions->'files' IS NOT NULL
|
|
AND NOT (permissions->'files' ? 'edit');
|