-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;
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};
.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: {
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
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()?;
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
};
});
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()
};
},
)]
/// 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)?;
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(¤t_auth_id) {
+ bail!("only superuser can edit pam credentials!");
+ }
authenticator.store_password(user.userid.name(), &password)?;
}
},
},
},
- 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),
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: {
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,
},
)]
/// Update user configuration.
+#[allow(clippy::too_many_arguments)]
pub fn update_user(
userid: Userid,
comment: Option<String>,
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)?;
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() {
}
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(¤t_auth_id) {
+ bail!("only superuser can edit pam credentials!");
+ }
let authenticator = crate::auth::lookup_authenticator(userid.realm())?;
authenticator.store_password(userid.name(), &password)?;
}
/// 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()?;
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(())
}
},
},
},
- 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),
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);
}
token_shadow::set_secret(&tokenid, &secret)?;
let token = user::ApiToken {
- tokenid: tokenid.clone(),
+ tokenid,
comment,
enable,
expire,