alibi_api/plugins/organization/rbac/
mod.rs1use std::collections::HashMap;
2
3#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5pub enum Resource {
6 Organization,
7 Member,
8 Invitation,
9 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#[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
51pub type Permissions = HashMap<Resource, Vec<Action>>;
53
54#[derive(Debug, Clone)]
56pub struct Role {
57 pub name: String,
58 pub permissions: Permissions,
59}
60
61#[must_use]
63pub fn default_roles() -> HashMap<String, Role> {
64 let mut roles = HashMap::new();
65
66 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 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 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#[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 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 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#[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#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[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 #[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 #[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 #[test]
246 fn test_composite_roles() {
247 let custom = HashMap::new();
248
249 assert!(has_permission_any(
251 "member,admin",
252 &Resource::Organization,
253 &Action::Update,
254 &custom
255 ));
256
257 assert!(!has_permission_any(
259 "member",
260 &Resource::Organization,
261 &Action::Update,
262 &custom
263 ));
264 }
265}
266