rustfs_policy/policy/
resource.rs1use 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}