use std::fmt;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Role {
SuperAdmin,
Admin,
Editor,
Viewer,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Permission {
View,
Create,
Edit,
Delete,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize)]
pub struct PermissionSet {
pub view: bool,
pub create: bool,
pub edit: bool,
pub delete: bool,
}
impl PermissionSet {
pub const NONE: Self = Self {
view: false,
create: false,
edit: false,
delete: false,
};
pub const VIEW_ONLY: Self = Self {
view: true,
create: false,
edit: false,
delete: false,
};
pub const NO_DELETE: Self = Self {
view: true,
create: true,
edit: true,
delete: false,
};
pub const ALL: Self = Self {
view: true,
create: true,
edit: true,
delete: true,
};
pub fn allows(&self, action: Permission) -> bool {
match action {
Permission::View => self.view,
Permission::Create => self.create,
Permission::Edit => self.edit,
Permission::Delete => self.delete,
}
}
}
impl Role {
pub fn from_role_string(s: &str) -> Option<Role> {
let normalised = s.trim().to_ascii_lowercase();
match normalised.as_str() {
"superadmin" | "super_admin" | "super-admin" => Some(Role::SuperAdmin),
"admin" => Some(Role::SuperAdmin),
"restricted_admin" | "restricted-admin" => Some(Role::Admin),
"editor" => Some(Role::Editor),
"viewer" => Some(Role::Viewer),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
Role::SuperAdmin => "superadmin",
Role::Admin => "restricted_admin",
Role::Editor => "editor",
Role::Viewer => "viewer",
}
}
pub fn display_name(self) -> &'static str {
match self {
Role::SuperAdmin => "Super Admin",
Role::Admin => "Admin",
Role::Editor => "Editor",
Role::Viewer => "Viewer",
}
}
pub fn permissions_for(self, model_table: &str) -> PermissionSet {
let system = is_system_table(model_table);
match (self, system) {
(Role::SuperAdmin, _) => PermissionSet::ALL,
(Role::Admin, true) => PermissionSet::VIEW_ONLY,
(Role::Admin, false) => PermissionSet::ALL,
(Role::Editor, true) => PermissionSet::VIEW_ONLY,
(Role::Editor, false) => PermissionSet::NO_DELETE,
(Role::Viewer, _) => PermissionSet::VIEW_ONLY,
}
}
pub fn can(self, model_table: &str, action: Permission) -> bool {
self.permissions_for(model_table).allows(action)
}
}
impl fmt::Display for Role {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
pub fn is_system_table(table: &str) -> bool {
table.starts_with("rustio_")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn legacy_admin_resolves_to_super_admin() {
assert_eq!(Role::from_role_string("admin"), Some(Role::SuperAdmin));
assert_eq!(Role::from_role_string("ADMIN"), Some(Role::SuperAdmin));
}
#[test]
fn legacy_user_and_unknown_resolve_to_none() {
assert_eq!(Role::from_role_string("user"), None);
assert_eq!(Role::from_role_string(""), None);
assert_eq!(Role::from_role_string(" "), None);
assert_eq!(Role::from_role_string("nobody"), None);
}
#[test]
fn canonical_strings_round_trip() {
for role in [Role::SuperAdmin, Role::Admin, Role::Editor, Role::Viewer] {
assert_eq!(
Role::from_role_string(role.as_str()),
Some(role),
"round-trip failed for {role:?}"
);
}
}
#[test]
fn super_admin_can_do_everything_everywhere() {
let r = Role::SuperAdmin;
assert_eq!(r.permissions_for("posts"), PermissionSet::ALL);
assert_eq!(r.permissions_for("rustio_users"), PermissionSet::ALL);
assert_eq!(r.permissions_for("rustio_sessions"), PermissionSet::ALL);
}
#[test]
fn admin_cannot_write_system_tables() {
let r = Role::Admin;
assert_eq!(r.permissions_for("posts"), PermissionSet::ALL);
assert_eq!(r.permissions_for("rustio_users"), PermissionSet::VIEW_ONLY);
assert!(!r.can("rustio_users", Permission::Delete));
assert!(!r.can("rustio_users", Permission::Edit));
assert!(r.can("rustio_users", Permission::View));
}
#[test]
fn editor_cannot_delete_anywhere() {
let r = Role::Editor;
assert!(!r.can("posts", Permission::Delete));
assert!(r.can("posts", Permission::Edit));
assert!(r.can("posts", Permission::Create));
assert!(!r.can("rustio_users", Permission::Edit));
}
#[test]
fn viewer_only_views() {
let r = Role::Viewer;
assert_eq!(r.permissions_for("posts"), PermissionSet::VIEW_ONLY);
assert_eq!(r.permissions_for("rustio_users"), PermissionSet::VIEW_ONLY);
}
#[test]
fn is_system_table_matches_prefix() {
assert!(is_system_table("rustio_users"));
assert!(is_system_table("rustio_admin_actions"));
assert!(!is_system_table("posts"));
assert!(!is_system_table("my_rustio_table"));
}
#[test]
fn display_name_is_humanised() {
assert_eq!(Role::SuperAdmin.display_name(), "Super Admin");
assert_eq!(Role::Admin.display_name(), "Admin");
}
#[test]
fn permission_set_allows_matches_field() {
let ps = PermissionSet::NO_DELETE;
assert!(ps.allows(Permission::View));
assert!(ps.allows(Permission::Create));
assert!(ps.allows(Permission::Edit));
assert!(!ps.allows(Permission::Delete));
}
}