3 use anyhow
::{bail, format_err, Error}
;
4 use serde
::{Serialize, Deserialize}
;
5 use serde_json
::{json, Value}
;
6 use std
::collections
::HashMap
;
8 use proxmox
::api
::{api, ApiMethod, Router, RpcEnvironment, Permission}
;
9 use proxmox
::api
::router
::SubdirMap
;
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
,
17 use pbs_config
::token_shadow
;
19 use pbs_config
::CachedUserInfo
;
21 fn new_user_with_tokens(user
: User
) -> UserWithTokens
{
24 comment
: user
.comment
,
27 firstname
: user
.firstname
,
28 lastname
: user
.lastname
,
39 description
: "Include user's API tokens in returned list.",
46 description
: "List users (with config digest).",
48 items
: { type: UserWithTokens }
,
51 permission
: &Permission
::Anybody
,
52 description
: "Returns all or just the logged-in user (/API token owner), depending on privileges.",
59 mut rpcenv
: &mut dyn RpcEnvironment
,
60 ) -> Result
<Vec
<UserWithTokens
>, Error
> {
62 let (config
, digest
) = pbs_config
::user
::config()?
;
64 let auth_id
: Authid
= rpcenv
66 .ok_or_else(|| format_err
!("no authid available"))?
69 let userid
= auth_id
.user();
71 let user_info
= CachedUserInfo
::new()?
;
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;
76 let filter_by_privs
= |user
: &User
| {
77 top_level_allowed
|| user
.userid
== *userid
81 let list
:Vec
<User
> = config
.convert_to_typed_array("user")?
;
83 rpcenv
["digest"] = proxmox
::tools
::digest_to_hex(&digest
).into();
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
92 |mut map
: HashMap
<Userid
, Vec
<ApiToken
>>, token
: ApiToken
| {
93 if token
.tokenid
.is_token() {
95 .entry(token
.tokenid
.user().clone())
103 let mut user
= new_user_with_tokens(user
);
104 user
.tokens
= user_to_tokens
.remove(&user
.userid
).unwrap_or_default();
109 iter
.map(new_user_with_tokens
)
125 schema
: PBS_PASSWORD_SCHEMA
,
131 permission
: &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
136 password
: Option
<String
>,
138 rpcenv
: &mut dyn RpcEnvironment
139 ) -> Result
<(), Error
> {
141 let _lock
= pbs_config
::user
::lock_config()?
;
143 let (mut section_config
, _digest
) = pbs_config
::user
::config()?
;
145 if section_config
.sections
.get(config
.userid
.as_str()).is_some() {
146 bail
!("user '{}' already exists.", config
.userid
);
149 section_config
.set_data(config
.userid
.as_str(), "user", &config
)?
;
151 let realm
= config
.userid
.realm();
153 // Fails if realm does not exist!
154 let authenticator
= crate::auth
::lookup_authenticator(realm
)?
;
156 pbs_config
::user
::save_config(§ion_config
)?
;
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(¤t_auth_id
) {
162 bail
!("only superuser can edit pam credentials!");
164 authenticator
.store_password(config
.userid
.name(), &password
)?
;
178 returns
: { type: User }
,
180 permission
: &Permission
::Or(&[
181 &Permission
::Privilege(&["access", "users"], PRIV_SYS_AUDIT
, false),
182 &Permission
::UserParam("userid"),
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();
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.
201 /// Delete the firstname property.
203 /// Delete the lastname property.
205 /// Delete the email property.
221 schema
: PBS_PASSWORD_SCHEMA
,
225 description
: "List of properties to delete.",
229 type: DeletableProperty
,
234 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
239 permission
: &Permission
::Or(&[
240 &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
241 &Permission
::UserParam("userid"),
245 /// Update user configuration.
246 #[allow(clippy::too_many_arguments)]
250 password
: Option
<String
>,
251 delete
: Option
<Vec
<DeletableProperty
>>,
252 digest
: Option
<String
>,
253 rpcenv
: &mut dyn RpcEnvironment
,
254 ) -> Result
<(), Error
> {
256 let _lock
= pbs_config
::user
::lock_config()?
;
258 let (mut config
, expected_digest
) = pbs_config
::user
::config()?
;
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
)?
;
265 let mut data
: User
= config
.lookup("user", userid
.as_str())?
;
267 if let Some(delete
) = delete
{
268 for delete_prop
in delete
{
270 DeletableProperty
::comment
=> data
.comment
= None
,
271 DeletableProperty
::firstname
=> data
.firstname
= None
,
272 DeletableProperty
::lastname
=> data
.lastname
= None
,
273 DeletableProperty
::email
=> data
.email
= None
,
278 if let Some(comment
) = update
.comment
{
279 let comment
= comment
.trim().to_string();
280 if comment
.is_empty() {
283 data
.comment
= Some(comment
);
287 if let Some(enable
) = update
.enable
{
288 data
.enable
= if enable { None }
else { Some(false) }
;
291 if let Some(expire
) = update
.expire
{
292 data
.expire
= if expire
> 0 { Some(expire) }
else { None }
;
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(¤t_auth_id
) {
301 bail
!("only superuser can edit pam credentials!");
303 let authenticator
= crate::auth
::lookup_authenticator(userid
.realm())?
;
304 authenticator
.store_password(userid
.name(), &password
)?
;
307 if let Some(firstname
) = update
.firstname
{
308 data
.firstname
= if firstname
.is_empty() { None }
else { Some(firstname) }
;
311 if let Some(lastname
) = update
.lastname
{
312 data
.lastname
= if lastname
.is_empty() { None }
else { Some(lastname) }
;
314 if let Some(email
) = update
.email
{
315 data
.email
= if email
.is_empty() { None }
else { Some(email) }
;
318 config
.set_data(userid
.as_str(), "user", &data
)?
;
320 pbs_config
::user
::save_config(&config
)?
;
334 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
339 permission
: &Permission
::Or(&[
340 &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
341 &Permission
::UserParam("userid"),
345 /// Remove a user from the configuration file.
346 pub fn delete_user(userid
: Userid
, digest
: Option
<String
>) -> Result
<(), Error
> {
348 let _lock
= pbs_config
::user
::lock_config()?
;
349 let _tfa_lock
= crate::config
::tfa
::write_lock()?
;
351 let (mut config
, expected_digest
) = pbs_config
::user
::config()?
;
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
)?
;
358 match config
.sections
.get(userid
.as_str()) {
359 Some(_
) => { config.sections.remove(userid.as_str()); }
,
360 None
=> bail
!("user '{}' does not exist.", userid
),
363 pbs_config
::user
::save_config(&config
)?
;
365 let authenticator
= crate::auth
::lookup_authenticator(userid
.realm())?
;
366 match authenticator
.remove_password(userid
.name()) {
370 "error removing password after deleting user {:?}: {}",
376 match crate::config
::tfa
::read().and_then(|mut cfg
| {
377 let _
: bool
= cfg
.remove_user(&userid
);
378 crate::config
::tfa
::write(&cfg
)
383 "error updating TFA config after deleting user {:?}: {}",
403 returns
: { type: ApiToken }
,
405 permission
: &Permission
::Or(&[
406 &Permission
::Privilege(&["access", "users"], PRIV_SYS_AUDIT
, false),
407 &Permission
::UserParam("userid"),
411 /// Read user's API token metadata
414 token_name
: Tokenname
,
416 mut rpcenv
: &mut dyn RpcEnvironment
,
417 ) -> Result
<ApiToken
, Error
> {
419 let (config
, digest
) = pbs_config
::user
::config()?
;
421 let tokenid
= Authid
::from((userid
, Some(token_name
)));
423 rpcenv
["digest"] = proxmox
::tools
::digest_to_hex(&digest
).into();
424 config
.lookup("token", &tokenid
.to_string())
439 schema
: SINGLE_LINE_COMMENT_SCHEMA
,
442 schema
: ENABLE_USER_SCHEMA
,
446 schema
: EXPIRE_USER_SCHEMA
,
451 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
456 permission
: &Permission
::Or(&[
457 &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
458 &Permission
::UserParam("userid"),
462 description
: "API token identifier + generated secret.",
466 description
: "The API token secret",
470 description
: "The API token identifier",
475 /// Generate a new API token with given metadata
476 pub fn generate_token(
478 token_name
: Tokenname
,
479 comment
: Option
<String
>,
480 enable
: Option
<bool
>,
482 digest
: Option
<String
>,
483 ) -> Result
<Value
, Error
> {
485 let _lock
= pbs_config
::user
::lock_config()?
;
487 let (mut config
, expected_digest
) = pbs_config
::user
::config()?
;
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
)?
;
494 let tokenid
= Authid
::from((userid
.clone(), Some(token_name
.clone())));
495 let tokenid_string
= tokenid
.to_string();
497 if config
.sections
.get(&tokenid_string
).is_some() {
498 bail
!("token '{}' for user '{}' already exists.", token_name
.as_str(), userid
);
501 let secret
= format
!("{:x}", proxmox
::tools
::uuid
::Uuid
::generate());
502 token_shadow
::set_secret(&tokenid
, &secret
)?
;
504 let token
= ApiToken
{
511 config
.set_data(&tokenid_string
, "token", &token
)?
;
513 pbs_config
::user
::save_config(&config
)?
;
516 "tokenid": tokenid_string
,
533 schema
: SINGLE_LINE_COMMENT_SCHEMA
,
536 schema
: ENABLE_USER_SCHEMA
,
540 schema
: EXPIRE_USER_SCHEMA
,
545 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
550 permission
: &Permission
::Or(&[
551 &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
552 &Permission
::UserParam("userid"),
556 /// Update user's API token metadata
559 token_name
: Tokenname
,
560 comment
: Option
<String
>,
561 enable
: Option
<bool
>,
563 digest
: Option
<String
>,
564 ) -> Result
<(), Error
> {
566 let _lock
= pbs_config
::user
::lock_config()?
;
568 let (mut config
, expected_digest
) = pbs_config
::user
::config()?
;
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
)?
;
575 let tokenid
= Authid
::from((userid
, Some(token_name
)));
576 let tokenid_string
= tokenid
.to_string();
578 let mut data
: ApiToken
= config
.lookup("token", &tokenid_string
)?
;
580 if let Some(comment
) = comment
{
581 let comment
= comment
.trim().to_string();
582 if comment
.is_empty() {
585 data
.comment
= Some(comment
);
589 if let Some(enable
) = enable
{
590 data
.enable
= if enable { None }
else { Some(false) }
;
593 if let Some(expire
) = expire
{
594 data
.expire
= if expire
> 0 { Some(expire) }
else { None }
;
597 config
.set_data(&tokenid_string
, "token", &data
)?
;
599 pbs_config
::user
::save_config(&config
)?
;
616 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
621 permission
: &Permission
::Or(&[
622 &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
623 &Permission
::UserParam("userid"),
627 /// Delete a user's API token
630 token_name
: Tokenname
,
631 digest
: Option
<String
>,
632 ) -> Result
<(), Error
> {
634 let _lock
= pbs_config
::user
::lock_config()?
;
636 let (mut config
, expected_digest
) = pbs_config
::user
::config()?
;
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
)?
;
643 let tokenid
= Authid
::from((userid
.clone(), Some(token_name
.clone())));
644 let tokenid_string
= tokenid
.to_string();
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
),
651 token_shadow
::delete_secret(&tokenid
)?
;
653 pbs_config
::user
::save_config(&config
)?
;
660 "token-name": { type: Tokenname }
,
661 token
: { type: ApiToken }
,
664 #[derive(Serialize, Deserialize)]
665 #[serde(rename_all="kebab-case")]
666 /// A Token Entry that contains the token-name
667 pub struct TokenApiEntry
{
669 pub token_name
: Tokenname
,
683 description
: "List user's API tokens (with config digest).",
685 items
: { type: TokenApiEntry }
,
688 permission
: &Permission
::Or(&[
689 &Permission
::Privilege(&["access", "users"], PRIV_SYS_AUDIT
, false),
690 &Permission
::UserParam("userid"),
694 /// List user's API tokens
698 mut rpcenv
: &mut dyn RpcEnvironment
,
699 ) -> Result
<Vec
<TokenApiEntry
>, Error
> {
701 let (config
, digest
) = pbs_config
::user
::config()?
;
703 let list
:Vec
<ApiToken
> = config
.convert_to_typed_array("token")?
;
705 rpcenv
["digest"] = proxmox
::tools
::digest_to_hex(&digest
).into();
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();
719 let res
= list
.into_iter().filter_map(filter_by_owner
).collect();
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
);
730 const TOKEN_ROUTER
: Router
= Router
::new()
731 .get(&API_METHOD_LIST_TOKENS
)
732 .match_all("token-name", &TOKEN_ITEM_ROUTER
);
734 const USER_SUBDIRS
: SubdirMap
= &[
735 ("token", &TOKEN_ROUTER
),
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
);
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
);