]> git.proxmox.com Git - proxmox-backup.git/blame - src/config/acl.rs
privs: remove PRIV_REMOVE_PRUNE
[proxmox-backup.git] / src / config / acl.rs
CommitLineData
5c6cdf98 1use std::io::Write;
babab85b 2use std::collections::{HashMap, BTreeMap, BTreeSet};
5c6cdf98 3use std::path::{PathBuf, Path};
5354511f 4use std::sync::{Arc, RwLock};
bc0d0388 5use std::str::FromStr;
5c6cdf98 6
f7d4e4b5 7use anyhow::{bail, Error};
5c6cdf98
DM
8
9use lazy_static::lazy_static;
10
bc0d0388
DM
11use ::serde::{Deserialize, Serialize};
12use serde::de::{value, IntoDeserializer};
13
5c6cdf98 14use proxmox::tools::{fs::replace_file, fs::CreateOptions};
fddc8aa4 15use proxmox::constnamedbitmap;
bc0d0388 16use proxmox::api::{api, schema::*};
5c6cdf98 17
e6dc35ac 18use crate::api2::types::{Authid,Userid};
e7cb4dc5 19
5c6cdf98
DM
20// define Privilege bitfield
21
fddc8aa4 22constnamedbitmap! {
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 53pub const ROLE_ADMIN: u64 = std::u64::MAX;
05be0984
TL
54
55/// NoAccess can be used to remove privileges from specific paths
0815ec7e
DM
56pub const ROLE_NO_ACCESS: u64 = 0;
57
5c6cdf98
DM
58pub const ROLE_AUDIT: u64 =
59PRIV_SYS_AUDIT |
9765092e 60PRIV_DATASTORE_AUDIT;
5c6cdf98 61
6f6aa95a 62/// Datastore.Admin can do anything on the datastore.
9765092e
DM
63pub const ROLE_DATASTORE_ADMIN: u64 =
64PRIV_DATASTORE_AUDIT |
9c7fe29d 65PRIV_DATASTORE_MODIFY |
d00e1a21 66PRIV_DATASTORE_READ |
09f6a240 67PRIV_DATASTORE_VERIFY |
54552dda 68PRIV_DATASTORE_BACKUP |
d00e1a21 69PRIV_DATASTORE_PRUNE;
5c6cdf98 70
09f6a240 71/// Datastore.Reader can read/verify datastore content and do restore
6f6aa95a
DM
72pub const ROLE_DATASTORE_READER: u64 =
73PRIV_DATASTORE_AUDIT |
09f6a240 74PRIV_DATASTORE_VERIFY |
6f6aa95a
DM
75PRIV_DATASTORE_READ;
76
77/// Datastore.Backup can do backup and restore, but no prune.
78pub const ROLE_DATASTORE_BACKUP: u64 =
79PRIV_DATASTORE_BACKUP;
80
81/// Datastore.PowerUser can do backup, restore, and prune.
82pub const ROLE_DATASTORE_POWERUSER: u64 =
83PRIV_DATASTORE_PRUNE |
54552dda 84PRIV_DATASTORE_BACKUP;
d00e1a21 85
6f6aa95a 86/// Datastore.Audit can audit the datastore.
d00e1a21
DM
87pub const ROLE_DATASTORE_AUDIT: u64 =
88PRIV_DATASTORE_AUDIT;
9765092e 89
8247db5b
DM
90/// Remote.Audit can audit the remote
91pub const ROLE_REMOTE_AUDIT: u64 =
92PRIV_REMOTE_AUDIT;
93
94/// Remote.Admin can do anything on the remote.
95pub const ROLE_REMOTE_ADMIN: u64 =
96PRIV_REMOTE_AUDIT |
97PRIV_REMOTE_MODIFY |
2fc45a97 98PRIV_REMOTE_READ;
8247db5b
DM
99
100/// Remote.SyncOperator can do read and prune on the remote.
101pub const ROLE_REMOTE_SYNC_OPERATOR: u64 =
102PRIV_REMOTE_AUDIT |
2fc45a97 103PRIV_REMOTE_READ;
8247db5b 104
8d048af2 105pub const ROLE_NAME_NO_ACCESS: &str ="NoAccess";
5c6cdf98 106
bc0d0388
DM
107#[api()]
108#[repr(u64)]
109#[derive(Serialize, Deserialize)]
110/// Role
111pub 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
136impl 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 144lazy_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 162pub 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
176pub 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 229pub struct AclTree {
ed3e60ae 230 pub root: AclTreeNode,
5c6cdf98
DM
231}
232
ed3e60ae 233pub 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
239impl 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
346impl 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
605pub const ACL_CFG_FILENAME: &str = "/etc/proxmox-backup/acl.cfg";
606pub const ACL_CFG_LOCKFILE: &str = "/etc/proxmox-backup/.acl.lck";
607
608pub fn config() -> Result<(AclTree, [u8; 32]), Error> {
609 let path = PathBuf::from(ACL_CFG_FILENAME);
610 AclTree::load(&path)
611}
612
5354511f
DM
613pub 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 658pub 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)]
678mod 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###"
724acl:1:/storage:user1@pbs:Admin
bc0d0388
DM
725acl:1:/storage/store1:user1@pbs:DatastoreBackup
726acl: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###"
747acl:1:/:user1@pbs:Admin
748acl:1:/storage:user1@pbs:NoAccess
bc0d0388 749acl: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###"
759acl:1:/:user1@pbs:Admin
760acl:0:/storage:user1@pbs:NoAccess
bc0d0388 761acl: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}