|
|
@@ -827,7 +827,8 @@ public class FlowEngineService : ITransient
|
|
|
// S8-EXCEPTION-FLOW-TENANT-CONTEXT-1:trustedTenantId 仅由 StartFlowCore 的受信任路径透传;
|
|
|
// 其余既有推进路径(Approve/ReturnToPrev/超时自动通过等)不传,回退到 _userManager.TenantId,行为不变。
|
|
|
var effectiveTenantId = trustedTenantId ?? _userManager.TenantId;
|
|
|
- var approvers = await ResolveApprovers(node.Properties, instance.InitiatorId, effectiveTenantId);
|
|
|
+ var approvers = await ResolveApprovers(node.Properties, instance.InitiatorId, effectiveTenantId,
|
|
|
+ new FlowAuthorityContext(instance.BizType, instance.FlowId, nodeId, node.Properties?.NodeName));
|
|
|
if (approvers.Count == 0)
|
|
|
throw Oops.Oh($"节点 [{node.Properties?.NodeName ?? nodeId}] 未配置审批人或审批人列表为空");
|
|
|
|
|
|
@@ -928,12 +929,77 @@ public class FlowEngineService : ITransient
|
|
|
await _taskRep.AsUpdateable(paired).ExecuteCommandAsync();
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// S6-TENANT-AUTHORITY-1:审批人解析的诊断上下文,供跨租户权威守卫判定 BizType 并输出可定位日志。
|
|
|
+ /// 不参与任何业务判断,缺省为 null 时守卫整体不生效(保持既有行为)。
|
|
|
+ /// </summary>
|
|
|
+ private sealed record FlowAuthorityContext(string? BizType, long FlowId, string? NodeId, string? NodeName);
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// S6-TENANT-AUTHORITY-1:**已完成**租户本地化迁移的 BizType 白名单。
|
|
|
+ ///
|
|
|
+ /// 为什么是白名单而不是全局生效:S5_IQC_INSPBILL / S7_FQC_INSPBILL 等 BizType 的流程定义同样
|
|
|
+ /// 写死了默认租户物理 RoleId,而它们**当前正靠跨租户 SysUserRole 绑定在工作**(实测 28 个节点组合)。
|
|
|
+ /// 无条件启用会把这些仍可用的节点当场打成 0 审批人,且这些模块本批不在迁移范围内。
|
|
|
+ /// 各模块完成「租户本地角色 + 同租户成员 + RoleCode 定义」迁移后,再把自己的 BizType 加进来。
|
|
|
+ /// </summary>
|
|
|
+ private static readonly HashSet<string> TenantStrictRoleBizTypes = new(StringComparer.Ordinal)
|
|
|
+ {
|
|
|
+ "IPQC_INSPECTION",
|
|
|
+ "S6_PROCESS_INSPECTION",
|
|
|
+ };
|
|
|
+
|
|
|
+ private static bool IsTenantStrictBizType(string? bizType) =>
|
|
|
+ bizType != null && TenantStrictRoleBizTypes.Contains(bizType);
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// S6-TENANT-AUTHORITY-1:对白名单内的 BizType,FlowJson 中写死的数字 RoleId 必须属于当前租户。
|
|
|
+ ///
|
|
|
+ /// 原行为是无条件信任该 RoleId,只在最后按 <c>SysUser.TenantId</c> 过滤成员——那是「成员归属过滤」,
|
|
|
+ /// 挡得住跨租户泄漏,挡不住跨租户**引用**:角色属于别的租户时静默解析成 0 人,
|
|
|
+ /// 报错却是「未配置审批人」,把配置缺陷伪装成配置缺失。此处改为显式 fail-closed 并分类。
|
|
|
+ ///
|
|
|
+ /// 刻意**不做** auto-heal(不按 Code 隐式替换成本租户角色):那会让错误定义继续成功运行、
|
|
|
+ /// 永远没人去修,迁移也就永远不会真正完成。正确路径是把定义改成 RoleCode。
|
|
|
+ /// </summary>
|
|
|
+ private async Task EnsureRoleAuthorityTenantScopedAsync(List<long> roleIds, long effectiveTenantId, FlowAuthorityContext? ctx)
|
|
|
+ {
|
|
|
+ if (roleIds.Count == 0 || !IsTenantStrictBizType(ctx?.BizType)) return;
|
|
|
+
|
|
|
+ var roles = await _userRep.Context.Queryable<SysRole>()
|
|
|
+ .ClearFilter()
|
|
|
+ .Where(r => roleIds.Contains(r.Id))
|
|
|
+ .ToListAsync();
|
|
|
+
|
|
|
+ var missing = roleIds.Except(roles.Select(r => r.Id)).ToList();
|
|
|
+ if (missing.Count > 0)
|
|
|
+ {
|
|
|
+ _logger.LogWarning(
|
|
|
+ "INVALID_ROLE: BizType={BizType}, FlowId={FlowId}, NodeId={NodeId}, RoleRef={RoleRef}, EffectiveTenantId={TenantId}",
|
|
|
+ ctx!.BizType, ctx.FlowId, ctx.NodeId, string.Join(",", missing), effectiveTenantId);
|
|
|
+ throw Oops.Oh($"节点 [{ctx.NodeName ?? ctx.NodeId}] 配置的审批角色不存在,请检查流程定义。");
|
|
|
+ }
|
|
|
+
|
|
|
+ var foreign = roles.Where(r => r.TenantId != effectiveTenantId).ToList();
|
|
|
+ if (foreign.Count == 0) return;
|
|
|
+
|
|
|
+ // 日志带全部定位字段;对外文案不暴露其它租户的 Id 与角色名称。
|
|
|
+ _logger.LogWarning(
|
|
|
+ "CROSS_TENANT_ROLE_REFERENCE: BizType={BizType}, FlowId={FlowId}, NodeId={NodeId}, RoleRef={RoleRef}, RoleTenants={RoleTenants}, EffectiveTenantId={TenantId}",
|
|
|
+ ctx!.BizType, ctx.FlowId, ctx.NodeId,
|
|
|
+ string.Join(",", foreign.Select(r => r.Id)),
|
|
|
+ string.Join(",", foreign.Select(r => r.TenantId)),
|
|
|
+ effectiveTenantId);
|
|
|
+ throw Oops.Oh($"节点 [{ctx.NodeName ?? ctx.NodeId}] 引用了不属于当前租户的审批角色,请将流程定义改为按角色编码配置。");
|
|
|
+ }
|
|
|
+
|
|
|
/// <summary>
|
|
|
/// S8-EXCEPTION-FLOW-TENANT-CONTEXT-1:<paramref name="effectiveTenantId"/> 是本方法内所有租户相关判断
|
|
|
/// 的唯一真值源——Role Code → RoleId 查询、RoleId → 用户查询,一律使用它,不得再读 _userManager.TenantId。
|
|
|
/// 调用方负责传入正确值:HTTP 场景传 <c>_userManager.TenantId</c>;受信任后台场景传调用方已持有的业务租户。
|
|
|
/// </summary>
|
|
|
- private async Task<List<(long userId, string userName)>> ResolveApprovers(FlowProperties? props, long initiatorId, long effectiveTenantId)
|
|
|
+ private async Task<List<(long userId, string userName)>> ResolveApprovers(FlowProperties? props, long initiatorId, long effectiveTenantId,
|
|
|
+ FlowAuthorityContext? authorityContext = null)
|
|
|
{
|
|
|
if (props == null || string.IsNullOrWhiteSpace(props.ApproverType))
|
|
|
return new List<(long, string)>();
|
|
|
@@ -965,6 +1031,11 @@ public class FlowEngineService : ITransient
|
|
|
|
|
|
if (approverType == nameof(ApproverTypeEnum.Role))
|
|
|
{
|
|
|
+ // S6-TENANT-AUTHORITY-1:对已完成租户本地化迁移的 BizType,数字 RoleId 必须属于当前租户。
|
|
|
+ // 放在 Code 解析之前,针对的是 FlowJson 里原样写死的物理 RoleId(codes 分支解析出的
|
|
|
+ // RoleId 天生就带 TenantId == effectiveTenantId 条件,不在检查范围)。
|
|
|
+ await EnsureRoleAuthorityTenantScopedAsync(ids, effectiveTenantId, authorityContext);
|
|
|
+
|
|
|
if (codes.Count > 0)
|
|
|
{
|
|
|
var codeRoleIds = await _userRep.Context.Queryable<SysRole>()
|
|
|
@@ -993,6 +1064,17 @@ public class FlowEngineService : ITransient
|
|
|
.ClearFilter()
|
|
|
.Where(u => userIds.Contains(u.Id) && u.TenantId == effectiveTenantId)
|
|
|
.ToListAsync();
|
|
|
+
|
|
|
+ // S6-TENANT-AUTHORITY-1:第三类失败——角色配置正确、就是本租户内没人。
|
|
|
+ // 原先与「跨租户误配」一样退化成空列表,最终只报一句语义误导的「未配置审批人」。
|
|
|
+ if (users.Count == 0 && IsTenantStrictBizType(authorityContext?.BizType))
|
|
|
+ {
|
|
|
+ _logger.LogWarning(
|
|
|
+ "LOCAL_ROLE_HAS_NO_MEMBER: BizType={BizType}, FlowId={FlowId}, NodeId={NodeId}, RoleRef={RoleRef}, EffectiveTenantId={TenantId}",
|
|
|
+ authorityContext!.BizType, authorityContext.FlowId, authorityContext.NodeId, props.ApproverIds, effectiveTenantId);
|
|
|
+ throw Oops.Oh($"节点 [{authorityContext.NodeName ?? authorityContext.NodeId}] 的审批角色在当前租户下没有成员,请先为该角色分配人员。");
|
|
|
+ }
|
|
|
+
|
|
|
return users.Select(u => (u.Id, u.RealName ?? "")).ToList();
|
|
|
}
|
|
|
|