9407810fe1
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
302 lines
8.8 KiB
Rust
302 lines
8.8 KiB
Rust
//! 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<String>,
|
|
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>("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<Vec<methods::TypedTfaInfo>, 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<methods::TypedTfaInfo, Error> {
|
|
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<String>,
|
|
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<Vec<methods::TfaUser>, 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<String>,
|
|
totp: Option<String>,
|
|
value: Option<String>,
|
|
challenge: Option<String>,
|
|
password: Option<String>,
|
|
r#type: methods::TfaType,
|
|
rpcenv: &mut dyn RpcEnvironment,
|
|
) -> Result<methods::TfaUpdateInfo, Error> {
|
|
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<String>,
|
|
enable: Option<bool>,
|
|
password: Option<String>,
|
|
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);
|