Skip to main content

rustfs_policy/policy/
resource.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 crate::error::{Error, Result};
16use serde::{Deserialize, Serialize};
17use std::{
18    collections::{HashMap, HashSet},
19    hash::Hash,
20    ops::Deref,
21};
22
23use super::{
24    Error as IamError, Validator,
25    function::key_name::KeyName,
26    utils::{path, wildcard},
27};
28
29#[derive(Serialize, Deserialize, Clone, Default, Debug)]
30pub struct ResourceSet(pub HashSet<Resource>);
31
32impl ResourceSet {
33    pub fn is_match(&self, resource: &str, conditons: &HashMap<String, Vec<String>>) -> bool {
34        for re in self.0.iter() {
35            if re.is_match(resource, conditons) {
36                return true;
37            }
38        }
39
40        false
41    }
42
43    pub fn match_resource(&self, resource: &str) -> bool {
44        for re in self.0.iter() {
45            if re.match_resource(resource) {
46                return true;
47            }
48        }
49
50        false
51    }
52}
53
54impl Deref for ResourceSet {
55    type Target = HashSet<Resource>;
56
57    fn deref(&self) -> &Self::Target {
58        &self.0
59    }
60}
61
62impl Validator for ResourceSet {
63    type Error = Error;
64    fn is_valid(&self) -> Result<()> {
65        for resource in self.0.iter() {
66            resource.is_valid()?;
67        }
68
69        Ok(())
70    }
71}
72
73impl PartialEq for ResourceSet {
74    fn eq(&self, other: &Self) -> bool {
75        self.len() == other.len() && self.0.iter().all(|x| other.0.contains(x))
76    }
77}
78
79#[derive(Hash, Eq, PartialEq, Clone, Debug)]
80pub enum Resource {
81    S3(String),
82    Kms(String),
83}
84
85impl Resource {
86    pub const S3_PREFIX: &'static str = "arn:aws:s3:::";
87
88    pub fn is_match(&self, resource: &str, conditons: &HashMap<String, Vec<String>>) -> bool {
89        let mut pattern = match self {
90            Resource::S3(s) => s.to_owned(),
91            Resource::Kms(s) => s.to_owned(),
92        };
93        if !conditons.is_empty() {
94            for key in KeyName::COMMON_KEYS {
95                if let Some(rvalue) = conditons.get(key.name()) {
96                    if matches!(rvalue.first().map(|c| !c.is_empty()), Some(true)) {
97                        pattern = pattern.replace(&key.var_name(), &rvalue[0]);
98                    }
99                }
100            }
101        }
102
103        let cp = path::clean(resource);
104        if cp != "." && cp == pattern.as_str() {
105            return true;
106        }
107
108        wildcard::is_match(pattern, resource)
109    }
110
111    pub fn match_resource(&self, resource: &str) -> bool {
112        self.is_match(resource, &HashMap::new())
113    }
114}
115
116impl TryFrom<&str> for Resource {
117    type Error = Error;
118    fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
119        let resource = if value.starts_with(Self::S3_PREFIX) {
120            Resource::S3(value.strip_prefix(Self::S3_PREFIX).unwrap().into())
121        } else {
122            return Err(IamError::InvalidResource("unknown".into(), value.into()).into());
123        };
124
125        resource.is_valid()?;
126        Ok(resource)
127    }
128}
129
130impl Validator for Resource {
131    type Error = Error;
132    fn is_valid(&self) -> std::result::Result<(), Error> {
133        match self {
134            Self::S3(pattern) => {
135                if pattern.is_empty() || pattern.starts_with('/') {
136                    return Err(IamError::InvalidResource("s3".into(), pattern.into()).into());
137                }
138            }
139            Self::Kms(pattern) => {
140                if pattern.is_empty()
141                    || pattern
142                        .char_indices()
143                        .find(|&(_, c)| c == '/' || c == '\\' || c == '.')
144                        .map(|(i, _)| i)
145                        .is_some()
146                {
147                    return Err(IamError::InvalidResource("kms".into(), pattern.into()).into());
148                }
149            }
150        }
151        Ok(())
152    }
153}
154
155impl Serialize for Resource {
156    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
157    where
158        S: serde::Serializer,
159    {
160        match self {
161            Resource::S3(s) => serializer.serialize_str(&format!("{}{}", Self::S3_PREFIX, s)),
162            Resource::Kms(s) => serializer.serialize_str(s),
163        }
164    }
165}
166
167impl<'de> Deserialize<'de> for Resource {
168    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
169    where
170        D: serde::Deserializer<'de>,
171    {
172        let value = String::deserialize(deserializer)?;
173        Resource::try_from(value.as_str()).map_err(serde::de::Error::custom)
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use crate::policy::resource::Resource;
180    use std::collections::HashMap;
181    use test_case::test_case;
182
183    #[test_case("arn:aws:s3:::*","mybucket" => true; "1")]
184    #[test_case("arn:aws:s3:::*","mybucket/myobject" => true; "2")]
185    #[test_case("arn:aws:s3:::mybucket*","mybucket" => true; "3")]
186    #[test_case("arn:aws:s3:::mybucket*","mybucket/myobject" => true; "4")]
187    #[test_case("arn:aws:s3:::*/*","mybucket/myobject"=> true; "5")]
188    #[test_case("arn:aws:s3:::mybucket/*","mybucket/myobject" => true; "6")]
189    #[test_case("arn:aws:s3:::mybucket*/myobject","mybucket/myobject" => true; "7")]
190    #[test_case("arn:aws:s3:::mybucket*/myobject","mybucket100/myobject" => true; "8")]
191    #[test_case("arn:aws:s3:::mybucket?0/2010/photos/*","mybucket20/2010/photos/1.jpg" => true; "9")]
192    #[test_case("arn:aws:s3:::mybucket","mybucket" => true; "10")]
193    #[test_case("arn:aws:s3:::mybucket?0","mybucket30" => true; "11")]
194    #[test_case("arn:aws:s3:::*/*","mybucket" => false; "12")]
195    #[test_case("arn:aws:s3:::mybucket/*","mybucket10/myobject" => false; "13")]
196    #[test_case("arn:aws:s3:::mybucket?0/2010/photos/*","mybucket0/2010/photos/1.jpg" => false; "14")]
197    #[test_case("arn:aws:s3:::mybucket","mybucket/myobject" => false; "15")]
198    fn test_resource_is_match(resource: &str, object: &str) -> bool {
199        let resource: Resource = resource.try_into().unwrap();
200        resource.is_match(object, &HashMap::new())
201    }
202}