Skip to main content

rs_tenant/
scope.rs

1use crate::error::{Error, Result};
2use crate::ids::TenantId;
3use std::borrow::Borrow;
4use std::collections::BTreeSet;
5use std::fmt;
6
7const MAX_SCOPE_PATH_LEN: usize = 256;
8
9/// 层级范围路径,例如 `agent/123/store/456`。
10#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
11pub struct ScopePath(String);
12
13impl ScopePath {
14    /// 解析并校验范围路径。
15    pub fn parse(value: impl AsRef<str>) -> Result<Self> {
16        let trimmed = value.as_ref().trim();
17        if trimmed.is_empty() {
18            return Err(Error::InvalidScope(
19                "scope path must not be empty".to_string(),
20            ));
21        }
22        if trimmed.len() > MAX_SCOPE_PATH_LEN {
23            return Err(Error::InvalidScope(format!(
24                "scope path length must be <= {MAX_SCOPE_PATH_LEN}"
25            )));
26        }
27        for segment in trimmed.split('/') {
28            if segment.is_empty() {
29                return Err(Error::InvalidScope(
30                    "scope path contains empty segment".to_string(),
31                ));
32            }
33            if !segment
34                .chars()
35                .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.'))
36            {
37                return Err(Error::InvalidScope(
38                    "scope path contains invalid characters".to_string(),
39                ));
40            }
41        }
42        Ok(Self(trimmed.to_string()))
43    }
44
45    /// 返回路径字符串。
46    pub fn as_str(&self) -> &str {
47        &self.0
48    }
49
50    /// 返回当前路径是否为 `other` 的严格祖先。
51    pub fn is_ancestor_of(&self, other: &ScopePath) -> bool {
52        self != other && other.0.starts_with(&format!("{}/", self.0))
53    }
54
55    /// 返回当前路径是否允许访问 `target`。
56    pub fn allows(&self, target: &ScopePath) -> bool {
57        self == target || self.is_ancestor_of(target)
58    }
59}
60
61impl fmt::Display for ScopePath {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        f.write_str(self.as_str())
64    }
65}
66
67impl AsRef<str> for ScopePath {
68    fn as_ref(&self) -> &str {
69        self.as_str()
70    }
71}
72
73impl Borrow<str> for ScopePath {
74    fn borrow(&self) -> &str {
75        self.as_str()
76    }
77}
78
79impl TryFrom<&str> for ScopePath {
80    type Error = Error;
81
82    fn try_from(value: &str) -> Result<Self> {
83        Self::parse(value)
84    }
85}
86
87#[cfg(feature = "serde")]
88impl serde::Serialize for ScopePath {
89    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
90    where
91        S: serde::Serializer,
92    {
93        serializer.serialize_str(self.as_str())
94    }
95}
96
97#[cfg(feature = "serde")]
98impl<'de> serde::Deserialize<'de> for ScopePath {
99    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
100    where
101        D: serde::Deserializer<'de>,
102    {
103        let value = String::deserialize(deserializer)?;
104        Self::parse(value).map_err(serde::de::Error::custom)
105    }
106}
107
108/// 单个角色分配授予的范围。
109#[derive(Clone, Debug, Eq, PartialEq, Hash)]
110pub enum GrantScope {
111    /// 租户级授权。
112    Tenant,
113    /// 以一个或多个范围路径为根的授权。
114    Paths(ScopeRoots),
115}
116
117/// 非空且已压缩的范围根路径集合。
118#[derive(Clone, Debug, Eq, PartialEq, Hash)]
119pub struct ScopeRoots {
120    roots: Vec<ScopePath>,
121}
122
123impl ScopeRoots {
124    /// 创建已校验的范围根路径集合。
125    pub fn new(roots: Vec<ScopePath>) -> Result<Self> {
126        if roots.is_empty() {
127            return Err(Error::InvalidScope(
128                "grant scope paths must not be empty".to_string(),
129            ));
130        }
131        Ok(Self {
132            roots: compact_paths(roots),
133        })
134    }
135
136    /// 返回压缩后的根路径。
137    pub fn as_slice(&self) -> &[ScopePath] {
138        &self.roots
139    }
140
141    /// 消耗包装类型并返回根路径。
142    pub fn into_vec(self) -> Vec<ScopePath> {
143        self.roots
144    }
145}
146
147impl GrantScope {
148    /// 创建租户级授权范围。
149    pub fn tenant() -> Self {
150        Self::Tenant
151    }
152
153    /// 创建路径级授权范围。
154    pub fn paths(roots: Vec<ScopePath>) -> Result<Self> {
155        ScopeRoots::new(roots).map(Self::Paths)
156    }
157
158    /// 返回该授权是否覆盖整个租户。
159    pub fn is_tenant(&self) -> bool {
160        matches!(self, Self::Tenant)
161    }
162
163    /// 返回路径级授权的根路径。
164    pub fn roots(&self) -> &[ScopePath] {
165        match self {
166            Self::Tenant => &[],
167            Self::Paths(roots) => roots.as_slice(),
168        }
169    }
170}
171
172#[cfg(feature = "serde")]
173impl serde::Serialize for ScopeRoots {
174    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
175    where
176        S: serde::Serializer,
177    {
178        self.roots.serialize(serializer)
179    }
180}
181
182#[cfg(feature = "serde")]
183impl<'de> serde::Deserialize<'de> for ScopeRoots {
184    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
185    where
186        D: serde::Deserializer<'de>,
187    {
188        let roots = Vec::<ScopePath>::deserialize(deserializer)?;
189        Self::new(roots).map_err(serde::de::Error::custom)
190    }
191}
192
193#[cfg(feature = "serde")]
194impl serde::Serialize for GrantScope {
195    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
196    where
197        S: serde::Serializer,
198    {
199        #[derive(serde::Serialize)]
200        #[serde(tag = "type", rename_all = "snake_case")]
201        enum GrantScopeWire<'a> {
202            Tenant,
203            Paths { roots: &'a [ScopePath] },
204        }
205        match self {
206            Self::Tenant => GrantScopeWire::Tenant.serialize(serializer),
207            Self::Paths(roots) => GrantScopeWire::Paths {
208                roots: roots.as_slice(),
209            }
210            .serialize(serializer),
211        }
212    }
213}
214
215#[cfg(feature = "serde")]
216impl<'de> serde::Deserialize<'de> for GrantScope {
217    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
218    where
219        D: serde::Deserializer<'de>,
220    {
221        #[derive(serde::Deserialize)]
222        #[serde(tag = "type", rename_all = "snake_case")]
223        enum GrantScopeWire {
224            Tenant,
225            Paths { roots: Vec<ScopePath> },
226        }
227        match GrantScopeWire::deserialize(deserializer)? {
228            GrantScopeWire::Tenant => Ok(Self::Tenant),
229            GrantScopeWire::Paths { roots } => Self::paths(roots).map_err(serde::de::Error::custom),
230        }
231    }
232}
233
234/// 针对具体权限查询合并后的访问范围。
235#[derive(Clone, Debug, Eq, PartialEq)]
236pub enum AccessScope {
237    /// 没有匹配的访问权。
238    None,
239    /// 租户级访问权。
240    Tenant { tenant: TenantId },
241    /// 基于路径根的访问权。
242    Paths {
243        /// 查询所属租户。
244        tenant: TenantId,
245        /// 覆盖所有允许后代路径的压缩根路径。
246        roots: Vec<ScopePath>,
247    },
248}
249
250impl AccessScope {
251    /// 将授权范围合并为最终访问范围。
252    pub fn merge(tenant: TenantId, grants: impl IntoIterator<Item = GrantScope>) -> Self {
253        let mut roots = Vec::new();
254        for grant in grants {
255            match grant {
256                GrantScope::Tenant => return Self::Tenant { tenant },
257                GrantScope::Paths(grant_roots) => roots.extend(grant_roots.into_vec()),
258            }
259        }
260        if roots.is_empty() {
261            Self::None
262        } else {
263            Self::Paths {
264                tenant,
265                roots: compact_paths(roots),
266            }
267        }
268    }
269
270    /// 返回该范围是否允许访问目标路径。
271    pub fn allows_path(&self, target: &ScopePath) -> bool {
272        match self {
273            Self::None => false,
274            Self::Tenant { .. } => true,
275            Self::Paths { roots, .. } => roots.iter().any(|root| root.allows(target)),
276        }
277    }
278}
279
280/// 对根路径去重,并删除已被祖先路径覆盖的子路径。
281fn compact_paths(roots: Vec<ScopePath>) -> Vec<ScopePath> {
282    let ordered: BTreeSet<_> = roots.into_iter().collect();
283    let mut compacted: Vec<ScopePath> = Vec::new();
284    for path in ordered {
285        if compacted.iter().any(|root| root.allows(&path)) {
286            continue;
287        }
288        compacted.push(path);
289    }
290    compacted
291}
292
293#[cfg(test)]
294mod tests {
295    use super::{AccessScope, GrantScope, MAX_SCOPE_PATH_LEN, ScopePath};
296    use crate::TenantId;
297
298    #[test]
299    fn scope_path_should_allow_descendant() {
300        let root = ScopePath::parse("agent/123").expect("scope path");
301        let target = ScopePath::parse("agent/123/store/456").expect("scope path");
302
303        assert!(root.allows(&target));
304        assert!(!target.allows(&root));
305    }
306
307    #[test]
308    fn scope_path_should_not_allow_prefix_sibling() {
309        let root = ScopePath::parse("agent/1").expect("scope path");
310        let sibling = ScopePath::parse("agent/10/store/456").expect("scope path");
311
312        assert!(!root.allows(&sibling));
313    }
314
315    #[test]
316    fn scope_path_should_reject_invalid_boundaries() {
317        for value in ["", "agent//1", "agent/中文"] {
318            let err = ScopePath::parse(value).expect_err("must reject");
319            assert!(err.to_string().contains("scope path"));
320        }
321
322        let oversized = "a".repeat(MAX_SCOPE_PATH_LEN + 1);
323        let err = ScopePath::parse(oversized).expect_err("must reject");
324        assert!(err.to_string().contains("length must be"));
325    }
326
327    #[test]
328    fn grant_scope_paths_should_reject_empty_roots() {
329        let err = GrantScope::paths(Vec::new()).expect_err("must reject");
330        assert!(err.to_string().contains("must not be empty"));
331    }
332
333    #[test]
334    fn access_scope_should_compact_child_paths() {
335        let tenant = TenantId::parse("tenant_1").expect("tenant");
336        let parent = ScopePath::parse("agent/123").expect("scope path");
337        let child = ScopePath::parse("agent/123/store/456").expect("scope path");
338
339        let scope = AccessScope::merge(
340            tenant,
341            [GrantScope::paths(vec![child, parent]).expect("grant scope")],
342        );
343
344        let AccessScope::Paths { roots, .. } = scope else {
345            panic!("expected paths");
346        };
347        assert_eq!(roots.len(), 1);
348        assert_eq!(roots[0].as_str(), "agent/123");
349    }
350
351    #[cfg(feature = "serde")]
352    #[test]
353    fn serde_should_reject_empty_grant_paths() {
354        let err = serde_json::from_str::<GrantScope>(r#"{"type":"paths","roots":[]}"#)
355            .expect_err("must reject");
356        assert!(err.to_string().contains("must not be empty"));
357    }
358}