public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Shannon Sterz <s.sterz@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH datacenter-manager 2/3] server: api: add support for adding openid realms and openid logins
Date: Tue, 14 Oct 2025 15:30:43 +0200	[thread overview]
Message-ID: <20251014133044.337162-8-s.sterz@proxmox.com> (raw)
In-Reply-To: <20251014133044.337162-1-s.sterz@proxmox.com>

only supports the new HttpOnly authentication flow, as PDM does not
support non-HttpOnly authentication at the moment.

Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
 Cargo.toml                             |   2 +-
 server/Cargo.toml                      |   1 +
 server/src/api/access/mod.rs           |   2 +
 server/src/api/access/openid.rs        | 311 +++++++++++++++++++++++++
 server/src/api/config/access/mod.rs    |   2 +
 server/src/api/config/access/openid.rs | 290 +++++++++++++++++++++++
 server/src/auth/mod.rs                 |   6 +-
 7 files changed, 612 insertions(+), 2 deletions(-)
 create mode 100644 server/src/api/access/openid.rs
 create mode 100644 server/src/api/config/access/openid.rs

diff --git a/Cargo.toml b/Cargo.toml
index f820409..39a0b23 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -68,7 +68,7 @@ proxmox-uuid = "1"
 
 # other proxmox crates
 proxmox-acme = "0.5"
-proxmox-openid = "0.10"
+proxmox-openid = "1.0.2"
 
 # api implementation creates
 proxmox-config-digest = "1"
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 0dfcb6c..94420b4 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -44,6 +44,7 @@ proxmox-lang.workspace = true
 proxmox-ldap.workspace = true
 proxmox-log.workspace = true
 proxmox-login.workspace = true
+proxmox-openid.workspace = true
 proxmox-rest-server = { workspace = true, features = [ "templates" ] }
 proxmox-router = { workspace = true, features = [ "cli", "server"] }
 proxmox-rrd.workspace = true
diff --git a/server/src/api/access/mod.rs b/server/src/api/access/mod.rs
index 345b22f..a6874a5 100644
--- a/server/src/api/access/mod.rs
+++ b/server/src/api/access/mod.rs
@@ -14,6 +14,7 @@ use proxmox_sortable_macro::sortable;
 use pdm_api_types::{Authid, ACL_PATH_SCHEMA, PRIVILEGES, PRIV_ACCESS_AUDIT};
 
 mod domains;
+mod openid;
 mod tfa;
 mod users;
 
@@ -33,6 +34,7 @@ const SUBDIRS: SubdirMap = &sorted!([
             .post(&proxmox_auth_api::api::API_METHOD_CREATE_TICKET_HTTP_ONLY)
             .delete(&proxmox_auth_api::api::API_METHOD_LOGOUT),
     ),
+    ("openid", &openid::ROUTER),
     ("users", &users::ROUTER),
 ]);
 
diff --git a/server/src/api/access/openid.rs b/server/src/api/access/openid.rs
new file mode 100644
index 0000000..5f662f0
--- /dev/null
+++ b/server/src/api/access/openid.rs
@@ -0,0 +1,311 @@
+//! OpenID redirect/login API
+use anyhow::{bail, format_err, Error};
+use hyper::http::request::Parts;
+use hyper::Response;
+use pdm_api_types::{OpenIdRealmConfig, OPENID_DEFAULT_SCOPE_LIST, REALM_ID_SCHEMA};
+use pdm_buildcfg::PDM_RUN_DIR_M;
+use proxmox_auth_api::api::ApiTicket;
+use proxmox_login::api::CreateTicketResponse;
+use serde_json::{json, Value};
+
+use proxmox_auth_api::api::{assemble_csrf_prevention_token, AuthContext};
+use proxmox_auth_api::ticket::Ticket;
+use proxmox_router::{
+    http_err, list_subdirs_api_method, ApiHandler, ApiMethod, ApiResponseFuture, Permission,
+    Router, RpcEnvironment, SubdirMap,
+};
+use proxmox_schema::{api, BooleanSchema, ObjectSchema, ParameterSchema, StringSchema};
+use proxmox_sortable_macro::sortable;
+
+use proxmox_openid::{OpenIdAuthenticator, OpenIdConfig};
+
+use proxmox_access_control::types::{User, EMAIL_SCHEMA, FIRST_NAME_SCHEMA, LAST_NAME_SCHEMA};
+use proxmox_auth_api::types::Userid;
+
+use proxmox_access_control::CachedUserInfo;
+
+use crate::auth;
+
+fn openid_authenticator(
+    realm_config: &OpenIdRealmConfig,
+    redirect_url: &str,
+) -> Result<OpenIdAuthenticator, Error> {
+    let scopes: Vec<String> = realm_config
+        .scopes
+        .as_deref()
+        .unwrap_or(OPENID_DEFAULT_SCOPE_LIST)
+        .split(|c: char| c == ',' || c == ';' || char::is_ascii_whitespace(&c))
+        .filter(|s| !s.is_empty())
+        .map(String::from)
+        .collect();
+
+    let mut acr_values = None;
+    if let Some(ref list) = realm_config.acr_values {
+        acr_values = Some(
+            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),
+        acr_values,
+    };
+    OpenIdAuthenticator::discover(&config, redirect_url)
+}
+
+#[sortable]
+pub const API_METHOD_OPENID_LOGIN: ApiMethod = ApiMethod::new_full(
+    &ApiHandler::AsyncHttpBodyParameters(&create_ticket_http_only),
+    ParameterSchema::Object(&ObjectSchema::new(
+        "Get a new ticket as an HttpOnly cookie. Supports tickets via cookies.",
+        &sorted!([
+            ("state", false, &StringSchema::new("OpenId state.").schema()),
+            (
+                "code",
+                false,
+                &StringSchema::new("OpenId authorization code.").schema(),
+            ),
+            (
+                "redirect-url",
+                false,
+                &StringSchema::new(
+                    "Redirection Url. The client should set this to used server url.",
+                )
+                .schema(),
+            ),
+        ]),
+    )),
+)
+.returns(::proxmox_schema::ReturnType::new(
+    false,
+    &ObjectSchema::new(
+        "An authentication ticket with additional infos.",
+        &sorted!([
+            ("username", false, &StringSchema::new("User name.").schema()),
+            (
+                "ticket",
+                true,
+                &StringSchema::new(
+                    "Auth ticket, present if http-only was not provided or is false."
+                )
+                .schema()
+            ),
+            ("ticket-info",
+                true,
+                &StringSchema::new(
+                    "Informational ticket, can only be used to check if the ticket is expired. Present if http-only was true."
+                ).schema()),
+            (
+                "CSRFPreventionToken",
+                false,
+                &StringSchema::new("Cross Site Request Forgery Prevention Token.").schema(),
+            ),
+        ]),
+    )
+    .schema(),
+))
+.protected(true)
+.access(None, &Permission::World)
+.reload_timezone(true);
+
+fn create_ticket_http_only(
+    _parts: Parts,
+    param: Value,
+    _info: &ApiMethod,
+    rpcenv: Box<dyn RpcEnvironment>,
+) -> ApiResponseFuture {
+    Box::pin(async move {
+        use proxmox_rest_server::RestEnvironment;
+
+        let code = param["code"]
+            .as_str()
+            .ok_or_else(|| format_err!("missing non-optional parameter: code"))?
+            .to_owned();
+        let state = param["state"]
+            .as_str()
+            .ok_or_else(|| format_err!("missing non-optional parameter: state"))?
+            .to_owned();
+        let redirect_url = param["redirect-url"]
+            .as_str()
+            .ok_or_else(|| format_err!("missing non-optional parameter: redirect-url"))?
+            .to_owned();
+
+        let env: &RestEnvironment = rpcenv
+            .as_any()
+            .downcast_ref::<RestEnvironment>()
+            .ok_or_else(|| format_err!("detected wrong RpcEnvironment type"))?;
+
+        let user_info = CachedUserInfo::new()?;
+        let auth_context = auth::get_auth_context()
+            .ok_or_else(|| format_err!("could not get authentication context"))?;
+
+        let mut tested_username = None;
+
+        let result = (|| {
+            let (realm, private_auth_state) =
+                OpenIdAuthenticator::verify_public_auth_state(PDM_RUN_DIR_M!(), &state)?;
+
+            let (domains, _digest) = pdm_config::domains::config()?;
+            let config: OpenIdRealmConfig = domains.lookup("openid", &realm)?;
+            let open_id = openid_authenticator(&config, &redirect_url)?;
+            let info = open_id.verify_authorization_code_simple(&code, &private_auth_state)?;
+            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 = if let Some(name) = info[name_attr]
+                .as_str()
+                .or_else(|| try_attr.and_then(|att| info[att].as_str()))
+            {
+                name.to_owned()
+            } else {
+                bail!("missing claim '{name_attr}'");
+            };
+
+            let user_id = Userid::try_from(format!("{unique_name}@{realm}"))?;
+            tested_username = Some(unique_name);
+
+            if !user_info.is_active_user_id(&user_id) {
+                if config.autocreate.unwrap_or(false) {
+                    let _lock = proxmox_access_control::user::lock_config()?;
+                    let (mut user_config, _digest) = proxmox_access_control::user::config()?;
+
+                    if let Ok(old_user) = user_config.lookup::<User>("user", user_id.as_str()) {
+                        if let Some(false) = old_user.enable {
+                            bail!("user '{user_id}' is disabled.");
+                        } else {
+                            bail!("autocreate user failed - '{user_id}' already exists.");
+                        }
+                    }
+
+                    let firstname = info["given_name"]
+                        .as_str()
+                        .map(|n| n.to_string())
+                        .filter(|n| FIRST_NAME_SCHEMA.parse_simple_value(n).is_ok());
+
+                    let lastname = info["family_name"]
+                        .as_str()
+                        .map(|n| n.to_string())
+                        .filter(|n| LAST_NAME_SCHEMA.parse_simple_value(n).is_ok());
+
+                    let email = info["email"]
+                        .as_str()
+                        .map(|n| n.to_string())
+                        .filter(|n| EMAIL_SCHEMA.parse_simple_value(n).is_ok());
+
+                    let user = User {
+                        userid: user_id.clone(),
+                        comment: None,
+                        enable: None,
+                        expire: None,
+                        firstname,
+                        lastname,
+                        email,
+                    };
+
+                    user_config.set_data(user.userid.as_str(), "user", &user)?;
+                    proxmox_access_control::user::save_config(&user_config)?;
+                } else {
+                    bail!("user account '{user_id}' missing, disabled or expired.");
+                }
+            }
+
+            let api_ticket = ApiTicket::Full(user_id.clone());
+            let ticket = Ticket::new(auth_context.auth_prefix(), &api_ticket)?;
+            let token = assemble_csrf_prevention_token(auth_context.csrf_secret(), &user_id);
+            env.log_auth(user_id.as_str());
+
+            Ok((user_id, ticket, token))
+        })();
+
+        let (user_id, mut ticket, token) = result.map_err(|err| {
+            let msg = err.to_string();
+            env.log_failed_auth(tested_username, &msg);
+            http_err!(UNAUTHORIZED, "{msg}")
+        })?;
+
+        let cookie = format!(
+            "{}={}; Secure; SameSite=Lax; HttpOnly; Path=/;",
+            auth_context.prefixed_auth_cookie_name(),
+            ticket.sign(auth_context.keyring(), None)?,
+        );
+
+        let response = Response::builder()
+            .header(hyper::http::header::CONTENT_TYPE, "application/json")
+            .header(hyper::header::SET_COOKIE, cookie);
+
+        let data = CreateTicketResponse {
+            csrfprevention_token: Some(token),
+            clustername: None,
+            ticket: None,
+            ticket_info: Some(ticket.ticket_info()),
+            username: user_id.to_string(),
+        };
+
+        Ok(response.body(
+            json!({"data": data, "status": 200, "success": true })
+                .to_string()
+                .into(),
+        )?)
+    })
+}
+
+#[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
+pub fn openid_auth_url(
+    realm: String,
+    redirect_url: String,
+    _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<String, Error> {
+    let (domains, _digest) = pdm_config::domains::config()?;
+    let config: OpenIdRealmConfig = domains.lookup("openid", &realm)?;
+
+    let open_id = openid_authenticator(&config, &redirect_url)?;
+
+    let url = open_id.authorize_url(PDM_RUN_DIR_M!(), &realm)?;
+
+    Ok(url)
+}
+
+#[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/server/src/api/config/access/mod.rs b/server/src/api/config/access/mod.rs
index 7454f53..5776152 100644
--- a/server/src/api/config/access/mod.rs
+++ b/server/src/api/config/access/mod.rs
@@ -4,12 +4,14 @@ use proxmox_sortable_macro::sortable;
 
 mod ad;
 mod ldap;
+mod openid;
 pub mod tfa;
 
 #[sortable]
 const SUBDIRS: SubdirMap = &sorted!([
     ("tfa", &tfa::ROUTER),
     ("ldap", &ldap::ROUTER),
+    ("openid", &openid::ROUTER),
     ("ad", &ad::ROUTER),
 ]);
 
diff --git a/server/src/api/config/access/openid.rs b/server/src/api/config/access/openid.rs
new file mode 100644
index 0000000..555a1e1
--- /dev/null
+++ b/server/src/api/config/access/openid.rs
@@ -0,0 +1,290 @@
+use ::serde::{Deserialize, Serialize};
+/// Configure OpenId realms
+use anyhow::Error;
+use pdm_api_types::ConfigDigest;
+use serde_json::Value;
+
+use proxmox_router::{http_bail, Permission, Router, RpcEnvironment};
+use proxmox_schema::{api, param_bail};
+
+use pdm_api_types::{
+    OpenIdRealmConfig, OpenIdRealmConfigUpdater, PRIV_REALM_ALLOCATE, PRIV_SYS_AUDIT,
+    REALM_ID_SCHEMA,
+};
+
+use pdm_config::domains;
+
+#[api(
+    input: {
+        properties: {},
+    },
+    returns: {
+        description: "List of configured OpenId realms.",
+        type: Array,
+        items: { type: OpenIdRealmConfig },
+    },
+    access: {
+        permission: &Permission::Privilege(&["access", "domains"], PRIV_REALM_ALLOCATE, false),
+    },
+)]
+/// List configured OpenId realms
+pub fn list_openid_realms(
+    _param: Value,
+    rpcenv: &mut dyn RpcEnvironment,
+) -> Result<Vec<OpenIdRealmConfig>, Error> {
+    let (config, digest) = domains::config()?;
+
+    let list = config.convert_to_typed_array("openid")?;
+
+    rpcenv["digest"] = hex::encode(digest).into();
+
+    Ok(list)
+}
+
+#[api(
+    protected: true,
+    input: {
+        properties: {
+            config: {
+                type: OpenIdRealmConfig,
+                flatten: true,
+            },
+        },
+    },
+    access: {
+        permission: &Permission::Privilege(&["access", "domains"], PRIV_REALM_ALLOCATE, 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 domains::exists(&domains, &config.realm) {
+        param_bail!("realm", "realm '{}' already exists.", config.realm);
+    }
+
+    if let Some(true) = config.default {
+        domains::unset_default_realm(&mut domains)?;
+    }
+
+    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,
+                type: ConfigDigest,
+            },
+        },
+    },
+    access: {
+        permission: &Permission::Privilege(&["access", "domains"], PRIV_REALM_ALLOCATE, false),
+    },
+)]
+/// Remove a OpenID realm configuration
+pub fn delete_openid_realm(
+    realm: String,
+    digest: Option<ConfigDigest>,
+    _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<(), Error> {
+    let _lock = domains::lock_config()?;
+
+    let (mut domains, expected_digest) = domains::config()?;
+    expected_digest.detect_modification(digest.as_ref())?;
+
+    if domains.sections.remove(&realm).is_none() {
+        http_bail!(NOT_FOUND, "realm '{realm}' does not exist.");
+    }
+
+    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,
+    rpcenv: &mut dyn RpcEnvironment,
+) -> Result<OpenIdRealmConfig, Error> {
+    let (domains, digest) = domains::config()?;
+
+    let config = domains.lookup("openid", &realm)?;
+    rpcenv["digest"] = hex::encode(digest).into();
+
+    Ok(config)
+}
+
+#[api()]
+#[derive(Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+/// Deletable property name
+pub enum DeletableProperty {
+    /// Delete the client key.
+    ClientKey,
+    /// Delete the comment property.
+    Comment,
+    /// Delete the default property.
+    Default,
+    /// Delete the autocreate property
+    Autocreate,
+    /// Delete the scopes property
+    Scopes,
+    /// Delete the prompt property
+    Prompt,
+    /// Delete the acr_values property
+    AcrValues,
+}
+
+#[api(
+    protected: true,
+    input: {
+        properties: {
+            realm: {
+                schema: REALM_ID_SCHEMA,
+            },
+            update: {
+                type: OpenIdRealmConfigUpdater,
+                flatten: true,
+            },
+            delete: {
+                description: "List of properties to delete.",
+                type: Array,
+                optional: true,
+                items: {
+                    type: DeletableProperty,
+                }
+            },
+            digest: {
+                optional: true,
+                type: ConfigDigest,
+            },
+        },
+    },
+    returns:  { type: OpenIdRealmConfig },
+    access: {
+        permission: &Permission::Privilege(&["access", "domains"], PRIV_REALM_ALLOCATE, false),
+    },
+)]
+/// Update an OpenID realm configuration
+pub fn update_openid_realm(
+    realm: String,
+    update: OpenIdRealmConfigUpdater,
+    delete: Option<Vec<DeletableProperty>>,
+    digest: Option<ConfigDigest>,
+    _rpcenv: &mut dyn RpcEnvironment,
+) -> Result<(), Error> {
+    let _lock = domains::lock_config()?;
+
+    let (mut domains, expected_digest) = domains::config()?;
+    expected_digest.detect_modification(digest.as_ref())?;
+
+    let mut config: OpenIdRealmConfig = domains.lookup("openid", &realm)?;
+
+    if let Some(delete) = delete {
+        for delete_prop in delete {
+            match delete_prop {
+                DeletableProperty::ClientKey => {
+                    config.client_key = None;
+                }
+                DeletableProperty::Comment => {
+                    config.comment = None;
+                }
+                DeletableProperty::Default => {
+                    config.default = None;
+                }
+                DeletableProperty::Autocreate => {
+                    config.autocreate = None;
+                }
+                DeletableProperty::Scopes => {
+                    config.scopes = None;
+                }
+                DeletableProperty::Prompt => {
+                    config.prompt = None;
+                }
+                DeletableProperty::AcrValues => {
+                    config.acr_values = None;
+                }
+            }
+        }
+    }
+
+    if let Some(comment) = update.comment {
+        let comment = comment.trim().to_string();
+        if comment.is_empty() {
+            config.comment = None;
+        } else {
+            config.comment = Some(comment);
+        }
+    }
+
+    if let Some(true) = update.default {
+        domains::unset_default_realm(&mut domains)?;
+        config.default = Some(true);
+    } else {
+        config.default = None;
+    }
+
+    if let Some(issuer_url) = update.issuer_url {
+        config.issuer_url = issuer_url;
+    }
+    if let Some(client_id) = update.client_id {
+        config.client_id = client_id;
+    }
+
+    if update.client_key.is_some() {
+        config.client_key = update.client_key;
+    }
+    if update.autocreate.is_some() {
+        config.autocreate = update.autocreate;
+    }
+    if update.scopes.is_some() {
+        config.scopes = update.scopes;
+    }
+    if update.prompt.is_some() {
+        config.prompt = update.prompt;
+    }
+    if update.acr_values.is_some() {
+        config.acr_values = update.acr_values;
+    }
+
+    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("realm", &ITEM_ROUTER);
diff --git a/server/src/auth/mod.rs b/server/src/auth/mod.rs
index 532350d..9413a83 100644
--- a/server/src/auth/mod.rs
+++ b/server/src/auth/mod.rs
@@ -81,7 +81,11 @@ fn setup_auth_context(use_private_key: bool) {
     proxmox_auth_api::set_auth_context(AUTH_CONTEXT.get().unwrap());
 }
 
-struct PdmAuthContext {
+pub(crate) fn get_auth_context() -> Option<&'static PdmAuthContext> {
+    AUTH_CONTEXT.get()
+}
+
+pub(crate) struct PdmAuthContext {
     keyring: Keyring,
     csrf_secret: &'static HMACKey,
 }
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


  parent reply	other threads:[~2025-10-14 13:30 UTC|newest]

Thread overview: 14+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-10-14 13:30 [pdm-devel] [PATCH datacenter-manager/yew-comp 0/8] openid support for PDM Shannon Sterz
2025-10-14 13:30 ` [pdm-devel] [PATCH yew-comp 1/5] login_panel/realm_selector: use default realm provided by api Shannon Sterz
2025-10-14 13:30 ` [pdm-devel] [PATCH yew-comp 2/5] login_panel/realm_selector: add support for openid realm logins Shannon Sterz
2025-10-14 13:30 ` [pdm-devel] [PATCH yew-comp 3/5] auth view: add openid icon to openid menu option Shannon Sterz
2025-10-14 13:30 ` [pdm-devel] [PATCH yew-comp 4/5] auth edit openid: add a default realm checkbox Shannon Sterz
2025-10-14 13:30 ` [pdm-devel] [PATCH yew-comp 5/5] utils/login panel: move openid redirection authorization helper to utils Shannon Sterz
2025-10-14 13:30 ` [pdm-devel] [PATCH datacenter-manager 1/3] api-types: add default field to openid realm config Shannon Sterz
2025-10-14 13:30 ` Shannon Sterz [this message]
2025-10-17  7:57   ` [pdm-devel] [PATCH datacenter-manager 2/3] server: api: add support for adding openid realms and openid logins Fabian Grünbichler
2025-10-17 13:36     ` Shannon Sterz
2025-10-14 13:30 ` [pdm-devel] [PATCH datacenter-manager 3/3] ui: enable openid realms in realm panel Shannon Sterz
2025-10-17  8:01 ` [pdm-devel] [PATCH datacenter-manager/yew-comp 0/8] openid support for PDM Fabian Grünbichler
2025-10-17 14:36   ` Shannon Sterz
2025-10-17 14:13 ` [pdm-devel] Superseded: " Shannon Sterz

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20251014133044.337162-8-s.sterz@proxmox.com \
    --to=s.sterz@proxmox.com \
    --cc=pdm-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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