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