//! Two Factor Authentication use anyhow::Error; use proxmox_router::{http_bail, http_err, Permission, Router, RpcEnvironment}; use proxmox_schema::api; use proxmox_tfa::api::methods; use pbs_api_types::{ Authid, User, Userid, PASSWORD_SCHEMA, PRIV_PERMISSIONS_MODIFY, PRIV_SYS_AUDIT, }; use pbs_config::CachedUserInfo; use crate::config::tfa::UserAccess; /// Perform first-factor (password) authentication only. Ignore password for the root user. /// Otherwise check the current user's password. /// /// This means that user admins need to type in their own password while editing a user, and /// regular users, which can only change their own TFA settings (checked at the API level), can /// change their own settings using their own password. fn tfa_update_auth( rpcenv: &mut dyn RpcEnvironment, userid: &Userid, password: Option, must_exist: bool, ) -> Result<(), Error> { let authid: Authid = rpcenv.get_auth_id().unwrap().parse()?; if authid.user() != Userid::root_userid() { let password = password.ok_or_else(|| http_err!(UNAUTHORIZED, "missing password"))?; let _: () = crate::auth::authenticate_user(authid.user(), &password) .map_err(|err| http_err!(UNAUTHORIZED, "{}", err))?; } // After authentication, verify that the to-be-modified user actually exists: if must_exist && authid.user() != userid { let (config, _digest) = pbs_config::user::config()?; if config.lookup::("user", userid.as_str()).is_err() { http_bail!(UNAUTHORIZED, "user '{}' does not exists.", userid); } } Ok(()) } #[api( protected: true, input: { properties: { userid: { type: Userid } }, }, access: { permission: &Permission::Or(&[ &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false), &Permission::UserParam("userid"), ]), }, )] /// Add a TOTP secret to the user. fn list_user_tfa(userid: Userid) -> Result, Error> { let _lock = crate::config::tfa::read_lock()?; methods::list_user_tfa(&crate::config::tfa::read()?, userid.as_str()) } #[api( protected: true, input: { properties: { userid: { type: Userid }, id: { description: "the tfa entry id" } }, }, access: { permission: &Permission::Or(&[ &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false), &Permission::UserParam("userid"), ]), }, )] /// Get a single TFA entry. fn get_tfa_entry(userid: Userid, id: String) -> Result { let _lock = crate::config::tfa::read_lock()?; match methods::get_tfa_entry(&crate::config::tfa::read()?, userid.as_str(), &id) { Some(entry) => Ok(entry), None => http_bail!(NOT_FOUND, "no such tfa entry: {}/{}", userid, id), } } #[api( protected: true, input: { properties: { userid: { type: Userid }, id: { description: "the tfa entry id", }, password: { schema: PASSWORD_SCHEMA, optional: true, }, }, }, access: { permission: &Permission::Or(&[ &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false), &Permission::UserParam("userid"), ]), }, )] /// Delete a single TFA entry. fn delete_tfa( userid: Userid, id: String, password: Option, rpcenv: &mut dyn RpcEnvironment, ) -> Result<(), Error> { tfa_update_auth(rpcenv, &userid, password, false)?; let _lock = crate::config::tfa::write_lock()?; let mut data = crate::config::tfa::read()?; match methods::delete_tfa(&mut data, userid.as_str(), &id) { Ok(_) => (), Err(methods::EntryNotFound) => { http_bail!(NOT_FOUND, "no such tfa entry: {}/{}", userid, id) } } crate::config::tfa::write(&data)?; Ok(()) } #[api( protected: true, input: { properties: {}, }, access: { permission: &Permission::Anybody, description: "Returns all or just the logged-in user, depending on privileges.", }, returns: { description: "The list tuples of user and TFA entries.", type: Array, items: { type: methods::TfaUser } }, )] /// List user TFA configuration. fn list_tfa(rpcenv: &mut dyn RpcEnvironment) -> Result, Error> { let authid: Authid = rpcenv.get_auth_id().unwrap().parse()?; let user_info = CachedUserInfo::new()?; let top_level_privs = user_info.lookup_privs(&authid, &["access", "users"]); let top_level_allowed = (top_level_privs & PRIV_SYS_AUDIT) != 0; let _lock = crate::config::tfa::read_lock()?; let tfa_data = crate::config::tfa::read()?; methods::list_tfa(&tfa_data, authid.user().as_str(), top_level_allowed) } #[api( protected: true, input: { properties: { userid: { type: Userid }, description: { description: "A description to distinguish multiple entries from one another", type: String, max_length: 255, optional: true, }, "type": { type: methods::TfaType }, totp: { description: "A totp URI.", optional: true, }, value: { description: "The current value for the provided totp URI, or a Webauthn/U2F challenge response", optional: true, }, challenge: { description: "When responding to a u2f challenge: the original challenge string", optional: true, }, password: { schema: PASSWORD_SCHEMA, optional: true, }, }, }, returns: { type: methods::TfaUpdateInfo }, access: { permission: &Permission::Or(&[ &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false), &Permission::UserParam("userid"), ]), }, )] /// Add a TFA entry to the user. #[allow(clippy::too_many_arguments)] fn add_tfa_entry( userid: Userid, description: Option, totp: Option, value: Option, challenge: Option, password: Option, r#type: methods::TfaType, rpcenv: &mut dyn RpcEnvironment, ) -> Result { tfa_update_auth(rpcenv, &userid, password, true)?; let _lock = crate::config::tfa::write_lock()?; let mut data = crate::config::tfa::read()?; let out = methods::add_tfa_entry( &mut data, UserAccess, userid.as_str(), description, totp, value, challenge, r#type, )?; crate::config::tfa::write(&data)?; Ok(out) } #[api( protected: true, input: { properties: { userid: { type: Userid }, id: { description: "the tfa entry id", }, description: { description: "A description to distinguish multiple entries from one another", type: String, max_length: 255, optional: true, }, enable: { description: "Whether this entry should currently be enabled or disabled", optional: true, }, password: { schema: PASSWORD_SCHEMA, optional: true, }, }, }, access: { permission: &Permission::Or(&[ &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false), &Permission::UserParam("userid"), ]), }, )] /// Update user's TFA entry description. fn update_tfa_entry( userid: Userid, id: String, description: Option, enable: Option, password: Option, rpcenv: &mut dyn RpcEnvironment, ) -> Result<(), Error> { tfa_update_auth(rpcenv, &userid, password, true)?; let _lock = crate::config::tfa::write_lock()?; let mut data = crate::config::tfa::read()?; match methods::update_tfa_entry(&mut data, userid.as_str(), &id, description, enable) { Ok(()) => (), Err(methods::EntryNotFound) => http_bail!(NOT_FOUND, "no such entry: {}/{}", userid, id), } crate::config::tfa::write(&data)?; Ok(()) } pub const ROUTER: Router = Router::new() .get(&API_METHOD_LIST_TFA) .match_all("userid", &USER_ROUTER); const USER_ROUTER: Router = Router::new() .get(&API_METHOD_LIST_USER_TFA) .post(&API_METHOD_ADD_TFA_ENTRY) .match_all("id", &ITEM_ROUTER); const ITEM_ROUTER: Router = Router::new() .get(&API_METHOD_GET_TFA_ENTRY) .put(&API_METHOD_UPDATE_TFA_ENTRY) .delete(&API_METHOD_DELETE_TFA);