@rebasepro/studio 0.15.0 → 0.16.0
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/dist/{ApiExplorer-CDOlUQzk.js → ApiExplorer-9iwGvNnt.js} +5 -5
- package/dist/ApiExplorer-9iwGvNnt.js.map +1 -0
- package/dist/{JSEditor-BaSrzWhF.js → JSEditor-BZnvK1n6.js} +14 -14
- package/dist/JSEditor-BZnvK1n6.js.map +1 -0
- package/dist/{RLSEditor-CV8R1G3W.js → RLSEditor-CBX4uD66.js} +163 -49
- package/dist/RLSEditor-CBX4uD66.js.map +1 -0
- package/dist/{SQLEditor-BmI0iIWK.js → SQLEditor-ByecwX8B.js} +16 -16
- package/dist/SQLEditor-ByecwX8B.js.map +1 -0
- package/dist/{SchemaVisualizer-vxKSG80C.js → SchemaVisualizer-D6tMZgeC.js} +4 -4
- package/dist/SchemaVisualizer-D6tMZgeC.js.map +1 -0
- package/dist/{StorageView-CwDI4sBG.js → StorageView-CGm-cacx.js} +9 -9
- package/dist/StorageView-CGm-cacx.js.map +1 -0
- package/dist/components/RLSEditor/PolicyEditor.d.ts +9 -1
- package/dist/components/RLSEditor/policy-presets.d.ts +74 -0
- package/dist/index.es.js +6 -6
- package/package.json +9 -9
- package/src/components/ApiExplorer/ApiExplorer.tsx +2 -2
- package/src/components/ApiExplorer/EndpointDetail.tsx +1 -1
- package/src/components/ApiExplorer/TryItPanel.tsx +1 -1
- package/src/components/JSEditor/JSEditor.tsx +7 -7
- package/src/components/JSEditor/JSEditorSidebar.tsx +6 -6
- package/src/components/RLSEditor/PolicyEditor.tsx +31 -96
- package/src/components/RLSEditor/RLSEditor.tsx +46 -4
- package/src/components/RLSEditor/policy-presets.ts +176 -0
- package/src/components/SQLEditor/SQLEditor.tsx +10 -10
- package/src/components/SQLEditor/SQLEditorSidebar.tsx +4 -4
- package/src/components/SQLEditor/SchemaBrowser.tsx +1 -1
- package/src/components/SchemaVisualizer/SchemaVisualizer.tsx +1 -1
- package/src/components/SchemaVisualizer/TableNode.tsx +2 -2
- package/src/components/StorageView/StorageView.tsx +8 -8
- package/dist/ApiExplorer-CDOlUQzk.js.map +0 -1
- package/dist/JSEditor-BaSrzWhF.js.map +0 -1
- package/dist/RLSEditor-CV8R1G3W.js.map +0 -1
- package/dist/SQLEditor-BmI0iIWK.js.map +0 -1
- package/dist/SchemaVisualizer-vxKSG80C.js.map +0 -1
- package/dist/StorageView-CwDI4sBG.js.map +0 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export type PolicyCommand = "ALL" | "SELECT" | "INSERT" | "UPDATE" | "DELETE";
|
|
2
|
+
export declare const COMMAND_OPTIONS: PolicyCommand[];
|
|
3
|
+
/**
|
|
4
|
+
* The roles a policy's `TO` list can name when nothing better is known.
|
|
5
|
+
*
|
|
6
|
+
* Not `authenticated` / `anon` / `admin`, which is what this list used to be.
|
|
7
|
+
* The first two are Supabase's role names and Rebase never creates them, so
|
|
8
|
+
* `CREATE POLICY ... TO authenticated` — which is literally what the editor
|
|
9
|
+
* builds and executes — fails with `role "authenticated" does not exist`. The
|
|
10
|
+
* driver's `validatePolicyPgRoles` rejects the same three names in
|
|
11
|
+
* `SecurityRule.pgRoles` and says why: they are another platform's convention,
|
|
12
|
+
* and application roles belong in a condition, not in the `TO` list.
|
|
13
|
+
*
|
|
14
|
+
* `admin` was the more dangerous entry, because it is a plausible *application*
|
|
15
|
+
* role. Had one existed as a database role too, the policy would have been
|
|
16
|
+
* created successfully and then matched nothing at all: requests run as
|
|
17
|
+
* {@link REBASE_USER_ROLE} after a `SET LOCAL ROLE`, and a `TO` list naming a
|
|
18
|
+
* role the request never assumes filters every row without erroring.
|
|
19
|
+
*
|
|
20
|
+
* So: `public` (what the framework's own generator emits) and the role requests
|
|
21
|
+
* actually arrive as. "Signed-in users" and "admins" are not roles here — they
|
|
22
|
+
* are conditions over `rebase.uid()` and `rebase.roles()`, which is what the
|
|
23
|
+
* presets below now compile to.
|
|
24
|
+
*/
|
|
25
|
+
export declare const FALLBACK_ROLE_OPTIONS: string[];
|
|
26
|
+
/**
|
|
27
|
+
* The `TO` list the editor offers, given what the database reported and what
|
|
28
|
+
* the policy being edited already targets.
|
|
29
|
+
*
|
|
30
|
+
* Three sources, and each is there for a reason the others do not cover:
|
|
31
|
+
*
|
|
32
|
+
* - {@link FALLBACK_ROLE_OPTIONS} always, because `public` is a **keyword, not
|
|
33
|
+
* a row in `pg_roles`** — `fetchAvailableRoles` cannot return it, so seeding
|
|
34
|
+
* from the live fetch alone would drop the one role every generated policy
|
|
35
|
+
* targets.
|
|
36
|
+
* - the live roles, so an operator's own `app_read` is reachable without
|
|
37
|
+
* hand-editing SQL.
|
|
38
|
+
* - the edited policy's own roles, because a `MultiSelect` silently drops a
|
|
39
|
+
* value with no matching item. That value is the `TO` list of a policy
|
|
40
|
+
* already enforcing something, so opening an unrelated policy for a one-word
|
|
41
|
+
* rename would have quietly rewritten who it applies to.
|
|
42
|
+
*/
|
|
43
|
+
export declare function roleOptionsFor(fetched: readonly string[] | undefined, policyRoles: readonly string[] | undefined): string[];
|
|
44
|
+
/**
|
|
45
|
+
* Preset conditions, compiled by the framework's own policy compiler rather
|
|
46
|
+
* than written out here.
|
|
47
|
+
*
|
|
48
|
+
* `policyToPostgres` is the function `db push` generates policies with, so a
|
|
49
|
+
* preset cannot drift from what the framework emits — and the one that matters
|
|
50
|
+
* has drifted before: `authenticated()` used to compile to a bare
|
|
51
|
+
* `IS NOT NULL`, which is true for anonymous visitors, and any copy of that
|
|
52
|
+
* string sitting in a preset would still be handing out the old grant today.
|
|
53
|
+
*/
|
|
54
|
+
export declare const SIGNED_IN_SQL: string;
|
|
55
|
+
export declare const IS_ADMIN_SQL: string;
|
|
56
|
+
export declare const OWNS_ROW_SQL: string;
|
|
57
|
+
export interface PolicyPreset {
|
|
58
|
+
id: string;
|
|
59
|
+
label: string;
|
|
60
|
+
description: string;
|
|
61
|
+
policyname: string;
|
|
62
|
+
cmd: PolicyCommand;
|
|
63
|
+
permissive: "PERMISSIVE" | "RESTRICTIVE";
|
|
64
|
+
roles: string[];
|
|
65
|
+
qual: string;
|
|
66
|
+
with_check: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Every preset targets `TO public`, which is what the framework's generator
|
|
70
|
+
* emits and what actually reaches a request running as `rebase_user`. The part
|
|
71
|
+
* that used to be expressed as a role — "authenticated", "admin" — is a
|
|
72
|
+
* condition now, so it is enforced where Postgres will actually evaluate it.
|
|
73
|
+
*/
|
|
74
|
+
export declare const POLICY_PRESETS: PolicyPreset[];
|
package/dist/index.es.js
CHANGED
|
@@ -549,15 +549,15 @@ function SyntaxHighlightedSnippet() {
|
|
|
549
549
|
}
|
|
550
550
|
//#endregion
|
|
551
551
|
//#region src/components/RebaseStudio.tsx
|
|
552
|
-
var SQLEditor = lazyChunk(() => import("./SQLEditor-
|
|
553
|
-
var JSEditor = lazyChunk(() => import("./JSEditor-
|
|
554
|
-
var RLSEditor = lazyChunk(() => import("./RLSEditor-
|
|
555
|
-
var StorageView = lazyChunk(() => import("./StorageView-
|
|
552
|
+
var SQLEditor = lazyChunk(() => import("./SQLEditor-ByecwX8B.js").then((m) => ({ default: m.SQLEditor })));
|
|
553
|
+
var JSEditor = lazyChunk(() => import("./JSEditor-BZnvK1n6.js").then((m) => ({ default: m.JSEditor })));
|
|
554
|
+
var RLSEditor = lazyChunk(() => import("./RLSEditor-CBX4uD66.js").then((m) => ({ default: m.RLSEditor })));
|
|
555
|
+
var StorageView = lazyChunk(() => import("./StorageView-CGm-cacx.js").then((m) => ({ default: m.StorageView })));
|
|
556
556
|
var CronJobsView = lazyChunk(() => import("./CronJobsView-BHtJZeJZ.js").then((m) => ({ default: m.CronJobsView })));
|
|
557
|
-
var SchemaVisualizer = lazyChunk(() => import("./SchemaVisualizer-
|
|
557
|
+
var SchemaVisualizer = lazyChunk(() => import("./SchemaVisualizer-D6tMZgeC.js").then((m) => ({ default: m.SchemaVisualizer })));
|
|
558
558
|
var BranchesView = lazyChunk(() => import("./BranchesView-CM3B-ER3.js").then((m) => ({ default: m.BranchesView })));
|
|
559
559
|
var BackupsView = lazyChunk(() => import("./BackupsView-DNS6LdVg.js").then((m) => ({ default: m.BackupsView })));
|
|
560
|
-
var ApiExplorer = lazyChunk(() => import("./ApiExplorer-
|
|
560
|
+
var ApiExplorer = lazyChunk(() => import("./ApiExplorer-9iwGvNnt.js").then((m) => ({ default: m.ApiExplorer })));
|
|
561
561
|
var LogsExplorer = lazyChunk(() => import("./LogsExplorer-DI8SVpya.js").then((m) => ({ default: m.LogsExplorer })));
|
|
562
562
|
var ApiKeysView = lazyChunk(() => import("./ApiKeysView-C_UoUPuR.js").then((m) => ({ default: m.ApiKeysView })));
|
|
563
563
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/studio",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.16.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "./dist/index.es.js",
|
|
7
7
|
"module": "./dist/index.es.js",
|
|
@@ -16,19 +16,19 @@
|
|
|
16
16
|
"pgsql-ast-parser": "12.0.2",
|
|
17
17
|
"prism-react-renderer": "^2.4.1",
|
|
18
18
|
"react-dropzone": "^19.1.1",
|
|
19
|
-
"@rebasepro/app": "0.
|
|
20
|
-
"@rebasepro/
|
|
21
|
-
"@rebasepro/
|
|
22
|
-
"@rebasepro/
|
|
23
|
-
"@rebasepro/
|
|
24
|
-
"@rebasepro/
|
|
25
|
-
"@rebasepro/utils": "0.
|
|
19
|
+
"@rebasepro/app": "0.16.0",
|
|
20
|
+
"@rebasepro/client": "0.16.0",
|
|
21
|
+
"@rebasepro/common": "0.16.0",
|
|
22
|
+
"@rebasepro/ui": "0.16.0",
|
|
23
|
+
"@rebasepro/types": "0.16.0",
|
|
24
|
+
"@rebasepro/admin-types": "0.16.0",
|
|
25
|
+
"@rebasepro/utils": "0.16.0"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"react": ">=19.2.7",
|
|
29
29
|
"react-dom": ">=19.2.7",
|
|
30
30
|
"react-router": "^8.3.0",
|
|
31
|
-
"@rebasepro/admin": "0.
|
|
31
|
+
"@rebasepro/admin": "0.16.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependenciesMeta": {
|
|
34
34
|
"@rebasepro/admin": {
|
|
@@ -210,7 +210,7 @@ allEndpoints: [] };
|
|
|
210
210
|
<span className="truncate text-[13px] opacity-90">{ep.summary || ep.shortPath}</span>
|
|
211
211
|
<span
|
|
212
212
|
className={cls(
|
|
213
|
-
"text-[10px] font-
|
|
213
|
+
"text-[10px] font-semibold uppercase shrink-0",
|
|
214
214
|
METHOD_COLORS[ep.method] ?? "text-text-secondary"
|
|
215
215
|
)}
|
|
216
216
|
>
|
|
@@ -247,7 +247,7 @@ allEndpoints: [] };
|
|
|
247
247
|
<div className="flex items-center gap-3 min-w-0">
|
|
248
248
|
<span
|
|
249
249
|
className={cls(
|
|
250
|
-
"text-xs font-
|
|
250
|
+
"text-xs font-semibold uppercase",
|
|
251
251
|
METHOD_COLORS[selectedEndpoint.method] ?? ""
|
|
252
252
|
)}
|
|
253
253
|
>
|
|
@@ -275,5 +275,5 @@ function StatusBadge({ code }: { code: string }) {
|
|
|
275
275
|
? "text-amber-600 dark:text-amber-400"
|
|
276
276
|
: "text-red-600 dark:text-red-400";
|
|
277
277
|
|
|
278
|
-
return <span className={cls("text-xs font-
|
|
278
|
+
return <span className={cls("text-xs font-semibold font-mono", color)}>{code}</span>;
|
|
279
279
|
}
|
|
@@ -456,7 +456,7 @@ function ResponseBadge({ status }: { status: number }) {
|
|
|
456
456
|
else if (status >= 400 && status < 500) colorClass = "text-amber-500";
|
|
457
457
|
|
|
458
458
|
return (
|
|
459
|
-
<span className={cls("text-xs font-
|
|
459
|
+
<span className={cls("text-xs font-semibold font-mono", colorClass)}>
|
|
460
460
|
{status || "ERR"}
|
|
461
461
|
</span>
|
|
462
462
|
);
|
|
@@ -778,7 +778,7 @@ message: t("studio_sql_markdown_copy_failed") });
|
|
|
778
778
|
<div className="h-full w-full flex flex-col bg-surface-50 dark:bg-surface-950 overflow-hidden min-h-0">
|
|
779
779
|
{/* Result header — matches SQL editor */}
|
|
780
780
|
<div className={cls("p-2 px-4 bg-surface-100 dark:bg-surface-900 border-b shrink-0 flex items-center", defaultBorderMixin)}>
|
|
781
|
-
<Typography variant="caption" className="font-
|
|
781
|
+
<Typography variant="caption" className="font-semibold text-text-disabled dark:text-text-disabled-dark uppercase tracking-widest text-[10px]">
|
|
782
782
|
{t("studio_sql_query_results")}
|
|
783
783
|
</Typography>
|
|
784
784
|
|
|
@@ -846,7 +846,7 @@ message: t("studio_sql_markdown_copy_failed") });
|
|
|
846
846
|
{matchedCollections.length > 0 && (
|
|
847
847
|
<div className={cls("px-4 py-1.5 border-b flex items-center gap-2 shrink-0 bg-surface-50 dark:bg-surface-900", defaultBorderMixin)}>
|
|
848
848
|
<Tooltip title={t("studio_sql_admin_collections_tooltip")}>
|
|
849
|
-
<Typography variant="caption" className="text-[10px] font-
|
|
849
|
+
<Typography variant="caption" className="text-[10px] font-semibold uppercase tracking-widest text-text-disabled dark:text-text-disabled-dark mr-1 shrink-0 cursor-help">{t("studio_sql_collections_label")}</Typography>
|
|
850
850
|
</Tooltip>
|
|
851
851
|
<div className="flex items-center gap-1.5 overflow-x-auto no-scrollbar">
|
|
852
852
|
{matchedCollections.map(mc => (
|
|
@@ -1002,11 +1002,11 @@ id: String(ra.entityId) })}
|
|
|
1002
1002
|
<div className={cls("p-2 px-4 border-t bg-surface-50 dark:bg-surface-900 flex justify-between items-center shrink-0", defaultBorderMixin)}>
|
|
1003
1003
|
<div className="flex space-x-4">
|
|
1004
1004
|
<div className="flex items-center text-[11px]">
|
|
1005
|
-
<span className="font-
|
|
1005
|
+
<span className="font-semibold text-text-disabled dark:text-text-disabled-dark mr-2 uppercase tracking-tighter">{t("studio_sql_rows")}</span>
|
|
1006
1006
|
<span className="font-mono text-text-secondary dark:text-text-secondary-dark">{tableData.data.length}</span>
|
|
1007
1007
|
</div>
|
|
1008
1008
|
<div className="flex items-center text-[11px]">
|
|
1009
|
-
<span className="font-
|
|
1009
|
+
<span className="font-semibold text-text-disabled dark:text-text-disabled-dark mr-2 uppercase tracking-tighter">{t("studio_sql_time")}</span>
|
|
1010
1010
|
<span className="font-mono text-text-secondary dark:text-text-secondary-dark">{result.duration.toFixed(0)}ms</span>
|
|
1011
1011
|
</div>
|
|
1012
1012
|
</div>
|
|
@@ -1014,7 +1014,7 @@ id: String(ra.entityId) })}
|
|
|
1014
1014
|
<Button
|
|
1015
1015
|
size="small"
|
|
1016
1016
|
variant="text"
|
|
1017
|
-
className="text-[10px] uppercase font-
|
|
1017
|
+
className="text-[10px] uppercase font-semibold text-text-secondary dark:text-text-secondary-dark whitespace-nowrap"
|
|
1018
1018
|
onClick={handleExportMarkdown}
|
|
1019
1019
|
>
|
|
1020
1020
|
{t("studio_sql_copy_markdown")}
|
|
@@ -1022,7 +1022,7 @@ id: String(ra.entityId) })}
|
|
|
1022
1022
|
<Button
|
|
1023
1023
|
size="small"
|
|
1024
1024
|
variant="text"
|
|
1025
|
-
className="text-[10px] uppercase font-
|
|
1025
|
+
className="text-[10px] uppercase font-semibold text-text-secondary dark:text-text-secondary-dark whitespace-nowrap"
|
|
1026
1026
|
onClick={exportResult}
|
|
1027
1027
|
>
|
|
1028
1028
|
{t("studio_sql_export_json")}
|
|
@@ -1030,7 +1030,7 @@ id: String(ra.entityId) })}
|
|
|
1030
1030
|
<Button
|
|
1031
1031
|
size="small"
|
|
1032
1032
|
variant="text"
|
|
1033
|
-
className="text-[10px] uppercase font-
|
|
1033
|
+
className="text-[10px] uppercase font-semibold text-text-secondary dark:text-text-secondary-dark whitespace-nowrap"
|
|
1034
1034
|
onClick={handleExportCSV}
|
|
1035
1035
|
>
|
|
1036
1036
|
{t("studio_sql_export_csv")}
|
|
@@ -119,7 +119,7 @@ export const JSEditorSidebar = ({
|
|
|
119
119
|
{activeTab === "collections" && (
|
|
120
120
|
<div className="flex flex-col h-full">
|
|
121
121
|
<div className={cls("flex items-center justify-between px-3 py-2 border-b bg-surface-50 dark:bg-surface-900 min-h-[48px]", defaultBorderMixin)}>
|
|
122
|
-
<Typography variant="caption" className="font-
|
|
122
|
+
<Typography variant="caption" className="font-semibold uppercase tracking-wider text-text-disabled dark:text-text-disabled-dark">
|
|
123
123
|
Collections
|
|
124
124
|
</Typography>
|
|
125
125
|
</div>
|
|
@@ -147,7 +147,7 @@ export const JSEditorSidebar = ({
|
|
|
147
147
|
{activeTab === "reference" && (
|
|
148
148
|
<div className="flex flex-col h-full">
|
|
149
149
|
<div className={cls("flex items-center justify-between px-3 py-2 border-b bg-surface-50 dark:bg-surface-900 min-h-[48px]", defaultBorderMixin)}>
|
|
150
|
-
<Typography variant="caption" className="font-
|
|
150
|
+
<Typography variant="caption" className="font-semibold uppercase tracking-wider text-text-disabled dark:text-text-disabled-dark">
|
|
151
151
|
SDK Reference
|
|
152
152
|
</Typography>
|
|
153
153
|
</div>
|
|
@@ -187,7 +187,7 @@ export const JSEditorSidebar = ({
|
|
|
187
187
|
return (
|
|
188
188
|
<div className="flex flex-col h-full">
|
|
189
189
|
<div className={cls("flex items-center justify-between px-3 py-2 border-b bg-surface-50 dark:bg-surface-900 min-h-[48px]", defaultBorderMixin)}>
|
|
190
|
-
<Typography variant="caption" className="font-
|
|
190
|
+
<Typography variant="caption" className="font-semibold uppercase tracking-wider text-text-disabled dark:text-text-disabled-dark">Snippets</Typography>
|
|
191
191
|
</div>
|
|
192
192
|
<div className="flex-grow overflow-y-auto p-2 space-y-2 no-scrollbar">
|
|
193
193
|
{snippets.length === 0 ? (
|
|
@@ -200,7 +200,7 @@ export const JSEditorSidebar = ({
|
|
|
200
200
|
<>
|
|
201
201
|
{favorites.length > 0 && (
|
|
202
202
|
<div className="mb-3">
|
|
203
|
-
<Typography variant="caption" className="text-[10px] font-
|
|
203
|
+
<Typography variant="caption" className="text-[10px] font-semibold uppercase tracking-wider text-text-disabled dark:text-text-disabled-dark mb-2 px-1 flex items-center">
|
|
204
204
|
<svg className="w-3 h-3 mr-1 text-amber-500" fill="currentColor" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/></svg>
|
|
205
205
|
Favorites
|
|
206
206
|
</Typography>
|
|
@@ -214,7 +214,7 @@ export const JSEditorSidebar = ({
|
|
|
214
214
|
{others.length > 0 && (
|
|
215
215
|
<div>
|
|
216
216
|
{favorites.length > 0 && (
|
|
217
|
-
<Typography variant="caption" className="text-[10px] font-
|
|
217
|
+
<Typography variant="caption" className="text-[10px] font-semibold uppercase tracking-wider text-text-disabled dark:text-text-disabled-dark mb-2 px-1">
|
|
218
218
|
Others
|
|
219
219
|
</Typography>
|
|
220
220
|
)}
|
|
@@ -236,7 +236,7 @@ export const JSEditorSidebar = ({
|
|
|
236
236
|
{activeTab === "history" && (
|
|
237
237
|
<div className="flex flex-col h-full">
|
|
238
238
|
<div className={cls("flex items-center justify-between px-3 py-2 border-b bg-surface-50 dark:bg-surface-900 min-h-[48px]", defaultBorderMixin)}>
|
|
239
|
-
<Typography variant="caption" className="font-
|
|
239
|
+
<Typography variant="caption" className="font-semibold uppercase tracking-wider text-text-disabled dark:text-text-disabled-dark">History</Typography>
|
|
240
240
|
</div>
|
|
241
241
|
<div className="flex-grow overflow-y-auto p-1 space-y-1 no-scrollbar">
|
|
242
242
|
{history.length === 0 ? (
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import React, { useState, useEffect } from "react";
|
|
1
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
3
2
|
import {
|
|
4
3
|
Button,
|
|
5
4
|
cls,
|
|
@@ -20,6 +19,13 @@ import {
|
|
|
20
19
|
Typography
|
|
21
20
|
} from "@rebasepro/ui";
|
|
22
21
|
import { useTranslation } from "@rebasepro/app";
|
|
22
|
+
import { RLS_JWT_SQL, RLS_ROLES_SQL, RLS_UID_SQL } from "@rebasepro/types";
|
|
23
|
+
import {
|
|
24
|
+
COMMAND_OPTIONS,
|
|
25
|
+
POLICY_PRESETS,
|
|
26
|
+
roleOptionsFor,
|
|
27
|
+
type PolicyCommand
|
|
28
|
+
} from "./policy-presets";
|
|
23
29
|
import { MonacoEditor } from "../SQLEditor/MonacoEditor";
|
|
24
30
|
import type { PostgresPolicy } from "./RLSEditor";
|
|
25
31
|
|
|
@@ -27,99 +33,23 @@ export interface PolicyEditorProps {
|
|
|
27
33
|
policy?: PostgresPolicy;
|
|
28
34
|
schema: string;
|
|
29
35
|
table: string;
|
|
36
|
+
/**
|
|
37
|
+
* Native PostgreSQL roles this database actually has, for the `TO` list.
|
|
38
|
+
*
|
|
39
|
+
* Supplied by the caller from `fetchAvailableRoles()`. Merged with the
|
|
40
|
+
* always-offered defaults by `roleOptionsFor`, never used in place of them:
|
|
41
|
+
* `public` is a keyword and never comes back from that query.
|
|
42
|
+
*/
|
|
43
|
+
roleOptions?: readonly string[];
|
|
30
44
|
onSave: (policyData: Partial<PostgresPolicy>) => void;
|
|
31
45
|
onCancel: () => void;
|
|
32
46
|
}
|
|
33
47
|
|
|
34
|
-
type PolicyCommand = "ALL" | "SELECT" | "INSERT" | "UPDATE" | "DELETE";
|
|
35
|
-
const COMMAND_OPTIONS: PolicyCommand[] = ["ALL", "SELECT", "INSERT", "UPDATE", "DELETE"];
|
|
36
|
-
const ROLE_OPTIONS = ["public", "authenticated", "anon", "admin"];
|
|
37
|
-
|
|
38
|
-
interface PolicyPreset {
|
|
39
|
-
id: string;
|
|
40
|
-
label: string;
|
|
41
|
-
description: string;
|
|
42
|
-
policyname: string;
|
|
43
|
-
cmd: PolicyCommand;
|
|
44
|
-
permissive: "PERMISSIVE" | "RESTRICTIVE";
|
|
45
|
-
roles: string[];
|
|
46
|
-
qual: string;
|
|
47
|
-
with_check: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const POLICY_PRESETS: PolicyPreset[] = [
|
|
51
|
-
{
|
|
52
|
-
id: "public_read",
|
|
53
|
-
label: "Enable read access to everyone",
|
|
54
|
-
description: "Anyone can read data, regardless of authentication status.",
|
|
55
|
-
policyname: "Enable read access for all users",
|
|
56
|
-
cmd: "SELECT",
|
|
57
|
-
permissive: "PERMISSIVE",
|
|
58
|
-
roles: ["public"],
|
|
59
|
-
qual: "true",
|
|
60
|
-
with_check: ""
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
id: "auth_read",
|
|
64
|
-
label: "Enable read access for authenticated users only",
|
|
65
|
-
description: "Only logged-in users are allowed to read data.",
|
|
66
|
-
policyname: "Enable read access for authenticated users",
|
|
67
|
-
cmd: "SELECT",
|
|
68
|
-
permissive: "PERMISSIVE",
|
|
69
|
-
roles: ["authenticated"],
|
|
70
|
-
qual: "true",
|
|
71
|
-
with_check: ""
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
id: "auth_insert",
|
|
75
|
-
label: "Enable insert for authenticated users only",
|
|
76
|
-
description: "Only logged-in users are allowed to insert new data.",
|
|
77
|
-
policyname: "Enable insert for authenticated users only",
|
|
78
|
-
cmd: "INSERT",
|
|
79
|
-
permissive: "PERMISSIVE",
|
|
80
|
-
roles: ["authenticated"],
|
|
81
|
-
qual: "",
|
|
82
|
-
with_check: "true"
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
id: "user_select_own",
|
|
86
|
-
label: "Users can read their own rows",
|
|
87
|
-
description: "Users can only read rows where the uid matches their auth.uid()",
|
|
88
|
-
policyname: "Users can select their own data",
|
|
89
|
-
cmd: "SELECT",
|
|
90
|
-
permissive: "PERMISSIVE",
|
|
91
|
-
roles: ["authenticated"],
|
|
92
|
-
qual: "auth.uid() = uid",
|
|
93
|
-
with_check: ""
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
id: "user_update_own",
|
|
97
|
-
label: "Users can update their own rows",
|
|
98
|
-
description: "Users can only update rows where the uid matches their auth.uid()",
|
|
99
|
-
policyname: "Users can update their own data",
|
|
100
|
-
cmd: "UPDATE",
|
|
101
|
-
permissive: "PERMISSIVE",
|
|
102
|
-
roles: ["authenticated"],
|
|
103
|
-
qual: "auth.uid() = uid",
|
|
104
|
-
with_check: "auth.uid() = uid"
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
id: "user_delete_own",
|
|
108
|
-
label: "Users can delete their own rows",
|
|
109
|
-
description: "Users can only delete rows where the uid matches their auth.uid()",
|
|
110
|
-
policyname: "Users can delete their own data",
|
|
111
|
-
cmd: "DELETE",
|
|
112
|
-
permissive: "PERMISSIVE",
|
|
113
|
-
roles: ["authenticated"],
|
|
114
|
-
qual: "auth.uid() = uid",
|
|
115
|
-
with_check: ""
|
|
116
|
-
}
|
|
117
|
-
];
|
|
118
|
-
|
|
119
48
|
export const PolicyEditor = ({
|
|
120
49
|
policy,
|
|
121
50
|
schema,
|
|
122
51
|
table,
|
|
52
|
+
roleOptions,
|
|
123
53
|
onSave,
|
|
124
54
|
onCancel
|
|
125
55
|
}: PolicyEditorProps) => {
|
|
@@ -135,6 +65,11 @@ export const PolicyEditor = ({
|
|
|
135
65
|
|
|
136
66
|
const [selectedPreset, setSelectedPreset] = useState<string>("");
|
|
137
67
|
|
|
68
|
+
const availableRoles = useMemo(
|
|
69
|
+
() => roleOptionsFor(roleOptions, policy?.roles),
|
|
70
|
+
[roleOptions, policy]
|
|
71
|
+
);
|
|
72
|
+
|
|
138
73
|
useEffect(() => {
|
|
139
74
|
if (policy) {
|
|
140
75
|
setName(policy.policyname || "");
|
|
@@ -300,7 +235,7 @@ export const PolicyEditor = ({
|
|
|
300
235
|
onValueChange={setRoles}
|
|
301
236
|
placeholder={t("studio_policy_roles_placeholder")}
|
|
302
237
|
>
|
|
303
|
-
{
|
|
238
|
+
{availableRoles.map(role => (
|
|
304
239
|
<MultiSelectItem key={role} value={role}>
|
|
305
240
|
{role}
|
|
306
241
|
</MultiSelectItem>
|
|
@@ -396,7 +331,7 @@ export const PolicyEditor = ({
|
|
|
396
331
|
{t("studio_policy_help_step3_desc")}
|
|
397
332
|
</Typography>
|
|
398
333
|
<div className={cls("bg-surface-100 dark:bg-surface-950 px-3 py-2 rounded-md font-mono text-sm my-2", defaultBorderMixin)}>
|
|
399
|
-
Example:
|
|
334
|
+
Example: {RLS_UID_SQL} = uid
|
|
400
335
|
</div>
|
|
401
336
|
<Typography variant="caption" className="text-text-secondary dark:text-text-secondary-dark">
|
|
402
337
|
{t("studio_policy_help_step3_example")}
|
|
@@ -409,7 +344,7 @@ export const PolicyEditor = ({
|
|
|
409
344
|
{t("studio_policy_help_step4_desc")}
|
|
410
345
|
</Typography>
|
|
411
346
|
<div className={cls("bg-surface-100 dark:bg-surface-950 px-3 py-2 rounded-md font-mono text-sm my-2", defaultBorderMixin)}>
|
|
412
|
-
Example:
|
|
347
|
+
Example: {RLS_UID_SQL} = uid
|
|
413
348
|
</div>
|
|
414
349
|
<Typography variant="caption" className="text-text-secondary dark:text-text-secondary-dark">
|
|
415
350
|
{t("studio_policy_help_step4_example")}
|
|
@@ -423,16 +358,16 @@ export const PolicyEditor = ({
|
|
|
423
358
|
</Typography>
|
|
424
359
|
<ul className="list-disc pl-5 space-y-2 text-sm text-text-secondary dark:text-text-secondary-dark font-normal">
|
|
425
360
|
<li>
|
|
426
|
-
<code className="bg-surface-100 dark:bg-surface-950 px-1.5 py-0.5 rounded mr-1 whitespace-nowrap">
|
|
427
|
-
<span className="block mt-0.5">Returns the current user's ID as text. Example: <code className="bg-surface-100 dark:bg-surface-950 px-1 py-0.5 rounded text-[11px]">
|
|
361
|
+
<code className="bg-surface-100 dark:bg-surface-950 px-1.5 py-0.5 rounded mr-1 whitespace-nowrap">{RLS_UID_SQL}</code>
|
|
362
|
+
<span className="block mt-0.5">Returns the current user's ID as text. Example: <code className="bg-surface-100 dark:bg-surface-950 px-1 py-0.5 rounded text-[11px]">{RLS_UID_SQL} = uid</code></span>
|
|
428
363
|
</li>
|
|
429
364
|
<li>
|
|
430
|
-
<code className="bg-surface-100 dark:bg-surface-950 px-1.5 py-0.5 rounded mr-1 whitespace-nowrap">
|
|
431
|
-
<span className="block mt-0.5">Returns the full JWT payload as JSONB so you can check custom claims. Example: <code className="bg-surface-100 dark:bg-surface-950 px-1 py-0.5 rounded text-[11px]">
|
|
365
|
+
<code className="bg-surface-100 dark:bg-surface-950 px-1.5 py-0.5 rounded mr-1 whitespace-nowrap">{RLS_JWT_SQL}</code>
|
|
366
|
+
<span className="block mt-0.5">Returns the full JWT payload as JSONB so you can check custom claims. Example: <code className="bg-surface-100 dark:bg-surface-950 px-1 py-0.5 rounded text-[11px]">{RLS_JWT_SQL} ->> 'email' = 'admin@example.com'</code></span>
|
|
432
367
|
</li>
|
|
433
368
|
<li>
|
|
434
|
-
<code className="bg-surface-100 dark:bg-surface-950 px-1.5 py-0.5 rounded mr-1 whitespace-nowrap">
|
|
435
|
-
<span className="block mt-0.5">Returns the user's role IDs as a comma-separated string. Best used with: <code className="bg-surface-100 dark:bg-surface-950 px-1 py-0.5 rounded text-[11px]">string_to_array(
|
|
369
|
+
<code className="bg-surface-100 dark:bg-surface-950 px-1.5 py-0.5 rounded mr-1 whitespace-nowrap">{RLS_ROLES_SQL}</code>
|
|
370
|
+
<span className="block mt-0.5">Returns the user's role IDs as a comma-separated string. Best used with: <code className="bg-surface-100 dark:bg-surface-950 px-1 py-0.5 rounded text-[11px]">string_to_array({RLS_ROLES_SQL}, ',') @> ARRAY['admin']</code></span>
|
|
436
371
|
</li>
|
|
437
372
|
</ul>
|
|
438
373
|
</Paper>
|
|
@@ -181,6 +181,25 @@ export const RLSEditor = ({ apiUrl = "" }: { apiUrl?: string }) => {
|
|
|
181
181
|
|
|
182
182
|
const [editingPolicy, setEditingPolicy] = useState<PostgresPolicy | "new" | null>(null);
|
|
183
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Native PostgreSQL roles, for the policy editor's `TO` list.
|
|
186
|
+
*
|
|
187
|
+
* `fetchAvailableRoles` and not `fetchApplicationRoles`: a policy's `TO`
|
|
188
|
+
* clause names database roles, and an application role put there produces a
|
|
189
|
+
* policy that either fails to create or matches nothing. The editor falls
|
|
190
|
+
* back to a static pair when the driver cannot answer.
|
|
191
|
+
*/
|
|
192
|
+
const [pgRoleOptions, setPgRoleOptions] = useState<string[]>([]);
|
|
193
|
+
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
let mounted = true;
|
|
196
|
+
if (!databaseAdmin?.fetchAvailableRoles) return;
|
|
197
|
+
databaseAdmin.fetchAvailableRoles()
|
|
198
|
+
.then((roles) => { if (mounted) setPgRoleOptions(roles); })
|
|
199
|
+
.catch((e) => console.error("Failed to fetch database roles", e));
|
|
200
|
+
return () => { mounted = false; };
|
|
201
|
+
}, [databaseAdmin]);
|
|
202
|
+
|
|
184
203
|
const [sidebarSize, setSidebarSize] = useState(() => {
|
|
185
204
|
try {
|
|
186
205
|
const saved = localStorage.getItem("rebase_rls_editor_sidebar_size");
|
|
@@ -428,7 +447,14 @@ status: "live" };
|
|
|
428
447
|
tablename: activeTableData.tableName,
|
|
429
448
|
permissive: (rule.mode || "permissive").toUpperCase() as PostgresPolicy["permissive"],
|
|
430
449
|
cmd: (ops[opIdx] ?? rule.operation ?? "ALL").toUpperCase() as PostgresPolicy["cmd"],
|
|
431
|
-
|
|
450
|
+
// `pgRoles`, not `roles`. This is a policy's `TO` list —
|
|
451
|
+
// database roles — while `SecurityRule.roles` holds
|
|
452
|
+
// *application* roles, which the generator compiles into
|
|
453
|
+
// the USING clause via `rebase.roles()` and never puts in
|
|
454
|
+
// the `TO` list. Reading the wrong field made every rule
|
|
455
|
+
// scoped `roles: ["admin"]` render as `TO admin`, a
|
|
456
|
+
// policy the framework has never written.
|
|
457
|
+
roles: [...(rule.pgRoles ?? ["public"])],
|
|
432
458
|
qual: rule.using || null,
|
|
433
459
|
with_check: rule.withCheck || null,
|
|
434
460
|
// "both" = defined in code and live in Postgres (potentially edited)
|
|
@@ -503,7 +529,7 @@ totalPolicies };
|
|
|
503
529
|
{sidebarTab === "tables" && (
|
|
504
530
|
<div className="flex flex-col h-full">
|
|
505
531
|
<div className={cls("flex items-center justify-between px-3 py-2 border-b bg-surface-50 dark:bg-surface-900 min-h-[48px]", defaultBorderMixin)}>
|
|
506
|
-
<Typography variant="caption" className="font-
|
|
532
|
+
<Typography variant="caption" className="font-semibold uppercase tracking-wider text-text-disabled dark:text-text-disabled-dark">
|
|
507
533
|
{t("studio_schema_tables")}
|
|
508
534
|
</Typography>
|
|
509
535
|
<IconButton size="small" onClick={fetchRLSData} title="Refresh">
|
|
@@ -600,7 +626,7 @@ totalPolicies };
|
|
|
600
626
|
{sidebarTab === "info" && (
|
|
601
627
|
<div className="flex flex-col h-full">
|
|
602
628
|
<div className={cls("flex items-center justify-between px-3 py-2 border-b bg-surface-50 dark:bg-surface-900 min-h-[48px]", defaultBorderMixin)}>
|
|
603
|
-
<Typography variant="caption" className="font-
|
|
629
|
+
<Typography variant="caption" className="font-semibold uppercase tracking-wider text-text-disabled dark:text-text-disabled-dark">
|
|
604
630
|
Overview
|
|
605
631
|
</Typography>
|
|
606
632
|
</div>
|
|
@@ -759,6 +785,7 @@ message: e instanceof Error ? e.message : String(e) });
|
|
|
759
785
|
policy={editingPolicy === "new" ? undefined : editingPolicy}
|
|
760
786
|
schema={activeTableData.schemaName}
|
|
761
787
|
table={activeTableData.tableName}
|
|
788
|
+
roleOptions={pgRoleOptions}
|
|
762
789
|
onSave={async (newPolicy) => {
|
|
763
790
|
/*
|
|
764
791
|
* Where a policy for a *mapped* table belongs depends
|
|
@@ -787,7 +814,22 @@ message: e instanceof Error ? e.message : String(e) });
|
|
|
787
814
|
mode: newPolicy.permissive?.toLowerCase(),
|
|
788
815
|
using: newPolicy.qual || undefined,
|
|
789
816
|
withCheck: newPolicy.with_check || undefined,
|
|
790
|
-
|
|
817
|
+
// The editor edits a `PostgresPolicy`, whose
|
|
818
|
+
// `roles` is the `TO` list — so it maps to
|
|
819
|
+
// `pgRoles`. Writing it to `roles` filed
|
|
820
|
+
// database roles as *application* roles, and
|
|
821
|
+
// the generator then compiled them into a
|
|
822
|
+
// `rebase.roles()` check no user could
|
|
823
|
+
// satisfy: the rule saved cleanly, pushed
|
|
824
|
+
// cleanly, and matched nothing.
|
|
825
|
+
//
|
|
826
|
+
// Omitted at the default so a rule that
|
|
827
|
+
// targets `public` — nearly all of them —
|
|
828
|
+
// does not carry an advanced field it does
|
|
829
|
+
// not need.
|
|
830
|
+
...(newPolicy.roles && !(newPolicy.roles.length === 1 && newPolicy.roles[0] === "public")
|
|
831
|
+
? { pgRoles: newPolicy.roles }
|
|
832
|
+
: {})
|
|
791
833
|
};
|
|
792
834
|
|
|
793
835
|
const existingRules = (isPostgresCollectionConfig(activeCollection) ? activeCollection.securityRules : undefined) || [];
|