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}