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 roles.insert(
61 "owner".to_string(),
62 Role {
63 name: "owner".to_string(),
64 permissions: {
65 let mut p = HashMap::new();
66 p.insert(Resource::Organization, vec![Action::Update, Action::Delete]);
67 p.insert(
68 Resource::Member,
69 vec![Action::Create, Action::Update, Action::Delete],
70 );
71 p.insert(Resource::Invitation, vec![Action::Create, Action::Cancel]);
72 p
73 },
74 },
75 );
76
77 roles.insert(
79 "admin".to_string(),
80 Role {
81 name: "admin".to_string(),
82 permissions: {
83 let mut p = HashMap::new();
84 p.insert(Resource::Organization, vec![Action::Update]);
85 p.insert(
86 Resource::Member,
87 vec![Action::Create, Action::Update, Action::Delete],
88 );
89 p.insert(Resource::Invitation, vec![Action::Create, Action::Cancel]);
90 p
91 },
92 },
93 );
94
95 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]
165 fn test_owner_has_full_permissions() {
166 let custom = HashMap::new();
167
168 assert!(has_permission(
169 "owner",
170 &Resource::Organization,
171 &Action::Update,
172 &custom
173 ));
174 assert!(has_permission(
175 "owner",
176 &Resource::Organization,
177 &Action::Delete,
178 &custom
179 ));
180 assert!(has_permission(
181 "owner",
182 &Resource::Member,
183 &Action::Create,
184 &custom
185 ));
186 assert!(has_permission(
187 "owner",
188 &Resource::Invitation,
189 &Action::Cancel,
190 &custom
191 ));
192 }
193
194 #[test]
195 fn test_admin_cannot_delete_organization() {
196 let custom = HashMap::new();
197
198 assert!(has_permission(
199 "admin",
200 &Resource::Organization,
201 &Action::Update,
202 &custom
203 ));
204 assert!(!has_permission(
205 "admin",
206 &Resource::Organization,
207 &Action::Delete,
208 &custom
209 ));
210 }
211
212 #[test]
213 fn test_member_has_no_permissions() {
214 let custom = HashMap::new();
215
216 assert!(!has_permission(
217 "member",
218 &Resource::Organization,
219 &Action::Update,
220 &custom
221 ));
222 assert!(!has_permission(
223 "member",
224 &Resource::Member,
225 &Action::Create,
226 &custom
227 ));
228 }
229
230 #[test]
231 fn test_composite_roles() {
232 let custom = HashMap::new();
233
234 assert!(has_permission_any(
236 "member,admin",
237 &Resource::Organization,
238 &Action::Update,
239 &custom
240 ));
241
242 assert!(!has_permission_any(
244 "member",
245 &Resource::Organization,
246 &Action::Update,
247 &custom
248 ));
249 }
250}