public inbox for pbs-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pbs-devel] [PATCH proxmox-backup v2 0/9] OpenID connect realms
@ 2021-06-24 10:17 Dietmar Maurer
  2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 1/9] depend on proxmox-openid-rs Dietmar Maurer
                   ` (8 more replies)
  0 siblings, 9 replies; 10+ messages in thread
From: Dietmar Maurer @ 2021-06-24 10:17 UTC (permalink / raw)
  To: pbs-devel

This implements OpenID connect realms using the new
"proxmox-openid-rs" crate.

Changes since v1:
- really fix commit message of first patch
- change api endpoints (/access/openid/{login|auth-url})
- merged all api implementation patches

Changes since preview version (for Fabian):
- fix commit message
- reserve namen 'pam' and 'pbs'
- fix 'make deb'


Dietmar Maurer (9):
  depend on proxmox-openid-rs
  config: new domains.cfg to configure openid realm
  check_acl_path: add /access/domains and /access/openid
  add API to manage openid realms
  cli: add CLI to manage openid realms.
  implement new helper is_active_user_id()
  cleanup user/token is_active() check
  api: add openid redirect/login API
  ui: implement OpenId login

 Cargo.toml                               |   2 +
 src/api2/access.rs                       |   4 +-
 src/api2/access/domain.rs                |  18 ++
 src/api2/access/openid.rs                | 192 ++++++++++++++++
 src/api2/config/access/mod.rs            |   8 +-
 src/api2/config/access/openid.rs         | 274 +++++++++++++++++++++++
 src/bin/proxmox-backup-manager.rs        |   1 +
 src/bin/proxmox_backup_manager/mod.rs    |   2 +
 src/bin/proxmox_backup_manager/openid.rs |  99 ++++++++
 src/config.rs                            |   1 +
 src/config/acl.rs                        |   8 +-
 src/config/cached_user_info.rs           |  35 ++-
 src/config/domains.rs                    | 175 +++++++++++++++
 src/config/user.rs                       |  32 +++
 www/Application.js                       |   8 +-
 www/LoginView.js                         | 100 ++++++++-
 www/Utils.js                             |   8 +
 17 files changed, 940 insertions(+), 27 deletions(-)
 create mode 100644 src/api2/access/openid.rs
 create mode 100644 src/api2/config/access/openid.rs
 create mode 100644 src/bin/proxmox_backup_manager/openid.rs
 create mode 100644 src/config/domains.rs

-- 
2.30.2




^ permalink raw reply	[flat|nested] 10+ messages in thread

* [pbs-devel] [PATCH proxmox-backup v2 1/9] depend on proxmox-openid-rs
  2021-06-24 10:17 [pbs-devel] [PATCH proxmox-backup v2 0/9] OpenID connect realms Dietmar Maurer
@ 2021-06-24 10:17 ` Dietmar Maurer
  2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 2/9] config: new domains.cfg to configure openid realm Dietmar Maurer
                   ` (7 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Dietmar Maurer @ 2021-06-24 10:17 UTC (permalink / raw)
  To: pbs-devel

---
 Cargo.toml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/Cargo.toml b/Cargo.toml
index 976f18bc..5085e767 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -84,6 +84,8 @@ crossbeam-channel = "0.5"
 
 proxmox-acme-rs = "0.2.1"
 
+proxmox-openid = "0.5.0"
+
 [features]
 default = []
 #valgrind = ["valgrind_request"]
-- 
2.30.2




^ permalink raw reply	[flat|nested] 10+ messages in thread

* [pbs-devel] [PATCH proxmox-backup v2 2/9] config: new domains.cfg to configure openid realm
  2021-06-24 10:17 [pbs-devel] [PATCH proxmox-backup v2 0/9] OpenID connect realms Dietmar Maurer
  2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 1/9] depend on proxmox-openid-rs Dietmar Maurer
@ 2021-06-24 10:17 ` Dietmar Maurer
  2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 3/9] check_acl_path: add /access/domains and /access/openid Dietmar Maurer
                   ` (6 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Dietmar Maurer @ 2021-06-24 10:17 UTC (permalink / raw)
  To: pbs-devel

Or other realmy types...
---
 src/api2/access/domain.rs |  21 +++++++
 src/config.rs             |   1 +
 src/config/domains.rs     | 122 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 144 insertions(+)
 create mode 100644 src/config/domains.rs

diff --git a/src/api2/access/domain.rs b/src/api2/access/domain.rs
index a2fd746d..2dff9d32 100644
--- a/src/api2/access/domain.rs
+++ b/src/api2/access/domain.rs
@@ -38,10 +38,31 @@ use crate::api2::types::*;
 )]
 /// Authentication domain/realm index.
 fn list_domains() -> Result<Value, Error> {
+
     let mut list = Vec::new();
+
     list.push(json!({ "realm": "pam", "comment": "Linux PAM standard authentication", "default": true }));
     list.push(json!({ "realm": "pbs", "comment": "Proxmox Backup authentication server" }));
+
+    let (config, _digest) = crate::config::domains::config()?;
+
+    for (realm, (section_type, v)) in config.sections.iter() {
+        let mut item = json!({
+            "type": section_type,
+            "realm": realm,
+        });
+
+        if v["comment"].as_str().is_some() {
+            item["comment"] = v["comment"].clone();
+        }
+        list.push(item);
+
+    }
+
     Ok(list.into())
+
+
+
 }
 
 pub const ROUTER: Router = Router::new()
diff --git a/src/config.rs b/src/config.rs
index b9cd6281..329315ec 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -31,6 +31,7 @@ pub mod drive;
 pub mod media_pool;
 pub mod tape_encryption_keys;
 pub mod tape_job;
+pub mod domains;
 
 /// Check configuration directory permissions
 ///
diff --git a/src/config/domains.rs b/src/config/domains.rs
new file mode 100644
index 00000000..ee6fd754
--- /dev/null
+++ b/src/config/domains.rs
@@ -0,0 +1,122 @@
+use anyhow::{Error};
+use lazy_static::lazy_static;
+use std::collections::HashMap;
+use serde::{Serialize, Deserialize};
+
+use proxmox::api::{
+    api,
+    schema::*,
+    section_config::{
+        SectionConfig,
+        SectionConfigData,
+        SectionConfigPlugin,
+    }
+};
+
+use proxmox::tools::fs::{
+    open_file_locked,
+    replace_file,
+    CreateOptions,
+};
+
+use crate::api2::types::*;
+
+pub const OPENID_STATE_DIR: &str = "/var/run/proxmox-backup";
+
+lazy_static! {
+    pub static ref CONFIG: SectionConfig = init();
+}
+
+
+#[api(
+    properties: {
+        realm: {
+            schema: REALM_ID_SCHEMA,
+        },
+        "issuer-url": {
+            description: "OpenID Issuer Url",
+            type: String,
+        },
+        "client-id": {
+            description: "OpenID Client ID",
+            type: String,
+        },
+        "client-key": {
+            description: "OpenID Client Key",
+            type: String,
+            optional: true,
+        },
+        comment: {
+            optional: true,
+            schema: SINGLE_LINE_COMMENT_SCHEMA,
+        },
+    },
+)]
+#[derive(Serialize,Deserialize)]
+#[serde(rename_all="kebab-case")]
+/// OpenID configuration properties.
+pub struct OpenIdRealmConfig {
+    pub realm: String,
+    pub issuer_url: String,
+    pub client_id: String,
+    #[serde(skip_serializing_if="Option::is_none")]
+    pub client_key: Option<String>,
+    #[serde(skip_serializing_if="Option::is_none")]
+    pub comment: Option<String>,
+}
+
+fn init() -> SectionConfig {
+    let obj_schema = match OpenIdRealmConfig::API_SCHEMA {
+        Schema::Object(ref obj_schema) => obj_schema,
+        _ => unreachable!(),
+    };
+
+    let plugin = SectionConfigPlugin::new("openid".to_string(), Some(String::from("realm")), obj_schema);
+    let mut config = SectionConfig::new(&REALM_ID_SCHEMA);
+    config.register_plugin(plugin);
+
+    config
+}
+
+pub const DOMAINS_CFG_FILENAME: &str = "/etc/proxmox-backup/domains.cfg";
+pub const DOMAINS_CFG_LOCKFILE: &str = "/etc/proxmox-backup/.domains.lck";
+
+/// Get exclusive lock
+pub fn lock_config() -> Result<std::fs::File, Error> {
+    open_file_locked(DOMAINS_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)
+}
+
+pub fn config() -> Result<(SectionConfigData, [u8;32]), Error> {
+
+    let content = proxmox::tools::fs::file_read_optional_string(DOMAINS_CFG_FILENAME)?
+        .unwrap_or_else(|| "".to_string());
+
+    let digest = openssl::sha::sha256(content.as_bytes());
+    let data = CONFIG.parse(DOMAINS_CFG_FILENAME, &content)?;
+    Ok((data, digest))
+}
+
+pub fn save_config(config: &SectionConfigData) -> Result<(), Error> {
+    let raw = CONFIG.write(DOMAINS_CFG_FILENAME, &config)?;
+
+    let backup_user = crate::backup::backup_user()?;
+    let mode = nix::sys::stat::Mode::from_bits_truncate(0o0640);
+    // set the correct owner/group/permissions while saving file
+    // owner(rw) = root, group(r)= backup
+    let options = CreateOptions::new()
+        .perm(mode)
+        .owner(nix::unistd::ROOT)
+        .group(backup_user.gid);
+
+    replace_file(DOMAINS_CFG_FILENAME, raw.as_bytes(), options)?;
+
+    Ok(())
+}
+
+// shell completion helper
+pub fn complete_realm_name(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
+    match config() {
+        Ok((data, _digest)) => data.sections.iter().map(|(id, _)| id.to_string()).collect(),
+        Err(_) => return vec![],
+    }
+}
-- 
2.30.2




^ permalink raw reply	[flat|nested] 10+ messages in thread

* [pbs-devel] [PATCH proxmox-backup v2 3/9] check_acl_path: add /access/domains and /access/openid
  2021-06-24 10:17 [pbs-devel] [PATCH proxmox-backup v2 0/9] OpenID connect realms Dietmar Maurer
  2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 1/9] depend on proxmox-openid-rs Dietmar Maurer
  2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 2/9] config: new domains.cfg to configure openid realm Dietmar Maurer
@ 2021-06-24 10:17 ` Dietmar Maurer
  2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 4/9] add API to manage openid realms Dietmar Maurer
                   ` (5 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Dietmar Maurer @ 2021-06-24 10:17 UTC (permalink / raw)
  To: pbs-devel

---
 src/config/acl.rs | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/config/acl.rs b/src/config/acl.rs
index 04d42854..e468586e 100644
--- a/src/config/acl.rs
+++ b/src/config/acl.rs
@@ -283,11 +283,17 @@ pub fn check_acl_path(path: &str) -> Result<(), Error> {
                 return Ok(());
             }
             match components[1] {
-                "acl" | "users" => {
+                "acl" | "users" | "domains" => {
                     if components_len == 2 {
                         return Ok(());
                     }
                 }
+                // /access/openid/{endpoint}
+                "openid" => {
+                    if components_len <= 3 {
+                        return Ok(());
+                    }
+                }
                 _ => {}
             }
         }
-- 
2.30.2




^ permalink raw reply	[flat|nested] 10+ messages in thread

* [pbs-devel] [PATCH proxmox-backup v2 4/9] add API to manage openid realms
  2021-06-24 10:17 [pbs-devel] [PATCH proxmox-backup v2 0/9] OpenID connect realms Dietmar Maurer
                   ` (2 preceding siblings ...)
  2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 3/9] check_acl_path: add /access/domains and /access/openid Dietmar Maurer
@ 2021-06-24 10:17 ` Dietmar Maurer
  2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 5/9] cli: add CLI " Dietmar Maurer
                   ` (4 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Dietmar Maurer @ 2021-06-24 10:17 UTC (permalink / raw)
  To: pbs-devel

---
 src/api2/config/access/mod.rs    |   8 +-
 src/api2/config/access/openid.rs | 264 +++++++++++++++++++++++++++++++
 2 files changed, 271 insertions(+), 1 deletion(-)
 create mode 100644 src/api2/config/access/openid.rs

diff --git a/src/api2/config/access/mod.rs b/src/api2/config/access/mod.rs
index 659815e0..6e7d98be 100644
--- a/src/api2/config/access/mod.rs
+++ b/src/api2/config/access/mod.rs
@@ -1,9 +1,15 @@
 use proxmox::api::{Router, SubdirMap};
 use proxmox::list_subdirs_api_method;
+use proxmox::{identity, sortable};
 
 pub mod tfa;
+pub mod openid;
 
-const SUBDIRS: SubdirMap = &[("tfa", &tfa::ROUTER)];
+#[sortable]
+const SUBDIRS: SubdirMap = &sorted!([
+    ("openid", &openid::ROUTER),
+    ("tfa", &tfa::ROUTER),
+]);
 
 pub const ROUTER: Router = Router::new()
     .get(&list_subdirs_api_method!(SUBDIRS))
diff --git a/src/api2/config/access/openid.rs b/src/api2/config/access/openid.rs
new file mode 100644
index 00000000..15fddaf0
--- /dev/null
+++ b/src/api2/config/access/openid.rs
@@ -0,0 +1,264 @@
+/// Configure OpenId realms
+
+use anyhow::{bail, Error};
+use serde_json::Value;
+use ::serde::{Deserialize, Serialize};
+
+use proxmox::api::{api, Permission, Router, RpcEnvironment};
+
+use crate::config::domains::{self, OpenIdRealmConfig};
+use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_PERMISSIONS_MODIFY};
+use crate::api2::types::*;
+
+#[api(
+    input: {
+        properties: {},
+    },
+    returns: {
+        description: "List of configured OpenId realms.",
+        type: Array,
+        items: { type: OpenIdRealmConfig },
+    },
+    access: {
+        permission: &Permission::Privilege(&["access", "domains"], PRIV_SYS_AUDIT, false),
+    },
+)]
+/// List configured OpenId realms
+pub fn list_openid_realms(
+    _param: Value,
+    mut rpcenv: &mut dyn RpcEnvironment,
+) -> Result<Vec<OpenIdRealmConfig>, Error> {
+
+    let (config, digest) = domains::config()?;
+
+    let list = config.convert_to_typed_array("openid")?;
+
+    rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
+
+    Ok(list)
+}
+
+#[api(
+    protected: true,
+    input: {
+        properties: {
+            config: {
+                type: OpenIdRealmConfig,
+                flatten: true,
+            },
+        },
+    },
+    access: {
+        permission: &Permission::Privilege(&["access", "domains"], PRIV_PERMISSIONS_MODIFY, false),
+    },
+)]
+/// Create a new OpenId realm
+pub fn create_openid_realm(config: OpenIdRealmConfig) -> Result<(), Error> {
+
+    let _lock = domains::lock_config()?;
+
+    let (mut domains, _digest) = domains::config()?;
+
+    if config.realm == "pbs" ||
+        config.realm == "pam" ||
+        domains.sections.get(&config.realm).is_some()
+    {
+        bail!("realm '{}' already exists.", config.realm);
+    }
+
+    domains.set_data(&config.realm, "openid", &config)?;
+
+    domains::save_config(&domains)?;
+
+    Ok(())
+}
+
+#[api(
+    protected: true,
+    input: {
+        properties: {
+            realm: {
+                schema: REALM_ID_SCHEMA,
+            },
+            digest: {
+                optional: true,
+                schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
+            },
+        },
+    },
+    access: {
+        permission: &Permission::Privilege(&["access", "domains"], PRIV_PERMISSIONS_MODIFY, false),
+    },
+)]
+/// Remove a OpenID realm configuration
+pub fn delete_openid_realm(
+    realm: String,
+    digest: Option<String>,
+    _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<(), Error> {
+
+    let _lock = domains::lock_config()?;
+
+    let (mut domains, expected_digest) = domains::config()?;
+
+    if let Some(ref digest) = digest {
+        let digest = proxmox::tools::hex_to_digest(digest)?;
+        crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
+    }
+
+    if domains.sections.remove(&realm).is_none()  {
+        bail!("realm '{}' does not exist.", realm);
+    }
+
+    domains::save_config(&domains)?;
+
+    Ok(())
+}
+
+#[api(
+    input: {
+        properties: {
+            realm: {
+                schema: REALM_ID_SCHEMA,
+            },
+        },
+    },
+    returns:  { type: OpenIdRealmConfig },
+    access: {
+        permission: &Permission::Privilege(&["access", "domains"], PRIV_SYS_AUDIT, false),
+    },
+)]
+/// Read the OpenID realm configuration
+pub fn read_openid_realm(
+    realm: String,
+    mut rpcenv: &mut dyn RpcEnvironment,
+) -> Result<OpenIdRealmConfig, Error> {
+
+    let (domains, digest) = domains::config()?;
+
+    let config = domains.lookup("openid", &realm)?;
+
+    rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
+
+    Ok(config)
+}
+
+#[api()]
+#[derive(Serialize, Deserialize)]
+#[serde(rename_all="kebab-case")]
+#[allow(non_camel_case_types)]
+/// Deletable property name
+pub enum DeletableProperty {
+    /// Delete the client key.
+    client_key,
+    /// Delete the comment property.
+    comment,
+}
+
+#[api(
+    protected: true,
+    input: {
+        properties: {
+            realm: {
+                schema: REALM_ID_SCHEMA,
+            },
+            "issuer-url": {
+                description: "OpenID Issuer Url",
+                type: String,
+                optional: true,
+            },
+            "client-id": {
+                description: "OpenID Client ID",
+                type: String,
+                optional: true,
+            },
+            "client-key": {
+                description: "OpenID Client Key",
+                type: String,
+                optional: true,
+            },
+            comment: {
+                schema: SINGLE_LINE_COMMENT_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,
+            },
+        },
+    },
+    returns:  { type: OpenIdRealmConfig },
+    access: {
+        permission: &Permission::Privilege(&["access", "domains"], PRIV_PERMISSIONS_MODIFY, false),
+    },
+)]
+/// Update an OpenID realm configuration
+pub fn update_openid_realm(
+    realm: String,
+    issuer_url: Option<String>,
+    client_id: Option<String>,
+    client_key: Option<String>,
+    comment: Option<String>,
+    delete: Option<Vec<DeletableProperty>>,
+    digest: Option<String>,
+    _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<(), Error> {
+
+    let _lock = domains::lock_config()?;
+
+    let (mut domains, expected_digest) = domains::config()?;
+
+    if let Some(ref digest) = digest {
+        let digest = proxmox::tools::hex_to_digest(digest)?;
+        crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
+    }
+
+    let mut config: OpenIdRealmConfig = domains.lookup("openid", &realm)?;
+
+    if let Some(delete) = delete {
+        for delete_prop in delete {
+            match delete_prop {
+                DeletableProperty::client_key => { config.client_key = None; },
+                DeletableProperty::comment => { config.comment = None; },
+            }
+        }
+    }
+
+    if let Some(comment) = comment {
+        let comment = comment.trim().to_string();
+        if comment.is_empty() {
+            config.comment = None;
+        } else {
+            config.comment = Some(comment);
+        }
+    }
+
+    if let Some(issuer_url) = issuer_url { config.issuer_url = issuer_url; }
+    if let Some(client_id) = client_id { config.client_id = client_id; }
+
+    if client_key.is_some() { config.client_key = client_key; }
+
+    domains.set_data(&realm, "openid", &config)?;
+
+    domains::save_config(&domains)?;
+
+    Ok(())
+}
+
+const ITEM_ROUTER: Router = Router::new()
+    .get(&API_METHOD_READ_OPENID_REALM)
+    .put(&API_METHOD_UPDATE_OPENID_REALM)
+    .delete(&API_METHOD_DELETE_OPENID_REALM);
+
+pub const ROUTER: Router = Router::new()
+    .get(&API_METHOD_LIST_OPENID_REALMS)
+    .post(&API_METHOD_CREATE_OPENID_REALM)
+    .match_all("id", &ITEM_ROUTER);
-- 
2.30.2




^ permalink raw reply	[flat|nested] 10+ messages in thread

* [pbs-devel] [PATCH proxmox-backup v2 5/9] cli: add CLI to manage openid realms.
  2021-06-24 10:17 [pbs-devel] [PATCH proxmox-backup v2 0/9] OpenID connect realms Dietmar Maurer
                   ` (3 preceding siblings ...)
  2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 4/9] add API to manage openid realms Dietmar Maurer
@ 2021-06-24 10:17 ` Dietmar Maurer
  2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 6/9] implement new helper is_active_user_id() Dietmar Maurer
                   ` (3 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Dietmar Maurer @ 2021-06-24 10:17 UTC (permalink / raw)
  To: pbs-devel

---
 src/bin/proxmox-backup-manager.rs        |  1 +
 src/bin/proxmox_backup_manager/mod.rs    |  2 +
 src/bin/proxmox_backup_manager/openid.rs | 99 ++++++++++++++++++++++++
 src/config/domains.rs                    |  9 +++
 4 files changed, 111 insertions(+)
 create mode 100644 src/bin/proxmox_backup_manager/openid.rs

diff --git a/src/bin/proxmox-backup-manager.rs b/src/bin/proxmox-backup-manager.rs
index c3806a31..461d45bd 100644
--- a/src/bin/proxmox-backup-manager.rs
+++ b/src/bin/proxmox-backup-manager.rs
@@ -354,6 +354,7 @@ fn main() {
         .insert("network", network_commands())
         .insert("node", node_commands())
         .insert("user", user_commands())
+        .insert("openid", openid_commands())
         .insert("remote", remote_commands())
         .insert("garbage-collection", garbage_collection_commands())
         .insert("acme", acme_mgmt_cli())
diff --git a/src/bin/proxmox_backup_manager/mod.rs b/src/bin/proxmox_backup_manager/mod.rs
index 21004bbe..a3a16246 100644
--- a/src/bin/proxmox_backup_manager/mod.rs
+++ b/src/bin/proxmox_backup_manager/mod.rs
@@ -24,3 +24,5 @@ mod disk;
 pub use disk::*;
 mod node;
 pub use node::*;
+mod openid;
+pub use openid::*;
diff --git a/src/bin/proxmox_backup_manager/openid.rs b/src/bin/proxmox_backup_manager/openid.rs
new file mode 100644
index 00000000..13915339
--- /dev/null
+++ b/src/bin/proxmox_backup_manager/openid.rs
@@ -0,0 +1,99 @@
+use anyhow::Error;
+use serde_json::Value;
+
+use proxmox::api::{api, cli::*, RpcEnvironment, ApiHandler};
+
+use proxmox_backup::{config, api2, api2::types::REALM_ID_SCHEMA};
+
+
+#[api(
+    input: {
+        properties: {
+            "output-format": {
+                schema: OUTPUT_FORMAT,
+                optional: true,
+            },
+        }
+    }
+)]
+/// List configured OpenId realms
+fn list_openid_realms(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> {
+
+    let output_format = get_output_format(&param);
+
+    let info = &api2::config::access::openid::API_METHOD_LIST_OPENID_REALMS;
+    let mut data = match info.handler {
+        ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
+        _ => unreachable!(),
+    };
+
+    let options = default_table_format_options()
+        .column(ColumnConfig::new("realm"))
+        .column(ColumnConfig::new("issuer-url"))
+        .column(ColumnConfig::new("comment"));
+
+    format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
+
+    Ok(Value::Null)
+}
+#[api(
+    input: {
+        properties: {
+            realm: {
+                schema: REALM_ID_SCHEMA,
+            },
+            "output-format": {
+                schema: OUTPUT_FORMAT,
+                optional: true,
+            },
+        }
+    }
+)]
+
+/// Show OpenID realm configuration
+fn show_openid_realm(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> {
+
+    let output_format = get_output_format(&param);
+
+    let info = &api2::config::access::openid::API_METHOD_READ_OPENID_REALM;
+    let mut data = match info.handler {
+        ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
+        _ => unreachable!(),
+    };
+
+    let options = default_table_format_options();
+    format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
+
+    Ok(Value::Null)
+}
+
+pub fn openid_commands() -> CommandLineInterface {
+
+    let cmd_def = CliCommandMap::new()
+        .insert("list", CliCommand::new(&&API_METHOD_LIST_OPENID_REALMS))
+        .insert("show", CliCommand::new(&&API_METHOD_SHOW_OPENID_REALM)
+                .arg_param(&["realm"])
+                .completion_cb("realm", config::domains::complete_openid_realm_name)
+        )
+        .insert("create",
+                CliCommand::new(&api2::config::access::openid::API_METHOD_CREATE_OPENID_REALM)
+                .arg_param(&["realm"])
+                .arg_param(&["realm"])
+                .completion_cb("realm", config::domains::complete_openid_realm_name)
+        )
+        .insert("update",
+                CliCommand::new(&api2::config::access::openid::API_METHOD_UPDATE_OPENID_REALM)
+                .arg_param(&["realm"])
+                .arg_param(&["realm"])
+                .completion_cb("realm", config::domains::complete_openid_realm_name)
+        )
+        .insert("delete",
+                CliCommand::new(&api2::config::access::openid::API_METHOD_DELETE_OPENID_REALM)
+                .arg_param(&["realm"])
+                .arg_param(&["realm"])
+                .completion_cb("realm", config::domains::complete_openid_realm_name)
+        )
+        ;
+
+       cmd_def.into()
+}
diff --git a/src/config/domains.rs b/src/config/domains.rs
index ee6fd754..ce3f6f23 100644
--- a/src/config/domains.rs
+++ b/src/config/domains.rs
@@ -120,3 +120,12 @@ pub fn complete_realm_name(_arg: &str, _param: &HashMap<String, String>) -> Vec<
         Err(_) => return vec![],
     }
 }
+
+pub fn complete_openid_realm_name(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
+    match config() {
+        Ok((data, _digest)) => data.sections.iter()
+            .filter_map(|(id, (t, _))| if t == "openid" { Some(id.to_string()) } else { None })
+            .collect(),
+        Err(_) => return vec![],
+    }
+}
-- 
2.30.2




^ permalink raw reply	[flat|nested] 10+ messages in thread

* [pbs-devel] [PATCH proxmox-backup v2 6/9] implement new helper is_active_user_id()
  2021-06-24 10:17 [pbs-devel] [PATCH proxmox-backup v2 0/9] OpenID connect realms Dietmar Maurer
                   ` (4 preceding siblings ...)
  2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 5/9] cli: add CLI " Dietmar Maurer
@ 2021-06-24 10:17 ` Dietmar Maurer
  2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 7/9] cleanup user/token is_active() check Dietmar Maurer
                   ` (2 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Dietmar Maurer @ 2021-06-24 10:17 UTC (permalink / raw)
  To: pbs-devel

---
 src/config/cached_user_info.rs | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/src/config/cached_user_info.rs b/src/config/cached_user_info.rs
index c85d643c..a574043f 100644
--- a/src/config/cached_user_info.rs
+++ b/src/config/cached_user_info.rs
@@ -65,10 +65,8 @@ impl CachedUserInfo {
         }
     }
 
-    /// Test if a authentication id is enabled and not expired
-    pub fn is_active_auth_id(&self, auth_id: &Authid) -> bool {
-        let userid = auth_id.user();
-
+    /// Test if a user_id is enabled and not expired
+    pub fn is_active_user_id(&self, userid: &Userid) -> bool {
         if let Ok(info) = self.user_cfg.lookup::<User>("user", userid.as_str()) {
             if !info.enable.unwrap_or(true) {
                 return false;
@@ -78,7 +76,17 @@ impl CachedUserInfo {
                     return false;
                 }
             }
+            true
         } else {
+            false
+        }
+    }
+
+    /// Test if a authentication id is enabled and not expired
+    pub fn is_active_auth_id(&self, auth_id: &Authid) -> bool {
+        let userid = auth_id.user();
+
+        if !self.is_active_user_id(userid) {
             return false;
         }
 
-- 
2.30.2




^ permalink raw reply	[flat|nested] 10+ messages in thread

* [pbs-devel] [PATCH proxmox-backup v2 7/9] cleanup user/token is_active() check
  2021-06-24 10:17 [pbs-devel] [PATCH proxmox-backup v2 0/9] OpenID connect realms Dietmar Maurer
                   ` (5 preceding siblings ...)
  2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 6/9] implement new helper is_active_user_id() Dietmar Maurer
@ 2021-06-24 10:17 ` Dietmar Maurer
  2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 8/9] api: add openid redirect/login API Dietmar Maurer
  2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 9/9] ui: implement OpenId login Dietmar Maurer
  8 siblings, 0 replies; 10+ messages in thread
From: Dietmar Maurer @ 2021-06-24 10:17 UTC (permalink / raw)
  To: pbs-devel

---
 src/config/cached_user_info.rs | 25 ++++---------------------
 src/config/user.rs             | 32 ++++++++++++++++++++++++++++++++
 2 files changed, 36 insertions(+), 21 deletions(-)

diff --git a/src/config/cached_user_info.rs b/src/config/cached_user_info.rs
index a574043f..6cb64162 100644
--- a/src/config/cached_user_info.rs
+++ b/src/config/cached_user_info.rs
@@ -7,6 +7,7 @@ use anyhow::{Error, bail};
 use proxmox::api::section_config::SectionConfigData;
 use lazy_static::lazy_static;
 use proxmox::api::UserInformation;
+use proxmox::tools::time::epoch_i64;
 
 use super::acl::{AclTree, ROLE_NAMES, ROLE_ADMIN};
 use super::user::{ApiToken, User};
@@ -18,8 +19,6 @@ pub struct CachedUserInfo {
     acl_tree: Arc<AclTree>,
 }
 
-fn now() -> i64 { unsafe { libc::time(std::ptr::null_mut()) } }
-
 struct ConfigCache {
     data: Option<Arc<CachedUserInfo>>,
     last_update: i64,
@@ -35,7 +34,7 @@ impl CachedUserInfo {
 
     /// Returns a cached instance (up to 5 seconds old).
     pub fn new() -> Result<Arc<Self>, Error> {
-        let now = now();
+        let now = epoch_i64();
         { // limit scope
             let cache = CACHED_CONFIG.read().unwrap();
             if (now - cache.last_update) < 5 {
@@ -68,15 +67,7 @@ impl CachedUserInfo {
     /// Test if a user_id is enabled and not expired
     pub fn is_active_user_id(&self, userid: &Userid) -> bool {
         if let Ok(info) = self.user_cfg.lookup::<User>("user", userid.as_str()) {
-            if !info.enable.unwrap_or(true) {
-                return false;
-            }
-            if let Some(expire) = info.expire {
-                if expire > 0 && expire <= now() {
-                    return false;
-                }
-            }
-            true
+            info.is_active()
         } else {
             false
         }
@@ -92,15 +83,7 @@ impl CachedUserInfo {
 
         if auth_id.is_token() {
             if let Ok(info) = self.user_cfg.lookup::<ApiToken>("token", &auth_id.to_string()) {
-                if !info.enable.unwrap_or(true) {
-                    return false;
-                }
-                if let Some(expire) = info.expire {
-                    if expire > 0 && expire <= now() {
-                        return false;
-                    }
-                }
-                return true;
+                return info.is_active();
             } else {
                 return false;
             }
diff --git a/src/config/user.rs b/src/config/user.rs
index ff7e54e4..28e81876 100644
--- a/src/config/user.rs
+++ b/src/config/user.rs
@@ -83,6 +83,22 @@ pub struct ApiToken {
     pub expire: Option<i64>,
 }
 
+impl ApiToken {
+
+    pub fn is_active(&self) -> bool {
+        if !self.enable.unwrap_or(true) {
+            return false;
+        }
+        if let Some(expire) = self.expire {
+            let now =  proxmox::tools::time::epoch_i64();
+            if expire > 0 && expire <= now {
+                return false;
+            }
+        }
+        true
+    }
+}
+
 #[api(
     properties: {
         userid: {
@@ -132,6 +148,22 @@ pub struct User {
     pub email: Option<String>,
 }
 
+impl User {
+
+    pub fn is_active(&self) -> bool {
+        if !self.enable.unwrap_or(true) {
+            return false;
+        }
+        if let Some(expire) = self.expire {
+            let now =  proxmox::tools::time::epoch_i64();
+            if expire > 0 && expire <= now {
+                return false;
+            }
+        }
+        true
+    }
+}
+
 fn init() -> SectionConfig {
     let mut config = SectionConfig::new(&Authid::API_SCHEMA);
 
-- 
2.30.2




^ permalink raw reply	[flat|nested] 10+ messages in thread

* [pbs-devel] [PATCH proxmox-backup v2 8/9] api: add openid redirect/login API
  2021-06-24 10:17 [pbs-devel] [PATCH proxmox-backup v2 0/9] OpenID connect realms Dietmar Maurer
                   ` (6 preceding siblings ...)
  2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 7/9] cleanup user/token is_active() check Dietmar Maurer
@ 2021-06-24 10:17 ` Dietmar Maurer
  2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 9/9] ui: implement OpenId login Dietmar Maurer
  8 siblings, 0 replies; 10+ messages in thread
From: Dietmar Maurer @ 2021-06-24 10:17 UTC (permalink / raw)
  To: pbs-devel

---
 src/api2/access.rs               |   4 +-
 src/api2/access/domain.rs        |   3 -
 src/api2/access/openid.rs        | 192 +++++++++++++++++++++++++++++++
 src/api2/config/access/openid.rs |  10 ++
 src/config/domains.rs            |  44 +++++++
 5 files changed, 249 insertions(+), 4 deletions(-)
 create mode 100644 src/api2/access/openid.rs

diff --git a/src/api2/access.rs b/src/api2/access.rs
index 46725c97..e5430f62 100644
--- a/src/api2/access.rs
+++ b/src/api2/access.rs
@@ -26,6 +26,7 @@ pub mod domain;
 pub mod role;
 pub mod tfa;
 pub mod user;
+pub mod openid;
 
 #[allow(clippy::large_enum_variant)]
 enum AuthResult {
@@ -335,7 +336,7 @@ pub fn list_permissions(
     let auth_id = match auth_id {
         Some(auth_id) if auth_id == current_auth_id => current_auth_id,
         Some(auth_id) => {
-            if user_privs & PRIV_SYS_AUDIT != 0 
+            if user_privs & PRIV_SYS_AUDIT != 0
                 || (auth_id.is_token()
                     && !current_auth_id.is_token()
                     && auth_id.user() == current_auth_id.user())
@@ -423,6 +424,7 @@ const SUBDIRS: SubdirMap = &sorted!([
         &Router::new().get(&API_METHOD_LIST_PERMISSIONS)
     ),
     ("ticket", &Router::new().post(&API_METHOD_CREATE_TICKET)),
+    ("openid", &openid::ROUTER),
     ("domains", &domain::ROUTER),
     ("roles", &role::ROUTER),
     ("users", &user::ROUTER),
diff --git a/src/api2/access/domain.rs b/src/api2/access/domain.rs
index 2dff9d32..69809acc 100644
--- a/src/api2/access/domain.rs
+++ b/src/api2/access/domain.rs
@@ -60,9 +60,6 @@ fn list_domains() -> Result<Value, Error> {
     }
 
     Ok(list.into())
-
-
-
 }
 
 pub const ROUTER: Router = Router::new()
diff --git a/src/api2/access/openid.rs b/src/api2/access/openid.rs
new file mode 100644
index 00000000..cfe1a586
--- /dev/null
+++ b/src/api2/access/openid.rs
@@ -0,0 +1,192 @@
+//! OpenID redirect/login API
+use std::convert::TryFrom;
+
+use anyhow::{bail, Error};
+
+use serde_json::{json, Value};
+
+use proxmox::api::router::{Router, SubdirMap};
+use proxmox::api::{api, Permission, RpcEnvironment};
+use proxmox::{list_subdirs_api_method};
+use proxmox::{identity, sortable};
+use proxmox::tools::fs::open_file_locked;
+
+use proxmox_openid::OpenIdAuthenticator;
+
+use crate::server::ticket::ApiTicket;
+use crate::tools::ticket::Ticket;
+
+use crate::config::domains::{OPENID_STATE_DIR, OpenIdUserAttribute, OpenIdRealmConfig};
+use crate::config::cached_user_info::CachedUserInfo;
+
+use crate::api2::types::*;
+use crate::auth_helpers::*;
+
+#[api(
+    input: {
+        properties: {
+            state: {
+                description: "OpenId state.",
+                type: String,
+            },
+            code: {
+                description: "OpenId authorization code.",
+                type: String,
+            },
+            "redirect-url": {
+                description: "Redirection Url. The client should set this to used server url.",
+                type: String,
+            },
+        },
+    },
+    returns: {
+        properties: {
+            username: {
+                type: String,
+                description: "User name.",
+            },
+            ticket: {
+                type: String,
+                description: "Auth ticket.",
+            },
+            CSRFPreventionToken: {
+                type: String,
+                description: "Cross Site Request Forgery Prevention Token.",
+            },
+        },
+    },
+    protected: true,
+    access: {
+        permission: &Permission::World,
+    },
+)]
+/// Verify OpenID authorization code and create a ticket
+///
+/// Returns: An authentication ticket with additional infos.
+pub fn openid_login(
+    state: String,
+    code: String,
+    redirect_url: String,
+    _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<Value, Error> {
+    let user_info = CachedUserInfo::new()?;
+
+    let (realm, private_auth_state) =
+        OpenIdAuthenticator::verify_public_auth_state(OPENID_STATE_DIR, &state)?;
+
+    let (domains, _digest) = crate::config::domains::config()?;
+    let config: OpenIdRealmConfig = domains.lookup("openid", &realm)?;
+
+    let open_id = config.authenticator(&redirect_url)?;
+
+    let info = open_id.verify_authorization_code(&code, &private_auth_state)?;
+
+    // eprintln!("VERIFIED {} {:?} {:?}", info.subject().as_str(), info.name(), info.email());
+
+    let unique_name = match config.user_attr {
+        None | Some(OpenIdUserAttribute::Subject) => info.subject().as_str(),
+        Some(OpenIdUserAttribute::Username) => {
+            match info.preferred_username() {
+                Some(name) => name.as_str(),
+                None => bail!("missing claim 'preferred_name'"),
+            }
+        }
+        Some(OpenIdUserAttribute::Email) => {
+            match info.email() {
+                Some(name) => name.as_str(),
+                None => bail!("missing claim 'email'"),
+            }
+        }
+    };
+
+    let user_id = Userid::try_from(format!("{}@{}", unique_name, realm))?;
+
+    if !user_info.is_active_user_id(&user_id) {
+        if config.autocreate.unwrap_or(false) {
+            use crate::config::user;
+            let _lock = open_file_locked(user::USER_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
+            let user = user::User {
+                userid: user_id.clone(),
+                comment: None,
+                enable: None,
+                expire: None,
+                firstname: info.given_name().and_then(|n| n.get(None)).map(|n| n.to_string()),
+                lastname: info.family_name().and_then(|n| n.get(None)).map(|n| n.to_string()),
+                email: info.email().map(|e| e.to_string()),
+            };
+            let (mut config, _digest) = user::config()?;
+            if config.sections.get(user.userid.as_str()).is_some() {
+                bail!("autocreate user failed - '{}' already exists.", user.userid);
+            }
+            config.set_data(user.userid.as_str(), "user", &user)?;
+            user::save_config(&config)?;
+            // fixme: replace sleep with shared memory change notification
+            std::thread::sleep(std::time::Duration::new(6, 0));
+        } else {
+            bail!("user account '{}' missing, disabled or expired.", user_id);
+        }
+    }
+
+    let api_ticket = ApiTicket::full(user_id.clone());
+    let ticket = Ticket::new("PBS", &api_ticket)?.sign(private_auth_key(), None)?;
+    let token = assemble_csrf_prevention_token(csrf_secret(), &user_id);
+
+    crate::server::rest::auth_logger()?
+        .log(format!("successful auth for user '{}'", user_id));
+
+    Ok(json!({
+        "username": user_id,
+        "ticket": ticket,
+        "CSRFPreventionToken": token,
+    }))
+}
+
+#[api(
+    protected: true,
+    input: {
+        properties: {
+            realm: {
+                schema: REALM_ID_SCHEMA,
+            },
+            "redirect-url": {
+                description: "Redirection Url. The client should set this to used server url.",
+                type: String,
+            },
+        },
+    },
+    returns: {
+        description: "Redirection URL.",
+        type: String,
+    },
+    access: {
+        description: "Anyone can access this (before the user is authenticated).",
+        permission: &Permission::World,
+    },
+)]
+/// Create OpenID Redirect Session
+fn openid_auth_url(
+    realm: String,
+    redirect_url: String,
+    _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<String, Error> {
+
+    let (domains, _digest) = crate::config::domains::config()?;
+    let config: OpenIdRealmConfig = domains.lookup("openid", &realm)?;
+
+    let open_id = config.authenticator(&redirect_url)?;
+
+    let url = open_id.authorize_url(OPENID_STATE_DIR, &realm)?
+        .to_string();
+
+    Ok(url.into())
+}
+
+#[sortable]
+const SUBDIRS: SubdirMap = &sorted!([
+    ("login", &Router::new().post(&API_METHOD_OPENID_LOGIN)),
+    ("auth-url", &Router::new().post(&API_METHOD_OPENID_AUTH_URL)),
+]);
+
+pub const ROUTER: Router = Router::new()
+    .get(&list_subdirs_api_method!(SUBDIRS))
+    .subdirs(SUBDIRS);
diff --git a/src/api2/config/access/openid.rs b/src/api2/config/access/openid.rs
index 15fddaf0..9325de94 100644
--- a/src/api2/config/access/openid.rs
+++ b/src/api2/config/access/openid.rs
@@ -153,6 +153,8 @@ pub enum DeletableProperty {
     client_key,
     /// Delete the comment property.
     comment,
+    /// Delete the autocreate property
+    autocreate,
 }
 
 #[api(
@@ -177,6 +179,11 @@ pub enum DeletableProperty {
                 type: String,
                 optional: true,
             },
+            autocreate: {
+                description: "Automatically create users if they do not exist.",
+                optional: true,
+                type: bool,
+            },
             comment: {
                 schema: SINGLE_LINE_COMMENT_SCHEMA,
                 optional: true,
@@ -206,6 +213,7 @@ pub fn update_openid_realm(
     issuer_url: Option<String>,
     client_id: Option<String>,
     client_key: Option<String>,
+    autocreate: Option<bool>,
     comment: Option<String>,
     delete: Option<Vec<DeletableProperty>>,
     digest: Option<String>,
@@ -228,6 +236,7 @@ pub fn update_openid_realm(
             match delete_prop {
                 DeletableProperty::client_key => { config.client_key = None; },
                 DeletableProperty::comment => { config.comment = None; },
+                DeletableProperty::autocreate => { config.autocreate = None; },
             }
         }
     }
@@ -245,6 +254,7 @@ pub fn update_openid_realm(
     if let Some(client_id) = client_id { config.client_id = client_id; }
 
     if client_key.is_some() { config.client_key = client_key; }
+    if autocreate.is_some() { config.autocreate = autocreate; }
 
     domains.set_data(&realm, "openid", &config)?;
 
diff --git a/src/config/domains.rs b/src/config/domains.rs
index ce3f6f23..a975fed1 100644
--- a/src/config/domains.rs
+++ b/src/config/domains.rs
@@ -3,6 +3,8 @@ use lazy_static::lazy_static;
 use std::collections::HashMap;
 use serde::{Serialize, Deserialize};
 
+use proxmox_openid::{OpenIdAuthenticator,  OpenIdConfig};
+
 use proxmox::api::{
     api,
     schema::*,
@@ -27,6 +29,22 @@ lazy_static! {
     pub static ref CONFIG: SectionConfig = init();
 }
 
+#[api()]
+#[derive(Eq, PartialEq, Debug, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+/// Use the value of this attribute/claim as unique user name. It is
+/// up to the identity provider to guarantee the uniqueness. The
+/// OpenID specification only guarantees that Subject ('sub') is unique. Also
+/// make sure that the user is not allowed to change that attribute by
+/// himself!
+pub enum OpenIdUserAttribute {
+    /// Subject (OpenId 'sub' claim)
+    Subject,
+    /// Username (OpenId 'preferred_username' claim)
+    Username,
+    /// Email (OpenId 'email' claim)
+    Email,
+}
 
 #[api(
     properties: {
@@ -50,6 +68,16 @@ lazy_static! {
             optional: true,
             schema: SINGLE_LINE_COMMENT_SCHEMA,
         },
+        autocreate: {
+            description: "Automatically create users if they do not exist.",
+            optional: true,
+            type: bool,
+            default: false,
+        },
+        "user-attr": {
+            type: OpenIdUserAttribute,
+            optional: true,
+        },
     },
 )]
 #[derive(Serialize,Deserialize)]
@@ -63,6 +91,22 @@ pub struct OpenIdRealmConfig {
     pub client_key: Option<String>,
     #[serde(skip_serializing_if="Option::is_none")]
     pub comment: Option<String>,
+    #[serde(skip_serializing_if="Option::is_none")]
+    pub autocreate: Option<bool>,
+    #[serde(skip_serializing_if="Option::is_none")]
+    pub user_attr: Option<OpenIdUserAttribute>,
+}
+
+impl OpenIdRealmConfig {
+
+    pub fn authenticator(&self, redirect_url: &str) -> Result<OpenIdAuthenticator, Error> {
+        let config = OpenIdConfig {
+            issuer_url: self.issuer_url.clone(),
+            client_id: self.client_id.clone(),
+            client_key: self.client_key.clone(),
+        };
+        OpenIdAuthenticator::discover(&config, redirect_url)
+    }
 }
 
 fn init() -> SectionConfig {
-- 
2.30.2




^ permalink raw reply	[flat|nested] 10+ messages in thread

* [pbs-devel] [PATCH proxmox-backup v2 9/9] ui: implement OpenId login
  2021-06-24 10:17 [pbs-devel] [PATCH proxmox-backup v2 0/9] OpenID connect realms Dietmar Maurer
                   ` (7 preceding siblings ...)
  2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 8/9] api: add openid redirect/login API Dietmar Maurer
@ 2021-06-24 10:17 ` Dietmar Maurer
  8 siblings, 0 replies; 10+ messages in thread
From: Dietmar Maurer @ 2021-06-24 10:17 UTC (permalink / raw)
  To: pbs-devel

---
 www/Application.js |   8 +++-
 www/LoginView.js   | 100 ++++++++++++++++++++++++++++++++++++++++++++-
 www/Utils.js       |   8 ++++
 3 files changed, 114 insertions(+), 2 deletions(-)

diff --git a/www/Application.js b/www/Application.js
index c4df600e..20ae26bd 100644
--- a/www/Application.js
+++ b/www/Application.js
@@ -49,9 +49,15 @@ Ext.define('PBS.Application', {
 	var provider = new Ext.state.LocalStorageProvider({ prefix: 'ext-pbs-' });
 	Ext.state.Manager.setProvider(provider);
 
+	let openid_login = false;
+	let param = PBS.Utils.openid_login_param();
+	if (param !== undefined) {
+	    openid_login = true;
+	}
+
 	// show login window if not loggedin
 	var loggedin = Proxmox.Utils.authOK();
-	if (!loggedin) {
+	if (openid_login || !loggedin) {
 	    me.changeView('loginview', true);
 	} else {
 	    me.changeView('mainview', true);
diff --git a/www/LoginView.js b/www/LoginView.js
index 6dd09646..ff3b5540 100644
--- a/www/LoginView.js
+++ b/www/LoginView.js
@@ -2,6 +2,21 @@ Ext.define('PBS.LoginView', {
     extend: 'Ext.container.Container',
     xtype: 'loginview',
 
+    viewModel: {
+	data: {
+	    openid: false,
+	},
+	formulas: {
+	    button_text: function(get) {
+		if (get("openid") === true) {
+		    return gettext("Login (OpenID redirect)");
+		} else {
+		    return gettext("Login");
+		}
+	    },
+	},
+    },
+
     controller: {
 	xclass: 'Ext.app.ViewController',
 
@@ -15,8 +30,33 @@ Ext.define('PBS.LoginView', {
 		return;
 	    }
 
+	    let redirect_url = location.origin;
+
 	    let params = loginForm.getValues();
 
+	    if (this.getViewModel().data.openid === true) {
+		let realm = params.realm;
+		try {
+		    let resp = await PBS.Async.api2({
+			url: '/api2/extjs/access/openid/auth-url',
+			params: {
+			    realm: realm,
+			    "redirect-url": redirect_url,
+			},
+			method: 'POST',
+		    });
+		    window.location = resp.result.data;
+		} catch (error) {
+		    Proxmox.Utils.authClear();
+		    loginForm.unmask();
+		    Ext.MessageBox.alert(
+			gettext('Error'),
+			gettext('OpenId redirect failed. Please try again<br>Error: ' + error),
+		    );
+		}
+		return;
+	    }
+
 	    params.username = params.username + '@' + params.realm;
 	    delete params.realm;
 
@@ -98,6 +138,14 @@ Ext.define('PBS.LoginView', {
 		    window.location.reload();
 		},
 	    },
+	    'field[name=realm]': {
+		change: function(f, value) {
+		    let record = f.store.getById(value);
+		    if (record === undefined) return;
+		    let data = record.data;
+		    this.getViewModel().set("openid", data.type === "openid");
+		},
+	    },
 	    'button[reference=loginButton]': {
 		click: 'submitForm',
 	    },
@@ -116,6 +164,43 @@ Ext.define('PBS.LoginView', {
 			var pwField = this.lookupReference('passwordField');
 			pwField.focus();
 		    }
+
+		    let param = PBS.Utils.openid_login_param();
+		    if (param !== undefined) {
+			Proxmox.Utils.authClear();
+
+			let loginForm = this.lookupReference('loginForm');
+			loginForm.mask(gettext('OpenID login - please wait...'), 'x-mask-loading');
+
+			let redirect_url = location.origin;
+
+			Proxmox.Utils.API2Request({
+			    url: '/api2/extjs/access/openid/login',
+			    params: {
+				state: param.state,
+				code: param.code,
+				"redirect-url": redirect_url,
+			    },
+			    method: 'POST',
+			    failure: function(response) {
+				loginForm.unmask();
+				Ext.MessageBox.alert(
+				    gettext('Error'),
+				    gettext('Login failed. Please try again<br>Error: ' + response.htmlStatus),
+				    function() {
+					window.location = redirect_url;
+				    },
+				);
+			    },
+			    success: function(response, options) {
+				loginForm.unmask();
+				let data = response.result.data;
+				PBS.Utils.updateLoginData(data);
+				PBS.app.changeView('mainview');
+				history.replaceState(null, '', redirect_url + '#pbsDashboard');
+			    },
+			});
+		    }
 		},
 	    },
 	},
@@ -191,6 +276,10 @@ Ext.define('PBS.LoginView', {
 			    itemId: 'usernameField',
 			    reference: 'usernameField',
 			    stateId: 'login-username',
+			    bind: {
+				visible: "{!openid}",
+				disabled: "{openid}",
+			    },
 			},
 			{
 			    xtype: 'textfield',
@@ -199,6 +288,10 @@ Ext.define('PBS.LoginView', {
 			    name: 'password',
 			    itemId: 'passwordField',
 			    reference: 'passwordField',
+			    bind: {
+				visible: "{!openid}",
+				disabled: "{openid}",
+			    },
 			},
 			{
 			    xtype: 'pmxRealmComboBox',
@@ -223,9 +316,14 @@ Ext.define('PBS.LoginView', {
 			    labelWidth: 250,
 			    labelAlign: 'right',
 			    submitValue: false,
+			    bind: {
+				visible: "{!openid}",
+			    },
 			},
 			{
-			    text: gettext('Login'),
+			    bind: {
+				text: "{button_text}",
+			    },
 			    reference: 'loginButton',
 			    formBind: true,
 			},
diff --git a/www/Utils.js b/www/Utils.js
index 6b378355..677f2204 100644
--- a/www/Utils.js
+++ b/www/Utils.js
@@ -326,6 +326,14 @@ Ext.define('PBS.Utils', {
         };
     },
 
+    openid_login_param: function() {
+	let param = Ext.Object.fromQueryString(window.location.search);
+	if (param.state !== undefined && param.code !== undefined) {
+	    return param;
+	}
+	return undefined;
+    },
+
     calculate_dedup_factor: function(gcstatus) {
 	let dedup = 1.0;
 	if (gcstatus['disk-bytes'] > 0) {
-- 
2.30.2




^ permalink raw reply	[flat|nested] 10+ messages in thread

end of thread, other threads:[~2021-06-24 10:18 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2021-06-24 10:17 [pbs-devel] [PATCH proxmox-backup v2 0/9] OpenID connect realms Dietmar Maurer
2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 1/9] depend on proxmox-openid-rs Dietmar Maurer
2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 2/9] config: new domains.cfg to configure openid realm Dietmar Maurer
2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 3/9] check_acl_path: add /access/domains and /access/openid Dietmar Maurer
2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 4/9] add API to manage openid realms Dietmar Maurer
2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 5/9] cli: add CLI " Dietmar Maurer
2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 6/9] implement new helper is_active_user_id() Dietmar Maurer
2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 7/9] cleanup user/token is_active() check Dietmar Maurer
2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 8/9] api: add openid redirect/login API Dietmar Maurer
2021-06-24 10:17 ` [pbs-devel] [PATCH proxmox-backup v2 9/9] ui: implement OpenId login Dietmar Maurer

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal