Walk the inheritance chain starting from `start_name` upward through the given `roles` map. Returns the chain length (number of hops including `start_name` itself). If the chain is a cycle or exceeds `MAX_ROLE_INHERITANCE_DEPTH`, returns the corresponding error. This is the single authoritative write-time check — both `create_role` and `prepare_role` call it so the guarantee holds for every catal
(
proposed_child: &str,
proposed_parent: &str,
roles: &HashMap<String, CustomRole>,
)
| 334 | /// This is the single authoritative write-time check — both `create_role` and |
| 335 | /// `prepare_role` call it so the guarantee holds for every catalog mutation path. |
| 336 | fn check_inheritance_chain( |
| 337 | proposed_child: &str, |
| 338 | proposed_parent: &str, |
| 339 | roles: &HashMap<String, CustomRole>, |
| 340 | ) -> crate::Result<()> { |
| 341 | // Walk upward from the proposed parent. If we encounter `proposed_child` |
| 342 | // we have a cycle. If we exceed MAX_ROLE_INHERITANCE_DEPTH hops we stop. |
| 343 | let mut current = proposed_parent; |
| 344 | // depth counts the total chain: proposed_child (1) + proposed_parent (2) + ancestors. |
| 345 | let mut depth: usize = 2; |
| 346 | |
| 347 | loop { |
| 348 | if current == proposed_child { |
| 349 | return Err(crate::Error::RoleInheritanceCycle { |
| 350 | child: proposed_child.to_string(), |
| 351 | parent: proposed_parent.to_string(), |
| 352 | }); |
| 353 | } |
| 354 | if depth > MAX_ROLE_INHERITANCE_DEPTH { |
| 355 | return Err(crate::Error::RoleInheritanceDepthExceeded { |
| 356 | depth, |
| 357 | limit: MAX_ROLE_INHERITANCE_DEPTH, |
| 358 | }); |
| 359 | } |
| 360 | // Built-in roles have no further parents; chain ends here. |
| 361 | if is_builtin(current) { |
| 362 | break; |
| 363 | } |
| 364 | match roles.get(current) { |
| 365 | Some(role) => match &role.parent { |
| 366 | Some(parent_name) => { |
| 367 | current = parent_name.as_str(); |
| 368 | depth += 1; |
| 369 | } |
| 370 | None => break, |
| 371 | }, |
| 372 | None => break, |
| 373 | } |
| 374 | } |
| 375 | Ok(()) |
| 376 | } |
| 377 | |
| 378 | /// Validate that `parent_name` refers to an existing role (built-in or |
| 379 | /// custom) and that adopting it as `child_name`'s parent does not create a |