@nextblock-cms/db 0.22.1 → 0.22.3
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 +47 -47
- package/lib/media-actions.cjs.js +1 -1
- package/lib/media-actions.d.ts +13 -35
- package/lib/media-actions.es.js +25 -49
- package/lib/media-actions.test.d.ts +1 -0
- package/lib/media-record.cjs.js +1 -0
- package/lib/media-record.d.ts +46 -0
- package/lib/media-record.es.js +47 -0
- package/lib/package-validation.cjs.js +1 -1
- package/lib/package-validation.es.js +1 -1
- package/lib/secret-crypto.cjs.js +1 -1
- package/lib/secret-crypto.es.js +1 -0
- package/lib/supabase/server.cjs.js +1 -1
- package/lib/supabase/server.es.js +1 -0
- package/package.json +1 -1
- package/server.cjs.js +1 -1
- package/server.d.ts +1 -0
- package/server.es.js +6 -5
- package/supabase/migrations/02000_catchup_gen1.sql +5880 -5880
- package/supabase/migrations/02001_baseline_schema.sql +2734 -2734
- package/supabase/migrations/02002_baseline_constraints_and_indexes.sql +1262 -1262
- package/supabase/migrations/02003_baseline_security_and_grants.sql +1577 -1577
- package/supabase/migrations/02004_baseline_seed.sql +565 -565
- package/supabase/migrations/02005_home_live_demo_promo_cadence_15min.sql +53 -53
- package/supabase/migrations/02006_home_marketing_contrast.sql +56 -56
- package/supabase/migrations/02007_home_pillar_caption_contrast.sql +44 -44
- package/supabase/migrations/02008_home_fr_parity.sql +89 -89
- package/supabase/migrations/02009_fr_cortex_posts.sql +67 -67
- package/supabase/migrations/02010_shop_hero_redesign.sql +72 -72
- package/supabase/migrations/02011_drop_backdrop_blur_on_pages.sql +26 -26
- package/supabase/migrations/02012_drop_variant_backdrop_blur.sql +17 -17
- package/supabase/migrations/02013_advisor_rls_and_trigger_grants.sql +102 -102
- package/supabase/migrations/02014_package_activations_staff_read.sql +18 -18
- package/supabase/migrations/02018_install_paths_and_seed_copy_accuracy.sql +1820 -1820
- package/supabase/migrations/02019_open_handed_licensing_and_mcp_clients.sql +448 -448
- package/supabase/migrations/02020_commerce_product_accuracy.sql +93 -93
- package/supabase/migrations/02021_profiles_staff_read_and_role_guard.sql +362 -362
- package/supabase/migrations/02022_security_hardening_writer_boundary_and_aal.sql +504 -504
- package/supabase/migrations/02024_advisor_trigger_grants_and_pg_net_schema.sql +113 -0
- package/supabase/migrations/02025_api_role_grant_hardening.sql +66 -0
- package/supabase/migrations/02026_authenticated_write_grant_hardening.sql +91 -0
- package/supabase/migrations/02027_api_role_select_grant_hardening.sql +150 -0
- package/supabase/migrations/02028_writer_settings_allowlist_email_factor_aal_and_media_keys.sql +284 -0
package/supabase/migrations/02028_writer_settings_allowlist_email_factor_aal_and_media_keys.sql
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
-- 02028: close the database holes the 2026-10-21 audit confirmed on a local replay (each one
|
|
2
|
+
-- reproduced over PostgREST: SET LOCAL ROLE authenticated + request.jwt.claims, rolled back).
|
|
3
|
+
-- Append-only, forward-only, idempotent: every policy is dropped by name and recreated, every
|
|
4
|
+
-- function is CREATE OR REPLACE, every grant is safe to repeat. 02022/02023 are the model.
|
|
5
|
+
--
|
|
6
|
+
-- 1. site_settings: the WRITER write branch becomes an allow-list (HIGH).
|
|
7
|
+
-- 02022/02023 let ADMIN *or WRITER* write every key outside a 13-key deny-list. A WRITER
|
|
8
|
+
-- could therefore PATCH `email_public` to point SMTP at their own server, and
|
|
9
|
+
-- lib/config/email-settings.ts combines that host with the decrypted ADMIN-only
|
|
10
|
+
-- `email_secret` credentials: the SMTP password and every outgoing mail (ADMIN email-2FA
|
|
11
|
+
-- codes included) went to the attacker. The same branch let a WRITER turn the captcha off
|
|
12
|
+
-- (`bot_protection_public`), rewrite payment and invoice settings, redirect the store,
|
|
13
|
+
-- form and moderation notification addresses, change `migration_baseline_generation`
|
|
14
|
+
-- (which 02000 trusts) and the Cortex agent settings. An unfiltered PATCH updated 13 rows.
|
|
15
|
+
-- Writes now fall into three tiers, and a key in none of the first two is in the third:
|
|
16
|
+
-- * WRITER tier (ADMIN or WRITER, first factor): exactly the keys staff code writes
|
|
17
|
+
-- through the cookie client — site identity (site_title, site_description,
|
|
18
|
+
-- site_keywords, site_social_image, active_logo_id: app/cms/settings/logos/actions.ts),
|
|
19
|
+
-- the footer (footer_copyright, footer_show_attribution: settings/copyright/actions.ts)
|
|
20
|
+
-- and onboarding_state (lib/onboarding/actions.ts, the dashboard's dismiss button).
|
|
21
|
+
-- * ADMIN tier (ADMIN, first factor): the two keys an ADMIN still writes through the
|
|
22
|
+
-- cookie client, enabled_payment_providers and ecommerce_inventory_settings
|
|
23
|
+
-- (libs/ecommerce payments, shipping, taxes). Moving those writes to the service role
|
|
24
|
+
-- lets them drop into the tier below (docs/04 lists it as a follow-up).
|
|
25
|
+
-- * everything else: ADMIN with a satisfied second factor (public.is_admin_mfa()). That
|
|
26
|
+
-- is the old deny-list plus email_public, bot_protection_public, payment_public,
|
|
27
|
+
-- invoice_settings, store_contact, forms_contact, interactions_notification_emails,
|
|
28
|
+
-- migration_baseline_generation, the cortex_ai_* settings and any key added later.
|
|
29
|
+
-- The app writes all of them with the service role after requireCmsAdmin() /
|
|
30
|
+
-- requireEcomAdmin(), Cortex too (its context is the service client); the three that
|
|
31
|
+
-- still used the cookie client move in the same change (logos/actions.ts
|
|
32
|
+
-- saveInvoiceSettings, cms/inquiries saveStoreContactEmail, the interactions
|
|
33
|
+
-- notification-address actions). The last two were already broken for an aal1 admin:
|
|
34
|
+
-- an upsert's ON CONFLICT path checks the read policy, and both keys sit above the
|
|
35
|
+
-- staff read tier. Reads are untouched (02021/02023 tiers).
|
|
36
|
+
--
|
|
37
|
+
-- 2. The email second factor counts in RLS (HIGH).
|
|
38
|
+
-- private.meets_aal_requirement() passed whenever the caller had no verified row in
|
|
39
|
+
-- auth.mfa_factors. The app's email factor never creates one (only TOTP does), so for an
|
|
40
|
+
-- email-2FA ADMIN every is_admin_mfa() policy was a plain role check: a stolen password and
|
|
41
|
+
-- an aal1 JWT reached mcp_access_tokens (a persistent MCP bearer), package_activations,
|
|
42
|
+
-- the secret tier of site_settings and other accounts' profiles. The no-factor branch now
|
|
43
|
+
-- also requires that the caller has no enrolled factor in public.user_security_settings
|
|
44
|
+
-- (`mfa_enabled AND mfa_type IS NOT NULL`, written only by the service role since 02026).
|
|
45
|
+
-- An email factor never yields an aal2 Supabase session, so for those admins every
|
|
46
|
+
-- is_admin_mfa() row is now reachable only through the service role, after the app's own
|
|
47
|
+
-- check (lib/auth/cms-actor.ts getCmsActor() refuses a session whose factor is pending).
|
|
48
|
+
-- A TOTP admin keeps aal2, and an admin who enrolled nothing is never locked out.
|
|
49
|
+
--
|
|
50
|
+
-- 3. ADMIN branches that skipped the second factor (MEDIUM).
|
|
51
|
+
-- site_scripts / site_script_revisions writes (scripts render with the CSP nonce on every
|
|
52
|
+
-- route, /cms included, so this was script injection into a real admin's session),
|
|
53
|
+
-- cms_redirects writes (an open redirect on any path), system_configuration (sign-up
|
|
54
|
+
-- policy) and the PII reads and updates on product_inquiries, message_threads and
|
|
55
|
+
-- thread_messages now use is_admin_mfa(). The CMS actions for all of them run on the
|
|
56
|
+
-- service role after requireCmsAdmin() (site scripts, redirects, registration/security,
|
|
57
|
+
-- messages); the public thread page and the contact paths already did. The public read
|
|
58
|
+
-- policies (active scripts, active redirects) are unchanged.
|
|
59
|
+
--
|
|
60
|
+
-- 4. media: uploader_id is pinned on insert, and storage keys in use are visible (MEDIUM).
|
|
61
|
+
-- media_insert_policy only checked the role, so a WRITER could insert a row attributed to
|
|
62
|
+
-- anyone; nothing pins object_key or variants either, and app/cms/media/actions.ts deleted
|
|
63
|
+
-- every variants[].objectKey of a deleted row with server credentials. A WRITER could
|
|
64
|
+
-- insert a decoy row listing other uploaders' keys and delete their files. Insert now pins
|
|
65
|
+
-- uploader_id to the caller (ADMIN keeps any). The delete and move actions keep only the
|
|
66
|
+
-- keys derived from the row's own object_key (lib/storage/media-object-keys.ts) and ask
|
|
67
|
+
-- public.media_storage_keys_in_use() which keys another row still references, so a shared
|
|
68
|
+
-- key is never deleted. The function is SECURITY INVOKER and service-role only: the app
|
|
69
|
+
-- calls it after its own role check, and the API roles have no use for it.
|
|
70
|
+
--
|
|
71
|
+
-- 5. package_activations: license keys are not readable over the API (MEDIUM, sandbox).
|
|
72
|
+
-- The public demo signs every visitor in as an ADMIN with no factor, so is_admin_mfa()
|
|
73
|
+
-- passes and "Staff read package activations" handed out the plaintext license key.
|
|
74
|
+
-- authenticated now holds SELECT on every column except license_key and meta (meta keeps
|
|
75
|
+
-- the Freemius activation response, install API token included). Every app read of the
|
|
76
|
+
-- table already uses the service role (packages page, dashboard, verifyPackageOnline,
|
|
77
|
+
-- revalidation, activation). Both apply paths repeat the column grant after their blanket
|
|
78
|
+
-- grants (API_ROLE_REVOKE_SQL), the same shape as profiles in 02025.
|
|
79
|
+
--
|
|
80
|
+
-- 6. media_read_policy stays USING (true) for anon (LOW, decision recorded).
|
|
81
|
+
-- The bucket is public-read and every rendered image carries its object URL, so a media
|
|
82
|
+
-- row discloses nothing the object URL does not, beyond its file name, description and
|
|
83
|
+
-- uploader id. Scoping it to "media referenced by live content" would have to follow every
|
|
84
|
+
-- reference (image blocks, section backgrounds, posts, products, logos, site_settings) and
|
|
85
|
+
-- would break images on any path it missed. docs/04 records the decision.
|
|
86
|
+
--
|
|
87
|
+
-- tools/scripts/security-verify.sql sections 30-34 check every item above, before and after.
|
|
88
|
+
|
|
89
|
+
-- ---------------------------------------------------------------------------
|
|
90
|
+
-- 1. site_settings: three write tiers, WRITER on an allow-list
|
|
91
|
+
-- ---------------------------------------------------------------------------
|
|
92
|
+
DROP POLICY IF EXISTS site_settings_insert_policy ON public.site_settings;
|
|
93
|
+
CREATE POLICY site_settings_insert_policy ON public.site_settings
|
|
94
|
+
FOR INSERT TO authenticated
|
|
95
|
+
WITH CHECK (
|
|
96
|
+
((key = ANY (ARRAY['site_title','site_description','site_keywords','site_social_image','active_logo_id','footer_copyright','footer_show_attribution','onboarding_state']::text[]))
|
|
97
|
+
AND (SELECT public.get_current_user_role()) = ANY (ARRAY['ADMIN'::public.user_role,'WRITER'::public.user_role]))
|
|
98
|
+
OR ((key = ANY (ARRAY['enabled_payment_providers','ecommerce_inventory_settings']::text[]))
|
|
99
|
+
AND (SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role)
|
|
100
|
+
OR ((SELECT public.is_admin_mfa()) IS TRUE)
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
DROP POLICY IF EXISTS site_settings_update_policy ON public.site_settings;
|
|
104
|
+
CREATE POLICY site_settings_update_policy ON public.site_settings
|
|
105
|
+
FOR UPDATE TO authenticated
|
|
106
|
+
USING (
|
|
107
|
+
((key = ANY (ARRAY['site_title','site_description','site_keywords','site_social_image','active_logo_id','footer_copyright','footer_show_attribution','onboarding_state']::text[]))
|
|
108
|
+
AND (SELECT public.get_current_user_role()) = ANY (ARRAY['ADMIN'::public.user_role,'WRITER'::public.user_role]))
|
|
109
|
+
OR ((key = ANY (ARRAY['enabled_payment_providers','ecommerce_inventory_settings']::text[]))
|
|
110
|
+
AND (SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role)
|
|
111
|
+
OR ((SELECT public.is_admin_mfa()) IS TRUE)
|
|
112
|
+
)
|
|
113
|
+
WITH CHECK (
|
|
114
|
+
((key = ANY (ARRAY['site_title','site_description','site_keywords','site_social_image','active_logo_id','footer_copyright','footer_show_attribution','onboarding_state']::text[]))
|
|
115
|
+
AND (SELECT public.get_current_user_role()) = ANY (ARRAY['ADMIN'::public.user_role,'WRITER'::public.user_role]))
|
|
116
|
+
OR ((key = ANY (ARRAY['enabled_payment_providers','ecommerce_inventory_settings']::text[]))
|
|
117
|
+
AND (SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role)
|
|
118
|
+
OR ((SELECT public.is_admin_mfa()) IS TRUE)
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
DROP POLICY IF EXISTS site_settings_delete_policy ON public.site_settings;
|
|
122
|
+
CREATE POLICY site_settings_delete_policy ON public.site_settings
|
|
123
|
+
FOR DELETE TO authenticated
|
|
124
|
+
USING (
|
|
125
|
+
((key = ANY (ARRAY['site_title','site_description','site_keywords','site_social_image','active_logo_id','footer_copyright','footer_show_attribution','onboarding_state']::text[]))
|
|
126
|
+
AND (SELECT public.get_current_user_role()) = ANY (ARRAY['ADMIN'::public.user_role,'WRITER'::public.user_role]))
|
|
127
|
+
OR ((key = ANY (ARRAY['enabled_payment_providers','ecommerce_inventory_settings']::text[]))
|
|
128
|
+
AND (SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role)
|
|
129
|
+
OR ((SELECT public.is_admin_mfa()) IS TRUE)
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
-- ---------------------------------------------------------------------------
|
|
133
|
+
-- 2. meets_aal_requirement(): an enrolled email factor counts as a factor
|
|
134
|
+
-- ---------------------------------------------------------------------------
|
|
135
|
+
-- True when the session is aal2, or when the caller enrolled no second factor at all: no
|
|
136
|
+
-- verified Supabase factor (TOTP) AND no enabled factor in user_security_settings (TOTP or the
|
|
137
|
+
-- app's email codes). Same signature, owner and grants as 02022; CREATE OR REPLACE keeps the
|
|
138
|
+
-- is_admin_mfa() wrapper and every policy that calls it.
|
|
139
|
+
CREATE OR REPLACE FUNCTION private.meets_aal_requirement()
|
|
140
|
+
RETURNS boolean
|
|
141
|
+
LANGUAGE sql STABLE SECURITY DEFINER
|
|
142
|
+
SET search_path TO ''
|
|
143
|
+
AS $$
|
|
144
|
+
SELECT COALESCE((auth.jwt() ->> 'aal'), 'aal1') = 'aal2'
|
|
145
|
+
OR (
|
|
146
|
+
NOT EXISTS (
|
|
147
|
+
SELECT 1 FROM auth.mfa_factors f
|
|
148
|
+
WHERE f.user_id = auth.uid() AND f.status = 'verified'
|
|
149
|
+
)
|
|
150
|
+
AND NOT EXISTS (
|
|
151
|
+
SELECT 1 FROM public.user_security_settings s
|
|
152
|
+
WHERE s.user_id = auth.uid() AND s.mfa_enabled AND s.mfa_type IS NOT NULL
|
|
153
|
+
)
|
|
154
|
+
);
|
|
155
|
+
$$;
|
|
156
|
+
REVOKE ALL ON FUNCTION private.meets_aal_requirement() FROM PUBLIC;
|
|
157
|
+
GRANT EXECUTE ON FUNCTION private.meets_aal_requirement() TO anon, authenticated, service_role;
|
|
158
|
+
|
|
159
|
+
-- ---------------------------------------------------------------------------
|
|
160
|
+
-- 3. Second factor on the remaining ADMIN branches
|
|
161
|
+
-- ---------------------------------------------------------------------------
|
|
162
|
+
-- site_scripts: INSERT / UPDATE / DELETE (the two read policies of 02013 are unchanged)
|
|
163
|
+
DROP POLICY IF EXISTS "Admins insert site scripts" ON public.site_scripts;
|
|
164
|
+
CREATE POLICY "Admins insert site scripts" ON public.site_scripts
|
|
165
|
+
FOR INSERT TO authenticated WITH CHECK (((SELECT public.is_admin_mfa()) IS TRUE));
|
|
166
|
+
DROP POLICY IF EXISTS "Admins update site scripts" ON public.site_scripts;
|
|
167
|
+
CREATE POLICY "Admins update site scripts" ON public.site_scripts
|
|
168
|
+
FOR UPDATE TO authenticated USING (((SELECT public.is_admin_mfa()) IS TRUE)) WITH CHECK (((SELECT public.is_admin_mfa()) IS TRUE));
|
|
169
|
+
DROP POLICY IF EXISTS "Admins delete site scripts" ON public.site_scripts;
|
|
170
|
+
CREATE POLICY "Admins delete site scripts" ON public.site_scripts
|
|
171
|
+
FOR DELETE TO authenticated USING (((SELECT public.is_admin_mfa()) IS TRUE));
|
|
172
|
+
|
|
173
|
+
-- site_script_revisions: INSERT (UPDATE / DELETE have no policy and no grant since 02026)
|
|
174
|
+
DROP POLICY IF EXISTS "Admins insert site script revisions" ON public.site_script_revisions;
|
|
175
|
+
CREATE POLICY "Admins insert site script revisions" ON public.site_script_revisions
|
|
176
|
+
FOR INSERT TO authenticated WITH CHECK (((SELECT public.is_admin_mfa()) IS TRUE));
|
|
177
|
+
|
|
178
|
+
-- cms_redirects: INSERT / UPDATE / DELETE (the two read policies of 02013 are unchanged)
|
|
179
|
+
DROP POLICY IF EXISTS "Admins insert redirects" ON public.cms_redirects;
|
|
180
|
+
CREATE POLICY "Admins insert redirects" ON public.cms_redirects
|
|
181
|
+
FOR INSERT TO authenticated WITH CHECK (((SELECT public.is_admin_mfa()) IS TRUE));
|
|
182
|
+
DROP POLICY IF EXISTS "Admins update redirects" ON public.cms_redirects;
|
|
183
|
+
CREATE POLICY "Admins update redirects" ON public.cms_redirects
|
|
184
|
+
FOR UPDATE TO authenticated USING (((SELECT public.is_admin_mfa()) IS TRUE)) WITH CHECK (((SELECT public.is_admin_mfa()) IS TRUE));
|
|
185
|
+
DROP POLICY IF EXISTS "Admins delete redirects" ON public.cms_redirects;
|
|
186
|
+
CREATE POLICY "Admins delete redirects" ON public.cms_redirects
|
|
187
|
+
FOR DELETE TO authenticated USING (((SELECT public.is_admin_mfa()) IS TRUE));
|
|
188
|
+
|
|
189
|
+
-- system_configuration: every command (the sign-up path reads it with the service role)
|
|
190
|
+
DROP POLICY IF EXISTS system_configuration_admin_select ON public.system_configuration;
|
|
191
|
+
CREATE POLICY system_configuration_admin_select ON public.system_configuration
|
|
192
|
+
FOR SELECT TO authenticated USING (((SELECT public.is_admin_mfa()) IS TRUE));
|
|
193
|
+
DROP POLICY IF EXISTS system_configuration_admin_insert ON public.system_configuration;
|
|
194
|
+
CREATE POLICY system_configuration_admin_insert ON public.system_configuration
|
|
195
|
+
FOR INSERT TO authenticated WITH CHECK (((SELECT public.is_admin_mfa()) IS TRUE));
|
|
196
|
+
DROP POLICY IF EXISTS system_configuration_admin_update ON public.system_configuration;
|
|
197
|
+
CREATE POLICY system_configuration_admin_update ON public.system_configuration
|
|
198
|
+
FOR UPDATE TO authenticated USING (((SELECT public.is_admin_mfa()) IS TRUE)) WITH CHECK (((SELECT public.is_admin_mfa()) IS TRUE));
|
|
199
|
+
DROP POLICY IF EXISTS system_configuration_admin_delete ON public.system_configuration;
|
|
200
|
+
CREATE POLICY system_configuration_admin_delete ON public.system_configuration
|
|
201
|
+
FOR DELETE TO authenticated USING (((SELECT public.is_admin_mfa()) IS TRUE));
|
|
202
|
+
|
|
203
|
+
-- product_inquiries / message_threads: ADMIN read and update (INSERT / DELETE revoked in 02026)
|
|
204
|
+
DROP POLICY IF EXISTS product_inquiries_admin_read_policy ON public.product_inquiries;
|
|
205
|
+
CREATE POLICY product_inquiries_admin_read_policy ON public.product_inquiries
|
|
206
|
+
FOR SELECT TO authenticated USING (((SELECT public.is_admin_mfa()) IS TRUE));
|
|
207
|
+
DROP POLICY IF EXISTS product_inquiries_admin_update_policy ON public.product_inquiries;
|
|
208
|
+
CREATE POLICY product_inquiries_admin_update_policy ON public.product_inquiries
|
|
209
|
+
FOR UPDATE TO authenticated USING (((SELECT public.is_admin_mfa()) IS TRUE)) WITH CHECK (((SELECT public.is_admin_mfa()) IS TRUE));
|
|
210
|
+
|
|
211
|
+
DROP POLICY IF EXISTS message_threads_admin_read_policy ON public.message_threads;
|
|
212
|
+
CREATE POLICY message_threads_admin_read_policy ON public.message_threads
|
|
213
|
+
FOR SELECT TO authenticated USING (((SELECT public.is_admin_mfa()) IS TRUE));
|
|
214
|
+
DROP POLICY IF EXISTS message_threads_admin_update_policy ON public.message_threads;
|
|
215
|
+
CREATE POLICY message_threads_admin_update_policy ON public.message_threads
|
|
216
|
+
FOR UPDATE TO authenticated USING (((SELECT public.is_admin_mfa()) IS TRUE)) WITH CHECK (((SELECT public.is_admin_mfa()) IS TRUE));
|
|
217
|
+
|
|
218
|
+
-- thread_messages: ADMIN read (every write is the service role's, 02026)
|
|
219
|
+
DROP POLICY IF EXISTS thread_messages_admin_read_policy ON public.thread_messages;
|
|
220
|
+
CREATE POLICY thread_messages_admin_read_policy ON public.thread_messages
|
|
221
|
+
FOR SELECT TO authenticated USING (((SELECT public.is_admin_mfa()) IS TRUE));
|
|
222
|
+
|
|
223
|
+
-- ---------------------------------------------------------------------------
|
|
224
|
+
-- 4. media: pin uploader_id on insert; which storage keys a row still references
|
|
225
|
+
-- ---------------------------------------------------------------------------
|
|
226
|
+
-- Same shape as 02022's update/delete policies: a WRITER inserts rows attributed to
|
|
227
|
+
-- themselves, an ADMIN may attribute a row to anyone (imports, restores).
|
|
228
|
+
DROP POLICY IF EXISTS media_insert_policy ON public.media;
|
|
229
|
+
CREATE POLICY media_insert_policy ON public.media
|
|
230
|
+
FOR INSERT TO authenticated
|
|
231
|
+
WITH CHECK (
|
|
232
|
+
(SELECT public.is_admin()) IS TRUE
|
|
233
|
+
OR (uploader_id = (SELECT auth.uid())
|
|
234
|
+
AND (SELECT public.get_current_user_role()) = ANY (ARRAY['ADMIN'::public.user_role,'WRITER'::public.user_role]))
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
-- The keys of p_keys that some media row other than p_exclude_id references as its object_key,
|
|
238
|
+
-- its file_path or a variants[].objectKey. The CMS delete and move actions call it (service
|
|
239
|
+
-- role) after removing or claiming their own rows, and keep every key it returns.
|
|
240
|
+
CREATE OR REPLACE FUNCTION public.media_storage_keys_in_use(p_keys text[], p_exclude_id uuid DEFAULT NULL)
|
|
241
|
+
RETURNS SETOF text
|
|
242
|
+
LANGUAGE sql STABLE SECURITY INVOKER
|
|
243
|
+
SET search_path TO ''
|
|
244
|
+
AS $$
|
|
245
|
+
SELECT DISTINCT k.key
|
|
246
|
+
FROM unnest(p_keys) AS k(key)
|
|
247
|
+
WHERE EXISTS (
|
|
248
|
+
SELECT 1
|
|
249
|
+
FROM public.media m
|
|
250
|
+
WHERE m.id IS DISTINCT FROM p_exclude_id
|
|
251
|
+
AND (m.object_key = k.key
|
|
252
|
+
OR m.file_path = k.key
|
|
253
|
+
OR m.variants @> jsonb_build_array(jsonb_build_object('objectKey', k.key)))
|
|
254
|
+
);
|
|
255
|
+
$$;
|
|
256
|
+
REVOKE ALL ON FUNCTION public.media_storage_keys_in_use(text[], uuid) FROM PUBLIC, anon, authenticated;
|
|
257
|
+
GRANT EXECUTE ON FUNCTION public.media_storage_keys_in_use(text[], uuid) TO service_role;
|
|
258
|
+
|
|
259
|
+
-- ---------------------------------------------------------------------------
|
|
260
|
+
-- 5. package_activations: no license_key / meta for authenticated
|
|
261
|
+
-- ---------------------------------------------------------------------------
|
|
262
|
+
-- Table-level SELECT goes (revoking it also drops any column grant), then SELECT comes back on
|
|
263
|
+
-- every column except the two secrets. The column list is read from the catalog so the
|
|
264
|
+
-- statement matches API_ROLE_REVOKE_SQL; a new column is readable by an aal2 ADMIN, add it to
|
|
265
|
+
-- the exclusions there and here if it is sensitive.
|
|
266
|
+
DO $nb_package_activations$
|
|
267
|
+
DECLARE
|
|
268
|
+
v_cols text;
|
|
269
|
+
BEGIN
|
|
270
|
+
SELECT string_agg(quote_ident(a.attname), ', ' ORDER BY a.attnum)
|
|
271
|
+
INTO v_cols
|
|
272
|
+
FROM pg_attribute a
|
|
273
|
+
WHERE a.attrelid = 'public.package_activations'::regclass AND a.attnum > 0 AND NOT a.attisdropped
|
|
274
|
+
AND a.attname NOT IN ('license_key', 'meta');
|
|
275
|
+
REVOKE SELECT ON TABLE public.package_activations FROM authenticated;
|
|
276
|
+
IF v_cols IS NOT NULL THEN
|
|
277
|
+
EXECUTE format('GRANT SELECT (%s) ON TABLE public.package_activations TO authenticated', v_cols);
|
|
278
|
+
END IF;
|
|
279
|
+
END
|
|
280
|
+
$nb_package_activations$;
|
|
281
|
+
|
|
282
|
+
-- ---------------------------------------------------------------------------
|
|
283
|
+
-- 6. media_read_policy: unchanged on purpose (see the header)
|
|
284
|
+
-- ---------------------------------------------------------------------------
|