better_auth_api/plugins/admin/
access.rs1use std::collections::HashMap;
4
5use super::AdminConfig;
6
7#[derive(Debug, Clone, Default, PartialEq, Eq)]
9pub struct RolePermissions {
10 pub permissions: HashMap<String, Vec<String>>,
12}
13
14impl RolePermissions {
15 pub fn new() -> Self {
17 Self::default()
18 }
19
20 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 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 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}