notifications(P4f): audit fixes, lock down 047 + best-effort enqueue paths

Two findings from an ultrathink audit of the Phase 4 work.

(1) members_with_permission was callable by any PostgREST client. Postgres
grants EXECUTE on new functions to PUBLIC by default, and this codebase
has no existing REVOKE pattern. The function returns a SET of member_ids
for any (space, permission) pair, so without a lockdown ANY authenticated
caller could enumerate "who has permission X in space Y" for spaces they
are not even members of, learning member_ids and the permission
distribution. user_has_permission has a narrower exposure (per-uid
boolean) and is needed by anon/authenticated for RLS-policy evaluation,
so it stays callable; the new function is locked to service_role since
the only legitimate caller (the form-submission fan-out) goes through
the admin client. Migration 047 + schema.sql now REVOKE EXECUTE FROM
PUBLIC and GRANT TO service_role.

(2) The Phase 4 enqueue blocks in equipment.ts (reserve, cancel) and
classes.ts (signUp, cancelMySignup waitlist promotion, updateSession
session-cancellation fan-out) used the shared helpers, but the helpers
themselves were not internally best-effort. The Stripe webhook's
original enqueueDues wrapped its entire body in try/catch so a thrown
member-lookup error could never bubble into the money path; my refactor
removed that inner wrap and relied on enqueueNotification's
best-effort. resolveMemberContact and getSpaceName could still throw on
a transient supabase network error, which would surface to the action
return value after the underlying mutation already committed: the
client would see an error even though their booking, signup, or
session-cancellation succeeded. Fixed at both layers: the helpers now
swallow throws internally (returning null / empty), and each call site
in equipment/classes is wrapped in try/catch around the lookup chain so
secondary action-specific queries (equipment name+location lookup,
promoted-signup session lookup, session-cancellation signup fan-out)
also cannot leak. forms.submitForm already had the wrap from P4d.

2 new helper tests pin the best-effort behavior; suite 567, build
clean.
This commit is contained in:
Moheeb Zara
2026-05-19 16:12:03 -07:00
parent 6abac53380
commit bd8f22a3ff
6 changed files with 260 additions and 181 deletions
+8
View File
@@ -1775,6 +1775,12 @@ $$;
-- user_effective_roles fallback. Used by notification fan-outs (e.g. the
-- forms.manage admin alert) where N user_has_permission calls would be N
-- round-trips. Equivalent to scripts/047_members_with_permission.sql.
-- LOCKDOWN: unlike user_has_permission (a per-user boolean that is hard to
-- enumerate without already knowing target uids), this function returns a SET
-- of member_ids for any (space, permission) pair. Postgres grants EXECUTE on
-- new functions to PUBLIC by default, so an explicit REVOKE is required to
-- prevent any authenticated PostgREST caller from enumerating permission
-- holders across spaces they are not members of. Locked to service_role.
CREATE OR REPLACE FUNCTION public.members_with_permission(sid uuid, perm text)
RETURNS TABLE(member_id uuid) LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public
AS $$
@@ -1791,6 +1797,8 @@ AS $$
)
);
$$;
REVOKE EXECUTE ON FUNCTION public.members_with_permission(uuid, text) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.members_with_permission(uuid, text) TO service_role;
ALTER TABLE public.space_role_permissions ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ops_acl ENABLE ROW LEVEL SECURITY;