Skip to main content

axum_admin/
auth.rs

1use crate::error::AdminError;
2use async_trait::async_trait;
3use std::{
4    collections::HashMap,
5    sync::{Arc, RwLock},
6};
7use uuid::Uuid;
8
9#[derive(Debug, Clone)]
10pub struct AdminUser {
11    pub username: String,
12    pub session_id: String,
13    /// true = bypasses all permission checks (superuser access)
14    pub is_superuser: bool,
15}
16
17impl AdminUser {
18    pub fn superuser(username: &str, session_id: &str) -> Self {
19        Self {
20            username: username.to_string(),
21            session_id: session_id.to_string(),
22            is_superuser: true,
23        }
24    }
25}
26
27#[async_trait]
28pub trait AdminAuth: Send + Sync {
29    async fn authenticate(
30        &self,
31        username: &str,
32        password: &str,
33    ) -> Result<AdminUser, AdminError>;
34
35    async fn get_session(&self, session_id: &str) -> Result<Option<AdminUser>, AdminError>;
36}
37
38/// In-memory admin auth. Credentials configured at startup, sessions stored in memory.
39pub struct DefaultAdminAuth {
40    credentials: Arc<RwLock<HashMap<String, String>>>,
41    sessions: Arc<RwLock<HashMap<String, AdminUser>>>,
42}
43
44impl DefaultAdminAuth {
45    pub fn new() -> Self {
46        Self {
47            credentials: Arc::new(RwLock::new(HashMap::new())),
48            sessions: Arc::new(RwLock::new(HashMap::new())),
49        }
50    }
51
52    pub fn add_user(self, username: &str, password: &str) -> Self {
53        let hash = bcrypt::hash(password, bcrypt::DEFAULT_COST)
54            .expect("bcrypt hash failed");
55        self.credentials
56            .write()
57            .unwrap()
58            .insert(username.to_string(), hash);
59        self
60    }
61}
62
63impl Default for DefaultAdminAuth {
64    fn default() -> Self {
65        Self::new()
66    }
67}
68
69#[async_trait]
70impl AdminAuth for DefaultAdminAuth {
71    async fn authenticate(&self, username: &str, password: &str) -> Result<AdminUser, AdminError> {
72        let hash = {
73            let creds = self.credentials.read().unwrap();
74            creds.get(username).cloned()
75        };
76
77        let hash = hash.ok_or(AdminError::Unauthorized)?;
78
79        let valid = bcrypt::verify(password, &hash).unwrap_or(false);
80        if !valid {
81            return Err(AdminError::Unauthorized);
82        }
83
84        let session_id = Uuid::new_v4().to_string();
85        let user = AdminUser {
86            username: username.to_string(),
87            session_id: session_id.clone(),
88            is_superuser: true,
89        };
90
91        self.sessions
92            .write()
93            .unwrap()
94            .insert(session_id, user.clone());
95
96        Ok(user)
97    }
98
99    async fn get_session(&self, session_id: &str) -> Result<Option<AdminUser>, AdminError> {
100        let sessions = self.sessions.read().unwrap();
101        Ok(sessions.get(session_id).cloned())
102    }
103}
104
105/// Returns true if the user can perform `required` action.
106/// - `None` required → always allowed.
107/// - `is_superuser` → always allowed.
108/// - `enforcer` present → ask Casbin. Permission format: "entity.action" (e.g. "posts.view").
109/// - No enforcer → deny non-superusers (safe default).
110#[cfg(feature = "seaorm")]
111pub async fn check_permission(
112    user: &AdminUser,
113    required: &Option<String>,
114    enforcer: Option<&std::sync::Arc<tokio::sync::RwLock<casbin::Enforcer>>>,
115) -> bool {
116    use casbin::CoreApi;
117    if user.is_superuser {
118        return true;
119    }
120    let enforcer = match enforcer {
121        Some(e) => e,
122        // No enforcer: allow only if no permission required
123        None => return required.is_none(),
124    };
125    // No explicit permission string: default-deny when enforcer is active
126    let perm = match required {
127        None => return false,
128        Some(p) => p,
129    };
130    let parts: Vec<&str> = perm.splitn(2, '.').collect();
131    let (obj, act) = if parts.len() == 2 {
132        (parts[0], parts[1])
133    } else {
134        (perm.as_str(), "")
135    };
136    let guard = enforcer.read().await;
137    guard.enforce((user.username.as_str(), obj, act)).unwrap_or(false)
138}
139
140/// Check entity-level permission using Casbin, auto-deriving the permission
141/// string as `"entity_name.action"` when `required` is `None`.
142#[cfg(feature = "seaorm")]
143pub async fn check_entity_permission(
144    user: &AdminUser,
145    entity_name: &str,
146    action: &str,
147    required: &Option<String>,
148    enforcer: Option<&std::sync::Arc<tokio::sync::RwLock<casbin::Enforcer>>>,
149) -> bool {
150    use casbin::CoreApi;
151    if user.is_superuser {
152        return true;
153    }
154    let enforcer = match enforcer {
155        Some(e) => e,
156        None => return required.is_none(),
157    };
158    // Use explicit permission string if set, otherwise default to "entity.action"
159    let perm_owned;
160    let perm = match required {
161        Some(p) => p.as_str(),
162        None => {
163            perm_owned = format!("{}.{}", entity_name, action);
164            &perm_owned
165        }
166    };
167    let parts: Vec<&str> = perm.splitn(2, '.').collect();
168    let (obj, act) = if parts.len() == 2 {
169        (parts[0], parts[1])
170    } else {
171        (perm, "")
172    };
173    let guard = enforcer.read().await;
174    guard.enforce((user.username.as_str(), obj, act)).unwrap_or(false)
175}
176
177#[cfg(not(feature = "seaorm"))]
178pub fn check_permission(
179    user: &AdminUser,
180    required: &Option<String>,
181    _enforcer: Option<&()>,
182) -> bool {
183    match required {
184        None => true,
185        Some(_) => user.is_superuser,
186    }
187}