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