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}