From a281150f121eff1b9e02e009f0abf546f3b23c7b Mon Sep 17 00:00:00 2001 From: pavlov346346-source Date: Sat, 12 Sep 2026 14:16:16 +0300 Subject: [PATCH] docs: record the missing sun_employee_prepare/finalize_v28 migration sun_employee_prepare_v28 and sun_employee_finalize_v28 - the RPCs caterium-create-employee's whole authorization model rests on - were applied directly to the production database and were never committed, so the actual authorization logic wasn't auditable from the repo. Recorded verbatim via pg_get_functiondef() against the live database on 2026-09-12 (read-only; nothing was re-applied). Confirmed both match what the security audit inferred from the Edge Function's error-message handling: caller identity + workspace-owner/platform-admin check + plan/feature gates + member limits, all enforced here rather than in the Edge Function itself. Co-Authored-By: Claude Sonnet 5 --- ops/sql/SUPABASE-EMPLOYEE-MANAGEMENT-V28.sql | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 ops/sql/SUPABASE-EMPLOYEE-MANAGEMENT-V28.sql diff --git a/ops/sql/SUPABASE-EMPLOYEE-MANAGEMENT-V28.sql b/ops/sql/SUPABASE-EMPLOYEE-MANAGEMENT-V28.sql new file mode 100644 index 0000000..69e38a9 --- /dev/null +++ b/ops/sql/SUPABASE-EMPLOYEE-MANAGEMENT-V28.sql @@ -0,0 +1,18 @@ +-- Caterium v17.7.4-ish: employee invitation/creation RPCs backing the +-- caterium-create-employee Edge Function. +-- +-- These were applied directly to the production database and were missing +-- from the repository (found during the 2026-09-12 security/infra audit). +-- Recorded here for auditability, verbatim via pg_get_functiondef() against +-- the live database on 2026-09-12 — not re-applied as part of this commit. +-- +-- Authorization model: both are SECURITY DEFINER but re-check the caller's +-- own privileges internally (auth.uid(), caterium_is_workspace_owner / +-- sun_is_platform_admin, plan/feature gates, member limits) before doing +-- anything privileged - the Edge Function's service_role client only ever +-- calls auth.admin.createUser; workspace-membership authorization always +-- happens here, in Postgres, as the calling user. + +create or replace function public.sun_employee_prepare_v28(p_workspace uuid, p_email text, p_display_name text DEFAULT ''::text, p_role text DEFAULT 'manager'::text) RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public', 'auth' AS $function$ declare v_email text:=lower(trim(coalesce(p_email,''))); v_name text:=trim(coalesce(p_display_name,'')); v_role text:=lower(coalesce(p_role,'manager')); v_user uuid; v_member public.sun_workspace_members%rowtype; v_max integer; v_count integer; begin if auth.uid() is null then raise exception 'Сначала войдите в Caterium'; end if; if not public.caterium_is_workspace_owner(p_workspace) and not public.sun_is_platform_admin() then raise exception 'Только владелец компании может добавлять сотрудников'; end if; if v_email='' or v_email !~* '^[^[:space:]@]+@[^[:space:]@]+\.[^[:space:]@]+$' then raise exception 'Введите корректный email сотрудника'; end if; if v_role not in ('manager','kitchen','courier','viewer') then raise exception 'Сотруднику нельзя назначить роль владельца'; end if; if v_name='' then v_name:=split_part(v_email,'@',1); end if; if public.sun_workspace_access_mode_internal_v28(p_workspace)<>'full' then raise exception 'Подписка компании не позволяет добавлять сотрудников'; end if; if not public.sun_workspace_feature_internal_v28(p_workspace,'users_manage') then raise exception 'Добавление сотрудников недоступно на текущем тарифе'; end if; select p.max_members into v_max from public.sun_workspace_subscriptions s join public.sun_plans p on p.id=s.plan_id where s.workspace_id=p_workspace; select count(*)::int into v_count from public.sun_workspace_members where workspace_id=p_workspace and is_active=true; select u.id into v_user from auth.users u where lower(coalesce(u.email,''))=v_email order by u.created_at asc limit 1; if v_user is not null then select * into v_member from public.sun_workspace_members where workspace_id=p_workspace and user_id=v_user limit 1; if found and v_member.is_active then return jsonb_build_object('status','already_member','user_id',v_user,'email',v_email,'display_name',coalesce(nullif(v_member.display_name,''),v_name),'role',v_member.role); end if; end if; if v_max is not null and v_count>=v_max then raise exception 'Достигнут лимит пользователей тарифа (%)',v_max; end if; return jsonb_build_object('status',case when v_user is null then 'new' else 'existing' end,'user_id',v_user,'email',v_email,'display_name',v_name,'role',v_role,'max_members',v_max,'active_members',v_count); end; $function$; + +create or replace function public.sun_employee_finalize_v28(p_workspace uuid, p_user_id uuid, p_display_name text DEFAULT ''::text, p_role text DEFAULT 'manager'::text) RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public', 'auth' AS $function$ declare v_name text:=trim(coalesce(p_display_name,'')); v_role text:=lower(coalesce(p_role,'manager')); v_email text; v_max integer; v_count integer; v_already boolean:=false; begin if auth.uid() is null then raise exception 'Сначала войдите в Caterium'; end if; if not public.caterium_is_workspace_owner(p_workspace) and not public.sun_is_platform_admin() then raise exception 'Только владелец компании может добавлять сотрудников'; end if; if public.sun_workspace_access_mode_internal_v28(p_workspace)<>'full' then raise exception 'Подписка компании не позволяет добавлять сотрудников'; end if; if not public.sun_workspace_feature_internal_v28(p_workspace,'users_manage') then raise exception 'Добавление сотрудников недоступно на текущем тарифе'; end if; if v_role not in ('manager','kitchen','courier','viewer') then raise exception 'Сотруднику нельзя назначить роль владельца'; end if; select lower(email) into v_email from auth.users where id=p_user_id; if v_email is null then raise exception 'Аккаунт сотрудника не найден'; end if; if v_name='' then v_name:=split_part(v_email,'@',1); end if; select exists(select 1 from public.sun_workspace_members where workspace_id=p_workspace and user_id=p_user_id and is_active=true) into v_already; select p.max_members into v_max from public.sun_workspace_subscriptions s join public.sun_plans p on p.id=s.plan_id where s.workspace_id=p_workspace; select count(*)::int into v_count from public.sun_workspace_members where workspace_id=p_workspace and is_active=true; if not v_already and v_max is not null and v_count>=v_max then raise exception 'Достигнут лимит пользователей тарифа (%)',v_max; end if; insert into public.sun_workspace_members(workspace_id,user_id,role,display_name,is_active,permissions,updated_at) values(p_workspace,p_user_id,v_role,left(v_name,120),true,public.sun_role_default_permissions(v_role)||jsonb_build_object('users.manage',false),now()) on conflict(workspace_id,user_id) do update set role=excluded.role,display_name=excluded.display_name,is_active=true,permissions=excluded.permissions,updated_at=now(); delete from public.sun_workspace_invites where workspace_id=p_workspace and used_at is null and lower(coalesce(email,''))=v_email; return jsonb_build_object('status',case when v_already then 'updated' else 'added' end,'user_id',p_user_id,'email',v_email,'display_name',v_name,'role',v_role); end; $function$;