/** * Applying a bundle's RLS policies to a database at boot, idempotently. * * ## Why this exists * * {@link ensureCollectionTables} creates the collection *tables* a managed * runtime boots against, but a table with row-level security disabled and no * policies is not servable: authenticated requests run as the restricted * `rebase_user` role, so a read with no `SELECT` policy returns nothing (a * public collection answered 401) and a write with no `INSERT`/`UPDATE` policy * is denied. The policies live in the collections' `securityRules`; nothing at * boot applied them. `rebase db push` does — but it drives Atlas against a * local `DATABASE_URL`, and a managed tenant's database is reachable only from * inside the cluster, by the runtime that is already connected to it. So the * runtime is the only thing that *can* apply them, and this is where it does. * * ## Why this is safe to run on every boot * * Every statement is idempotent: `ENABLE ROW LEVEL SECURITY` is a no-op once * enabled, and each policy is a `DROP POLICY IF EXISTS` immediately followed by * a `CREATE POLICY`, so re-applying asserts exactly the declared state. It adds * and replaces; it never drops data. (It does not *reconcile* — a policy a * previous push left behind under an old name is not removed here; that stays a * `db push` / `db migrate` concern, alongside destructive schema changes.) * * Unlike table creation, a failure here is not fatal: RLS stays enabled, so a * table whose policies could not be applied fails **closed** (denies) rather * than leaking rows. One collection's policy failing (e.g. a rule that * references a table a real migration has not created yet) must not crash-loop * the whole deployment and take the other collections' working routes down with * it. Failures are reported loudly and per-table so the operator can see * exactly which collection is not yet servable and why. */ import { type CollectionConfig } from "@rebasepro/types"; import { type Queryable } from "./ensure-collection-tables"; export interface PolicyEnsureResult { /** `CREATE POLICY` statements that ran successfully. */ policiesApplied: number; /** Tables that had RLS enabled. */ tablesSecured: number; /** Declared tables absent from the database — left to a real migration. */ skipped: { table: string; reason: string; }[]; /** * Tables that have RLS on but did not get every policy. They deny — RLS * with no matching policy is deny-all — so they are safe but not servable. */ failures: { table: string; error: string; }[]; /** * Tables RLS could not be enabled on, whose DML grant was withdrawn instead. * * This state had no name, and that was the bug: `ENABLE ROW LEVEL SECURITY` * failing was recorded as a `failure` and reported with the same "it stays * locked (denies)" wording as a failed policy — but the two are opposites. * A policy statement failing leaves RLS on and the table denying. `enableRls` * failing leaves RLS *off*, and the schema-wide grant to the user role has * already been made by `ensureRlsEnforcement`, so the table is readable and * writable by every authenticated request with no row filtering at all. */ unsecured: { table: string; error: string; grantWithdrawn: boolean; }[]; /** * Generated policies removed because no current rule produces them. * * A policy's name embeds a hash of the rule's semantics, so editing a rule * does not update a policy — it creates a new one and abandons the old. * Postgres ORs permissive policies, so the abandoned one keeps granting: * a `USING (true)` tightened to an owner check went on admitting everyone, * forever, while the deploy logged success. * * `db push` reconciles this, and cannot reach a managed tenant's in-cluster * database — which is the reason this module exists. So boot has to do it. */ orphansDropped: number; } export declare function ensureCollectionPolicies(client: Queryable, collections: CollectionConfig[], log?: (message: string) => void): Promise;