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
;
10 use proxmox
::api
::schema
::{Schema, StringSchema}
;
11 use proxmox
::tools
::fs
::open_file_locked
;
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
;
19 pub const PBS_PASSWORD_SCHEMA
: Schema
= StringSchema
::new("User Password.")
20 .format(&PASSWORD_FORMAT
)
32 schema
: SINGLE_LINE_COMMENT_SCHEMA
,
36 schema
: user
::ENABLE_USER_SCHEMA
,
40 schema
: user
::EXPIRE_USER_SCHEMA
,
44 schema
: user
::FIRST_NAME_SCHEMA
,
47 schema
: user
::LAST_NAME_SCHEMA
,
51 schema
: user
::EMAIL_SCHEMA
,
57 description
: "List of user's API tokens.",
64 #[derive(Serialize,Deserialize)]
65 /// User properties with added list of ApiTokens
66 pub struct UserWithTokens
{
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
>,
85 fn new(user
: user
::User
) -> Self {
88 comment
: user
.comment
,
91 firstname
: user
.firstname
,
92 lastname
: user
.lastname
,
104 description
: "Include user's API tokens in returned list.",
111 description
: "List users (with config digest).",
113 items
: { type: UserWithTokens }
,
116 permission
: &Permission
::Anybody
,
117 description
: "Returns all or just the logged-in user (/API token owner), depending on privileges.",
122 include_tokens
: bool
,
124 mut rpcenv
: &mut dyn RpcEnvironment
,
125 ) -> Result
<Vec
<UserWithTokens
>, Error
> {
127 let (config
, digest
) = user
::config()?
;
129 let auth_id
: Authid
= rpcenv
131 .ok_or_else(|| format_err
!("no authid available"))?
134 let userid
= auth_id
.user();
136 let user_info
= CachedUserInfo
::new()?
;
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;
141 let filter_by_privs
= |user
: &user
::User
| {
142 top_level_allowed
|| user
.userid
== *userid
146 let list
:Vec
<user
::User
> = config
.convert_to_typed_array("user")?
;
148 rpcenv
["digest"] = proxmox
::tools
::digest_to_hex(&digest
).into();
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
157 |mut map
: HashMap
<Userid
, Vec
<user
::ApiToken
>>, token
: user
::ApiToken
| {
158 if token
.tokenid
.is_token() {
160 .entry(token
.tokenid
.user().clone())
167 .map(|user
: user
::User
| {
168 let mut user
= UserWithTokens
::new(user
);
169 user
.tokens
= user_to_tokens
.remove(&user
.userid
).unwrap_or_default();
174 iter
.map(UserWithTokens
::new
)
189 schema
: SINGLE_LINE_COMMENT_SCHEMA
,
193 schema
: PBS_PASSWORD_SCHEMA
,
197 schema
: user
::ENABLE_USER_SCHEMA
,
201 schema
: user
::EXPIRE_USER_SCHEMA
,
205 schema
: user
::FIRST_NAME_SCHEMA
,
209 schema
: user
::LAST_NAME_SCHEMA
,
213 schema
: user
::EMAIL_SCHEMA
,
219 permission
: &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
224 password
: Option
<String
>,
226 rpcenv
: &mut dyn RpcEnvironment
227 ) -> Result
<(), Error
> {
229 let _lock
= open_file_locked(user
::USER_CFG_LOCKFILE
, std
::time
::Duration
::new(10, 0), true)?
;
231 let user
: user
::User
= serde_json
::from_value(param
)?
;
233 let (mut config
, _digest
) = user
::config()?
;
235 if config
.sections
.get(user
.userid
.as_str()).is_some() {
236 bail
!("user '{}' already exists.", user
.userid
);
239 config
.set_data(user
.userid
.as_str(), "user", &user
)?
;
241 let realm
= user
.userid
.realm();
243 // Fails if realm does not exist!
244 let authenticator
= crate::auth
::lookup_authenticator(realm
)?
;
246 user
::save_config(&config
)?
;
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(¤t_auth_id
) {
252 bail
!("only superuser can edit pam credentials!");
254 authenticator
.store_password(user
.userid
.name(), &password
)?
;
268 returns
: { type: user::User }
,
270 permission
: &Permission
::Or(&[
271 &Permission
::Privilege(&["access", "users"], PRIV_SYS_AUDIT
, false),
272 &Permission
::UserParam("userid"),
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();
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.
291 /// Delete the firstname property.
293 /// Delete the lastname property.
295 /// Delete the email property.
308 schema
: SINGLE_LINE_COMMENT_SCHEMA
,
311 schema
: PBS_PASSWORD_SCHEMA
,
315 schema
: user
::ENABLE_USER_SCHEMA
,
319 schema
: user
::EXPIRE_USER_SCHEMA
,
323 schema
: user
::FIRST_NAME_SCHEMA
,
327 schema
: user
::LAST_NAME_SCHEMA
,
331 schema
: user
::EMAIL_SCHEMA
,
335 description
: "List of properties to delete.",
339 type: DeletableProperty
,
344 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
349 permission
: &Permission
::Or(&[
350 &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
351 &Permission
::UserParam("userid"),
355 /// Update user configuration.
358 comment
: Option
<String
>,
359 enable
: Option
<bool
>,
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
> {
370 let _lock
= open_file_locked(user
::USER_CFG_LOCKFILE
, std
::time
::Duration
::new(10, 0), true)?
;
372 let (mut config
, expected_digest
) = user
::config()?
;
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
)?
;
379 let mut data
: user
::User
= config
.lookup("user", userid
.as_str())?
;
381 if let Some(delete
) = delete
{
382 for delete_prop
in delete
{
384 DeletableProperty
::comment
=> data
.comment
= None
,
385 DeletableProperty
::firstname
=> data
.firstname
= None
,
386 DeletableProperty
::lastname
=> data
.lastname
= None
,
387 DeletableProperty
::email
=> data
.email
= None
,
392 if let Some(comment
) = comment
{
393 let comment
= comment
.trim().to_string();
394 if comment
.is_empty() {
397 data
.comment
= Some(comment
);
401 if let Some(enable
) = enable
{
402 data
.enable
= if enable { None }
else { Some(false) }
;
405 if let Some(expire
) = expire
{
406 data
.expire
= if expire
> 0 { Some(expire) }
else { None }
;
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(¤t_auth_id
) {
415 bail
!("only superuser can edit pam credentials!");
417 let authenticator
= crate::auth
::lookup_authenticator(userid
.realm())?
;
418 authenticator
.store_password(userid
.name(), &password
)?
;
421 if let Some(firstname
) = firstname
{
422 data
.firstname
= if firstname
.is_empty() { None }
else { Some(firstname) }
;
425 if let Some(lastname
) = lastname
{
426 data
.lastname
= if lastname
.is_empty() { None }
else { Some(lastname) }
;
428 if let Some(email
) = email
{
429 data
.email
= if email
.is_empty() { None }
else { Some(email) }
;
432 config
.set_data(userid
.as_str(), "user", &data
)?
;
434 user
::save_config(&config
)?
;
448 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
453 permission
: &Permission
::Or(&[
454 &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
455 &Permission
::UserParam("userid"),
459 /// Remove a user from the configuration file.
460 pub fn delete_user(userid
: Userid
, digest
: Option
<String
>) -> Result
<(), Error
> {
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)?
;
465 let (mut config
, expected_digest
) = user
::config()?
;
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
)?
;
472 match config
.sections
.get(userid
.as_str()) {
473 Some(_
) => { config.sections.remove(userid.as_str()); }
,
474 None
=> bail
!("user '{}' does not exist.", userid
),
477 user
::save_config(&config
)?
;
479 match crate::config
::tfa
::read().and_then(|mut cfg
| {
480 let _
: bool
= cfg
.remove_user(&userid
);
481 crate::config
::tfa
::write(&cfg
)
486 "error updating TFA config after deleting user {:?}: {}",
506 returns
: { type: user::ApiToken }
,
508 permission
: &Permission
::Or(&[
509 &Permission
::Privilege(&["access", "users"], PRIV_SYS_AUDIT
, false),
510 &Permission
::UserParam("userid"),
514 /// Read user's API token metadata
517 tokenname
: Tokenname
,
519 mut rpcenv
: &mut dyn RpcEnvironment
,
520 ) -> Result
<user
::ApiToken
, Error
> {
522 let (config
, digest
) = user
::config()?
;
524 let tokenid
= Authid
::from((userid
, Some(tokenname
)));
526 rpcenv
["digest"] = proxmox
::tools
::digest_to_hex(&digest
).into();
527 config
.lookup("token", &tokenid
.to_string())
542 schema
: SINGLE_LINE_COMMENT_SCHEMA
,
545 schema
: user
::ENABLE_USER_SCHEMA
,
549 schema
: user
::EXPIRE_USER_SCHEMA
,
554 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
559 permission
: &Permission
::Or(&[
560 &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
561 &Permission
::UserParam("userid"),
565 description
: "API token identifier + generated secret.",
569 description
: "The API token secret",
573 description
: "The API token identifier",
578 /// Generate a new API token with given metadata
579 pub fn generate_token(
581 tokenname
: Tokenname
,
582 comment
: Option
<String
>,
583 enable
: Option
<bool
>,
585 digest
: Option
<String
>,
586 ) -> Result
<Value
, Error
> {
588 let _lock
= open_file_locked(user
::USER_CFG_LOCKFILE
, std
::time
::Duration
::new(10, 0), true)?
;
590 let (mut config
, expected_digest
) = user
::config()?
;
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
)?
;
597 let tokenid
= Authid
::from((userid
.clone(), Some(tokenname
.clone())));
598 let tokenid_string
= tokenid
.to_string();
600 if config
.sections
.get(&tokenid_string
).is_some() {
601 bail
!("token '{}' for user '{}' already exists.", tokenname
.as_str(), userid
);
604 let secret
= format
!("{:x}", proxmox
::tools
::uuid
::Uuid
::generate());
605 token_shadow
::set_secret(&tokenid
, &secret
)?
;
607 let token
= user
::ApiToken
{
614 config
.set_data(&tokenid_string
, "token", &token
)?
;
616 user
::save_config(&config
)?
;
619 "tokenid": tokenid_string
,
636 schema
: SINGLE_LINE_COMMENT_SCHEMA
,
639 schema
: user
::ENABLE_USER_SCHEMA
,
643 schema
: user
::EXPIRE_USER_SCHEMA
,
648 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
653 permission
: &Permission
::Or(&[
654 &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
655 &Permission
::UserParam("userid"),
659 /// Update user's API token metadata
662 tokenname
: Tokenname
,
663 comment
: Option
<String
>,
664 enable
: Option
<bool
>,
666 digest
: Option
<String
>,
667 ) -> Result
<(), Error
> {
669 let _lock
= open_file_locked(user
::USER_CFG_LOCKFILE
, std
::time
::Duration
::new(10, 0), true)?
;
671 let (mut config
, expected_digest
) = user
::config()?
;
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
)?
;
678 let tokenid
= Authid
::from((userid
, Some(tokenname
)));
679 let tokenid_string
= tokenid
.to_string();
681 let mut data
: user
::ApiToken
= config
.lookup("token", &tokenid_string
)?
;
683 if let Some(comment
) = comment
{
684 let comment
= comment
.trim().to_string();
685 if comment
.is_empty() {
688 data
.comment
= Some(comment
);
692 if let Some(enable
) = enable
{
693 data
.enable
= if enable { None }
else { Some(false) }
;
696 if let Some(expire
) = expire
{
697 data
.expire
= if expire
> 0 { Some(expire) }
else { None }
;
700 config
.set_data(&tokenid_string
, "token", &data
)?
;
702 user
::save_config(&config
)?
;
719 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
724 permission
: &Permission
::Or(&[
725 &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
726 &Permission
::UserParam("userid"),
730 /// Delete a user's API token
733 tokenname
: Tokenname
,
734 digest
: Option
<String
>,
735 ) -> Result
<(), Error
> {
737 let _lock
= open_file_locked(user
::USER_CFG_LOCKFILE
, std
::time
::Duration
::new(10, 0), true)?
;
739 let (mut config
, expected_digest
) = user
::config()?
;
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
)?
;
746 let tokenid
= Authid
::from((userid
.clone(), Some(tokenname
.clone())));
747 let tokenid_string
= tokenid
.to_string();
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
),
754 token_shadow
::delete_secret(&tokenid
)?
;
756 user
::save_config(&config
)?
;
770 description
: "List user's API tokens (with config digest).",
772 items
: { type: user::ApiToken }
,
775 permission
: &Permission
::Or(&[
776 &Permission
::Privilege(&["access", "users"], PRIV_SYS_AUDIT
, false),
777 &Permission
::UserParam("userid"),
781 /// List user's API tokens
785 mut rpcenv
: &mut dyn RpcEnvironment
,
786 ) -> Result
<Vec
<user
::ApiToken
>, Error
> {
788 let (config
, digest
) = user
::config()?
;
790 let list
:Vec
<user
::ApiToken
> = config
.convert_to_typed_array("token")?
;
792 rpcenv
["digest"] = proxmox
::tools
::digest_to_hex(&digest
).into();
794 let filter_by_owner
= |token
: &user
::ApiToken
| {
795 if token
.tokenid
.is_token() {
796 token
.tokenid
.user() == &userid
802 Ok(list
.into_iter().filter(filter_by_owner
).collect())
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
);
811 const TOKEN_ROUTER
: Router
= Router
::new()
812 .get(&API_METHOD_LIST_TOKENS
)
813 .match_all("tokenname", &TOKEN_ITEM_ROUTER
);
815 const USER_SUBDIRS
: SubdirMap
= &[
816 ("token", &TOKEN_ROUTER
),
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
);
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
);