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