1 use anyhow
::{bail, format_err, Error}
;
2 use serde
::{Serialize, Deserialize}
;
3 use serde_json
::{json, Value}
;
4 use std
::collections
::HashMap
;
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
;
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
;
17 pub const PBS_PASSWORD_SCHEMA
: Schema
= StringSchema
::new("User Password.")
18 .format(&PASSWORD_FORMAT
)
30 schema
: SINGLE_LINE_COMMENT_SCHEMA
,
34 schema
: user
::ENABLE_USER_SCHEMA
,
38 schema
: user
::EXPIRE_USER_SCHEMA
,
42 schema
: user
::FIRST_NAME_SCHEMA
,
45 schema
: user
::LAST_NAME_SCHEMA
,
49 schema
: user
::EMAIL_SCHEMA
,
55 description
: "List of user's API tokens.",
62 #[derive(Serialize,Deserialize)]
63 /// User properties with added list of ApiTokens
64 pub struct UserWithTokens
{
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
>,
83 fn new(user
: user
::User
) -> Self {
86 comment
: user
.comment
,
89 firstname
: user
.firstname
,
90 lastname
: user
.lastname
,
102 description
: "Include user's API tokens in returned list.",
109 description
: "List users (with config digest).",
111 items
: { type: UserWithTokens }
,
114 permission
: &Permission
::Anybody
,
115 description
: "Returns all or just the logged-in user (/API token owner), depending on privileges.",
120 include_tokens
: bool
,
122 mut rpcenv
: &mut dyn RpcEnvironment
,
123 ) -> Result
<Vec
<UserWithTokens
>, Error
> {
125 let (config
, digest
) = user
::config()?
;
127 let auth_id
: Authid
= rpcenv
129 .ok_or_else(|| format_err
!("no authid available"))?
132 let userid
= auth_id
.user();
134 let user_info
= CachedUserInfo
::new()?
;
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;
139 let filter_by_privs
= |user
: &user
::User
| {
140 top_level_allowed
|| user
.userid
== *userid
144 let list
:Vec
<user
::User
> = config
.convert_to_typed_array("user")?
;
146 rpcenv
["digest"] = proxmox
::tools
::digest_to_hex(&digest
).into();
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
155 |mut map
: HashMap
<Userid
, Vec
<user
::ApiToken
>>, token
: user
::ApiToken
| {
156 if token
.tokenid
.is_token() {
158 .entry(token
.tokenid
.user().clone())
165 .map(|user
: user
::User
| {
166 let mut user
= UserWithTokens
::new(user
);
167 user
.tokens
= user_to_tokens
.remove(&user
.userid
).unwrap_or_default();
172 iter
.map(|user
: user
::User
| UserWithTokens
::new(user
))
187 schema
: SINGLE_LINE_COMMENT_SCHEMA
,
191 schema
: PBS_PASSWORD_SCHEMA
,
195 schema
: user
::ENABLE_USER_SCHEMA
,
199 schema
: user
::EXPIRE_USER_SCHEMA
,
203 schema
: user
::FIRST_NAME_SCHEMA
,
207 schema
: user
::LAST_NAME_SCHEMA
,
211 schema
: user
::EMAIL_SCHEMA
,
217 permission
: &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
222 password
: Option
<String
>,
224 rpcenv
: &mut dyn RpcEnvironment
225 ) -> Result
<(), Error
> {
227 let _lock
= open_file_locked(user
::USER_CFG_LOCKFILE
, std
::time
::Duration
::new(10, 0), true)?
;
229 let user
: user
::User
= serde_json
::from_value(param
)?
;
231 let (mut config
, _digest
) = user
::config()?
;
233 if let Some(_
) = config
.sections
.get(user
.userid
.as_str()) {
234 bail
!("user '{}' already exists.", user
.userid
);
237 config
.set_data(user
.userid
.as_str(), "user", &user
)?
;
239 let realm
= user
.userid
.realm();
241 // Fails if realm does not exist!
242 let authenticator
= crate::auth
::lookup_authenticator(realm
)?
;
244 user
::save_config(&config
)?
;
246 if let Some(password
) = password
{
247 let user_info
= CachedUserInfo
::new()?
;
248 let current_auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
249 if realm
== "pam" && !user_info
.is_superuser(¤t_auth_id
) {
250 bail
!("only superuser can edit pam credentials!");
252 authenticator
.store_password(user
.userid
.name(), &password
)?
;
266 returns
: { type: user::User }
,
268 permission
: &Permission
::Or(&[
269 &Permission
::Privilege(&["access", "users"], PRIV_SYS_AUDIT
, false),
270 &Permission
::UserParam("userid"),
274 /// Read user configuration data.
275 pub fn read_user(userid
: Userid
, mut rpcenv
: &mut dyn RpcEnvironment
) -> Result
<user
::User
, Error
> {
276 let (config
, digest
) = user
::config()?
;
277 let user
= config
.lookup("user", userid
.as_str())?
;
278 rpcenv
["digest"] = proxmox
::tools
::digest_to_hex(&digest
).into();
283 #[derive(Serialize, Deserialize)]
284 #[serde(rename_all="kebab-case")]
285 #[allow(non_camel_case_types)]
286 pub enum DeletableProperty
{
287 /// Delete the comment property.
289 /// Delete the firstname property.
291 /// Delete the lastname property.
293 /// Delete the email property.
306 schema
: SINGLE_LINE_COMMENT_SCHEMA
,
309 schema
: PBS_PASSWORD_SCHEMA
,
313 schema
: user
::ENABLE_USER_SCHEMA
,
317 schema
: user
::EXPIRE_USER_SCHEMA
,
321 schema
: user
::FIRST_NAME_SCHEMA
,
325 schema
: user
::LAST_NAME_SCHEMA
,
329 schema
: user
::EMAIL_SCHEMA
,
333 description
: "List of properties to delete.",
337 type: DeletableProperty
,
342 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
347 permission
: &Permission
::Or(&[
348 &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
349 &Permission
::UserParam("userid"),
353 /// Update user configuration.
356 comment
: Option
<String
>,
357 enable
: Option
<bool
>,
359 password
: Option
<String
>,
360 firstname
: Option
<String
>,
361 lastname
: Option
<String
>,
362 email
: Option
<String
>,
363 delete
: Option
<Vec
<DeletableProperty
>>,
364 digest
: Option
<String
>,
365 rpcenv
: &mut dyn RpcEnvironment
,
366 ) -> Result
<(), Error
> {
368 let _lock
= open_file_locked(user
::USER_CFG_LOCKFILE
, std
::time
::Duration
::new(10, 0), true)?
;
370 let (mut config
, expected_digest
) = user
::config()?
;
372 if let Some(ref digest
) = digest
{
373 let digest
= proxmox
::tools
::hex_to_digest(digest
)?
;
374 crate::tools
::detect_modified_configuration_file(&digest
, &expected_digest
)?
;
377 let mut data
: user
::User
= config
.lookup("user", userid
.as_str())?
;
379 if let Some(delete
) = delete
{
380 for delete_prop
in delete
{
382 DeletableProperty
::comment
=> data
.comment
= None
,
383 DeletableProperty
::firstname
=> data
.firstname
= None
,
384 DeletableProperty
::lastname
=> data
.lastname
= None
,
385 DeletableProperty
::email
=> data
.email
= None
,
390 if let Some(comment
) = comment
{
391 let comment
= comment
.trim().to_string();
392 if comment
.is_empty() {
395 data
.comment
= Some(comment
);
399 if let Some(enable
) = enable
{
400 data
.enable
= if enable { None }
else { Some(false) }
;
403 if let Some(expire
) = expire
{
404 data
.expire
= if expire
> 0 { Some(expire) }
else { None }
;
407 if let Some(password
) = password
{
408 let user_info
= CachedUserInfo
::new()?
;
409 let current_auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
410 let self_service
= current_auth_id
.user() == &userid
;
411 let target_realm
= userid
.realm();
412 if !self_service
&& target_realm
== "pam" && !user_info
.is_superuser(¤t_auth_id
) {
413 bail
!("only superuser can edit pam credentials!");
415 let authenticator
= crate::auth
::lookup_authenticator(userid
.realm())?
;
416 authenticator
.store_password(userid
.name(), &password
)?
;
419 if let Some(firstname
) = firstname
{
420 data
.firstname
= if firstname
.is_empty() { None }
else { Some(firstname) }
;
423 if let Some(lastname
) = lastname
{
424 data
.lastname
= if lastname
.is_empty() { None }
else { Some(lastname) }
;
426 if let Some(email
) = email
{
427 data
.email
= if email
.is_empty() { None }
else { Some(email) }
;
430 config
.set_data(userid
.as_str(), "user", &data
)?
;
432 user
::save_config(&config
)?
;
446 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
451 permission
: &Permission
::Or(&[
452 &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
453 &Permission
::UserParam("userid"),
457 /// Remove a user from the configuration file.
458 pub fn delete_user(userid
: Userid
, digest
: Option
<String
>) -> Result
<(), Error
> {
460 let _tfa_lock
= crate::config
::tfa
::write_lock()?
;
461 let _lock
= open_file_locked(user
::USER_CFG_LOCKFILE
, std
::time
::Duration
::new(10, 0), true)?
;
463 let (mut config
, expected_digest
) = user
::config()?
;
465 if let Some(ref digest
) = digest
{
466 let digest
= proxmox
::tools
::hex_to_digest(digest
)?
;
467 crate::tools
::detect_modified_configuration_file(&digest
, &expected_digest
)?
;
470 match config
.sections
.get(userid
.as_str()) {
471 Some(_
) => { config.sections.remove(userid.as_str()); }
,
472 None
=> bail
!("user '{}' does not exist.", userid
),
475 user
::save_config(&config
)?
;
477 match crate::config
::tfa
::read().and_then(|mut cfg
| {
478 let _
: bool
= cfg
.remove_user(&userid
);
479 crate::config
::tfa
::write(&cfg
)
484 "error updating TFA config after deleting user {:?}: {}",
504 returns
: { type: user::ApiToken }
,
506 permission
: &Permission
::Or(&[
507 &Permission
::Privilege(&["access", "users"], PRIV_SYS_AUDIT
, false),
508 &Permission
::UserParam("userid"),
512 /// Read user's API token metadata
515 tokenname
: Tokenname
,
517 mut rpcenv
: &mut dyn RpcEnvironment
,
518 ) -> Result
<user
::ApiToken
, Error
> {
520 let (config
, digest
) = user
::config()?
;
522 let tokenid
= Authid
::from((userid
, Some(tokenname
)));
524 rpcenv
["digest"] = proxmox
::tools
::digest_to_hex(&digest
).into();
525 config
.lookup("token", &tokenid
.to_string())
540 schema
: SINGLE_LINE_COMMENT_SCHEMA
,
543 schema
: user
::ENABLE_USER_SCHEMA
,
547 schema
: user
::EXPIRE_USER_SCHEMA
,
552 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
557 permission
: &Permission
::Or(&[
558 &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
559 &Permission
::UserParam("userid"),
563 description
: "API token identifier + generated secret.",
567 description
: "The API token secret",
571 description
: "The API token identifier",
576 /// Generate a new API token with given metadata
577 pub fn generate_token(
579 tokenname
: Tokenname
,
580 comment
: Option
<String
>,
581 enable
: Option
<bool
>,
583 digest
: Option
<String
>,
584 ) -> Result
<Value
, Error
> {
586 let _lock
= open_file_locked(user
::USER_CFG_LOCKFILE
, std
::time
::Duration
::new(10, 0), true)?
;
588 let (mut config
, expected_digest
) = user
::config()?
;
590 if let Some(ref digest
) = digest
{
591 let digest
= proxmox
::tools
::hex_to_digest(digest
)?
;
592 crate::tools
::detect_modified_configuration_file(&digest
, &expected_digest
)?
;
595 let tokenid
= Authid
::from((userid
.clone(), Some(tokenname
.clone())));
596 let tokenid_string
= tokenid
.to_string();
598 if let Some(_
) = config
.sections
.get(&tokenid_string
) {
599 bail
!("token '{}' for user '{}' already exists.", tokenname
.as_str(), userid
);
602 let secret
= format
!("{:x}", proxmox
::tools
::uuid
::Uuid
::generate());
603 token_shadow
::set_secret(&tokenid
, &secret
)?
;
605 let token
= user
::ApiToken
{
612 config
.set_data(&tokenid_string
, "token", &token
)?
;
614 user
::save_config(&config
)?
;
617 "tokenid": tokenid_string
,
634 schema
: SINGLE_LINE_COMMENT_SCHEMA
,
637 schema
: user
::ENABLE_USER_SCHEMA
,
641 schema
: user
::EXPIRE_USER_SCHEMA
,
646 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
651 permission
: &Permission
::Or(&[
652 &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
653 &Permission
::UserParam("userid"),
657 /// Update user's API token metadata
660 tokenname
: Tokenname
,
661 comment
: Option
<String
>,
662 enable
: Option
<bool
>,
664 digest
: Option
<String
>,
665 ) -> Result
<(), Error
> {
667 let _lock
= open_file_locked(user
::USER_CFG_LOCKFILE
, std
::time
::Duration
::new(10, 0), true)?
;
669 let (mut config
, expected_digest
) = user
::config()?
;
671 if let Some(ref digest
) = digest
{
672 let digest
= proxmox
::tools
::hex_to_digest(digest
)?
;
673 crate::tools
::detect_modified_configuration_file(&digest
, &expected_digest
)?
;
676 let tokenid
= Authid
::from((userid
, Some(tokenname
)));
677 let tokenid_string
= tokenid
.to_string();
679 let mut data
: user
::ApiToken
= config
.lookup("token", &tokenid_string
)?
;
681 if let Some(comment
) = comment
{
682 let comment
= comment
.trim().to_string();
683 if comment
.is_empty() {
686 data
.comment
= Some(comment
);
690 if let Some(enable
) = enable
{
691 data
.enable
= if enable { None }
else { Some(false) }
;
694 if let Some(expire
) = expire
{
695 data
.expire
= if expire
> 0 { Some(expire) }
else { None }
;
698 config
.set_data(&tokenid_string
, "token", &data
)?
;
700 user
::save_config(&config
)?
;
717 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
722 permission
: &Permission
::Or(&[
723 &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
724 &Permission
::UserParam("userid"),
728 /// Delete a user's API token
731 tokenname
: Tokenname
,
732 digest
: Option
<String
>,
733 ) -> Result
<(), Error
> {
735 let _lock
= open_file_locked(user
::USER_CFG_LOCKFILE
, std
::time
::Duration
::new(10, 0), true)?
;
737 let (mut config
, expected_digest
) = user
::config()?
;
739 if let Some(ref digest
) = digest
{
740 let digest
= proxmox
::tools
::hex_to_digest(digest
)?
;
741 crate::tools
::detect_modified_configuration_file(&digest
, &expected_digest
)?
;
744 let tokenid
= Authid
::from((userid
.clone(), Some(tokenname
.clone())));
745 let tokenid_string
= tokenid
.to_string();
747 match config
.sections
.get(&tokenid_string
) {
748 Some(_
) => { config.sections.remove(&tokenid_string); }
,
749 None
=> bail
!("token '{}' of user '{}' does not exist.", tokenname
.as_str(), userid
),
752 token_shadow
::delete_secret(&tokenid
)?
;
754 user
::save_config(&config
)?
;
768 description
: "List user's API tokens (with config digest).",
770 items
: { type: user::ApiToken }
,
773 permission
: &Permission
::Or(&[
774 &Permission
::Privilege(&["access", "users"], PRIV_SYS_AUDIT
, false),
775 &Permission
::UserParam("userid"),
779 /// List user's API tokens
783 mut rpcenv
: &mut dyn RpcEnvironment
,
784 ) -> Result
<Vec
<user
::ApiToken
>, Error
> {
786 let (config
, digest
) = user
::config()?
;
788 let list
:Vec
<user
::ApiToken
> = config
.convert_to_typed_array("token")?
;
790 rpcenv
["digest"] = proxmox
::tools
::digest_to_hex(&digest
).into();
792 let filter_by_owner
= |token
: &user
::ApiToken
| {
793 if token
.tokenid
.is_token() {
794 token
.tokenid
.user() == &userid
800 Ok(list
.into_iter().filter(filter_by_owner
).collect())
803 const TOKEN_ITEM_ROUTER
: Router
= Router
::new()
804 .get(&API_METHOD_READ_TOKEN
)
805 .put(&API_METHOD_UPDATE_TOKEN
)
806 .post(&API_METHOD_GENERATE_TOKEN
)
807 .delete(&API_METHOD_DELETE_TOKEN
);
809 const TOKEN_ROUTER
: Router
= Router
::new()
810 .get(&API_METHOD_LIST_TOKENS
)
811 .match_all("tokenname", &TOKEN_ITEM_ROUTER
);
813 const USER_SUBDIRS
: SubdirMap
= &[
814 ("token", &TOKEN_ROUTER
),
817 const USER_ROUTER
: Router
= Router
::new()
818 .get(&API_METHOD_READ_USER
)
819 .put(&API_METHOD_UPDATE_USER
)
820 .delete(&API_METHOD_DELETE_USER
)
821 .subdirs(USER_SUBDIRS
);
823 pub const ROUTER
: Router
= Router
::new()
824 .get(&API_METHOD_LIST_USERS
)
825 .post(&API_METHOD_CREATE_USER
)
826 .match_all("userid", &USER_ROUTER
);