all lists on 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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal