bios_basic/rbum/helper/
rbum_scope_helper.rs

1//! Scope helper
2//!
3//! Scope matching rule, assuming item length is 4 :
4//!
5//! | own_paths        | scope_level | visit level 0  | visit level 1  | visit level 2  | visit level 3   |
6//! | ---------------  | ----------- | -------------  | -------------  | -------------  | --------------   |
7//! | ''               | -1          | ''             |                |                |                 |
8//! | ''               | 0           | %              | %              | %              | %               |
9//! | ''               | 1           | ''             | %              | %              | %               |
10//! | ''               | 2           | ''             |                | %              | %               |
11//! | ''               | 3           | ''             |                |                | %               |
12//! | 'AAAA'           | -1          |                | AAAA           |                |                 |
13//! | 'AAAA'           | 0           | %              | %              | %              | %               |
14//! | 'AAAA'           | 1           |                | AAAA%          | AAAA%          | AAAA%           |
15//! | 'AAAA'           | 2           |                |                | AAAA%          | AAAA%           |
16//! | 'AAAA'           | 3           |                |                |                | AAAA%           |
17//! | 'AAAA/BBBB'      | -1          |                |                | AAAA/BBBB      |                 |
18//! | 'AAAA/BBBB'      | 0           | %              | %              | %              | %               |
19//! | 'AAAA/BBBB'      | 1           |                | AAAA%          | AAAA%          | AAAA%           |
20//! | 'AAAA/BBBB'      | 2           |                |                | AAAA/BBBB%     | AAAA/BBBB%      |
21//! | 'AAAA/BBBB'      | 3           |                |                |                | AAAA/BBBB%      |
22//! | 'AAAA/BBBB/CCCC' | -1          |                |                |                | AAAA/BBBB/CCCC  |
23//! | 'AAAA/BBBB/CCCC' | 0           | %              | %              | %              | %               |
24//! | 'AAAA/BBBB/CCCC' | 1           |                | AAAA%          | AAAA%          | AAAA%           |
25//! | 'AAAA/BBBB/CCCC' | 2           |                |                | AAAA/BBBB%     | AAAA/BBBB%      |
26//! | 'AAAA/BBBB/CCCC' | 3           |                |                |                | AAAA/BBBB/CCCC% |
27//!
28use std::cmp::Ordering;
29
30use crate::rbum::dto::rbum_filer_dto::RbumBasicFilterReq;
31use crate::rbum::rbum_config::RbumConfigApi;
32use itertools::Itertools;
33use tardis::basic::dto::TardisContext;
34use tardis::basic::error::TardisError;
35use tardis::basic::result::TardisResult;
36use tardis::{TardisFuns, TardisFunsInst};
37
38use crate::rbum::rbum_enumeration::RbumScopeLevelKind;
39
40/// Get the previous path at the specified level from the own paths.
41///
42/// # Examples
43///
44/// ```
45/// use bios_basic::rbum::helper::rbum_scope_helper::get_pre_paths;
46/// assert_eq!(get_pre_paths(0, "a/b/c"), Some("".to_string()));
47/// assert_eq!(get_pre_paths(0, "a/b/c/"), Some("".to_string()));
48/// assert_eq!(get_pre_paths(1, "a/b/c"), Some("a".to_string()));
49/// assert_eq!(get_pre_paths(2, "a/b/c"), Some("a/b".to_string()));
50/// assert_eq!(get_pre_paths(3, "a/b/c"), Some("a/b/c".to_string()));
51/// assert_eq!(get_pre_paths(4, "a/b/c"), None);
52/// ```
53pub fn get_pre_paths(scope_level: i16, own_paths: &str) -> Option<String> {
54    let own_paths = own_paths.trim();
55    let own_paths = own_paths.strip_suffix('/').unwrap_or(own_paths).to_string();
56    if scope_level == 0 {
57        return Some("".to_string());
58    }
59    let split_items = if own_paths.is_empty() { vec![] } else { own_paths.split('/').collect::<Vec<_>>() };
60    match split_items.len().cmp(&(scope_level as usize)) {
61        Ordering::Less => {
62            // unmatched characters
63            None
64        }
65        _ => Some(split_items.iter().take(scope_level as usize).join("/")),
66    }
67}
68
69/// Get the path entries at the specified level from the own paths.
70///
71/// # Examples
72///
73/// ```
74/// use bios_basic::rbum::helper::rbum_scope_helper::get_path_item;
75/// assert_eq!(get_path_item(0, "a/b/c"), None);
76/// assert_eq!(get_path_item(0, "a/b/c/"), None);
77/// assert_eq!(get_path_item(1, "a/b/c"), Some("a".to_string()));
78/// assert_eq!(get_path_item(2, "a/b/c"), Some("b".to_string()));
79/// assert_eq!(get_path_item(3, "a/b/c"), Some("c".to_string()));
80/// assert_eq!(get_path_item(4, "a/b/c"), None);
81/// ```
82pub fn get_path_item(scope_level: i16, own_paths: &str) -> Option<String> {
83    let own_paths = own_paths.trim();
84    let own_paths = own_paths.strip_suffix('/').unwrap_or(own_paths).to_string();
85    if scope_level == 0 || own_paths.is_empty() {
86        return None;
87    }
88    let split_items = own_paths.split('/').collect::<Vec<&str>>();
89    if split_items.len() < scope_level as usize {
90        return None;
91    }
92    split_items.get(scope_level as usize - 1).map(|s| s.to_string())
93}
94
95/// Get the scope level from the own paths in context.
96pub fn get_scope_level_by_context(ctx: &TardisContext) -> TardisResult<RbumScopeLevelKind> {
97    let own_paths = ctx.own_paths.trim();
98    let own_paths = own_paths.strip_suffix('/').unwrap_or(own_paths).to_string();
99    if own_paths == *"" {
100        return Ok(RbumScopeLevelKind::Root);
101    }
102    RbumScopeLevelKind::from_int(own_paths.split('/').count() as i16)
103}
104
105/// Get the max level id from the own paths in the context.
106///
107/// # Examples
108/// ```
109/// use bios_basic::rbum::helper::rbum_scope_helper::get_max_level_id_by_context;
110/// use tardis::basic::dto::TardisContext;
111/// let mut ctx = TardisContext::default();
112/// ctx.own_paths = "".to_string();
113/// assert_eq!(get_max_level_id_by_context(&ctx), None);
114/// ctx.own_paths = "a".to_string();
115/// assert_eq!(get_max_level_id_by_context(&ctx), Some("a".to_string()));
116/// ctx.own_paths = "a/b/c".to_string();
117/// assert_eq!(get_max_level_id_by_context(&ctx), Some("c".to_string()));
118/// ctx.own_paths = "a/b/c/".to_string();
119/// assert_eq!(get_max_level_id_by_context(&ctx), Some("c".to_string()));
120/// ```
121pub fn get_max_level_id_by_context(ctx: &TardisContext) -> Option<String> {
122    let own_paths = ctx.own_paths.trim();
123    if own_paths.is_empty() {
124        return None;
125    }
126    let own_paths = own_paths.strip_suffix('/').unwrap_or(own_paths).to_string();
127    own_paths.split('/').collect::<Vec<&str>>().last().map(|s| s.to_string())
128}
129
130/// Downgrade of own paths.
131///
132/// The new own paths must be a subpath of the own paths in the context.
133///
134/// # Examples
135/// ```
136/// use bios_basic::rbum::helper::rbum_scope_helper::degrade_own_paths;
137/// use tardis::basic::dto::TardisContext;
138/// let mut ctx = TardisContext::default();
139/// ctx.own_paths = "a/b".to_string();
140/// assert_eq!(degrade_own_paths(ctx.clone(), "a/b/c").unwrap().own_paths, "a/b/c");
141/// ctx.own_paths = "".to_string();
142/// assert_eq!(degrade_own_paths(ctx.clone(), "a/b").unwrap().own_paths, "a/b");
143/// ctx.own_paths = "a".to_string();
144/// assert!(degrade_own_paths(ctx.clone(), "b").is_err());
145/// ctx.own_paths = "a/b".to_string();
146/// assert!(degrade_own_paths(ctx.clone(), "a/c").is_err());
147/// ```
148pub fn degrade_own_paths(mut ctx: TardisContext, new_own_paths: &str) -> TardisResult<TardisContext> {
149    if !new_own_paths.starts_with(&ctx.own_paths) {
150        return Err(TardisError::conflict("not qualified for downgrade", "409-rbum-*-downgrade-error"));
151    }
152    ctx.own_paths = new_own_paths.to_string();
153    Ok(ctx)
154}
155
156/// Check scope Legality.
157///
158/// Legality Rules:
159/// 1. Determine the ``standard_own_paths`` : When ``filter.own_paths`` is empty, use ``ctx_own_paths`` as the standard own paths, otherwise use ``filter.own_paths`` as the standard own paths.
160/// 1. If ``record_own_paths`` is equal to the ``standard_own_paths`` or if ``filter.with_sub_own_paths`` is true and ``record_own_paths`` is a sub-path of the ``standard_own_paths``, then return true.
161/// 1. If ``filter.ignore_scope`` is true, it means only ``own_paths`` comparison is required, so directly return false.
162/// 1. If ``record_scope_level`` exists, then get the prefix path of the ``standard_own_paths`` based on the value of ``record_scope_level``, and this prefix path must be the same as or a sub-path of ``record_own_paths``.
163///
164/// # Examples
165///
166/// ```
167/// use bios_basic::rbum::helper::rbum_scope_helper::check_scope;
168/// use bios_basic::rbum::dto::rbum_filer_dto::RbumBasicFilterReq;
169/// let mut filter = RbumBasicFilterReq::default();
170/// filter.ignore_scope = false;
171/// filter.with_sub_own_paths = true;
172/// filter.own_paths = None;
173/// assert!(check_scope("a/b", None, &filter, "a/b"));  // standard_own_paths == record_own_paths
174/// assert!(check_scope("a/b/c", None, &filter, "a/b"));  // record_own_paths.starts_with(standard_own_paths) == true
175/// assert!(!check_scope("a/b/c", None, &filter, "a/b/z"));  // record_own_paths.starts_with(standard_own_paths) == false
176/// assert!(!check_scope("a", None, &filter, "a/b"));  // record_own_paths.starts_with(standard_own_paths) == false
177/// // standard_sub_paths.starts_with(record_sub_paths)
178/// assert!(check_scope("a/b", Some(0), &filter, "a/b/c"));  // "".starts_with("") == true
179/// assert!(check_scope("c", Some(0), &filter, "a/b/c"));  // "".starts_with("") == true
180/// assert!(check_scope("a/b", Some(1), &filter, "a/b/c"));  // "a".starts_with("a") == true
181/// assert!(check_scope("", Some(1), &filter, "a/b/c"));  // "a".starts_with("") == true
182/// assert!(!check_scope("x/b", Some(1), &filter, "a/b/c"));  // "a".starts_with("") == false
183/// assert!(check_scope("a/b", Some(2), &filter, "a/b/c"));  // "a/b".starts_with("a/b") == true
184/// assert!(check_scope("", Some(2), &filter, "a/b/c"));  // "a/b".starts_with("") == true
185/// assert!(check_scope("a", Some(2), &filter, "a/b/c"));  // "a/b".starts_with("a") == true
186/// assert!(!check_scope("a/x", Some(2), &filter, "a/b/c"));  // "a/b".starts_with("a/x") == false
187/// assert!(check_scope("a/b/c", Some(3), &filter, "a/b/c"));  // "a/b/c".starts_with("a/b/c") == true
188/// assert!(check_scope("", Some(3), &filter, "a/b/c"));  // "a/b/c".starts_with("") == true
189/// assert!(check_scope("a", Some(3), &filter, "a/b/c"));  // "a/b/c".starts_with("a") == true
190/// assert!(check_scope("a/b", Some(3), &filter, "a/b/c"));  // "a/b/c".starts_with("a/b") == true
191/// assert!(!check_scope("a/b/x", Some(3), &filter, "a/b/c"));  // "a/b/c".starts_with("a/b/x") == false
192/// ```
193pub fn check_scope(record_own_paths: &str, record_scope_level: Option<i16>, filter: &RbumBasicFilterReq, ctx_own_paths: &str) -> bool {
194    let standard_own_paths = if let Some(own_paths) = &filter.own_paths { own_paths.as_str() } else { ctx_own_paths };
195    if record_own_paths == standard_own_paths || filter.with_sub_own_paths && record_own_paths.starts_with(standard_own_paths) {
196        return true;
197    }
198    if filter.ignore_scope {
199        return false;
200    }
201    if let Some(record_scope_level) = record_scope_level {
202        if let Some(standard_sub_paths) = get_pre_paths(record_scope_level, standard_own_paths) {
203            let record_sub_paths = if record_own_paths.len() <= standard_sub_paths.len() {
204                record_own_paths
205            } else {
206                &record_own_paths[0..standard_sub_paths.len()]
207            };
208            return standard_sub_paths.starts_with(record_sub_paths);
209        }
210    }
211    false
212}
213
214/// Fill the context.
215///
216/// This method will fetch the context from the request header (default: 'Bios-Ctx') and fill the current context.
217///
218/// Warning: This operation is unsafe, and it should only be used in scenarios where there is no security risk.
219
220fn do_unsafe_fill_ctx<F>(request: &tardis::web::poem::Request, f: F, funs: &TardisFunsInst, ctx: &mut TardisContext) -> TardisResult<()>
221where
222    F: FnOnce(TardisContext, &mut TardisContext),
223{
224    let bios_ctx = if let Some(bios_ctx) = request.header(funs.rbum_head_key_bios_ctx()).or_else(|| request.header(funs.rbum_head_key_bios_ctx().to_lowercase())) {
225        TardisFuns::json.str_to_obj::<TardisContext>(&TardisFuns::crypto.base64.decode_to_string(bios_ctx)?)?
226    } else if ctx.owner.is_empty() && ctx.ak.is_empty() && ctx.own_paths.is_empty() && ctx.roles.is_empty() && ctx.groups.is_empty() {
227        return Err(TardisError::unauthorized(
228            &format!("[Basic] Request is not legal, missing header [{}]", funs.rbum_head_key_bios_ctx()),
229            "404-rbum-req-ctx-not-exist",
230        ));
231    } else {
232        return Ok(());
233    };
234
235    if bios_ctx.own_paths.starts_with(&ctx.own_paths) {
236        f(bios_ctx, ctx);
237        Ok(())
238    } else {
239        Err(TardisError::forbidden(
240            &format!("[Basic] Request is not legal from head [{}]", funs.rbum_head_key_bios_ctx()),
241            "401-rbum-req-ctx-permission-denied",
242        ))
243    }
244}
245
246/// Check ``owner`` field of the context and fill the context.
247///
248/// When using ``ak/sk`` authentication from an internal calling interface (mostly ``ci`` type interfaces),
249/// there is ``ak`` field,
250/// so this method can be used to determine whether it comes from an internal calling interface.
251///
252/// This method will fetch the context from the request header (default: 'Bios-Ctx') and fill the current context.
253///
254/// Warning: This operation is unsafe, and it should only be used in scenarios where there is no security risk.
255
256pub fn check_without_owner_and_unsafe_fill_ctx(request: &tardis::web::poem::Request, funs: &TardisFunsInst, ctx: &mut TardisContext) -> TardisResult<()> {
257    if ctx.ak.is_empty() {
258        return Err(TardisError::forbidden("[Basic] Request contex ak is not empty", "403-rbum-req-ctx-ak-is-not-empty"));
259    }
260    unsafe_fill_ctx(request, funs, ctx)
261}
262
263/// Fill the context.
264///
265/// This method will fetch the context from the request header (default: 'Bios-Ctx') and fill the current context.
266///
267/// Warning: This operation is unsafe, and it should only be used in scenarios where there is no security risk.
268
269pub fn unsafe_fill_ctx(request: &tardis::web::poem::Request, funs: &TardisFunsInst, ctx: &mut TardisContext) -> TardisResult<()> {
270    do_unsafe_fill_ctx(
271        request,
272        |bios_ctx, ctx| {
273            let mut roles = bios_ctx.roles.clone();
274            for role in bios_ctx.roles.clone() {
275                if role.contains(':') {
276                    let extend_role = role.split(':').collect::<Vec<_>>()[0];
277                    roles.push(extend_role.to_string());
278                }
279            }
280            ctx.owner.clone_from(&bios_ctx.owner);
281            ctx.roles = roles;
282            ctx.groups = bios_ctx.groups;
283            ctx.own_paths = bios_ctx.own_paths;
284        },
285        funs,
286        ctx,
287    )
288}
289
290/// Fill the ``owner`` field of the context.
291///
292/// This method will fetch the context from the request header (default: 'Bios-Ctx') and fill the current context.
293///
294/// Warning: This operation is unsafe, and it should only be used in scenarios where there is no security risk.
295
296pub fn unsafe_fill_owner_only(request: &tardis::web::poem::Request, funs: &TardisFunsInst, ctx: &mut TardisContext) -> TardisResult<()> {
297    do_unsafe_fill_ctx(
298        request,
299        |bios_ctx, ctx| {
300            ctx.owner.clone_from(&bios_ctx.owner);
301        },
302        funs,
303        ctx,
304    )
305}
306
307/// Fill the ``own_paths`` field of the context.
308///
309/// This method will fetch the context from the request header (default: 'Bios-Ctx') and fill the current context.
310///
311/// Warning: This operation is unsafe, and it should only be used in scenarios where there is no security risk.
312
313pub fn unsafe_fill_own_paths_only(request: &tardis::web::poem::Request, funs: &TardisFunsInst, ctx: &mut TardisContext) -> TardisResult<()> {
314    do_unsafe_fill_ctx(
315        request,
316        |bios_ctx, ctx| {
317            ctx.own_paths = bios_ctx.own_paths;
318        },
319        funs,
320        ctx,
321    )
322}
323
324/// Fill the ``roles`` field of the context.
325///
326/// This method will fetch the context from the request header (default: 'Bios-Ctx') and fill the current context.
327///
328/// Warning: This operation is unsafe, and it should only be used in scenarios where there is no security risk.
329
330pub fn unsafe_fill_roles_only(request: &tardis::web::poem::Request, funs: &TardisFunsInst, ctx: &mut TardisContext) -> TardisResult<()> {
331    do_unsafe_fill_ctx(
332        request,
333        |bios_ctx, ctx| {
334            let mut roles = bios_ctx.roles.clone();
335            for role in bios_ctx.roles.clone() {
336                if role.contains(':') {
337                    let extend_role = role.split(':').collect::<Vec<_>>()[0];
338                    roles.push(extend_role.to_string());
339                }
340            }
341            ctx.roles = roles;
342        },
343        funs,
344        ctx,
345    )
346}
347
348/// Fill the ``group`` field of the context.
349///
350/// This method will fetch the context from the request header (default: 'Bios-Ctx') and fill the current context.
351///
352/// Warning: This operation is unsafe, and it should only be used in scenarios where there is no security risk.
353
354pub fn unsafe_fill_groups_only(request: &tardis::web::poem::Request, funs: &TardisFunsInst, ctx: &mut TardisContext) -> TardisResult<()> {
355    do_unsafe_fill_ctx(
356        request,
357        |bios_ctx, ctx| {
358            ctx.groups = bios_ctx.groups;
359        },
360        funs,
361        ctx,
362    )
363}