Skip to main content

adminx_rbac/
resources.rs

1// adminx-rbac/src/resources.rs
2//
3// Two adminx resources that make roles and permissions editable inside the
4// panel — the whole point of a DB-backed model (change access without a
5// redeploy). Both are admin-only.
6//
7// `PermissionResource` writes are what change authorization, so its create /
8// update / delete refresh the in-memory cache on success (the reload-on-write
9// invalidation strategy). `RoleResource` is metadata the `can` check never
10// reads, so it needs no reload.
11//
12// A `PermissionResource` mutation legitimately stores the wildcard grants `"*"`
13// (any resource) and `"manage"` (any action) — those are not rejected here; they
14// are how an admin grants broad access from the panel.
15
16use adminx_core::request::ReqCtx;
17use adminx_core::resource::Resource;
18use adminx_core::response::ApiResponse;
19use async_trait::async_trait;
20use serde_json::Value;
21
22/// Refresh the authorizer cache after a permission write; log but don't fail the
23/// request if the reload itself errors (the write already succeeded).
24async fn reload_cache() {
25    if let Err(e) = crate::reload().await {
26        tracing::error!("adminx-rbac: cache reload after a permission write failed: {e:?}");
27    }
28}
29
30/// Editable `adminx_permissions` rows: one grant = `(role, resource, action)`.
31#[derive(Clone)]
32pub struct PermissionResource;
33
34#[async_trait]
35impl Resource for PermissionResource {
36    fn resource_name(&self) -> &'static str {
37        "Permissions"
38    }
39    fn base_path(&self) -> &'static str {
40        "adminx-permissions"
41    }
42    fn table_name(&self) -> &'static str {
43        "adminx_permissions"
44    }
45    fn clone_box(&self) -> Box<dyn Resource> {
46        Box::new(self.clone())
47    }
48    fn permit_keys(&self) -> Vec<&'static str> {
49        vec!["role", "resource", "action"]
50    }
51    fn menu(&self) -> &'static str {
52        "Permissions"
53    }
54
55    // create / update / delete delegate to the shared default bodies, then
56    // reload the cache on success. Delegating rather than copying is what keeps
57    // permission edits in the audit log — a hand-copied body would silently miss
58    // any invariant the default gains later. The form handlers route through
59    // these, so both the API and the panel UI trigger a reload.
60
61    async fn create(&self, ctx: &ReqCtx, body: Value) -> ApiResponse {
62        let resp = adminx_core::crud::create(self, ctx, body).await;
63        if resp.status < 300 {
64            reload_cache().await;
65        }
66        resp
67    }
68
69    async fn update(&self, ctx: &ReqCtx, id: &str, body: Value) -> ApiResponse {
70        let resp = adminx_core::crud::update(self, ctx, id, body).await;
71        if resp.status < 300 {
72            reload_cache().await;
73        }
74        resp
75    }
76
77    async fn delete(&self, ctx: &ReqCtx, id: &str) -> ApiResponse {
78        let resp = adminx_core::crud::delete(self, ctx, id).await;
79        if resp.status < 300 {
80            reload_cache().await;
81        }
82        resp
83    }
84}
85
86/// Editable `adminx_roles` metadata (name + description). Not consulted by the
87/// `can` check, so no cache reload is needed on write.
88#[derive(Clone)]
89pub struct RoleResource;
90
91#[async_trait]
92impl Resource for RoleResource {
93    fn resource_name(&self) -> &'static str {
94        "Roles"
95    }
96    fn base_path(&self) -> &'static str {
97        "adminx-roles"
98    }
99    fn table_name(&self) -> &'static str {
100        "adminx_roles"
101    }
102    fn clone_box(&self) -> Box<dyn Resource> {
103        Box::new(self.clone())
104    }
105    fn permit_keys(&self) -> Vec<&'static str> {
106        vec!["name", "description"]
107    }
108    fn menu(&self) -> &'static str {
109        "Roles"
110    }
111}