1use bon::Builder;
6use serde::Serialize;
7use serde_json;
8use std::collections::HashMap;
9
10#[derive(Debug, Default, Clone, Serialize, PartialEq, Eq)]
12pub enum PolicyVersion {
13 #[serde(rename = "1")]
14 #[default]
15 V1,
16}
17
18#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
20pub enum Effect {
21 Allow,
22 Deny,
23}
24
25#[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#[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}
83pub type ConditionOperator = String; pub type ConditionKey = String; pub type ConditionMap =
88 HashMap<ConditionOperator, HashMap<ConditionKey, OneOrMany<ConditionValue>>>;
89
90#[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 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
123pub mod condition_ops {
125 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 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 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 pub const BOOL: &str = "Bool";
151
152 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#[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 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#[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#[derive(thiserror::Error, Debug)]
201pub enum PolicyValidationError {
202 #[error("statement must contain either Action or NotAction")]
204 MissingActionAndNotAction,
205 #[error("statement cannot contain both Action and NotAction")]
207 BothActionAndNotActionPresent,
208}
209
210impl Statement {
211 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 let mut cond = ConditionBlock::new();
255 cond.insert(
256 condition_ops::STRING_EQUALS,
257 "acs:ResourceTag/team",
258 ConditionValue::from("dev"),
259 );
260 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 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}