]> git.proxmox.com Git - proxmox-backup.git/blob - src/api2/access/user.rs
api: allow tokens to list users
[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(|user: user::User| UserWithTokens::new(user))
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(password: Option<String>, param: Value) -> Result<(), Error> {
222
223 let _lock = open_file_locked(user::USER_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
224
225 let user: user::User = serde_json::from_value(param)?;
226
227 let (mut config, _digest) = user::config()?;
228
229 if let Some(_) = config.sections.get(user.userid.as_str()) {
230 bail!("user '{}' already exists.", user.userid);
231 }
232
233 let authenticator = crate::auth::lookup_authenticator(&user.userid.realm())?;
234
235 config.set_data(user.userid.as_str(), "user", &user)?;
236
237 user::save_config(&config)?;
238
239 if let Some(password) = password {
240 authenticator.store_password(user.userid.name(), &password)?;
241 }
242
243 Ok(())
244 }
245
246 #[api(
247 input: {
248 properties: {
249 userid: {
250 type: Userid,
251 },
252 },
253 },
254 returns: { type: user::User },
255 access: {
256 permission: &Permission::Or(&[
257 &Permission::Privilege(&["access", "users"], PRIV_SYS_AUDIT, false),
258 &Permission::UserParam("userid"),
259 ]),
260 },
261 )]
262 /// Read user configuration data.
263 pub fn read_user(userid: Userid, mut rpcenv: &mut dyn RpcEnvironment) -> Result<user::User, Error> {
264 let (config, digest) = user::config()?;
265 let user = config.lookup("user", userid.as_str())?;
266 rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
267 Ok(user)
268 }
269
270 #[api()]
271 #[derive(Serialize, Deserialize)]
272 #[serde(rename_all="kebab-case")]
273 #[allow(non_camel_case_types)]
274 pub enum DeletableProperty {
275 /// Delete the comment property.
276 comment,
277 /// Delete the firstname property.
278 firstname,
279 /// Delete the lastname property.
280 lastname,
281 /// Delete the email property.
282 email,
283 }
284
285 #[api(
286 protected: true,
287 input: {
288 properties: {
289 userid: {
290 type: Userid,
291 },
292 comment: {
293 optional: true,
294 schema: SINGLE_LINE_COMMENT_SCHEMA,
295 },
296 password: {
297 schema: PBS_PASSWORD_SCHEMA,
298 optional: true,
299 },
300 enable: {
301 schema: user::ENABLE_USER_SCHEMA,
302 optional: true,
303 },
304 expire: {
305 schema: user::EXPIRE_USER_SCHEMA,
306 optional: true,
307 },
308 firstname: {
309 schema: user::FIRST_NAME_SCHEMA,
310 optional: true,
311 },
312 lastname: {
313 schema: user::LAST_NAME_SCHEMA,
314 optional: true,
315 },
316 email: {
317 schema: user::EMAIL_SCHEMA,
318 optional: true,
319 },
320 delete: {
321 description: "List of properties to delete.",
322 type: Array,
323 optional: true,
324 items: {
325 type: DeletableProperty,
326 }
327 },
328 digest: {
329 optional: true,
330 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
331 },
332 },
333 },
334 access: {
335 permission: &Permission::Or(&[
336 &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false),
337 &Permission::UserParam("userid"),
338 ]),
339 },
340 )]
341 /// Update user configuration.
342 pub fn update_user(
343 userid: Userid,
344 comment: Option<String>,
345 enable: Option<bool>,
346 expire: Option<i64>,
347 password: Option<String>,
348 firstname: Option<String>,
349 lastname: Option<String>,
350 email: Option<String>,
351 delete: Option<Vec<DeletableProperty>>,
352 digest: Option<String>,
353 ) -> Result<(), Error> {
354
355 let _lock = open_file_locked(user::USER_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
356
357 let (mut config, expected_digest) = user::config()?;
358
359 if let Some(ref digest) = digest {
360 let digest = proxmox::tools::hex_to_digest(digest)?;
361 crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
362 }
363
364 let mut data: user::User = config.lookup("user", userid.as_str())?;
365
366 if let Some(delete) = delete {
367 for delete_prop in delete {
368 match delete_prop {
369 DeletableProperty::comment => data.comment = None,
370 DeletableProperty::firstname => data.firstname = None,
371 DeletableProperty::lastname => data.lastname = None,
372 DeletableProperty::email => data.email = None,
373 }
374 }
375 }
376
377 if let Some(comment) = comment {
378 let comment = comment.trim().to_string();
379 if comment.is_empty() {
380 data.comment = None;
381 } else {
382 data.comment = Some(comment);
383 }
384 }
385
386 if let Some(enable) = enable {
387 data.enable = if enable { None } else { Some(false) };
388 }
389
390 if let Some(expire) = expire {
391 data.expire = if expire > 0 { Some(expire) } else { None };
392 }
393
394 if let Some(password) = password {
395 let authenticator = crate::auth::lookup_authenticator(userid.realm())?;
396 authenticator.store_password(userid.name(), &password)?;
397 }
398
399 if let Some(firstname) = firstname {
400 data.firstname = if firstname.is_empty() { None } else { Some(firstname) };
401 }
402
403 if let Some(lastname) = lastname {
404 data.lastname = if lastname.is_empty() { None } else { Some(lastname) };
405 }
406 if let Some(email) = email {
407 data.email = if email.is_empty() { None } else { Some(email) };
408 }
409
410 config.set_data(userid.as_str(), "user", &data)?;
411
412 user::save_config(&config)?;
413
414 Ok(())
415 }
416
417 #[api(
418 protected: true,
419 input: {
420 properties: {
421 userid: {
422 type: Userid,
423 },
424 digest: {
425 optional: true,
426 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
427 },
428 },
429 },
430 access: {
431 permission: &Permission::Or(&[
432 &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false),
433 &Permission::UserParam("userid"),
434 ]),
435 },
436 )]
437 /// Remove a user from the configuration file.
438 pub fn delete_user(userid: Userid, digest: Option<String>) -> Result<(), Error> {
439
440 let _lock = open_file_locked(user::USER_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
441
442 let (mut config, expected_digest) = user::config()?;
443
444 if let Some(ref digest) = digest {
445 let digest = proxmox::tools::hex_to_digest(digest)?;
446 crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
447 }
448
449 match config.sections.get(userid.as_str()) {
450 Some(_) => { config.sections.remove(userid.as_str()); },
451 None => bail!("user '{}' does not exist.", userid),
452 }
453
454 user::save_config(&config)?;
455
456 Ok(())
457 }
458
459 #[api(
460 input: {
461 properties: {
462 userid: {
463 type: Userid,
464 },
465 tokenname: {
466 type: Tokenname,
467 },
468 },
469 },
470 returns: { type: user::ApiToken },
471 access: {
472 permission: &Permission::Or(&[
473 &Permission::Privilege(&["access", "users"], PRIV_SYS_AUDIT, false),
474 &Permission::UserParam("userid"),
475 ]),
476 },
477 )]
478 /// Read user's API token metadata
479 pub fn read_token(
480 userid: Userid,
481 tokenname: Tokenname,
482 _info: &ApiMethod,
483 mut rpcenv: &mut dyn RpcEnvironment,
484 ) -> Result<user::ApiToken, Error> {
485
486 let (config, digest) = user::config()?;
487
488 let tokenid = Authid::from((userid, Some(tokenname)));
489
490 rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
491 config.lookup("token", &tokenid.to_string())
492 }
493
494 #[api(
495 protected: true,
496 input: {
497 properties: {
498 userid: {
499 type: Userid,
500 },
501 tokenname: {
502 type: Tokenname,
503 },
504 comment: {
505 optional: true,
506 schema: SINGLE_LINE_COMMENT_SCHEMA,
507 },
508 enable: {
509 schema: user::ENABLE_USER_SCHEMA,
510 optional: true,
511 },
512 expire: {
513 schema: user::EXPIRE_USER_SCHEMA,
514 optional: true,
515 },
516 digest: {
517 optional: true,
518 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
519 },
520 },
521 },
522 access: {
523 permission: &Permission::Or(&[
524 &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false),
525 &Permission::UserParam("userid"),
526 ]),
527 },
528 returns: {
529 description: "API token identifier + generated secret.",
530 properties: {
531 value: {
532 type: String,
533 description: "The API token secret",
534 },
535 tokenid: {
536 type: String,
537 description: "The API token identifier",
538 },
539 },
540 },
541 )]
542 /// Generate a new API token with given metadata
543 pub fn generate_token(
544 userid: Userid,
545 tokenname: Tokenname,
546 comment: Option<String>,
547 enable: Option<bool>,
548 expire: Option<i64>,
549 digest: Option<String>,
550 ) -> Result<Value, Error> {
551
552 let _lock = open_file_locked(user::USER_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
553
554 let (mut config, expected_digest) = user::config()?;
555
556 if let Some(ref digest) = digest {
557 let digest = proxmox::tools::hex_to_digest(digest)?;
558 crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
559 }
560
561 let tokenid = Authid::from((userid.clone(), Some(tokenname.clone())));
562 let tokenid_string = tokenid.to_string();
563
564 if let Some(_) = config.sections.get(&tokenid_string) {
565 bail!("token '{}' for user '{}' already exists.", tokenname.as_str(), userid);
566 }
567
568 let secret = format!("{:x}", proxmox::tools::uuid::Uuid::generate());
569 token_shadow::set_secret(&tokenid, &secret)?;
570
571 let token = user::ApiToken {
572 tokenid: tokenid.clone(),
573 comment,
574 enable,
575 expire,
576 };
577
578 config.set_data(&tokenid_string, "token", &token)?;
579
580 user::save_config(&config)?;
581
582 Ok(json!({
583 "tokenid": tokenid_string,
584 "value": secret
585 }))
586 }
587
588 #[api(
589 protected: true,
590 input: {
591 properties: {
592 userid: {
593 type: Userid,
594 },
595 tokenname: {
596 type: Tokenname,
597 },
598 comment: {
599 optional: true,
600 schema: SINGLE_LINE_COMMENT_SCHEMA,
601 },
602 enable: {
603 schema: user::ENABLE_USER_SCHEMA,
604 optional: true,
605 },
606 expire: {
607 schema: user::EXPIRE_USER_SCHEMA,
608 optional: true,
609 },
610 digest: {
611 optional: true,
612 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
613 },
614 },
615 },
616 access: {
617 permission: &Permission::Or(&[
618 &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false),
619 &Permission::UserParam("userid"),
620 ]),
621 },
622 )]
623 /// Update user's API token metadata
624 pub fn update_token(
625 userid: Userid,
626 tokenname: Tokenname,
627 comment: Option<String>,
628 enable: Option<bool>,
629 expire: Option<i64>,
630 digest: Option<String>,
631 ) -> Result<(), Error> {
632
633 let _lock = open_file_locked(user::USER_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
634
635 let (mut config, expected_digest) = user::config()?;
636
637 if let Some(ref digest) = digest {
638 let digest = proxmox::tools::hex_to_digest(digest)?;
639 crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
640 }
641
642 let tokenid = Authid::from((userid, Some(tokenname)));
643 let tokenid_string = tokenid.to_string();
644
645 let mut data: user::ApiToken = config.lookup("token", &tokenid_string)?;
646
647 if let Some(comment) = comment {
648 let comment = comment.trim().to_string();
649 if comment.is_empty() {
650 data.comment = None;
651 } else {
652 data.comment = Some(comment);
653 }
654 }
655
656 if let Some(enable) = enable {
657 data.enable = if enable { None } else { Some(false) };
658 }
659
660 if let Some(expire) = expire {
661 data.expire = if expire > 0 { Some(expire) } else { None };
662 }
663
664 config.set_data(&tokenid_string, "token", &data)?;
665
666 user::save_config(&config)?;
667
668 Ok(())
669 }
670
671 #[api(
672 protected: true,
673 input: {
674 properties: {
675 userid: {
676 type: Userid,
677 },
678 tokenname: {
679 type: Tokenname,
680 },
681 digest: {
682 optional: true,
683 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
684 },
685 },
686 },
687 access: {
688 permission: &Permission::Or(&[
689 &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false),
690 &Permission::UserParam("userid"),
691 ]),
692 },
693 )]
694 /// Delete a user's API token
695 pub fn delete_token(
696 userid: Userid,
697 tokenname: Tokenname,
698 digest: Option<String>,
699 ) -> Result<(), Error> {
700
701 let _lock = open_file_locked(user::USER_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
702
703 let (mut config, expected_digest) = user::config()?;
704
705 if let Some(ref digest) = digest {
706 let digest = proxmox::tools::hex_to_digest(digest)?;
707 crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
708 }
709
710 let tokenid = Authid::from((userid.clone(), Some(tokenname.clone())));
711 let tokenid_string = tokenid.to_string();
712
713 match config.sections.get(&tokenid_string) {
714 Some(_) => { config.sections.remove(&tokenid_string); },
715 None => bail!("token '{}' of user '{}' does not exist.", tokenname.as_str(), userid),
716 }
717
718 token_shadow::delete_secret(&tokenid)?;
719
720 user::save_config(&config)?;
721
722 Ok(())
723 }
724
725 #[api(
726 input: {
727 properties: {
728 userid: {
729 type: Userid,
730 },
731 },
732 },
733 returns: {
734 description: "List user's API tokens (with config digest).",
735 type: Array,
736 items: { type: user::ApiToken },
737 },
738 access: {
739 permission: &Permission::Or(&[
740 &Permission::Privilege(&["access", "users"], PRIV_SYS_AUDIT, false),
741 &Permission::UserParam("userid"),
742 ]),
743 },
744 )]
745 /// List user's API tokens
746 pub fn list_tokens(
747 userid: Userid,
748 _info: &ApiMethod,
749 mut rpcenv: &mut dyn RpcEnvironment,
750 ) -> Result<Vec<user::ApiToken>, Error> {
751
752 let (config, digest) = user::config()?;
753
754 let list:Vec<user::ApiToken> = config.convert_to_typed_array("token")?;
755
756 rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
757
758 let filter_by_owner = |token: &user::ApiToken| {
759 if token.tokenid.is_token() {
760 token.tokenid.user() == &userid
761 } else {
762 false
763 }
764 };
765
766 Ok(list.into_iter().filter(filter_by_owner).collect())
767 }
768
769 const TOKEN_ITEM_ROUTER: Router = Router::new()
770 .get(&API_METHOD_READ_TOKEN)
771 .put(&API_METHOD_UPDATE_TOKEN)
772 .post(&API_METHOD_GENERATE_TOKEN)
773 .delete(&API_METHOD_DELETE_TOKEN);
774
775 const TOKEN_ROUTER: Router = Router::new()
776 .get(&API_METHOD_LIST_TOKENS)
777 .match_all("tokenname", &TOKEN_ITEM_ROUTER);
778
779 const USER_SUBDIRS: SubdirMap = &[
780 ("token", &TOKEN_ROUTER),
781 ];
782
783 const USER_ROUTER: Router = Router::new()
784 .get(&API_METHOD_READ_USER)
785 .put(&API_METHOD_UPDATE_USER)
786 .delete(&API_METHOD_DELETE_USER)
787 .subdirs(USER_SUBDIRS);
788
789 pub const ROUTER: Router = Router::new()
790 .get(&API_METHOD_LIST_USERS)
791 .post(&API_METHOD_CREATE_USER)
792 .match_all("userid", &USER_ROUTER);