Skip to main content

better_auth_api/plugins/organization/
rbac.rs

1use std::collections::HashMap;
2
3/// Resource types for permission checks
4#[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/// Actions that can be performed on resources
23#[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
45/// Permission definition
46pub type Permissions = HashMap<Resource, Vec<Action>>;
47
48/// Role with associated permissions
49#[derive(Debug, Clone)]
50pub struct Role {
51    pub name: String,
52    pub permissions: Permissions,
53}
54
55/// Get default role definitions matching TypeScript implementation
56pub fn default_roles() -> HashMap<String, Role> {
57    let mut roles = HashMap::new();
58
59    // Owner - full permissions
60    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    // Admin - most permissions except org deletion
78    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    // Member - read-only
96    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
107/// Check if a role has permission for an action on a resource
108pub 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    // Check custom roles first
117    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    // Fall back to default roles
136    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
145/// Handle composite roles (comma-separated)
146pub 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    // Upstream reference: packages/better-auth/src/plugins/access/access.test.ts and packages/better-auth/src/plugins/organization/access/statement.ts; adapted to the Rust organization RBAC helpers.
165    #[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    // Upstream reference: packages/better-auth/src/plugins/access/access.test.ts and packages/better-auth/src/plugins/organization/access/statement.ts; adapted to the Rust organization RBAC helpers.
196    #[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    // Upstream reference: packages/better-auth/src/plugins/access/access.test.ts and packages/better-auth/src/plugins/organization/access/statement.ts; adapted to the Rust organization RBAC helpers.
215    #[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    // Upstream reference: packages/better-auth/src/plugins/access/access.test.ts and packages/better-auth/src/plugins/organization/access/statement.ts; adapted to the Rust organization RBAC helpers.
234    #[test]
235    fn test_composite_roles() {
236        let custom = HashMap::new();
237
238        // member,admin should have admin permissions
239        assert!(has_permission_any(
240            "member,admin",
241            &Resource::Organization,
242            &Action::Update,
243            &custom
244        ));
245
246        // member alone should not
247        assert!(!has_permission_any(
248            "member",
249            &Resource::Organization,
250            &Action::Update,
251            &custom
252        ));
253    }
254}