@rebasepro/common 0.17.3 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/collections/CollectionRegistry.d.ts +1 -1
- package/dist/collections/default-collections.d.ts +15 -84
- package/dist/data/buildRebaseData.d.ts +1 -1
- package/dist/data/filter-dialect.d.ts +11 -0
- package/dist/data/sort-dialect.d.ts +15 -3
- package/dist/index.es.js +375 -63
- package/dist/index.es.js.map +1 -1
- package/dist/util/builders.d.ts +69 -24
- package/dist/util/callback-errors.d.ts +77 -0
- package/dist/util/callback-errors.test.d.ts +1 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/policy/evaluatePolicy.d.ts +6 -0
- package/dist/util/relations.d.ts +41 -0
- package/dist/util/table-name.test.d.ts +1 -0
- package/package.json +26 -22
- package/src/collections/CollectionRegistry.ts +0 -485
- package/src/collections/default-collections.ts +0 -109
- package/src/collections/index.ts +0 -2
- package/src/data/buildRebaseData.ts +0 -816
- package/src/data/buildRoutedRebaseData.ts +0 -103
- package/src/data/filter-conditions.ts +0 -46
- package/src/data/filter-dialect.ts +0 -737
- package/src/data/paginate.ts +0 -334
- package/src/data/query_builder.ts +0 -176
- package/src/data/resolveDataSource.ts +0 -135
- package/src/data/sort-dialect.ts +0 -237
- package/src/index.ts +0 -11
- package/src/table-classification.ts +0 -109
- package/src/types/json-logic-js.d.ts +0 -8
- package/src/util/auth-default-policies.ts +0 -215
- package/src/util/builders.ts +0 -82
- package/src/util/callbacks.ts +0 -122
- package/src/util/collections.ts +0 -117
- package/src/util/common.ts +0 -2
- package/src/util/conditions.ts +0 -168
- package/src/util/email.ts +0 -32
- package/src/util/entities.ts +0 -282
- package/src/util/enums.ts +0 -26
- package/src/util/identity.ts +0 -202
- package/src/util/index.ts +0 -21
- package/src/util/internal-tables.test.ts +0 -188
- package/src/util/internal-tables.ts +0 -197
- package/src/util/junction-policies.ts +0 -355
- package/src/util/paths.ts +0 -27
- package/src/util/permissions.test.ts +0 -866
- package/src/util/permissions.ts +0 -206
- package/src/util/pg-column-to-property.ts +0 -377
- package/src/util/policy/evaluatePolicy.ts +0 -194
- package/src/util/policy/index.ts +0 -4
- package/src/util/policy/policyToPostgres.ts +0 -263
- package/src/util/policy/securityRuleToConditions.ts +0 -67
- package/src/util/policy/sqlToPolicy.ts +0 -422
- package/src/util/relations.ts +0 -236
- package/src/util/resolutions.ts +0 -534
- package/src/util/resolve-relation.ts +0 -243
- package/src/util/storage.ts +0 -177
- package/src/util/string-column-length.ts +0 -31
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
REBASE_INTERNAL_TABLES,
|
|
3
|
-
REBASE_USER_ROLE,
|
|
4
|
-
revokeInternalTableAccess,
|
|
5
|
-
revokeInternalTableSql
|
|
6
|
-
} from "./internal-tables";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* The privilege boundary around Rebase's own tables.
|
|
10
|
-
*
|
|
11
|
-
* This module is the only thing keeping `rebase_user` away from refresh-token
|
|
12
|
-
* hashes, TOTP secrets and API keys, and until now nothing tested it. Its
|
|
13
|
-
* failure mode is the quiet kind: `revokeInternalTableAccess` swallows every
|
|
14
|
-
* error into an `onError` callback, so a revoke that fails on all twenty tables
|
|
15
|
-
* produces a boot that looks completely normal.
|
|
16
|
-
*
|
|
17
|
-
* The list membership assertions matter as much as the SQL ones. A table
|
|
18
|
-
* revoked at creation but missing from `REBASE_INTERNAL_TABLES` is stranded on
|
|
19
|
-
* every database provisioned before that revoke shipped — the boot-time sweep
|
|
20
|
-
* in `ensureAppRole` iterates this list and nothing else can repair an
|
|
21
|
-
* already-granted table.
|
|
22
|
-
*/
|
|
23
|
-
describe("internal table access", () => {
|
|
24
|
-
|
|
25
|
-
// ─── The list itself ─────────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
describe("REBASE_INTERNAL_TABLES", () => {
|
|
28
|
-
|
|
29
|
-
it("covers every table a creator revokes at creation time", () => {
|
|
30
|
-
// Kept in sync by hand with the `revokeInternalTableSql` call sites.
|
|
31
|
-
// A creation-time revoke fires once, on the boot that first makes
|
|
32
|
-
// the table; only the sweep can fix a database that predates it.
|
|
33
|
-
const revokedAtCreation = [
|
|
34
|
-
"api_keys",
|
|
35
|
-
"branches",
|
|
36
|
-
"channel_cursors",
|
|
37
|
-
"channel_messages",
|
|
38
|
-
"channel_presence",
|
|
39
|
-
"cron_claims",
|
|
40
|
-
"cron_logs",
|
|
41
|
-
"entity_history",
|
|
42
|
-
"idempotency_keys",
|
|
43
|
-
"jobs",
|
|
44
|
-
"rate_limit_hits"
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
const missing = revokedAtCreation.filter((table) => !REBASE_INTERNAL_TABLES.includes(table));
|
|
48
|
-
expect(missing).toEqual([]);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("holds the auth tables whose rows no end user may ever address", () => {
|
|
52
|
-
for (const table of [
|
|
53
|
-
"refresh_tokens",
|
|
54
|
-
"password_reset_tokens",
|
|
55
|
-
"magic_link_tokens",
|
|
56
|
-
"mfa_factors",
|
|
57
|
-
"mfa_challenges",
|
|
58
|
-
"recovery_codes",
|
|
59
|
-
"user_identities"
|
|
60
|
-
]) {
|
|
61
|
-
expect(REBASE_INTERNAL_TABLES).toContain(table);
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("leaves `users` out, because revoking there breaks sign-in", () => {
|
|
66
|
-
// The auth user table is also a collection: RLS enabled, policies
|
|
67
|
-
// applied, and users read their own row through it.
|
|
68
|
-
expect(REBASE_INTERNAL_TABLES).not.toContain("users");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("has no duplicates", () => {
|
|
72
|
-
expect(new Set(REBASE_INTERNAL_TABLES).size).toBe(REBASE_INTERNAL_TABLES.length);
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
// ─── The statement ───────────────────────────────────────────────────────
|
|
77
|
-
|
|
78
|
-
describe("revokeInternalTableSql", () => {
|
|
79
|
-
|
|
80
|
-
it("revokes every privilege from the end-user role", () => {
|
|
81
|
-
const sql = revokeInternalTableSql("rebase", "refresh_tokens");
|
|
82
|
-
|
|
83
|
-
expect(sql).toContain(`REVOKE ALL ON "rebase"."refresh_tokens" FROM ${REBASE_USER_ROLE}`);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("guards on the role existing, so an unprivileged install is a no-op", () => {
|
|
87
|
-
// A bare REVOKE naming a missing role is an error, not a no-op, and
|
|
88
|
-
// it would abort the boot it runs in.
|
|
89
|
-
const sql = revokeInternalTableSql("rebase", "api_keys");
|
|
90
|
-
|
|
91
|
-
expect(sql).toContain(`SELECT 1 FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}'`);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("tolerates a table that does not exist yet", () => {
|
|
95
|
-
// `cron_logs` never appears in a project with no cron jobs.
|
|
96
|
-
expect(revokeInternalTableSql("rebase", "cron_logs")).toContain("to_regclass");
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("skips a table with RLS enabled, because that is somebody's collection", () => {
|
|
100
|
-
// The names in the list are unqualified and the boot sweep applies
|
|
101
|
-
// them to every project schema, so `public.jobs` — a job board's
|
|
102
|
-
// vacancies — was being revoked on every boot by the entry meant for
|
|
103
|
-
// `rebase.jobs`, the background queue. RLS is what tells them apart:
|
|
104
|
-
// collections have it, framework tables do not.
|
|
105
|
-
const sql = revokeInternalTableSql("public", "jobs");
|
|
106
|
-
|
|
107
|
-
expect(sql).toContain("relrowsecurity");
|
|
108
|
-
expect(sql).toMatch(/NOT \(SELECT relrowsecurity FROM pg_class WHERE oid = to_regclass\('"public"\."jobs"'\)\)/);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("is a single statement, for handles that reject multi-statement strings", () => {
|
|
112
|
-
// The semicolons inside the block are PL/pgSQL's, not the protocol's:
|
|
113
|
-
// what matters is that the whole string is one dollar-quoted DO,
|
|
114
|
-
// opened once and closed at the very end.
|
|
115
|
-
const sql = revokeInternalTableSql("rebase", "jobs");
|
|
116
|
-
|
|
117
|
-
expect(sql.startsWith("DO $rebase_revoke$")).toBe(true);
|
|
118
|
-
expect(sql.endsWith("$rebase_revoke$;")).toBe(true);
|
|
119
|
-
expect(sql.split("$rebase_revoke$")).toHaveLength(3);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("refuses to interpolate an unsafe identifier", () => {
|
|
123
|
-
expect(() => revokeInternalTableSql("rebase", 'x"; DROP TABLE users; --')).toThrow(/unsafe table name/);
|
|
124
|
-
expect(() => revokeInternalTableSql('pub"lic', "api_keys")).toThrow(/unsafe schema name/);
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// ─── The sweep ───────────────────────────────────────────────────────────
|
|
129
|
-
|
|
130
|
-
describe("revokeInternalTableAccess", () => {
|
|
131
|
-
|
|
132
|
-
it("issues one statement per internal table", async () => {
|
|
133
|
-
const executed: string[] = [];
|
|
134
|
-
|
|
135
|
-
await revokeInternalTableAccess(async (sql) => { executed.push(sql); }, "rebase");
|
|
136
|
-
|
|
137
|
-
expect(executed).toHaveLength(REBASE_INTERNAL_TABLES.length);
|
|
138
|
-
for (const table of REBASE_INTERNAL_TABLES) {
|
|
139
|
-
expect(executed.some((sql) => sql.includes(`"rebase"."${table}"`))).toBe(true);
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("reports a failure rather than swallowing it", async () => {
|
|
144
|
-
// The whole risk: an onError that no caller wires up turns twenty
|
|
145
|
-
// failed revokes into a silent, apparently-clean boot.
|
|
146
|
-
const seen: string[] = [];
|
|
147
|
-
|
|
148
|
-
await revokeInternalTableAccess(
|
|
149
|
-
async (sql) => {
|
|
150
|
-
if (sql.includes("refresh_tokens")) throw new Error("permission denied");
|
|
151
|
-
},
|
|
152
|
-
"rebase",
|
|
153
|
-
{ onError: (table) => seen.push(table) }
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
expect(seen).toEqual(["refresh_tokens"]);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it("keeps going after one table fails", async () => {
|
|
160
|
-
const executed: string[] = [];
|
|
161
|
-
|
|
162
|
-
await revokeInternalTableAccess(
|
|
163
|
-
async (sql) => {
|
|
164
|
-
executed.push(sql);
|
|
165
|
-
if (sql.includes("user_identities")) throw new Error("not the owner");
|
|
166
|
-
},
|
|
167
|
-
"rebase",
|
|
168
|
-
{ onError: () => undefined }
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
// A connection that does not own one table must not strand the rest.
|
|
172
|
-
expect(executed).toHaveLength(REBASE_INTERNAL_TABLES.length);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it("honours an explicit table list", async () => {
|
|
176
|
-
const executed: string[] = [];
|
|
177
|
-
|
|
178
|
-
await revokeInternalTableAccess(
|
|
179
|
-
async (sql) => { executed.push(sql); },
|
|
180
|
-
"rebase",
|
|
181
|
-
{ tables: ["api_keys"] }
|
|
182
|
-
);
|
|
183
|
-
|
|
184
|
-
expect(executed).toHaveLength(1);
|
|
185
|
-
expect(executed[0]).toContain('"rebase"."api_keys"');
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
});
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The tables Rebase creates for its own bookkeeping, and the SQL that keeps the
|
|
3
|
-
* end-user role away from them.
|
|
4
|
-
*
|
|
5
|
-
* ## Why this exists
|
|
6
|
-
*
|
|
7
|
-
* Authenticated requests run as {@link REBASE_USER_ROLE}, and the boot-time role
|
|
8
|
-
* provisioning grants that role `SELECT, INSERT, UPDATE, DELETE` on every table
|
|
9
|
-
* in the schemas a project uses — including `rebase`, because a project's own
|
|
10
|
-
* collections are allowed to live there (the scaffold puts `users` there). It
|
|
11
|
-
* also sets `ALTER DEFAULT PRIVILEGES`, so a table created *later* by the
|
|
12
|
-
* migrating role inherits the same grant.
|
|
13
|
-
*
|
|
14
|
-
* Every framework-internal table is created later: auth's tables come up during
|
|
15
|
-
* `initializeAuth`, `api_keys` during route mounting, `cron_logs` when the first
|
|
16
|
-
* job registers, `idempotency_keys` on the first request that carries a key. So
|
|
17
|
-
* they all inherited full DML for the end-user role — and none of them enables
|
|
18
|
-
* row-level security, because none of them is a collection with
|
|
19
|
-
* `securityRules`. Measured on a freshly provisioned database, `SET ROLE
|
|
20
|
-
* rebase_user` could read `rebase.refresh_tokens` (session token hashes),
|
|
21
|
-
* `rebase.mfa_factors` (`secret_encrypted`), `rebase.recovery_codes`, and
|
|
22
|
-
* `rebase.api_keys` (including its `admin` flag), and insert into
|
|
23
|
-
* `rebase.app_config`.
|
|
24
|
-
*
|
|
25
|
-
* Nothing routes a user-context query at those tables today, so this was not
|
|
26
|
-
* reachable over the API. That is the wrong thing to depend on: the documented
|
|
27
|
-
* model is that RLS is the authorization boundary, and these tables sat outside
|
|
28
|
-
* it. The boundary is now a privilege boundary instead — the role simply cannot
|
|
29
|
-
* address them.
|
|
30
|
-
*
|
|
31
|
-
* ## Why REVOKE rather than ENABLE ROW LEVEL SECURITY
|
|
32
|
-
*
|
|
33
|
-
* RLS with no policy denies every row, which is the same outcome, but it is the
|
|
34
|
-
* *weaker* statement: it leaves the grant in place, so a later policy — or a
|
|
35
|
-
* `FORCE` flag cleared by some future migration — reopens the table. There is no
|
|
36
|
-
* row of `refresh_tokens` any end user should ever reach, so the honest encoding
|
|
37
|
-
* is "this role has no privilege here at all". It also keeps the owner
|
|
38
|
-
* connection (which auth actually runs on) completely unaffected.
|
|
39
|
-
*
|
|
40
|
-
* ## Keeping it true
|
|
41
|
-
*
|
|
42
|
-
* `packages/rls-check` scans the `rebase` schema — it used to skip it as a
|
|
43
|
-
* "platform" schema — and its `rls-disabled` check fires on exactly the
|
|
44
|
-
* condition this module removes: RLS off *and* a DML grant to a reachable role.
|
|
45
|
-
* So a table added here without a revoke is caught by `pnpm rls:check`, not by
|
|
46
|
-
* someone re-reading this file.
|
|
47
|
-
*/
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* The Postgres role authenticated requests run as.
|
|
51
|
-
*
|
|
52
|
-
* Defined here rather than in the Postgres driver because both the driver (which
|
|
53
|
-
* provisions the role) and this module (which revokes on its behalf) need it,
|
|
54
|
-
* and a second spelling of a role name is a silent no-op waiting to happen.
|
|
55
|
-
*/
|
|
56
|
-
export const REBASE_USER_ROLE = "rebase_user";
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Framework-internal table names, unqualified.
|
|
60
|
-
*
|
|
61
|
-
* Deliberately NOT including `users`: the auth user table is also a collection,
|
|
62
|
-
* with `securityRules`, RLS enabled and policies applied. Users read their own
|
|
63
|
-
* row through it — revoking there would break sign-in. `revokeInternalTableSql`
|
|
64
|
-
* now skips any table with RLS enabled, so that exception is enforced rather
|
|
65
|
-
* than merely remembered — and so is the same hazard for every other name here,
|
|
66
|
-
* any of which a project may legitimately use for a collection of its own.
|
|
67
|
-
*
|
|
68
|
-
* `atlas_schema_revisions` is Atlas's migration ledger, which lands in `rebase`
|
|
69
|
-
* because `db migrate apply` passes `--revisions-schema rebase`.
|
|
70
|
-
*
|
|
71
|
-
* Every entry here must also be revoked by whatever creates it, and vice versa:
|
|
72
|
-
* the creation-time revoke fires once, on the boot that first makes the table,
|
|
73
|
-
* so it cannot help a database provisioned before that revoke existed. This
|
|
74
|
-
* list is what the boot-time sweep in `ensureAppRole` iterates, and the sweep
|
|
75
|
-
* is the only thing that can repair an already-granted table. A table revoked
|
|
76
|
-
* at creation but missing here is therefore permanently stranded on any
|
|
77
|
-
* database that predates its revoke.
|
|
78
|
-
*
|
|
79
|
-
* These names are unqualified, and the boot-time sweep applies them to every
|
|
80
|
-
* schema a project uses — so an entry here is a claim on that name in `public`
|
|
81
|
-
* as much as in `rebase`. `jobs` is Rebase's queue at `rebase.jobs` AND a
|
|
82
|
-
* perfectly ordinary collection name, and revoking `public.jobs` from a project
|
|
83
|
-
* that owns it leaves every read failing 42501 with correct policies applied
|
|
84
|
-
* and nothing in the RLS logs to explain it. The `relrowsecurity` guard in
|
|
85
|
-
* `revokeInternalTableSql` is what makes a common noun safe here — it is not a
|
|
86
|
-
* licence to claim more of them.
|
|
87
|
-
*/
|
|
88
|
-
export const REBASE_INTERNAL_TABLES: readonly string[] = [
|
|
89
|
-
// auth
|
|
90
|
-
"user_identities",
|
|
91
|
-
"refresh_tokens",
|
|
92
|
-
"password_reset_tokens",
|
|
93
|
-
"magic_link_tokens",
|
|
94
|
-
"mfa_factors",
|
|
95
|
-
"mfa_challenges",
|
|
96
|
-
"recovery_codes",
|
|
97
|
-
"app_config",
|
|
98
|
-
"schema_meta",
|
|
99
|
-
// platform services
|
|
100
|
-
"api_keys",
|
|
101
|
-
"cron_logs",
|
|
102
|
-
"cron_claims",
|
|
103
|
-
"jobs",
|
|
104
|
-
"rate_limit_hits",
|
|
105
|
-
"idempotency_keys",
|
|
106
|
-
"entity_history",
|
|
107
|
-
"branches",
|
|
108
|
-
"metric_samples",
|
|
109
|
-
// realtime channels — authorization for these lives in the channel rules the
|
|
110
|
-
// server evaluates before it reads or writes, never in a row policy
|
|
111
|
-
"channel_messages",
|
|
112
|
-
"channel_cursors",
|
|
113
|
-
"channel_presence",
|
|
114
|
-
// migration bookkeeping
|
|
115
|
-
"atlas_schema_revisions"
|
|
116
|
-
];
|
|
117
|
-
|
|
118
|
-
/** Postgres identifiers this module is willing to interpolate. */
|
|
119
|
-
const SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* A single statement that takes every privilege on `schema.table` away from the
|
|
123
|
-
* end-user role.
|
|
124
|
-
*
|
|
125
|
-
* Wrapped in a `DO` block guarded on `pg_roles` for two reasons, both of which
|
|
126
|
-
* happen in practice:
|
|
127
|
-
*
|
|
128
|
-
* - the role does not exist when the connection is unprivileged (Rebase then
|
|
129
|
-
* relies on native RLS rather than a role switch), and a bare `REVOKE` on a
|
|
130
|
-
* missing role is an error, not a no-op;
|
|
131
|
-
* - the table may not exist yet — `cron_logs` never appears in a project with
|
|
132
|
-
* no cron jobs — and `to_regclass` returning NULL has to be tolerated too.
|
|
133
|
-
*
|
|
134
|
-
* The third guard is the one that decides whether the *right* table is being
|
|
135
|
-
* revoked. The names in {@link REBASE_INTERNAL_TABLES} are unqualified, and the
|
|
136
|
-
* boot-time sweep in `ensureAppRole` applies all of them to every schema a
|
|
137
|
-
* project uses — including the schema its own collections live in. A project is
|
|
138
|
-
* free to call a collection `jobs`, `branches` or `api_keys`, and when it does,
|
|
139
|
-
* the sweep was revoking `rebase_user`'s DML on the project's table on every
|
|
140
|
-
* single boot. That is not a subtle degradation: the collection's whole API
|
|
141
|
-
* answers 500 `permission denied for table …` from then on, which is what
|
|
142
|
-
* happened to a public job board whose vacancies live in `public.jobs`.
|
|
143
|
-
*
|
|
144
|
-
* `relrowsecurity` separates the two cleanly, and it is the same fact this
|
|
145
|
-
* module already relies on. Framework-internal tables carry no RLS — that is the
|
|
146
|
-
* premise stated at the top of this file, and the reason a revoke is needed at
|
|
147
|
-
* all. Every collection table has it enabled, because that is how Rebase
|
|
148
|
-
* enforces `securityRules`. So "RLS is off" is exactly "this is not somebody's
|
|
149
|
-
* collection", and the guard also subsumes the hand-carved `users` exception:
|
|
150
|
-
* the auth user table is a collection, has RLS, and would now be skipped on its
|
|
151
|
-
* own merits rather than by being kept off a list.
|
|
152
|
-
*
|
|
153
|
-
* One command, so it is safe on handles that speak the extended query protocol
|
|
154
|
-
* and reject multi-statement strings.
|
|
155
|
-
*/
|
|
156
|
-
export function revokeInternalTableSql(schema: string, table: string): string {
|
|
157
|
-
if (!SAFE_IDENTIFIER.test(schema)) {
|
|
158
|
-
throw new Error(`Refusing to build SQL with an unsafe schema name: ${JSON.stringify(schema)}`);
|
|
159
|
-
}
|
|
160
|
-
if (!SAFE_IDENTIFIER.test(table)) {
|
|
161
|
-
throw new Error(`Refusing to build SQL with an unsafe table name: ${JSON.stringify(table)}`);
|
|
162
|
-
}
|
|
163
|
-
const qualified = `"${schema}"."${table}"`;
|
|
164
|
-
return `
|
|
165
|
-
DO $rebase_revoke$
|
|
166
|
-
BEGIN
|
|
167
|
-
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}')
|
|
168
|
-
AND to_regclass('${qualified}') IS NOT NULL
|
|
169
|
-
AND NOT (SELECT relrowsecurity FROM pg_class WHERE oid = to_regclass('${qualified}')) THEN
|
|
170
|
-
EXECUTE 'REVOKE ALL ON ${qualified} FROM ${REBASE_USER_ROLE}';
|
|
171
|
-
END IF;
|
|
172
|
-
END
|
|
173
|
-
$rebase_revoke$;
|
|
174
|
-
`.trim();
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Revoke on every internal table in `schema`, one statement at a time.
|
|
179
|
-
*
|
|
180
|
-
* Best-effort per table: a connection that does not own one of them (a
|
|
181
|
-
* pre-provisioned database, a platform-managed ledger) cannot revoke on it, and
|
|
182
|
-
* that must not take down a boot. The caller decides how loud to be — `onError`
|
|
183
|
-
* exists so the driver can warn without this module importing a logger.
|
|
184
|
-
*/
|
|
185
|
-
export async function revokeInternalTableAccess(
|
|
186
|
-
execute: (sql: string) => Promise<unknown>,
|
|
187
|
-
schema: string,
|
|
188
|
-
options?: { tables?: readonly string[]; onError?: (table: string, error: unknown) => void }
|
|
189
|
-
): Promise<void> {
|
|
190
|
-
for (const table of options?.tables ?? REBASE_INTERNAL_TABLES) {
|
|
191
|
-
try {
|
|
192
|
-
await execute(revokeInternalTableSql(schema, table));
|
|
193
|
-
} catch (error) {
|
|
194
|
-
options?.onError?.(table, error);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|