0fcd617fd1
migrate DB entries to authz engine
and wire authz engine to caldav and carddav
146 lines
6.3 KiB
SQL
146 lines
6.3 KiB
SQL
-- ─────────────────────────────────────────────────────────────────────────
|
|
-- Round 3 Phase 2 — backfill role_grants from the legacy per-domain
|
|
-- share tables.
|
|
--
|
|
-- Companion to `20260906000000_role_grants_calendar_address_book.sql`
|
|
-- (Phase 1: CHECK constraint extension). This migration seeds the
|
|
-- unified `storage.role_grants` table with:
|
|
--
|
|
-- 1. Owner grants for every existing calendar and address book —
|
|
-- replaces the implicit "owner via `caldav.calendars.owner_id`"
|
|
-- short-circuit that the bespoke `check_calendar_access`
|
|
-- helper used.
|
|
-- 2. Non-owner grants translated from `caldav.calendar_shares` and
|
|
-- `carddav.address_book_shares` — the existing "shared with me"
|
|
-- relationships continue working after Phase 3's service
|
|
-- rewrite starts reading grants from `role_grants` only.
|
|
--
|
|
-- The legacy share tables stay in place through this PR for
|
|
-- rollback safety. They get dropped in a follow-up migration one
|
|
-- release later, once the new engine path bakes.
|
|
--
|
|
-- Idempotent: every INSERT uses `ON CONFLICT DO NOTHING` on the
|
|
-- `(subject_type, subject_id, resource_type, resource_id)` unique
|
|
-- key so a re-run (or a duplicate row in the legacy table where
|
|
-- someone shared with themselves) is a no-op.
|
|
|
|
-- ── 1. Owner grants for calendars ───────────────────────────────────────
|
|
--
|
|
-- One row per calendar in `caldav.calendars`. `granted_by = owner_id`
|
|
-- is the self-seeded creation event — the calendar's owner brought
|
|
-- themselves into existence as its owner, matching the pattern used
|
|
-- by the drive lifecycle hook for personal drives.
|
|
INSERT INTO storage.role_grants
|
|
(subject_type, subject_id, resource_type, resource_id, role, granted_by)
|
|
SELECT 'user', c.owner_id, 'calendar', c.id, 'owner'::storage.grant_role, c.owner_id
|
|
FROM caldav.calendars c
|
|
ON CONFLICT (subject_type, subject_id, resource_type, resource_id)
|
|
DO NOTHING;
|
|
|
|
-- ── 2. Owner grants for address books ───────────────────────────────────
|
|
INSERT INTO storage.role_grants
|
|
(subject_type, subject_id, resource_type, resource_id, role, granted_by)
|
|
SELECT 'user', a.owner_id, 'address_book', a.id, 'owner'::storage.grant_role, a.owner_id
|
|
FROM carddav.address_books a
|
|
ON CONFLICT (subject_type, subject_id, resource_type, resource_id)
|
|
DO NOTHING;
|
|
|
|
-- ── 3. Non-owner grants from calendar_shares ────────────────────────────
|
|
--
|
|
-- `caldav.calendar_shares.access_level` is a VARCHAR(10) with values
|
|
-- `'read'`, `'write'`, or `'owner'`. Map:
|
|
-- - `'read'` → `viewer` (bundle: Read only)
|
|
-- - `'write'` → `editor` (bundle: Read + Update)
|
|
-- - `'owner'` → `owner` (bundle: everything, including Share/Manage)
|
|
-- Anything else (defensive) falls through to `viewer` — losing
|
|
-- permission is safer than silently gaining permission if a stray
|
|
-- value slipped past the pre-D0 CHECK.
|
|
--
|
|
-- `granted_by` = calendar owner, since the legacy share table didn't
|
|
-- track the granter. Best available signal — the owner is the only
|
|
-- principal who could have created the share via the legacy code path.
|
|
INSERT INTO storage.role_grants
|
|
(subject_type, subject_id, resource_type, resource_id, role, granted_by)
|
|
SELECT
|
|
'user',
|
|
s.user_id,
|
|
'calendar',
|
|
s.calendar_id,
|
|
(CASE s.access_level
|
|
WHEN 'write' THEN 'editor'
|
|
WHEN 'owner' THEN 'owner'
|
|
ELSE 'viewer'
|
|
END)::storage.grant_role,
|
|
c.owner_id
|
|
FROM caldav.calendar_shares s
|
|
JOIN caldav.calendars c ON c.id = s.calendar_id
|
|
WHERE s.user_id <> c.owner_id -- skip self-shares (owner grant already covers them)
|
|
ON CONFLICT (subject_type, subject_id, resource_type, resource_id)
|
|
DO NOTHING;
|
|
|
|
-- ── 4. Non-owner grants from address_book_shares ────────────────────────
|
|
--
|
|
-- `carddav.address_book_shares.can_write` is a BOOLEAN. Map:
|
|
-- - `false` → `viewer`
|
|
-- - `true` → `editor`
|
|
INSERT INTO storage.role_grants
|
|
(subject_type, subject_id, resource_type, resource_id, role, granted_by)
|
|
SELECT
|
|
'user',
|
|
s.user_id,
|
|
'address_book',
|
|
s.address_book_id,
|
|
(CASE WHEN s.can_write THEN 'editor' ELSE 'viewer' END)::storage.grant_role,
|
|
a.owner_id
|
|
FROM carddav.address_book_shares s
|
|
JOIN carddav.address_books a ON a.id = s.address_book_id
|
|
WHERE s.user_id <> a.owner_id
|
|
ON CONFLICT (subject_type, subject_id, resource_type, resource_id)
|
|
DO NOTHING;
|
|
|
|
-- ── 5. Post-flight sanity ───────────────────────────────────────────────
|
|
--
|
|
-- Every calendar / address book must now have an owner role_grant.
|
|
-- If any row is missing one, the Phase 3 service rewrite would
|
|
-- lock owners out of their own resources — refuse to leave the
|
|
-- migration in that state.
|
|
DO $BODY$
|
|
DECLARE
|
|
missing_cal_owners BIGINT;
|
|
missing_ab_owners BIGINT;
|
|
BEGIN
|
|
SELECT COUNT(*) INTO missing_cal_owners
|
|
FROM caldav.calendars c
|
|
WHERE NOT EXISTS (
|
|
SELECT 1 FROM storage.role_grants g
|
|
WHERE g.subject_type = 'user'
|
|
AND g.subject_id = c.owner_id
|
|
AND g.resource_type = 'calendar'
|
|
AND g.resource_id = c.id
|
|
AND g.role = 'owner'::storage.grant_role
|
|
);
|
|
|
|
SELECT COUNT(*) INTO missing_ab_owners
|
|
FROM carddav.address_books a
|
|
WHERE NOT EXISTS (
|
|
SELECT 1 FROM storage.role_grants g
|
|
WHERE g.subject_type = 'user'
|
|
AND g.subject_id = a.owner_id
|
|
AND g.resource_type = 'address_book'
|
|
AND g.resource_id = a.id
|
|
AND g.role = 'owner'::storage.grant_role
|
|
);
|
|
|
|
IF missing_cal_owners > 0 THEN
|
|
RAISE EXCEPTION
|
|
'Round 3 backfill left % calendars without an Owner role_grant',
|
|
missing_cal_owners;
|
|
END IF;
|
|
IF missing_ab_owners > 0 THEN
|
|
RAISE EXCEPTION
|
|
'Round 3 backfill left % address books without an Owner role_grant',
|
|
missing_ab_owners;
|
|
END IF;
|
|
END;
|
|
$BODY$;
|