Skip to main content

adminx_rbac/
ability.rs

1// adminx-rbac/src/ability.rs
2//
3// The "central ability block": a code-level way to declare a role's grants,
4// used to SEED the database on first boot. Once the permission table has rows
5// the database is authoritative and admins edit it in the panel — so this block
6// is a starting point, not a running config. That reconciles "define policy in
7// code" with "edit policy at runtime without a redeploy".
8
9use serde_json::{Map, Value};
10
11/// Wildcard resource: a grant on `"*"` applies to every resource.
12pub const ANY_RESOURCE: &str = "*";
13/// Wildcard action: a grant of `"manage"` applies to every action on its resource.
14pub const MANAGE: &str = "manage";
15
16/// Grants for a single role. Build with [`Ability::role`] then chain `can*`.
17///
18/// ```ignore
19/// Ability::role("editor")
20///     .can("update", "posts")
21///     .can("publish", "posts")   // a custom action, by name
22///     .can_manage("comments");   // every action on comments
23/// ```
24#[derive(Debug, Clone)]
25pub struct Ability {
26    role: String,
27    /// `(resource, action)` pairs. `resource` may be `"*"`, `action` may be `"manage"`.
28    grants: Vec<(String, String)>,
29}
30
31impl Ability {
32    /// Begin a grant set for `role` (the string stored in the admin user's
33    /// `role` column and carried in the JWT).
34    pub fn role(name: impl Into<String>) -> Self {
35        Self {
36            role: name.into(),
37            grants: Vec::new(),
38        }
39    }
40
41    /// Allow `action` on `resource`. `action` is a built-in token
42    /// (`list`/`read`/`create`/`update`/`delete`/`export`) or a custom action's
43    /// name; `resource` is a resource's `base_path()`.
44    pub fn can(mut self, action: impl Into<String>, resource: impl Into<String>) -> Self {
45        self.grants.push((resource.into(), action.into()));
46        self
47    }
48
49    /// Allow every action on `resource`.
50    pub fn can_manage(self, resource: impl Into<String>) -> Self {
51        let resource = resource.into();
52        self.can(MANAGE, resource)
53    }
54
55    /// Allow every action on every resource (the superuser grant).
56    pub fn can_manage_all(self) -> Self {
57        self.can(MANAGE, ANY_RESOURCE)
58    }
59
60    /// Allow read-only access everywhere: `list`, `read`, and `export` on every
61    /// resource. Handy for a "viewer" role.
62    pub fn can_read_all(self) -> Self {
63        self.can("list", ANY_RESOURCE)
64            .can("read", ANY_RESOURCE)
65            .can("export", ANY_RESOURCE)
66    }
67
68    pub(crate) fn role_name(&self) -> &str {
69        &self.role
70    }
71
72    /// One `{role, resource, action}` row per grant, for seeding
73    /// `adminx_permissions`.
74    pub(crate) fn permission_rows(&self) -> Vec<Map<String, Value>> {
75        self.grants
76            .iter()
77            .map(|(resource, action)| {
78                let mut m = Map::new();
79                m.insert("role".into(), Value::String(self.role.clone()));
80                m.insert("resource".into(), Value::String(resource.clone()));
81                m.insert("action".into(), Value::String(action.clone()));
82                m
83            })
84            .collect()
85    }
86}