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),
221 pub fn create_user(password
: Option
<String
>, param
: Value
) -> Result
<(), Error
> {
223 let _lock
= open_file_locked(user
::USER_CFG_LOCKFILE
, std
::time
::Duration
::new(10, 0), true)?
;
225 let user
: user
::User
= serde_json
::from_value(param
)?
;
227 let (mut config
, _digest
) = user
::config()?
;
229 if let Some(_
) = config
.sections
.get(user
.userid
.as_str()) {
230 bail
!("user '{}' already exists.", user
.userid
);
233 let authenticator
= crate::auth
::lookup_authenticator(&user
.userid
.realm())?
;
235 config
.set_data(user
.userid
.as_str(), "user", &user
)?
;
237 user
::save_config(&config
)?
;
239 if let Some(password
) = password
{
240 authenticator
.store_password(user
.userid
.name(), &password
)?
;
254 returns
: { type: user::User }
,
256 permission
: &Permission
::Or(&[
257 &Permission
::Privilege(&["access", "users"], PRIV_SYS_AUDIT
, false),
258 &Permission
::UserParam("userid"),
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();
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.
277 /// Delete the firstname property.
279 /// Delete the lastname property.
281 /// Delete the email property.
294 schema
: SINGLE_LINE_COMMENT_SCHEMA
,
297 schema
: PBS_PASSWORD_SCHEMA
,
301 schema
: user
::ENABLE_USER_SCHEMA
,
305 schema
: user
::EXPIRE_USER_SCHEMA
,
309 schema
: user
::FIRST_NAME_SCHEMA
,
313 schema
: user
::LAST_NAME_SCHEMA
,
317 schema
: user
::EMAIL_SCHEMA
,
321 description
: "List of properties to delete.",
325 type: DeletableProperty
,
330 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
335 permission
: &Permission
::Or(&[
336 &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
337 &Permission
::UserParam("userid"),
341 /// Update user configuration.
344 comment
: Option
<String
>,
345 enable
: Option
<bool
>,
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
> {
355 let _lock
= open_file_locked(user
::USER_CFG_LOCKFILE
, std
::time
::Duration
::new(10, 0), true)?
;
357 let (mut config
, expected_digest
) = user
::config()?
;
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
)?
;
364 let mut data
: user
::User
= config
.lookup("user", userid
.as_str())?
;
366 if let Some(delete
) = delete
{
367 for delete_prop
in delete
{
369 DeletableProperty
::comment
=> data
.comment
= None
,
370 DeletableProperty
::firstname
=> data
.firstname
= None
,
371 DeletableProperty
::lastname
=> data
.lastname
= None
,
372 DeletableProperty
::email
=> data
.email
= None
,
377 if let Some(comment
) = comment
{
378 let comment
= comment
.trim().to_string();
379 if comment
.is_empty() {
382 data
.comment
= Some(comment
);
386 if let Some(enable
) = enable
{
387 data
.enable
= if enable { None }
else { Some(false) }
;
390 if let Some(expire
) = expire
{
391 data
.expire
= if expire
> 0 { Some(expire) }
else { None }
;
394 if let Some(password
) = password
{
395 let authenticator
= crate::auth
::lookup_authenticator(userid
.realm())?
;
396 authenticator
.store_password(userid
.name(), &password
)?
;
399 if let Some(firstname
) = firstname
{
400 data
.firstname
= if firstname
.is_empty() { None }
else { Some(firstname) }
;
403 if let Some(lastname
) = lastname
{
404 data
.lastname
= if lastname
.is_empty() { None }
else { Some(lastname) }
;
406 if let Some(email
) = email
{
407 data
.email
= if email
.is_empty() { None }
else { Some(email) }
;
410 config
.set_data(userid
.as_str(), "user", &data
)?
;
412 user
::save_config(&config
)?
;
426 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
431 permission
: &Permission
::Or(&[
432 &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
433 &Permission
::UserParam("userid"),
437 /// Remove a user from the configuration file.
438 pub fn delete_user(userid
: Userid
, digest
: Option
<String
>) -> Result
<(), Error
> {
440 let _lock
= open_file_locked(user
::USER_CFG_LOCKFILE
, std
::time
::Duration
::new(10, 0), true)?
;
442 let (mut config
, expected_digest
) = user
::config()?
;
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
)?
;
449 match config
.sections
.get(userid
.as_str()) {
450 Some(_
) => { config.sections.remove(userid.as_str()); }
,
451 None
=> bail
!("user '{}' does not exist.", userid
),
454 user
::save_config(&config
)?
;
470 returns
: { type: user::ApiToken }
,
472 permission
: &Permission
::Or(&[
473 &Permission
::Privilege(&["access", "users"], PRIV_SYS_AUDIT
, false),
474 &Permission
::UserParam("userid"),
478 /// Read user's API token metadata
481 tokenname
: Tokenname
,
483 mut rpcenv
: &mut dyn RpcEnvironment
,
484 ) -> Result
<user
::ApiToken
, Error
> {
486 let (config
, digest
) = user
::config()?
;
488 let tokenid
= Authid
::from((userid
, Some(tokenname
)));
490 rpcenv
["digest"] = proxmox
::tools
::digest_to_hex(&digest
).into();
491 config
.lookup("token", &tokenid
.to_string())
506 schema
: SINGLE_LINE_COMMENT_SCHEMA
,
509 schema
: user
::ENABLE_USER_SCHEMA
,
513 schema
: user
::EXPIRE_USER_SCHEMA
,
518 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
523 permission
: &Permission
::Or(&[
524 &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
525 &Permission
::UserParam("userid"),
529 description
: "API token identifier + generated secret.",
533 description
: "The API token secret",
537 description
: "The API token identifier",
542 /// Generate a new API token with given metadata
543 pub fn generate_token(
545 tokenname
: Tokenname
,
546 comment
: Option
<String
>,
547 enable
: Option
<bool
>,
549 digest
: Option
<String
>,
550 ) -> Result
<Value
, Error
> {
552 let _lock
= open_file_locked(user
::USER_CFG_LOCKFILE
, std
::time
::Duration
::new(10, 0), true)?
;
554 let (mut config
, expected_digest
) = user
::config()?
;
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
)?
;
561 let tokenid
= Authid
::from((userid
.clone(), Some(tokenname
.clone())));
562 let tokenid_string
= tokenid
.to_string();
564 if let Some(_
) = config
.sections
.get(&tokenid_string
) {
565 bail
!("token '{}' for user '{}' already exists.", tokenname
.as_str(), userid
);
568 let secret
= format
!("{:x}", proxmox
::tools
::uuid
::Uuid
::generate());
569 token_shadow
::set_secret(&tokenid
, &secret
)?
;
571 let token
= user
::ApiToken
{
572 tokenid
: tokenid
.clone(),
578 config
.set_data(&tokenid_string
, "token", &token
)?
;
580 user
::save_config(&config
)?
;
583 "tokenid": tokenid_string
,
600 schema
: SINGLE_LINE_COMMENT_SCHEMA
,
603 schema
: user
::ENABLE_USER_SCHEMA
,
607 schema
: user
::EXPIRE_USER_SCHEMA
,
612 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
617 permission
: &Permission
::Or(&[
618 &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
619 &Permission
::UserParam("userid"),
623 /// Update user's API token metadata
626 tokenname
: Tokenname
,
627 comment
: Option
<String
>,
628 enable
: Option
<bool
>,
630 digest
: Option
<String
>,
631 ) -> Result
<(), Error
> {
633 let _lock
= open_file_locked(user
::USER_CFG_LOCKFILE
, std
::time
::Duration
::new(10, 0), true)?
;
635 let (mut config
, expected_digest
) = user
::config()?
;
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
)?
;
642 let tokenid
= Authid
::from((userid
, Some(tokenname
)));
643 let tokenid_string
= tokenid
.to_string();
645 let mut data
: user
::ApiToken
= config
.lookup("token", &tokenid_string
)?
;
647 if let Some(comment
) = comment
{
648 let comment
= comment
.trim().to_string();
649 if comment
.is_empty() {
652 data
.comment
= Some(comment
);
656 if let Some(enable
) = enable
{
657 data
.enable
= if enable { None }
else { Some(false) }
;
660 if let Some(expire
) = expire
{
661 data
.expire
= if expire
> 0 { Some(expire) }
else { None }
;
664 config
.set_data(&tokenid_string
, "token", &data
)?
;
666 user
::save_config(&config
)?
;
683 schema
: PROXMOX_CONFIG_DIGEST_SCHEMA
,
688 permission
: &Permission
::Or(&[
689 &Permission
::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY
, false),
690 &Permission
::UserParam("userid"),
694 /// Delete a user's API token
697 tokenname
: Tokenname
,
698 digest
: Option
<String
>,
699 ) -> Result
<(), Error
> {
701 let _lock
= open_file_locked(user
::USER_CFG_LOCKFILE
, std
::time
::Duration
::new(10, 0), true)?
;
703 let (mut config
, expected_digest
) = user
::config()?
;
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
)?
;
710 let tokenid
= Authid
::from((userid
.clone(), Some(tokenname
.clone())));
711 let tokenid_string
= tokenid
.to_string();
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
),
718 token_shadow
::delete_secret(&tokenid
)?
;
720 user
::save_config(&config
)?
;
734 description
: "List user's API tokens (with config digest).",
736 items
: { type: user::ApiToken }
,
739 permission
: &Permission
::Or(&[
740 &Permission
::Privilege(&["access", "users"], PRIV_SYS_AUDIT
, false),
741 &Permission
::UserParam("userid"),
745 /// List user's API tokens
749 mut rpcenv
: &mut dyn RpcEnvironment
,
750 ) -> Result
<Vec
<user
::ApiToken
>, Error
> {
752 let (config
, digest
) = user
::config()?
;
754 let list
:Vec
<user
::ApiToken
> = config
.convert_to_typed_array("token")?
;
756 rpcenv
["digest"] = proxmox
::tools
::digest_to_hex(&digest
).into();
758 let filter_by_owner
= |token
: &user
::ApiToken
| {
759 if token
.tokenid
.is_token() {
760 token
.tokenid
.user() == &userid
766 Ok(list
.into_iter().filter(filter_by_owner
).collect())
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
);
775 const TOKEN_ROUTER
: Router
= Router
::new()
776 .get(&API_METHOD_LIST_TOKENS
)
777 .match_all("tokenname", &TOKEN_ITEM_ROUTER
);
779 const USER_SUBDIRS
: SubdirMap
= &[
780 ("token", &TOKEN_ROUTER
),
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
);
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
);