@accounter/server
Version:
Accounter GraphQL server
106 lines (104 loc) • 4.11 kB
JavaScript
import { sql } from 'slonik';
/**
* Multi-business read scope for RLS.
*
* Adds `get_current_business_scope()` (the request's authorized read scope as a
* UUID array) and switches every tenant_isolation read predicate (USING) to
* `owner_id = ANY (get_current_business_scope())`, while writes (WITH CHECK)
* stay pinned to the single `get_current_business_id()` target.
*
* The scope helper falls back to `ARRAY[get_current_business_id()]` when
* `app.current_business_scope` is unset, so this migration is backward
* compatible: until the tenant DB client sets the scope (a later step), reads
* behave exactly as before.
*
* The separate `allow_bootstrap_root` (businesses / financial_entities) and
* `super_admins_select` policies are intentionally left untouched.
*/
export default {
name: '2026-05-25T10-00-00.rls-multi-business-scope.sql',
run: async ({ connection }) => {
await connection.query(sql.unsafe `
CREATE OR REPLACE FUNCTION accounter_schema.get_current_business_scope()
RETURNS UUID[]
LANGUAGE plpgsql
STABLE
SECURITY DEFINER
SET search_path = pg_catalog
AS $$
DECLARE
v_scope_text TEXT;
BEGIN
-- Read scope is serialized as a Postgres array literal, e.g. '{uuid1,uuid2}'.
v_scope_text := NULLIF(current_setting('app.current_business_scope', true), '');
-- Fallback to the single business context while the scope is not yet set,
-- preserving pre-multi-business read behavior.
IF v_scope_text IS NULL THEN
RETURN ARRAY[accounter_schema.get_current_business_id()];
END IF;
RETURN v_scope_text::uuid[];
END;
$$;
`);
const tables = [
'business_tax_category_match',
'business_trip_charges',
'business_trips',
'business_trips_attendees',
'business_trips_employee_payments',
'business_trips_transactions',
'business_trips_transactions_accommodations',
'business_trips_transactions_car_rental',
'business_trips_transactions_flights',
'business_trips_transactions_match',
'business_trips_transactions_other',
'business_trips_transactions_tns',
'businesses',
'businesses_admin',
'charge_balance_cancellation',
'charge_spread',
'charge_tags',
'charge_unbalanced_ledger_businesses',
'charges',
'charges_bank_deposits',
'clients',
'clients_contracts',
'corporate_tax_variables',
'deel_invoices',
'deel_workers',
'depreciation',
'dividends',
'documents',
'documents_issued',
'dynamic_report_templates',
'employees',
'financial_accounts',
'financial_accounts_tax_categories',
'financial_bank_accounts',
'financial_entities',
'ledger_records',
'misc_expenses',
'pcn874',
'pension_funds',
'salaries',
'sort_codes',
'tags',
'tax_categories',
'transactions',
'user_context',
'annual_audit_step_status',
];
for (const table of tables) {
await connection.query(sql.unsafe `DROP POLICY IF EXISTS tenant_isolation ON accounter_schema.${sql.identifier([table])}`);
// Reads (USING): any business in the request's authorized scope.
// Writes (WITH CHECK): strictly the single explicit write-target business.
await connection.query(sql.unsafe `
CREATE POLICY tenant_isolation ON accounter_schema.${sql.identifier([table])}
FOR ALL
USING (owner_id = ANY (accounter_schema.get_current_business_scope()))
WITH CHECK (owner_id = accounter_schema.get_current_business_id())
`);
}
},
};
//# sourceMappingURL=2026-05-25T10-00-00.rls-multi-business-scope.js.map