public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: "Fabian Grünbichler" <f.gruenbichler@proxmox.com>
To: Proxmox Datacenter Manager development discussion
	<pdm-devel@lists.proxmox.com>
Subject: Re: [pdm-devel] [PATCH datacenter-manager 2/3] server: api: add support for adding openid realms and openid logins
Date: Fri, 17 Oct 2025 09:57:20 +0200	[thread overview]
Message-ID: <1760684951.paenarjg51.astroid@yuna.none> (raw)
In-Reply-To: <20251014133044.337162-8-s.sterz@proxmox.com>

On October 14, 2025 3:30 pm, Shannon Sterz wrote:
> 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;

nit: these two should be listed last..

> +use proxmox_auth_api::api::ApiTicket;
> +use proxmox_login::api::CreateTicketResponse;

and these two below, but - this is the wrong CreateTicketResponse?

should be proxmox_auth_api::types::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};

nit: BooleanSchema is not used..

> +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;

and those last five are not sorted

> +
> +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(

and if we use the right CreateTicketResponse type above, we can use the
API_SCHEMA it already has here :)

> +        "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 = (|| {

is there a reason to not use try_block! for this, like in the rest of
our code base?

> +            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);

this is copied over from PBS, but isn't this also kinda wrong? not every
error above is a failed auth.. at least if we compare error handling
here with the one in the regular ticket flow, this is inconsistent..

might rather be follow-up material after closer thought though, and
ensuring PBS and PDM behave the same afterwards..

> +            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,

this is then removed

> +            ticket: None,
> +            ticket_info: Some(ticket.ticket_info()),
> +            username: user_id.to_string(),

as well as the to_string here

> +        };
> +
> +        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,

String type without a schema :-/

> +            },
> +        },
> +    },
> +    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;

same here as for the other module

> +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
> 
> 
> 


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


  reply	other threads:[~2025-10-17  7:57 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 ` [pdm-devel] [PATCH datacenter-manager 2/3] server: api: add support for adding openid realms and openid logins Shannon Sterz
2025-10-17  7:57   ` Fabian Grünbichler [this message]
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=1760684951.paenarjg51.astroid@yuna.none \
    --to=f.gruenbichler@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