Skip to main content

rs_tenant/
permission.rs

1use crate::error::{Error, Result};
2use std::borrow::Borrow;
3use std::fmt;
4
5const MAX_PERMISSION_PART_LEN: usize = 128;
6
7/// 修剪权限片段并转换为小写。
8fn normalize(value: &str) -> String {
9    value.trim().to_ascii_lowercase()
10}
11
12/// 校验权限资源或动作片段。
13fn validate_segment(value: &str, kind: &str, allow_slash: bool) -> Result<()> {
14    if value.is_empty() {
15        return Err(Error::InvalidPermission(format!(
16            "{kind} must not be empty"
17        )));
18    }
19    if value.len() > MAX_PERMISSION_PART_LEN {
20        return Err(Error::InvalidPermission(format!(
21            "{kind} length must be <= {MAX_PERMISSION_PART_LEN}"
22        )));
23    }
24    if value == "*" {
25        return Ok(());
26    }
27    for segment in value.split('/') {
28        if segment.is_empty() {
29            return Err(Error::InvalidPermission(format!(
30                "{kind} contains empty segment"
31            )));
32        }
33        if !allow_slash && segment != value {
34            return Err(Error::InvalidPermission(format!(
35                "{kind} must not contain '/'"
36            )));
37        }
38        if !segment
39            .chars()
40            .all(|ch| matches!(ch, 'a'..='z' | '0'..='9' | '_' | '-'))
41        {
42            return Err(Error::InvalidPermission(format!(
43                "{kind} contains invalid characters"
44            )));
45        }
46    }
47    Ok(())
48}
49
50macro_rules! define_permission_part {
51    ($(#[$doc:meta])* $name:ident, $kind:expr, $allow_slash:expr) => {
52        $(#[$doc])*
53        #[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
54        pub struct $name(String);
55
56        impl $name {
57            /// 解析、规范化并校验取值。
58            pub fn parse(value: impl AsRef<str>) -> Result<Self> {
59                let normalized = normalize(value.as_ref());
60                validate_segment(&normalized, $kind, $allow_slash)?;
61                Ok(Self(normalized))
62            }
63
64            /// 返回规范化后的取值。
65            pub fn as_str(&self) -> &str {
66                &self.0
67            }
68
69            /// 返回该片段是否为完整通配符。
70            pub fn is_wildcard(&self) -> bool {
71                self.0 == "*"
72            }
73        }
74
75        impl fmt::Display for $name {
76            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77                f.write_str(self.as_str())
78            }
79        }
80
81        impl AsRef<str> for $name {
82            fn as_ref(&self) -> &str {
83                self.as_str()
84            }
85        }
86
87        impl Borrow<str> for $name {
88            fn borrow(&self) -> &str {
89                self.as_str()
90            }
91        }
92
93        impl TryFrom<&str> for $name {
94            type Error = Error;
95
96            fn try_from(value: &str) -> Result<Self> {
97                Self::parse(value)
98            }
99        }
100
101        #[cfg(feature = "serde")]
102        impl serde::Serialize for $name {
103            fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
104            where
105                S: serde::Serializer,
106            {
107                serializer.serialize_str(self.as_str())
108            }
109        }
110
111        #[cfg(feature = "serde")]
112        impl<'de> serde::Deserialize<'de> for $name {
113            fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
114            where
115                D: serde::Deserializer<'de>,
116            {
117                let value = String::deserialize(deserializer)?;
118                Self::parse(value).map_err(serde::de::Error::custom)
119            }
120        }
121    };
122}
123
124define_permission_part!(
125    /// 权限资源,例如 `billing/invoice`。
126    Resource,
127    "resource",
128    true
129);
130define_permission_part!(
131    /// 权限动作,例如 `read`。
132    Action,
133    "action",
134    false
135);
136
137/// 规范化后的 `resource:action` 权限。
138#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
139pub struct Permission {
140    resource: Resource,
141    action: Action,
142}
143
144impl Permission {
145    /// 从已校验片段创建权限。
146    pub fn new(resource: Resource, action: Action) -> Self {
147        Self { resource, action }
148    }
149
150    /// 解析 `resource:action` 字符串。
151    pub fn parse(value: impl AsRef<str>) -> Result<Self> {
152        let normalized = normalize(value.as_ref());
153        let mut parts = normalized.split(':');
154        let resource = parts.next().unwrap_or_default();
155        let action = parts.next().ok_or_else(|| {
156            Error::InvalidPermission("permission must be in resource:action format".to_string())
157        })?;
158        if parts.next().is_some() {
159            return Err(Error::InvalidPermission(
160                "permission must contain exactly one ':' separator".to_string(),
161            ));
162        }
163        Ok(Self::new(
164            Resource::parse(resource)?,
165            Action::parse(action)?,
166        ))
167    }
168
169    /// 返回资源片段。
170    pub fn resource(&self) -> &Resource {
171        &self.resource
172    }
173
174    /// 返回动作片段。
175    pub fn action(&self) -> &Action {
176        &self.action
177    }
178
179    /// 返回该权限是否包含任意通配符片段。
180    pub fn has_wildcard(&self) -> bool {
181        self.resource.is_wildcard() || self.action.is_wildcard()
182    }
183
184    /// 返回当前授予的权限是否覆盖 `required`。
185    pub fn matches(&self, required: &Permission, enable_wildcard: bool) -> bool {
186        if !enable_wildcard && self.has_wildcard() {
187            return false;
188        }
189        if !enable_wildcard {
190            return self == required;
191        }
192        let resource_matches = self.resource.is_wildcard() || self.resource == required.resource;
193        let action_matches = self.action.is_wildcard() || self.action == required.action;
194        resource_matches && action_matches
195    }
196}
197
198impl fmt::Display for Permission {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        write!(f, "{}:{}", self.resource, self.action)
201    }
202}
203
204impl TryFrom<&str> for Permission {
205    type Error = Error;
206
207    fn try_from(value: &str) -> Result<Self> {
208        Self::parse(value)
209    }
210}
211
212#[cfg(feature = "serde")]
213impl serde::Serialize for Permission {
214    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
215    where
216        S: serde::Serializer,
217    {
218        serializer.serialize_str(&self.to_string())
219    }
220}
221
222#[cfg(feature = "serde")]
223impl<'de> serde::Deserialize<'de> for Permission {
224    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
225    where
226        D: serde::Deserializer<'de>,
227    {
228        let value = String::deserialize(deserializer)?;
229        Self::parse(value).map_err(serde::de::Error::custom)
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::{Action, MAX_PERMISSION_PART_LEN, Permission, Resource};
236    use crate::Error;
237
238    #[test]
239    fn permission_should_trim_and_lowercase() {
240        let permission = Permission::parse(" Billing/Invoice:Read ").expect("permission");
241        assert_eq!(permission.to_string(), "billing/invoice:read");
242    }
243
244    #[test]
245    fn permission_should_reject_resource_colon() {
246        let err = Permission::parse("billing:invoice:read").expect_err("must reject");
247        assert!(matches!(err, Error::InvalidPermission(_)));
248    }
249
250    #[test]
251    fn permission_should_reject_missing_separator_and_empty_segments() {
252        for value in ["invoice", "invoice:", ":read", "billing//invoice:read"] {
253            let err = Permission::parse(value).expect_err("must reject");
254            assert!(matches!(err, Error::InvalidPermission(_)));
255        }
256    }
257
258    #[test]
259    fn action_should_reject_slash_and_overlong_values() {
260        let slash_err = Action::parse("read/write").expect_err("must reject");
261        assert!(slash_err.to_string().contains("must not contain '/'"));
262
263        let oversized = "a".repeat(MAX_PERMISSION_PART_LEN + 1);
264        let length_err = Action::parse(oversized).expect_err("must reject");
265        assert!(length_err.to_string().contains("length must be"));
266    }
267
268    #[test]
269    fn wildcard_should_match_only_when_enabled() {
270        let granted = Permission::parse("invoice:*").expect("permission");
271        let required = Permission::parse("invoice:read").expect("permission");
272
273        assert!(!granted.matches(&required, false));
274        assert!(granted.matches(&required, true));
275    }
276
277    #[test]
278    fn resource_should_not_support_partial_wildcard() {
279        let err = Resource::parse("billing/*").expect_err("must reject");
280        assert!(matches!(err, Error::InvalidPermission(_)));
281    }
282
283    #[cfg(feature = "serde")]
284    #[test]
285    fn serde_should_validate_permission() {
286        let err = serde_json::from_str::<Permission>("\"billing:invoice:read\"")
287            .expect_err("must reject");
288        assert!(err.to_string().contains("exactly one"));
289    }
290}