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    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    // Admin - most permissions except org deletion
78    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    // Member - read-only
96    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    #[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        // member,admin should have admin permissions
235        assert!(has_permission_any(
236            "member,admin",
237            &Resource::Organization,
238            &Action::Update,
239            &custom
240        ));
241
242        // member alone should not
243        assert!(!has_permission_any(
244            "member",
245            &Resource::Organization,
246            &Action::Update,
247            &custom
248        ));
249    }
250}