]>
Commit | Line | Data |
---|---|---|
bf78f708 DM |
1 | //! User Management |
2 | ||
08ac90f9 | 3 | use anyhow::{bail, format_err, Error}; |
6746bbb1 | 4 | use serde::{Serialize, Deserialize}; |
942078c4 | 5 | use serde_json::{json, Value}; |
6746bbb1 | 6 | use std::collections::HashMap; |
579728c6 | 7 | |
6ef1b649 WB |
8 | use proxmox_router::{ApiMethod, Router, RpcEnvironment, SubdirMap, Permission}; |
9 | use proxmox_schema::api; | |
579728c6 | 10 | |
2b7f8dd5 | 11 | use pbs_api_types::{ |
b65dfff5 DM |
12 | PROXMOX_CONFIG_DIGEST_SCHEMA, SINGLE_LINE_COMMENT_SCHEMA, Authid, |
13 | Tokenname, UserWithTokens, Userid, User, UserUpdater, ApiToken, | |
14 | ENABLE_USER_SCHEMA, EXPIRE_USER_SCHEMA, PBS_PASSWORD_SCHEMA, | |
15 | PRIV_SYS_AUDIT, PRIV_PERMISSIONS_MODIFY, | |
2b7f8dd5 | 16 | }; |
1cb08a0a | 17 | use pbs_config::token_shadow; |
2b7f8dd5 | 18 | |
ba3d7e19 | 19 | use pbs_config::CachedUserInfo; |
579728c6 | 20 | |
b65dfff5 | 21 | fn new_user_with_tokens(user: User) -> UserWithTokens { |
2b7f8dd5 WB |
22 | UserWithTokens { |
23 | userid: user.userid, | |
24 | comment: user.comment, | |
25 | enable: user.enable, | |
26 | expire: user.expire, | |
27 | firstname: user.firstname, | |
28 | lastname: user.lastname, | |
29 | email: user.email, | |
30 | tokens: Vec::new(), | |
6746bbb1 FG |
31 | } |
32 | } | |
33 | ||
579728c6 DM |
34 | #[api( |
35 | input: { | |
6746bbb1 FG |
36 | properties: { |
37 | include_tokens: { | |
38 | type: bool, | |
39 | description: "Include user's API tokens in returned list.", | |
40 | optional: true, | |
41 | default: false, | |
42 | }, | |
43 | }, | |
579728c6 DM |
44 | }, |
45 | returns: { | |
46 | description: "List users (with config digest).", | |
47 | type: Array, | |
906ef6c5 | 48 | items: { type: UserWithTokens }, |
579728c6 | 49 | }, |
d4f020f4 | 50 | access: { |
be3bd0f9 | 51 | permission: &Permission::Anybody, |
08ac90f9 | 52 | description: "Returns all or just the logged-in user (/API token owner), depending on privileges.", |
d4f020f4 | 53 | }, |
579728c6 | 54 | )] |
be3bd0f9 | 55 | /// List users |
579728c6 | 56 | pub fn list_users( |
6746bbb1 | 57 | include_tokens: bool, |
579728c6 | 58 | _info: &ApiMethod, |
522c0da0 | 59 | mut rpcenv: &mut dyn RpcEnvironment, |
6746bbb1 | 60 | ) -> Result<Vec<UserWithTokens>, Error> { |
579728c6 | 61 | |
ba3d7e19 | 62 | let (config, digest) = pbs_config::user::config()?; |
579728c6 | 63 | |
08ac90f9 FG |
64 | let auth_id: Authid = rpcenv |
65 | .get_auth_id() | |
66 | .ok_or_else(|| format_err!("no authid available"))? | |
67 | .parse()?; | |
68 | ||
69 | let userid = auth_id.user(); | |
e6dc35ac | 70 | |
be3bd0f9 FG |
71 | let user_info = CachedUserInfo::new()?; |
72 | ||
e6dc35ac | 73 | let top_level_privs = user_info.lookup_privs(&auth_id, &["access", "users"]); |
be3bd0f9 FG |
74 | let top_level_allowed = (top_level_privs & PRIV_SYS_AUDIT) != 0; |
75 | ||
b65dfff5 | 76 | let filter_by_privs = |user: &User| { |
08ac90f9 | 77 | top_level_allowed || user.userid == *userid |
be3bd0f9 FG |
78 | }; |
79 | ||
6746bbb1 | 80 | |
b65dfff5 | 81 | let list:Vec<User> = config.convert_to_typed_array("user")?; |
522c0da0 DC |
82 | |
83 | rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into(); | |
579728c6 | 84 | |
6746bbb1 FG |
85 | let iter = list.into_iter().filter(filter_by_privs); |
86 | let list = if include_tokens { | |
b65dfff5 | 87 | let tokens: Vec<ApiToken> = config.convert_to_typed_array("token")?; |
6746bbb1 FG |
88 | let mut user_to_tokens = tokens |
89 | .into_iter() | |
90 | .fold( | |
91 | HashMap::new(), | |
b65dfff5 | 92 | |mut map: HashMap<Userid, Vec<ApiToken>>, token: ApiToken| { |
6746bbb1 FG |
93 | if token.tokenid.is_token() { |
94 | map | |
95 | .entry(token.tokenid.user().clone()) | |
96 | .or_default() | |
97 | .push(token); | |
98 | } | |
99 | map | |
100 | }); | |
101 | iter | |
b65dfff5 | 102 | .map(|user: User| { |
2b7f8dd5 | 103 | let mut user = new_user_with_tokens(user); |
ea1853a1 | 104 | user.tokens = user_to_tokens.remove(&user.userid).unwrap_or_default(); |
6746bbb1 FG |
105 | user |
106 | }) | |
107 | .collect() | |
108 | } else { | |
2b7f8dd5 | 109 | iter.map(new_user_with_tokens) |
6746bbb1 FG |
110 | .collect() |
111 | }; | |
112 | ||
113 | Ok(list) | |
579728c6 DM |
114 | } |
115 | ||
116 | #[api( | |
117 | protected: true, | |
118 | input: { | |
119 | properties: { | |
b65dfff5 DM |
120 | config: { |
121 | type: User, | |
122 | flatten: true, | |
579728c6 DM |
123 | }, |
124 | password: { | |
125 | schema: PBS_PASSWORD_SCHEMA, | |
126 | optional: true, | |
127 | }, | |
579728c6 DM |
128 | }, |
129 | }, | |
d4f020f4 | 130 | access: { |
74c08a57 | 131 | permission: &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false), |
d4f020f4 | 132 | }, |
579728c6 DM |
133 | )] |
134 | /// Create new user. | |
5aa10190 OB |
135 | pub fn create_user( |
136 | password: Option<String>, | |
b65dfff5 | 137 | config: User, |
5aa10190 OB |
138 | rpcenv: &mut dyn RpcEnvironment |
139 | ) -> Result<(), Error> { | |
579728c6 | 140 | |
ba3d7e19 | 141 | let _lock = pbs_config::user::lock_config()?; |
579728c6 | 142 | |
ba3d7e19 | 143 | let (mut section_config, _digest) = pbs_config::user::config()?; |
579728c6 | 144 | |
b65dfff5 DM |
145 | if section_config.sections.get(config.userid.as_str()).is_some() { |
146 | bail!("user '{}' already exists.", config.userid); | |
579728c6 DM |
147 | } |
148 | ||
b65dfff5 | 149 | section_config.set_data(config.userid.as_str(), "user", &config)?; |
579728c6 | 150 | |
b65dfff5 | 151 | let realm = config.userid.realm(); |
5aa10190 OB |
152 | |
153 | // Fails if realm does not exist! | |
154 | let authenticator = crate::auth::lookup_authenticator(realm)?; | |
155 | ||
ba3d7e19 | 156 | pbs_config::user::save_config(§ion_config)?; |
579728c6 | 157 | |
7d817b03 | 158 | if let Some(password) = password { |
5aa10190 OB |
159 | let user_info = CachedUserInfo::new()?; |
160 | let current_auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; | |
161 | if realm == "pam" && !user_info.is_superuser(¤t_auth_id) { | |
162 | bail!("only superuser can edit pam credentials!"); | |
163 | } | |
b65dfff5 | 164 | authenticator.store_password(config.userid.name(), &password)?; |
7d817b03 DM |
165 | } |
166 | ||
579728c6 DM |
167 | Ok(()) |
168 | } | |
169 | ||
170 | #[api( | |
171 | input: { | |
172 | properties: { | |
173 | userid: { | |
e7cb4dc5 | 174 | type: Userid, |
579728c6 | 175 | }, |
685e1334 | 176 | }, |
579728c6 | 177 | }, |
b65dfff5 | 178 | returns: { type: User }, |
d4f020f4 | 179 | access: { |
be3bd0f9 FG |
180 | permission: &Permission::Or(&[ |
181 | &Permission::Privilege(&["access", "users"], PRIV_SYS_AUDIT, false), | |
182 | &Permission::UserParam("userid"), | |
183 | ]), | |
d4f020f4 | 184 | }, |
579728c6 DM |
185 | )] |
186 | /// Read user configuration data. | |
b65dfff5 | 187 | pub fn read_user(userid: Userid, mut rpcenv: &mut dyn RpcEnvironment) -> Result<User, Error> { |
ba3d7e19 | 188 | let (config, digest) = pbs_config::user::config()?; |
e7cb4dc5 | 189 | let user = config.lookup("user", userid.as_str())?; |
522c0da0 DC |
190 | rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into(); |
191 | Ok(user) | |
579728c6 DM |
192 | } |
193 | ||
c0026563 DC |
194 | #[api()] |
195 | #[derive(Serialize, Deserialize)] | |
196 | #[serde(rename_all="kebab-case")] | |
197 | #[allow(non_camel_case_types)] | |
198 | pub enum DeletableProperty { | |
199 | /// Delete the comment property. | |
200 | comment, | |
201 | /// Delete the firstname property. | |
202 | firstname, | |
203 | /// Delete the lastname property. | |
204 | lastname, | |
205 | /// Delete the email property. | |
206 | email, | |
207 | } | |
208 | ||
579728c6 DM |
209 | #[api( |
210 | protected: true, | |
211 | input: { | |
212 | properties: { | |
213 | userid: { | |
e7cb4dc5 | 214 | type: Userid, |
579728c6 | 215 | }, |
b65dfff5 DM |
216 | update: { |
217 | type: UserUpdater, | |
218 | flatten: true, | |
579728c6 DM |
219 | }, |
220 | password: { | |
221 | schema: PBS_PASSWORD_SCHEMA, | |
222 | optional: true, | |
223 | }, | |
c0026563 DC |
224 | delete: { |
225 | description: "List of properties to delete.", | |
226 | type: Array, | |
227 | optional: true, | |
228 | items: { | |
229 | type: DeletableProperty, | |
230 | } | |
231 | }, | |
579728c6 DM |
232 | digest: { |
233 | optional: true, | |
234 | schema: PROXMOX_CONFIG_DIGEST_SCHEMA, | |
235 | }, | |
236 | }, | |
237 | }, | |
d4f020f4 | 238 | access: { |
be3bd0f9 FG |
239 | permission: &Permission::Or(&[ |
240 | &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false), | |
241 | &Permission::UserParam("userid"), | |
242 | ]), | |
d4f020f4 | 243 | }, |
579728c6 DM |
244 | )] |
245 | /// Update user configuration. | |
367c0ff7 | 246 | #[allow(clippy::too_many_arguments)] |
579728c6 | 247 | pub fn update_user( |
e7cb4dc5 | 248 | userid: Userid, |
b65dfff5 | 249 | update: UserUpdater, |
579728c6 | 250 | password: Option<String>, |
c0026563 | 251 | delete: Option<Vec<DeletableProperty>>, |
579728c6 | 252 | digest: Option<String>, |
5aa10190 | 253 | rpcenv: &mut dyn RpcEnvironment, |
579728c6 DM |
254 | ) -> Result<(), Error> { |
255 | ||
ba3d7e19 | 256 | let _lock = pbs_config::user::lock_config()?; |
579728c6 | 257 | |
ba3d7e19 | 258 | let (mut config, expected_digest) = pbs_config::user::config()?; |
579728c6 DM |
259 | |
260 | if let Some(ref digest) = digest { | |
261 | let digest = proxmox::tools::hex_to_digest(digest)?; | |
262 | crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?; | |
263 | } | |
264 | ||
b65dfff5 | 265 | let mut data: User = config.lookup("user", userid.as_str())?; |
579728c6 | 266 | |
c0026563 DC |
267 | if let Some(delete) = delete { |
268 | for delete_prop in delete { | |
269 | match delete_prop { | |
270 | DeletableProperty::comment => data.comment = None, | |
271 | DeletableProperty::firstname => data.firstname = None, | |
272 | DeletableProperty::lastname => data.lastname = None, | |
273 | DeletableProperty::email => data.email = None, | |
274 | } | |
275 | } | |
276 | } | |
277 | ||
b65dfff5 | 278 | if let Some(comment) = update.comment { |
579728c6 DM |
279 | let comment = comment.trim().to_string(); |
280 | if comment.is_empty() { | |
281 | data.comment = None; | |
282 | } else { | |
283 | data.comment = Some(comment); | |
284 | } | |
285 | } | |
286 | ||
b65dfff5 | 287 | if let Some(enable) = update.enable { |
579728c6 DM |
288 | data.enable = if enable { None } else { Some(false) }; |
289 | } | |
290 | ||
b65dfff5 | 291 | if let Some(expire) = update.expire { |
579728c6 DM |
292 | data.expire = if expire > 0 { Some(expire) } else { None }; |
293 | } | |
294 | ||
295 | if let Some(password) = password { | |
5aa10190 OB |
296 | let user_info = CachedUserInfo::new()?; |
297 | let current_auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; | |
298 | let self_service = current_auth_id.user() == &userid; | |
299 | let target_realm = userid.realm(); | |
300 | if !self_service && target_realm == "pam" && !user_info.is_superuser(¤t_auth_id) { | |
301 | bail!("only superuser can edit pam credentials!"); | |
302 | } | |
e7cb4dc5 WB |
303 | let authenticator = crate::auth::lookup_authenticator(userid.realm())?; |
304 | authenticator.store_password(userid.name(), &password)?; | |
579728c6 DM |
305 | } |
306 | ||
b65dfff5 | 307 | if let Some(firstname) = update.firstname { |
579728c6 DM |
308 | data.firstname = if firstname.is_empty() { None } else { Some(firstname) }; |
309 | } | |
310 | ||
b65dfff5 | 311 | if let Some(lastname) = update.lastname { |
579728c6 DM |
312 | data.lastname = if lastname.is_empty() { None } else { Some(lastname) }; |
313 | } | |
b65dfff5 | 314 | if let Some(email) = update.email { |
579728c6 DM |
315 | data.email = if email.is_empty() { None } else { Some(email) }; |
316 | } | |
317 | ||
e7cb4dc5 | 318 | config.set_data(userid.as_str(), "user", &data)?; |
579728c6 | 319 | |
ba3d7e19 | 320 | pbs_config::user::save_config(&config)?; |
579728c6 DM |
321 | |
322 | Ok(()) | |
323 | } | |
324 | ||
325 | #[api( | |
326 | protected: true, | |
327 | input: { | |
328 | properties: { | |
329 | userid: { | |
e7cb4dc5 | 330 | type: Userid, |
579728c6 DM |
331 | }, |
332 | digest: { | |
333 | optional: true, | |
334 | schema: PROXMOX_CONFIG_DIGEST_SCHEMA, | |
335 | }, | |
336 | }, | |
337 | }, | |
d4f020f4 | 338 | access: { |
be3bd0f9 FG |
339 | permission: &Permission::Or(&[ |
340 | &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false), | |
341 | &Permission::UserParam("userid"), | |
342 | ]), | |
d4f020f4 | 343 | }, |
579728c6 DM |
344 | )] |
345 | /// Remove a user from the configuration file. | |
e7cb4dc5 | 346 | pub fn delete_user(userid: Userid, digest: Option<String>) -> Result<(), Error> { |
579728c6 | 347 | |
ba3d7e19 | 348 | let _lock = pbs_config::user::lock_config()?; |
f22dfb5e | 349 | let _tfa_lock = crate::config::tfa::write_lock()?; |
b65dfff5 | 350 | |
ba3d7e19 | 351 | let (mut config, expected_digest) = pbs_config::user::config()?; |
579728c6 DM |
352 | |
353 | if let Some(ref digest) = digest { | |
354 | let digest = proxmox::tools::hex_to_digest(digest)?; | |
355 | crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?; | |
356 | } | |
357 | ||
e7cb4dc5 WB |
358 | match config.sections.get(userid.as_str()) { |
359 | Some(_) => { config.sections.remove(userid.as_str()); }, | |
579728c6 DM |
360 | None => bail!("user '{}' does not exist.", userid), |
361 | } | |
362 | ||
ba3d7e19 | 363 | pbs_config::user::save_config(&config)?; |
579728c6 | 364 | |
a4e871f5 DC |
365 | let authenticator = crate::auth::lookup_authenticator(userid.realm())?; |
366 | match authenticator.remove_password(userid.name()) { | |
367 | Ok(()) => {}, | |
368 | Err(err) => { | |
369 | eprintln!( | |
370 | "error removing password after deleting user {:?}: {}", | |
371 | userid, err | |
372 | ); | |
373 | } | |
374 | } | |
375 | ||
f22dfb5e WB |
376 | match crate::config::tfa::read().and_then(|mut cfg| { |
377 | let _: bool = cfg.remove_user(&userid); | |
378 | crate::config::tfa::write(&cfg) | |
379 | }) { | |
380 | Ok(()) => (), | |
381 | Err(err) => { | |
382 | eprintln!( | |
383 | "error updating TFA config after deleting user {:?}: {}", | |
384 | userid, err | |
385 | ); | |
386 | } | |
387 | } | |
388 | ||
579728c6 DM |
389 | Ok(()) |
390 | } | |
391 | ||
942078c4 FG |
392 | #[api( |
393 | input: { | |
394 | properties: { | |
395 | userid: { | |
396 | type: Userid, | |
397 | }, | |
f54634a8 | 398 | "token-name": { |
942078c4 FG |
399 | type: Tokenname, |
400 | }, | |
401 | }, | |
402 | }, | |
b65dfff5 | 403 | returns: { type: ApiToken }, |
942078c4 FG |
404 | access: { |
405 | permission: &Permission::Or(&[ | |
406 | &Permission::Privilege(&["access", "users"], PRIV_SYS_AUDIT, false), | |
407 | &Permission::UserParam("userid"), | |
408 | ]), | |
409 | }, | |
410 | )] | |
411 | /// Read user's API token metadata | |
412 | pub fn read_token( | |
413 | userid: Userid, | |
f54634a8 | 414 | token_name: Tokenname, |
942078c4 FG |
415 | _info: &ApiMethod, |
416 | mut rpcenv: &mut dyn RpcEnvironment, | |
b65dfff5 | 417 | ) -> Result<ApiToken, Error> { |
942078c4 | 418 | |
ba3d7e19 | 419 | let (config, digest) = pbs_config::user::config()?; |
942078c4 | 420 | |
f54634a8 | 421 | let tokenid = Authid::from((userid, Some(token_name))); |
942078c4 FG |
422 | |
423 | rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into(); | |
424 | config.lookup("token", &tokenid.to_string()) | |
425 | } | |
426 | ||
427 | #[api( | |
428 | protected: true, | |
429 | input: { | |
430 | properties: { | |
431 | userid: { | |
432 | type: Userid, | |
433 | }, | |
f54634a8 | 434 | "token-name": { |
942078c4 FG |
435 | type: Tokenname, |
436 | }, | |
437 | comment: { | |
438 | optional: true, | |
439 | schema: SINGLE_LINE_COMMENT_SCHEMA, | |
440 | }, | |
441 | enable: { | |
b65dfff5 | 442 | schema: ENABLE_USER_SCHEMA, |
942078c4 FG |
443 | optional: true, |
444 | }, | |
445 | expire: { | |
b65dfff5 | 446 | schema: EXPIRE_USER_SCHEMA, |
942078c4 FG |
447 | optional: true, |
448 | }, | |
449 | digest: { | |
450 | optional: true, | |
451 | schema: PROXMOX_CONFIG_DIGEST_SCHEMA, | |
452 | }, | |
453 | }, | |
454 | }, | |
455 | access: { | |
456 | permission: &Permission::Or(&[ | |
457 | &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false), | |
458 | &Permission::UserParam("userid"), | |
459 | ]), | |
460 | }, | |
461 | returns: { | |
462 | description: "API token identifier + generated secret.", | |
463 | properties: { | |
464 | value: { | |
465 | type: String, | |
466 | description: "The API token secret", | |
467 | }, | |
468 | tokenid: { | |
469 | type: String, | |
470 | description: "The API token identifier", | |
471 | }, | |
472 | }, | |
473 | }, | |
474 | )] | |
475 | /// Generate a new API token with given metadata | |
476 | pub fn generate_token( | |
477 | userid: Userid, | |
f54634a8 | 478 | token_name: Tokenname, |
942078c4 FG |
479 | comment: Option<String>, |
480 | enable: Option<bool>, | |
481 | expire: Option<i64>, | |
482 | digest: Option<String>, | |
483 | ) -> Result<Value, Error> { | |
484 | ||
ba3d7e19 | 485 | let _lock = pbs_config::user::lock_config()?; |
942078c4 | 486 | |
ba3d7e19 | 487 | let (mut config, expected_digest) = pbs_config::user::config()?; |
942078c4 FG |
488 | |
489 | if let Some(ref digest) = digest { | |
490 | let digest = proxmox::tools::hex_to_digest(digest)?; | |
491 | crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?; | |
492 | } | |
493 | ||
f54634a8 | 494 | let tokenid = Authid::from((userid.clone(), Some(token_name.clone()))); |
942078c4 FG |
495 | let tokenid_string = tokenid.to_string(); |
496 | ||
3984a5fd | 497 | if config.sections.get(&tokenid_string).is_some() { |
f54634a8 | 498 | bail!("token '{}' for user '{}' already exists.", token_name.as_str(), userid); |
942078c4 FG |
499 | } |
500 | ||
6ef1b649 | 501 | let secret = format!("{:x}", proxmox_uuid::Uuid::generate()); |
942078c4 FG |
502 | token_shadow::set_secret(&tokenid, &secret)?; |
503 | ||
b65dfff5 | 504 | let token = ApiToken { |
44288184 | 505 | tokenid, |
942078c4 FG |
506 | comment, |
507 | enable, | |
508 | expire, | |
509 | }; | |
510 | ||
511 | config.set_data(&tokenid_string, "token", &token)?; | |
512 | ||
ba3d7e19 | 513 | pbs_config::user::save_config(&config)?; |
942078c4 FG |
514 | |
515 | Ok(json!({ | |
516 | "tokenid": tokenid_string, | |
517 | "value": secret | |
518 | })) | |
519 | } | |
520 | ||
521 | #[api( | |
522 | protected: true, | |
523 | input: { | |
524 | properties: { | |
525 | userid: { | |
526 | type: Userid, | |
527 | }, | |
f54634a8 | 528 | "token-name": { |
942078c4 FG |
529 | type: Tokenname, |
530 | }, | |
531 | comment: { | |
532 | optional: true, | |
533 | schema: SINGLE_LINE_COMMENT_SCHEMA, | |
534 | }, | |
535 | enable: { | |
b65dfff5 | 536 | schema: ENABLE_USER_SCHEMA, |
942078c4 FG |
537 | optional: true, |
538 | }, | |
539 | expire: { | |
b65dfff5 | 540 | schema: EXPIRE_USER_SCHEMA, |
942078c4 FG |
541 | optional: true, |
542 | }, | |
543 | digest: { | |
544 | optional: true, | |
545 | schema: PROXMOX_CONFIG_DIGEST_SCHEMA, | |
546 | }, | |
547 | }, | |
548 | }, | |
549 | access: { | |
550 | permission: &Permission::Or(&[ | |
551 | &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false), | |
552 | &Permission::UserParam("userid"), | |
553 | ]), | |
554 | }, | |
555 | )] | |
556 | /// Update user's API token metadata | |
557 | pub fn update_token( | |
558 | userid: Userid, | |
f54634a8 | 559 | token_name: Tokenname, |
942078c4 FG |
560 | comment: Option<String>, |
561 | enable: Option<bool>, | |
562 | expire: Option<i64>, | |
563 | digest: Option<String>, | |
564 | ) -> Result<(), Error> { | |
565 | ||
ba3d7e19 | 566 | let _lock = pbs_config::user::lock_config()?; |
942078c4 | 567 | |
ba3d7e19 | 568 | let (mut config, expected_digest) = pbs_config::user::config()?; |
942078c4 FG |
569 | |
570 | if let Some(ref digest) = digest { | |
571 | let digest = proxmox::tools::hex_to_digest(digest)?; | |
572 | crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?; | |
573 | } | |
574 | ||
f54634a8 | 575 | let tokenid = Authid::from((userid, Some(token_name))); |
942078c4 FG |
576 | let tokenid_string = tokenid.to_string(); |
577 | ||
b65dfff5 | 578 | let mut data: ApiToken = config.lookup("token", &tokenid_string)?; |
942078c4 FG |
579 | |
580 | if let Some(comment) = comment { | |
581 | let comment = comment.trim().to_string(); | |
582 | if comment.is_empty() { | |
583 | data.comment = None; | |
584 | } else { | |
585 | data.comment = Some(comment); | |
586 | } | |
587 | } | |
588 | ||
589 | if let Some(enable) = enable { | |
590 | data.enable = if enable { None } else { Some(false) }; | |
591 | } | |
592 | ||
593 | if let Some(expire) = expire { | |
594 | data.expire = if expire > 0 { Some(expire) } else { None }; | |
595 | } | |
596 | ||
597 | config.set_data(&tokenid_string, "token", &data)?; | |
598 | ||
ba3d7e19 | 599 | pbs_config::user::save_config(&config)?; |
942078c4 FG |
600 | |
601 | Ok(()) | |
602 | } | |
603 | ||
604 | #[api( | |
605 | protected: true, | |
606 | input: { | |
607 | properties: { | |
608 | userid: { | |
609 | type: Userid, | |
610 | }, | |
f54634a8 | 611 | "token-name": { |
942078c4 FG |
612 | type: Tokenname, |
613 | }, | |
614 | digest: { | |
615 | optional: true, | |
616 | schema: PROXMOX_CONFIG_DIGEST_SCHEMA, | |
617 | }, | |
618 | }, | |
619 | }, | |
620 | access: { | |
621 | permission: &Permission::Or(&[ | |
622 | &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false), | |
623 | &Permission::UserParam("userid"), | |
624 | ]), | |
625 | }, | |
626 | )] | |
627 | /// Delete a user's API token | |
628 | pub fn delete_token( | |
629 | userid: Userid, | |
f54634a8 | 630 | token_name: Tokenname, |
942078c4 FG |
631 | digest: Option<String>, |
632 | ) -> Result<(), Error> { | |
633 | ||
ba3d7e19 | 634 | let _lock = pbs_config::user::lock_config()?; |
942078c4 | 635 | |
ba3d7e19 | 636 | let (mut config, expected_digest) = pbs_config::user::config()?; |
942078c4 FG |
637 | |
638 | if let Some(ref digest) = digest { | |
639 | let digest = proxmox::tools::hex_to_digest(digest)?; | |
640 | crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?; | |
641 | } | |
642 | ||
f54634a8 | 643 | let tokenid = Authid::from((userid.clone(), Some(token_name.clone()))); |
942078c4 FG |
644 | let tokenid_string = tokenid.to_string(); |
645 | ||
646 | match config.sections.get(&tokenid_string) { | |
647 | Some(_) => { config.sections.remove(&tokenid_string); }, | |
f54634a8 | 648 | None => bail!("token '{}' of user '{}' does not exist.", token_name.as_str(), userid), |
942078c4 FG |
649 | } |
650 | ||
651 | token_shadow::delete_secret(&tokenid)?; | |
652 | ||
ba3d7e19 | 653 | pbs_config::user::save_config(&config)?; |
942078c4 FG |
654 | |
655 | Ok(()) | |
656 | } | |
657 | ||
f54634a8 DC |
658 | #[api( |
659 | properties: { | |
660 | "token-name": { type: Tokenname }, | |
661 | token: { type: ApiToken }, | |
662 | } | |
663 | )] | |
664 | #[derive(Serialize, Deserialize)] | |
665 | #[serde(rename_all="kebab-case")] | |
666 | /// A Token Entry that contains the token-name | |
667 | pub struct TokenApiEntry { | |
668 | /// The Token name | |
669 | pub token_name: Tokenname, | |
670 | #[serde(flatten)] | |
671 | pub token: ApiToken, | |
672 | } | |
673 | ||
942078c4 FG |
674 | #[api( |
675 | input: { | |
676 | properties: { | |
677 | userid: { | |
678 | type: Userid, | |
679 | }, | |
680 | }, | |
681 | }, | |
682 | returns: { | |
683 | description: "List user's API tokens (with config digest).", | |
684 | type: Array, | |
f54634a8 | 685 | items: { type: TokenApiEntry }, |
942078c4 FG |
686 | }, |
687 | access: { | |
688 | permission: &Permission::Or(&[ | |
689 | &Permission::Privilege(&["access", "users"], PRIV_SYS_AUDIT, false), | |
690 | &Permission::UserParam("userid"), | |
691 | ]), | |
692 | }, | |
693 | )] | |
694 | /// List user's API tokens | |
695 | pub fn list_tokens( | |
696 | userid: Userid, | |
697 | _info: &ApiMethod, | |
698 | mut rpcenv: &mut dyn RpcEnvironment, | |
f54634a8 | 699 | ) -> Result<Vec<TokenApiEntry>, Error> { |
942078c4 | 700 | |
ba3d7e19 | 701 | let (config, digest) = pbs_config::user::config()?; |
942078c4 | 702 | |
b65dfff5 | 703 | let list:Vec<ApiToken> = config.convert_to_typed_array("token")?; |
942078c4 FG |
704 | |
705 | rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into(); | |
706 | ||
f54634a8 DC |
707 | let filter_by_owner = |token: ApiToken| { |
708 | if token.tokenid.is_token() && token.tokenid.user() == &userid { | |
709 | let token_name = token.tokenid.tokenname().unwrap().to_owned(); | |
710 | Some(TokenApiEntry { | |
711 | token_name, | |
712 | token, | |
713 | }) | |
942078c4 | 714 | } else { |
f54634a8 | 715 | None |
942078c4 FG |
716 | } |
717 | }; | |
718 | ||
f54634a8 DC |
719 | let res = list.into_iter().filter_map(filter_by_owner).collect(); |
720 | ||
721 | Ok(res) | |
942078c4 FG |
722 | } |
723 | ||
724 | const TOKEN_ITEM_ROUTER: Router = Router::new() | |
725 | .get(&API_METHOD_READ_TOKEN) | |
726 | .put(&API_METHOD_UPDATE_TOKEN) | |
727 | .post(&API_METHOD_GENERATE_TOKEN) | |
728 | .delete(&API_METHOD_DELETE_TOKEN); | |
729 | ||
730 | const TOKEN_ROUTER: Router = Router::new() | |
731 | .get(&API_METHOD_LIST_TOKENS) | |
f54634a8 | 732 | .match_all("token-name", &TOKEN_ITEM_ROUTER); |
942078c4 FG |
733 | |
734 | const USER_SUBDIRS: SubdirMap = &[ | |
735 | ("token", &TOKEN_ROUTER), | |
736 | ]; | |
737 | ||
738 | const USER_ROUTER: Router = Router::new() | |
579728c6 DM |
739 | .get(&API_METHOD_READ_USER) |
740 | .put(&API_METHOD_UPDATE_USER) | |
942078c4 FG |
741 | .delete(&API_METHOD_DELETE_USER) |
742 | .subdirs(USER_SUBDIRS); | |
579728c6 DM |
743 | |
744 | pub const ROUTER: Router = Router::new() | |
745 | .get(&API_METHOD_LIST_USERS) | |
746 | .post(&API_METHOD_CREATE_USER) | |
942078c4 | 747 | .match_all("userid", &USER_ROUTER); |