Files
komp_ac/web/src/pages/permissions/roles/state.rs
2026-08-15 19:57:09 +02:00

155 lines
4.7 KiB
Rust

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<RoleRow>,
/// Roles a new role may inherit from.
pub parents: Vec<String>,
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<usize>,
/// 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<Vec<(&'static str, &'static str)>, 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"
);
}
}