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