Skip to main content

better_auth_api/plugins/admin/
access.rs

1//! Permission helpers for the admin plugin.
2
3use std::collections::HashMap;
4
5use super::AdminConfig;
6
7/// Role-based permission grants for the admin plugin.
8#[derive(Debug, Clone, Default, PartialEq, Eq)]
9pub struct RolePermissions {
10    /// Resource name -> allowed actions.
11    pub permissions: HashMap<String, Vec<String>>,
12}
13
14impl RolePermissions {
15    /// Create an empty role definition.
16    pub fn new() -> Self {
17        Self::default()
18    }
19
20    /// Allow a set of actions for one resource.
21    pub fn allow<I, S>(mut self, resource: impl Into<String>, actions: I) -> Self
22    where
23        I: IntoIterator<Item = S>,
24        S: Into<String>,
25    {
26        let _ = self.permissions.insert(
27            resource.into(),
28            actions.into_iter().map(Into::into).collect(),
29        );
30        self
31    }
32
33    fn allows(&self, requested: &HashMap<String, Vec<String>>) -> bool {
34        if requested.is_empty() {
35            return false;
36        }
37
38        // Better Auth 1.7.6 `isResourceAuthorized` rejects an empty action list.
39        requested.iter().all(|(resource, actions)| {
40            !actions.is_empty()
41                && self.permissions.get(resource).is_some_and(|allowed| {
42                    actions
43                        .iter()
44                        .all(|action| allowed.iter().any(|item| item == action))
45                })
46        })
47    }
48}
49
50pub(super) fn default_roles() -> HashMap<String, RolePermissions> {
51    HashMap::from([
52        (
53            "admin".to_string(),
54            RolePermissions::new()
55                .allow(
56                    "user",
57                    [
58                        "create",
59                        "list",
60                        "set-role",
61                        "ban",
62                        "impersonate",
63                        "delete",
64                        "set-password",
65                        "set-email",
66                        "get",
67                        "update",
68                    ],
69                )
70                .allow("session", ["list", "revoke", "delete"]),
71        ),
72        ("user".to_string(), RolePermissions::new()),
73    ])
74}
75
76fn configured_roles(config: &AdminConfig) -> HashMap<String, RolePermissions> {
77    config.roles.clone().unwrap_or_else(default_roles)
78}
79
80fn role_names<'a>(role: Option<&'a str>, default_role: &'a str) -> Vec<&'a str> {
81    role.filter(|role| !role.is_empty())
82        .unwrap_or(if default_role.is_empty() {
83            "user"
84        } else {
85            default_role
86        })
87        .split(',')
88        .collect()
89}
90
91pub(super) fn is_admin_user_id(user_id: Option<&str>, config: &AdminConfig) -> bool {
92    user_id.is_some_and(|user_id| {
93        config
94            .admin_user_ids
95            .as_ref()
96            .is_some_and(|ids| ids.iter().any(|id| id == user_id))
97    })
98}
99
100pub(super) fn has_permission(
101    user_id: Option<&str>,
102    role: Option<&str>,
103    config: &AdminConfig,
104    requested: &HashMap<String, Vec<String>>,
105) -> bool {
106    if is_admin_user_id(user_id, config) {
107        return true;
108    }
109
110    let roles = configured_roles(config);
111    role_names(role, &config.default_role)
112        .into_iter()
113        .filter_map(|role| roles.get(role))
114        .any(|role| role.allows(requested))
115}
116
117pub(super) fn is_admin_role(role: Option<&str>, config: &AdminConfig) -> bool {
118    role_names(role, &config.default_role)
119        .into_iter()
120        .any(|role| match &config.admin_roles {
121            Some(admins) => admins.iter().any(|admin| admin.trim() == role),
122            None => role == "admin",
123        })
124}
125
126impl AdminConfig {
127    /// Validate explicitly supplied administrator roles against the active role map.
128    pub fn validate(&self) -> better_auth_core::AuthResult<()> {
129        let Some(admin_roles) = &self.admin_roles else {
130            return Ok(());
131        };
132        let roles = configured_roles(self);
133        let invalid: Vec<_> = admin_roles
134            .iter()
135            .filter(|role| {
136                !roles
137                    .keys()
138                    .any(|defined| defined.to_lowercase() == role.to_lowercase())
139            })
140            .cloned()
141            .collect();
142        if invalid.is_empty() {
143            Ok(())
144        } else {
145            Err(better_auth_core::AuthError::config(format!(
146                "Invalid admin roles: {}. Admin roles must be defined in the 'roles' configuration.",
147                invalid.join(", ")
148            )))
149        }
150    }
151}