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