Skip to main content

alibi_api/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    // Owner - full permissions
67    drop(roles.insert(
68        "owner".to_owned(),
69        Role {
70            name: "owner".to_owned(),
71            permissions: {
72                let mut p = HashMap::new();
73                drop(p.insert(Resource::Organization, vec![Action::Update, Action::Delete]));
74                drop(p.insert(
75                    Resource::Member,
76                    vec![Action::Create, Action::Update, Action::Delete],
77                ));
78                drop(p.insert(Resource::Invitation, vec![Action::Create, Action::Cancel]));
79                p
80            },
81        },
82    ));
83
84    // Admin - most permissions except org deletion
85    drop(roles.insert(
86        "admin".to_owned(),
87        Role {
88            name: "admin".to_owned(),
89            permissions: {
90                let mut p = HashMap::new();
91                drop(p.insert(Resource::Organization, vec![Action::Update]));
92                drop(p.insert(
93                    Resource::Member,
94                    vec![Action::Create, Action::Update, Action::Delete],
95                ));
96                drop(p.insert(Resource::Invitation, vec![Action::Create, Action::Cancel]));
97                p
98            },
99        },
100    ));
101
102    // Member - read-only
103    drop(roles.insert(
104        "member".to_owned(),
105        Role {
106            name: "member".to_owned(),
107            permissions: HashMap::new(),
108        },
109    ));
110
111    roles
112}
113
114/// Check if a role has permission for an action on a resource
115#[must_use]
116pub fn has_permission<H: std::hash::BuildHasher>(
117    role: &str,
118    resource: &Resource,
119    action: &Action,
120    custom_roles: &HashMap<String, crate::plugins::organization::RolePermissions, H>,
121) -> bool {
122    let default = default_roles();
123
124    // Check custom roles first
125    if let Some(custom_role) = custom_roles.get(role) {
126        let actions = match resource {
127            Resource::Organization => &custom_role.organization,
128            Resource::Member => &custom_role.member,
129            Resource::Invitation => &custom_role.invitation,
130            Resource::ApiKey => &custom_role.api_key,
131        };
132        let action_str = match action {
133            Action::Create => "create",
134            Action::Read => "read",
135            Action::Update => "update",
136            Action::Delete => "delete",
137            Action::Cancel => "cancel",
138        };
139        if actions.iter().any(|a| a == action_str) {
140            return true;
141        }
142    }
143
144    // Fall back to default roles
145    if let Some(role_def) = default.get(role)
146        && let Some(actions) = role_def.permissions.get(resource)
147    {
148        return actions.contains(action);
149    }
150
151    false
152}
153
154/// Handle composite roles (comma-separated)
155#[must_use]
156pub fn has_permission_any<H: std::hash::BuildHasher>(
157    roles_str: &str,
158    resource: &Resource,
159    action: &Action,
160    custom_roles: &HashMap<String, crate::plugins::organization::RolePermissions, H>,
161) -> bool {
162    for role in roles_str.split(',').map(str::trim) {
163        if has_permission(role, resource, action, custom_roles) {
164            return true;
165        }
166    }
167    false
168}
169
170// LCOV_EXCL_START
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    // 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.
176    #[test]
177    fn test_owner_has_full_permissions() {
178        let custom = HashMap::new();
179
180        assert!(has_permission(
181            "owner",
182            &Resource::Organization,
183            &Action::Update,
184            &custom
185        ));
186        assert!(has_permission(
187            "owner",
188            &Resource::Organization,
189            &Action::Delete,
190            &custom
191        ));
192        assert!(has_permission(
193            "owner",
194            &Resource::Member,
195            &Action::Create,
196            &custom
197        ));
198        assert!(has_permission(
199            "owner",
200            &Resource::Invitation,
201            &Action::Cancel,
202            &custom
203        ));
204    }
205
206    // 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.
207    #[test]
208    fn test_admin_cannot_delete_organization() {
209        let custom = HashMap::new();
210
211        assert!(has_permission(
212            "admin",
213            &Resource::Organization,
214            &Action::Update,
215            &custom
216        ));
217        assert!(!has_permission(
218            "admin",
219            &Resource::Organization,
220            &Action::Delete,
221            &custom
222        ));
223    }
224
225    // 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.
226    #[test]
227    fn test_member_has_no_permissions() {
228        let custom = HashMap::new();
229
230        assert!(!has_permission(
231            "member",
232            &Resource::Organization,
233            &Action::Update,
234            &custom
235        ));
236        assert!(!has_permission(
237            "member",
238            &Resource::Member,
239            &Action::Create,
240            &custom
241        ));
242    }
243
244    // 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.
245    #[test]
246    fn test_composite_roles() {
247        let custom = HashMap::new();
248
249        // member,admin should have admin permissions
250        assert!(has_permission_any(
251            "member,admin",
252            &Resource::Organization,
253            &Action::Update,
254            &custom
255        ));
256
257        // member alone should not
258        assert!(!has_permission_any(
259            "member",
260            &Resource::Organization,
261            &Action::Update,
262            &custom
263        ));
264    }
265}
266// LCOV_EXCL_STOP