Skip to main content

sz_orm_core/governance/
mod.rs

1//! 编译期数据治理(v4.3.0 M3-T3/T4,`compile-governance` feature)
2//!
3//! 由 [`GovernedModel`] trait + [`Governed`] 派生宏(sz-orm-macros)驱动:
4//!
5//! - **编译期强制**:`#[pii]` 字段必须声明 `#[mask(strategy = "...")]`,
6//!   策略必须在白名单内(hash/partial/replace/encrypt),违反即编译失败
7//! - **运行时元数据**:`pii_fields()` 暴露 PII 字段与脱敏策略,
8//!   供脱敏执行(sz-orm-masking)与审计(sz-orm-audit)消费
9//! - **合规报告**:[`compliance_report`] 生成 GDPR/等保清单(JSON 可审计)
10//!
11//! ```ignore
12//! use sz_orm_core::governance::{compliance_report, GovernedModel};
13//!
14//! #[derive(sz_orm_macros::Governed)]
15//! struct User {
16//!     id: i64,
17//!     #[pii]
18//!     #[mask(strategy = "partial")]
19//!     email: String,
20//! }
21//!
22//! let report = compliance_report(&[User::pii_fields()]);
23//! ```
24
25/// 数据治理模型 trait(由 `#[derive(Governed)]` 自动实现)
26pub trait GovernedModel {
27    /// 返回 PII 字段列表:`(字段名, 脱敏策略)`,策略 ∈ {hash, partial, replace, encrypt}
28    fn pii_fields() -> Vec<(&'static str, &'static str)>;
29}
30
31/// 单条 PII 字段合规条目
32#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
33pub struct PiiFieldEntry {
34    /// 字段名
35    pub field: String,
36    /// 脱敏策略
37    pub strategy: String,
38}
39
40/// 合规报告(GDPR / 等保清单)
41#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
42pub struct ComplianceReport {
43    /// 全部 PII 字段清单
44    pub pii_fields: Vec<PiiFieldEntry>,
45    /// 数据保留天数(`None` = 未配置)
46    pub retention_days: Option<u32>,
47    /// 报告生成时间(ISO 8601)
48    pub generated_at: String,
49}
50
51impl ComplianceReport {
52    /// 序列化为 JSON(供审计工具/CI 消费)
53    pub fn to_json(&self) -> Result<String, serde_json::Error> {
54        serde_json::to_string_pretty(self)
55    }
56
57    /// PII 字段数量
58    pub fn pii_field_count(&self) -> usize {
59        self.pii_fields.len()
60    }
61}
62
63/// 从多个模型的 `pii_fields()` 汇总生成合规报告
64pub fn compliance_report(models: &[Vec<(&'static str, &'static str)>]) -> ComplianceReport {
65    let mut seen = std::collections::HashSet::new();
66    let mut pii_fields = Vec::new();
67    for model_fields in models {
68        for (field, strategy) in model_fields {
69            if seen.insert((*field, *strategy)) {
70                pii_fields.push(PiiFieldEntry {
71                    field: (*field).to_string(),
72                    strategy: (*strategy).to_string(),
73                });
74            }
75        }
76    }
77    ComplianceReport {
78        pii_fields,
79        retention_days: None,
80        generated_at: now_iso8601(),
81    }
82}
83
84/// 设置数据保留天数(合规策略配置)
85pub fn with_retention(mut report: ComplianceReport, days: u32) -> ComplianceReport {
86    report.retention_days = Some(days);
87    report
88}
89
90/// 当前 UTC 时间 ISO 8601(无外部依赖,Hinnant civil-from-days 算法)
91fn now_iso8601() -> String {
92    let Ok(d) = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) else {
93        return "1970-01-01T00:00:00Z".to_string();
94    };
95    let secs = d.as_secs();
96    let days = secs / 86_400;
97    let rem = secs % 86_400;
98    let (h, m, s) = (rem / 3600, (rem % 3600) / 60, rem % 60);
99
100    let z = days as i64 + 719_468;
101    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
102    let doe = z - era * 146_097;
103    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
104    let y = yoe + era * 400;
105    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
106    let mp = (5 * doy + 2) / 153;
107    let day = doy - (153 * mp + 2) / 5 + 1;
108    let month = if mp < 10 { mp + 3 } else { mp - 9 };
109    let year = if month <= 2 { y + 1 } else { y };
110
111    format!("{year:04}-{month:02}-{day:02}T{h:02}:{m:02}:{s:02}Z")
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    struct User;
119    impl GovernedModel for User {
120        fn pii_fields() -> Vec<(&'static str, &'static str)> {
121            vec![("email", "partial"), ("phone", "hash")]
122        }
123    }
124
125    struct Order;
126    impl GovernedModel for Order {
127        fn pii_fields() -> Vec<(&'static str, &'static str)> {
128            vec![("phone", "hash")] // 与 User 重叠,应去重
129        }
130    }
131
132    #[test]
133    fn aggregates_and_deduplicates() {
134        let report = compliance_report(&[User::pii_fields(), Order::pii_fields()]);
135        assert_eq!(report.pii_field_count(), 2);
136        assert!(report.pii_fields.contains(&PiiFieldEntry {
137            field: "email".into(),
138            strategy: "partial".into()
139        }));
140        assert!(report.pii_fields.contains(&PiiFieldEntry {
141            field: "phone".into(),
142            strategy: "hash".into()
143        }));
144    }
145
146    #[test]
147    fn json_output_is_valid() {
148        let report = with_retention(compliance_report(&[User::pii_fields()]), 730);
149        let json = report.to_json().expect("serialize");
150        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
151        assert_eq!(parsed["retention_days"], 730);
152        assert_eq!(parsed["pii_fields"].as_array().unwrap().len(), 2);
153        assert!(parsed["generated_at"].as_str().unwrap().ends_with('Z'));
154    }
155
156    #[test]
157    fn empty_models_produce_empty_report() {
158        let report = compliance_report(&[]);
159        assert_eq!(report.pii_field_count(), 0);
160        assert!(report.to_json().is_ok());
161    }
162}