@orion-studios/cms 0.5.6 → 0.5.7
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 +21 -4
- package/dist/{chunk-AYV6KYDP.js → chunk-NSAZCP4I.js} +20 -1
- package/dist/content/index.js +1 -1
- package/dist/server/index.d.ts +84 -18
- package/dist/server/index.js +708 -117
- package/dist/studio/index.js +25 -5
- package/package.json +1 -1
- package/sql/bootstrap.sql +313 -6
- package/sql/migrations/20260829032027_cms_security_and_sync_contracts.sql +390 -0
package/dist/studio/index.js
CHANGED
|
@@ -208,6 +208,20 @@ var studioAccess = (role) => ({
|
|
|
208
208
|
import { createClient } from "@supabase/supabase-js";
|
|
209
209
|
import { useCallback, useEffect, useMemo, useState as useState2 } from "react";
|
|
210
210
|
|
|
211
|
+
// src/supabase-keys.ts
|
|
212
|
+
var isServerSupabaseKey = (key) => {
|
|
213
|
+
if (key.startsWith("sb_secret_")) return true;
|
|
214
|
+
const payload = key.split(".")[1];
|
|
215
|
+
if (!payload) return false;
|
|
216
|
+
try {
|
|
217
|
+
const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
218
|
+
const decoded = JSON.parse(atob(normalized));
|
|
219
|
+
return decoded.role === "service_role";
|
|
220
|
+
} catch {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
211
225
|
// src/studio/PasswordInput.tsx
|
|
212
226
|
import { useState } from "react";
|
|
213
227
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -260,18 +274,24 @@ function PasswordInput({ className, ...props }) {
|
|
|
260
274
|
// src/studio/auth.tsx
|
|
261
275
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
262
276
|
var browserClient = null;
|
|
277
|
+
var publicSupabaseKey = () => process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "";
|
|
263
278
|
function getBrowserSupabase() {
|
|
264
279
|
if (!browserClient) {
|
|
265
280
|
const url = process.env.NEXT_PUBLIC_SUPABASE_URL || "";
|
|
266
|
-
const
|
|
267
|
-
if (!url || !
|
|
268
|
-
throw new Error(
|
|
281
|
+
const publishableKey = publicSupabaseKey();
|
|
282
|
+
if (!url || !publishableKey) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
"Orion Studio: NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY must be set. NEXT_PUBLIC_SUPABASE_ANON_KEY remains supported for legacy projects."
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
if (isServerSupabaseKey(publishableKey)) {
|
|
288
|
+
throw new Error("Orion Studio: a Supabase server key cannot be used in a NEXT_PUBLIC variable.");
|
|
269
289
|
}
|
|
270
|
-
browserClient = createClient(url,
|
|
290
|
+
browserClient = createClient(url, publishableKey);
|
|
271
291
|
}
|
|
272
292
|
return browserClient;
|
|
273
293
|
}
|
|
274
|
-
var isStudioMemoryMode = () => (!process.env.NEXT_PUBLIC_SUPABASE_URL || !
|
|
294
|
+
var isStudioMemoryMode = () => (!process.env.NEXT_PUBLIC_SUPABASE_URL || !publicSupabaseKey()) && process.env.NODE_ENV !== "production";
|
|
275
295
|
var MEMORY_DEV_TOKEN = "orion-dev-token";
|
|
276
296
|
var memorySession = {
|
|
277
297
|
access_token: MEMORY_DEV_TOKEN,
|
package/package.json
CHANGED
package/sql/bootstrap.sql
CHANGED
|
@@ -213,8 +213,13 @@ create table if not exists cms_globals (
|
|
|
213
213
|
key text primary key,
|
|
214
214
|
label text not null default '',
|
|
215
215
|
data jsonb not null default '{}'::jsonb,
|
|
216
|
+
builder_owned boolean not null default false,
|
|
216
217
|
updated_at timestamptz not null default now()
|
|
217
218
|
);
|
|
219
|
+
alter table cms_globals add column if not exists builder_owned boolean;
|
|
220
|
+
update cms_globals set builder_owned = true where builder_owned is null;
|
|
221
|
+
alter table cms_globals alter column builder_owned set not null;
|
|
222
|
+
alter table cms_globals alter column builder_owned set default false;
|
|
218
223
|
|
|
219
224
|
create table if not exists cms_media (
|
|
220
225
|
id uuid primary key default gen_random_uuid(),
|
|
@@ -226,9 +231,14 @@ create table if not exists cms_media (
|
|
|
226
231
|
width integer,
|
|
227
232
|
height integer,
|
|
228
233
|
filesize integer,
|
|
234
|
+
builder_owned boolean not null default false,
|
|
229
235
|
created_at timestamptz not null default now(),
|
|
230
236
|
updated_at timestamptz not null default now()
|
|
231
237
|
);
|
|
238
|
+
alter table cms_media add column if not exists builder_owned boolean;
|
|
239
|
+
update cms_media set builder_owned = true where builder_owned is null;
|
|
240
|
+
alter table cms_media alter column builder_owned set not null;
|
|
241
|
+
alter table cms_media alter column builder_owned set default false;
|
|
232
242
|
|
|
233
243
|
create table if not exists cms_forms (
|
|
234
244
|
id uuid primary key default gen_random_uuid(),
|
|
@@ -236,9 +246,14 @@ create table if not exists cms_forms (
|
|
|
236
246
|
title text not null default '',
|
|
237
247
|
config jsonb not null default '{}'::jsonb, -- steps/fields, per forms module contract
|
|
238
248
|
success_message text not null default '',
|
|
249
|
+
builder_owned boolean not null default false,
|
|
239
250
|
created_at timestamptz not null default now(),
|
|
240
251
|
updated_at timestamptz not null default now()
|
|
241
252
|
);
|
|
253
|
+
alter table cms_forms add column if not exists builder_owned boolean;
|
|
254
|
+
update cms_forms set builder_owned = true where builder_owned is null;
|
|
255
|
+
alter table cms_forms alter column builder_owned set not null;
|
|
256
|
+
alter table cms_forms alter column builder_owned set default false;
|
|
242
257
|
|
|
243
258
|
-- Notification settings live OUTSIDE config: config is anon-readable (the
|
|
244
259
|
-- public form renderer needs the field list), notify addresses are not.
|
|
@@ -251,6 +266,43 @@ update cms_forms
|
|
|
251
266
|
config = config - 'notify'
|
|
252
267
|
where config ? 'notify';
|
|
253
268
|
|
|
269
|
+
create table if not exists cms_preview_grants (
|
|
270
|
+
id uuid primary key,
|
|
271
|
+
page_id uuid not null references cms_pages (id) on delete cascade,
|
|
272
|
+
user_id uuid not null,
|
|
273
|
+
project_ref text not null check (project_ref ~ '^[a-z0-9][a-z0-9-]{0,63}$'),
|
|
274
|
+
key_id text not null check (key_id ~ '^[A-Za-z0-9._-]{1,64}$'),
|
|
275
|
+
expires_at timestamptz not null,
|
|
276
|
+
revoked_at timestamptz,
|
|
277
|
+
last_used_at timestamptz,
|
|
278
|
+
use_count integer not null default 0 check (use_count >= 0),
|
|
279
|
+
max_uses integer not null check (max_uses between 1 and 32),
|
|
280
|
+
created_at timestamptz not null default now()
|
|
281
|
+
);
|
|
282
|
+
create index if not exists cms_preview_grants_page_active_idx
|
|
283
|
+
on cms_preview_grants (page_id, expires_at)
|
|
284
|
+
where revoked_at is null;
|
|
285
|
+
create index if not exists cms_preview_grants_user_active_idx
|
|
286
|
+
on cms_preview_grants (user_id, expires_at)
|
|
287
|
+
where revoked_at is null;
|
|
288
|
+
|
|
289
|
+
create table if not exists cms_target_identity (
|
|
290
|
+
singleton boolean primary key default true check (singleton),
|
|
291
|
+
project_ref text not null check (project_ref ~ '^[a-z0-9][a-z0-9-]{0,63}$'),
|
|
292
|
+
installed_at timestamptz not null default now(),
|
|
293
|
+
updated_at timestamptz not null default now()
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
create table if not exists cms_sync_runs (
|
|
297
|
+
manifest_hash text primary key check (manifest_hash ~ '^[a-f0-9]{64}$'),
|
|
298
|
+
target_receipt_hash text not null check (target_receipt_hash ~ '^[a-f0-9]{64}$'),
|
|
299
|
+
status text not null check (status in ('active', 'complete', 'failed')),
|
|
300
|
+
result jsonb,
|
|
301
|
+
error text,
|
|
302
|
+
started_at timestamptz not null default now(),
|
|
303
|
+
updated_at timestamptz not null default now()
|
|
304
|
+
);
|
|
305
|
+
|
|
254
306
|
create table if not exists cms_form_submissions (
|
|
255
307
|
id bigint generated always as identity primary key,
|
|
256
308
|
form_id uuid not null references cms_forms (id) on delete cascade,
|
|
@@ -284,8 +336,13 @@ create table if not exists cms_redirects (
|
|
|
284
336
|
from_path text not null unique,
|
|
285
337
|
to_path text not null,
|
|
286
338
|
permanent boolean not null default true,
|
|
339
|
+
builder_owned boolean not null default false,
|
|
287
340
|
created_at timestamptz not null default now()
|
|
288
341
|
);
|
|
342
|
+
alter table cms_redirects add column if not exists builder_owned boolean;
|
|
343
|
+
update cms_redirects set builder_owned = true where builder_owned is null;
|
|
344
|
+
alter table cms_redirects alter column builder_owned set not null;
|
|
345
|
+
alter table cms_redirects alter column builder_owned set default false;
|
|
289
346
|
|
|
290
347
|
-- First-party analytics events. Append-only; pruned after the retention
|
|
291
348
|
-- window by the cron route. session_key rotates daily. visitor_key is an
|
|
@@ -780,15 +837,16 @@ create or replace function cms_update_global(
|
|
|
780
837
|
) returns cms_globals
|
|
781
838
|
language plpgsql
|
|
782
839
|
security definer
|
|
783
|
-
set search_path = public
|
|
840
|
+
set search_path = public, pg_catalog
|
|
784
841
|
as $$
|
|
785
842
|
declare
|
|
786
843
|
result cms_globals;
|
|
787
844
|
begin
|
|
788
|
-
insert into cms_globals (key, data, updated_at)
|
|
789
|
-
values (p_key, p_data, now())
|
|
845
|
+
insert into cms_globals (key, data, builder_owned, updated_at)
|
|
846
|
+
values (p_key, p_data, true, now())
|
|
790
847
|
on conflict (key) do update
|
|
791
848
|
set data = excluded.data,
|
|
849
|
+
builder_owned = true,
|
|
792
850
|
updated_at = now()
|
|
793
851
|
returning * into result;
|
|
794
852
|
|
|
@@ -799,6 +857,223 @@ begin
|
|
|
799
857
|
end;
|
|
800
858
|
$$;
|
|
801
859
|
|
|
860
|
+
create or replace function cms_consume_preview_grant(
|
|
861
|
+
p_grant_id uuid,
|
|
862
|
+
p_page_id uuid,
|
|
863
|
+
p_user_id uuid,
|
|
864
|
+
p_project_ref text,
|
|
865
|
+
p_key_id text
|
|
866
|
+
) returns boolean
|
|
867
|
+
language plpgsql
|
|
868
|
+
security definer
|
|
869
|
+
set search_path = public, pg_catalog
|
|
870
|
+
as $$
|
|
871
|
+
declare
|
|
872
|
+
consumed_id uuid;
|
|
873
|
+
begin
|
|
874
|
+
update cms_preview_grants
|
|
875
|
+
set use_count = use_count + 1,
|
|
876
|
+
last_used_at = now()
|
|
877
|
+
where id = p_grant_id
|
|
878
|
+
and page_id = p_page_id
|
|
879
|
+
and user_id = p_user_id
|
|
880
|
+
and project_ref = p_project_ref
|
|
881
|
+
and key_id = p_key_id
|
|
882
|
+
and revoked_at is null
|
|
883
|
+
and expires_at > now()
|
|
884
|
+
and use_count < max_uses
|
|
885
|
+
returning id into consumed_id;
|
|
886
|
+
|
|
887
|
+
return consumed_id is not null;
|
|
888
|
+
end;
|
|
889
|
+
$$;
|
|
890
|
+
|
|
891
|
+
create or replace function cms_sync_global(
|
|
892
|
+
p_key text,
|
|
893
|
+
p_data jsonb
|
|
894
|
+
) returns boolean
|
|
895
|
+
language plpgsql
|
|
896
|
+
security definer
|
|
897
|
+
set search_path = public, pg_catalog
|
|
898
|
+
as $$
|
|
899
|
+
declare
|
|
900
|
+
changed_key text;
|
|
901
|
+
begin
|
|
902
|
+
insert into cms_globals (key, data, builder_owned, updated_at)
|
|
903
|
+
values (p_key, p_data, false, now())
|
|
904
|
+
on conflict (key) do update
|
|
905
|
+
set data = excluded.data,
|
|
906
|
+
updated_at = now()
|
|
907
|
+
where not cms_globals.builder_owned
|
|
908
|
+
returning key into changed_key;
|
|
909
|
+
|
|
910
|
+
return changed_key is not null;
|
|
911
|
+
end;
|
|
912
|
+
$$;
|
|
913
|
+
|
|
914
|
+
create or replace function cms_sync_form(
|
|
915
|
+
p_slug text,
|
|
916
|
+
p_title text,
|
|
917
|
+
p_config jsonb,
|
|
918
|
+
p_success_message text
|
|
919
|
+
) returns boolean
|
|
920
|
+
language plpgsql
|
|
921
|
+
security definer
|
|
922
|
+
set search_path = public, pg_catalog
|
|
923
|
+
as $$
|
|
924
|
+
declare
|
|
925
|
+
changed_slug text;
|
|
926
|
+
begin
|
|
927
|
+
insert into cms_forms (
|
|
928
|
+
slug, title, config, success_message, builder_owned, updated_at
|
|
929
|
+
) values (
|
|
930
|
+
p_slug, p_title, p_config - 'notify', p_success_message, false, now()
|
|
931
|
+
)
|
|
932
|
+
on conflict (slug) do update
|
|
933
|
+
set title = excluded.title,
|
|
934
|
+
config = excluded.config,
|
|
935
|
+
success_message = excluded.success_message,
|
|
936
|
+
updated_at = now()
|
|
937
|
+
where not cms_forms.builder_owned
|
|
938
|
+
returning slug into changed_slug;
|
|
939
|
+
|
|
940
|
+
return changed_slug is not null;
|
|
941
|
+
end;
|
|
942
|
+
$$;
|
|
943
|
+
|
|
944
|
+
create or replace function cms_sync_media(
|
|
945
|
+
p_storage_path text,
|
|
946
|
+
p_filename text,
|
|
947
|
+
p_alt text,
|
|
948
|
+
p_caption text,
|
|
949
|
+
p_mime_type text,
|
|
950
|
+
p_width integer,
|
|
951
|
+
p_height integer,
|
|
952
|
+
p_filesize bigint
|
|
953
|
+
) returns boolean
|
|
954
|
+
language plpgsql
|
|
955
|
+
security definer
|
|
956
|
+
set search_path = public, pg_catalog
|
|
957
|
+
as $$
|
|
958
|
+
declare
|
|
959
|
+
changed_path text;
|
|
960
|
+
begin
|
|
961
|
+
insert into cms_media (
|
|
962
|
+
storage_path, filename, alt, caption, mime_type, width, height, filesize, builder_owned, updated_at
|
|
963
|
+
) values (
|
|
964
|
+
p_storage_path, p_filename, p_alt, p_caption, p_mime_type,
|
|
965
|
+
p_width, p_height, p_filesize, false, now()
|
|
966
|
+
)
|
|
967
|
+
on conflict (storage_path) do update
|
|
968
|
+
set filename = excluded.filename,
|
|
969
|
+
alt = excluded.alt,
|
|
970
|
+
caption = excluded.caption,
|
|
971
|
+
mime_type = excluded.mime_type,
|
|
972
|
+
width = excluded.width,
|
|
973
|
+
height = excluded.height,
|
|
974
|
+
filesize = excluded.filesize,
|
|
975
|
+
updated_at = now()
|
|
976
|
+
where not cms_media.builder_owned
|
|
977
|
+
returning storage_path into changed_path;
|
|
978
|
+
return changed_path is not null;
|
|
979
|
+
end;
|
|
980
|
+
$$;
|
|
981
|
+
|
|
982
|
+
create or replace function cms_sync_redirect(
|
|
983
|
+
p_from_path text,
|
|
984
|
+
p_to_path text,
|
|
985
|
+
p_permanent boolean
|
|
986
|
+
) returns boolean
|
|
987
|
+
language plpgsql
|
|
988
|
+
security definer
|
|
989
|
+
set search_path = public, pg_catalog
|
|
990
|
+
as $$
|
|
991
|
+
declare
|
|
992
|
+
changed_path text;
|
|
993
|
+
begin
|
|
994
|
+
insert into cms_redirects (from_path, to_path, permanent, builder_owned)
|
|
995
|
+
values (p_from_path, p_to_path, p_permanent, false)
|
|
996
|
+
on conflict (from_path) do update
|
|
997
|
+
set to_path = excluded.to_path,
|
|
998
|
+
permanent = excluded.permanent
|
|
999
|
+
where not cms_redirects.builder_owned
|
|
1000
|
+
returning from_path into changed_path;
|
|
1001
|
+
return changed_path is not null;
|
|
1002
|
+
end;
|
|
1003
|
+
$$;
|
|
1004
|
+
|
|
1005
|
+
create or replace function cms_begin_sync(
|
|
1006
|
+
p_manifest_hash text,
|
|
1007
|
+
p_target_receipt_hash text
|
|
1008
|
+
) returns jsonb
|
|
1009
|
+
language plpgsql
|
|
1010
|
+
security definer
|
|
1011
|
+
set search_path = public, pg_catalog
|
|
1012
|
+
as $$
|
|
1013
|
+
declare
|
|
1014
|
+
active_run cms_sync_runs;
|
|
1015
|
+
requested_run cms_sync_runs;
|
|
1016
|
+
begin
|
|
1017
|
+
if p_manifest_hash !~ '^[a-f0-9]{64}$' or p_target_receipt_hash !~ '^[a-f0-9]{64}$' then
|
|
1018
|
+
raise exception 'Invalid sync receipt.';
|
|
1019
|
+
end if;
|
|
1020
|
+
-- Serialize lease decisions across every manifest. Row locks cannot protect
|
|
1021
|
+
-- the empty set when two new manifests start at the same time.
|
|
1022
|
+
perform pg_advisory_xact_lock(hashtextextended('orion_cms_content_sync', 0));
|
|
1023
|
+
select * into requested_run from cms_sync_runs where manifest_hash = p_manifest_hash for update;
|
|
1024
|
+
if requested_run.status = 'complete' then
|
|
1025
|
+
return jsonb_build_object('status', 'complete', 'result', requested_run.result);
|
|
1026
|
+
end if;
|
|
1027
|
+
if requested_run.status = 'active' and requested_run.updated_at > now() - interval '15 minutes' then
|
|
1028
|
+
return jsonb_build_object('status', 'busy');
|
|
1029
|
+
end if;
|
|
1030
|
+
select * into active_run
|
|
1031
|
+
from cms_sync_runs
|
|
1032
|
+
where status = 'active' and manifest_hash <> p_manifest_hash
|
|
1033
|
+
order by updated_at desc
|
|
1034
|
+
limit 1
|
|
1035
|
+
for update;
|
|
1036
|
+
if active_run.manifest_hash is not null and active_run.updated_at > now() - interval '15 minutes' then
|
|
1037
|
+
return jsonb_build_object('status', 'busy');
|
|
1038
|
+
end if;
|
|
1039
|
+
if active_run.manifest_hash is not null then
|
|
1040
|
+
update cms_sync_runs
|
|
1041
|
+
set status = 'failed', error = 'Sync lease expired.', updated_at = now()
|
|
1042
|
+
where manifest_hash = active_run.manifest_hash;
|
|
1043
|
+
end if;
|
|
1044
|
+
insert into cms_sync_runs (manifest_hash, target_receipt_hash, status)
|
|
1045
|
+
values (p_manifest_hash, p_target_receipt_hash, 'active')
|
|
1046
|
+
on conflict (manifest_hash) do update
|
|
1047
|
+
set target_receipt_hash = excluded.target_receipt_hash,
|
|
1048
|
+
status = 'active', result = null, error = null, updated_at = now();
|
|
1049
|
+
return jsonb_build_object('status', case when requested_run.manifest_hash is null then 'started' else 'resume' end);
|
|
1050
|
+
end;
|
|
1051
|
+
$$;
|
|
1052
|
+
|
|
1053
|
+
create or replace function cms_finish_sync(
|
|
1054
|
+
p_manifest_hash text,
|
|
1055
|
+
p_status text,
|
|
1056
|
+
p_result jsonb default null,
|
|
1057
|
+
p_error text default null
|
|
1058
|
+
) returns boolean
|
|
1059
|
+
language plpgsql
|
|
1060
|
+
security definer
|
|
1061
|
+
set search_path = public, pg_catalog
|
|
1062
|
+
as $$
|
|
1063
|
+
begin
|
|
1064
|
+
if p_status not in ('complete', 'failed') then
|
|
1065
|
+
raise exception 'Invalid sync completion status.';
|
|
1066
|
+
end if;
|
|
1067
|
+
update cms_sync_runs
|
|
1068
|
+
set status = p_status,
|
|
1069
|
+
result = case when p_status = 'complete' then p_result else null end,
|
|
1070
|
+
error = case when p_status = 'failed' then left(coalesce(p_error, 'Sync failed.'), 500) else null end,
|
|
1071
|
+
updated_at = now()
|
|
1072
|
+
where manifest_hash = p_manifest_hash and status = 'active';
|
|
1073
|
+
return found;
|
|
1074
|
+
end;
|
|
1075
|
+
$$;
|
|
1076
|
+
|
|
802
1077
|
-- 3) Row-level security -------------------------------------------------------
|
|
803
1078
|
-- Defense-in-depth. The app's API layer (service role) is the primary
|
|
804
1079
|
-- enforcement point; anonymous access is read-only and published-only.
|
|
@@ -816,6 +1091,11 @@ alter table cms_redirects enable row level security;
|
|
|
816
1091
|
alter table cms_activity enable row level security;
|
|
817
1092
|
alter table cms_events enable row level security;
|
|
818
1093
|
alter table cms_rate_limits enable row level security;
|
|
1094
|
+
alter table cms_preview_grants enable row level security;
|
|
1095
|
+
alter table cms_target_identity enable row level security;
|
|
1096
|
+
alter table cms_sync_runs enable row level security;
|
|
1097
|
+
alter table cms_preview_grants enable row level security;
|
|
1098
|
+
alter table cms_target_identity enable row level security;
|
|
819
1099
|
-- cms_events: no anon policies or grants — ingest and reads go through the
|
|
820
1100
|
-- API layer only.
|
|
821
1101
|
|
|
@@ -848,7 +1128,7 @@ begin
|
|
|
848
1128
|
drop policy if exists cms_profiles_self_read on cms_profiles;
|
|
849
1129
|
execute 'create policy cms_profiles_self_read on cms_profiles '
|
|
850
1130
|
|| 'for select using (auth.uid() = user_id)';
|
|
851
|
-
exception when insufficient_privilege then
|
|
1131
|
+
exception when insufficient_privilege or invalid_schema_name or undefined_function then
|
|
852
1132
|
raise notice 'Skipped cms_profiles_self_read policy (no auth schema access).';
|
|
853
1133
|
end $profiles_policy$;
|
|
854
1134
|
|
|
@@ -861,6 +1141,8 @@ end $profiles_policy$;
|
|
|
861
1141
|
-- default and Supabase exposes public-schema functions as RPC, so without an
|
|
862
1142
|
-- explicit revoke the anon key could call them and bypass RLS + the API
|
|
863
1143
|
-- permission matrix. Only the API layer (service role) may execute them.
|
|
1144
|
+
alter default privileges revoke execute on functions from public;
|
|
1145
|
+
|
|
864
1146
|
do $fn_lockdown$
|
|
865
1147
|
declare
|
|
866
1148
|
fn text;
|
|
@@ -875,10 +1157,28 @@ declare
|
|
|
875
1157
|
'cms_rate_limit_consume(text, integer, integer)',
|
|
876
1158
|
'cms_rate_limit_consume(text, integer, integer, integer)',
|
|
877
1159
|
'cms_prune_rate_limits(timestamptz)',
|
|
878
|
-
'cms_update_global(text, jsonb, uuid)'
|
|
1160
|
+
'cms_update_global(text, jsonb, uuid)',
|
|
1161
|
+
'cms_consume_preview_grant(uuid, uuid, uuid, text, text)',
|
|
1162
|
+
'cms_sync_global(text, jsonb)',
|
|
1163
|
+
'cms_sync_form(text, text, jsonb, text)',
|
|
1164
|
+
'cms_sync_media(text, text, text, text, text, integer, integer, bigint)',
|
|
1165
|
+
'cms_sync_redirect(text, text, boolean)',
|
|
1166
|
+
'cms_begin_sync(text, text)',
|
|
1167
|
+
'cms_finish_sync(text, text, jsonb, text)'
|
|
879
1168
|
];
|
|
880
1169
|
begin
|
|
1170
|
+
if exists (select 1 from pg_roles where rolname = 'anon') then
|
|
1171
|
+
execute 'alter default privileges in schema public revoke all on tables from anon';
|
|
1172
|
+
execute 'alter default privileges in schema public revoke all on sequences from anon';
|
|
1173
|
+
execute 'alter default privileges in schema public revoke execute on functions from anon';
|
|
1174
|
+
end if;
|
|
1175
|
+
if exists (select 1 from pg_roles where rolname = 'authenticated') then
|
|
1176
|
+
execute 'alter default privileges in schema public revoke all on tables from authenticated';
|
|
1177
|
+
execute 'alter default privileges in schema public revoke all on sequences from authenticated';
|
|
1178
|
+
execute 'alter default privileges in schema public revoke execute on functions from authenticated';
|
|
1179
|
+
end if;
|
|
881
1180
|
foreach fn in array fns loop
|
|
1181
|
+
execute format('alter function %s set search_path to public, pg_catalog', fn);
|
|
882
1182
|
execute format('revoke execute on function %s from public', fn);
|
|
883
1183
|
if exists (select 1 from pg_roles where rolname = 'anon') then
|
|
884
1184
|
execute format('revoke execute on function %s from anon', fn);
|
|
@@ -901,14 +1201,19 @@ revoke update, delete on cms_page_versions from public;
|
|
|
901
1201
|
revoke select on cms_forms from public;
|
|
902
1202
|
revoke select (notify) on cms_forms from public;
|
|
903
1203
|
revoke select on cms_media from public;
|
|
1204
|
+
revoke all on cms_preview_grants from public;
|
|
1205
|
+
revoke all on cms_target_identity from public;
|
|
1206
|
+
revoke all on cms_sync_runs from public;
|
|
904
1207
|
do $grants$
|
|
905
1208
|
begin
|
|
906
1209
|
if exists (select 1 from pg_roles where rolname = 'service_role') then
|
|
907
1210
|
grant select, insert, update, delete on
|
|
908
1211
|
cms_settings, cms_profiles, cms_pages,
|
|
909
1212
|
cms_globals, cms_media, cms_forms, cms_form_submissions,
|
|
910
|
-
cms_global_versions, cms_redirects, cms_activity, cms_events
|
|
1213
|
+
cms_global_versions, cms_redirects, cms_activity, cms_events,
|
|
1214
|
+
cms_preview_grants, cms_sync_runs
|
|
911
1215
|
to service_role;
|
|
1216
|
+
grant select on cms_target_identity to service_role;
|
|
912
1217
|
revoke update, delete on cms_page_versions from service_role;
|
|
913
1218
|
grant select, insert on cms_page_versions to service_role;
|
|
914
1219
|
end if;
|
|
@@ -926,6 +1231,7 @@ begin
|
|
|
926
1231
|
revoke select (notify) on cms_forms from anon;
|
|
927
1232
|
grant select (id, slug, title, config, success_message, created_at, updated_at)
|
|
928
1233
|
on cms_forms to anon;
|
|
1234
|
+
revoke all on cms_preview_grants, cms_target_identity, cms_sync_runs from anon;
|
|
929
1235
|
end if;
|
|
930
1236
|
if exists (select 1 from pg_roles where rolname = 'authenticated') then
|
|
931
1237
|
grant select on cms_globals, cms_profiles, cms_redirects to authenticated;
|
|
@@ -938,5 +1244,6 @@ begin
|
|
|
938
1244
|
revoke select (notify) on cms_forms from authenticated;
|
|
939
1245
|
grant select (id, slug, title, config, success_message, created_at, updated_at)
|
|
940
1246
|
on cms_forms to authenticated;
|
|
1247
|
+
revoke all on cms_preview_grants, cms_target_identity, cms_sync_runs from authenticated;
|
|
941
1248
|
end if;
|
|
942
1249
|
end $grants$;
|