web permissions2

This commit is contained in:
Priec
2026-08-11 14:33:24 +02:00
parent 5602140d05
commit 077d69d756
47 changed files with 2348 additions and 674 deletions

View File

@@ -0,0 +1,133 @@
use crate::{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) -> &'static str {
if !self.editable {
"outranks you"
} else if self.built_in {
"built in"
} else if self.users.is_some_and(|count| count > 0) {
"still assigned"
} else {
""
}
}
}
/// 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(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));
}
}
Ok(grants)
}
other => Err(format!("'{other}' is not a starter access level.")),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn starter_access_never_hands_out_a_write_on_ecb_rates() {
assert!(starter_grants("none").unwrap().is_empty());
assert!(starter_grants("nonsense").is_err());
for level in ["read", "full"] {
let grants = starter_grants(level).unwrap();
assert!(
grants
.iter()
.all(|(object, action)| *object != "ecb:*" || *action == "read"),
"{level} granted a write on ECB rates"
);
}
assert_eq!(starter_grants("read").unwrap().len(), 3);
assert_eq!(starter_grants("full").unwrap().len(), 9);
}
#[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(), "still assigned");
}
}