* [pbs-devel] [PATCH backup 1/4] add tools::json for canonical json generation
@ 2021-01-15 9:20 Wolfgang Bumiller
2021-01-15 9:20 ` [pbs-devel] [PATCH backup 2/4] bakckup::manifest: use tools::json for canonical representation Wolfgang Bumiller
` (2 more replies)
0 siblings, 3 replies; 4+ messages in thread
From: Wolfgang Bumiller @ 2021-01-15 9:20 UTC (permalink / raw)
To: pbs-devel
moving this from backup::manifest, no functional changes
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
src/tools.rs | 1 +
src/tools/json.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 50 insertions(+)
create mode 100644 src/tools/json.rs
diff --git a/src/tools.rs b/src/tools.rs
index 599bbd04..d96cd647 100644
--- a/src/tools.rs
+++ b/src/tools.rs
@@ -28,6 +28,7 @@ pub mod format;
pub mod fs;
pub mod fuse_loop;
pub mod http;
+pub mod json;
pub mod logrotate;
pub mod loopdev;
pub mod lru_cache;
diff --git a/src/tools/json.rs b/src/tools/json.rs
new file mode 100644
index 00000000..c81afccb
--- /dev/null
+++ b/src/tools/json.rs
@@ -0,0 +1,49 @@
+use anyhow::{bail, Error};
+use serde_json::Value;
+
+// Generate canonical json
+pub fn to_canonical_json(value: &Value) -> Result<Vec<u8>, Error> {
+ let mut data = Vec::new();
+ write_canonical_json(value, &mut data)?;
+ Ok(data)
+}
+
+pub fn write_canonical_json(value: &Value, output: &mut Vec<u8>) -> Result<(), Error> {
+ match value {
+ Value::Null => bail!("got unexpected null value"),
+ Value::String(_) | Value::Number(_) | Value::Bool(_) => {
+ serde_json::to_writer(output, &value)?;
+ }
+ Value::Array(list) => {
+ output.push(b'[');
+ let mut iter = list.iter();
+ if let Some(item) = iter.next() {
+ write_canonical_json(item, output)?;
+ for item in iter {
+ output.push(b',');
+ write_canonical_json(item, output)?;
+ }
+ }
+ output.push(b']');
+ }
+ Value::Object(map) => {
+ output.push(b'{');
+ let mut keys: Vec<&str> = map.keys().map(String::as_str).collect();
+ keys.sort();
+ let mut iter = keys.into_iter();
+ if let Some(key) = iter.next() {
+ serde_json::to_writer(&mut *output, &key)?;
+ output.push(b':');
+ write_canonical_json(&map[key], output)?;
+ for key in iter {
+ output.push(b',');
+ serde_json::to_writer(&mut *output, &key)?;
+ output.push(b':');
+ write_canonical_json(&map[key], output)?;
+ }
+ }
+ output.push(b'}');
+ }
+ }
+ Ok(())
+}
--
2.20.1
^ permalink raw reply [flat|nested] 4+ messages in thread
* [pbs-devel] [PATCH backup 2/4] bakckup::manifest: use tools::json for canonical representation
2021-01-15 9:20 [pbs-devel] [PATCH backup 1/4] add tools::json for canonical json generation Wolfgang Bumiller
@ 2021-01-15 9:20 ` Wolfgang Bumiller
2021-01-15 9:20 ` [pbs-devel] [PATCH backup 3/4] tfa: add webauthn configuration API entry points Wolfgang Bumiller
2021-01-15 9:20 ` [pbs-devel] [PATCH backup 4/4] gui: tfa configuration Wolfgang Bumiller
2 siblings, 0 replies; 4+ messages in thread
From: Wolfgang Bumiller @ 2021-01-15 9:20 UTC (permalink / raw)
To: pbs-devel
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
src/backup/manifest.rs | 44 +-----------------------------------------
1 file changed, 1 insertion(+), 43 deletions(-)
diff --git a/src/backup/manifest.rs b/src/backup/manifest.rs
index 8742cb0a..d9a55655 100644
--- a/src/backup/manifest.rs
+++ b/src/backup/manifest.rs
@@ -149,49 +149,7 @@ impl BackupManifest {
// Generate canonical json
fn to_canonical_json(value: &Value) -> Result<Vec<u8>, Error> {
- let mut data = Vec::new();
- Self::write_canonical_json(value, &mut data)?;
- Ok(data)
- }
-
- fn write_canonical_json(value: &Value, output: &mut Vec<u8>) -> Result<(), Error> {
- match value {
- Value::Null => bail!("got unexpected null value"),
- Value::String(_) | Value::Number(_) | Value::Bool(_) => {
- serde_json::to_writer(output, &value)?;
- }
- Value::Array(list) => {
- output.push(b'[');
- let mut iter = list.iter();
- if let Some(item) = iter.next() {
- Self::write_canonical_json(item, output)?;
- for item in iter {
- output.push(b',');
- Self::write_canonical_json(item, output)?;
- }
- }
- output.push(b']');
- }
- Value::Object(map) => {
- output.push(b'{');
- let mut keys: Vec<&str> = map.keys().map(String::as_str).collect();
- keys.sort();
- let mut iter = keys.into_iter();
- if let Some(key) = iter.next() {
- serde_json::to_writer(&mut *output, &key)?;
- output.push(b':');
- Self::write_canonical_json(&map[key], output)?;
- for key in iter {
- output.push(b',');
- serde_json::to_writer(&mut *output, &key)?;
- output.push(b':');
- Self::write_canonical_json(&map[key], output)?;
- }
- }
- output.push(b'}');
- }
- }
- Ok(())
+ crate::tools::json::to_canonical_json(value)
}
/// Compute manifest signature
--
2.20.1
^ permalink raw reply [flat|nested] 4+ messages in thread
* [pbs-devel] [PATCH backup 3/4] tfa: add webauthn configuration API entry points
2021-01-15 9:20 [pbs-devel] [PATCH backup 1/4] add tools::json for canonical json generation Wolfgang Bumiller
2021-01-15 9:20 ` [pbs-devel] [PATCH backup 2/4] bakckup::manifest: use tools::json for canonical representation Wolfgang Bumiller
@ 2021-01-15 9:20 ` Wolfgang Bumiller
2021-01-15 9:20 ` [pbs-devel] [PATCH backup 4/4] gui: tfa configuration Wolfgang Bumiller
2 siblings, 0 replies; 4+ messages in thread
From: Wolfgang Bumiller @ 2021-01-15 9:20 UTC (permalink / raw)
To: pbs-devel
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>
---
src/api2/config.rs | 4 +-
src/api2/config/access/mod.rs | 10 ++++
src/api2/config/access/tfa/mod.rs | 84 +++++++++++++++++++++++++++++++
src/config/tfa.rs | 72 ++++++++++++++++++++++++++
4 files changed, 169 insertions(+), 1 deletion(-)
create mode 100644 src/api2/config/access/mod.rs
create mode 100644 src/api2/config/access/tfa/mod.rs
diff --git a/src/api2/config.rs b/src/api2/config.rs
index 0e269b77..7ad43f9f 100644
--- a/src/api2/config.rs
+++ b/src/api2/config.rs
@@ -1,6 +1,7 @@
use proxmox::api::router::{Router, SubdirMap};
use proxmox::list_subdirs_api_method;
+pub mod access;
pub mod datastore;
pub mod remote;
pub mod sync;
@@ -10,13 +11,14 @@ pub mod changer;
pub mod media_pool;
const SUBDIRS: SubdirMap = &[
+ ("access", &access::ROUTER),
("changer", &changer::ROUTER),
("datastore", &datastore::ROUTER),
("drive", &drive::ROUTER),
("media-pool", &media_pool::ROUTER),
("remote", &remote::ROUTER),
("sync", &sync::ROUTER),
- ("verify", &verify::ROUTER)
+ ("verify", &verify::ROUTER),
];
pub const ROUTER: Router = Router::new()
diff --git a/src/api2/config/access/mod.rs b/src/api2/config/access/mod.rs
new file mode 100644
index 00000000..659815e0
--- /dev/null
+++ b/src/api2/config/access/mod.rs
@@ -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);
diff --git a/src/api2/config/access/tfa/mod.rs b/src/api2/config/access/tfa/mod.rs
new file mode 100644
index 00000000..63c34815
--- /dev/null
+++ b/src/api2/config/access/tfa/mod.rs
@@ -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(())
+}
diff --git a/src/config/tfa.rs b/src/config/tfa.rs
index 9e9e9516..e0f2fcfe 100644
--- a/src/config/tfa.rs
+++ b/src/config/tfa.rs
@@ -58,6 +58,21 @@ pub fn read() -> Result<TfaConfig, Error> {
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.
pub fn write(data: &TfaConfig) -> Result<(), Error> {
let options = CreateOptions::new().perm(Mode::from_bits_truncate(0o0600));
@@ -71,7 +86,10 @@ pub struct U2fConfig {
appid: String,
}
+#[api]
#[derive(Clone, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+/// Server side webauthn server configuration.
pub struct WebauthnConfig {
/// Relying party name. Any text identifier.
///
@@ -90,6 +108,60 @@ pub struct WebauthnConfig {
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.
///
/// Note that we may consider changing this so `get_origin` returns the `Host:` header provided by
--
2.20.1
^ permalink raw reply [flat|nested] 4+ messages in thread
* [pbs-devel] [PATCH backup 4/4] gui: tfa configuration
2021-01-15 9:20 [pbs-devel] [PATCH backup 1/4] add tools::json for canonical json generation Wolfgang Bumiller
2021-01-15 9:20 ` [pbs-devel] [PATCH backup 2/4] bakckup::manifest: use tools::json for canonical representation Wolfgang Bumiller
2021-01-15 9:20 ` [pbs-devel] [PATCH backup 3/4] tfa: add webauthn configuration API entry points Wolfgang Bumiller
@ 2021-01-15 9:20 ` Wolfgang Bumiller
2 siblings, 0 replies; 4+ messages in thread
From: Wolfgang Bumiller @ 2021-01-15 9:20 UTC (permalink / raw)
To: pbs-devel
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
www/Makefile | 1 +
www/SystemConfiguration.js | 26 +++++++++++
www/config/WebauthnView.js | 91 ++++++++++++++++++++++++++++++++++++++
3 files changed, 118 insertions(+)
create mode 100644 www/config/WebauthnView.js
diff --git a/www/Makefile b/www/Makefile
index 3d8d4fa1..c2d80c74 100644
--- a/www/Makefile
+++ b/www/Makefile
@@ -30,6 +30,7 @@ JSSRC= \
config/ACLView.js \
config/SyncView.js \
config/VerifyView.js \
+ config/WebauthnView.js \
window/ACLEdit.js \
window/AddTfaRecovery.js \
window/AddTotp.js \
diff --git a/www/SystemConfiguration.js b/www/SystemConfiguration.js
index d82396af..025e0273 100644
--- a/www/SystemConfiguration.js
+++ b/www/SystemConfiguration.js
@@ -44,6 +44,27 @@ Ext.define('PBS.SystemConfiguration', {
},
],
},
+ {
+ title: gettext('Authentication'),
+ itemId: 'authentication',
+ xtype: 'panel',
+ layout: {
+ type: 'vbox',
+ align: 'stretch',
+ multi: true,
+ },
+ defaults: {
+ collapsible: true,
+ animCollapse: false,
+ margin: '10 10 0 10',
+ },
+ items: [
+ {
+ title: gettext('Webauthn'),
+ xtype: 'pbsWebauthnConfigView',
+ },
+ ],
+ },
],
initComponent: function() {
@@ -55,6 +76,11 @@ Ext.define('PBS.SystemConfiguration', {
Ext.Array.forEach(networktime.query(), function(item) {
item.relayEvents(networktime, ['activate', 'deactivate', 'destroy']);
});
+
+ let authentication = me.getComponent('authentication');
+ Ext.Array.forEach(authentication.query(), function(item) {
+ item.relayEvents(authentication, ['activate', 'deactivate', 'destroy']);
+ });
},
});
diff --git a/www/config/WebauthnView.js b/www/config/WebauthnView.js
new file mode 100644
index 00000000..3e8df709
--- /dev/null
+++ b/www/config/WebauthnView.js
@@ -0,0 +1,91 @@
+Ext.define('PBS.WebauthnConfigView', {
+ extend: 'Proxmox.grid.ObjectGrid',
+ alias: ['widget.pbsWebauthnConfigView'],
+
+ initComponent: function() {
+ let me = this;
+
+ let run_editor = function() {
+ let win = Ext.create('PBS.WebauthnConfigEdit');
+ win.show();
+ };
+
+ Ext.apply(me, {
+ url: "/api2/json/config/access/tfa/webauthn",
+ cwidth1: 150,
+ interval: 1000,
+ run_editor: run_editor,
+ rows: {
+ rp: {
+ header: gettext('Relying Party'),
+ required: true,
+ },
+ origin: {
+ header: gettext('Origin'),
+ required: true,
+ },
+ id: {
+ header: gettext('Id'),
+ required: true,
+ },
+ },
+ tbar: [
+ {
+ text: gettext("Edit"),
+ handler: run_editor,
+ },
+ ],
+ listeners: {
+ itemdblclick: run_editor,
+ },
+ });
+
+ me.callParent();
+
+ me.on('activate', me.rstore.startUpdate);
+ me.on('deactivate', me.rstore.stopUpdate);
+ me.on('destroy', me.rstore.stopUpdate);
+ },
+});
+
+Ext.define('PBS.WebauthnConfigEdit', {
+ extend: 'Proxmox.window.Edit',
+ alias: ['widget.pbsWebauthnConfigEdit'],
+
+ initComponent: function() {
+ let me = this;
+
+ me.items = [
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Relying Party'),
+ name: 'rp',
+ allowBlank: false,
+ },
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Origin'),
+ name: 'origin',
+ allowBlank: false,
+ },
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('id'),
+ name: 'id',
+ allowBlank: false,
+ },
+ ];
+
+ Ext.applyIf(me, {
+ subject: gettext('DNS'),
+ url: "/api2/extjs/config/access/tfa/webauthn",
+ fieldDefaults: {
+ labelWidth: 120,
+ },
+ });
+
+ me.callParent();
+
+ me.load();
+ },
+});
--
2.20.1
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2021-01-15 9:21 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2021-01-15 9:20 [pbs-devel] [PATCH backup 1/4] add tools::json for canonical json generation Wolfgang Bumiller
2021-01-15 9:20 ` [pbs-devel] [PATCH backup 2/4] bakckup::manifest: use tools::json for canonical representation Wolfgang Bumiller
2021-01-15 9:20 ` [pbs-devel] [PATCH backup 3/4] tfa: add webauthn configuration API entry points Wolfgang Bumiller
2021-01-15 9:20 ` [pbs-devel] [PATCH backup 4/4] gui: tfa configuration Wolfgang Bumiller
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox