Skip to main content

u_sdk/sts/
ram_policy.rs

1//! 权限策略语言
2//!
3//! [官方文档](https://help.aliyun.com/zh/ram/policy-language/)
4
5use bon::Builder;
6use serde::Serialize;
7use serde_json;
8use std::collections::HashMap;
9
10/// Version 目前只有 "1"
11#[derive(Debug, Default, Clone, Serialize, PartialEq, Eq)]
12pub enum PolicyVersion {
13    #[serde(rename = "1")]
14    #[default]
15    V1,
16}
17
18/// Effect = "Allow" | "Deny"
19#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
20pub enum Effect {
21    Allow,
22    Deny,
23}
24
25/// JSON 中“可以是单值或数组”的通用包装:
26/// 比如 Action 可以是 "ecs:*" 或 ["ecs:*", "oss:*"]
27#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
28#[serde(untagged)]
29pub enum OneOrMany<T> {
30    One(T),
31    Many(Vec<T>),
32}
33
34impl<T> From<T> for OneOrMany<T> {
35    fn from(value: T) -> Self {
36        OneOrMany::One(value)
37    }
38}
39
40impl<T> From<Vec<T>> for OneOrMany<T> {
41    fn from(values: Vec<T>) -> Self {
42        OneOrMany::Many(values)
43    }
44}
45
46// Policy / Statement
47/// 条件值:文档要求 Number/Boolean/Date/IP 都用字符串包起来
48///(例如 `"10"`、`"true"`、`"2019-08-12T17:00:00+08:00"`)
49/// 用新类型方便做 From<bool/number> 等转换。
50#[derive(Debug, Clone, Serialize, PartialEq, Eq, Hash)]
51#[serde(transparent)]
52pub struct ConditionValue(pub String);
53
54impl From<String> for ConditionValue {
55    fn from(s: String) -> Self {
56        ConditionValue(s)
57    }
58}
59
60impl From<&str> for ConditionValue {
61    fn from(s: &str) -> Self {
62        ConditionValue(s.to_owned())
63    }
64}
65
66impl From<bool> for ConditionValue {
67    fn from(b: bool) -> Self {
68        ConditionValue(if b { "true".into() } else { "false".into() })
69    }
70}
71
72impl From<i64> for ConditionValue {
73    fn from(n: i64) -> Self {
74        ConditionValue(n.to_string())
75    }
76}
77
78impl From<u64> for ConditionValue {
79    fn from(n: u64) -> Self {
80        ConditionValue(n.to_string())
81    }
82}
83/// Condition 相关别名,对应:
84/// <condition_map> = { <condition_type_string> : { <condition_key_string> : <condition_value_list>, ... }, ... }
85pub type ConditionOperator = String; // 比如 "StringEquals" / "IpAddress"
86pub type ConditionKey = String; // 比如 "acs:SourceIp"
87pub type ConditionMap =
88    HashMap<ConditionOperator, HashMap<ConditionKey, OneOrMany<ConditionValue>>>;
89
90/// 条件块 Condition Block
91#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
92#[serde(transparent)]
93pub struct ConditionBlock(pub ConditionMap);
94
95impl ConditionBlock {
96    pub fn new() -> Self {
97        ConditionBlock(HashMap::new())
98    }
99
100    /// 通用插入方式:
101    /// op: "StringEquals" / "IpAddress" / ...
102    /// key: 条件键,例如 "acs:SourceIp"
103    /// values: 单值或多值
104    pub fn insert(
105        &mut self,
106        op: impl Into<String>,
107        key: impl Into<String>,
108        values: impl Into<OneOrMany<ConditionValue>>,
109    ) {
110        let op = op.into();
111        let key = key.into();
112        let entry = self.0.entry(op).or_default();
113        entry.insert(key, values.into());
114    }
115}
116
117impl Default for ConditionBlock {
118    fn default() -> Self {
119        Self::new()
120    }
121}
122
123/// Condition 运算符名字常量(避免魔法字符串)
124pub mod condition_ops {
125    // String
126    pub const STRING_EQUALS: &str = "StringEquals";
127    pub const STRING_NOT_EQUALS: &str = "StringNotEquals";
128    pub const STRING_EQUALS_IGNORE_CASE: &str = "StringEqualsIgnoreCase";
129    pub const STRING_NOT_EQUALS_IGNORE_CASE: &str = "StringNotEqualsIgnoreCase";
130    pub const STRING_LIKE: &str = "StringLike";
131    pub const STRING_NOT_LIKE: &str = "StringNotLike";
132
133    // Number
134    pub const NUMERIC_EQUALS: &str = "NumericEquals";
135    pub const NUMERIC_NOT_EQUALS: &str = "NumericNotEquals";
136    pub const NUMERIC_LESS_THAN: &str = "NumericLessThan";
137    pub const NUMERIC_LESS_THAN_EQUALS: &str = "NumericLessThanEquals";
138    pub const NUMERIC_GREATER_THAN: &str = "NumericGreaterThan";
139    pub const NUMERIC_GREATER_THAN_EQUALS: &str = "NumericGreaterThanEquals";
140
141    // Date and time
142    pub const DATE_EQUALS: &str = "DateEquals";
143    pub const DATE_NOT_EQUALS: &str = "DateNotEquals";
144    pub const DATE_LESS_THAN: &str = "DateLessThan";
145    pub const DATE_LESS_THAN_EQUALS: &str = "DateLessThanEquals";
146    pub const DATE_GREATER_THAN: &str = "DateGreaterThan";
147    pub const DATE_GREATER_THAN_EQUALS: &str = "DateGreaterThanEquals";
148
149    // Boolean
150    pub const BOOL: &str = "Bool";
151
152    // IP address
153    pub const IP_ADDRESS: &str = "IpAddress";
154    pub const NOT_IP_ADDRESS: &str = "NotIpAddress";
155    pub const IP_ADDRESS_INCLUDE_BORDER: &str = "IpAddressIncludeBorder";
156    pub const NOT_IP_ADDRESS_INCLUDE_BORDER: &str = "NotIpAddressIncludeBorder";
157}
158
159/// 单条授权语句 Statement,
160/// 对应语法:
161/// ```txt
162/// <statement> = {
163///   <effect_block>,
164///   <action_block>,
165///   <resource_block>,
166///   <condition_block?>
167/// }
168/// ```
169#[serde_with::skip_serializing_none]
170#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
171#[serde(rename_all = "PascalCase")]
172pub struct Statement {
173    pub effect: Effect,
174    /// Action / NotAction 二选一(语义层面),用 validate() 做约束。
175    pub action: Option<OneOrMany<String>>,
176    pub not_action: Option<OneOrMany<String>>,
177    pub resource: OneOrMany<String>,
178    pub condition: Option<ConditionBlock>,
179}
180
181/// 整个 Policy
182///
183/// ```txt
184/// policy = {
185///   "Version": "1",
186///   "Statement": [ <statement>, ... ]
187/// }
188/// ```
189#[derive(Builder, Debug, Clone, Serialize, PartialEq, Eq)]
190#[serde(rename_all = "PascalCase")]
191pub struct Policy {
192    #[builder(field)]
193    pub statement: Vec<Statement>,
194    #[builder(default)]
195    pub version: PolicyVersion,
196}
197
198// 校验 Action / NotAction 约束
199/// Policy 结构校验错误,只关注语法约束(Action/NotAction)
200#[derive(thiserror::Error, Debug)]
201pub enum PolicyValidationError {
202    /// 既没有 Action 也没有 NotAction
203    #[error("statement must contain either Action or NotAction")]
204    MissingActionAndNotAction,
205    /// 同时包含 Action 与 NotAction
206    #[error("statement cannot contain both Action and NotAction")]
207    BothActionAndNotActionPresent,
208}
209
210impl Statement {
211    /// 校验当前语句是否满足文档要求:
212    /// - Action / NotAction 必须二选一
213    fn validate(&self) -> Result<(), PolicyValidationError> {
214        match (&self.action, &self.not_action) {
215            (None, None) => Err(PolicyValidationError::MissingActionAndNotAction),
216            (Some(_), Some(_)) => Err(PolicyValidationError::BothActionAndNotActionPresent),
217            _ => Ok(()),
218        }
219    }
220}
221
222impl<S: policy_builder::State> PolicyBuilder<S> {
223    pub fn statement(mut self, stmt: Statement) -> Result<Self, PolicyValidationError> {
224        stmt.validate()?;
225        self.statement.push(stmt);
226        Ok(self)
227    }
228
229    pub fn statements(
230        mut self,
231        stmts: impl IntoIterator<Item = Statement>,
232    ) -> Result<Self, PolicyValidationError> {
233        for stmt in stmts.into_iter() {
234            stmt.validate()?;
235            self.statement.push(stmt);
236        }
237        Ok(self)
238    }
239}
240
241impl Policy {
242    pub fn to_json_string(&self) -> Result<String, serde_json::Error> {
243        serde_json::to_string(self)
244    }
245
246    pub fn to_json_string_pretty(&self) -> Result<String, serde_json::Error> {
247        serde_json::to_string_pretty(self)
248    }
249}
250
251#[test]
252fn build_policy_test() {
253    // Condition
254    let mut cond = ConditionBlock::new();
255    cond.insert(
256        condition_ops::STRING_EQUALS,
257        "acs:ResourceTag/team",
258        ConditionValue::from("dev"),
259    );
260    // Statement
261    let stmt = Statement {
262        effect: Effect::Allow,
263        action: Some(OneOrMany::One("ecs:*".to_string())),
264        not_action: None,
265        resource: OneOrMany::One("*".to_string()),
266        condition: Some(cond),
267    };
268    // Policy
269    let policy = Policy::builder().statement(stmt).unwrap().build();
270    println!("policy json:\n{}", policy.to_json_string_pretty().unwrap());
271
272    let mut cond = ConditionBlock::new();
273    cond.insert(
274        condition_ops::NUMERIC_LESS_THAN_EQUALS,
275        "kms:RecoveryWindowInDays",
276        ConditionValue::from(10_i64),
277    );
278    let stmt = Statement {
279        effect: Effect::Deny,
280        action: Some(OneOrMany::One("kms:DeleteSecret".to_string())),
281        not_action: None,
282        resource: OneOrMany::One("*".to_string()),
283        condition: Some(cond),
284    };
285    let policy = Policy::builder().statement(stmt).unwrap().build();
286    println!("policy json:\n{}", policy.to_json_string_pretty().unwrap());
287
288    let mut cond = ConditionBlock::new();
289    cond.insert(
290        condition_ops::DATE_LESS_THAN,
291        "acs:CurrentTime",
292        ConditionValue::from("2019-08-12T17:00:00+08:00"),
293    );
294    let stmt = Statement {
295        effect: Effect::Deny,
296        action: Some(OneOrMany::One("oss:DeleteObject".to_string())),
297        not_action: None,
298        resource: OneOrMany::One("acs:oss:*:*:mybucket/myobject".to_string()),
299        condition: Some(cond),
300    };
301    let s = Policy::builder().statement(stmt).unwrap().build();
302    println!("policy json:\n{}", s.to_json_string_pretty().unwrap());
303}