Skip to main content

alibi_plugins/organization/rbac/
mod.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    /// Organization-owned API keys. Upstream's default statements grant no
10    /// role any action here; only the creator role is implicitly allowed.
11    ApiKey,
12}
13
14impl Resource {
15    #[must_use]
16    pub fn parse(s: &str) -> Option<Self> {
17        match s.to_lowercase().as_str() {
18            "organization" => Some(Self::Organization),
19            "member" => Some(Self::Member),
20            "invitation" => Some(Self::Invitation),
21            "apikey" => Some(Self::ApiKey),
22            _ => None,
23        }
24    }
25}
26
27/// Actions that can be performed on resources
28#[derive(Debug, Clone, PartialEq, Eq, Hash)]
29pub enum Action {
30    Create,
31    Read,
32    Update,
33    Delete,
34    Cancel,
35}
36
37impl Action {
38    #[must_use]
39    pub fn parse(s: &str) -> Option<Self> {
40        match s.to_lowercase().as_str() {
41            "create" => Some(Self::Create),
42            "read" => Some(Self::Read),
43            "update" => Some(Self::Update),
44            "delete" => Some(Self::Delete),
45            "cancel" => Some(Self::Cancel),
46            _ => None,
47        }
48    }
49}
50
51/// Permission definition
52pub type Permissions = HashMap<Resource, Vec<Action>>;
53
54/// Role with associated permissions
55#[derive(Debug, Clone)]
56pub struct Role {
57    pub name: String,
58    pub permissions: Permissions,
59}
60
61/// Get default role definitions matching TypeScript implementation
62#[must_use]
63pub fn default_roles() -> HashMap<String, Role> {
64    let mut roles = HashMap::new();
65
66    _ = roles.insert(
67        "owner".to_owned(),
68        Role {
69            name: "owner".to_owned(),
70            permissions: {
71                let mut p = HashMap::new();
72                _ = p.insert(Resource::Organization, vec![Action::Update, Action::Delete]);
73                _ = p.insert(
74                    Resource::Member,
75                    vec![Action::Create, Action::Update, Action::Delete],
76                );
77                _ = p.insert(Resource::Invitation, vec![Action::Create, Action::Cancel]);
78                p
79            },
80        },
81    );
82
83    _ = roles.insert(
84        "admin".to_owned(),
85        Role {
86            name: "admin".to_owned(),
87            permissions: {
88                let mut p = HashMap::new();
89                _ = p.insert(Resource::Organization, vec![Action::Update]);
90                _ = p.insert(
91                    Resource::Member,
92                    vec![Action::Create, Action::Update, Action::Delete],
93                );
94                _ = p.insert(Resource::Invitation, vec![Action::Create, Action::Cancel]);
95                p
96            },
97        },
98    );
99
100    _ = roles.insert(
101        "member".to_owned(),
102        Role {
103            name: "member".to_owned(),
104            permissions: HashMap::new(),
105        },
106    );
107
108    roles
109}
110
111/// Check if a role has permission for an action on a resource
112#[must_use]
113pub fn has_permission<H: std::hash::BuildHasher>(
114    role: &str,
115    resource: &Resource,
116    action: &Action,
117    custom_roles: &HashMap<String, crate::organization::RolePermissions, H>,
118) -> bool {
119    let default = default_roles();
120
121    if let Some(custom_role) = custom_roles.get(role) {
122        let actions = match resource {
123            Resource::Organization => &custom_role.organization,
124            Resource::Member => &custom_role.member,
125            Resource::Invitation => &custom_role.invitation,
126            Resource::ApiKey => &custom_role.api_key,
127        };
128        let action_str = match action {
129            Action::Create => "create",
130            Action::Read => "read",
131            Action::Update => "update",
132            Action::Delete => "delete",
133            Action::Cancel => "cancel",
134        };
135        if actions.iter().any(|a| a == action_str) {
136            return true;
137        }
138    }
139
140    if let Some(role_def) = default.get(role)
141        && let Some(actions) = role_def.permissions.get(resource)
142    {
143        return actions.contains(action);
144    }
145
146    false
147}
148
149/// Handle composite roles (comma-separated)
150#[must_use]
151pub fn has_permission_any<H: std::hash::BuildHasher>(
152    roles_str: &str,
153    resource: &Resource,
154    action: &Action,
155    custom_roles: &HashMap<String, crate::organization::RolePermissions, H>,
156) -> bool {
157    for role in roles_str.split(',').map(str::trim) {
158        if has_permission(role, resource, action, custom_roles) {
159            return true;
160        }
161    }
162    false
163}
164
165// LCOV_EXCL_START
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    // 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.
171    #[test]
172    fn test_owner_has_full_permissions() {
173        let custom = HashMap::new();
174
175        assert!(has_permission(
176            "owner",
177            &Resource::Organization,
178            &Action::Update,
179            &custom
180        ));
181        assert!(has_permission(
182            "owner",
183            &Resource::Organization,
184            &Action::Delete,
185            &custom
186        ));
187        assert!(has_permission(
188            "owner",
189            &Resource::Member,
190            &Action::Create,
191            &custom
192        ));
193        assert!(has_permission(
194            "owner",
195            &Resource::Invitation,
196            &Action::Cancel,
197            &custom
198        ));
199    }
200
201    // 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.
202    #[test]
203    fn test_admin_cannot_delete_organization() {
204        let custom = HashMap::new();
205
206        assert!(has_permission(
207            "admin",
208            &Resource::Organization,
209            &Action::Update,
210            &custom
211        ));
212        assert!(!has_permission(
213            "admin",
214            &Resource::Organization,
215            &Action::Delete,
216            &custom
217        ));
218    }
219
220    // 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.
221    #[test]
222    fn test_member_has_no_permissions() {
223        let custom = HashMap::new();
224
225        assert!(!has_permission(
226            "member",
227            &Resource::Organization,
228            &Action::Update,
229            &custom
230        ));
231        assert!(!has_permission(
232            "member",
233            &Resource::Member,
234            &Action::Create,
235            &custom
236        ));
237    }
238
239    // 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.
240    #[test]
241    fn test_composite_roles() {
242        let custom = HashMap::new();
243
244        // member,admin should have admin permissions
245        assert!(has_permission_any(
246            "member,admin",
247            &Resource::Organization,
248            &Action::Update,
249            &custom
250        ));
251
252        // member alone should not
253        assert!(!has_permission_any(
254            "member",
255            &Resource::Organization,
256            &Action::Update,
257            &custom
258        ));
259    }
260}
261// LCOV_EXCL_STOP