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