]> git.proxmox.com Git - proxmox-backup.git/blob - src/config/acl.rs
start ACL api
[proxmox-backup.git] / src / config / acl.rs
1 use std::io::Write;
2 use std::collections::{HashMap, HashSet, BTreeMap, BTreeSet};
3 use std::path::{PathBuf, Path};
4
5 use failure::*;
6
7 use lazy_static::lazy_static;
8
9 use proxmox::tools::{fs::replace_file, fs::CreateOptions};
10
11 // define Privilege bitfield
12
13 pub const PRIV_SYS_AUDIT: u64 = 1 << 0;
14 pub const PRIV_SYS_MODIFY: u64 = 1 << 1;
15 pub const PRIV_SYS_POWER_MANAGEMENT: u64 = 1 << 2;
16
17 pub const PRIV_STORE_AUDIT: u64 = 1 << 3;
18 pub const PRIV_STORE_ALLOCATE: u64 = 1 << 4;
19 pub const PRIV_STORE_ALLOCATE_SPACE: u64 = 1 << 5;
20
21 pub const ROLE_ADMIN: u64 = std::u64::MAX;
22 pub const ROLE_NO_ACCESS: u64 = 0;
23
24 pub const ROLE_AUDIT: u64 =
25 PRIV_SYS_AUDIT |
26 PRIV_STORE_AUDIT;
27
28 pub const ROLE_STORE_ADMIN: u64 =
29 PRIV_STORE_AUDIT |
30 PRIV_STORE_ALLOCATE |
31 PRIV_STORE_ALLOCATE_SPACE;
32
33 pub const ROLE_STORE_USER: u64 =
34 PRIV_STORE_AUDIT |
35 PRIV_STORE_ALLOCATE_SPACE;
36
37 lazy_static! {
38 static ref ROLE_NAMES: HashMap<&'static str, u64> = {
39 let mut map = HashMap::new();
40
41 map.insert("Admin", ROLE_ADMIN);
42 map.insert("Audit", ROLE_AUDIT);
43 map.insert("NoAccess", ROLE_NO_ACCESS);
44
45 map.insert("Store.Admin", ROLE_STORE_ADMIN);
46 map.insert("Store.User", ROLE_STORE_USER);
47
48 map
49 };
50 }
51
52 fn split_acl_path(path: &str) -> Vec<&str> {
53
54 let items = path.split('/');
55
56 let mut components = vec![];
57
58 for name in items {
59 if name.is_empty() { continue; }
60 components.push(name);
61 }
62
63 components
64 }
65
66 pub struct AclTree {
67 pub root: AclTreeNode,
68 }
69
70 pub struct AclTreeNode {
71 pub users: HashMap<String, HashMap<String, bool>>,
72 pub groups: HashMap<String, HashMap<String, bool>>,
73 pub children: BTreeMap<String, AclTreeNode>,
74 }
75
76 impl AclTreeNode {
77
78 pub fn new() -> Self {
79 Self {
80 users: HashMap::new(),
81 groups: HashMap::new(),
82 children: BTreeMap::new(),
83 }
84 }
85
86 pub fn extract_roles(&self, user: &str, all: bool) -> HashSet<String> {
87 let user_roles = self.extract_user_roles(user, all);
88 if !user_roles.is_empty() {
89 // user privs always override group privs
90 return user_roles
91 };
92
93 self.extract_group_roles(user, all)
94 }
95
96 pub fn extract_user_roles(&self, user: &str, all: bool) -> HashSet<String> {
97
98 let mut set = HashSet::new();
99
100 let roles = match self.users.get(user) {
101 Some(m) => m,
102 None => return set,
103 };
104
105 for (role, propagate) in roles {
106 if *propagate || all {
107 if role == "NoAccess" {
108 // return a set with a single role 'NoAccess'
109 let mut set = HashSet::new();
110 set.insert(role.to_string());
111 return set;
112 }
113 set.insert(role.to_string());
114 }
115 }
116
117 set
118 }
119
120 pub fn extract_group_roles(&self, _user: &str, all: bool) -> HashSet<String> {
121
122 let mut set = HashSet::new();
123
124 for (_group, roles) in &self.groups {
125 let is_member = false; // fixme: check if user is member of the group
126 if !is_member { continue; }
127
128 for (role, propagate) in roles {
129 if *propagate || all {
130 if role == "NoAccess" {
131 // return a set with a single role 'NoAccess'
132 let mut set = HashSet::new();
133 set.insert(role.to_string());
134 return set;
135 }
136 set.insert(role.to_string());
137 }
138 }
139 }
140
141 set
142 }
143
144 pub fn insert_group_role(&mut self, group: String, role: String, propagate: bool) {
145 self.groups
146 .entry(group).or_insert_with(|| HashMap::new())
147 .insert(role, propagate);
148 }
149
150 pub fn insert_user_role(&mut self, user: String, role: String, propagate: bool) {
151 self.users
152 .entry(user).or_insert_with(|| HashMap::new())
153 .insert(role, propagate);
154 }
155 }
156
157 impl AclTree {
158
159 pub fn new() -> Self {
160 Self { root: AclTreeNode::new() }
161 }
162
163 fn get_or_insert_node(&mut self, path: &[&str]) -> &mut AclTreeNode {
164 let mut node = &mut self.root;
165 for comp in path {
166 node = node.children.entry(String::from(*comp))
167 .or_insert_with(|| AclTreeNode::new());
168 }
169 node
170 }
171
172 pub fn insert_group_role(&mut self, path: &str, group: &str, role: &str, propagate: bool) {
173 let path = split_acl_path(path);
174 let node = self.get_or_insert_node(&path);
175 node.insert_group_role(group.to_string(), role.to_string(), propagate);
176 }
177
178 pub fn insert_user_role(&mut self, path: &str, user: &str, role: &str, propagate: bool) {
179 let path = split_acl_path(path);
180 let node = self.get_or_insert_node(&path);
181 node.insert_user_role(user.to_string(), role.to_string(), propagate);
182 }
183
184 fn write_node_config(
185 node: &AclTreeNode,
186 path: &str,
187 w: &mut dyn Write,
188 ) -> Result<(), Error> {
189
190 let mut role_ug_map0 = HashMap::new();
191 let mut role_ug_map1 = HashMap::new();
192
193 for (user, roles) in &node.users {
194 // no need to save, because root is always 'Administrator'
195 if user == "root@pam" { continue; }
196 for (role, propagate) in roles {
197 let role = role.as_str();
198 let user = user.to_string();
199 if *propagate {
200 role_ug_map1.entry(role).or_insert_with(|| BTreeSet::new())
201 .insert(user);
202 } else {
203 role_ug_map0.entry(role).or_insert_with(|| BTreeSet::new())
204 .insert(user);
205 }
206 }
207 }
208
209 for (group, roles) in &node.groups {
210 for (role, propagate) in roles {
211 let group = format!("@{}", group);
212 if *propagate {
213 role_ug_map1.entry(role).or_insert_with(|| BTreeSet::new())
214 .insert(group);
215 } else {
216 role_ug_map0.entry(role).or_insert_with(|| BTreeSet::new())
217 .insert(group);
218 }
219 }
220 }
221
222 fn group_by_property_list(
223 item_property_map: &HashMap<&str, BTreeSet<String>>,
224 ) -> BTreeMap<String, BTreeSet<String>> {
225 let mut result_map = BTreeMap::new();
226 for (item, property_map) in item_property_map {
227 let item_list = property_map.iter().fold(String::new(), |mut acc, v| {
228 if !acc.is_empty() { acc.push(','); }
229 acc.push_str(v);
230 acc
231 });
232 result_map.entry(item_list).or_insert_with(|| BTreeSet::new())
233 .insert(item.to_string());
234 }
235 result_map
236 }
237
238 let uglist_role_map0 = group_by_property_list(&role_ug_map0);
239 let uglist_role_map1 = group_by_property_list(&role_ug_map1);
240
241 for (uglist, roles) in uglist_role_map0 {
242 let role_list = roles.iter().fold(String::new(), |mut acc, v| {
243 if !acc.is_empty() { acc.push(','); }
244 acc.push_str(v);
245 acc
246 });
247 writeln!(w, "acl:0:{}:{}:{}", path, uglist, role_list)?;
248 }
249
250 for (uglist, roles) in uglist_role_map1 {
251 let role_list = roles.iter().fold(String::new(), |mut acc, v| {
252 if !acc.is_empty() { acc.push(','); }
253 acc.push_str(v);
254 acc
255 });
256 writeln!(w, "acl:1:{}:{}:{}", path, uglist, role_list)?;
257 }
258
259 for (name, child) in node.children.iter() {
260 let child_path = format!("{}/{}", path, name);
261 Self::write_node_config(child, &child_path, w)?;
262 }
263
264 Ok(())
265 }
266
267 pub fn write_config(&self, w: &mut dyn Write) -> Result<(), Error> {
268 Self::write_node_config(&self.root, "", w)
269 }
270
271 fn parse_acl_line(&mut self, line: &str) -> Result<(), Error> {
272
273 let items: Vec<&str> = line.split(':').collect();
274
275 if items.len() != 5 {
276 bail!("wrong number of items.");
277 }
278
279 if items[0] != "acl" {
280 bail!("line does not start with 'acl'.");
281 }
282
283 let propagate = if items[1] == "0" {
284 false
285 } else if items[1] == "1" {
286 true
287 } else {
288 bail!("expected '0' or '1' for propagate flag.");
289 };
290
291 let path = split_acl_path(items[2]);
292 let node = self.get_or_insert_node(&path);
293
294 let uglist: Vec<&str> = items[3].split(',').map(|v| v.trim()).collect();
295
296 let rolelist: Vec<&str> = items[4].split(',').map(|v| v.trim()).collect();
297
298 for user_or_group in &uglist {
299 for role in &rolelist {
300 if !ROLE_NAMES.contains_key(role) {
301 bail!("unknown role '{}'", role);
302 }
303 if user_or_group.starts_with('@') {
304 let group = &user_or_group[1..];
305 node.insert_group_role(group.to_string(), role.to_string(), propagate);
306 } else {
307 node.insert_user_role(user_or_group.to_string(), role.to_string(), propagate);
308 }
309 }
310 }
311
312 Ok(())
313 }
314
315 pub fn load(filename: &Path) -> Result<(Self, [u8;32]), Error> {
316 let mut tree = Self::new();
317
318 let raw = match std::fs::read_to_string(filename) {
319 Ok(v) => v,
320 Err(err) => {
321 if err.kind() == std::io::ErrorKind::NotFound {
322 String::new()
323 } else {
324 bail!("unable to read acl config {:?} - {}", filename, err);
325 }
326 }
327 };
328
329 let digest = openssl::sha::sha256(raw.as_bytes());
330
331 for (linenr, line) in raw.lines().enumerate() {
332 let line = line.trim();
333 if line.is_empty() { continue; }
334 if let Err(err) = tree.parse_acl_line(line) {
335 bail!("unable to parse acl config {:?}, line {} - {}",
336 filename, linenr+1, err);
337 }
338 }
339
340 Ok((tree, digest))
341 }
342
343 pub fn from_raw(raw: &str) -> Result<Self, Error> {
344 let mut tree = Self::new();
345 for (linenr, line) in raw.lines().enumerate() {
346 let line = line.trim();
347 if line.is_empty() { continue; }
348 if let Err(err) = tree.parse_acl_line(line) {
349 bail!("unable to parse acl config data, line {} - {}", linenr+1, err);
350 }
351 }
352 Ok(tree)
353 }
354
355 pub fn roles(&self, userid: &str, path: &[&str]) -> HashSet<String> {
356
357 let mut node = &self.root;
358 let mut role_set = node.extract_roles(userid, path.is_empty());
359
360 for (pos, comp) in path.iter().enumerate() {
361 let last_comp = (pos + 1) == path.len();
362 node = match node.children.get(*comp) {
363 Some(n) => n,
364 None => return role_set, // path not found
365 };
366 let new_set = node.extract_roles(userid, last_comp);
367 if !new_set.is_empty() {
368 // overwrite previous settings
369 role_set = new_set;
370 }
371 }
372
373 role_set
374 }
375 }
376
377 pub const ACL_CFG_FILENAME: &str = "/etc/proxmox-backup/acl.cfg";
378 pub const ACL_CFG_LOCKFILE: &str = "/etc/proxmox-backup/.acl.lck";
379
380 pub fn config() -> Result<(AclTree, [u8; 32]), Error> {
381 let path = PathBuf::from(ACL_CFG_FILENAME);
382 AclTree::load(&path)
383 }
384
385 pub fn store_config(acl: &AclTree, filename: &Path) -> Result<(), Error> {
386 let mut raw: Vec<u8> = Vec::new();
387
388 acl.write_config(&mut raw)?;
389
390 let backup_user = crate::backup::backup_user()?;
391 let mode = nix::sys::stat::Mode::from_bits_truncate(0o0640);
392 // set the correct owner/group/permissions while saving file
393 // owner(rw) = root, group(r)= backup
394 let options = CreateOptions::new()
395 .perm(mode)
396 .owner(nix::unistd::ROOT)
397 .group(backup_user.gid);
398
399 replace_file(filename, &raw, options)?;
400
401 Ok(())
402 }
403
404
405 #[cfg(test)]
406 mod test {
407
408 use failure::*;
409 use super::AclTree;
410
411 fn check_roles(
412 tree: &AclTree,
413 user: &str,
414 path: &str,
415 expected_roles: &str,
416 ) {
417
418 let path_vec = super::split_acl_path(path);
419 let mut roles = tree.roles(user, &path_vec)
420 .iter().map(|v| v.clone()).collect::<Vec<String>>();
421 roles.sort();
422 let roles = roles.join(",");
423
424 assert_eq!(roles, expected_roles, "\nat check_roles for '{}' on '{}'", user, path);
425 }
426
427 #[test]
428 fn test_acl_line_compression() -> Result<(), Error> {
429
430 let tree = AclTree::from_raw(r###"
431 acl:0:/store/store2:user1:Admin
432 acl:0:/store/store2:user2:Admin
433 acl:0:/store/store2:user1:Store.User
434 acl:0:/store/store2:user2:Store.User
435 "###)?;
436
437 let mut raw: Vec<u8> = Vec::new();
438 tree.write_config(&mut raw)?;
439 let raw = std::str::from_utf8(&raw)?;
440
441 assert_eq!(raw, "acl:0:/store/store2:user1,user2:Admin,Store.User\n");
442
443 Ok(())
444 }
445
446 #[test]
447 fn test_roles_1() -> Result<(), Error> {
448
449 let tree = AclTree::from_raw(r###"
450 acl:1:/storage:user1@pbs:Admin
451 acl:1:/storage/store1:user1@pbs:Store.User
452 acl:1:/storage/store2:user2@pbs:Store.User
453 "###)?;
454 check_roles(&tree, "user1@pbs", "/", "");
455 check_roles(&tree, "user1@pbs", "/storage", "Admin");
456 check_roles(&tree, "user1@pbs", "/storage/store1", "Store.User");
457 check_roles(&tree, "user1@pbs", "/storage/store2", "Admin");
458
459 check_roles(&tree, "user2@pbs", "/", "");
460 check_roles(&tree, "user2@pbs", "/storage", "");
461 check_roles(&tree, "user2@pbs", "/storage/store1", "");
462 check_roles(&tree, "user2@pbs", "/storage/store2", "Store.User");
463
464 Ok(())
465 }
466
467 #[test]
468 fn test_role_no_access() -> Result<(), Error> {
469
470 let tree = AclTree::from_raw(r###"
471 acl:1:/:user1@pbs:Admin
472 acl:1:/storage:user1@pbs:NoAccess
473 acl:1:/storage/store1:user1@pbs:Store.User
474 "###)?;
475 check_roles(&tree, "user1@pbs", "/", "Admin");
476 check_roles(&tree, "user1@pbs", "/storage", "NoAccess");
477 check_roles(&tree, "user1@pbs", "/storage/store1", "Store.User");
478 check_roles(&tree, "user1@pbs", "/storage/store2", "NoAccess");
479 check_roles(&tree, "user1@pbs", "/system", "Admin");
480
481 let tree = AclTree::from_raw(r###"
482 acl:1:/:user1@pbs:Admin
483 acl:0:/storage:user1@pbs:NoAccess
484 acl:1:/storage/store1:user1@pbs:Store.User
485 "###)?;
486 check_roles(&tree, "user1@pbs", "/", "Admin");
487 check_roles(&tree, "user1@pbs", "/storage", "NoAccess");
488 check_roles(&tree, "user1@pbs", "/storage/store1", "Store.User");
489 check_roles(&tree, "user1@pbs", "/storage/store2", "Admin");
490 check_roles(&tree, "user1@pbs", "/system", "Admin");
491
492 Ok(())
493 }
494 }