diff --git a/Cargo.toml b/Cargo.toml index 1442de812..05f0811c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,7 +109,7 @@ proxmox-shared-memory = "0.1.1" proxmox-acme-rs = "0.3" proxmox-apt = "0.8.0" -proxmox-openid = "0.8.1" +proxmox-openid = "0.9.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 01c14cc4a..4247bba3c 100644 --- a/pbs-api-types/src/lib.rs +++ b/pbs-api-types/src/lib.rs @@ -68,6 +68,9 @@ pub use crypto::{CryptMode, Fingerprint, bytes_as_fingerprint}; pub mod file_restore; +mod openid; +pub use openid::*; + mod remote; pub use remote::*; diff --git a/pbs-api-types/src/openid.rs b/pbs-api-types/src/openid.rs new file mode 100644 index 000000000..65967bd1f --- /dev/null +++ b/pbs-api-types/src/openid.rs @@ -0,0 +1,121 @@ +use serde::{Deserialize, Serialize}; + +use proxmox_schema::{ + api, ApiStringFormat, ArraySchema, Schema, StringSchema, Updater, +}; + +use super::{ + PROXMOX_SAFE_ID_REGEX, PROXMOX_SAFE_ID_FORMAT, REALM_ID_SCHEMA, + SINGLE_LINE_COMMENT_SCHEMA, +}; + +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_ACR_FORMAT: ApiStringFormat = + ApiStringFormat::Pattern(&PROXMOX_SAFE_ID_REGEX); + +pub const OPENID_ACR_SCHEMA: Schema = StringSchema::new("OpenID Authentication Context Class Reference.") + .format(&OPENID_SCOPE_FORMAT) + .schema(); + +pub const OPENID_ACR_ARRAY_SCHEMA: Schema = ArraySchema::new( + "Array of OpenId ACRs.", &OPENID_ACR_SCHEMA).schema(); + +pub const OPENID_ACR_LIST_FORMAT: ApiStringFormat = + ApiStringFormat::PropertyString(&OPENID_ACR_ARRAY_SCHEMA); + +pub const OPENID_ACR_LIST_SCHEMA: Schema = StringSchema::new("OpenID ACR List") + .format(&OPENID_ACR_LIST_FORMAT) + .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(); + +#[api( + properties: { + realm: { + schema: REALM_ID_SCHEMA, + }, + "client-key": { + optional: true, + }, + "scopes": { + schema: OPENID_SCOPE_LIST_SCHEMA, + optional: true, + }, + "acr-values": { + schema: OPENID_ACR_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, + }, + autocreate: { + optional: true, + default: false, + }, + "username-claim": { + schema: OPENID_USERNAME_CLAIM_SCHEMA, + optional: true, + }, + }, +)] +#[derive(Serialize, Deserialize, Updater)] +#[serde(rename_all="kebab-case")] +/// OpenID configuration properties. +pub struct OpenIdRealmConfig { + #[updater(skip)] + pub realm: String, + /// OpenID Issuer Url + pub issuer_url: String, + /// OpenID Client ID + pub client_id: String, + #[serde(skip_serializing_if="Option::is_none")] + pub scopes: Option, + #[serde(skip_serializing_if="Option::is_none")] + pub acr_values: Option, + #[serde(skip_serializing_if="Option::is_none")] + pub prompt: Option, + /// OpenID Client Key + #[serde(skip_serializing_if="Option::is_none")] + pub client_key: Option, + #[serde(skip_serializing_if="Option::is_none")] + pub comment: Option, + /// Automatically create users if they do not exist. + #[serde(skip_serializing_if="Option::is_none")] + pub autocreate: Option, + #[updater(skip)] + #[serde(skip_serializing_if="Option::is_none")] + pub username_claim: Option, +} diff --git a/pbs-config/src/domains.rs b/pbs-config/src/domains.rs index 3d3f93b08..3a8921a6c 100644 --- a/pbs-config/src/domains.rs +++ b/pbs-config/src/domains.rs @@ -2,79 +2,17 @@ use std::collections::HashMap; use anyhow::{Error}; use lazy_static::lazy_static; -use serde::{Serialize, Deserialize}; -use proxmox_schema::{api, ApiType, Updater, Schema}; +use proxmox_schema::{ApiType, Schema}; use proxmox_section_config::{SectionConfig, SectionConfigData, SectionConfigPlugin}; -use pbs_api_types::{REALM_ID_SCHEMA, SINGLE_LINE_COMMENT_SCHEMA}; +use pbs_api_types::{OpenIdRealmConfig, REALM_ID_SCHEMA}; use crate::{open_backup_lockfile, replace_backup_config, BackupLockGuard}; 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: { - schema: REALM_ID_SCHEMA, - }, - "client-key": { - optional: true, - }, - comment: { - optional: true, - schema: SINGLE_LINE_COMMENT_SCHEMA, - }, - autocreate: { - optional: true, - default: false, - }, - "username-claim": { - type: OpenIdUserAttribute, - optional: true, - }, - }, -)] -#[derive(Serialize, Deserialize, Updater)] -#[serde(rename_all="kebab-case")] -/// OpenID configuration properties. -pub struct OpenIdRealmConfig { - #[updater(skip)] - pub realm: String, - /// OpenID Issuer Url - pub issuer_url: String, - /// OpenID Client ID - pub client_id: String, - /// OpenID Client Key - #[serde(skip_serializing_if="Option::is_none")] - pub client_key: Option, - #[serde(skip_serializing_if="Option::is_none")] - pub comment: Option, - /// Automatically create users if they do not exist. - #[serde(skip_serializing_if="Option::is_none")] - pub autocreate: Option, - #[updater(skip)] - #[serde(skip_serializing_if="Option::is_none")] - pub username_claim: Option, -} fn init() -> SectionConfig { let obj_schema = match OpenIdRealmConfig::API_SCHEMA { diff --git a/src/api2/access/openid.rs b/src/api2/access/openid.rs index df64e20d5..6424b47f8 100644 --- a/src/api2/access/openid.rs +++ b/src/api2/access/openid.rs @@ -11,12 +11,15 @@ use proxmox_router::{ }; use proxmox_schema::{api, parse_simple_value}; -use proxmox_openid::{OpenIdAuthenticator, OpenIdConfig}; +use proxmox_openid::{OpenIdAuthenticator, OpenIdConfig}; -use pbs_api_types::{User, Userid, EMAIL_SCHEMA, FIRST_NAME_SCHEMA, LAST_NAME_SCHEMA, REALM_ID_SCHEMA}; +use pbs_api_types::{ + OpenIdRealmConfig, User, Userid, + EMAIL_SCHEMA, FIRST_NAME_SCHEMA, LAST_NAME_SCHEMA, OPENID_DEFAILT_SCOPE_LIST, + REALM_ID_SCHEMA, +}; use pbs_buildcfg::PROXMOX_BACKUP_RUN_DIR_M; use pbs_tools::ticket::Ticket; -use pbs_config::domains::{OpenIdUserAttribute, OpenIdRealmConfig}; use pbs_config::CachedUserInfo; use pbs_config::open_backup_lockfile; @@ -25,15 +28,35 @@ use crate::auth_helpers::*; use crate::server::ticket::ApiTicket; fn openid_authenticator(realm_config: &OpenIdRealmConfig, redirect_url: &str) -> Result { + + let scopes: Vec = realm_config.scopes.as_deref().unwrap_or(OPENID_DEFAILT_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) } - #[api( input: { properties: { @@ -100,27 +123,33 @@ 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); } } }; - let user_id = Userid::try_from(format!("{}@{}", unique_name, realm))?; tested_username = Some(unique_name.to_string()); @@ -129,17 +158,14 @@ pub fn openid_login( use pbs_config::user; let _lock = open_backup_lockfile(user::USER_CFG_LOCKFILE, None, true)?; - let firstname = info.given_name().and_then(|n| n.get(None)) - .filter(|n| parse_simple_value(n, &FIRST_NAME_SCHEMA).is_ok()) - .map(|n| n.to_string()); + let firstname = info["given_name"].as_str().map(|n| n.to_string()) + .filter(|n| parse_simple_value(n, &FIRST_NAME_SCHEMA).is_ok()); - let lastname = info.family_name().and_then(|n| n.get(None)) - .filter(|n| parse_simple_value(n, &LAST_NAME_SCHEMA).is_ok()) - .map(|n| n.to_string()); + let lastname = info["family_name"].as_str().map(|n| n.to_string()) + .filter(|n| parse_simple_value(n, &LAST_NAME_SCHEMA).is_ok()); - let email = info.email() - .filter(|n| parse_simple_value(n, &EMAIL_SCHEMA).is_ok()) - .map(|e| e.to_string()); + let email = info["email"].as_str().map(|n| n.to_string()) + .filter(|n| parse_simple_value(n, &EMAIL_SCHEMA).is_ok()); let user = User { userid: user_id.clone(), diff --git a/src/api2/config/access/openid.rs b/src/api2/config/access/openid.rs index 027d5b91d..b5501f67c 100644 --- a/src/api2/config/access/openid.rs +++ b/src/api2/config/access/openid.rs @@ -8,9 +8,11 @@ use proxmox_router::{Router, RpcEnvironment, Permission}; use proxmox_schema::api; use pbs_api_types::{ + OpenIdRealmConfig, OpenIdRealmConfigUpdater, PROXMOX_CONFIG_DIGEST_SCHEMA, REALM_ID_SCHEMA, PRIV_SYS_AUDIT, PRIV_REALM_ALLOCATE, }; -use pbs_config::domains::{self, OpenIdRealmConfig, OpenIdRealmConfigUpdater}; + +use pbs_config::domains; #[api( input: { @@ -157,6 +159,12 @@ pub enum DeletableProperty { comment, /// Delete the autocreate property autocreate, + /// Delete the scopes property + scopes, + /// Delete the prompt property + prompt, + /// Delete the acr_values property + acr_values, } #[api( @@ -215,6 +223,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::acr_values => { config.acr_values = None; }, } } } @@ -233,6 +244,9 @@ pub fn update_openid_realm( 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)?;