| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374 |
- -- Role/status changes recheck the actor under a company lock. Two managers
- -- cannot concurrently disable/demote each other and leave no manager behind.
- CREATE OR REPLACE FUNCTION xs_operator_role(u jsonb) RETURNS text LANGUAGE sql IMMUTABLE AS $$
- SELECT CASE
- WHEN u->>'adminRoleKey' IN ('super-admin','ops-manager','ops-staff','ops-auditor') THEN u->>'adminRoleKey'
- WHEN (CASE WHEN jsonb_typeof(u->'roles')='array' THEN u->'roles' ELSE '[]'::jsonb END) ? 'super-admin' THEN 'super-admin'
- WHEN (CASE WHEN jsonb_typeof(u->'roles')='array' THEN u->'roles' ELSE '[]'::jsonb END) ? 'ops-manager' THEN 'ops-manager'
- WHEN (CASE WHEN jsonb_typeof(u->'roles')='array' THEN u->'roles' ELSE '[]'::jsonb END) ? 'ops-staff' THEN 'ops-staff'
- WHEN (CASE WHEN jsonb_typeof(u->'roles')='array' THEN u->'roles' ELSE '[]'::jsonb END) ? 'ops-auditor' THEN 'ops-auditor'
- 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-%')
- 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'
- ELSE '' END
- $$;
- 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)
- RETURNS jsonb LANGUAGE plpgsql AS $$
- 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;
- BEGIN
- PERFORM set_config('xiaoshu.operator_write','on',true);
- IF co IS NULL OR co='' OR length(trim(reason)) NOT BETWEEN 2 AND 500 THEN RAISE EXCEPTION '帐套或操作原因无效'; END IF;
- PERFORM pg_advisory_xact_lock(hashtextextended('operator-account:'||co,0));
- SELECT * INTO actor FROM "_User" WHERE "objectId"=actor_id FOR UPDATE;
- actor_role := xs_operator_role(to_jsonb(actor));
- 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;
- SELECT * INTO target FROM "_User" WHERE "objectId"=target_id AND "company"=co FOR UPDATE;
- target_role := xs_operator_role(to_jsonb(target));
- 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;
- IF target_role='super-admin' AND actor_role<>'super-admin' THEN RAISE EXCEPTION '不能修改超级管理员'; END IF;
- IF NULLIF(expected,'') IS NOT NULL AND date_trunc('milliseconds',target."updatedAt") IS DISTINCT FROM expected::timestamptz THEN RAISE EXCEPTION '账号已被其他管理员修改,请刷新'; END IF;
- before_value := jsonb_build_object('displayName',target."realName",'roleKey',target_role,'disabled',COALESCE(target."isDisabled",false));
- IF operation IN ('save','create') THEN
- next_role := payload->>'roleKey';
- 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;
- IF actor_id=target_id AND next_role<>target_role THEN RAISE EXCEPTION '不能修改自己的管理角色'; END IF;
- IF length(trim(COALESCE(payload->>'displayName',''))) NOT BETWEEN 1 AND 80 THEN RAISE EXCEPTION '请填写 1 至 80 字的姓名'; END IF;
- 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;
- IF next_role<>target_role THEN DELETE FROM "_Session" WHERE "user"=target_id;GET DIAGNOSTICS revoked=ROW_COUNT; END IF;
- ELSIF operation='status' THEN
- IF actor_id=target_id THEN RAISE EXCEPTION '不能停用当前登录账号'; END IF;
- IF jsonb_typeof(payload->'disabled') IS DISTINCT FROM 'boolean' THEN RAISE EXCEPTION '账号状态无效'; END IF;
- UPDATE "_User" SET "isDisabled"=(payload->>'disabled')::boolean,"updatedAt"=date_trunc('milliseconds',clock_timestamp()) WHERE "objectId"=target_id;
- IF (payload->>'disabled')::boolean THEN DELETE FROM "_Session" WHERE "user"=target_id;GET DIAGNOSTICS revoked=ROW_COUNT; END IF;
- ELSIF operation='revoke-sessions' THEN
- DELETE FROM "_Session" WHERE "user"=target_id;GET DIAGNOSTICS revoked=ROW_COUNT;
- ELSE RAISE EXCEPTION '不支持的账号操作'; END IF;
- 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;
- 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());
- PERFORM set_config('xiaoshu.operator_write','off',true);
- RETURN jsonb_build_object('objectId',target_id,'revokedSessions',revoked);
- END $$;
- -- Parse user endpoints allow owners to update custom fields even with a private
- -- ACL. Protect authorization at storage level, including crafted sign-up data.
- CREATE OR REPLACE FUNCTION xs_guard_operator_authority() RETURNS trigger LANGUAGE plpgsql AS $$
- DECLARE old_json jsonb; new_json jsonb := to_jsonb(NEW); key text;
- BEGIN
- IF current_setting('xiaoshu.operator_write',true)='on' THEN IF TG_OP='DELETE' THEN RETURN OLD; END IF; RETURN NEW; END IF;
- IF TG_OP='DELETE' THEN IF xs_operator_role(to_jsonb(OLD))<>'' THEN RAISE EXCEPTION '运营账号请通过账号管理停用,不能直接删除'; END IF; RETURN OLD; END IF;
- IF TG_OP='INSERT' THEN
- IF xs_operator_role(new_json)<>'' OR NEW."isAdmin"=true OR COALESCE(NEW."adminRoleKey",'')<>'' OR NEW."role"='admin' THEN RAISE EXCEPTION '管理员角色只能通过运营账号管理分配'; END IF;
- RETURN NEW;
- END IF;
- old_json := to_jsonb(OLD);
- FOREACH key IN ARRAY ARRAY['roles','role','adminRoleKey','isAdmin'] LOOP
- IF old_json->key IS DISTINCT FROM new_json->key THEN RAISE EXCEPTION '管理员权限字段只能通过运营账号管理修改'; END IF;
- END LOOP;
- IF xs_operator_role(old_json)<>'' OR OLD."identityType"='operator' THEN
- FOREACH key IN ARRAY ARRAY['company','identityType','isDisabled','isDeleted'] LOOP
- IF old_json->key IS DISTINCT FROM new_json->key THEN RAISE EXCEPTION '管理员归属与状态只能通过运营账号管理修改'; END IF;
- END LOOP;
- END IF;
- RETURN NEW;
- END $$;
- DROP TRIGGER IF EXISTS xs_operator_authority_guard ON "_User";
- CREATE TRIGGER xs_operator_authority_guard BEFORE INSERT OR UPDATE OR DELETE ON "_User" FOR EACH ROW EXECUTE FUNCTION xs_guard_operator_authority();
|