Skip to main content

rustfs_policy/policy/
policy.rs

1// Copyright 2024 RustFS Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use super::{Effect, Error as IamError, ID, Statement, action::Action, statement::BPStatement};
16use crate::error::{Error, Result};
17use serde::{Deserialize, Serialize};
18use serde_json::Value;
19use std::collections::{HashMap, HashSet};
20
21/// DEFAULT_VERSION is the default version.
22/// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_version.html
23pub const DEFAULT_VERSION: &str = "2012-10-17";
24
25/// check the data is Validator
26pub trait Validator {
27    type Error;
28    fn is_valid(&self) -> Result<()> {
29        Ok(())
30    }
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct Args<'a> {
35    pub account: &'a str,
36    pub groups: &'a Option<Vec<String>>,
37    pub action: Action,
38    pub bucket: &'a str,
39    pub conditions: &'a HashMap<String, Vec<String>>,
40    pub is_owner: bool,
41    pub object: &'a str,
42    pub claims: &'a HashMap<String, Value>,
43    pub deny_only: bool,
44}
45
46impl Args<'_> {
47    pub fn get_role_arn(&self) -> Option<&str> {
48        self.claims.get("roleArn").and_then(|x| x.as_str())
49    }
50    pub fn get_policies(&self, policy_claim_name: &str) -> (HashSet<String>, bool) {
51        get_policies_from_claims(self.claims, policy_claim_name)
52    }
53}
54
55#[derive(Serialize, Deserialize, Clone, Default, Debug)]
56pub struct Policy {
57    #[serde(default, rename = "ID")]
58    pub id: ID,
59    #[serde(rename = "Version")]
60    pub version: String,
61    #[serde(rename = "Statement")]
62    pub statements: Vec<Statement>,
63}
64
65impl Policy {
66    pub fn is_allowed(&self, args: &Args) -> bool {
67        for statement in self.statements.iter().filter(|s| matches!(s.effect, Effect::Deny)) {
68            if !statement.is_allowed(args) {
69                return false;
70            }
71        }
72
73        if args.deny_only || args.is_owner {
74            return true;
75        }
76
77        for statement in self.statements.iter().filter(|s| matches!(s.effect, Effect::Allow)) {
78            if statement.is_allowed(args) {
79                return true;
80            }
81        }
82
83        false
84    }
85
86    pub fn match_resource(&self, resource: &str) -> bool {
87        for statement in self.statements.iter() {
88            if statement.resources.match_resource(resource) {
89                return true;
90            }
91        }
92        false
93    }
94
95    fn drop_duplicate_statements(&mut self) {
96        let mut dups = HashSet::new();
97        for i in 0..self.statements.len() {
98            if dups.contains(&i) {
99                // i is already a duplicate of some statement, so we do not need to
100                // compare with it.
101                continue;
102            }
103            for j in (i + 1)..self.statements.len() {
104                if !self.statements[i].eq(&self.statements[j]) {
105                    continue;
106                }
107
108                // save duplicate statement index for removal.
109                dups.insert(j);
110            }
111        }
112
113        // remove duplicate items from the slice.
114        let mut c = 0;
115        for i in 0..self.statements.len() {
116            if dups.contains(&i) {
117                continue;
118            }
119            self.statements[c] = self.statements[i].clone();
120            c += 1;
121        }
122        self.statements.truncate(c);
123    }
124    pub fn merge_policies(inputs: Vec<Policy>) -> Policy {
125        let mut merged = Policy::default();
126
127        for p in inputs {
128            if merged.version.is_empty() {
129                merged.version = p.version.clone();
130            }
131            for st in p.statements {
132                merged.statements.push(st.clone());
133            }
134        }
135        merged.drop_duplicate_statements();
136        merged
137    }
138
139    pub fn is_empty(&self) -> bool {
140        self.statements.is_empty()
141    }
142
143    pub fn validate(&self) -> Result<()> {
144        self.is_valid()
145    }
146
147    pub fn parse_config(data: &[u8]) -> Result<Policy> {
148        let policy: Policy = serde_json::from_slice(data)?;
149        policy.validate()?;
150        Ok(policy)
151    }
152}
153
154impl Validator for Policy {
155    type Error = Error;
156
157    fn is_valid(&self) -> Result<()> {
158        if !self.id.is_empty() && !self.id.eq(DEFAULT_VERSION) {
159            return Err(IamError::InvalidVersion(self.id.0.clone()).into());
160        }
161
162        for statement in self.statements.iter() {
163            statement.is_valid()?;
164        }
165
166        Ok(())
167    }
168}
169
170#[derive(Debug, Clone, PartialEq, Eq)]
171pub struct BucketPolicyArgs<'a> {
172    pub account: &'a str,
173    pub groups: &'a Option<Vec<String>>,
174    pub action: Action,
175    pub bucket: &'a str,
176    pub conditions: &'a HashMap<String, Vec<String>>,
177    pub is_owner: bool,
178    pub object: &'a str,
179}
180
181#[derive(Serialize, Deserialize, Clone, Default, Debug)]
182pub struct BucketPolicy {
183    #[serde(default, rename = "ID")]
184    pub id: ID,
185    #[serde(rename = "Version")]
186    pub version: String,
187    #[serde(rename = "Statement")]
188    pub statements: Vec<BPStatement>,
189}
190
191impl BucketPolicy {
192    pub fn is_allowed(&self, args: &BucketPolicyArgs) -> bool {
193        for statement in self.statements.iter().filter(|s| matches!(s.effect, Effect::Deny)) {
194            if !statement.is_allowed(args) {
195                return false;
196            }
197        }
198
199        if args.is_owner {
200            return true;
201        }
202
203        for statement in self.statements.iter().filter(|s| matches!(s.effect, Effect::Allow)) {
204            if statement.is_allowed(args) {
205                return true;
206            }
207        }
208
209        false
210    }
211}
212
213impl Validator for BucketPolicy {
214    type Error = Error;
215
216    fn is_valid(&self) -> Result<()> {
217        if !self.id.is_empty() && !self.id.eq(DEFAULT_VERSION) {
218            return Err(IamError::InvalidVersion(self.id.0.clone()).into());
219        }
220
221        for statement in self.statements.iter() {
222            statement.is_valid()?;
223        }
224
225        Ok(())
226    }
227}
228
229fn get_values_from_claims(claims: &HashMap<String, Value>, claim_name: &str) -> (HashSet<String>, bool) {
230    let mut s = HashSet::new();
231    if let Some(pname) = claims.get(claim_name) {
232        if let Some(pnames) = pname.as_array() {
233            for pname in pnames {
234                if let Some(pname_str) = pname.as_str() {
235                    for pname in pname_str.split(',') {
236                        let pname = pname.trim();
237                        if !pname.is_empty() {
238                            s.insert(pname.to_string());
239                        }
240                    }
241                }
242            }
243            return (s, true);
244        } else if let Some(pname_str) = pname.as_str() {
245            for pname in pname_str.split(',') {
246                let pname = pname.trim();
247                if !pname.is_empty() {
248                    s.insert(pname.to_string());
249                }
250            }
251            return (s, true);
252        }
253    }
254    (s, false)
255}
256
257pub fn get_policies_from_claims(claims: &HashMap<String, Value>, policy_claim_name: &str) -> (HashSet<String>, bool) {
258    get_values_from_claims(claims, policy_claim_name)
259}
260
261pub fn iam_policy_claim_name_sa() -> String {
262    "sa-policy".to_string()
263}
264
265pub mod default {
266    use std::{collections::HashSet, sync::LazyLock};
267
268    use crate::policy::{
269        ActionSet, DEFAULT_VERSION, Effect, Functions, ResourceSet, Statement,
270        action::{Action, AdminAction, KmsAction, S3Action},
271        resource::Resource,
272    };
273
274    use super::Policy;
275
276    #[allow(clippy::incompatible_msrv)]
277    pub static DEFAULT_POLICIES: LazyLock<[(&'static str, Policy); 6]> = LazyLock::new(|| {
278        [
279            (
280                "readwrite",
281                Policy {
282                    id: "".into(),
283                    version: DEFAULT_VERSION.into(),
284                    statements: vec![Statement {
285                        sid: "".into(),
286                        effect: Effect::Allow,
287                        actions: ActionSet({
288                            let mut hash_set = HashSet::new();
289                            hash_set.insert(Action::S3Action(S3Action::AllActions));
290                            hash_set
291                        }),
292                        not_actions: ActionSet(Default::default()),
293                        resources: ResourceSet({
294                            let mut hash_set = HashSet::new();
295                            hash_set.insert(Resource::S3("*".into()));
296                            hash_set
297                        }),
298                        conditions: Functions::default(),
299                        ..Default::default()
300                    }],
301                },
302            ),
303            (
304                "readonly",
305                Policy {
306                    id: "".into(),
307                    version: DEFAULT_VERSION.into(),
308                    statements: vec![Statement {
309                        sid: "".into(),
310                        effect: Effect::Allow,
311                        actions: ActionSet({
312                            let mut hash_set = HashSet::new();
313                            hash_set.insert(Action::S3Action(S3Action::GetBucketLocationAction));
314                            hash_set.insert(Action::S3Action(S3Action::GetObjectAction));
315                            hash_set
316                        }),
317                        not_actions: ActionSet(Default::default()),
318                        resources: ResourceSet({
319                            let mut hash_set = HashSet::new();
320                            hash_set.insert(Resource::S3("*".into()));
321                            hash_set
322                        }),
323                        conditions: Functions::default(),
324                        ..Default::default()
325                    }],
326                },
327            ),
328            (
329                "writeonly",
330                Policy {
331                    id: "".into(),
332                    version: DEFAULT_VERSION.into(),
333                    statements: vec![Statement {
334                        sid: "".into(),
335                        effect: Effect::Allow,
336                        actions: ActionSet({
337                            let mut hash_set = HashSet::new();
338                            hash_set.insert(Action::S3Action(S3Action::PutObjectAction));
339                            hash_set
340                        }),
341                        not_actions: ActionSet(Default::default()),
342                        resources: ResourceSet({
343                            let mut hash_set = HashSet::new();
344                            hash_set.insert(Resource::S3("*".into()));
345                            hash_set
346                        }),
347                        conditions: Functions::default(),
348                        ..Default::default()
349                    }],
350                },
351            ),
352            (
353                "writeonly",
354                Policy {
355                    id: "".into(),
356                    version: DEFAULT_VERSION.into(),
357                    statements: vec![Statement {
358                        sid: "".into(),
359                        effect: Effect::Allow,
360                        actions: ActionSet({
361                            let mut hash_set = HashSet::new();
362                            hash_set.insert(Action::S3Action(S3Action::PutObjectAction));
363                            hash_set
364                        }),
365                        not_actions: ActionSet(Default::default()),
366                        resources: ResourceSet({
367                            let mut hash_set = HashSet::new();
368                            hash_set.insert(Resource::S3("*".into()));
369                            hash_set
370                        }),
371                        conditions: Functions::default(),
372                        ..Default::default()
373                    }],
374                },
375            ),
376            (
377                "diagnostics",
378                Policy {
379                    id: "".into(),
380                    version: DEFAULT_VERSION.into(),
381                    statements: vec![Statement {
382                        sid: "".into(),
383                        effect: Effect::Allow,
384                        actions: ActionSet({
385                            let mut hash_set = HashSet::new();
386                            hash_set.insert(Action::AdminAction(AdminAction::ProfilingAdminAction));
387                            hash_set.insert(Action::AdminAction(AdminAction::TraceAdminAction));
388                            hash_set.insert(Action::AdminAction(AdminAction::ConsoleLogAdminAction));
389                            hash_set.insert(Action::AdminAction(AdminAction::ServerInfoAdminAction));
390                            hash_set.insert(Action::AdminAction(AdminAction::TopLocksAdminAction));
391                            hash_set.insert(Action::AdminAction(AdminAction::HealthInfoAdminAction));
392                            hash_set.insert(Action::AdminAction(AdminAction::PrometheusAdminAction));
393                            hash_set.insert(Action::AdminAction(AdminAction::BandwidthMonitorAction));
394                            hash_set
395                        }),
396                        not_actions: ActionSet(Default::default()),
397                        resources: ResourceSet({
398                            let mut hash_set = HashSet::new();
399                            hash_set.insert(Resource::S3("*".into()));
400                            hash_set
401                        }),
402                        conditions: Functions::default(),
403                        ..Default::default()
404                    }],
405                },
406            ),
407            (
408                "consoleAdmin",
409                Policy {
410                    id: "".into(),
411                    version: DEFAULT_VERSION.into(),
412                    statements: vec![
413                        Statement {
414                            sid: "".into(),
415                            effect: Effect::Allow,
416                            actions: ActionSet({
417                                let mut hash_set = HashSet::new();
418                                hash_set.insert(Action::AdminAction(AdminAction::AllAdminActions));
419                                hash_set
420                            }),
421                            not_actions: ActionSet(Default::default()),
422                            resources: ResourceSet(HashSet::new()),
423                            conditions: Functions::default(),
424                            ..Default::default()
425                        },
426                        Statement {
427                            sid: "".into(),
428                            effect: Effect::Allow,
429                            actions: ActionSet({
430                                let mut hash_set = HashSet::new();
431                                hash_set.insert(Action::KmsAction(KmsAction::AllActions));
432                                hash_set
433                            }),
434                            not_actions: ActionSet(Default::default()),
435                            resources: ResourceSet(HashSet::new()),
436                            conditions: Functions::default(),
437                            ..Default::default()
438                        },
439                        Statement {
440                            sid: "".into(),
441                            effect: Effect::Allow,
442                            actions: ActionSet({
443                                let mut hash_set = HashSet::new();
444                                hash_set.insert(Action::S3Action(S3Action::AllActions));
445                                hash_set
446                            }),
447                            not_actions: ActionSet(Default::default()),
448                            resources: ResourceSet({
449                                let mut hash_set = HashSet::new();
450                                hash_set.insert(Resource::S3("*".into()));
451                                hash_set
452                            }),
453                            conditions: Functions::default(),
454                            ..Default::default()
455                        },
456                    ],
457                },
458            ),
459        ]
460    });
461}
462
463#[cfg(test)]
464mod test {
465    use super::*;
466    use crate::error::Result;
467
468    #[tokio::test]
469    async fn test_parse_policy() -> Result<()> {
470        let data = r#"
471{
472  "Version": "2012-10-17",
473  "Statement": [
474    {
475      "Effect": "Allow",
476      "Action": ["s3:GetObject"],
477      "Resource": ["arn:aws:s3:::dada/*"],
478      "Condition": {
479        "StringEquals": {
480          "s3:ExistingObjectTag/security": "public"
481        }
482      }
483    },
484    {
485      "Effect": "Allow",
486      "Action": ["s3:DeleteObjectTagging"],
487      "Resource": ["arn:aws:s3:::dada/*"],
488      "Condition": {
489        "StringEquals": {
490          "s3:ExistingObjectTag/security": "public"
491        }
492      }
493    },
494    {
495      "Effect": "Allow",
496      "Action": ["s3:DeleteObject"],
497      "Resource": ["arn:aws:s3:::dada/*"]
498    },
499    {
500      "Effect": "Allow",
501      "Action": [
502        "s3:PutObject"
503      ],
504      "Resource": [
505        "arn:aws:s3:::dada/*"
506      ],
507      "Condition": {
508        "ForAllValues:StringLike": {
509          "s3:RequestObjectTagKeys": [
510            "security",
511            "virus"
512          ]
513        }
514      }
515    }
516  ]
517}
518"#;
519
520        let p = Policy::parse_config(data.as_bytes())?;
521
522        // println!("{:?}", p);
523
524        let str = serde_json::to_string(&p)?;
525
526        // println!("----- {}", str);
527
528        let _p2 = Policy::parse_config(str.as_bytes())?;
529        // println!("33{:?}", p2);
530
531        // assert_eq!(p, p2);
532        Ok(())
533    }
534}