Skip to content

fix: transfer deploy history ownership with app#1946

Merged
riderx merged 2 commits into
mainfrom
codex/fix-transfer-app-deploy-history-owner-org
Apr 24, 2026
Merged

fix: transfer deploy history ownership with app#1946
riderx merged 2 commits into
mainfrom
codex/fix-transfer-app-deploy-history-owner-org

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented Apr 24, 2026

Summary (AI generated)

  • update public.transfer_app(...) to move deploy_history.owner_org during app transfers
  • harden the transfer RPC against concurrent cooldown bypasses and same-org no-op transfers
  • restate transfer_app function ACLs in the migration and tighten the pgTAP assertion to the seeded deploy row
  • backfill stale deploy_history.owner_org values to match current app ownership

Motivation (AI generated)

App transfers were leaving historical deployment rows attached to the source organization. Because deploy history read access is authorized from the row's stored owner_org, old orgs could retain access and destination orgs could lose access after a transfer. The follow-up review also identified concurrency and no-op transfer edge cases that could leave the cooldown logic inconsistent.

Business Impact (AI generated)

This closes a cross-org authorization gap on a sensitive ownership flow, preserves correct deployment audit history after transfers, and reduces the risk of support incidents or trust issues around org-boundary isolation.

Test Plan (AI generated)

  • bun lint
  • bunx supabase db query --workdir .context/supabase-worktrees/aec510ca --local "SELECT pg_get_functiondef('public.transfer_app(character varying, uuid)'::regprocedure) LIKE '%UPDATE public.deploy_history%' AS has_deploy_history_update;"
  • bunx supabase test db .context/supabase-worktrees/aec510ca/supabase/tests/00-supabase_test_helpers.sql supabase/tests/24_test_data_functions.sql --workdir .context/supabase-worktrees/aec510ca --local
  • GitHub Actions

Checklist (AI generated)

  • Backward-compatible change only
  • No customer-facing command/docs changes needed
  • No screenshots needed (database-only change)

Generated with AI

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 2026

📝 Walkthrough

Walkthrough

A new migration introduces a transfer_app() security function that transfers app ownership to a destination organization, validating authentication and transfer permissions, enforcing a 32-day transfer cooldown, updating the app and propagating ownership changes across related tables, with accompanying test coverage updates.

Changes

Cohort / File(s) Summary
App Transfer Function
supabase/migrations/20260424090941_fix_transfer_app_deploy_history_owner_org.sql
New SECURITY DEFINER function public.transfer_app() that validates user authentication, checks RBAC permissions for source and destination orgs, enforces 32-day minimum interval between transfers, updates app ownership in public.apps, propagates owner_org to app_versions, app_versions_meta, channel_devices, channels, and deploy_history, maintains transfer history in JSONB, and repairs stale deploy history records.
Transfer Function Tests
supabase/tests/24_test_data_functions.sql
Extended test coverage for transfer_app with new destination org UUID, added assertions validating post-transfer owner_org in both public.apps and public.deploy_history, and updated test plan count to 34.

Sequence Diagram

sequenceDiagram
    participant Client
    participant TransferApp as transfer_app()
    participant Auth as auth.uid()
    participant RBAC as rbac_check_permission()
    participant DB as Database
    
    Client->>TransferApp: Call transfer_app(p_app_id, p_new_org_id)
    TransferApp->>DB: Load app & transfer_history from public.apps
    
    TransferApp->>Auth: Verify authenticated user
    Auth-->>TransferApp: Return auth.uid() or NULL
    alt User Not Authenticated
        TransferApp-->>Client: Log TRANSFER_NO_AUTH & raise exception
    end
    
    TransferApp->>RBAC: Check source org permissions
    RBAC-->>TransferApp: Permission result
    alt Source Org Denied
        TransferApp-->>Client: Log TRANSFER_OLD_ORG_RIGHTS & raise exception
    end
    
    TransferApp->>RBAC: Check destination org permissions
    RBAC-->>TransferApp: Permission result
    alt Destination Org Denied
        TransferApp-->>Client: Log TRANSFER_NEW_ORG_RIGHTS & raise exception
    end
    
    TransferApp->>TransferApp: Validate 32-day minimum interval
    alt Interval Violated
        TransferApp-->>Client: Raise exception
    end
    
    TransferApp->>DB: Update public.apps with new owner_org & transfer_history
    TransferApp->>DB: Propagate owner_org to app_versions
    TransferApp->>DB: Propagate owner_org to app_versions_meta
    TransferApp->>DB: Propagate owner_org to channel_devices
    TransferApp->>DB: Propagate owner_org to channels
    TransferApp->>DB: Propagate owner_org to deploy_history
    TransferApp->>DB: Repair stale deploy_history records
    
    DB-->>TransferApp: Operations complete
    TransferApp-->>Client: Return success
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

codex

Poem

🐰 Hops with glee to announce,
An app now changes its home,
With thirty-two days to bounce,
And permissions to roam,
Transfer history's carved in stone!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the primary change: fixing the transfer_app function to update deploy_history ownership during app transfers.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description provides a clear summary, detailed motivation, business impact, and comprehensive test plan.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-transfer-app-deploy-history-owner-org

Comment @coderabbitai help to get the list of available commands and usage tips.

@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq Bot commented Apr 24, 2026

Merging this PR will not alter performance

✅ 28 untouched benchmarks


Comparing codex/fix-transfer-app-deploy-history-owner-org (eb1ef86) with main (633aeea)

Open in CodSpeed

@riderx riderx marked this pull request as ready for review April 24, 2026 09:56
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
supabase/migrations/20260424090941_fix_transfer_app_deploy_history_owner_org.sql (1)

14-88: Qualify pg_catalog calls inside the definer function.

With search_path = '', this function still relies on implicit resolution for array_length, jsonb_build_object, and now(). Prefixing those with pg_catalog. keeps the function aligned with the repo SQL rule.

As per coding guidelines, "**/*.sql: Set an empty search path (search_path = '') in every PostgreSQL function and use fully qualified names for all references`."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@supabase/migrations/20260424090941_fix_transfer_app_deploy_history_owner_org.sql`
around lines 14 - 88, The function uses unqualified built-ins (array_length,
jsonb_build_object, now(), COALESCE, jsonb operators and casts) while the
function sets search_path = '', so change those calls to their
pg_catalog-qualified equivalents (e.g. pg_catalog.array_length,
pg_catalog.jsonb_build_object, pg_catalog.now, pg_catalog.coalesce, and any
pg_catalog::jsonb casts/operators as needed) in the SELECT that reads
transfer_history into v_last_transfer and in the UPDATE that builds/concats
transfer_history; ensure all built-in functions/operators referenced in the
SELECT, IF checks, and UPDATE (including interval arithmetic) are fully
qualified with pg_catalog to comply with the SQL rule.
supabase/tests/24_test_data_functions.sql (1)

448-459: Assert on the seeded deploy row instead of the app-wide count.

This count(*) = 1 check will become brittle as soon as another fixture adds a deploy_history row for com.demoadmin.app, even if the transfer behavior is still correct. Key the assertion to the seeded channel/deploy row from Lines 397-427 instead of the whole app.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/tests/24_test_data_functions.sql` around lines 448 - 459, The test
currently asserts count(*) on public.deploy_history for app_id =
'com.demoadmin.app' and owner_org, which is brittle; change the assertion to
check the seeded deploy row inserted earlier in this SQL (the specific
deploy_history row used by the transfer_app test) by selecting that row's unique
identifier (e.g., deploy_id or channel identifier used in the seed) and
asserting its owner_org equals the destination org
('34a8c55d-2d0f-4652-a43f-684c7a9403ac'), instead of asserting a global count;
update the SELECT/is assertion to target deploy_history where app_id =
'com.demoadmin.app' AND the seeded deploy's unique key (deploy_id or channel) to
validate ownership moved.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@supabase/migrations/20260424090941_fix_transfer_app_deploy_history_owner_org.sql`:
- Around line 14-17: The SELECT that reads transfer_history into v_old_org_id
and v_last_transfer must lock the apps row to prevent concurrent reads passing
the cooldown check; change the query that selects owner_org,
transfer_history[array_length(...)] FROM public.apps WHERE app_id = p_app_id to
use a FOR UPDATE (or FOR NO KEY UPDATE) lock so the row is locked until the
transaction commits, and apply the same change to the analogous SELECT in the
later block (the logic around p_app_id, transfer_history, and the 32-day
cooldown in lines 69-89) so concurrent calls cannot observe the same
transfer_history tail.
- Around line 19-21: The procedure currently only checks for NULL v_old_org_id
and proceeds to write a transfer history even when p_new_org_id equals the
current owner; add an early guard that compares p_new_org_id to v_old_org_id (or
the retrieved current owner) and aborts (RAISE EXCEPTION or RETURN) before any
HISTORY insert or cooldown update runs so same-org "transfers" do not create a
history record or start the 32-day cooldown; update the logic around the
existing NULL check and the block that performs the HISTORY insert (the transfer
history/cooldown code referenced in the region handling v_old_org_id and the
subsequent insert/update between lines ~79-89) to enforce this equality check.
- Around line 114-124: The migration must explicitly set function ACLs for
public.transfer_app(p_app_id character varying, p_new_org_id uuid): after
setting OWNER use a deny-by-default approach by revoking all privileges from
PUBLIC (REVOKE ALL ON FUNCTION public.transfer_app(...) FROM PUBLIC) and then
grant only EXECUTE to the minimal set of caller roles that should be able to
invoke it (GRANT EXECUTE ON FUNCTION public.transfer_app(...) TO
<intended_roles>), replacing <intended_roles> with the actual service/auth roles
used by your RPC callers.

---

Nitpick comments:
In
`@supabase/migrations/20260424090941_fix_transfer_app_deploy_history_owner_org.sql`:
- Around line 14-88: The function uses unqualified built-ins (array_length,
jsonb_build_object, now(), COALESCE, jsonb operators and casts) while the
function sets search_path = '', so change those calls to their
pg_catalog-qualified equivalents (e.g. pg_catalog.array_length,
pg_catalog.jsonb_build_object, pg_catalog.now, pg_catalog.coalesce, and any
pg_catalog::jsonb casts/operators as needed) in the SELECT that reads
transfer_history into v_last_transfer and in the UPDATE that builds/concats
transfer_history; ensure all built-in functions/operators referenced in the
SELECT, IF checks, and UPDATE (including interval arithmetic) are fully
qualified with pg_catalog to comply with the SQL rule.

In `@supabase/tests/24_test_data_functions.sql`:
- Around line 448-459: The test currently asserts count(*) on
public.deploy_history for app_id = 'com.demoadmin.app' and owner_org, which is
brittle; change the assertion to check the seeded deploy row inserted earlier in
this SQL (the specific deploy_history row used by the transfer_app test) by
selecting that row's unique identifier (e.g., deploy_id or channel identifier
used in the seed) and asserting its owner_org equals the destination org
('34a8c55d-2d0f-4652-a43f-684c7a9403ac'), instead of asserting a global count;
update the SELECT/is assertion to target deploy_history where app_id =
'com.demoadmin.app' AND the seeded deploy's unique key (deploy_id or channel) to
validate ownership moved.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 241563a8-923c-4799-a691-d1eec330bd0c

📥 Commits

Reviewing files that changed from the base of the PR and between 633aeea and 2b7df8b.

📒 Files selected for processing (2)
  • supabase/migrations/20260424090941_fix_transfer_app_deploy_history_owner_org.sql
  • supabase/tests/24_test_data_functions.sql

Comment thread supabase/migrations/20260424090941_fix_transfer_app_deploy_history_owner_org.sql Outdated
@sonarqubecloud
Copy link
Copy Markdown

@riderx riderx merged commit 39f1700 into main Apr 24, 2026
16 checks passed
@riderx riderx deleted the codex/fix-transfer-app-deploy-history-owner-org branch April 24, 2026 12:26
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