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