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