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
next prev 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