better_auth_api/plugins/organization/
rbac.rs1use std::collections::HashMap;
2
3#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5pub enum Resource {
6 Organization,
7 Member,
8 Invitation,
9}
10
11impl Resource {
12 pub fn parse(s: &str) -> Option<Self> {
13 match s.to_lowercase().as_str() {
14 "organization" => Some(Self::Organization),
15 "member" => Some(Self::Member),
16 "invitation" => Some(Self::Invitation),
17 _ => None,
18 }
19 }
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, Hash)]
24pub enum Action {
25 Create,
26 Read,
27 Update,
28 Delete,
29 Cancel,
30}
31
32impl Action {
33 pub fn parse(s: &str) -> Option<Self> {
34 match s.to_lowercase().as_str() {
35 "create" => Some(Self::Create),
36 "read" => Some(Self::Read),
37 "update" => Some(Self::Update),
38 "delete" => Some(Self::Delete),
39 "cancel" => Some(Self::Cancel),
40 _ => None,
41 }
42 }
43}
44
45pub type Permissions = HashMap<Resource, Vec<Action>>;
47
48#[derive(Debug, Clone)]
50pub struct Role {
51 pub name: String,
52 pub permissions: Permissions,
53}
54
55pub fn default_roles() -> HashMap<String, Role> {
57 let mut roles = HashMap::new();
58
59 let _ = roles.insert(
61 "owner".to_string(),
62 Role {
63 name: "owner".to_string(),
64 permissions: {
65 let mut p = HashMap::new();
66 let _ = p.insert(Resource::Organization, vec![Action::Update, Action::Delete]);
67 let _ = p.insert(
68 Resource::Member,
69 vec![Action::Create, Action::Update, Action::Delete],
70 );
71 let _ = p.insert(Resource::Invitation, vec![Action::Create, Action::Cancel]);
72 p
73 },
74 },
75 );
76
77 let _ = roles.insert(
79 "admin".to_string(),
80 Role {
81 name: "admin".to_string(),
82 permissions: {
83 let mut p = HashMap::new();
84 let _ = p.insert(Resource::Organization, vec![Action::Update]);
85 let _ = p.insert(
86 Resource::Member,
87 vec![Action::Create, Action::Update, Action::Delete],
88 );
89 let _ = p.insert(Resource::Invitation, vec![Action::Create, Action::Cancel]);
90 p
91 },
92 },
93 );
94
95 let _ = roles.insert(
97 "member".to_string(),
98 Role {
99 name: "member".to_string(),
100 permissions: HashMap::new(),
101 },
102 );
103
104 roles
105}
106
107pub fn has_permission(
109 role: &str,
110 resource: &Resource,
111 action: &Action,
112 custom_roles: &HashMap<String, crate::plugins::organization::RolePermissions>,
113) -> bool {
114 let default = default_roles();
115
116 if let Some(custom_role) = custom_roles.get(role) {
118 let actions = match resource {
119 Resource::Organization => &custom_role.organization,
120 Resource::Member => &custom_role.member,
121 Resource::Invitation => &custom_role.invitation,
122 };
123 let action_str = match action {
124 Action::Create => "create",
125 Action::Read => "read",
126 Action::Update => "update",
127 Action::Delete => "delete",
128 Action::Cancel => "cancel",
129 };
130 if actions.iter().any(|a| a == action_str) {
131 return true;
132 }
133 }
134
135 if let Some(role_def) = default.get(role)
137 && let Some(actions) = role_def.permissions.get(resource)
138 {
139 return actions.contains(action);
140 }
141
142 false
143}
144
145pub fn has_permission_any(
147 roles_str: &str,
148 resource: &Resource,
149 action: &Action,
150 custom_roles: &HashMap<String, crate::plugins::organization::RolePermissions>,
151) -> bool {
152 for role in roles_str.split(',').map(|s| s.trim()) {
153 if has_permission(role, resource, action, custom_roles) {
154 return true;
155 }
156 }
157 false
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
166 fn test_owner_has_full_permissions() {
167 let custom = HashMap::new();
168
169 assert!(has_permission(
170 "owner",
171 &Resource::Organization,
172 &Action::Update,
173 &custom
174 ));
175 assert!(has_permission(
176 "owner",
177 &Resource::Organization,
178 &Action::Delete,
179 &custom
180 ));
181 assert!(has_permission(
182 "owner",
183 &Resource::Member,
184 &Action::Create,
185 &custom
186 ));
187 assert!(has_permission(
188 "owner",
189 &Resource::Invitation,
190 &Action::Cancel,
191 &custom
192 ));
193 }
194
195 #[test]
197 fn test_admin_cannot_delete_organization() {
198 let custom = HashMap::new();
199
200 assert!(has_permission(
201 "admin",
202 &Resource::Organization,
203 &Action::Update,
204 &custom
205 ));
206 assert!(!has_permission(
207 "admin",
208 &Resource::Organization,
209 &Action::Delete,
210 &custom
211 ));
212 }
213
214 #[test]
216 fn test_member_has_no_permissions() {
217 let custom = HashMap::new();
218
219 assert!(!has_permission(
220 "member",
221 &Resource::Organization,
222 &Action::Update,
223 &custom
224 ));
225 assert!(!has_permission(
226 "member",
227 &Resource::Member,
228 &Action::Create,
229 &custom
230 ));
231 }
232
233 #[test]
235 fn test_composite_roles() {
236 let custom = HashMap::new();
237
238 assert!(has_permission_any(
240 "member,admin",
241 &Resource::Organization,
242 &Action::Update,
243 &custom
244 ));
245
246 assert!(!has_permission_any(
248 "member",
249 &Resource::Organization,
250 &Action::Update,
251 &custom
252 ));
253 }
254}