use crate::{ {i18n::Locale, tr}, pages::permissions::common::state::Tabs, ui::Nav, }; #[derive(Clone, Debug, Default, serde::Deserialize)] pub(crate) struct Selection { #[serde(default)] pub updated: bool, } #[derive(Clone, Debug, Default, serde::Deserialize)] pub(crate) struct CreateRoleForm { pub name: String, #[serde(default)] pub parent: String, /// Which starter access the new role gets: `none`, `read`, or `full`. #[serde(default)] pub access: String, } #[derive(Clone, Debug, Default, serde::Deserialize)] pub(crate) struct RemoveRoleForm { pub role: String, } pub(crate) struct RolesPage { pub nav: Nav, pub tabs: Tabs, pub roles: Vec, /// Roles a new role may inherit from. pub parents: Vec, pub updated: bool, } pub(crate) struct RoleRow { pub name: String, pub kind: String, pub parent: String, pub built_in: bool, /// How many users hold the role. `None` when the caller does not manage /// users and so never saw the list. pub users: Option, /// Whether the caller may open this role's access, which is also what makes /// it removable. pub editable: bool, } impl RoleRow { pub(crate) fn removable(&self) -> bool { self.editable && !self.built_in && self.users.is_none_or(|count| count == 0) } /// Why the delete button is missing, so an undeletable role says so instead /// of showing nothing. pub(crate) fn keeps_reason(&self, locale: &Locale) -> String { if !self.editable { tr!(*locale, "perm-role-reason-outranks-you") } else if self.built_in { tr!(*locale, "perm-role-reason-built-in") } else if self.users.is_some_and(|count| count > 0) { tr!(*locale, "perm-role-reason-still-assigned") } else { String::new() } } } /// The grants a starter-access choice hands the new role. /// /// These are wildcard objects on purpose: they keep covering profiles and /// tables added later, which is what "everything" has to mean for a role /// created before the data exists. ECB rates are written by the server, so they /// are readable and nothing more. pub(crate) fn starter_grants( locale: Locale, access: &str, ) -> Result, String> { match access { "" | "none" => Ok(Vec::new()), "read" => Ok(vec![ ("data:*", "read"), ("journal:*", "read"), ("ecb:*", "read"), ]), "full" => { let mut grants = vec![("ecb:*", "read")]; for object in ["data:*", "journal:*"] { for action in ["read", "insert", "update", "delete"] { grants.push((object, action)); } } // Bulk transfer applies to tables, not to journals, which the // transfer pages never offer. grants.push(("data:*", "export")); grants.push(("data:*", "import")); Ok(grants) } other => Err(tr!( locale, "perm-err-bad-starter-access", "value" => other.to_string(), )), } } #[cfg(test)] mod tests { use super::*; #[test] fn starter_access_never_hands_out_a_write_on_ecb_rates() { let locale = Locale::default(); assert!(starter_grants(locale, "none").unwrap().is_empty()); assert!(starter_grants(locale, "nonsense").is_err()); for level in ["read", "full"] { let grants = starter_grants(locale, level).unwrap(); assert!( grants .iter() .all(|(object, action)| *object != "ecb:*" || *action == "read"), "{level} granted a write on ECB rates" ); } assert_eq!(starter_grants(locale, "read").unwrap().len(), 3); // Four row actions on two wildcard objects, ECB read, and the two // transfer actions, which apply to tables alone. assert_eq!(starter_grants(locale, "full").unwrap().len(), 11); } #[test] fn a_role_is_only_removable_once_nobody_holds_it() { let row = |built_in, users, editable| RoleRow { name: "sales".to_string(), kind: "data".to_string(), parent: String::new(), built_in, users, editable, }; assert!(row(false, Some(0), true).removable()); assert!(!row(false, Some(2), true).removable()); assert!(!row(true, Some(0), true).removable()); assert!(!row(false, Some(0), false).removable()); assert_eq!( row(false, Some(2), true).keeps_reason(&Locale::default()), "still assigned" ); } }