@selvajs/supabase-provider 0.20.0-beta.3 → 0.20.0-beta.5
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 +2 -0
- package/dist/auth/SupabaseAuthProvider.d.ts.map +1 -1
- package/dist/auth/SupabaseAuthProvider.js +2 -8
- package/dist/auth/SupabaseAuthProvider.js.map +1 -1
- package/dist/data/SupabaseAuditQuery.js +1 -1
- package/dist/data/SupabaseAuditQuery.js.map +1 -1
- package/dist/data/SupabaseComputeServerStore.d.ts +5 -4
- package/dist/data/SupabaseComputeServerStore.d.ts.map +1 -1
- package/dist/data/SupabaseComputeServerStore.js +53 -77
- package/dist/data/SupabaseComputeServerStore.js.map +1 -1
- package/dist/data/SupabaseDefinitionStore.js +7 -6
- package/dist/data/SupabaseDefinitionStore.js.map +1 -1
- package/dist/data/SupabaseInviteStore.d.ts +1 -1
- package/dist/data/SupabaseInviteStore.d.ts.map +1 -1
- package/dist/data/SupabaseInviteStore.js +3 -2
- package/dist/data/SupabaseInviteStore.js.map +1 -1
- package/dist/data/SupabaseOrgStore.d.ts.map +1 -1
- package/dist/data/SupabaseOrgStore.js +10 -34
- package/dist/data/SupabaseOrgStore.js.map +1 -1
- package/dist/data/SupabaseProjectStore.d.ts.map +1 -1
- package/dist/data/SupabaseProjectStore.js +15 -12
- package/dist/data/SupabaseProjectStore.js.map +1 -1
- package/dist/data/SupabaseShareLinkStore.d.ts.map +1 -1
- package/dist/data/SupabaseShareLinkStore.js +34 -29
- package/dist/data/SupabaseShareLinkStore.js.map +1 -1
- package/dist/data/SupabaseSolveMetricSink.d.ts +11 -3
- package/dist/data/SupabaseSolveMetricSink.d.ts.map +1 -1
- package/dist/data/SupabaseSolveMetricSink.js +38 -14
- package/dist/data/SupabaseSolveMetricSink.js.map +1 -1
- package/dist/data/errors.d.ts.map +1 -1
- package/dist/data/errors.js +3 -0
- package/dist/data/errors.js.map +1 -1
- package/dist/data/migrationHead.d.ts +1 -1
- package/dist/data/migrationHead.js +1 -1
- package/dist/data/pagination.d.ts +2 -3
- package/dist/data/pagination.d.ts.map +1 -1
- package/dist/data/pagination.js +6 -15
- package/dist/data/pagination.js.map +1 -1
- package/dist/permissions/SupabasePlatformPermissionStore.d.ts +2 -2
- package/dist/permissions/SupabasePlatformPermissionStore.d.ts.map +1 -1
- package/dist/permissions/SupabasePlatformPermissionStore.js +10 -49
- package/dist/permissions/SupabasePlatformPermissionStore.js.map +1 -1
- package/dist/storage/SupabaseStorageProvider.d.ts +3 -2
- package/dist/storage/SupabaseStorageProvider.d.ts.map +1 -1
- package/dist/storage/SupabaseStorageProvider.js +30 -19
- package/dist/storage/SupabaseStorageProvider.js.map +1 -1
- package/dist/userProfile/SupabaseUserProfileProvider.d.ts +7 -0
- package/dist/userProfile/SupabaseUserProfileProvider.d.ts.map +1 -1
- package/dist/userProfile/SupabaseUserProfileProvider.js +9 -19
- package/dist/userProfile/SupabaseUserProfileProvider.js.map +1 -1
- package/package.json +8 -7
- package/supabase/migrations/20261101152228_selva_revoke_privileged_functions.sql +44 -0
- package/supabase/migrations/20261101160000_selva_rls_is_the_boundary.sql +527 -0
- package/supabase/migrations/20261101170000_selva_compute_api_key_private.sql +14 -0
- package/supabase/migrations/20261101180000_selva_erasure_keeps_tenant_data.sql +28 -0
- package/supabase/migrations/20261101190000_selva_atomic_compute_save.sql +136 -0
- package/supabase/migrations/20261101200000_selva_last_admin_lock_order.sql +62 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- RLS is the boundary, and it matches `rules.ts`
|
|
3
|
+
--
|
|
4
|
+
-- The anon key is publishable, so anyone holding a session JWT can reach
|
|
5
|
+
-- PostgREST without going through Selva. Every rule the app enforces on a write
|
|
6
|
+
-- has to hold here on its own.
|
|
7
|
+
--
|
|
8
|
+
-- `public` still ignores ALLOW_CROSS_ORG_PUBLIC: Postgres cannot read the app's
|
|
9
|
+
-- env, so the app stays authoritative for that flag.
|
|
10
|
+
-- ============================================================================
|
|
11
|
+
|
|
12
|
+
-- ----------------------------------------------------------------------------
|
|
13
|
+
-- 1. Membership helpers stay inside a delegated session's token org
|
|
14
|
+
-- ----------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
-- A token for org A runs under its owner's JWT, so a helper that only looks at
|
|
17
|
+
-- `auth.uid()` also answers yes for every other org that user belongs to.
|
|
18
|
+
create or replace function selva.in_token_scope(o uuid)
|
|
19
|
+
returns boolean
|
|
20
|
+
language sql stable
|
|
21
|
+
as $$
|
|
22
|
+
select selva.delegated_org() is null or selva.delegated_org() = o;
|
|
23
|
+
$$;
|
|
24
|
+
|
|
25
|
+
create or replace function selva.is_org_member(o uuid)
|
|
26
|
+
returns boolean
|
|
27
|
+
language sql stable security definer set search_path = public
|
|
28
|
+
as $$
|
|
29
|
+
select selva.in_token_scope(o) and exists (
|
|
30
|
+
select 1 from selva.org_members
|
|
31
|
+
where org_id = o and user_id = auth.uid() and deleted_at is null
|
|
32
|
+
);
|
|
33
|
+
$$;
|
|
34
|
+
|
|
35
|
+
create or replace function selva.is_org_admin(o uuid)
|
|
36
|
+
returns boolean
|
|
37
|
+
language sql stable security definer set search_path = public
|
|
38
|
+
as $$
|
|
39
|
+
select selva.in_token_scope(o) and exists (
|
|
40
|
+
select 1 from selva.org_members
|
|
41
|
+
where org_id = o and user_id = auth.uid() and deleted_at is null
|
|
42
|
+
and role in ('owner', 'admin')
|
|
43
|
+
);
|
|
44
|
+
$$;
|
|
45
|
+
|
|
46
|
+
create or replace function selva.is_org_owner(o uuid)
|
|
47
|
+
returns boolean
|
|
48
|
+
language sql stable security definer set search_path = public
|
|
49
|
+
as $$
|
|
50
|
+
select selva.in_token_scope(o) and exists (
|
|
51
|
+
select 1 from selva.org_members
|
|
52
|
+
where org_id = o and user_id = auth.uid() and deleted_at is null
|
|
53
|
+
and role = 'owner'
|
|
54
|
+
);
|
|
55
|
+
$$;
|
|
56
|
+
|
|
57
|
+
create or replace function selva.has_org_permission(o uuid, perm text)
|
|
58
|
+
returns boolean
|
|
59
|
+
language sql stable security definer set search_path = public
|
|
60
|
+
as $$
|
|
61
|
+
select selva.is_instance_admin() or (selva.in_token_scope(o) and exists (
|
|
62
|
+
select 1 from selva.org_members
|
|
63
|
+
where org_id = o and user_id = auth.uid() and deleted_at is null
|
|
64
|
+
and perm = any(permissions)
|
|
65
|
+
));
|
|
66
|
+
$$;
|
|
67
|
+
|
|
68
|
+
create or replace function selva.is_project_member(p uuid)
|
|
69
|
+
returns boolean
|
|
70
|
+
language sql stable security definer set search_path = public
|
|
71
|
+
as $$
|
|
72
|
+
select exists (
|
|
73
|
+
select 1
|
|
74
|
+
from selva.project_members m
|
|
75
|
+
join selva.projects proj on proj.id = m.project_id
|
|
76
|
+
where m.project_id = p and m.user_id = auth.uid() and m.deleted_at is null
|
|
77
|
+
and selva.in_token_scope(proj.org_id)
|
|
78
|
+
);
|
|
79
|
+
$$;
|
|
80
|
+
|
|
81
|
+
create or replace function selva.is_project_owner(p uuid)
|
|
82
|
+
returns boolean
|
|
83
|
+
language sql stable security definer set search_path = public
|
|
84
|
+
as $$
|
|
85
|
+
select exists (
|
|
86
|
+
select 1
|
|
87
|
+
from selva.project_members m
|
|
88
|
+
join selva.projects proj on proj.id = m.project_id
|
|
89
|
+
where m.project_id = p and m.user_id = auth.uid() and m.deleted_at is null
|
|
90
|
+
and m.role = 'owner'
|
|
91
|
+
and selva.in_token_scope(proj.org_id)
|
|
92
|
+
);
|
|
93
|
+
$$;
|
|
94
|
+
|
|
95
|
+
create or replace function selva.is_project_editor(p uuid)
|
|
96
|
+
returns boolean
|
|
97
|
+
language sql stable security definer set search_path = public
|
|
98
|
+
as $$
|
|
99
|
+
select exists (
|
|
100
|
+
select 1
|
|
101
|
+
from selva.project_members m
|
|
102
|
+
join selva.projects proj on proj.id = m.project_id
|
|
103
|
+
where m.project_id = p and m.user_id = auth.uid() and m.deleted_at is null
|
|
104
|
+
and m.role in ('owner', 'editor')
|
|
105
|
+
and selva.in_token_scope(proj.org_id)
|
|
106
|
+
);
|
|
107
|
+
$$;
|
|
108
|
+
|
|
109
|
+
-- A creator who left the org keeps `owner_id`, which must not bring them back.
|
|
110
|
+
create or replace function selva.may_seed_project_owner(p uuid)
|
|
111
|
+
returns boolean
|
|
112
|
+
language sql stable security definer set search_path = public
|
|
113
|
+
as $$
|
|
114
|
+
select exists (
|
|
115
|
+
select 1 from selva.projects proj
|
|
116
|
+
where proj.id = p
|
|
117
|
+
and proj.deleted_at is null
|
|
118
|
+
and selva.is_org_member(proj.org_id)
|
|
119
|
+
and (proj.owner_id = auth.uid() or selva.is_org_admin(proj.org_id))
|
|
120
|
+
);
|
|
121
|
+
$$;
|
|
122
|
+
|
|
123
|
+
-- ----------------------------------------------------------------------------
|
|
124
|
+
-- 2. Commons authority follows `canEditDefinition`
|
|
125
|
+
-- ----------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
-- `ownerId` is stamped at upload and never revisited, so it only counts for as
|
|
128
|
+
-- long as the uploader belongs to the org.
|
|
129
|
+
create or replace function selva.is_commons_contributor(p uuid, definition_owner uuid)
|
|
130
|
+
returns boolean
|
|
131
|
+
language sql stable security definer set search_path = public
|
|
132
|
+
as $$
|
|
133
|
+
select definition_owner = auth.uid() and exists (
|
|
134
|
+
select 1 from selva.projects proj
|
|
135
|
+
where proj.id = p
|
|
136
|
+
and proj.deleted_at is null
|
|
137
|
+
and proj.auto_join_on_upload
|
|
138
|
+
and selva.is_org_member(proj.org_id)
|
|
139
|
+
);
|
|
140
|
+
$$;
|
|
141
|
+
|
|
142
|
+
-- An upload is several writes, and an outsider may make them on a public commons
|
|
143
|
+
-- without ever being a contributor, so this also carries the insert and the
|
|
144
|
+
-- first version.
|
|
145
|
+
create or replace function selva.may_upload_to_commons(p uuid, definition_owner uuid)
|
|
146
|
+
returns boolean
|
|
147
|
+
language sql stable security definer set search_path = public
|
|
148
|
+
as $$
|
|
149
|
+
select definition_owner = auth.uid() and selva.visible_project(p) and exists (
|
|
150
|
+
select 1 from selva.projects proj
|
|
151
|
+
where proj.id = p
|
|
152
|
+
and proj.deleted_at is null
|
|
153
|
+
and proj.auto_join_on_upload
|
|
154
|
+
and selva.in_token_scope(proj.org_id)
|
|
155
|
+
);
|
|
156
|
+
$$;
|
|
157
|
+
|
|
158
|
+
create or replace function selva.is_definition_editor(g uuid)
|
|
159
|
+
returns boolean
|
|
160
|
+
language sql stable security definer set search_path = public
|
|
161
|
+
as $$
|
|
162
|
+
select exists (
|
|
163
|
+
select 1 from selva.definitions d
|
|
164
|
+
where d.guid = g and d.deleted_at is null and selva.is_project_editor(d.project_id)
|
|
165
|
+
);
|
|
166
|
+
$$;
|
|
167
|
+
|
|
168
|
+
create or replace function selva.is_definition_contributor(g uuid)
|
|
169
|
+
returns boolean
|
|
170
|
+
language sql stable security definer set search_path = public
|
|
171
|
+
as $$
|
|
172
|
+
select exists (
|
|
173
|
+
select 1 from selva.definitions d
|
|
174
|
+
where d.guid = g and d.deleted_at is null
|
|
175
|
+
and selva.is_commons_contributor(d.project_id, d.owner_id)
|
|
176
|
+
);
|
|
177
|
+
$$;
|
|
178
|
+
|
|
179
|
+
-- The window closes when a version is attached.
|
|
180
|
+
create or replace function selva.is_unfinished_commons_upload(g uuid)
|
|
181
|
+
returns boolean
|
|
182
|
+
language sql stable security definer set search_path = public
|
|
183
|
+
as $$
|
|
184
|
+
select exists (
|
|
185
|
+
select 1 from selva.definitions d
|
|
186
|
+
where d.guid = g and d.deleted_at is null
|
|
187
|
+
and d.live_version_id is null and d.draft_version_id is null
|
|
188
|
+
and selva.may_upload_to_commons(d.project_id, d.owner_id)
|
|
189
|
+
);
|
|
190
|
+
$$;
|
|
191
|
+
|
|
192
|
+
drop policy if exists "definitions: editors can insert" on selva.definitions;
|
|
193
|
+
create policy "definitions: editors can insert"
|
|
194
|
+
on selva.definitions for insert
|
|
195
|
+
to authenticated
|
|
196
|
+
with check (
|
|
197
|
+
selva.is_instance_admin()
|
|
198
|
+
or selva.is_project_editor(project_id)
|
|
199
|
+
or selva.may_upload_to_commons(project_id, owner_id)
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
drop policy if exists "definitions: editors can update" on selva.definitions;
|
|
203
|
+
create policy "definitions: editors can update"
|
|
204
|
+
on selva.definitions for update
|
|
205
|
+
to authenticated
|
|
206
|
+
using (
|
|
207
|
+
selva.is_instance_admin()
|
|
208
|
+
or selva.is_project_editor(project_id)
|
|
209
|
+
or selva.is_commons_contributor(project_id, owner_id)
|
|
210
|
+
or selva.is_unfinished_commons_upload(guid)
|
|
211
|
+
)
|
|
212
|
+
with check (
|
|
213
|
+
selva.is_instance_admin()
|
|
214
|
+
or selva.is_project_editor(project_id)
|
|
215
|
+
or selva.is_commons_contributor(project_id, owner_id)
|
|
216
|
+
or selva.may_upload_to_commons(project_id, owner_id)
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
drop policy if exists "definitions: editors can delete" on selva.definitions;
|
|
220
|
+
create policy "definitions: editors can delete"
|
|
221
|
+
on selva.definitions for delete
|
|
222
|
+
to authenticated
|
|
223
|
+
using (
|
|
224
|
+
selva.is_instance_admin()
|
|
225
|
+
or selva.is_project_editor(project_id)
|
|
226
|
+
or selva.is_commons_contributor(project_id, owner_id)
|
|
227
|
+
or selva.is_unfinished_commons_upload(guid)
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
drop policy if exists "definition_versions: editors can insert" on selva.definition_versions;
|
|
231
|
+
create policy "definition_versions: editors can insert"
|
|
232
|
+
on selva.definition_versions for insert
|
|
233
|
+
to authenticated
|
|
234
|
+
with check (
|
|
235
|
+
selva.is_instance_admin()
|
|
236
|
+
or selva.is_definition_editor(definition_guid)
|
|
237
|
+
or (
|
|
238
|
+
uploaded_by = auth.uid()
|
|
239
|
+
and (
|
|
240
|
+
selva.is_definition_contributor(definition_guid)
|
|
241
|
+
or selva.is_unfinished_commons_upload(definition_guid)
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
drop policy if exists "definition_versions: editors can update" on selva.definition_versions;
|
|
247
|
+
create policy "definition_versions: editors can update"
|
|
248
|
+
on selva.definition_versions for update
|
|
249
|
+
to authenticated
|
|
250
|
+
using (selva.is_instance_admin() or selva.is_definition_editor(definition_guid))
|
|
251
|
+
with check (selva.is_instance_admin() or selva.is_definition_editor(definition_guid));
|
|
252
|
+
|
|
253
|
+
drop policy if exists "definition_versions: editors can delete" on selva.definition_versions;
|
|
254
|
+
create policy "definition_versions: editors can delete"
|
|
255
|
+
on selva.definition_versions for delete
|
|
256
|
+
to authenticated
|
|
257
|
+
using (
|
|
258
|
+
selva.is_instance_admin()
|
|
259
|
+
or selva.is_definition_editor(definition_guid)
|
|
260
|
+
or selva.is_definition_contributor(definition_guid)
|
|
261
|
+
or selva.is_unfinished_commons_upload(definition_guid)
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
drop policy if exists "share_links: editors can read" on selva.share_links;
|
|
265
|
+
create policy "share_links: editors can read"
|
|
266
|
+
on selva.share_links for select
|
|
267
|
+
to authenticated
|
|
268
|
+
using (
|
|
269
|
+
selva.is_instance_admin()
|
|
270
|
+
or selva.is_definition_editor(definition_guid)
|
|
271
|
+
or selva.is_definition_contributor(definition_guid)
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
drop policy if exists "share_links: editors can insert" on selva.share_links;
|
|
275
|
+
create policy "share_links: editors can insert"
|
|
276
|
+
on selva.share_links for insert
|
|
277
|
+
to authenticated
|
|
278
|
+
with check (
|
|
279
|
+
selva.is_instance_admin()
|
|
280
|
+
or selva.is_definition_editor(definition_guid)
|
|
281
|
+
or selva.is_definition_contributor(definition_guid)
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
drop policy if exists "share_links: editors can revoke" on selva.share_links;
|
|
285
|
+
create policy "share_links: editors can revoke"
|
|
286
|
+
on selva.share_links for update
|
|
287
|
+
to authenticated
|
|
288
|
+
using (
|
|
289
|
+
selva.is_instance_admin()
|
|
290
|
+
or selva.is_definition_editor(definition_guid)
|
|
291
|
+
or selva.is_definition_contributor(definition_guid)
|
|
292
|
+
)
|
|
293
|
+
with check (
|
|
294
|
+
selva.is_instance_admin()
|
|
295
|
+
or selva.is_definition_editor(definition_guid)
|
|
296
|
+
or selva.is_definition_contributor(definition_guid)
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
-- ----------------------------------------------------------------------------
|
|
300
|
+
-- 3. Org roles follow `canChangeOrgRole`
|
|
301
|
+
-- ----------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
-- `member` is the only standing an admin may hand out or take away, on the row
|
|
304
|
+
-- as it stands and on the row as it ends up.
|
|
305
|
+
create or replace function selva.may_set_org_role(o uuid, r text)
|
|
306
|
+
returns boolean
|
|
307
|
+
language sql stable security definer set search_path = public
|
|
308
|
+
as $$
|
|
309
|
+
select selva.is_instance_admin()
|
|
310
|
+
or selva.is_org_owner(o)
|
|
311
|
+
or (r = 'member' and selva.is_org_admin(o));
|
|
312
|
+
$$;
|
|
313
|
+
|
|
314
|
+
drop policy if exists "org_members: admins can insert" on selva.org_members;
|
|
315
|
+
create policy "org_members: admins can insert"
|
|
316
|
+
on selva.org_members for insert
|
|
317
|
+
to authenticated
|
|
318
|
+
with check (selva.may_set_org_role(org_id, role));
|
|
319
|
+
|
|
320
|
+
drop policy if exists "org_members: admins can update" on selva.org_members;
|
|
321
|
+
create policy "org_members: admins can update"
|
|
322
|
+
on selva.org_members for update
|
|
323
|
+
to authenticated
|
|
324
|
+
using (selva.may_set_org_role(org_id, role))
|
|
325
|
+
with check (selva.may_set_org_role(org_id, role));
|
|
326
|
+
|
|
327
|
+
drop policy if exists "org_members: admins can delete" on selva.org_members;
|
|
328
|
+
create policy "org_members: admins can delete"
|
|
329
|
+
on selva.org_members for delete
|
|
330
|
+
to authenticated
|
|
331
|
+
using (selva.may_set_org_role(org_id, role));
|
|
332
|
+
|
|
333
|
+
drop policy if exists "orgs: authenticated can create their own" on selva.orgs;
|
|
334
|
+
create policy "orgs: authenticated can create their own"
|
|
335
|
+
on selva.orgs for insert
|
|
336
|
+
to authenticated
|
|
337
|
+
with check (
|
|
338
|
+
selva.is_instance_admin()
|
|
339
|
+
or (owner_id = auth.uid() and selva.delegated_org() is null)
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
-- ----------------------------------------------------------------------------
|
|
343
|
+
-- 4. Invites carry authority, so they get the same checks as granting it
|
|
344
|
+
-- ----------------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
-- accept-invite applies a row's role and platform permissions through the
|
|
347
|
+
-- service role, so whoever can shape the row picks what the invitee receives.
|
|
348
|
+
create or replace function selva.may_issue_invite(o uuid, r text, platform text[])
|
|
349
|
+
returns boolean
|
|
350
|
+
language sql stable security definer set search_path = public
|
|
351
|
+
as $$
|
|
352
|
+
select selva.has_org_permission(o, 'manage_org_members')
|
|
353
|
+
and selva.may_set_org_role(o, r)
|
|
354
|
+
and (cardinality(platform) = 0 or selva.is_instance_admin());
|
|
355
|
+
$$;
|
|
356
|
+
|
|
357
|
+
drop policy if exists "invites: manage_org_members can insert" on selva.invites;
|
|
358
|
+
create policy "invites: manage_org_members can insert"
|
|
359
|
+
on selva.invites for insert
|
|
360
|
+
to authenticated
|
|
361
|
+
with check (selva.may_issue_invite(org_id, org_role, platform_permissions));
|
|
362
|
+
|
|
363
|
+
drop policy if exists "invites: manage_org_members can delete" on selva.invites;
|
|
364
|
+
create policy "invites: manage_org_members can delete"
|
|
365
|
+
on selva.invites for delete
|
|
366
|
+
to authenticated
|
|
367
|
+
using (selva.may_issue_invite(org_id, org_role, platform_permissions));
|
|
368
|
+
|
|
369
|
+
-- A pending invite is immutable apart from being accepted.
|
|
370
|
+
revoke update on selva.invites from anon, authenticated;
|
|
371
|
+
grant update (accepted_at, accepted_by_user_id) on selva.invites to authenticated;
|
|
372
|
+
|
|
373
|
+
-- ----------------------------------------------------------------------------
|
|
374
|
+
-- 5. Profiles and tokens
|
|
375
|
+
-- ----------------------------------------------------------------------------
|
|
376
|
+
|
|
377
|
+
create or replace function selva.shares_org_with(u uuid)
|
|
378
|
+
returns boolean
|
|
379
|
+
language sql stable security definer set search_path = public
|
|
380
|
+
as $$
|
|
381
|
+
select exists (
|
|
382
|
+
select 1
|
|
383
|
+
from selva.org_members mine
|
|
384
|
+
join selva.org_members theirs on theirs.org_id = mine.org_id
|
|
385
|
+
where mine.user_id = auth.uid() and mine.deleted_at is null
|
|
386
|
+
and theirs.user_id = u and theirs.deleted_at is null
|
|
387
|
+
and selva.in_token_scope(mine.org_id)
|
|
388
|
+
);
|
|
389
|
+
$$;
|
|
390
|
+
|
|
391
|
+
-- The app reads other users' profiles with the service role, so this only limits
|
|
392
|
+
-- direct REST.
|
|
393
|
+
drop policy if exists "user_profiles: any authenticated can read" on selva.user_profiles;
|
|
394
|
+
create policy "user_profiles: self and shared orgs can read"
|
|
395
|
+
on selva.user_profiles for select
|
|
396
|
+
to authenticated
|
|
397
|
+
using (user_id = auth.uid() or selva.is_instance_admin() or selva.shares_org_with(user_id));
|
|
398
|
+
|
|
399
|
+
drop policy if exists "api_tokens: owner or org leadership can revoke" on selva.api_tokens;
|
|
400
|
+
create policy "api_tokens: owner or org leadership can revoke"
|
|
401
|
+
on selva.api_tokens for update
|
|
402
|
+
to authenticated
|
|
403
|
+
using (
|
|
404
|
+
(user_id = auth.uid() and selva.in_token_scope(org_id))
|
|
405
|
+
or selva.has_org_permission(org_id, 'manage_org_members')
|
|
406
|
+
)
|
|
407
|
+
with check (
|
|
408
|
+
revoked_at is not null
|
|
409
|
+
and (
|
|
410
|
+
(user_id = auth.uid() and selva.in_token_scope(org_id))
|
|
411
|
+
or selva.has_org_permission(org_id, 'manage_org_members')
|
|
412
|
+
)
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
-- ----------------------------------------------------------------------------
|
|
416
|
+
-- 6. Removing an org member reaches rows the acting admin cannot see
|
|
417
|
+
-- ----------------------------------------------------------------------------
|
|
418
|
+
|
|
419
|
+
-- RLS hides private projects from an org admin who is not in them, so a cascade
|
|
420
|
+
-- run as that admin would leave the leaver's project access intact and report
|
|
421
|
+
-- success. Definer rights reach those rows, and one transaction ends both
|
|
422
|
+
-- memberships together.
|
|
423
|
+
create or replace function selva.remove_org_member(o uuid, u uuid)
|
|
424
|
+
returns void
|
|
425
|
+
language plpgsql security definer set search_path = public
|
|
426
|
+
as $$
|
|
427
|
+
declare
|
|
428
|
+
target_role text;
|
|
429
|
+
begin
|
|
430
|
+
select role into target_role
|
|
431
|
+
from selva.org_members
|
|
432
|
+
where org_id = o and user_id = u and deleted_at is null
|
|
433
|
+
for update;
|
|
434
|
+
if not found then
|
|
435
|
+
raise exception 'Org member not found' using errcode = 'P0002';
|
|
436
|
+
end if;
|
|
437
|
+
|
|
438
|
+
if auth.jwt() ->> 'role' is distinct from 'service_role'
|
|
439
|
+
and not selva.may_set_org_role(o, target_role) then
|
|
440
|
+
raise exception 'No permission to remove this member' using errcode = '42501';
|
|
441
|
+
end if;
|
|
442
|
+
|
|
443
|
+
update selva.project_members m
|
|
444
|
+
set deleted_at = now(), updated_at = now(), updated_by = coalesce(auth.uid(), m.updated_by)
|
|
445
|
+
from selva.projects p
|
|
446
|
+
where p.id = m.project_id and p.org_id = o and m.user_id = u and m.deleted_at is null;
|
|
447
|
+
|
|
448
|
+
update selva.org_members m
|
|
449
|
+
set deleted_at = now(), updated_at = now(), updated_by = coalesce(auth.uid(), m.updated_by)
|
|
450
|
+
where m.org_id = o and m.user_id = u and m.deleted_at is null;
|
|
451
|
+
end;
|
|
452
|
+
$$;
|
|
453
|
+
|
|
454
|
+
revoke all on function selva.remove_org_member(uuid, uuid) from public, anon;
|
|
455
|
+
grant execute on function selva.remove_org_member(uuid, uuid) to authenticated, service_role;
|
|
456
|
+
|
|
457
|
+
-- ----------------------------------------------------------------------------
|
|
458
|
+
-- 7. The share-link roster reaches every project in the org
|
|
459
|
+
-- ----------------------------------------------------------------------------
|
|
460
|
+
|
|
461
|
+
create or replace function selva.leads_definition_org(g uuid)
|
|
462
|
+
returns boolean
|
|
463
|
+
language sql stable security definer set search_path = public
|
|
464
|
+
as $$
|
|
465
|
+
select exists (
|
|
466
|
+
select 1
|
|
467
|
+
from selva.definitions d
|
|
468
|
+
join selva.projects p on p.id = d.project_id
|
|
469
|
+
where d.guid = g and d.deleted_at is null and p.deleted_at is null
|
|
470
|
+
and selva.has_org_permission(p.org_id, 'manage_org_members')
|
|
471
|
+
);
|
|
472
|
+
$$;
|
|
473
|
+
|
|
474
|
+
drop policy if exists "share_links: org leadership can read the roster" on selva.share_links;
|
|
475
|
+
create policy "share_links: org leadership can read the roster"
|
|
476
|
+
on selva.share_links for select
|
|
477
|
+
to authenticated
|
|
478
|
+
using (selva.leads_definition_org(definition_guid));
|
|
479
|
+
|
|
480
|
+
-- A definer function, not a policy: the roster embeds `definitions` and
|
|
481
|
+
-- `projects`, whose own RLS hides private projects from the leadership reading
|
|
482
|
+
-- it, and those are the links offboarding has to find.
|
|
483
|
+
create or replace function selva.org_share_link_roster(o uuid, page_limit integer, page_offset integer)
|
|
484
|
+
returns table (
|
|
485
|
+
id uuid,
|
|
486
|
+
definition_guid uuid,
|
|
487
|
+
channel text,
|
|
488
|
+
token_hash text,
|
|
489
|
+
name text,
|
|
490
|
+
created_by uuid,
|
|
491
|
+
created_at timestamptz,
|
|
492
|
+
expires_at timestamptz,
|
|
493
|
+
revoked_at timestamptz,
|
|
494
|
+
allow_solve boolean,
|
|
495
|
+
max_solves integer,
|
|
496
|
+
solve_count integer,
|
|
497
|
+
definition_name text,
|
|
498
|
+
project_id uuid,
|
|
499
|
+
project_name text,
|
|
500
|
+
total_count bigint
|
|
501
|
+
)
|
|
502
|
+
language plpgsql stable security definer set search_path = public
|
|
503
|
+
as $$
|
|
504
|
+
begin
|
|
505
|
+
if auth.jwt() ->> 'role' is distinct from 'service_role'
|
|
506
|
+
and not selva.has_org_permission(o, 'manage_org_members') then
|
|
507
|
+
raise exception 'No permission to read this roster' using errcode = '42501';
|
|
508
|
+
end if;
|
|
509
|
+
|
|
510
|
+
return query
|
|
511
|
+
select l.id, l.definition_guid, l.channel, l.token_hash, l.name, l.created_by, l.created_at,
|
|
512
|
+
l.expires_at, l.revoked_at, l.allow_solve, l.max_solves, l.solve_count,
|
|
513
|
+
d.display_name, p.id, p.name, count(*) over ()
|
|
514
|
+
from selva.share_links l
|
|
515
|
+
join selva.definitions d on d.guid = l.definition_guid and d.deleted_at is null
|
|
516
|
+
join selva.projects p on p.id = d.project_id and p.deleted_at is null
|
|
517
|
+
where p.org_id = o
|
|
518
|
+
and l.revoked_at is null
|
|
519
|
+
and (l.expires_at is null or l.expires_at > now())
|
|
520
|
+
order by l.created_at desc
|
|
521
|
+
limit page_limit offset page_offset;
|
|
522
|
+
end;
|
|
523
|
+
$$;
|
|
524
|
+
|
|
525
|
+
revoke all on function selva.org_share_link_roster(uuid, integer, integer) from public, anon;
|
|
526
|
+
grant execute on function selva.org_share_link_roster(uuid, integer, integer)
|
|
527
|
+
to authenticated, service_role;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- Sessions cannot read compute_servers.api_key
|
|
3
|
+
--
|
|
4
|
+
-- RLS is per row, so anyone who could see a shared platform server could also
|
|
5
|
+
-- select its ciphertext and try to replay it. The store reads keys with the
|
|
6
|
+
-- service role, after a session read has said which servers the caller sees.
|
|
7
|
+
-- ============================================================================
|
|
8
|
+
|
|
9
|
+
revoke select on selva.compute_servers from anon, authenticated;
|
|
10
|
+
|
|
11
|
+
grant select (
|
|
12
|
+
id, scope, owner_org_id, shared_with_all, label, server_url,
|
|
13
|
+
timeout_ms, retry_count, created_at, has_api_key
|
|
14
|
+
) on selva.compute_servers to authenticated;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- Erasing a user removes the person, not the tenant data they created
|
|
3
|
+
--
|
|
4
|
+
-- `owner_id` and `uploaded_by` record who created a row. Cascading on them took
|
|
5
|
+
-- whole orgs with a founder long gone, and a live version's restrict FK refused
|
|
6
|
+
-- to erase its uploader. Authority comes from membership rows, which still
|
|
7
|
+
-- cascade, so these columns go null and the stores read that as the erased actor.
|
|
8
|
+
-- ============================================================================
|
|
9
|
+
|
|
10
|
+
alter table selva.orgs alter column owner_id drop not null;
|
|
11
|
+
alter table selva.orgs drop constraint if exists orgs_owner_id_fkey;
|
|
12
|
+
alter table selva.orgs add constraint orgs_owner_id_fkey
|
|
13
|
+
foreign key (owner_id) references auth.users(id) on delete set null;
|
|
14
|
+
|
|
15
|
+
alter table selva.projects alter column owner_id drop not null;
|
|
16
|
+
alter table selva.projects drop constraint if exists projects_owner_id_fkey;
|
|
17
|
+
alter table selva.projects add constraint projects_owner_id_fkey
|
|
18
|
+
foreign key (owner_id) references auth.users(id) on delete set null;
|
|
19
|
+
|
|
20
|
+
alter table selva.definitions alter column owner_id drop not null;
|
|
21
|
+
alter table selva.definitions drop constraint if exists definitions_owner_id_fkey;
|
|
22
|
+
alter table selva.definitions add constraint definitions_owner_id_fkey
|
|
23
|
+
foreign key (owner_id) references auth.users(id) on delete set null;
|
|
24
|
+
|
|
25
|
+
alter table selva.definition_versions alter column uploaded_by drop not null;
|
|
26
|
+
alter table selva.definition_versions drop constraint if exists definition_versions_uploaded_by_fkey;
|
|
27
|
+
alter table selva.definition_versions add constraint definition_versions_uploaded_by_fkey
|
|
28
|
+
foreign key (uploaded_by) references auth.users(id) on delete set null;
|