]> git.proxmox.com Git - proxmox-backup.git/blobdiff - src/api2/access/user.rs
move client to pbs-client subcrate
[proxmox-backup.git] / src / api2 / access / user.rs
index 2d7f3ceca6daa901410f6a39e65f88d0c5d206c8..70481ffb1c5a61d35cf4ee0b290345995362d7e4 100644 (file)
@@ -1,4 +1,6 @@
-use anyhow::{bail, Error};
+//! User Management
+
+use anyhow::{bail, format_err, Error};
 use serde::{Serialize, Deserialize};
 use serde_json::{json, Value};
 use std::collections::HashMap;
@@ -8,7 +10,11 @@ use proxmox::api::router::SubdirMap;
 use proxmox::api::schema::{Schema, StringSchema};
 use proxmox::tools::fs::open_file_locked;
 
-use crate::api2::types::*;
+use pbs_api_types::{
+    PASSWORD_FORMAT, PROXMOX_CONFIG_DIGEST_SCHEMA, SINGLE_LINE_COMMENT_SCHEMA, Authid,
+    Tokenname, UserWithTokens, Userid,
+};
+
 use crate::config::user;
 use crate::config::token_shadow;
 use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_PERMISSIONS_MODIFY};
@@ -20,81 +26,19 @@ pub const PBS_PASSWORD_SCHEMA: Schema = StringSchema::new("User Password.")
     .max_length(64)
     .schema();
 
-#[api(
-    properties: {
-        userid: {
-            type: Userid,
-        },
-        comment: {
-            optional: true,
-            schema: SINGLE_LINE_COMMENT_SCHEMA,
-        },
-        enable: {
-            optional: true,
-            schema: user::ENABLE_USER_SCHEMA,
-        },
-        expire: {
-            optional: true,
-            schema: user::EXPIRE_USER_SCHEMA,
-        },
-        firstname: {
-            optional: true,
-            schema: user::FIRST_NAME_SCHEMA,
-        },
-        lastname: {
-            schema: user::LAST_NAME_SCHEMA,
-            optional: true,
-         },
-        email: {
-            schema: user::EMAIL_SCHEMA,
-            optional: true,
-        },
-        tokens: {
-            type: Array,
-            optional: true,
-            description: "List of user's API tokens.",
-            items: {
-                type: user::ApiToken
-            },
-        },
-    }
-)]
-#[derive(Serialize,Deserialize)]
-/// User properties with added list of ApiTokens
-pub struct UserWithTokens {
-    pub userid: Userid,
-    #[serde(skip_serializing_if="Option::is_none")]
-    pub comment: Option<String>,
-    #[serde(skip_serializing_if="Option::is_none")]
-    pub enable: Option<bool>,
-    #[serde(skip_serializing_if="Option::is_none")]
-    pub expire: Option<i64>,
-    #[serde(skip_serializing_if="Option::is_none")]
-    pub firstname: Option<String>,
-    #[serde(skip_serializing_if="Option::is_none")]
-    pub lastname: Option<String>,
-    #[serde(skip_serializing_if="Option::is_none")]
-    pub email: Option<String>,
-    #[serde(skip_serializing_if="Vec::is_empty")]
-    pub tokens: Vec<user::ApiToken>,
-}
-
-impl UserWithTokens {
-    fn new(user: user::User) -> Self {
-        Self {
-            userid: user.userid,
-            comment: user.comment,
-            enable: user.enable,
-            expire: user.expire,
-            firstname: user.firstname,
-            lastname: user.lastname,
-            email: user.email,
-            tokens: Vec::new(),
-        }
+fn new_user_with_tokens(user: user::User) -> UserWithTokens {
+    UserWithTokens {
+        userid: user.userid,
+        comment: user.comment,
+        enable: user.enable,
+        expire: user.expire,
+        firstname: user.firstname,
+        lastname: user.lastname,
+        email: user.email,
+        tokens: Vec::new(),
     }
 }
 
-
 #[api(
     input: {
         properties: {
@@ -109,11 +53,11 @@ impl UserWithTokens {
     returns: {
         description: "List users (with config digest).",
         type: Array,
-        items: { type: user::User },
+        items: { type: UserWithTokens },
     },
     access: {
         permission: &Permission::Anybody,
-        description: "Returns all or just the logged-in user, depending on privileges.",
+        description: "Returns all or just the logged-in user (/API token owner), depending on privileges.",
     },
 )]
 /// List users
@@ -125,9 +69,12 @@ pub fn list_users(
 
     let (config, digest) = user::config()?;
 
-    // intentionally user only for now
-    let userid: Userid = rpcenv.get_auth_id().unwrap().parse()?;
-    let auth_id = Authid::from(userid.clone());
+    let auth_id: Authid = rpcenv
+        .get_auth_id()
+        .ok_or_else(|| format_err!("no authid available"))?
+        .parse()?;
+
+    let userid = auth_id.user();
 
     let user_info = CachedUserInfo::new()?;
 
@@ -135,7 +82,7 @@ pub fn list_users(
     let top_level_allowed = (top_level_privs & PRIV_SYS_AUDIT) != 0;
 
     let filter_by_privs = |user: &user::User| {
-        top_level_allowed || user.userid == userid
+        top_level_allowed || user.userid == *userid
     };
 
 
@@ -161,13 +108,13 @@ pub fn list_users(
             });
         iter
             .map(|user: user::User| {
-                let mut user = UserWithTokens::new(user);
+                let mut user = new_user_with_tokens(user);
                 user.tokens = user_to_tokens.remove(&user.userid).unwrap_or_default();
                 user
             })
             .collect()
     } else {
-        iter.map(|user: user::User| UserWithTokens::new(user))
+        iter.map(new_user_with_tokens)
             .collect()
     };
 
@@ -216,7 +163,11 @@ pub fn list_users(
     },
 )]
 /// Create new user.
-pub fn create_user(password: Option<String>, param: Value) -> Result<(), Error> {
+pub fn create_user(
+    password: Option<String>,
+    param: Value,
+    rpcenv: &mut dyn RpcEnvironment
+) -> Result<(), Error> {
 
     let _lock = open_file_locked(user::USER_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
 
@@ -224,17 +175,25 @@ pub fn create_user(password: Option<String>, param: Value) -> Result<(), Error>
 
     let (mut config, _digest) = user::config()?;
 
-    if let Some(_) = config.sections.get(user.userid.as_str()) {
+    if config.sections.get(user.userid.as_str()).is_some() {
         bail!("user '{}' already exists.", user.userid);
     }
 
-    let authenticator = crate::auth::lookup_authenticator(&user.userid.realm())?;
-
     config.set_data(user.userid.as_str(), "user", &user)?;
 
+    let realm = user.userid.realm();
+
+    // Fails if realm does not exist!
+    let authenticator = crate::auth::lookup_authenticator(realm)?;
+
     user::save_config(&config)?;
 
     if let Some(password) = password {
+        let user_info = CachedUserInfo::new()?;
+        let current_auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
+        if realm == "pam" && !user_info.is_superuser(&current_auth_id) {
+            bail!("only superuser can edit pam credentials!");
+        }
         authenticator.store_password(user.userid.name(), &password)?;
     }
 
@@ -249,10 +208,7 @@ pub fn create_user(password: Option<String>, param: Value) -> Result<(), Error>
             },
          },
     },
-    returns: {
-        description: "The user configuration (with config digest).",
-        type: user::User,
-    },
+    returns: { type: user::User },
     access: {
         permission: &Permission::Or(&[
             &Permission::Privilege(&["access", "users"], PRIV_SYS_AUDIT, false),
@@ -268,6 +224,21 @@ pub fn read_user(userid: Userid, mut rpcenv: &mut dyn RpcEnvironment) -> Result<
     Ok(user)
 }
 
+#[api()]
+#[derive(Serialize, Deserialize)]
+#[serde(rename_all="kebab-case")]
+#[allow(non_camel_case_types)]
+pub enum DeletableProperty {
+    /// Delete the comment property.
+    comment,
+    /// Delete the firstname property.
+    firstname,
+    /// Delete the lastname property.
+    lastname,
+    /// Delete the email property.
+    email,
+}
+
 #[api(
     protected: true,
     input: {
@@ -303,6 +274,14 @@ pub fn read_user(userid: Userid, mut rpcenv: &mut dyn RpcEnvironment) -> Result<
                 schema: user::EMAIL_SCHEMA,
                 optional: true,
             },
+            delete: {
+                description: "List of properties to delete.",
+                type: Array,
+                optional: true,
+                items: {
+                    type: DeletableProperty,
+                }
+            },
             digest: {
                 optional: true,
                 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
@@ -317,6 +296,7 @@ pub fn read_user(userid: Userid, mut rpcenv: &mut dyn RpcEnvironment) -> Result<
     },
 )]
 /// Update user configuration.
+#[allow(clippy::too_many_arguments)]
 pub fn update_user(
     userid: Userid,
     comment: Option<String>,
@@ -326,7 +306,9 @@ pub fn update_user(
     firstname: Option<String>,
     lastname: Option<String>,
     email: Option<String>,
+    delete: Option<Vec<DeletableProperty>>,
     digest: Option<String>,
+    rpcenv: &mut dyn RpcEnvironment,
 ) -> Result<(), Error> {
 
     let _lock = open_file_locked(user::USER_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
@@ -340,6 +322,17 @@ pub fn update_user(
 
     let mut data: user::User = config.lookup("user", userid.as_str())?;
 
+    if let Some(delete) = delete {
+        for delete_prop in delete {
+            match delete_prop {
+                DeletableProperty::comment => data.comment = None,
+                DeletableProperty::firstname => data.firstname = None,
+                DeletableProperty::lastname => data.lastname = None,
+                DeletableProperty::email => data.email = None,
+            }
+        }
+    }
+
     if let Some(comment) = comment {
         let comment = comment.trim().to_string();
         if comment.is_empty() {
@@ -358,6 +351,13 @@ pub fn update_user(
     }
 
     if let Some(password) = password {
+        let user_info = CachedUserInfo::new()?;
+        let current_auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
+        let self_service = current_auth_id.user() == &userid;
+        let target_realm = userid.realm();
+        if !self_service && target_realm == "pam" && !user_info.is_superuser(&current_auth_id) {
+            bail!("only superuser can edit pam credentials!");
+        }
         let authenticator = crate::auth::lookup_authenticator(userid.realm())?;
         authenticator.store_password(userid.name(), &password)?;
     }
@@ -403,6 +403,7 @@ pub fn update_user(
 /// Remove a user from the configuration file.
 pub fn delete_user(userid: Userid, digest: Option<String>) -> Result<(), Error> {
 
+    let _tfa_lock = crate::config::tfa::write_lock()?;
     let _lock = open_file_locked(user::USER_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
 
     let (mut config, expected_digest) = user::config()?;
@@ -419,6 +420,30 @@ pub fn delete_user(userid: Userid, digest: Option<String>) -> Result<(), Error>
 
     user::save_config(&config)?;
 
+    let authenticator = crate::auth::lookup_authenticator(userid.realm())?;
+    match authenticator.remove_password(userid.name()) {
+        Ok(()) => {},
+        Err(err) => {
+            eprintln!(
+                "error removing password after deleting user {:?}: {}",
+                userid, err
+            );
+        }
+    }
+
+    match crate::config::tfa::read().and_then(|mut cfg| {
+        let _: bool = cfg.remove_user(&userid);
+        crate::config::tfa::write(&cfg)
+    }) {
+        Ok(()) => (),
+        Err(err) => {
+            eprintln!(
+                "error updating TFA config after deleting user {:?}: {}",
+                userid, err
+            );
+        }
+    }
+
     Ok(())
 }
 
@@ -433,10 +458,7 @@ pub fn delete_user(userid: Userid, digest: Option<String>) -> Result<(), Error>
             },
         },
     },
-    returns: {
-        description: "Get API token metadata (with config digest).",
-        type: user::ApiToken,
-    },
+    returns: { type: user::ApiToken },
     access: {
         permission: &Permission::Or(&[
             &Permission::Privilege(&["access", "users"], PRIV_SYS_AUDIT, false),
@@ -530,7 +552,7 @@ pub fn generate_token(
     let tokenid = Authid::from((userid.clone(), Some(tokenname.clone())));
     let tokenid_string = tokenid.to_string();
 
-    if let Some(_) = config.sections.get(&tokenid_string) {
+    if config.sections.get(&tokenid_string).is_some() {
         bail!("token '{}' for user '{}' already exists.", tokenname.as_str(), userid);
     }
 
@@ -538,7 +560,7 @@ pub fn generate_token(
     token_shadow::set_secret(&tokenid, &secret)?;
 
     let token = user::ApiToken {
-        tokenid: tokenid.clone(),
+        tokenid,
         comment,
         enable,
         expire,