@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 — which is exactly what happened to
73
- * `jobs`, added here after a scan of a pre-0.14 database found it.
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.0",
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.0",
44
- "@rebasepro/utils": "0.17.0"
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 — which is exactly what happened to
75
- * `jobs`, added here after a scan of a pre-0.14 database found it.
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 THEN
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