[pbs-devel] [PATCH proxmox-backup] openid: allow to configure scopes, prompt and arbitrary username-claim values

Dietmar Maurer dietmar at proxmox.com
Fri Aug 6 09:17:05 CEST 2021


- no longer set prompt to 'login' (makes auto-login possible)
- new prompt configuration
- allow arbitrary username-claim values

We now allow to change the username-claim in the update API.

Depend on proxmox-openid 0.7.0.
---
 Cargo.toml                       |  2 +-
 pbs-api-types/src/lib.rs         | 31 +++++++++++++++++-
 src/api2/access/openid.rs        | 54 +++++++++++++++++++++-----------
 src/api2/config/access/openid.rs | 29 +++++++++++++++++
 src/config/domains.rs            | 35 ++++++++++-----------
 5 files changed, 111 insertions(+), 40 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index c6141842..e71b87ba 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -92,7 +92,7 @@ proxmox = { version = "0.12.1", features = [ "sortable-macro", "api-macro", "cli
 proxmox-acme-rs = "0.2.1"
 proxmox-apt = "0.6.0"
 proxmox-http = { version = "0.3.0", features = [ "client", "http-helpers", "websocket" ] }
-proxmox-openid = "0.6.1"
+proxmox-openid = "0.7.0"
 
 pbs-api-types = { path = "pbs-api-types" }
 pbs-buildcfg = { path = "pbs-buildcfg" }
diff --git a/pbs-api-types/src/lib.rs b/pbs-api-types/src/lib.rs
index 576099eb..1cba21f4 100644
--- a/pbs-api-types/src/lib.rs
+++ b/pbs-api-types/src/lib.rs
@@ -3,7 +3,7 @@
 use serde::{Deserialize, Serialize};
 
 use proxmox::api::api;
-use proxmox::api::schema::{ApiStringFormat, EnumEntry, IntegerSchema, Schema, StringSchema};
+use proxmox::api::schema::{ApiStringFormat, EnumEntry, IntegerSchema, Schema, StringSchema, ArraySchema};
 use proxmox::const_regex;
 use proxmox::{IPRE, IPRE_BRACKET, IPV4OCTET, IPV4RE, IPV6H16, IPV6LS32, IPV6RE};
 
@@ -184,6 +184,35 @@ pub const PRUNE_SCHEMA_KEEP_YEARLY: Schema =
 pub const PROXMOX_SAFE_ID_FORMAT: ApiStringFormat =
     ApiStringFormat::Pattern(&PROXMOX_SAFE_ID_REGEX);
 
+pub const OPENID_SCOPE_FORMAT: ApiStringFormat =
+    ApiStringFormat::Pattern(&PROXMOX_SAFE_ID_REGEX);
+
+pub const OPENID_SCOPE_SCHEMA: Schema = StringSchema::new("OpenID Scope Name.")
+    .format(&OPENID_SCOPE_FORMAT)
+    .schema();
+
+pub const OPENID_SCOPE_ARRAY_SCHEMA: Schema = ArraySchema::new(
+    "Array of OpenId Scopes.", &OPENID_SCOPE_SCHEMA).schema();
+
+pub const OPENID_SCOPE_LIST_FORMAT: ApiStringFormat =
+    ApiStringFormat::PropertyString(&OPENID_SCOPE_ARRAY_SCHEMA);
+
+pub const OPENID_DEFAILT_SCOPE_LIST: &'static str = "email profile";
+pub const OPENID_SCOPE_LIST_SCHEMA: Schema = StringSchema::new("OpenID Scope List")
+    .format(&OPENID_SCOPE_LIST_FORMAT)
+    .default(OPENID_DEFAILT_SCOPE_LIST)
+    .schema();
+
+pub const OPENID_USERNAME_CLAIM_SCHEMA: Schema = StringSchema::new(
+    "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!")
+    .max_length(64)
+    .min_length(1)
+    .format(&PROXMOX_SAFE_ID_FORMAT) .schema();
+
 pub const SINGLE_LINE_COMMENT_FORMAT: ApiStringFormat =
     ApiStringFormat::Pattern(&SINGLE_LINE_COMMENT_REGEX);
 
diff --git a/src/api2/access/openid.rs b/src/api2/access/openid.rs
index f78e4674..43dc77e4 100644
--- a/src/api2/access/openid.rs
+++ b/src/api2/access/openid.rs
@@ -18,7 +18,7 @@ use pbs_tools::ticket::Ticket;
 
 use crate::server::ticket::ApiTicket;
 
-use crate::config::domains::{OpenIdUserAttribute, OpenIdRealmConfig};
+use crate::config::domains::OpenIdRealmConfig;
 use crate::config::cached_user_info::CachedUserInfo;
 
 use crate::backup::open_backup_lockfile;
@@ -27,15 +27,24 @@ use crate::api2::types::*;
 use crate::auth_helpers::*;
 
 fn openid_authenticator(realm_config: &OpenIdRealmConfig, redirect_url: &str) -> Result<OpenIdAuthenticator, Error> {
+
+    let ref list = realm_config.scopes.as_deref().unwrap_or(OPENID_DEFAILT_SCOPE_LIST);
+    let scopes: Vec<String> = list
+        .split(|c: char| c == ',' || c == ';' || char::is_ascii_whitespace(&c))
+        .filter(|s| !s.is_empty())
+        .map(String::from)
+        .collect();
+
     let config = OpenIdConfig {
         issuer_url: realm_config.issuer_url.clone(),
         client_id: realm_config.client_id.clone(),
         client_key: realm_config.client_key.clone(),
+        prompt: realm_config.prompt.clone(),
+        scopes: Some(scopes),
     };
     OpenIdAuthenticator::discover(&config, redirect_url)
 }
 
-
 #[api(
     input: {
         properties: {
@@ -93,22 +102,29 @@ pub fn openid_login(
 
     let open_id = openid_authenticator(&config, &redirect_url)?;
 
-    let info = open_id.verify_authorization_code(&code, &private_auth_state)?;
+    let info = open_id.verify_authorization_code_simple(&code, &private_auth_state)?;
 
-    // eprintln!("VERIFIED {} {:?} {:?}", info.subject().as_str(), info.name(), info.email());
+    // eprintln!("VERIFIED {:?}", info);
 
-    let unique_name = match config.username_claim {
-        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 name_attr = config.username_claim.as_deref().unwrap_or("sub");
+
+    // Try to be compatible with previous versions
+    let try_attr = match name_attr {
+        "subject" => Some("sub"),
+        "username" => Some("preferred_username"),
+        _ => None,
+    };
+
+    let unique_name = match info[name_attr].as_str() {
+        Some(name) => name.to_owned(),
+        None => {
+            if let Some(try_attr) = try_attr {
+                match info[try_attr].as_str() {
+                    Some(name) => name.to_owned(),
+                    None => bail!("missing claim '{}'", name_attr),
+                }
+            } else {
+                bail!("missing claim '{}'", name_attr);
             }
         }
     };
@@ -124,9 +140,9 @@ pub fn openid_login(
                 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()),
+                firstname: info["given_name"].as_str().map(|n| n.to_string()),
+                lastname: info["family_name"].as_str().map(|n| n.to_string()),
+                email: info["email"].as_str().map(|e| e.to_string()),
             };
             let (mut config, _digest) = user::config()?;
             if config.sections.get(user.userid.as_str()).is_some() {
diff --git a/src/api2/config/access/openid.rs b/src/api2/config/access/openid.rs
index b8b07306..9c7ff101 100644
--- a/src/api2/config/access/openid.rs
+++ b/src/api2/config/access/openid.rs
@@ -155,6 +155,12 @@ pub enum DeletableProperty {
     comment,
     /// Delete the autocreate property
     autocreate,
+    /// Delete the scopes property
+    scopes,
+    /// Delete the prompt property
+    prompt,
+    /// Delete the username-claim property
+    username_claim,
 }
 
 #[api(
@@ -179,6 +185,20 @@ pub enum DeletableProperty {
                 type: String,
                 optional: true,
             },
+            "username-claim": {
+                schema: OPENID_USERNAME_CLAIM_SCHEMA,
+                optional: true,
+            },
+            "scopes": {
+                schema: OPENID_SCOPE_LIST_SCHEMA,
+                optional: true,
+            },
+            prompt: {
+                description: "OpenID Prompt",
+                type: String,
+                format: &PROXMOX_SAFE_ID_FORMAT,
+                optional: true,
+            },
             autocreate: {
                 description: "Automatically create users if they do not exist.",
                 optional: true,
@@ -213,6 +233,9 @@ pub fn update_openid_realm(
     issuer_url: Option<String>,
     client_id: Option<String>,
     client_key: Option<String>,
+    scopes: Option<String>,
+    prompt: Option<String>,
+    username_claim: Option<String>,
     autocreate: Option<bool>,
     comment: Option<String>,
     delete: Option<Vec<DeletableProperty>>,
@@ -237,6 +260,9 @@ pub fn update_openid_realm(
                 DeletableProperty::client_key => { config.client_key = None; },
                 DeletableProperty::comment => { config.comment = None; },
                 DeletableProperty::autocreate => { config.autocreate = None; },
+                DeletableProperty::scopes => { config.scopes = None; },
+                DeletableProperty::prompt => { config.prompt = None; },
+                DeletableProperty::username_claim => { config.username_claim = None; },
             }
         }
     }
@@ -255,6 +281,9 @@ pub fn update_openid_realm(
 
     if client_key.is_some() { config.client_key = client_key; }
     if autocreate.is_some() { config.autocreate = autocreate; }
+    if scopes.is_some() { config.scopes = scopes; }
+    if prompt.is_some() { config.prompt = prompt; }
+    if username_claim.is_some() { config.username_claim = username_claim; }
 
     domains.set_data(&realm, "openid", &config)?;
 
diff --git a/src/config/domains.rs b/src/config/domains.rs
index 0d695777..5752e8ad 100644
--- a/src/config/domains.rs
+++ b/src/config/domains.rs
@@ -20,23 +20,6 @@ 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: {
         realm: {
@@ -55,6 +38,16 @@ pub enum OpenIdUserAttribute {
             type: String,
             optional: true,
         },
+        "scopes": {
+            schema: OPENID_SCOPE_LIST_SCHEMA,
+            optional: true,
+        },
+        prompt: {
+            description: "OpenID Prompt",
+            type: String,
+            format: &PROXMOX_SAFE_ID_FORMAT,
+            optional: true,
+        },
         comment: {
             optional: true,
             schema: SINGLE_LINE_COMMENT_SCHEMA,
@@ -66,7 +59,7 @@ pub enum OpenIdUserAttribute {
             default: false,
         },
         "username-claim": {
-            type: OpenIdUserAttribute,
+            schema: OPENID_USERNAME_CLAIM_SCHEMA,
             optional: true,
         },
     },
@@ -79,13 +72,17 @@ pub struct OpenIdRealmConfig {
     pub issuer_url: String,
     pub client_id: String,
     #[serde(skip_serializing_if="Option::is_none")]
+    pub scopes: Option<String>,
+    #[serde(skip_serializing_if="Option::is_none")]
+    pub prompt: Option<String>,
+    #[serde(skip_serializing_if="Option::is_none")]
     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 username_claim: Option<OpenIdUserAttribute>,
+    pub username_claim: Option<String>,
 }
 
 fn init() -> SectionConfig {
-- 
2.30.2






More information about the pbs-devel mailing list