tfa: add webauthn configuration API entry points
Currently there's not yet a node config and the WA config is somewhat "tightly coupled" to the user entries in that changing it can lock them all out, so for now I opted for fewer reorganization and just use a digest of the canonicalized config here, and keep it all in the tfa.json file. Experimentally using the flatten feature on the methods with an`Updater` struct similar to what the api macro is supposed to be able to derive on its own in the future. Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
parent
aefd74197a
commit
a670b99db1
@ -1,6 +1,7 @@
|
|||||||
use proxmox::api::router::{Router, SubdirMap};
|
use proxmox::api::router::{Router, SubdirMap};
|
||||||
use proxmox::list_subdirs_api_method;
|
use proxmox::list_subdirs_api_method;
|
||||||
|
|
||||||
|
pub mod access;
|
||||||
pub mod datastore;
|
pub mod datastore;
|
||||||
pub mod remote;
|
pub mod remote;
|
||||||
pub mod sync;
|
pub mod sync;
|
||||||
@ -10,13 +11,14 @@ pub mod changer;
|
|||||||
pub mod media_pool;
|
pub mod media_pool;
|
||||||
|
|
||||||
const SUBDIRS: SubdirMap = &[
|
const SUBDIRS: SubdirMap = &[
|
||||||
|
("access", &access::ROUTER),
|
||||||
("changer", &changer::ROUTER),
|
("changer", &changer::ROUTER),
|
||||||
("datastore", &datastore::ROUTER),
|
("datastore", &datastore::ROUTER),
|
||||||
("drive", &drive::ROUTER),
|
("drive", &drive::ROUTER),
|
||||||
("media-pool", &media_pool::ROUTER),
|
("media-pool", &media_pool::ROUTER),
|
||||||
("remote", &remote::ROUTER),
|
("remote", &remote::ROUTER),
|
||||||
("sync", &sync::ROUTER),
|
("sync", &sync::ROUTER),
|
||||||
("verify", &verify::ROUTER)
|
("verify", &verify::ROUTER),
|
||||||
];
|
];
|
||||||
|
|
||||||
pub const ROUTER: Router = Router::new()
|
pub const ROUTER: Router = Router::new()
|
||||||
|
10
src/api2/config/access/mod.rs
Normal file
10
src/api2/config/access/mod.rs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
use proxmox::api::{Router, SubdirMap};
|
||||||
|
use proxmox::list_subdirs_api_method;
|
||||||
|
|
||||||
|
pub mod tfa;
|
||||||
|
|
||||||
|
const SUBDIRS: SubdirMap = &[("tfa", &tfa::ROUTER)];
|
||||||
|
|
||||||
|
pub const ROUTER: Router = Router::new()
|
||||||
|
.get(&list_subdirs_api_method!(SUBDIRS))
|
||||||
|
.subdirs(SUBDIRS);
|
84
src/api2/config/access/tfa/mod.rs
Normal file
84
src/api2/config/access/tfa/mod.rs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
//! For now this only has the TFA subdir, which is in this file.
|
||||||
|
//! If we add more, it should be moved into a sub module.
|
||||||
|
|
||||||
|
use anyhow::Error;
|
||||||
|
|
||||||
|
use crate::api2::types::PROXMOX_CONFIG_DIGEST_SCHEMA;
|
||||||
|
use proxmox::api::{api, Permission, Router, RpcEnvironment, SubdirMap};
|
||||||
|
use proxmox::list_subdirs_api_method;
|
||||||
|
|
||||||
|
use crate::config::tfa::{self, WebauthnConfig, WebauthnConfigUpdater};
|
||||||
|
|
||||||
|
pub const ROUTER: Router = Router::new()
|
||||||
|
.get(&list_subdirs_api_method!(SUBDIRS))
|
||||||
|
.subdirs(SUBDIRS);
|
||||||
|
|
||||||
|
const SUBDIRS: SubdirMap = &[("webauthn", &WEBAUTHN_ROUTER)];
|
||||||
|
|
||||||
|
const WEBAUTHN_ROUTER: Router = Router::new()
|
||||||
|
.get(&API_METHOD_GET_WEBAUTHN_CONFIG)
|
||||||
|
.put(&API_METHOD_UPDATE_WEBAUTHN_CONFIG);
|
||||||
|
|
||||||
|
#[api(
|
||||||
|
protected: true,
|
||||||
|
input: {
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
returns: {
|
||||||
|
type: WebauthnConfig,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
permission: &Permission::Anybody,
|
||||||
|
},
|
||||||
|
)]
|
||||||
|
/// Get the TFA configuration.
|
||||||
|
pub fn get_webauthn_config(
|
||||||
|
mut rpcenv: &mut dyn RpcEnvironment,
|
||||||
|
) -> Result<Option<WebauthnConfig>, Error> {
|
||||||
|
let (config, digest) = match tfa::webauthn_config()? {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
|
||||||
|
Ok(Some(config))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[api(
|
||||||
|
protected: true,
|
||||||
|
input: {
|
||||||
|
properties: {
|
||||||
|
webauthn: {
|
||||||
|
flatten: true,
|
||||||
|
type: WebauthnConfigUpdater,
|
||||||
|
},
|
||||||
|
digest: {
|
||||||
|
optional: true,
|
||||||
|
schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)]
|
||||||
|
/// Update the TFA configuration.
|
||||||
|
pub fn update_webauthn_config(
|
||||||
|
webauthn: WebauthnConfigUpdater,
|
||||||
|
digest: Option<String>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let _lock = tfa::write_lock();
|
||||||
|
|
||||||
|
let mut tfa = tfa::read()?;
|
||||||
|
|
||||||
|
if let Some(wa) = &mut tfa.webauthn {
|
||||||
|
if let Some(ref digest) = digest {
|
||||||
|
let digest = proxmox::tools::hex_to_digest(digest)?;
|
||||||
|
crate::tools::detect_modified_configuration_file(&digest, &wa.digest()?)?;
|
||||||
|
}
|
||||||
|
webauthn.apply_to(wa);
|
||||||
|
} else {
|
||||||
|
tfa.webauthn = Some(webauthn.build()?);
|
||||||
|
}
|
||||||
|
|
||||||
|
tfa::write(&tfa)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -58,6 +58,21 @@ pub fn read() -> Result<TfaConfig, Error> {
|
|||||||
Ok(serde_json::from_reader(file)?)
|
Ok(serde_json::from_reader(file)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the webauthn config with a digest.
|
||||||
|
///
|
||||||
|
/// This is meant only for configuration updates, which currently only means webauthn updates.
|
||||||
|
/// Since this is meant to be done only once (since changes will lock out users), this should be
|
||||||
|
/// used rarely, since the digest calculation is currently a bit more involved.
|
||||||
|
pub fn webauthn_config() -> Result<Option<(WebauthnConfig, [u8; 32])>, Error>{
|
||||||
|
Ok(match read()?.webauthn {
|
||||||
|
Some(wa) => {
|
||||||
|
let digest = wa.digest()?;
|
||||||
|
Some((wa, digest))
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Requires the write lock to be held.
|
/// Requires the write lock to be held.
|
||||||
pub fn write(data: &TfaConfig) -> Result<(), Error> {
|
pub fn write(data: &TfaConfig) -> Result<(), Error> {
|
||||||
let options = CreateOptions::new().perm(Mode::from_bits_truncate(0o0600));
|
let options = CreateOptions::new().perm(Mode::from_bits_truncate(0o0600));
|
||||||
@ -71,7 +86,10 @@ pub struct U2fConfig {
|
|||||||
appid: String,
|
appid: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[api]
|
||||||
#[derive(Clone, Deserialize, Serialize)]
|
#[derive(Clone, Deserialize, Serialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
/// Server side webauthn server configuration.
|
||||||
pub struct WebauthnConfig {
|
pub struct WebauthnConfig {
|
||||||
/// Relying party name. Any text identifier.
|
/// Relying party name. Any text identifier.
|
||||||
///
|
///
|
||||||
@ -90,6 +108,60 @@ pub struct WebauthnConfig {
|
|||||||
id: String,
|
id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl WebauthnConfig {
|
||||||
|
pub fn digest(&self) -> Result<[u8; 32], Error> {
|
||||||
|
let digest_data = crate::tools::json::to_canonical_json(&serde_json::to_value(self)?)?;
|
||||||
|
Ok(openssl::sha::sha256(&digest_data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: api macro should be able to generate this struct & impl automatically:
|
||||||
|
#[api]
|
||||||
|
#[derive(Default, Deserialize, Serialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
/// Server side webauthn server configuration.
|
||||||
|
pub struct WebauthnConfigUpdater {
|
||||||
|
/// Relying party name. Any text identifier.
|
||||||
|
///
|
||||||
|
/// Changing this *may* break existing credentials.
|
||||||
|
rp: Option<String>,
|
||||||
|
|
||||||
|
/// Site origin. Must be a `https://` URL (or `http://localhost`). Should contain the address
|
||||||
|
/// users type in their browsers to access the web interface.
|
||||||
|
///
|
||||||
|
/// Changing this *may* break existing credentials.
|
||||||
|
origin: Option<String>,
|
||||||
|
|
||||||
|
/// Relying part ID. Must be the domain name without protocol, port or location.
|
||||||
|
///
|
||||||
|
/// Changing this *will* break existing credentials.
|
||||||
|
id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebauthnConfigUpdater {
|
||||||
|
pub fn apply_to(self, target: &mut WebauthnConfig) {
|
||||||
|
if let Some(val) = self.rp {
|
||||||
|
target.rp = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(val) = self.origin {
|
||||||
|
target.origin = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(val) = self.id {
|
||||||
|
target.id = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(self) -> Result<WebauthnConfig, Error> {
|
||||||
|
Ok(WebauthnConfig {
|
||||||
|
rp: self.rp.ok_or_else(|| format_err!("missing required field: `rp`"))?,
|
||||||
|
origin: self.origin.ok_or_else(|| format_err!("missing required field: `origin`"))?,
|
||||||
|
id: self.id.ok_or_else(|| format_err!("missing required field: `origin`"))?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// For now we just implement this on the configuration this way.
|
/// For now we just implement this on the configuration this way.
|
||||||
///
|
///
|
||||||
/// Note that we may consider changing this so `get_origin` returns the `Host:` header provided by
|
/// Note that we may consider changing this so `get_origin` returns the `Host:` header provided by
|
||||||
|
Loading…
Reference in New Issue
Block a user