]>
Commit | Line | Data |
---|---|---|
5c6cdf98 | 1 | use std::io::Write; |
babab85b | 2 | use std::collections::{HashMap, BTreeMap, BTreeSet}; |
5c6cdf98 | 3 | use std::path::{PathBuf, Path}; |
5354511f | 4 | use std::sync::{Arc, RwLock}; |
bc0d0388 | 5 | use std::str::FromStr; |
5c6cdf98 | 6 | |
f7d4e4b5 | 7 | use anyhow::{bail, Error}; |
5c6cdf98 DM |
8 | |
9 | use lazy_static::lazy_static; | |
10 | ||
bc0d0388 DM |
11 | use ::serde::{Deserialize, Serialize}; |
12 | use serde::de::{value, IntoDeserializer}; | |
13 | ||
5c6cdf98 | 14 | use proxmox::tools::{fs::replace_file, fs::CreateOptions}; |
fddc8aa4 | 15 | use proxmox::constnamedbitmap; |
bc0d0388 | 16 | use proxmox::api::{api, schema::*}; |
5c6cdf98 | 17 | |
e6dc35ac | 18 | use crate::api2::types::{Authid,Userid}; |
e7cb4dc5 | 19 | |
5c6cdf98 DM |
20 | // define Privilege bitfield |
21 | ||
fddc8aa4 | 22 | constnamedbitmap! { |
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 | 72 | pub const ROLE_ADMIN: u64 = std::u64::MAX; |
05be0984 | 73 | |
23dc68fd | 74 | /// NoAccess can be used to remove privileges from specific (sub-)paths |
0815ec7e DM |
75 | pub 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 |
79 | pub 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 |
85 | pub 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 |
95 | pub 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 |
102 | pub 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 |
107 | pub 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 |
113 | pub 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 |
118 | pub 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 |
123 | pub 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 |
130 | pub 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 | 135 | pub 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 |
144 | pub 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 | ||
169 | impl 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 | 177 | lazy_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 | 197 | pub(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 |
213 | pub 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 | 267 | pub 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 | 276 | pub 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 | ||
286 | impl 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 | ||
402 | impl 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 | 689 | pub const ACL_CFG_FILENAME: &str = "/etc/proxmox-backup/acl.cfg"; |
23dc68fd | 690 | /// Path used to lock the [AclTree] when modifying. |
5c6cdf98 DM |
691 | pub 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 |
694 | pub 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 |
703 | pub 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 | 750 | pub 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)] |
770 | mod 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###" | |
816 | acl:1:/storage:user1@pbs:Admin | |
bc0d0388 DM |
817 | acl:1:/storage/store1:user1@pbs:DatastoreBackup |
818 | acl: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###" | |
839 | acl:1:/:user1@pbs:Admin | |
840 | acl:1:/storage:user1@pbs:NoAccess | |
bc0d0388 | 841 | acl: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###" | |
851 | acl:1:/:user1@pbs:Admin | |
852 | acl:0:/storage:user1@pbs:NoAccess | |
bc0d0388 | 853 | acl: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 | } |