Files
Oxicloud/migrations/20260601000000_rebac_expiry_and_perms_cleanup.sql

69 lines
3.6 KiB
SQL

-- ════════════════════════════════════════════════════════════════════════════
-- ReBAC Phase 2: grant-level expiry + dead permission column cleanup
-- ════════════════════════════════════════════════════════════════════════════
-- This migration:
-- 1. Adds expires_at (TIMESTAMPTZ) to access_grants — uniform expiry for
-- all subject types (token, user, future external).
-- 2. Migrates existing token expiry from storage.shares.expires_at.
-- 3. Backfills any Read grants missing for shares created after the
-- initial migration (safety net — idempotent via NOT EXISTS).
-- 4. Adds two performance indexes (expires_at partial, granted_by).
-- 5. Drops the now-dead permission and expiry columns from storage.shares.
-- storage.shares becomes token-only metadata: id, token, password_hash,
-- access_count, created_at, created_by, item_id, item_type, item_name.
--
-- Conceptual model: a share token is an authentication principal, not a
-- permission type. Access = having a non-expired Read grant in access_grants
-- for Subject::Token(share.id). Tokens are always read-only by definition.
-- ── 1. Add expires_at ────────────────────────────────────────────────────────
ALTER TABLE storage.access_grants
ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ;
-- ── 2. Migrate token expiry (shares.expires_at is BIGINT unix seconds) ───────
UPDATE storage.access_grants ag
SET expires_at = to_timestamp(s.expires_at)
FROM storage.shares s
WHERE ag.subject_type = 'token'
AND ag.subject_id = s.id
AND s.expires_at IS NOT NULL;
-- ── 3. Backfill Read grants for shares that missed the initial migration ──────
INSERT INTO storage.access_grants
(subject_type, subject_id, resource_type, resource_id, permission, granted_by, granted_at)
SELECT
'token',
s.id,
s.item_type,
s.item_id::UUID,
'read',
s.created_by,
to_timestamp(s.created_at)
FROM storage.shares s
WHERE s.permissions_read
AND NOT EXISTS (
SELECT 1 FROM storage.access_grants ag
WHERE ag.subject_type = 'token'
AND ag.subject_id = s.id
AND ag.permission = 'read'
)
ON CONFLICT DO NOTHING;
-- ── 4. Performance indexes ───────────────────────────────────────────────────
-- Partial index for expiry checks (only rows that actually expire)
CREATE INDEX IF NOT EXISTS idx_grants_expires_at
ON storage.access_grants (expires_at) WHERE expires_at IS NOT NULL;
-- Needed for GET /api/grants/outgoing/resources (currently missing)
CREATE INDEX IF NOT EXISTS idx_grants_granted_by
ON storage.access_grants (granted_by);
-- ── 5. Drop dead columns from storage.shares ─────────────────────────────────
-- Permissions were never enforced (no public write endpoints, frontend
-- hard-codes write=false/reshare=false). Expiry is now in access_grants.
ALTER TABLE storage.shares
DROP COLUMN IF EXISTS permissions_read,
DROP COLUMN IF EXISTS permissions_write,
DROP COLUMN IF EXISTS permissions_reshare,
DROP COLUMN IF EXISTS expires_at;