alibi_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 _ = 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#[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#[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#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[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 #[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 #[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 #[test]
241 fn test_composite_roles() {
242 let custom = HashMap::new();
243
244 assert!(has_permission_any(
246 "member,admin",
247 &Resource::Organization,
248 &Action::Update,
249 &custom
250 ));
251
252 assert!(!has_permission_any(
254 "member",
255 &Resource::Organization,
256 &Action::Update,
257 &custom
258 ));
259 }
260}
261