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