]>
Commit | Line | Data |
---|---|---|
a7a5406c | 1 | use std::collections::{BTreeMap, BTreeSet, HashMap}; |
5c6cdf98 | 2 | use std::io::Write; |
a7a5406c | 3 | use std::path::{Path, PathBuf}; |
bc0d0388 | 4 | use std::str::FromStr; |
a7a5406c | 5 | use std::sync::{Arc, RwLock}; |
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 | ||
bc0d0388 | 14 | use proxmox::api::{api, schema::*}; |
a7a5406c | 15 | use proxmox::constnamedbitmap; |
5c6cdf98 | 16 | |
a7a5406c | 17 | use crate::api2::types::{Authid, Userid}; |
e7cb4dc5 | 18 | |
5c6cdf98 DM |
19 | // define Privilege bitfield |
20 | ||
fddc8aa4 | 21 | constnamedbitmap! { |
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 | 82 | pub const ROLE_ADMIN: u64 = std::u64::MAX; |
05be0984 | 83 | |
23dc68fd | 84 | /// NoAccess can be used to remove privileges from specific (sub-)paths |
0815ec7e DM |
85 | pub 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 |
90 | pub 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 |
97 | pub 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 |
108 | pub 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 |
116 | pub 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 |
122 | pub 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 |
129 | pub 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 |
135 | pub 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 |
141 | pub 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 |
149 | pub 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 | |
156 | pub 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 | |
162 | pub 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) | |
171 | pub 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 | |
179 | pub 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 | 184 | pub 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 |
195 | pub 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 | ||
228 | impl 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 | 236 | lazy_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 | 256 | pub(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 | 274 | pub 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 | 386 | pub 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 | 396 | pub 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 | ||
406 | impl 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 | ||
522 | impl 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 | 844 | pub const ACL_CFG_FILENAME: &str = "/etc/proxmox-backup/acl.cfg"; |
23dc68fd | 845 | /// Path used to lock the [AclTree] when modifying. |
5c6cdf98 DM |
846 | pub 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 |
849 | pub 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 | 858 | pub 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 | 909 | pub 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)] |
918 | mod 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 | 968 | acl:1:/storage:user1@pbs:Admin |
bc0d0388 DM |
969 | acl:1:/storage/store1:user1@pbs:DatastoreBackup |
970 | acl: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 |
992 | acl:1:/:user1@pbs:Admin |
993 | acl:1:/storage:user1@pbs:NoAccess | |
bc0d0388 | 994 | acl: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 |
1006 | acl:1:/:user1@pbs:Admin |
1007 | acl:0:/storage:user1@pbs:NoAccess | |
bc0d0388 | 1008 | acl: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 | } |