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