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 <noreply@anthropic.com>
This commit is contained in:
pavlov346346-source 2026-09-12 14:16:16 +03:00
parent aeca9eae99
commit a281150f12

View File

@ -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$;