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