openid: allow to configure scopes, prompt, ACRs and arbitrary username-claim values
- no longer set prompt to 'login' (makes auto-login possible) - new prompt configuration - allow arbitrary username-claim values Depend on proxmox-openid 0.9.0. Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
This commit is contained in:
parent
df32530750
commit
10beed1199
|
@ -109,7 +109,7 @@ proxmox-shared-memory = "0.1.1"
|
||||||
|
|
||||||
proxmox-acme-rs = "0.3"
|
proxmox-acme-rs = "0.3"
|
||||||
proxmox-apt = "0.8.0"
|
proxmox-apt = "0.8.0"
|
||||||
proxmox-openid = "0.8.1"
|
proxmox-openid = "0.9.0"
|
||||||
|
|
||||||
pbs-api-types = { path = "pbs-api-types" }
|
pbs-api-types = { path = "pbs-api-types" }
|
||||||
pbs-buildcfg = { path = "pbs-buildcfg" }
|
pbs-buildcfg = { path = "pbs-buildcfg" }
|
||||||
|
|
|
@ -68,6 +68,9 @@ pub use crypto::{CryptMode, Fingerprint, bytes_as_fingerprint};
|
||||||
|
|
||||||
pub mod file_restore;
|
pub mod file_restore;
|
||||||
|
|
||||||
|
mod openid;
|
||||||
|
pub use openid::*;
|
||||||
|
|
||||||
mod remote;
|
mod remote;
|
||||||
pub use remote::*;
|
pub use remote::*;
|
||||||
|
|
||||||
|
|
|
@ -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<String>,
|
||||||
|
#[serde(skip_serializing_if="Option::is_none")]
|
||||||
|
pub acr_values: Option<String>,
|
||||||
|
#[serde(skip_serializing_if="Option::is_none")]
|
||||||
|
pub prompt: Option<String>,
|
||||||
|
/// OpenID Client Key
|
||||||
|
#[serde(skip_serializing_if="Option::is_none")]
|
||||||
|
pub client_key: Option<String>,
|
||||||
|
#[serde(skip_serializing_if="Option::is_none")]
|
||||||
|
pub comment: Option<String>,
|
||||||
|
/// Automatically create users if they do not exist.
|
||||||
|
#[serde(skip_serializing_if="Option::is_none")]
|
||||||
|
pub autocreate: Option<bool>,
|
||||||
|
#[updater(skip)]
|
||||||
|
#[serde(skip_serializing_if="Option::is_none")]
|
||||||
|
pub username_claim: Option<String>,
|
||||||
|
}
|
|
@ -2,79 +2,17 @@ use std::collections::HashMap;
|
||||||
|
|
||||||
use anyhow::{Error};
|
use anyhow::{Error};
|
||||||
use lazy_static::lazy_static;
|
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 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};
|
use crate::{open_backup_lockfile, replace_backup_config, BackupLockGuard};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref CONFIG: SectionConfig = init();
|
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<String>,
|
|
||||||
#[serde(skip_serializing_if="Option::is_none")]
|
|
||||||
pub comment: Option<String>,
|
|
||||||
/// Automatically create users if they do not exist.
|
|
||||||
#[serde(skip_serializing_if="Option::is_none")]
|
|
||||||
pub autocreate: Option<bool>,
|
|
||||||
#[updater(skip)]
|
|
||||||
#[serde(skip_serializing_if="Option::is_none")]
|
|
||||||
pub username_claim: Option<OpenIdUserAttribute>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init() -> SectionConfig {
|
fn init() -> SectionConfig {
|
||||||
let obj_schema = match OpenIdRealmConfig::API_SCHEMA {
|
let obj_schema = match OpenIdRealmConfig::API_SCHEMA {
|
||||||
|
|
|
@ -11,12 +11,15 @@ use proxmox_router::{
|
||||||
};
|
};
|
||||||
use proxmox_schema::{api, parse_simple_value};
|
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_buildcfg::PROXMOX_BACKUP_RUN_DIR_M;
|
||||||
use pbs_tools::ticket::Ticket;
|
use pbs_tools::ticket::Ticket;
|
||||||
use pbs_config::domains::{OpenIdUserAttribute, OpenIdRealmConfig};
|
|
||||||
|
|
||||||
use pbs_config::CachedUserInfo;
|
use pbs_config::CachedUserInfo;
|
||||||
use pbs_config::open_backup_lockfile;
|
use pbs_config::open_backup_lockfile;
|
||||||
|
@ -25,15 +28,35 @@ use crate::auth_helpers::*;
|
||||||
use crate::server::ticket::ApiTicket;
|
use crate::server::ticket::ApiTicket;
|
||||||
|
|
||||||
fn openid_authenticator(realm_config: &OpenIdRealmConfig, redirect_url: &str) -> Result<OpenIdAuthenticator, Error> {
|
fn openid_authenticator(realm_config: &OpenIdRealmConfig, redirect_url: &str) -> Result<OpenIdAuthenticator, Error> {
|
||||||
|
|
||||||
|
let scopes: Vec<String> = 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 {
|
let config = OpenIdConfig {
|
||||||
issuer_url: realm_config.issuer_url.clone(),
|
issuer_url: realm_config.issuer_url.clone(),
|
||||||
client_id: realm_config.client_id.clone(),
|
client_id: realm_config.client_id.clone(),
|
||||||
client_key: realm_config.client_key.clone(),
|
client_key: realm_config.client_key.clone(),
|
||||||
|
prompt: realm_config.prompt.clone(),
|
||||||
|
scopes: Some(scopes),
|
||||||
|
acr_values,
|
||||||
};
|
};
|
||||||
OpenIdAuthenticator::discover(&config, redirect_url)
|
OpenIdAuthenticator::discover(&config, redirect_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[api(
|
#[api(
|
||||||
input: {
|
input: {
|
||||||
properties: {
|
properties: {
|
||||||
|
@ -100,27 +123,33 @@ pub fn openid_login(
|
||||||
|
|
||||||
let open_id = openid_authenticator(&config, &redirect_url)?;
|
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 {
|
let name_attr = config.username_claim.as_deref().unwrap_or("sub");
|
||||||
None | Some(OpenIdUserAttribute::Subject) => info.subject().as_str(),
|
|
||||||
Some(OpenIdUserAttribute::Username) => {
|
// Try to be compatible with previous versions
|
||||||
match info.preferred_username() {
|
let try_attr = match name_attr {
|
||||||
Some(name) => name.as_str(),
|
"subject" => Some("sub"),
|
||||||
None => bail!("missing claim 'preferred_name'"),
|
"username" => Some("preferred_username"),
|
||||||
}
|
_ => None,
|
||||||
}
|
};
|
||||||
Some(OpenIdUserAttribute::Email) => {
|
|
||||||
match info.email() {
|
let unique_name = match info[name_attr].as_str() {
|
||||||
Some(name) => name.as_str(),
|
Some(name) => name.to_owned(),
|
||||||
None => bail!("missing claim 'email'"),
|
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))?;
|
let user_id = Userid::try_from(format!("{}@{}", unique_name, realm))?;
|
||||||
tested_username = Some(unique_name.to_string());
|
tested_username = Some(unique_name.to_string());
|
||||||
|
|
||||||
|
@ -129,17 +158,14 @@ pub fn openid_login(
|
||||||
use pbs_config::user;
|
use pbs_config::user;
|
||||||
let _lock = open_backup_lockfile(user::USER_CFG_LOCKFILE, None, true)?;
|
let _lock = open_backup_lockfile(user::USER_CFG_LOCKFILE, None, true)?;
|
||||||
|
|
||||||
let firstname = info.given_name().and_then(|n| n.get(None))
|
let firstname = info["given_name"].as_str().map(|n| n.to_string())
|
||||||
.filter(|n| parse_simple_value(n, &FIRST_NAME_SCHEMA).is_ok())
|
.filter(|n| parse_simple_value(n, &FIRST_NAME_SCHEMA).is_ok());
|
||||||
.map(|n| n.to_string());
|
|
||||||
|
|
||||||
let lastname = info.family_name().and_then(|n| n.get(None))
|
let lastname = info["family_name"].as_str().map(|n| n.to_string())
|
||||||
.filter(|n| parse_simple_value(n, &LAST_NAME_SCHEMA).is_ok())
|
.filter(|n| parse_simple_value(n, &LAST_NAME_SCHEMA).is_ok());
|
||||||
.map(|n| n.to_string());
|
|
||||||
|
|
||||||
let email = info.email()
|
let email = info["email"].as_str().map(|n| n.to_string())
|
||||||
.filter(|n| parse_simple_value(n, &EMAIL_SCHEMA).is_ok())
|
.filter(|n| parse_simple_value(n, &EMAIL_SCHEMA).is_ok());
|
||||||
.map(|e| e.to_string());
|
|
||||||
|
|
||||||
let user = User {
|
let user = User {
|
||||||
userid: user_id.clone(),
|
userid: user_id.clone(),
|
||||||
|
|
|
@ -8,9 +8,11 @@ use proxmox_router::{Router, RpcEnvironment, Permission};
|
||||||
use proxmox_schema::api;
|
use proxmox_schema::api;
|
||||||
|
|
||||||
use pbs_api_types::{
|
use pbs_api_types::{
|
||||||
|
OpenIdRealmConfig, OpenIdRealmConfigUpdater,
|
||||||
PROXMOX_CONFIG_DIGEST_SCHEMA, REALM_ID_SCHEMA, PRIV_SYS_AUDIT, PRIV_REALM_ALLOCATE,
|
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(
|
#[api(
|
||||||
input: {
|
input: {
|
||||||
|
@ -157,6 +159,12 @@ pub enum DeletableProperty {
|
||||||
comment,
|
comment,
|
||||||
/// Delete the autocreate property
|
/// Delete the autocreate property
|
||||||
autocreate,
|
autocreate,
|
||||||
|
/// Delete the scopes property
|
||||||
|
scopes,
|
||||||
|
/// Delete the prompt property
|
||||||
|
prompt,
|
||||||
|
/// Delete the acr_values property
|
||||||
|
acr_values,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[api(
|
#[api(
|
||||||
|
@ -215,6 +223,9 @@ pub fn update_openid_realm(
|
||||||
DeletableProperty::client_key => { config.client_key = None; },
|
DeletableProperty::client_key => { config.client_key = None; },
|
||||||
DeletableProperty::comment => { config.comment = None; },
|
DeletableProperty::comment => { config.comment = None; },
|
||||||
DeletableProperty::autocreate => { config.autocreate = 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.client_key.is_some() { config.client_key = update.client_key; }
|
||||||
if update.autocreate.is_some() { config.autocreate = update.autocreate; }
|
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.set_data(&realm, "openid", &config)?;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue