@rebasepro/common 0.17.0 → 0.17.1-canary.gdc3e9e4
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.
|
@@ -58,7 +58,10 @@ export declare const REBASE_USER_ROLE = "rebase_user";
|
|
|
58
58
|
*
|
|
59
59
|
* Deliberately NOT including `users`: the auth user table is also a collection,
|
|
60
60
|
* with `securityRules`, RLS enabled and policies applied. Users read their own
|
|
61
|
-
* row through it — revoking there would break sign-in.
|
|
61
|
+
* row through it — revoking there would break sign-in. `revokeInternalTableSql`
|
|
62
|
+
* now skips any table with RLS enabled, so that exception is enforced rather
|
|
63
|
+
* than merely remembered — and so is the same hazard for every other name here,
|
|
64
|
+
* any of which a project may legitimately use for a collection of its own.
|
|
62
65
|
*
|
|
63
66
|
* `atlas_schema_revisions` is Atlas's migration ledger, which lands in `rebase`
|
|
64
67
|
* because `db migrate apply` passes `--revisions-schema rebase`.
|
|
@@ -69,8 +72,16 @@ export declare const REBASE_USER_ROLE = "rebase_user";
|
|
|
69
72
|
* list is what the boot-time sweep in `ensureAppRole` iterates, and the sweep
|
|
70
73
|
* is the only thing that can repair an already-granted table. A table revoked
|
|
71
74
|
* at creation but missing here is therefore permanently stranded on any
|
|
72
|
-
* database that predates its revoke
|
|
73
|
-
*
|
|
75
|
+
* database that predates its revoke.
|
|
76
|
+
*
|
|
77
|
+
* These names are unqualified, and the boot-time sweep applies them to every
|
|
78
|
+
* schema a project uses — so an entry here is a claim on that name in `public`
|
|
79
|
+
* as much as in `rebase`. `jobs` is Rebase's queue at `rebase.jobs` AND a
|
|
80
|
+
* perfectly ordinary collection name, and revoking `public.jobs` from a project
|
|
81
|
+
* that owns it leaves every read failing 42501 with correct policies applied
|
|
82
|
+
* and nothing in the RLS logs to explain it. The `relrowsecurity` guard in
|
|
83
|
+
* `revokeInternalTableSql` is what makes a common noun safe here — it is not a
|
|
84
|
+
* licence to claim more of them.
|
|
74
85
|
*/
|
|
75
86
|
export declare const REBASE_INTERNAL_TABLES: readonly string[];
|
|
76
87
|
/**
|
|
@@ -86,6 +97,25 @@ export declare const REBASE_INTERNAL_TABLES: readonly string[];
|
|
|
86
97
|
* - the table may not exist yet — `cron_logs` never appears in a project with
|
|
87
98
|
* no cron jobs — and `to_regclass` returning NULL has to be tolerated too.
|
|
88
99
|
*
|
|
100
|
+
* The third guard is the one that decides whether the *right* table is being
|
|
101
|
+
* revoked. The names in {@link REBASE_INTERNAL_TABLES} are unqualified, and the
|
|
102
|
+
* boot-time sweep in `ensureAppRole` applies all of them to every schema a
|
|
103
|
+
* project uses — including the schema its own collections live in. A project is
|
|
104
|
+
* free to call a collection `jobs`, `branches` or `api_keys`, and when it does,
|
|
105
|
+
* the sweep was revoking `rebase_user`'s DML on the project's table on every
|
|
106
|
+
* single boot. That is not a subtle degradation: the collection's whole API
|
|
107
|
+
* answers 500 `permission denied for table …` from then on, which is what
|
|
108
|
+
* happened to a public job board whose vacancies live in `public.jobs`.
|
|
109
|
+
*
|
|
110
|
+
* `relrowsecurity` separates the two cleanly, and it is the same fact this
|
|
111
|
+
* module already relies on. Framework-internal tables carry no RLS — that is the
|
|
112
|
+
* premise stated at the top of this file, and the reason a revoke is needed at
|
|
113
|
+
* all. Every collection table has it enabled, because that is how Rebase
|
|
114
|
+
* enforces `securityRules`. So "RLS is off" is exactly "this is not somebody's
|
|
115
|
+
* collection", and the guard also subsumes the hand-carved `users` exception:
|
|
116
|
+
* the auth user table is a collection, has RLS, and would now be skipped on its
|
|
117
|
+
* own merits rather than by being kept off a list.
|
|
118
|
+
*
|
|
89
119
|
* One command, so it is safe on handles that speak the extended query protocol
|
|
90
120
|
* and reject multi-statement strings.
|
|
91
121
|
*/
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/common",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.17.
|
|
4
|
+
"version": "0.17.1-canary.gdc3e9e4",
|
|
5
5
|
"description": "Rebase shared core — collection registry, data driver adapter and fluent query builder. No React dependency.",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"fast-equals": "6.0.2",
|
|
42
42
|
"json-logic-js": "^2.0.5",
|
|
43
|
-
"@rebasepro/types": "0.17.
|
|
44
|
-
"@rebasepro/utils": "0.17.
|
|
43
|
+
"@rebasepro/types": "0.17.1-canary.gdc3e9e4",
|
|
44
|
+
"@rebasepro/utils": "0.17.1-canary.gdc3e9e4"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@jest/globals": "^30.4.1",
|
|
@@ -96,6 +96,18 @@ describe("internal table access", () => {
|
|
|
96
96
|
expect(revokeInternalTableSql("rebase", "cron_logs")).toContain("to_regclass");
|
|
97
97
|
});
|
|
98
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
|
+
|
|
99
111
|
it("is a single statement, for handles that reject multi-statement strings", () => {
|
|
100
112
|
// The semicolons inside the block are PL/pgSQL's, not the protocol's:
|
|
101
113
|
// what matters is that the whole string is one dollar-quoted DO,
|
|
@@ -60,7 +60,10 @@ export const REBASE_USER_ROLE = "rebase_user";
|
|
|
60
60
|
*
|
|
61
61
|
* Deliberately NOT including `users`: the auth user table is also a collection,
|
|
62
62
|
* with `securityRules`, RLS enabled and policies applied. Users read their own
|
|
63
|
-
* row through it — revoking there would break sign-in.
|
|
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.
|
|
64
67
|
*
|
|
65
68
|
* `atlas_schema_revisions` is Atlas's migration ledger, which lands in `rebase`
|
|
66
69
|
* because `db migrate apply` passes `--revisions-schema rebase`.
|
|
@@ -71,8 +74,16 @@ export const REBASE_USER_ROLE = "rebase_user";
|
|
|
71
74
|
* list is what the boot-time sweep in `ensureAppRole` iterates, and the sweep
|
|
72
75
|
* is the only thing that can repair an already-granted table. A table revoked
|
|
73
76
|
* at creation but missing here is therefore permanently stranded on any
|
|
74
|
-
* database that predates its revoke
|
|
75
|
-
*
|
|
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.
|
|
76
87
|
*/
|
|
77
88
|
export const REBASE_INTERNAL_TABLES: readonly string[] = [
|
|
78
89
|
// auth
|
|
@@ -120,6 +131,25 @@ const SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;
|
|
|
120
131
|
* - the table may not exist yet — `cron_logs` never appears in a project with
|
|
121
132
|
* no cron jobs — and `to_regclass` returning NULL has to be tolerated too.
|
|
122
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
|
+
*
|
|
123
153
|
* One command, so it is safe on handles that speak the extended query protocol
|
|
124
154
|
* and reject multi-statement strings.
|
|
125
155
|
*/
|
|
@@ -135,7 +165,8 @@ export function revokeInternalTableSql(schema: string, table: string): string {
|
|
|
135
165
|
DO $rebase_revoke$
|
|
136
166
|
BEGIN
|
|
137
167
|
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}')
|
|
138
|
-
AND to_regclass('${qualified}') IS NOT NULL
|
|
168
|
+
AND to_regclass('${qualified}') IS NOT NULL
|
|
169
|
+
AND NOT (SELECT relrowsecurity FROM pg_class WHERE oid = to_regclass('${qualified}')) THEN
|
|
139
170
|
EXECUTE 'REVOKE ALL ON ${qualified} FROM ${REBASE_USER_ROLE}';
|
|
140
171
|
END IF;
|
|
141
172
|
END
|