Skip to main content

nasaga_runtime_core/
management.rs

1//! Saga 管理面主体、权限与原因合同。
2//!
3//! HTTP/JWT/mTLS 的认证实现属于宿主应用;运行时只接收认证层构造的不可变管理上下文,
4//! 并在任何数据库读取或状态变化前执行细粒度权限检查。actor 与 reason 会进入同事务审计。
5
6use std::collections::BTreeSet;
7
8use nasaga_backend::{
9    SagaAttemptAuditRow, SagaConflictFactRow, SagaControlAuditRow, SagaManagementAuditRow,
10    SagaStepAttemptRow, SagaTransitionAuditRow,
11};
12use serde::{Deserialize, Serialize};
13
14/// 业务作用:定义 Saga 管理操作的最小授权单元,避免用单一 admin 布尔值放大权限。
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub enum SagaManagementPermission {
17    /// 暂停自动路由与 timer 动作。
18    Pause,
19    /// 恢复自动路由与 timer 动作。
20    Resume,
21    /// 在人工介入后沿冻结计划重试补偿。
22    RetryCompensation,
23    /// 在 Unknown 解决预算耗尽后发布一次受审计的新查询。
24    RetryResolution,
25    /// 读取实例、attempt、transition、control 与冲突审计。
26    ReadAudit,
27    /// 租户受限的实例只读检索(状态/时间窗过滤 + keyset 分页)。
28    ///
29    /// 与写动作权限分离:运维定位待处置对象只需要本权限,不需要任何改变实例状态的
30    /// 能力;响应不含业务 payload。
31    ListInstances,
32    /// 系统外处置完成后把 `MANUAL_INTERVENTION` 实例人工关闭为 `MANUALLY_CLOSED`。
33    ///
34    /// 关闭只表达"自动化已由人工关闭",不伪造 `COMPLETED`/`COMPENSATED` 业务事实;
35    /// 动作要求一次性 operation identity 并与审计同事务提交。
36    ManualClose,
37}
38
39impl SagaManagementPermission {
40    /// 业务作用:返回进入鉴权日志与审计合同的稳定权限名。
41    ///
42    /// 参数说明: 无。
43    ///
44    /// 返回:低基数稳定权限字符串。
45    pub fn as_str(self) -> &'static str {
46        match self {
47            Self::Pause => "saga.pause",
48            Self::Resume => "saga.resume",
49            Self::RetryCompensation => "saga.retry_compensation",
50            Self::RetryResolution => "saga.retry_resolution",
51            Self::ReadAudit => "saga.audit.read",
52            Self::ListInstances => "saga.instance.list",
53            Self::ManualClose => "saga.manual_close",
54        }
55    }
56}
57
58/// 业务作用:封闭管理上下文、查询与写动作的确定性拒绝,供协议层稳定映射状态。
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60#[non_exhaustive]
61pub enum SagaManagementError {
62    /// 调用主体、原因或其它审计上下文不符合持久化边界。
63    InvalidContext,
64    /// 已认证主体缺少当前动作的最小权限。
65    PermissionDenied,
66    /// 目标实例不存在或不属于已授权租户。
67    NotFound,
68    /// 当前租户的管理动作窗口预算已耗尽。
69    RateLimitExceeded,
70    /// 实例状态、控制态、恢复证据或 CAS 快照不允许当前动作。
71    PreconditionFailed,
72    /// 稳定管理操作身份已绑定另一份动作或审计事实。
73    OperationConflict,
74}
75
76impl SagaManagementError {
77    /// 业务作用:从可能附加事务上下文的错误链提取可公开管理拒绝类别。
78    ///
79    /// 参数说明:`error` 是管理查询或动作返回的完整错误链。
80    ///
81    /// 返回:命中管理拒绝或后端唯一身份冲突时返回对应类别;基础设施或未知失败返回 `None`。
82    pub fn from_error(error: &anyhow::Error) -> Option<Self> {
83        error
84            .chain()
85            .find_map(|cause| cause.downcast_ref::<Self>().copied())
86            .or_else(|| {
87                error
88                    .chain()
89                    .any(|cause| {
90                        cause
91                            .downcast_ref::<nasaga_backend::SagaBackendError>()
92                            .is_some_and(|error| {
93                                error.kind() == nasaga_backend::SagaBackendErrorKind::Conflict
94                            })
95                    })
96                    .then_some(Self::OperationConflict)
97            })
98    }
99}
100
101impl std::fmt::Display for SagaManagementError {
102    /// 业务作用:输出不暴露实例存在性、租户用量或业务状态细节的稳定管理错误摘要。
103    ///
104    /// 参数说明:`formatter` 是标准格式化输出目标。
105    ///
106    /// 返回:摘要写入成功时返回 `Ok`;格式化失败返回对应错误。
107    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        formatter.write_str(match self {
109            Self::InvalidContext => "Saga management context is invalid",
110            Self::PermissionDenied => "Saga management permission is denied",
111            Self::NotFound => "Saga management target was not found",
112            Self::RateLimitExceeded => "saga_tenant_action_rate_exceeded",
113            Self::PreconditionFailed => "Saga management precondition is not satisfied",
114            Self::OperationConflict => "Saga management operation has different audit facts",
115        })
116    }
117}
118
119impl std::error::Error for SagaManagementError {}
120
121/// 业务作用:携带认证主体、操作原因和冻结权限集,是全部人工控制入口的强制参数。
122///
123/// `actor` 必须是认证层给出的稳定主体 id,禁止直接使用请求体中的显示名;`reason`
124/// 是事故单/变更单关联所需的人类可读原因,不得包含控制字符或敏感 payload。
125#[derive(Debug, Clone)]
126pub struct SagaManagementContext {
127    actor: String,
128    reason: String,
129    permissions: BTreeSet<SagaManagementPermission>,
130}
131
132/// 业务作用:冻结调用方观察到的 Saga 状态与控制版本,使管理动作在同一事务内拒绝过期快照。
133#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
134pub struct SagaManagementExpectation {
135    expected_state_version: Option<u64>,
136    expected_control_version: Option<u64>,
137}
138
139impl SagaManagementExpectation {
140    /// 业务作用:构造管理动作的可选双版本 CAS 合同。
141    ///
142    /// 参数说明:两个 expected version 分别约束业务状态和控制状态;未提供的一侧不参与比较。
143    ///
144    /// 返回:返回不可变前置条件,最终比较必须在持有实例事务锁后执行。
145    pub fn new(expected_state_version: Option<u64>, expected_control_version: Option<u64>) -> Self {
146        Self {
147            expected_state_version,
148            expected_control_version,
149        }
150    }
151
152    /// 业务作用:在实例已进入当前数据库事务后复验调用方快照,防止管理动作覆盖并发推进。
153    ///
154    /// 参数说明:`state_version` 与 `control_version` 来自事务内刚装载的实例行。
155    ///
156    /// 返回:已提供版本全部一致时成功;任一过期返回稳定并发裁决。
157    pub(crate) fn verify(self, state_version: u64, control_version: u64) -> anyhow::Result<()> {
158        if self
159            .expected_state_version
160            .is_some_and(|expected| expected != state_version)
161            || self
162                .expected_control_version
163                .is_some_and(|expected| expected != control_version)
164        {
165            return Err(crate::SagaConcurrencyError::StaleSnapshot.into());
166        }
167        Ok(())
168    }
169}
170
171/// 业务作用:聚合一次有界管理查询返回的 attempt、迁移、控制、人工恢复与冲突事实。
172///
173/// 每个集合都由同一租户门禁保护;高基数业务身份只出现在本管理响应,不进入指标标签。
174#[derive(Debug, Clone, PartialEq, Eq)]
175pub struct SagaAuditTrail {
176    /// 命令投递与结果终态事实。
177    pub attempts: Vec<SagaStepAttemptRow>,
178    /// 业务状态迁移审计链。
179    pub transitions: Vec<SagaTransitionAuditRow>,
180    /// pause/resume 控制态主体审计。
181    pub controls: Vec<SagaControlAuditRow>,
182    /// 人工恢复业务动作审计。
183    pub management_operations: Vec<SagaManagementAuditRow>,
184    /// 互斥结果事实。
185    pub conflicts: Vec<SagaConflictFactRow>,
186}
187
188/// 业务作用:携带最后已交付的全局审计事件序号,支持跨类别追加与断线续读。
189///
190/// `audit_seq` 由数据库在事实事务内单调分配;attempt 状态变化会生成新事件而非覆盖
191/// 已交付位置。协议层必须使用服务端密钥认证游标序列化结果。
192#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
193#[serde(deny_unknown_fields)]
194pub struct SagaAuditPageCursor {
195    /// 最后一条已返回事件的全局序号。
196    pub audit_seq: u64,
197}
198
199/// 业务作用:用封闭类别承载统一审计页中的一条事实,不向协议层暴露数据库表选择逻辑。
200#[derive(Debug, Clone, PartialEq, Eq)]
201pub enum SagaAuditRecord {
202    /// command attempt 事实及其开始时间。
203    Attempt(SagaAttemptAuditRow),
204    /// Saga 状态迁移事实。
205    Transition(SagaTransitionAuditRow),
206    /// pause/resume 控制态事实。
207    Control(SagaControlAuditRow),
208    /// 人工恢复或关闭事实。
209    Management(SagaManagementAuditRow),
210    /// 互斥结果证据。
211    Conflict(SagaConflictFactRow),
212}
213
214/// 业务作用:返回单一有界审计记录流及继续读取所需的稳定游标。
215#[derive(Debug, Clone, PartialEq, Eq)]
216pub struct SagaAuditPage {
217    /// 按数据库全局 `audit_seq` 严格递增的不可变事实快照。
218    pub records: Vec<SagaAuditRecord>,
219    /// 本页非空时指向末项,可作为后续追加事实的持久 checkpoint;空页为 `None`。
220    pub next_cursor: Option<SagaAuditPageCursor>,
221}
222
223impl SagaManagementContext {
224    /// 业务作用:校验并冻结一次管理调用的主体、原因和权限快照。
225    ///
226    /// 参数说明:
227    /// - `actor`: 已认证稳定主体 id,最多 128 字节。
228    /// - `reason`: 操作原因或工单摘要,最多 512 字节。
229    /// - `permissions`: 认证层为当前请求授予的最小权限集合。
230    ///
231    /// 返回:字段合法时返回上下文;空值、首尾空白、控制字符或超长输入返回错误。
232    pub fn new(
233        actor: impl Into<String>,
234        reason: impl Into<String>,
235        permissions: impl IntoIterator<Item = SagaManagementPermission>,
236    ) -> anyhow::Result<Self> {
237        let actor = actor.into();
238        let reason = reason.into();
239        if !valid_audit_text(&actor, 128) || !valid_audit_text(&reason, 512) {
240            return Err(SagaManagementError::InvalidContext.into());
241        }
242        let permissions = permissions.into_iter().collect::<BTreeSet<_>>();
243        Ok(Self {
244            actor,
245            reason,
246            permissions,
247        })
248    }
249
250    /// 业务作用:读取认证主体稳定 id,供同事务审计持久化。
251    ///
252    /// 参数说明: 无。
253    ///
254    /// 返回:actor 字符串切片。
255    pub fn actor(&self) -> &str {
256        &self.actor
257    }
258
259    /// 业务作用:读取本次管理动作原因,供同事务审计与事故复盘。
260    ///
261    /// 参数说明: 无。
262    ///
263    /// 返回:reason 字符串切片。
264    pub fn reason(&self) -> &str {
265        &self.reason
266    }
267
268    /// 业务作用:在任何持久化读取或副作用前执行最小权限门禁。
269    ///
270    /// 参数说明:
271    /// - `required`: 当前管理动作要求的权限。
272    ///
273    /// 返回:权限存在返回成功;缺失时返回不含业务数据的拒绝错误。
274    pub fn require(&self, required: SagaManagementPermission) -> anyhow::Result<()> {
275        if self.permissions.contains(&required) {
276            Ok(())
277        } else {
278            Err(SagaManagementError::PermissionDenied.into())
279        }
280    }
281}
282
283/// 业务作用:校验进入持久化审计列的文本边界,避免截断、日志注入和不可见主体。
284///
285/// 参数说明:
286/// - `value`: actor 或 reason。
287/// - `max_len`: 对应数据库列的字节上限。
288///
289/// 返回:非空、已修剪、无控制字符且长度有界时返回真。
290fn valid_audit_text(value: &str, max_len: usize) -> bool {
291    !value.is_empty()
292        && value.len() <= max_len
293        && value.trim() == value
294        && !value.chars().any(char::is_control)
295}