]>
Commit | Line | Data |
---|---|---|
5c6cdf98 | 1 | use std::io::Write; |
babab85b | 2 | use std::collections::{HashMap, BTreeMap, BTreeSet}; |
5c6cdf98 | 3 | use std::path::{PathBuf, Path}; |
5354511f | 4 | use std::sync::{Arc, RwLock}; |
bc0d0388 | 5 | use std::str::FromStr; |
5c6cdf98 | 6 | |
f7d4e4b5 | 7 | use anyhow::{bail, Error}; |
5c6cdf98 DM |
8 | |
9 | use lazy_static::lazy_static; | |
10 | ||
bc0d0388 DM |
11 | use ::serde::{Deserialize, Serialize}; |
12 | use serde::de::{value, IntoDeserializer}; | |
13 | ||
5c6cdf98 | 14 | use proxmox::tools::{fs::replace_file, fs::CreateOptions}; |
fddc8aa4 | 15 | use proxmox::constnamedbitmap; |
bc0d0388 | 16 | use proxmox::api::{api, schema::*}; |
5c6cdf98 | 17 | |
e6dc35ac | 18 | use crate::api2::types::{Authid,Userid}; |
e7cb4dc5 | 19 | |
5c6cdf98 DM |
20 | // define Privilege bitfield |
21 | ||
fddc8aa4 | 22 | constnamedbitmap! { |
1ad9dd08 DC |
23 | /// Contains a list of Privileges |
24 | PRIVILEGES: u64 => { | |
fddc8aa4 TL |
25 | PRIV_SYS_AUDIT("Sys.Audit"); |
26 | PRIV_SYS_MODIFY("Sys.Modify"); | |
27 | PRIV_SYS_POWER_MANAGEMENT("Sys.PowerManagement"); | |
5c6cdf98 | 28 | |
fddc8aa4 | 29 | PRIV_DATASTORE_AUDIT("Datastore.Audit"); |
41bfd249 | 30 | PRIV_DATASTORE_ALLOCATE("Datastore.Allocate"); |
fddc8aa4 TL |
31 | PRIV_DATASTORE_MODIFY("Datastore.Modify"); |
32 | PRIV_DATASTORE_READ("Datastore.Read"); | |
09f6a240 | 33 | PRIV_DATASTORE_VERIFY("Datastore.Verify"); |
54552dda | 34 | |
1ad9dd08 | 35 | /// Datastore.Backup also requires backup ownership |
fddc8aa4 | 36 | PRIV_DATASTORE_BACKUP("Datastore.Backup"); |
1ad9dd08 | 37 | /// Datastore.Prune also requires backup ownership |
fddc8aa4 | 38 | PRIV_DATASTORE_PRUNE("Datastore.Prune"); |
5c6cdf98 | 39 | |
fddc8aa4 | 40 | PRIV_PERMISSIONS_MODIFY("Permissions.Modify"); |
1ad9dd08 | 41 | |
fddc8aa4 TL |
42 | PRIV_REMOTE_AUDIT("Remote.Audit"); |
43 | PRIV_REMOTE_MODIFY("Remote.Modify"); | |
44 | PRIV_REMOTE_READ("Remote.Read"); | |
1c2f842a | 45 | |
fddc8aa4 | 46 | PRIV_SYS_CONSOLE("Sys.Console"); |
1ad9dd08 DC |
47 | } |
48 | } | |
4f66423f | 49 | |
8247db5b | 50 | |
05be0984 TL |
51 | /// Admin always has all privileges. It can do everything except a few actions |
52 | /// which are limited to the 'root@pam` superuser | |
0815ec7e | 53 | pub const ROLE_ADMIN: u64 = std::u64::MAX; |
05be0984 TL |
54 | |
55 | /// NoAccess can be used to remove privileges from specific paths | |
0815ec7e DM |
56 | pub const ROLE_NO_ACCESS: u64 = 0; |
57 | ||
5c6cdf98 DM |
58 | pub const ROLE_AUDIT: u64 = |
59 | PRIV_SYS_AUDIT | | |
9765092e | 60 | PRIV_DATASTORE_AUDIT; |
5c6cdf98 | 61 | |
6f6aa95a | 62 | /// Datastore.Admin can do anything on the datastore. |
9765092e DM |
63 | pub const ROLE_DATASTORE_ADMIN: u64 = |
64 | PRIV_DATASTORE_AUDIT | | |
9c7fe29d | 65 | PRIV_DATASTORE_MODIFY | |
d00e1a21 | 66 | PRIV_DATASTORE_READ | |
09f6a240 | 67 | PRIV_DATASTORE_VERIFY | |
54552dda | 68 | PRIV_DATASTORE_BACKUP | |
d00e1a21 | 69 | PRIV_DATASTORE_PRUNE; |
5c6cdf98 | 70 | |
09f6a240 | 71 | /// Datastore.Reader can read/verify datastore content and do restore |
6f6aa95a DM |
72 | pub const ROLE_DATASTORE_READER: u64 = |
73 | PRIV_DATASTORE_AUDIT | | |
09f6a240 | 74 | PRIV_DATASTORE_VERIFY | |
6f6aa95a DM |
75 | PRIV_DATASTORE_READ; |
76 | ||
77 | /// Datastore.Backup can do backup and restore, but no prune. | |
78 | pub const ROLE_DATASTORE_BACKUP: u64 = | |
79 | PRIV_DATASTORE_BACKUP; | |
80 | ||
81 | /// Datastore.PowerUser can do backup, restore, and prune. | |
82 | pub const ROLE_DATASTORE_POWERUSER: u64 = | |
83 | PRIV_DATASTORE_PRUNE | | |
54552dda | 84 | PRIV_DATASTORE_BACKUP; |
d00e1a21 | 85 | |
6f6aa95a | 86 | /// Datastore.Audit can audit the datastore. |
d00e1a21 DM |
87 | pub const ROLE_DATASTORE_AUDIT: u64 = |
88 | PRIV_DATASTORE_AUDIT; | |
9765092e | 89 | |
8247db5b DM |
90 | /// Remote.Audit can audit the remote |
91 | pub const ROLE_REMOTE_AUDIT: u64 = | |
92 | PRIV_REMOTE_AUDIT; | |
93 | ||
94 | /// Remote.Admin can do anything on the remote. | |
95 | pub const ROLE_REMOTE_ADMIN: u64 = | |
96 | PRIV_REMOTE_AUDIT | | |
97 | PRIV_REMOTE_MODIFY | | |
2fc45a97 | 98 | PRIV_REMOTE_READ; |
8247db5b DM |
99 | |
100 | /// Remote.SyncOperator can do read and prune on the remote. | |
101 | pub const ROLE_REMOTE_SYNC_OPERATOR: u64 = | |
102 | PRIV_REMOTE_AUDIT | | |
2fc45a97 | 103 | PRIV_REMOTE_READ; |
8247db5b | 104 | |
8d048af2 | 105 | pub const ROLE_NAME_NO_ACCESS: &str ="NoAccess"; |
5c6cdf98 | 106 | |
bc0d0388 DM |
107 | #[api()] |
108 | #[repr(u64)] | |
109 | #[derive(Serialize, Deserialize)] | |
110 | /// Role | |
111 | pub enum Role { | |
112 | /// Administrator | |
113 | Admin = ROLE_ADMIN, | |
114 | /// Auditor | |
115 | Audit = ROLE_AUDIT, | |
116 | /// Disable Access | |
117 | NoAccess = ROLE_NO_ACCESS, | |
118 | /// Datastore Administrator | |
119 | DatastoreAdmin = ROLE_DATASTORE_ADMIN, | |
120 | /// Datastore Reader (inspect datastore content and do restores) | |
121 | DatastoreReader = ROLE_DATASTORE_READER, | |
122 | /// Datastore Backup (backup and restore owned backups) | |
123 | DatastoreBackup = ROLE_DATASTORE_BACKUP, | |
124 | /// Datastore PowerUser (backup, restore and prune owned backup) | |
125 | DatastorePowerUser = ROLE_DATASTORE_POWERUSER, | |
126 | /// Datastore Auditor | |
127 | DatastoreAudit = ROLE_DATASTORE_AUDIT, | |
128 | /// Remote Auditor | |
129 | RemoteAudit = ROLE_REMOTE_AUDIT, | |
130 | /// Remote Administrator | |
131 | RemoteAdmin = ROLE_REMOTE_ADMIN, | |
132 | /// Syncronisation Opertator | |
133 | RemoteSyncOperator = ROLE_REMOTE_SYNC_OPERATOR, | |
134 | } | |
135 | ||
136 | impl FromStr for Role { | |
137 | type Err = value::Error; | |
138 | ||
139 | fn from_str(s: &str) -> Result<Self, Self::Err> { | |
140 | Self::deserialize(s.into_deserializer()) | |
141 | } | |
142 | } | |
143 | ||
5c6cdf98 | 144 | lazy_static! { |
3fff55b2 | 145 | pub static ref ROLE_NAMES: HashMap<&'static str, (u64, &'static str)> = { |
5c6cdf98 DM |
146 | let mut map = HashMap::new(); |
147 | ||
bc0d0388 DM |
148 | let list = match Role::API_SCHEMA { |
149 | Schema::String(StringSchema { format: Some(ApiStringFormat::Enum(list)), .. }) => list, | |
150 | _ => unreachable!(), | |
151 | }; | |
152 | ||
153 | for entry in list.iter() { | |
154 | let privs: u64 = Role::from_str(entry.value).unwrap() as u64; | |
155 | map.insert(entry.value, (privs, entry.description)); | |
156 | } | |
8247db5b | 157 | |
5c6cdf98 DM |
158 | map |
159 | }; | |
160 | } | |
161 | ||
9f4e47dd | 162 | pub fn split_acl_path(path: &str) -> Vec<&str> { |
5c6cdf98 DM |
163 | |
164 | let items = path.split('/'); | |
165 | ||
166 | let mut components = vec![]; | |
167 | ||
168 | for name in items { | |
169 | if name.is_empty() { continue; } | |
170 | components.push(name); | |
171 | } | |
172 | ||
173 | components | |
174 | } | |
175 | ||
74c08a57 DM |
176 | pub fn check_acl_path(path: &str) -> Result<(), Error> { |
177 | ||
178 | let components = split_acl_path(path); | |
179 | ||
180 | let components_len = components.len(); | |
181 | ||
182 | if components_len == 0 { return Ok(()); } | |
183 | match components[0] { | |
184 | "access" => { | |
185 | if components_len == 1 { return Ok(()); } | |
186 | match components[1] { | |
187 | "acl" | "users" => { | |
188 | if components_len == 2 { return Ok(()); } | |
189 | } | |
190 | _ => {}, | |
191 | } | |
192 | } | |
193 | "datastore" => { // /datastore/{store} | |
194 | if components_len <= 2 { return Ok(()); } | |
195 | } | |
196 | "remote" => { // /remote/{remote}/{store} | |
197 | if components_len <= 3 { return Ok(()); } | |
198 | } | |
199 | "system" => { | |
200 | if components_len == 1 { return Ok(()); } | |
201 | match components[1] { | |
fa2bdc13 | 202 | "disks" | "log" | "status" | "tasks" | "time" => { |
74c08a57 DM |
203 | if components_len == 2 { return Ok(()); } |
204 | } | |
205 | "services" => { // /system/services/{service} | |
206 | if components_len <= 3 { return Ok(()); } | |
207 | } | |
208 | "network" => { | |
209 | if components_len == 2 { return Ok(()); } | |
210 | match components[2] { | |
211 | "dns" => { | |
212 | if components_len == 3 { return Ok(()); } | |
213 | } | |
214 | "interfaces" => { // /system/network/interfaces/{iface} | |
215 | if components_len <= 4 { return Ok(()); } | |
216 | } | |
217 | _ => {} | |
218 | } | |
219 | } | |
220 | _ => {} | |
221 | } | |
222 | } | |
223 | _ => {} | |
224 | } | |
225 | ||
226 | bail!("invalid acl path '{}'.", path); | |
227 | } | |
228 | ||
5c6cdf98 | 229 | pub struct AclTree { |
ed3e60ae | 230 | pub root: AclTreeNode, |
5c6cdf98 DM |
231 | } |
232 | ||
ed3e60ae | 233 | pub struct AclTreeNode { |
e6dc35ac | 234 | pub users: HashMap<Authid, HashMap<String, bool>>, |
ed3e60ae DM |
235 | pub groups: HashMap<String, HashMap<String, bool>>, |
236 | pub children: BTreeMap<String, AclTreeNode>, | |
5c6cdf98 DM |
237 | } |
238 | ||
239 | impl AclTreeNode { | |
240 | ||
241 | pub fn new() -> Self { | |
242 | Self { | |
243 | users: HashMap::new(), | |
244 | groups: HashMap::new(), | |
a83eab3c | 245 | children: BTreeMap::new(), |
5c6cdf98 DM |
246 | } |
247 | } | |
248 | ||
babab85b | 249 | pub fn extract_roles(&self, auth_id: &Authid, all: bool) -> HashMap<String, bool> { |
e6dc35ac | 250 | let user_roles = self.extract_user_roles(auth_id, all); |
babab85b | 251 | if !user_roles.is_empty() || auth_id.is_token() { |
0815ec7e DM |
252 | // user privs always override group privs |
253 | return user_roles | |
254 | }; | |
255 | ||
e6dc35ac | 256 | self.extract_group_roles(auth_id.user(), all) |
0815ec7e DM |
257 | } |
258 | ||
babab85b | 259 | pub fn extract_user_roles(&self, auth_id: &Authid, all: bool) -> HashMap<String, bool> { |
0815ec7e | 260 | |
babab85b | 261 | let mut map = HashMap::new(); |
0815ec7e | 262 | |
e6dc35ac | 263 | let roles = match self.users.get(auth_id) { |
0815ec7e | 264 | Some(m) => m, |
babab85b | 265 | None => return map, |
0815ec7e DM |
266 | }; |
267 | ||
268 | for (role, propagate) in roles { | |
269 | if *propagate || all { | |
8d048af2 | 270 | if role == ROLE_NAME_NO_ACCESS { |
babab85b FG |
271 | // return a map with a single role 'NoAccess' |
272 | let mut map = HashMap::new(); | |
273 | map.insert(role.to_string(), false); | |
274 | return map; | |
0815ec7e | 275 | } |
babab85b | 276 | map.insert(role.to_string(), *propagate); |
0815ec7e DM |
277 | } |
278 | } | |
279 | ||
babab85b | 280 | map |
0815ec7e DM |
281 | } |
282 | ||
babab85b | 283 | pub fn extract_group_roles(&self, _user: &Userid, all: bool) -> HashMap<String, bool> { |
0815ec7e | 284 | |
babab85b | 285 | let mut map = HashMap::new(); |
0815ec7e DM |
286 | |
287 | for (_group, roles) in &self.groups { | |
288 | let is_member = false; // fixme: check if user is member of the group | |
289 | if !is_member { continue; } | |
290 | ||
291 | for (role, propagate) in roles { | |
292 | if *propagate || all { | |
8d048af2 | 293 | if role == ROLE_NAME_NO_ACCESS { |
babab85b FG |
294 | // return a map with a single role 'NoAccess' |
295 | let mut map = HashMap::new(); | |
296 | map.insert(role.to_string(), false); | |
297 | return map; | |
0815ec7e | 298 | } |
babab85b | 299 | map.insert(role.to_string(), *propagate); |
0815ec7e DM |
300 | } |
301 | } | |
302 | } | |
303 | ||
babab85b | 304 | map |
0815ec7e DM |
305 | } |
306 | ||
9765092e DM |
307 | pub fn delete_group_role(&mut self, group: &str, role: &str) { |
308 | let roles = match self.groups.get_mut(group) { | |
309 | Some(r) => r, | |
310 | None => return, | |
311 | }; | |
312 | roles.remove(role); | |
313 | } | |
314 | ||
e6dc35ac FG |
315 | pub fn delete_user_role(&mut self, auth_id: &Authid, role: &str) { |
316 | let roles = match self.users.get_mut(auth_id) { | |
9765092e DM |
317 | Some(r) => r, |
318 | None => return, | |
319 | }; | |
320 | roles.remove(role); | |
321 | } | |
322 | ||
5c6cdf98 | 323 | pub fn insert_group_role(&mut self, group: String, role: String, propagate: bool) { |
8d048af2 DM |
324 | let map = self.groups.entry(group).or_insert_with(|| HashMap::new()); |
325 | if role == ROLE_NAME_NO_ACCESS { | |
326 | map.clear(); | |
327 | map.insert(role, propagate); | |
328 | } else { | |
329 | map.remove(ROLE_NAME_NO_ACCESS); | |
330 | map.insert(role, propagate); | |
331 | } | |
5c6cdf98 DM |
332 | } |
333 | ||
e6dc35ac FG |
334 | pub fn insert_user_role(&mut self, auth_id: Authid, role: String, propagate: bool) { |
335 | let map = self.users.entry(auth_id).or_insert_with(|| HashMap::new()); | |
8d048af2 DM |
336 | if role == ROLE_NAME_NO_ACCESS { |
337 | map.clear(); | |
338 | map.insert(role, propagate); | |
339 | } else { | |
340 | map.remove(ROLE_NAME_NO_ACCESS); | |
341 | map.insert(role, propagate); | |
342 | } | |
5c6cdf98 DM |
343 | } |
344 | } | |
345 | ||
346 | impl AclTree { | |
347 | ||
348 | pub fn new() -> Self { | |
babab85b FG |
349 | Self { |
350 | root: AclTreeNode::new(), | |
351 | } | |
5c6cdf98 DM |
352 | } |
353 | ||
2882c881 DC |
354 | pub fn find_node(&mut self, path: &str) -> Option<&mut AclTreeNode> { |
355 | let path = split_acl_path(path); | |
356 | return self.get_node(&path); | |
357 | } | |
358 | ||
9765092e DM |
359 | fn get_node(&mut self, path: &[&str]) -> Option<&mut AclTreeNode> { |
360 | let mut node = &mut self.root; | |
361 | for comp in path { | |
362 | node = match node.children.get_mut(*comp) { | |
363 | Some(n) => n, | |
364 | None => return None, | |
365 | }; | |
366 | } | |
367 | Some(node) | |
368 | } | |
369 | ||
5c6cdf98 DM |
370 | fn get_or_insert_node(&mut self, path: &[&str]) -> &mut AclTreeNode { |
371 | let mut node = &mut self.root; | |
372 | for comp in path { | |
373 | node = node.children.entry(String::from(*comp)) | |
374 | .or_insert_with(|| AclTreeNode::new()); | |
375 | } | |
376 | node | |
377 | } | |
378 | ||
9765092e DM |
379 | pub fn delete_group_role(&mut self, path: &str, group: &str, role: &str) { |
380 | let path = split_acl_path(path); | |
381 | let node = match self.get_node(&path) { | |
382 | Some(n) => n, | |
383 | None => return, | |
384 | }; | |
385 | node.delete_group_role(group, role); | |
386 | } | |
387 | ||
e6dc35ac | 388 | pub fn delete_user_role(&mut self, path: &str, auth_id: &Authid, role: &str) { |
9765092e DM |
389 | let path = split_acl_path(path); |
390 | let node = match self.get_node(&path) { | |
391 | Some(n) => n, | |
392 | None => return, | |
393 | }; | |
e6dc35ac | 394 | node.delete_user_role(auth_id, role); |
9765092e DM |
395 | } |
396 | ||
5c6cdf98 DM |
397 | pub fn insert_group_role(&mut self, path: &str, group: &str, role: &str, propagate: bool) { |
398 | let path = split_acl_path(path); | |
399 | let node = self.get_or_insert_node(&path); | |
400 | node.insert_group_role(group.to_string(), role.to_string(), propagate); | |
401 | } | |
402 | ||
e6dc35ac | 403 | pub fn insert_user_role(&mut self, path: &str, auth_id: &Authid, role: &str, propagate: bool) { |
5c6cdf98 DM |
404 | let path = split_acl_path(path); |
405 | let node = self.get_or_insert_node(&path); | |
e6dc35ac | 406 | node.insert_user_role(auth_id.to_owned(), role.to_string(), propagate); |
5c6cdf98 DM |
407 | } |
408 | ||
409 | fn write_node_config( | |
410 | node: &AclTreeNode, | |
411 | path: &str, | |
412 | w: &mut dyn Write, | |
413 | ) -> Result<(), Error> { | |
414 | ||
415 | let mut role_ug_map0 = HashMap::new(); | |
416 | let mut role_ug_map1 = HashMap::new(); | |
417 | ||
e6dc35ac | 418 | for (auth_id, roles) in &node.users { |
5c6cdf98 | 419 | // no need to save, because root is always 'Administrator' |
e6dc35ac | 420 | if !auth_id.is_token() && auth_id.user() == "root@pam" { continue; } |
5c6cdf98 DM |
421 | for (role, propagate) in roles { |
422 | let role = role.as_str(); | |
e6dc35ac | 423 | let auth_id = auth_id.to_string(); |
5c6cdf98 | 424 | if *propagate { |
a83eab3c | 425 | role_ug_map1.entry(role).or_insert_with(|| BTreeSet::new()) |
e6dc35ac | 426 | .insert(auth_id); |
5c6cdf98 | 427 | } else { |
a83eab3c | 428 | role_ug_map0.entry(role).or_insert_with(|| BTreeSet::new()) |
e6dc35ac | 429 | .insert(auth_id); |
5c6cdf98 DM |
430 | } |
431 | } | |
432 | } | |
433 | ||
434 | for (group, roles) in &node.groups { | |
435 | for (role, propagate) in roles { | |
436 | let group = format!("@{}", group); | |
437 | if *propagate { | |
a83eab3c | 438 | role_ug_map1.entry(role).or_insert_with(|| BTreeSet::new()) |
5c6cdf98 DM |
439 | .insert(group); |
440 | } else { | |
a83eab3c | 441 | role_ug_map0.entry(role).or_insert_with(|| BTreeSet::new()) |
5c6cdf98 DM |
442 | .insert(group); |
443 | } | |
444 | } | |
445 | } | |
446 | ||
447 | fn group_by_property_list( | |
a83eab3c DM |
448 | item_property_map: &HashMap<&str, BTreeSet<String>>, |
449 | ) -> BTreeMap<String, BTreeSet<String>> { | |
450 | let mut result_map = BTreeMap::new(); | |
5c6cdf98 | 451 | for (item, property_map) in item_property_map { |
a83eab3c DM |
452 | let item_list = property_map.iter().fold(String::new(), |mut acc, v| { |
453 | if !acc.is_empty() { acc.push(','); } | |
454 | acc.push_str(v); | |
455 | acc | |
456 | }); | |
457 | result_map.entry(item_list).or_insert_with(|| BTreeSet::new()) | |
5c6cdf98 DM |
458 | .insert(item.to_string()); |
459 | } | |
460 | result_map | |
461 | } | |
462 | ||
a83eab3c DM |
463 | let uglist_role_map0 = group_by_property_list(&role_ug_map0); |
464 | let uglist_role_map1 = group_by_property_list(&role_ug_map1); | |
5c6cdf98 | 465 | |
8d048af2 DM |
466 | fn role_list(roles: &BTreeSet<String>) -> String { |
467 | if roles.contains(ROLE_NAME_NO_ACCESS) { return String::from(ROLE_NAME_NO_ACCESS); } | |
468 | roles.iter().fold(String::new(), |mut acc, v| { | |
a83eab3c DM |
469 | if !acc.is_empty() { acc.push(','); } |
470 | acc.push_str(v); | |
471 | acc | |
8d048af2 | 472 | }) |
5c6cdf98 DM |
473 | } |
474 | ||
8d048af2 DM |
475 | for (uglist, roles) in &uglist_role_map0 { |
476 | let role_list = role_list(roles); | |
477 | writeln!(w, "acl:0:{}:{}:{}", if path.is_empty() { "/" } else { path }, uglist, role_list)?; | |
478 | } | |
479 | ||
480 | for (uglist, roles) in &uglist_role_map1 { | |
481 | let role_list = role_list(roles); | |
482 | writeln!(w, "acl:1:{}:{}:{}", if path.is_empty() { "/" } else { path }, uglist, role_list)?; | |
5c6cdf98 DM |
483 | } |
484 | ||
a83eab3c | 485 | for (name, child) in node.children.iter() { |
5c6cdf98 DM |
486 | let child_path = format!("{}/{}", path, name); |
487 | Self::write_node_config(child, &child_path, w)?; | |
488 | } | |
489 | ||
490 | Ok(()) | |
491 | } | |
492 | ||
493 | pub fn write_config(&self, w: &mut dyn Write) -> Result<(), Error> { | |
494 | Self::write_node_config(&self.root, "", w) | |
495 | } | |
496 | ||
497 | fn parse_acl_line(&mut self, line: &str) -> Result<(), Error> { | |
498 | ||
499 | let items: Vec<&str> = line.split(':').collect(); | |
500 | ||
501 | if items.len() != 5 { | |
502 | bail!("wrong number of items."); | |
503 | } | |
504 | ||
505 | if items[0] != "acl" { | |
506 | bail!("line does not start with 'acl'."); | |
507 | } | |
508 | ||
509 | let propagate = if items[1] == "0" { | |
510 | false | |
511 | } else if items[1] == "1" { | |
512 | true | |
513 | } else { | |
514 | bail!("expected '0' or '1' for propagate flag."); | |
515 | }; | |
516 | ||
babab85b FG |
517 | let path_str = items[2]; |
518 | let path = split_acl_path(path_str); | |
5c6cdf98 DM |
519 | let node = self.get_or_insert_node(&path); |
520 | ||
521 | let uglist: Vec<&str> = items[3].split(',').map(|v| v.trim()).collect(); | |
522 | ||
523 | let rolelist: Vec<&str> = items[4].split(',').map(|v| v.trim()).collect(); | |
524 | ||
525 | for user_or_group in &uglist { | |
526 | for role in &rolelist { | |
527 | if !ROLE_NAMES.contains_key(role) { | |
528 | bail!("unknown role '{}'", role); | |
529 | } | |
530 | if user_or_group.starts_with('@') { | |
531 | let group = &user_or_group[1..]; | |
532 | node.insert_group_role(group.to_string(), role.to_string(), propagate); | |
533 | } else { | |
e7cb4dc5 | 534 | node.insert_user_role(user_or_group.parse()?, role.to_string(), propagate); |
5c6cdf98 DM |
535 | } |
536 | } | |
537 | } | |
538 | ||
539 | Ok(()) | |
540 | } | |
541 | ||
542 | pub fn load(filename: &Path) -> Result<(Self, [u8;32]), Error> { | |
543 | let mut tree = Self::new(); | |
544 | ||
545 | let raw = match std::fs::read_to_string(filename) { | |
546 | Ok(v) => v, | |
547 | Err(err) => { | |
548 | if err.kind() == std::io::ErrorKind::NotFound { | |
549 | String::new() | |
550 | } else { | |
551 | bail!("unable to read acl config {:?} - {}", filename, err); | |
552 | } | |
553 | } | |
554 | }; | |
555 | ||
556 | let digest = openssl::sha::sha256(raw.as_bytes()); | |
557 | ||
558 | for (linenr, line) in raw.lines().enumerate() { | |
0815ec7e DM |
559 | let line = line.trim(); |
560 | if line.is_empty() { continue; } | |
5c6cdf98 | 561 | if let Err(err) = tree.parse_acl_line(line) { |
0815ec7e DM |
562 | bail!("unable to parse acl config {:?}, line {} - {}", |
563 | filename, linenr+1, err); | |
5c6cdf98 DM |
564 | } |
565 | } | |
566 | ||
567 | Ok((tree, digest)) | |
568 | } | |
0815ec7e DM |
569 | |
570 | pub fn from_raw(raw: &str) -> Result<Self, Error> { | |
571 | let mut tree = Self::new(); | |
572 | for (linenr, line) in raw.lines().enumerate() { | |
573 | let line = line.trim(); | |
574 | if line.is_empty() { continue; } | |
575 | if let Err(err) = tree.parse_acl_line(line) { | |
576 | bail!("unable to parse acl config data, line {} - {}", linenr+1, err); | |
577 | } | |
578 | } | |
579 | Ok(tree) | |
580 | } | |
581 | ||
babab85b | 582 | pub fn roles(&self, auth_id: &Authid, path: &[&str]) -> HashMap<String, bool> { |
0815ec7e DM |
583 | |
584 | let mut node = &self.root; | |
babab85b | 585 | let mut role_map = node.extract_roles(auth_id, path.is_empty()); |
0815ec7e DM |
586 | |
587 | for (pos, comp) in path.iter().enumerate() { | |
588 | let last_comp = (pos + 1) == path.len(); | |
589 | node = match node.children.get(*comp) { | |
590 | Some(n) => n, | |
babab85b | 591 | None => return role_map, // path not found |
0815ec7e | 592 | }; |
babab85b FG |
593 | |
594 | let new_map = node.extract_roles(auth_id, last_comp); | |
595 | if !new_map.is_empty() { | |
596 | // overwrite previous maptings | |
597 | role_map = new_map; | |
0815ec7e DM |
598 | } |
599 | } | |
600 | ||
babab85b | 601 | role_map |
0815ec7e | 602 | } |
5c6cdf98 DM |
603 | } |
604 | ||
605 | pub const ACL_CFG_FILENAME: &str = "/etc/proxmox-backup/acl.cfg"; | |
606 | pub const ACL_CFG_LOCKFILE: &str = "/etc/proxmox-backup/.acl.lck"; | |
607 | ||
608 | pub fn config() -> Result<(AclTree, [u8; 32]), Error> { | |
609 | let path = PathBuf::from(ACL_CFG_FILENAME); | |
610 | AclTree::load(&path) | |
611 | } | |
612 | ||
5354511f DM |
613 | pub fn cached_config() -> Result<Arc<AclTree>, Error> { |
614 | ||
615 | struct ConfigCache { | |
616 | data: Option<Arc<AclTree>>, | |
617 | last_mtime: i64, | |
618 | last_mtime_nsec: i64, | |
619 | } | |
620 | ||
621 | lazy_static! { | |
622 | static ref CACHED_CONFIG: RwLock<ConfigCache> = RwLock::new( | |
623 | ConfigCache { data: None, last_mtime: 0, last_mtime_nsec: 0 }); | |
624 | } | |
625 | ||
b9f2f761 DM |
626 | let stat = match nix::sys::stat::stat(ACL_CFG_FILENAME) { |
627 | Ok(stat) => Some(stat), | |
628 | Err(nix::Error::Sys(nix::errno::Errno::ENOENT)) => None, | |
629 | Err(err) => bail!("unable to stat '{}' - {}", ACL_CFG_FILENAME, err), | |
630 | }; | |
5354511f | 631 | |
bd88dc41 | 632 | { // limit scope |
5354511f | 633 | let cache = CACHED_CONFIG.read().unwrap(); |
bd88dc41 DM |
634 | if let Some(ref config) = cache.data { |
635 | if let Some(stat) = stat { | |
636 | if stat.st_mtime == cache.last_mtime && stat.st_mtime_nsec == cache.last_mtime_nsec { | |
637 | return Ok(config.clone()); | |
638 | } | |
639 | } else if cache.last_mtime == 0 && cache.last_mtime_nsec == 0 { | |
5354511f DM |
640 | return Ok(config.clone()); |
641 | } | |
642 | } | |
643 | } | |
644 | ||
645 | let (config, _digest) = config()?; | |
646 | let config = Arc::new(config); | |
647 | ||
648 | let mut cache = CACHED_CONFIG.write().unwrap(); | |
b9f2f761 DM |
649 | if let Some(stat) = stat { |
650 | cache.last_mtime = stat.st_mtime; | |
651 | cache.last_mtime_nsec = stat.st_mtime_nsec; | |
652 | } | |
5354511f DM |
653 | cache.data = Some(config.clone()); |
654 | ||
655 | Ok(config) | |
656 | } | |
657 | ||
9765092e | 658 | pub fn save_config(acl: &AclTree) -> Result<(), Error> { |
5c6cdf98 DM |
659 | let mut raw: Vec<u8> = Vec::new(); |
660 | ||
661 | acl.write_config(&mut raw)?; | |
662 | ||
663 | let backup_user = crate::backup::backup_user()?; | |
664 | let mode = nix::sys::stat::Mode::from_bits_truncate(0o0640); | |
665 | // set the correct owner/group/permissions while saving file | |
666 | // owner(rw) = root, group(r)= backup | |
667 | let options = CreateOptions::new() | |
668 | .perm(mode) | |
669 | .owner(nix::unistd::ROOT) | |
670 | .group(backup_user.gid); | |
671 | ||
9765092e | 672 | replace_file(ACL_CFG_FILENAME, &raw, options)?; |
5c6cdf98 DM |
673 | |
674 | Ok(()) | |
675 | } | |
0815ec7e | 676 | |
0815ec7e DM |
677 | #[cfg(test)] |
678 | mod test { | |
f7d4e4b5 | 679 | use anyhow::{Error}; |
0815ec7e DM |
680 | use super::AclTree; |
681 | ||
e6dc35ac | 682 | use crate::api2::types::Authid; |
e7cb4dc5 | 683 | |
0815ec7e DM |
684 | fn check_roles( |
685 | tree: &AclTree, | |
e6dc35ac | 686 | auth_id: &Authid, |
0815ec7e DM |
687 | path: &str, |
688 | expected_roles: &str, | |
689 | ) { | |
690 | ||
691 | let path_vec = super::split_acl_path(path); | |
e6dc35ac | 692 | let mut roles = tree.roles(auth_id, &path_vec) |
babab85b | 693 | .iter().map(|(v, _)| v.clone()).collect::<Vec<String>>(); |
0815ec7e DM |
694 | roles.sort(); |
695 | let roles = roles.join(","); | |
696 | ||
e6dc35ac | 697 | assert_eq!(roles, expected_roles, "\nat check_roles for '{}' on '{}'", auth_id, path); |
0815ec7e DM |
698 | } |
699 | ||
700 | #[test] | |
e7cb4dc5 WB |
701 | fn test_acl_line_compression() { |
702 | ||
703 | let tree = AclTree::from_raw( | |
704 | "\ | |
705 | acl:0:/store/store2:user1@pbs:Admin\n\ | |
706 | acl:0:/store/store2:user2@pbs:Admin\n\ | |
707 | acl:0:/store/store2:user1@pbs:DatastoreBackup\n\ | |
708 | acl:0:/store/store2:user2@pbs:DatastoreBackup\n\ | |
709 | ", | |
710 | ) | |
711 | .expect("failed to parse acl tree"); | |
0815ec7e DM |
712 | |
713 | let mut raw: Vec<u8> = Vec::new(); | |
e7cb4dc5 WB |
714 | tree.write_config(&mut raw).expect("failed to write acl tree"); |
715 | let raw = std::str::from_utf8(&raw).expect("acl tree is not valid utf8"); | |
0815ec7e | 716 | |
e7cb4dc5 | 717 | assert_eq!(raw, "acl:0:/store/store2:user1@pbs,user2@pbs:Admin,DatastoreBackup\n"); |
0815ec7e DM |
718 | } |
719 | ||
720 | #[test] | |
721 | fn test_roles_1() -> Result<(), Error> { | |
722 | ||
723 | let tree = AclTree::from_raw(r###" | |
724 | acl:1:/storage:user1@pbs:Admin | |
bc0d0388 DM |
725 | acl:1:/storage/store1:user1@pbs:DatastoreBackup |
726 | acl:1:/storage/store2:user2@pbs:DatastoreBackup | |
0815ec7e | 727 | "###)?; |
e6dc35ac | 728 | let user1: Authid = "user1@pbs".parse()?; |
e7cb4dc5 WB |
729 | check_roles(&tree, &user1, "/", ""); |
730 | check_roles(&tree, &user1, "/storage", "Admin"); | |
731 | check_roles(&tree, &user1, "/storage/store1", "DatastoreBackup"); | |
732 | check_roles(&tree, &user1, "/storage/store2", "Admin"); | |
733 | ||
e6dc35ac | 734 | let user2: Authid = "user2@pbs".parse()?; |
e7cb4dc5 WB |
735 | check_roles(&tree, &user2, "/", ""); |
736 | check_roles(&tree, &user2, "/storage", ""); | |
737 | check_roles(&tree, &user2, "/storage/store1", ""); | |
738 | check_roles(&tree, &user2, "/storage/store2", "DatastoreBackup"); | |
0815ec7e DM |
739 | |
740 | Ok(()) | |
741 | } | |
742 | ||
743 | #[test] | |
744 | fn test_role_no_access() -> Result<(), Error> { | |
745 | ||
746 | let tree = AclTree::from_raw(r###" | |
747 | acl:1:/:user1@pbs:Admin | |
748 | acl:1:/storage:user1@pbs:NoAccess | |
bc0d0388 | 749 | acl:1:/storage/store1:user1@pbs:DatastoreBackup |
0815ec7e | 750 | "###)?; |
e6dc35ac | 751 | let user1: Authid = "user1@pbs".parse()?; |
e7cb4dc5 WB |
752 | check_roles(&tree, &user1, "/", "Admin"); |
753 | check_roles(&tree, &user1, "/storage", "NoAccess"); | |
754 | check_roles(&tree, &user1, "/storage/store1", "DatastoreBackup"); | |
755 | check_roles(&tree, &user1, "/storage/store2", "NoAccess"); | |
756 | check_roles(&tree, &user1, "/system", "Admin"); | |
0815ec7e DM |
757 | |
758 | let tree = AclTree::from_raw(r###" | |
759 | acl:1:/:user1@pbs:Admin | |
760 | acl:0:/storage:user1@pbs:NoAccess | |
bc0d0388 | 761 | acl:1:/storage/store1:user1@pbs:DatastoreBackup |
0815ec7e | 762 | "###)?; |
e7cb4dc5 WB |
763 | check_roles(&tree, &user1, "/", "Admin"); |
764 | check_roles(&tree, &user1, "/storage", "NoAccess"); | |
765 | check_roles(&tree, &user1, "/storage/store1", "DatastoreBackup"); | |
766 | check_roles(&tree, &user1, "/storage/store2", "Admin"); | |
767 | check_roles(&tree, &user1, "/system", "Admin"); | |
0815ec7e DM |
768 | |
769 | Ok(()) | |
770 | } | |
8d048af2 DM |
771 | |
772 | #[test] | |
773 | fn test_role_add_delete() -> Result<(), Error> { | |
774 | ||
775 | let mut tree = AclTree::new(); | |
776 | ||
e6dc35ac | 777 | let user1: Authid = "user1@pbs".parse()?; |
8d048af2 | 778 | |
e7cb4dc5 WB |
779 | tree.insert_user_role("/", &user1, "Admin", true); |
780 | tree.insert_user_role("/", &user1, "Audit", true); | |
8d048af2 | 781 | |
e7cb4dc5 WB |
782 | check_roles(&tree, &user1, "/", "Admin,Audit"); |
783 | ||
784 | tree.insert_user_role("/", &user1, "NoAccess", true); | |
785 | check_roles(&tree, &user1, "/", "NoAccess"); | |
8d048af2 DM |
786 | |
787 | let mut raw: Vec<u8> = Vec::new(); | |
788 | tree.write_config(&mut raw)?; | |
789 | let raw = std::str::from_utf8(&raw)?; | |
790 | ||
791 | assert_eq!(raw, "acl:1:/:user1@pbs:NoAccess\n"); | |
792 | ||
793 | Ok(()) | |
794 | } | |
795 | ||
796 | #[test] | |
797 | fn test_no_access_overwrite() -> Result<(), Error> { | |
798 | ||
799 | let mut tree = AclTree::new(); | |
800 | ||
e6dc35ac | 801 | let user1: Authid = "user1@pbs".parse()?; |
e7cb4dc5 WB |
802 | |
803 | tree.insert_user_role("/storage", &user1, "NoAccess", true); | |
8d048af2 | 804 | |
e7cb4dc5 | 805 | check_roles(&tree, &user1, "/storage", "NoAccess"); |
8d048af2 | 806 | |
e7cb4dc5 WB |
807 | tree.insert_user_role("/storage", &user1, "Admin", true); |
808 | tree.insert_user_role("/storage", &user1, "Audit", true); | |
8d048af2 | 809 | |
e7cb4dc5 | 810 | check_roles(&tree, &user1, "/storage", "Admin,Audit"); |
8d048af2 | 811 | |
e7cb4dc5 | 812 | tree.insert_user_role("/storage", &user1, "NoAccess", true); |
8d048af2 | 813 | |
e7cb4dc5 | 814 | check_roles(&tree, &user1, "/storage", "NoAccess"); |
8d048af2 DM |
815 | |
816 | Ok(()) | |
817 | } | |
0815ec7e | 818 | } |