operator-accounts.sql 7.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
  1. -- Role/status changes recheck the actor under a company lock. Two managers
  2. -- cannot concurrently disable/demote each other and leave no manager behind.
  3. CREATE OR REPLACE FUNCTION xs_operator_role(u jsonb) RETURNS text LANGUAGE sql IMMUTABLE AS $$
  4. SELECT CASE
  5. WHEN u->>'adminRoleKey' IN ('super-admin','ops-manager','ops-staff','ops-auditor') THEN u->>'adminRoleKey'
  6. WHEN (CASE WHEN jsonb_typeof(u->'roles')='array' THEN u->'roles' ELSE '[]'::jsonb END) ? 'super-admin' THEN 'super-admin'
  7. WHEN (CASE WHEN jsonb_typeof(u->'roles')='array' THEN u->'roles' ELSE '[]'::jsonb END) ? 'ops-manager' THEN 'ops-manager'
  8. WHEN (CASE WHEN jsonb_typeof(u->'roles')='array' THEN u->'roles' ELSE '[]'::jsonb END) ? 'ops-staff' THEN 'ops-staff'
  9. WHEN (CASE WHEN jsonb_typeof(u->'roles')='array' THEN u->'roles' ELSE '[]'::jsonb END) ? 'ops-auditor' THEN 'ops-auditor'
  10. WHEN COALESCE(u->>'adminRoleKey','')='' AND NOT EXISTS(SELECT 1 FROM jsonb_array_elements_text((CASE WHEN jsonb_typeof(u->'roles')='array' THEN u->'roles' ELSE '[]'::jsonb END)) r WHERE r LIKE 'ops-%')
  11. AND (u->>'isAdmin'='true' OR u->>'role'='admin' OR (CASE WHEN jsonb_typeof(u->'roles')='array' THEN u->'roles' ELSE '[]'::jsonb END) ? 'admin') THEN 'ops-manager'
  12. ELSE '' END
  13. $$;
  14. CREATE OR REPLACE FUNCTION xs_operator_change(co text, actor_id text, target_id text, operation text, payload jsonb, reason text, expected text, audit_id text)
  15. RETURNS jsonb LANGUAGE plpgsql AS $$
  16. DECLARE actor "_User"%ROWTYPE; target "_User"%ROWTYPE; actor_role text; target_role text; next_role text; before_value jsonb; after_value jsonb; revoked integer := 0;
  17. BEGIN
  18. PERFORM set_config('xiaoshu.operator_write','on',true);
  19. IF co IS NULL OR co='' OR length(trim(reason)) NOT BETWEEN 2 AND 500 THEN RAISE EXCEPTION '帐套或操作原因无效'; END IF;
  20. PERFORM pg_advisory_xact_lock(hashtextextended('operator-account:'||co,0));
  21. SELECT * INTO actor FROM "_User" WHERE "objectId"=actor_id FOR UPDATE;
  22. actor_role := xs_operator_role(to_jsonb(actor));
  23. IF actor."objectId" IS NULL OR COALESCE(actor."isDisabled",false) OR COALESCE(actor."isDeleted",false) OR actor_role NOT IN ('ops-manager','super-admin') OR actor."company" IS DISTINCT FROM co THEN RAISE EXCEPTION '当前账号无权管理运营账号'; END IF;
  24. SELECT * INTO target FROM "_User" WHERE "objectId"=target_id AND "company"=co FOR UPDATE;
  25. target_role := xs_operator_role(to_jsonb(target));
  26. IF target."objectId" IS NULL OR COALESCE(target."isDeleted",false) OR (target_role='' AND COALESCE(target."identityType",'')<>'operator' AND NOT COALESCE(target."isAdmin",false) AND COALESCE(target."role",'')<>'admin' AND NOT COALESCE(target."roles",'[]'::jsonb) ? 'admin') THEN RAISE EXCEPTION '运营账号不存在'; END IF;
  27. IF target_role='super-admin' AND actor_role<>'super-admin' THEN RAISE EXCEPTION '不能修改超级管理员'; END IF;
  28. IF NULLIF(expected,'') IS NOT NULL AND date_trunc('milliseconds',target."updatedAt") IS DISTINCT FROM expected::timestamptz THEN RAISE EXCEPTION '账号已被其他管理员修改,请刷新'; END IF;
  29. before_value := jsonb_build_object('displayName',target."realName",'roleKey',target_role,'disabled',COALESCE(target."isDisabled",false));
  30. IF operation IN ('save','create') THEN
  31. next_role := payload->>'roleKey';
  32. IF next_role IS NULL OR next_role NOT IN ('super-admin','ops-manager','ops-staff','ops-auditor') OR (next_role='super-admin' AND actor_role<>'super-admin') THEN RAISE EXCEPTION '不能分配该角色'; END IF;
  33. IF actor_id=target_id AND next_role<>target_role THEN RAISE EXCEPTION '不能修改自己的管理角色'; END IF;
  34. IF length(trim(COALESCE(payload->>'displayName',''))) NOT BETWEEN 1 AND 80 THEN RAISE EXCEPTION '请填写 1 至 80 字的姓名'; END IF;
  35. UPDATE "_User" SET "realName"=trim(payload->>'displayName'),"nickname"=trim(payload->>'displayName'),"roles"=jsonb_build_array('admin',next_role),"adminRoleKey"=next_role,"role"='admin',"isAdmin"=true,"identityType"='operator',"isDisabled"=CASE WHEN operation='create' THEN false ELSE "isDisabled" END,"updatedAt"=date_trunc('milliseconds',clock_timestamp()) WHERE "objectId"=target_id;
  36. IF next_role<>target_role THEN DELETE FROM "_Session" WHERE "user"=target_id;GET DIAGNOSTICS revoked=ROW_COUNT; END IF;
  37. ELSIF operation='status' THEN
  38. IF actor_id=target_id THEN RAISE EXCEPTION '不能停用当前登录账号'; END IF;
  39. IF jsonb_typeof(payload->'disabled') IS DISTINCT FROM 'boolean' THEN RAISE EXCEPTION '账号状态无效'; END IF;
  40. UPDATE "_User" SET "isDisabled"=(payload->>'disabled')::boolean,"updatedAt"=date_trunc('milliseconds',clock_timestamp()) WHERE "objectId"=target_id;
  41. IF (payload->>'disabled')::boolean THEN DELETE FROM "_Session" WHERE "user"=target_id;GET DIAGNOSTICS revoked=ROW_COUNT; END IF;
  42. ELSIF operation='revoke-sessions' THEN
  43. DELETE FROM "_Session" WHERE "user"=target_id;GET DIAGNOSTICS revoked=ROW_COUNT;
  44. ELSE RAISE EXCEPTION '不支持的账号操作'; END IF;
  45. SELECT jsonb_build_object('displayName',"realName",'roleKey',xs_operator_role(to_jsonb(u)),'disabled',COALESCE("isDisabled",false)) INTO after_value FROM "_User" u WHERE "objectId"=target_id;
  46. INSERT INTO "OperatorAccountAudit"("objectId","company","actorId","actorName","targetId","targetName","action","reason","before","after","createdAt","updatedAt") VALUES(audit_id,co,actor_id,actor."username",target_id,target."username",operation,reason,before_value,after_value,now(),now());
  47. PERFORM set_config('xiaoshu.operator_write','off',true);
  48. RETURN jsonb_build_object('objectId',target_id,'revokedSessions',revoked);
  49. END $$;
  50. -- Parse user endpoints allow owners to update custom fields even with a private
  51. -- ACL. Protect authorization at storage level, including crafted sign-up data.
  52. CREATE OR REPLACE FUNCTION xs_guard_operator_authority() RETURNS trigger LANGUAGE plpgsql AS $$
  53. DECLARE old_json jsonb; new_json jsonb := to_jsonb(NEW); key text;
  54. BEGIN
  55. IF current_setting('xiaoshu.operator_write',true)='on' THEN IF TG_OP='DELETE' THEN RETURN OLD; END IF; RETURN NEW; END IF;
  56. IF TG_OP='DELETE' THEN IF xs_operator_role(to_jsonb(OLD))<>'' THEN RAISE EXCEPTION '运营账号请通过账号管理停用,不能直接删除'; END IF; RETURN OLD; END IF;
  57. IF TG_OP='INSERT' THEN
  58. IF xs_operator_role(new_json)<>'' OR NEW."isAdmin"=true OR COALESCE(NEW."adminRoleKey",'')<>'' OR NEW."role"='admin' THEN RAISE EXCEPTION '管理员角色只能通过运营账号管理分配'; END IF;
  59. RETURN NEW;
  60. END IF;
  61. old_json := to_jsonb(OLD);
  62. FOREACH key IN ARRAY ARRAY['roles','role','adminRoleKey','isAdmin'] LOOP
  63. IF old_json->key IS DISTINCT FROM new_json->key THEN RAISE EXCEPTION '管理员权限字段只能通过运营账号管理修改'; END IF;
  64. END LOOP;
  65. IF xs_operator_role(old_json)<>'' OR OLD."identityType"='operator' THEN
  66. FOREACH key IN ARRAY ARRAY['company','identityType','isDisabled','isDeleted'] LOOP
  67. IF old_json->key IS DISTINCT FROM new_json->key THEN RAISE EXCEPTION '管理员归属与状态只能通过运营账号管理修改'; END IF;
  68. END LOOP;
  69. END IF;
  70. RETURN NEW;
  71. END $$;
  72. DROP TRIGGER IF EXISTS xs_operator_authority_guard ON "_User";
  73. CREATE TRIGGER xs_operator_authority_guard BEFORE INSERT OR UPDATE OR DELETE ON "_User" FOR EACH ROW EXECUTE FUNCTION xs_guard_operator_authority();