* [pmg-devel] [PATCH pve-common/perl-rs/pmg-api/widget-toolkit/pmg-gui v6 0/12] fix #3892: OpenID Connect
@ 2025-02-25 13:36 Markus Frank
2025-02-25 13:36 ` [pmg-devel] [PATCH pve-common v6 1/12] add Schema package with auth module that contains realm sync options Markus Frank
` (12 more replies)
0 siblings, 13 replies; 19+ messages in thread
From: Markus Frank @ 2025-02-25 13:36 UTC (permalink / raw)
To: pmg-devel
Patch-series to enable OpenID Connect Login for PMG
apply/compile order:
pve-common:
1 add Schema package with auth module that contains realm sync options
proxmox-perl-rs:
2 move openid code from pve-rs to common
3 remove empty PMG::RS::OpenId package to avoid confusion
pmg-api:
4 config: add plugin system for authentication realms
5 config: add oidc type authentication realm
6 api: add/update/remove authentication realms like in PVE
7 api: oidc login similar to PVE
proxmox-widget-toolkit:
8 fix: window: AuthEditBase: rename variable 'realm' to 'type'
9 fix: panel: AuthView: change API path in pmx-domains model
10 form: RealmComboBox: add option to change the API path
pmg-gui:
11 login: add option to login with OIDC realm
12 add realms panel to user management
I still need to add the option to create users for other realms than PMG
in the API and WebUI. The autocreate option of the OIDC realm can be
used instead for now. Also the autocreate-role option needs to be
exposed to the WebUI. I will send these things as follow-up patches or
in the next iteration if this series cannot be applied yet.
v6:
* renamed Realm to AuthRealm and renamed every domain variable to realm
* changed realm API-path from access/domains to access/auth-realm
* more v6-changes described in the individual patches
v5:
* renamed openid/OpenId variables, filenames and modules to oidc/OIDC
wherever possible
* renamed Authdomains to Realm
v4:
* split "config: add plugin system for realms & add openid type realms"
patch into two patches
* use the name 'OpenId' for filenames, but use 'OIDC' as realm type name
* added autocreate-role option to set the role for automatically created
users in a realm, but currently not exposed in GUI (needs a lot of
changes in pmg-gui and proxmox-widget-toolkit)
pve-common:
Markus Frank (1):
add Schema package with auth module that contains realm sync options
src/Makefile | 2 ++
src/PVE/Schema/Auth.pm | 46 ++++++++++++++++++++++++++++++++++++++++++
2 files changed, 48 insertions(+)
create mode 100644 src/PVE/Schema/Auth.pm
proxmox-perl-rs:
Markus Frank (2):
move openid code from pve-rs to common
remove empty PMG::RS::OpenId package to avoid confusion
common/pkg/Makefile | 1 +
common/src/mod.rs | 1 +
common/src/oidc/mod.rs | 63 ++++++++++++++++++++++++++++++++++++++++
pmg-rs/Cargo.toml | 1 +
pmg-rs/Makefile | 1 -
pmg-rs/debian/control | 1 +
pve-rs/src/openid/mod.rs | 32 +++++---------------
7 files changed, 75 insertions(+), 25 deletions(-)
create mode 100644 common/src/oidc/mod.rs
pmg-api:
Markus Frank (4):
config: add plugin system for authentication realms
config: add oidc type authentication realm
api: add/update/remove authentication realms like in PVE
api: oidc login similar to PVE
src/Makefile | 6 +
src/PMG/API2/AccessControl.pm | 17 ++-
src/PMG/API2/AuthRealm.pm | 274 ++++++++++++++++++++++++++++++++++
src/PMG/API2/OIDC.pm | 243 ++++++++++++++++++++++++++++++
src/PMG/API2/Users.pm | 1 +
src/PMG/AccessControl.pm | 40 +++++
src/PMG/Auth/OIDC.pm | 101 +++++++++++++
src/PMG/Auth/PAM.pm | 22 +++
src/PMG/Auth/PMG.pm | 39 +++++
src/PMG/Auth/Plugin.pm | 203 +++++++++++++++++++++++++
src/PMG/HTTPServer.pm | 4 +-
src/PMG/RESTEnvironment.pm | 14 ++
src/PMG/UserConfig.pm | 24 ++-
src/PMG/Utils.pm | 29 +++-
14 files changed, 1001 insertions(+), 16 deletions(-)
create mode 100644 src/PMG/API2/AuthRealm.pm
create mode 100644 src/PMG/API2/OIDC.pm
create mode 100755 src/PMG/Auth/OIDC.pm
create mode 100755 src/PMG/Auth/PAM.pm
create mode 100755 src/PMG/Auth/PMG.pm
create mode 100755 src/PMG/Auth/Plugin.pm
widget-toolkit:
Markus Frank (3):
fix: window: AuthEditBase: rename variable 'realm' to 'type'
fix: panel: AuthView: change API path in pmx-domains model
form: RealmComboBox: add option to change the API path
src/form/RealmComboBox.js | 2 ++
src/panel/AuthView.js | 20 ++++++++++++--------
src/window/AuthEditBase.js | 4 ++--
3 files changed, 16 insertions(+), 10 deletions(-)
pmg-gui:
Markus Frank (2):
login: add option to login with OIDC realm
add realms panel to user management
js/LoginView.js | 209 ++++++++++++++++++++++++++++++++-----------
js/UserManagement.js | 7 ++
js/Utils.js | 16 ++++
3 files changed, 181 insertions(+), 51 deletions(-)
--
2.39.5
_______________________________________________
pmg-devel mailing list
pmg-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel
^ permalink raw reply [flat|nested] 19+ messages in thread
* [pmg-devel] [PATCH pve-common v6 1/12] add Schema package with auth module that contains realm sync options
2025-02-25 13:36 [pmg-devel] [PATCH pve-common/perl-rs/pmg-api/widget-toolkit/pmg-gui v6 0/12] fix #3892: OpenID Connect Markus Frank
@ 2025-02-25 13:36 ` Markus Frank
2025-02-25 17:24 ` Thomas Lamprecht
2025-02-25 13:36 ` [pmg-devel] [PATCH proxmox-perl-rs v6 2/12] move openid code from pve-rs to common Markus Frank
` (11 subsequent siblings)
12 siblings, 1 reply; 19+ messages in thread
From: Markus Frank @ 2025-02-25 13:36 UTC (permalink / raw)
To: pmg-devel
This is because these standard options & formats are used by both PVE
and PMG. Schema-definitions are based on:
pve-access-control/src/PVE/Auth/Plugin.pm
Signed-off-by: Markus Frank <m.frank@proxmox.com>
---
v6: removed schema-definitions only used by PVE
src/Makefile | 2 ++
src/PVE/Schema/Auth.pm | 46 ++++++++++++++++++++++++++++++++++++++++++
2 files changed, 48 insertions(+)
create mode 100644 src/PVE/Schema/Auth.pm
diff --git a/src/Makefile b/src/Makefile
index 2d8bdc4..833bbc1 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -29,6 +29,7 @@ LIB_SOURCES = \
RESTEnvironment.pm \
RESTHandler.pm \
SafeSyslog.pm \
+ Schema/Auth.pm \
SectionConfig.pm \
SysFSTools.pm \
Syscall.pm \
@@ -41,6 +42,7 @@ all:
install: $(addprefix PVE/,${LIB_SOURCES})
install -d -m 0755 ${DESTDIR}${PERLDIR}/PVE
install -d -m 0755 ${DESTDIR}${PERLDIR}/PVE/Job
+ install -d -m 0755 ${DESTDIR}${PERLDIR}/PVE/Schema
for i in ${LIB_SOURCES}; do install -D -m 0644 PVE/$$i ${DESTDIR}${PERLDIR}/PVE/$$i; done
diff --git a/src/PVE/Schema/Auth.pm b/src/PVE/Schema/Auth.pm
new file mode 100644
index 0000000..031301e
--- /dev/null
+++ b/src/PVE/Schema/Auth.pm
@@ -0,0 +1,46 @@
+package PVE::Schema::Auth;
+
+use strict;
+use warnings;
+
+use PVE::JSONSchema qw(parse_property_string);
+
+my $tfa_format = {
+ type => {
+ description => "The type of 2nd factor authentication.",
+ format_description => 'TFATYPE',
+ type => 'string',
+ enum => [qw(oath)],
+ },
+ digits => {
+ description => "TOTP digits.",
+ format_description => 'COUNT',
+ type => 'integer',
+ minimum => 6, maximum => 8,
+ default => 6,
+ optional => 1,
+ },
+ step => {
+ description => "TOTP time period.",
+ format_description => 'SECONDS',
+ type => 'integer',
+ minimum => 10,
+ default => 30,
+ optional => 1,
+ },
+};
+
+PVE::JSONSchema::register_format('pve-tfa-config', $tfa_format);
+
+PVE::JSONSchema::register_standard_option('tfa', {
+ description => "Use Two-factor authentication.",
+ type => 'string', format => 'pve-tfa-config',
+ optional => 1,
+ maxLength => 128,
+});
+
+sub parse_tfa_config {
+ my ($data) = @_;
+
+ return parse_property_string($tfa_format, $data);
+}
--
2.39.5
_______________________________________________
pmg-devel mailing list
pmg-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel
^ permalink raw reply [flat|nested] 19+ messages in thread
* [pmg-devel] [PATCH proxmox-perl-rs v6 2/12] move openid code from pve-rs to common
2025-02-25 13:36 [pmg-devel] [PATCH pve-common/perl-rs/pmg-api/widget-toolkit/pmg-gui v6 0/12] fix #3892: OpenID Connect Markus Frank
2025-02-25 13:36 ` [pmg-devel] [PATCH pve-common v6 1/12] add Schema package with auth module that contains realm sync options Markus Frank
@ 2025-02-25 13:36 ` Markus Frank
2025-02-25 13:36 ` [pmg-devel] [PATCH proxmox-perl-rs v6 3/12] remove empty PMG::RS::OpenId package to avoid confusion Markus Frank
` (10 subsequent siblings)
12 siblings, 0 replies; 19+ messages in thread
From: Markus Frank @ 2025-02-25 13:36 UTC (permalink / raw)
To: pmg-devel
Change pve-rs functions to be wrapper functions for common.
Signed-off-by: Markus Frank <m.frank@proxmox.com>
---
v6: nothing changed
common/pkg/Makefile | 1 +
common/src/mod.rs | 1 +
common/src/oidc/mod.rs | 63 ++++++++++++++++++++++++++++++++++++++++
pmg-rs/Cargo.toml | 1 +
pmg-rs/debian/control | 1 +
pve-rs/src/openid/mod.rs | 32 +++++---------------
6 files changed, 75 insertions(+), 24 deletions(-)
create mode 100644 common/src/oidc/mod.rs
diff --git a/common/pkg/Makefile b/common/pkg/Makefile
index cbefdf7..5a537f9 100644
--- a/common/pkg/Makefile
+++ b/common/pkg/Makefile
@@ -24,6 +24,7 @@ PERLMOD_PACKAGES := \
Proxmox::RS::APT::Repositories \
Proxmox::RS::CalendarEvent \
Proxmox::RS::Notify \
+ Proxmox::RS::OIDC \
Proxmox::RS::SharedCache \
Proxmox::RS::Subscription
diff --git a/common/src/mod.rs b/common/src/mod.rs
index badfc98..06a7c20 100644
--- a/common/src/mod.rs
+++ b/common/src/mod.rs
@@ -2,5 +2,6 @@ pub mod apt;
mod calendar_event;
pub mod logger;
pub mod notify;
+pub mod oidc;
pub mod shared_cache;
mod subscription;
diff --git a/common/src/oidc/mod.rs b/common/src/oidc/mod.rs
new file mode 100644
index 0000000..b1cddaa
--- /dev/null
+++ b/common/src/oidc/mod.rs
@@ -0,0 +1,63 @@
+#[perlmod::package(name = "Proxmox::RS::OIDC")]
+pub mod export {
+ use std::sync::Mutex;
+
+ use anyhow::Error;
+
+ use perlmod::{to_value, Value};
+
+ use proxmox_openid::{OpenIdAuthenticator, OpenIdConfig, PrivateAuthState};
+
+ perlmod::declare_magic!(Box<OIDC> : &OIDC as "Proxmox::RS::OIDC");
+
+ /// An OpenIdAuthenticator client instance.
+ pub struct OIDC {
+ inner: Mutex<OpenIdAuthenticator>,
+ }
+
+ /// Create a new OIDC client instance
+ #[export(raw_return)]
+ pub fn discover(
+ #[raw] class: Value,
+ config: OpenIdConfig,
+ redirect_url: &str,
+ ) -> Result<Value, Error> {
+ let oidc = OpenIdAuthenticator::discover(&config, redirect_url)?;
+ Ok(perlmod::instantiate_magic!(
+ &class,
+ MAGIC => Box::new(OIDC {
+ inner: Mutex::new(oidc),
+ })
+ ))
+ }
+
+ #[export]
+ pub fn authorize_url(
+ #[try_from_ref] this: &OIDC,
+ state_dir: &str,
+ realm: &str,
+ ) -> Result<String, Error> {
+ let oidc = this.inner.lock().unwrap();
+ oidc.authorize_url(state_dir, realm)
+ }
+
+ #[export]
+ pub fn verify_public_auth_state(
+ state_dir: &str,
+ state: &str,
+ ) -> Result<(String, PrivateAuthState), Error> {
+ OpenIdAuthenticator::verify_public_auth_state(state_dir, state)
+ }
+
+ #[export(raw_return)]
+ pub fn verify_authorization_code(
+ #[try_from_ref] this: &OIDC,
+ code: &str,
+ private_auth_state: PrivateAuthState,
+ ) -> Result<Value, Error> {
+ let oidc = this.inner.lock().unwrap();
+ let claims = oidc.verify_authorization_code_simple(code, &private_auth_state)?;
+
+ Ok(to_value(&claims)?)
+ }
+}
diff --git a/pmg-rs/Cargo.toml b/pmg-rs/Cargo.toml
index 1252671..ce715bf 100644
--- a/pmg-rs/Cargo.toml
+++ b/pmg-rs/Cargo.toml
@@ -42,3 +42,4 @@ proxmox-subscription = "0.5"
proxmox-sys = "0.6"
proxmox-tfa = { version = "5", features = ["api"] }
proxmox-time = "2"
+proxmox-openid = "0.10.0"
diff --git a/pmg-rs/debian/control b/pmg-rs/debian/control
index 295dcb3..afd8cbb 100644
--- a/pmg-rs/debian/control
+++ b/pmg-rs/debian/control
@@ -27,6 +27,7 @@ Build-Depends: cargo:native <!nocheck>,
librust-proxmox-http-error-0.1+default-dev,
librust-proxmox-log-0.2+default-dev,
librust-proxmox-notify-0.5+default-dev,
+ librust-proxmox-openid-0.10+default-dev,
librust-proxmox-shared-cache-0.1+default-dev,
librust-proxmox-subscription-0.5+default-dev,
librust-proxmox-sys-0.6+default-dev,
diff --git a/pve-rs/src/openid/mod.rs b/pve-rs/src/openid/mod.rs
index 1fa7572..2adb8bb 100644
--- a/pve-rs/src/openid/mod.rs
+++ b/pve-rs/src/openid/mod.rs
@@ -1,19 +1,13 @@
#[perlmod::package(name = "PVE::RS::OpenId", lib = "pve_rs")]
mod export {
- use std::sync::Mutex;
-
use anyhow::Error;
- use perlmod::{to_value, Value};
-
- use proxmox_openid::{OpenIdAuthenticator, OpenIdConfig, PrivateAuthState};
+ use perlmod::Value;
- perlmod::declare_magic!(Box<OpenId> : &OpenId as "PVE::RS::OpenId");
+ use proxmox_openid::{OpenIdConfig, PrivateAuthState};
- /// An OpenIdAuthenticator client instance.
- pub struct OpenId {
- inner: Mutex<OpenIdAuthenticator>,
- }
+ use crate::common::oidc::export as common;
+ use crate::common::oidc::export::OIDC as OpenId;
/// Create a new OpenId client instance
#[export(raw_return)]
@@ -22,13 +16,7 @@ mod export {
config: OpenIdConfig,
redirect_url: &str,
) -> Result<Value, Error> {
- let open_id = OpenIdAuthenticator::discover(&config, redirect_url)?;
- Ok(perlmod::instantiate_magic!(
- &class,
- MAGIC => Box::new(OpenId {
- inner: Mutex::new(open_id),
- })
- ))
+ common::discover(class, config, redirect_url)
}
#[export]
@@ -37,8 +25,7 @@ mod export {
state_dir: &str,
realm: &str,
) -> Result<String, Error> {
- let open_id = this.inner.lock().unwrap();
- open_id.authorize_url(state_dir, realm)
+ common::authorize_url(this, state_dir, realm)
}
#[export]
@@ -46,7 +33,7 @@ mod export {
state_dir: &str,
state: &str,
) -> Result<(String, PrivateAuthState), Error> {
- OpenIdAuthenticator::verify_public_auth_state(state_dir, state)
+ common::verify_public_auth_state(state_dir, state)
}
#[export(raw_return)]
@@ -55,9 +42,6 @@ mod export {
code: &str,
private_auth_state: PrivateAuthState,
) -> Result<Value, Error> {
- let open_id = this.inner.lock().unwrap();
- let claims = open_id.verify_authorization_code_simple(code, &private_auth_state)?;
-
- Ok(to_value(&claims)?)
+ common::verify_authorization_code(this, code, private_auth_state)
}
}
--
2.39.5
_______________________________________________
pmg-devel mailing list
pmg-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel
^ permalink raw reply [flat|nested] 19+ messages in thread
* [pmg-devel] [PATCH proxmox-perl-rs v6 3/12] remove empty PMG::RS::OpenId package to avoid confusion
2025-02-25 13:36 [pmg-devel] [PATCH pve-common/perl-rs/pmg-api/widget-toolkit/pmg-gui v6 0/12] fix #3892: OpenID Connect Markus Frank
2025-02-25 13:36 ` [pmg-devel] [PATCH pve-common v6 1/12] add Schema package with auth module that contains realm sync options Markus Frank
2025-02-25 13:36 ` [pmg-devel] [PATCH proxmox-perl-rs v6 2/12] move openid code from pve-rs to common Markus Frank
@ 2025-02-25 13:36 ` Markus Frank
2025-02-25 13:36 ` [pmg-devel] [PATCH pmg-api v6 4/12] config: add plugin system for authentication realms Markus Frank
` (9 subsequent siblings)
12 siblings, 0 replies; 19+ messages in thread
From: Markus Frank @ 2025-02-25 13:36 UTC (permalink / raw)
To: pmg-devel
The common Proxmox::RS:OpenId package can be used instead.
Signed-off-by: Markus Frank <m.frank@proxmox.com>
---
v6: nothing changed
pmg-rs/Makefile | 1 -
1 file changed, 1 deletion(-)
diff --git a/pmg-rs/Makefile b/pmg-rs/Makefile
index 073a284..0b48a37 100644
--- a/pmg-rs/Makefile
+++ b/pmg-rs/Makefile
@@ -29,7 +29,6 @@ PERLMOD_PACKAGES := \
PMG::RS::APT::Repositories \
PMG::RS::Acme \
PMG::RS::CSR \
- PMG::RS::OpenId \
PMG::RS::TFA
PERLMOD_PACKAGE_FILES := $(addsuffix .pm,$(subst ::,/,$(PERLMOD_PACKAGES)))
--
2.39.5
_______________________________________________
pmg-devel mailing list
pmg-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel
^ permalink raw reply [flat|nested] 19+ messages in thread
* [pmg-devel] [PATCH pmg-api v6 4/12] config: add plugin system for authentication realms
2025-02-25 13:36 [pmg-devel] [PATCH pve-common/perl-rs/pmg-api/widget-toolkit/pmg-gui v6 0/12] fix #3892: OpenID Connect Markus Frank
` (2 preceding siblings ...)
2025-02-25 13:36 ` [pmg-devel] [PATCH proxmox-perl-rs v6 3/12] remove empty PMG::RS::OpenId package to avoid confusion Markus Frank
@ 2025-02-25 13:36 ` Markus Frank
2025-02-25 16:32 ` Stoiko Ivanov
2025-02-25 13:36 ` [pmg-devel] [PATCH pmg-api v6 5/12] config: add oidc type authentication realm Markus Frank
` (8 subsequent siblings)
12 siblings, 1 reply; 19+ messages in thread
From: Markus Frank @ 2025-02-25 13:36 UTC (permalink / raw)
To: pmg-devel
To differentiate between usernames, the realm is also stored in the
user.conf file. Old config file syntax can be read, but will be
overwritten after a change.
This is a carryover from PVE. Previously there was no realm for a
username. Now the realm is also stored after an @ sign in user.conf.
Utils generates a list of valid realm names, including any newly added
realms, to ensure proper validation of a specified realm name.
Signed-off-by: Markus Frank <m.frank@proxmox.com>
---
v6:
* instead of appending @realm to the username, the realm is now saved
after the other properties in user.conf.
* authenticate user and store password inside plugin like it is in PVE
* renamed $domainconfigfile to $realm_cfg_id
src/Makefile | 3 +
src/PMG/API2/Users.pm | 1 +
src/PMG/AccessControl.pm | 38 +++++++
src/PMG/Auth/PAM.pm | 22 ++++
src/PMG/Auth/PMG.pm | 39 +++++++
src/PMG/Auth/Plugin.pm | 203 +++++++++++++++++++++++++++++++++++++
src/PMG/RESTEnvironment.pm | 14 +++
src/PMG/UserConfig.pm | 24 +++--
src/PMG/Utils.pm | 29 ++++--
9 files changed, 359 insertions(+), 14 deletions(-)
create mode 100755 src/PMG/Auth/PAM.pm
create mode 100755 src/PMG/Auth/PMG.pm
create mode 100755 src/PMG/Auth/Plugin.pm
diff --git a/src/Makefile b/src/Makefile
index 1232880..659a666 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -166,6 +166,9 @@ LIBSOURCES = \
PMG/API2/ACMEPlugin.pm \
PMG/API2/NodeConfig.pm \
PMG/API2.pm \
+ PMG/Auth/Plugin.pm \
+ PMG/Auth/PAM.pm \
+ PMG/Auth/PMG.pm \
SOURCES = ${LIBSOURCES} ${CLI_BINARIES} ${TEMPLATES_FILES} ${CONF_MANS} ${CLI_MANS} ${SERVICE_MANS} ${SERVICE_UNITS} ${TIMER_UNITS} pmg-sources.list pmg-initramfs.conf
diff --git a/src/PMG/API2/Users.pm b/src/PMG/API2/Users.pm
index 99af38d..42c82b7 100644
--- a/src/PMG/API2/Users.pm
+++ b/src/PMG/API2/Users.pm
@@ -124,6 +124,7 @@ __PACKAGE__->register_method ({
$entry->{enable} //= 0;
$entry->{expire} //= 0;
$entry->{role} //= 'audit';
+ $entry->{realm} //= 'pmg';
$cfg->{$param->{userid}} = $entry;
diff --git a/src/PMG/AccessControl.pm b/src/PMG/AccessControl.pm
index e12d7cf..78105c0 100644
--- a/src/PMG/AccessControl.pm
+++ b/src/PMG/AccessControl.pm
@@ -5,6 +5,7 @@ use warnings;
use Authen::PAM;
use PVE::Tools;
+use PVE::INotify;
use PVE::JSONSchema qw(get_standard_option);
use PVE::Exception qw(raise raise_perm_exc);
@@ -13,6 +14,14 @@ use PMG::LDAPConfig;
use PMG::LDAPSet;
use PMG::TFAConfig;
+use PMG::Auth::Plugin;
+use PMG::Auth::PAM;
+use PMG::Auth::PMG;
+
+PMG::Auth::PAM->register();
+PMG::Auth::PMG->register();
+PMG::Auth::Plugin->init();
+
sub normalize_path {
my $path = shift;
@@ -38,6 +47,7 @@ sub authenticate_user : prototype($$$) {
($username, $ruid, $realm) = PMG::Utils::verify_username($username);
+ my $realm_regex = PMG::Utils::valid_pmg_realm_regex();
if ($realm eq 'pam') {
die "invalid pam user (only root allowed)\n" if $ruid ne 'root';
authenticate_pam_user($ruid, $password);
@@ -53,6 +63,11 @@ sub authenticate_user : prototype($$$) {
return ($pmail . '@quarantine', undef);
}
die "ldap login failed\n";
+ } elsif ($realm =~ m!(${realm_regex})!) {
+ my $realm_cfg = PVE::INotify::read_file(PMG::Auth::Plugin->realm_cfg_id());
+ my $cfg = $realm_cfg->{ids}->{$realm};
+ my $plugin = PMG::Auth::Plugin->lookup($cfg->{type});
+ $plugin->authenticate_user($cfg, $realm, $ruid, $password);
} else {
die "no such realm '$realm'\n";
}
@@ -79,6 +94,7 @@ sub set_user_password {
($username, $ruid, $realm) = PMG::Utils::verify_username($username);
+ my $realm_regex = PMG::Utils::valid_pmg_realm_regex();
if ($realm eq 'pam') {
die "invalid pam user (only root allowed)\n" if $ruid ne 'root';
@@ -92,6 +108,11 @@ sub set_user_password {
} elsif ($realm eq 'pmg') {
PMG::UserConfig->set_user_password($username, $password);
+ } elsif ($realm =~ m!(${realm_regex})!) {
+ my $realm_cfg = PVE::INotify::read_file(PMG::Auth::Plugin->realm_cfg_id());
+ my $cfg = $realm_cfg->{ids}->{$realm};
+ my $plugin = PMG::Auth::Plugin->lookup($cfg->{type});
+ $plugin->store_password($cfg, $realm, $username, $password);
} else {
die "no such realm '$realm'\n";
}
@@ -106,6 +127,7 @@ sub check_user_enabled {
($username, $ruid, $realm) = PMG::Utils::verify_username($username, 1);
+ my $realm_regex = PMG::Utils::valid_pmg_realm_regex();
if ($realm && $ruid) {
if ($realm eq 'pam') {
return 'root' if $ruid eq 'root';
@@ -115,6 +137,10 @@ sub check_user_enabled {
return $data->{role} if $data && $data->{enable};
} elsif ($realm eq 'quarantine') {
return 'quser';
+ } elsif ($realm =~ m!(${realm_regex})!) {
+ my $usercfg = PMG::UserConfig->new();
+ my $data = $usercfg->lookup_user_data($username, $noerr);
+ return $data->{role} if $data && $data->{enable};
}
}
@@ -123,6 +149,18 @@ sub check_user_enabled {
return undef;
}
+sub check_user_exist {
+ my ($usercfg, $username, $noerr) = @_;
+
+ $username = PMG::Utils::verify_username($username, $noerr);
+ return undef if !$username;
+ return $usercfg->{$username} if $usercfg && $usercfg->{$username};
+
+ die "no such user ('$username')\n" if !$noerr;
+
+ return undef;
+}
+
sub authenticate_pam_user {
my ($username, $password) = @_;
diff --git a/src/PMG/Auth/PAM.pm b/src/PMG/Auth/PAM.pm
new file mode 100755
index 0000000..3ffd8a6
--- /dev/null
+++ b/src/PMG/Auth/PAM.pm
@@ -0,0 +1,22 @@
+package PMG::Auth::PAM;
+
+use strict;
+use warnings;
+
+use PMG::Auth::Plugin;
+
+use base qw(PMG::Auth::Plugin);
+
+sub type {
+ return 'pam';
+}
+
+sub options {
+ return {
+ default => { optional => 1 },
+ comment => { optional => 1 },
+ tfa => { optional => 1 },
+ };
+}
+
+1;
diff --git a/src/PMG/Auth/PMG.pm b/src/PMG/Auth/PMG.pm
new file mode 100755
index 0000000..b083692
--- /dev/null
+++ b/src/PMG/Auth/PMG.pm
@@ -0,0 +1,39 @@
+package PMG::Auth::PMG;
+
+use strict;
+use warnings;
+
+use PMG::Auth::Plugin;
+
+use base qw(PMG::Auth::Plugin);
+
+sub type {
+ return 'pmg';
+}
+
+sub properties {
+ return {
+ default => {
+ description => "Use this as default realm",
+ type => 'boolean',
+ optional => 1,
+ },
+ comment => {
+ description => "Description.",
+ type => 'string',
+ optional => 1,
+ maxLength => 4096,
+ },
+ tfa => PVE::JSONSchema::get_standard_option('tfa'),
+ };
+}
+
+sub options {
+ return {
+ default => { optional => 1 },
+ comment => { optional => 1 },
+ tfa => { optional => 1 },
+ };
+}
+
+1;
diff --git a/src/PMG/Auth/Plugin.pm b/src/PMG/Auth/Plugin.pm
new file mode 100755
index 0000000..8e8492b
--- /dev/null
+++ b/src/PMG/Auth/Plugin.pm
@@ -0,0 +1,203 @@
+package PMG::Auth::Plugin;
+
+use strict;
+use warnings;
+
+use Digest::SHA;
+use Encode;
+
+use PMG::Utils;
+use PVE::INotify;
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::Schema::Auth;
+use PVE::SectionConfig;
+use PVE::Tools;
+
+use base qw(PVE::SectionConfig);
+
+my $realm_cfg_id = "realms.cfg";
+my $lockfile = "/var/lock/pmg-realms.lck";
+
+sub realm_cfg_id {
+ return $realm_cfg_id;
+}
+
+sub read_realms_conf {
+ my ($filename, $fh) = @_;
+
+ my $raw;
+ $raw = do { local $/ = undef; <$fh> } if defined($fh);
+
+ return PMG::Auth::Plugin->parse_config($filename, $raw);
+}
+
+sub write_realms_conf {
+ my ($filename, $fh, $cfg) = @_;
+
+ my $raw = PMG::Auth::Plugin->write_config($filename, $cfg);
+
+ PVE::Tools::safe_print($filename, $fh, $raw);
+}
+
+PVE::INotify::register_file(
+ $realm_cfg_id,
+ "/etc/pmg/realms.cfg",
+ \&read_realms_conf,
+ \&write_realms_conf,
+ undef,
+ always_call_parser => 1,
+);
+
+sub lock_realm_config {
+ my ($code, $errmsg) = @_;
+
+ PVE::Tools::lock_file($lockfile, undef, $code);
+ if (my $err = $@) {
+ $errmsg ? die "$errmsg: $err" : die $err;
+ }
+}
+
+my $realm_regex = qr/[A-Za-z][A-Za-z0-9\.\-_]+/;
+
+sub pmg_verify_realm {
+ my ($realm, $noerr) = @_;
+
+ if ($realm !~ m/^${realm_regex}$/) {
+ return undef if $noerr;
+ die "value does not look like a valid realm\n";
+ }
+ return $realm;
+}
+
+my $defaultData = {
+ propertyList => {
+ type => { description => "Realm type." },
+ realm => get_standard_option('realm'),
+ },
+};
+
+sub private {
+ return $defaultData;
+}
+
+sub parse_section_header {
+ my ($class, $line) = @_;
+
+ if ($line =~ m/^(\S+):\s*(\S+)\s*$/) {
+ my ($type, $realm) = (lc($1), $2);
+ my $errmsg = undef; # set if you want to skip whole section
+ eval { pmg_verify_realm($realm); };
+ $errmsg = $@ if $@;
+ my $config = {}; # to return additional attributes
+ return ($type, $realm, $errmsg, $config);
+ }
+ return undef;
+}
+
+sub parse_config {
+ my ($class, $filename, $raw) = @_;
+
+ my $cfg = $class->SUPER::parse_config($filename, $raw);
+
+ my $default;
+ foreach my $realm (keys %{$cfg->{ids}}) {
+ my $data = $cfg->{ids}->{$realm};
+ # make sure there is only one default marker
+ if ($data->{default}) {
+ if ($default) {
+ delete $data->{default};
+ } else {
+ $default = $realm;
+ }
+ }
+
+ if ($data->{comment}) {
+ $data->{comment} = PVE::Tools::decode_text($data->{comment});
+ }
+
+ }
+
+ # add default realms
+ $cfg->{ids}->{pmg}->{type} = 'pmg'; # force type
+ $cfg->{ids}->{pmg}->{comment} = "Proxmox Mail Gateway authentication server"
+ if !$cfg->{ids}->{pmg}->{comment};
+ $cfg->{ids}->{pmg}->{default} = 1
+ if !$cfg->{ids}->{pmg}->{default};
+
+ $cfg->{ids}->{pam}->{type} = 'pam'; # force type
+ $cfg->{ids}->{pam}->{comment} = "Linux PAM standard authentication"
+ if !$cfg->{ids}->{pam}->{comment};
+
+ return $cfg;
+};
+
+sub write_config {
+ my ($class, $filename, $cfg) = @_;
+
+ foreach my $realm (keys %{$cfg->{ids}}) {
+ my $data = $cfg->{ids}->{$realm};
+ if ($data->{comment}) {
+ $data->{comment} = PVE::Tools::encode_text($data->{comment});
+ }
+ }
+
+ $class->SUPER::write_config($filename, $cfg);
+}
+
+sub authenticate_user {
+ my ($class, $config, $realm, $username, $password) = @_;
+
+ die "overwrite me";
+}
+
+sub store_password {
+ my ($class, $config, $realm, $username, $password) = @_;
+
+ my $type = $class->type();
+
+ die "can't set password on auth type '$type'\n";
+}
+
+sub delete_user {
+ my ($class, $config, $realm, $username) = @_;
+
+ # do nothing by default
+}
+
+# called during addition of realm (before the new realm config got written)
+# `password` is moved to %param to avoid writing it out to the config
+# die to abort addition if there are (grave) problems
+# NOTE: runs in a realm config *locked* context
+sub on_add_hook {
+ my ($class, $realm, $config, %param) = @_;
+ # do nothing by default
+}
+
+# called during realm configuration update (before the updated realm config got
+# written). `password` is moved to %param to avoid writing it out to the config
+# die to abort the update if there are (grave) problems
+# NOTE: runs in a realm config *locked* context
+sub on_update_hook {
+ my ($class, $realm, $config, %param) = @_;
+ # do nothing by default
+}
+
+# called during deletion of realms (before the new realm config got written)
+# and if the activate check on addition fails, to cleanup all storage traces
+# which on_add_hook may have created.
+# die to abort deletion if there are (very grave) problems
+# NOTE: runs in a realm config *locked* context
+sub on_delete_hook {
+ my ($class, $realm, $config) = @_;
+ # do nothing by default
+}
+
+# called during addition and updates of realms (before the new realm config gets written)
+# die to abort addition/update in case the connection/bind fails
+# NOTE: runs in a realm config *locked* context
+sub check_connection {
+ my ($class, $realm, $config, %param) = @_;
+ # do nothing by default
+}
+
+1;
diff --git a/src/PMG/RESTEnvironment.pm b/src/PMG/RESTEnvironment.pm
index 3875720..f6ff449 100644
--- a/src/PMG/RESTEnvironment.pm
+++ b/src/PMG/RESTEnvironment.pm
@@ -88,6 +88,20 @@ sub get_role {
return $self->{role};
}
+sub check_user_enabled {
+ my ($self, $user, $noerr) = @_;
+
+ my $cfg = $self->{usercfg};
+ return PMG::AccessControl::check_user_enabled($cfg, $user, $noerr);
+}
+
+sub check_user_exist {
+ my ($self, $user, $noerr) = @_;
+
+ my $cfg = $self->{usercfg};
+ return PMG::AccessControl::check_user_exist($cfg, $user, $noerr);
+}
+
sub check_node_is_master {
my ($self, $noerr);
diff --git a/src/PMG/UserConfig.pm b/src/PMG/UserConfig.pm
index b9a83a7..672f152 100644
--- a/src/PMG/UserConfig.pm
+++ b/src/PMG/UserConfig.pm
@@ -80,7 +80,7 @@ my $schema = {
realm => {
description => "Authentication realm.",
type => 'string',
- enum => ['pam', 'pmg'],
+ format => 'pmg-realm',
default => 'pmg',
optional => 1,
},
@@ -188,6 +188,7 @@ my $fixup_root_properties = sub {
$cfg->{'root@pam'}->{expire} = 0;
$cfg->{'root@pam'}->{comment} = 'Unix Superuser';
$cfg->{'root@pam'}->{role} = 'root';
+ $cfg->{'root@pam'}->{realm} = 'pam';
delete $cfg->{'root@pam'}->{crypt_pass};
};
@@ -217,26 +218,29 @@ sub read_user_conf {
(?<firstname>(?:[^:]*)) :
(?<lastname>(?:[^:]*)) :
(?<keys>(?:[^:]*)) :
+ ((?<realm>(?:[^:]*)) :)?
$/x
) {
+ my $default_realm = ($+{userid} eq 'root') ? 'pam' : 'pmg';
+ my $realm = $+{realm} || $default_realm;
my $d = {
username => $+{userid},
- userid => $+{userid} . '@pmg',
- realm => 'pmg',
+ userid => $+{userid} . '@' . $realm,
+ realm => $realm,
enable => $+{enable} || 0,
expire => $+{expire} || 0,
role => $+{role},
};
$d->{comment} = $comment if $comment;
$comment = '';
- foreach my $k (qw(crypt_pass email firstname lastname keys)) {
+ foreach my $k (qw(crypt_pass email firstname lastname keys realm)) {
$d->{$k} = $+{$k} if $+{$k};
}
eval {
$verify_entry->($d);
$cfg->{$d->{userid}} = $d;
die "role 'root' is reserved\n"
- if $d->{role} eq 'root' && $d->{userid} ne 'root@pmg';
+ if $d->{role} eq 'root' && $d->{userid} ne 'root@pam';
};
if (my $err = $@) {
warn "$filename: $err";
@@ -274,10 +278,11 @@ sub write_user_conf {
$verify_entry->($d);
$cfg->{$d->{userid}} = $d;
+ my $realm_regex = PMG::Utils::valid_pmg_realm_regex();
if ($d->{userid} ne 'root@pam') {
die "role 'root' is reserved\n" if $d->{role} eq 'root';
die "unable to add users for realm '$d->{realm}'\n"
- if $d->{realm} && $d->{realm} ne 'pmg';
+ if $d->{realm} && $d->{realm} !~ m!(${realm_regex})!;
}
my $line;
@@ -287,12 +292,15 @@ sub write_user_conf {
$d->{crypt_pass} = '',
$d->{expire} = '0',
$d->{role} = 'root';
+ $d->{realm} = 'pam';
} else {
- next if $userid !~ m/^(?<username>.+)\@pmg$/;
+ next if $userid !~ m/^(?<username>.+)\@(${realm_regex})$/;
$line = "$+{username}:";
}
+ # default to pmg
+ $d->{realm} //= 'pmg';
- for my $k (qw(enable expire crypt_pass role email firstname lastname keys)) {
+ for my $k (qw(enable expire crypt_pass role email firstname lastname keys realm)) {
$line .= ($d->{$k} // '') . ':';
}
if (my $comment = $d->{comment}) {
diff --git a/src/PMG/Utils.pm b/src/PMG/Utils.pm
index 0b8945f..04a3a2e 100644
--- a/src/PMG/Utils.pm
+++ b/src/PMG/Utils.pm
@@ -49,12 +49,30 @@ postgres_admin_cmd
try_decode_utf8
);
-my $valid_pmg_realms = ['pam', 'pmg', 'quarantine'];
+my $user_regex = qr![^\s:/]+!;
+
+sub valid_pmg_realm_regex {
+ my $cfg = PVE::INotify::read_file('realms.cfg');
+ my $ids = $cfg->{ids};
+ my $realms = ['pam', 'quarantine', sort keys $cfg->{ids}->%* ];
+ return join('|', @$realms);
+}
+
+sub is_valid_realm {
+ my ($realm) = @_;
+ return 0 if !$realm;
+ return 1 if $realm eq 'pam' || $realm eq 'quarantine'; # built-in ones
+
+ my $cfg = PVE::INotify::read_file('realms.cfg');
+ return exists($cfg->{ids}->{$realm}) ? 1 : 0;
+}
+
+PVE::JSONSchema::register_format('pmg-realm', \&is_valid_realm);
PVE::JSONSchema::register_standard_option('realm', {
description => "Authentication domain ID",
type => 'string',
- enum => $valid_pmg_realms,
+ format => 'pmg-realm',
maxLength => 32,
});
@@ -82,16 +100,15 @@ sub verify_username {
die "user name '$username' is too short\n" if !$noerr;
return undef;
}
- if ($len > 64) {
- die "user name '$username' is too long ($len > 64)\n" if !$noerr;
+ if ($len > 128) {
+ die "user name '$username' is too long ($len > 128)\n" if !$noerr;
return undef;
}
# we only allow a limited set of characters. Colons aren't allowed, because we store usernames
# with colon separated lists! slashes aren't allowed because it is used as pve API delimiter
# also see "man useradd"
- my $realm_list = join('|', @$valid_pmg_realms);
- if ($username =~ m!^([^\s:/]+)\@(${realm_list})$!) {
+ if ($username =~ m!^(${user_regex})\@([A-Za-z][A-Za-z0-9\.\-_]+)$!) {
return wantarray ? ($username, $1, $2) : $username;
}
--
2.39.5
_______________________________________________
pmg-devel mailing list
pmg-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel
^ permalink raw reply [flat|nested] 19+ messages in thread
* [pmg-devel] [PATCH pmg-api v6 5/12] config: add oidc type authentication realm
2025-02-25 13:36 [pmg-devel] [PATCH pve-common/perl-rs/pmg-api/widget-toolkit/pmg-gui v6 0/12] fix #3892: OpenID Connect Markus Frank
` (3 preceding siblings ...)
2025-02-25 13:36 ` [pmg-devel] [PATCH pmg-api v6 4/12] config: add plugin system for authentication realms Markus Frank
@ 2025-02-25 13:36 ` Markus Frank
2025-02-26 10:20 ` Mira Limbeck
2025-02-25 13:36 ` [pmg-devel] [PATCH pmg-api v6 6/12] api: add/update/remove authentication realms like in PVE Markus Frank
` (7 subsequent siblings)
12 siblings, 1 reply; 19+ messages in thread
From: Markus Frank @ 2025-02-25 13:36 UTC (permalink / raw)
To: pmg-devel
PMG::Auth::OIDC is based on pve-access-control's PVE::Auth::OpenId and
adds an autocreate-role option. If the autocreate option is enabled, the
user is automatically created with the Audit role. With autocreate-role
this role can be changed.
Signed-off-by: Markus Frank <m.frank@proxmox.com>
---
v6: added regex patterns to restrict the string types
src/Makefile | 1 +
src/PMG/AccessControl.pm | 2 +
src/PMG/Auth/OIDC.pm | 101 +++++++++++++++++++++++++++++++++++++++
3 files changed, 104 insertions(+)
create mode 100755 src/PMG/Auth/OIDC.pm
diff --git a/src/Makefile b/src/Makefile
index 659a666..3cae7c7 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -169,6 +169,7 @@ LIBSOURCES = \
PMG/Auth/Plugin.pm \
PMG/Auth/PAM.pm \
PMG/Auth/PMG.pm \
+ PMG/Auth/OIDC.pm \
SOURCES = ${LIBSOURCES} ${CLI_BINARIES} ${TEMPLATES_FILES} ${CONF_MANS} ${CLI_MANS} ${SERVICE_MANS} ${SERVICE_UNITS} ${TIMER_UNITS} pmg-sources.list pmg-initramfs.conf
diff --git a/src/PMG/AccessControl.pm b/src/PMG/AccessControl.pm
index 78105c0..57d80f8 100644
--- a/src/PMG/AccessControl.pm
+++ b/src/PMG/AccessControl.pm
@@ -15,9 +15,11 @@ use PMG::LDAPSet;
use PMG::TFAConfig;
use PMG::Auth::Plugin;
+use PMG::Auth::OIDC;
use PMG::Auth::PAM;
use PMG::Auth::PMG;
+PMG::Auth::OIDC->register();
PMG::Auth::PAM->register();
PMG::Auth::PMG->register();
PMG::Auth::Plugin->init();
diff --git a/src/PMG/Auth/OIDC.pm b/src/PMG/Auth/OIDC.pm
new file mode 100755
index 0000000..600e124
--- /dev/null
+++ b/src/PMG/Auth/OIDC.pm
@@ -0,0 +1,101 @@
+package PMG::Auth::OIDC;
+
+use strict;
+use warnings;
+
+use PVE::Tools;
+use PMG::Auth::Plugin;
+
+use base qw(PMG::Auth::Plugin);
+
+sub type {
+ return 'oidc';
+}
+
+sub properties {
+ return {
+ 'issuer-url' => {
+ description => "OpenID Connect Issuer Url",
+ type => 'string',
+ maxLength => 256,
+ pattern => qr/^(https?):\/\/([a-zA-Z0-9.-]+)(:[0-9]{1,5})?(\/[^\s]*)?$/,
+ },
+ 'client-id' => {
+ description => "OpenID Connect Client ID",
+ type => 'string',
+ maxLength => 256,
+ pattern => qr/^[a-zA-Z0-9._:-]+$/,
+ },
+ 'client-key' => {
+ description => "OpenID Connect Client Key",
+ type => 'string',
+ optional => 1,
+ maxLength => 256,
+ pattern => qr/^[a-zA-Z0-9._:-]+$/,
+ },
+ autocreate => {
+ description => "Automatically create users if they do not exist.",
+ optional => 1,
+ type => 'boolean',
+ default => 0,
+ },
+ 'autocreate-role' => {
+ description => "Automatically create users with a specific role.",
+ type => 'string',
+ enum => ['admin', 'qmanager', 'audit', 'helpdesk'],
+ optional => 1,
+ },
+ 'username-claim' => {
+ description => "OpenID Connect claim used to generate the unique username.",
+ type => 'string',
+ optional => 1,
+ pattern => qr/^[a-zA-Z0-9._:-]+$/,
+ },
+ prompt => {
+ description => "Specifies whether the Authorization Server prompts the End-User for"
+ ." reauthentication and consent.",
+ type => 'string',
+ pattern => '(?:none|login|consent|select_account|\S+)', # \S+ is the extension variant
+ optional => 1,
+ },
+ scopes => {
+ description => "Specifies the scopes (user details) that should be authorized and"
+ ." returned, for example 'email' or 'profile'.",
+ type => 'string', # format => 'some-safe-id-list', # FIXME: TODO
+ default => "email profile",
+ pattern => qr/^[a-zA-Z0-9._:-]+$/,
+ optional => 1,
+ },
+ 'acr-values' => {
+ description => "Specifies the Authentication Context Class Reference values that the"
+ ."Authorization Server is being requested to use for the Auth Request.",
+ type => 'string', # format => 'some-safe-id-list', # FIXME: TODO
+ pattern => qr/^[a-zA-Z0-9._:-]+$/,
+ optional => 1,
+ },
+ };
+}
+
+sub options {
+ return {
+ 'issuer-url' => {},
+ 'client-id' => {},
+ 'client-key' => { optional => 1 },
+ autocreate => { optional => 1 },
+ 'autocreate-role' => { optional => 1 },
+ 'username-claim' => { optional => 1, fixed => 1 },
+ prompt => { optional => 1 },
+ scopes => { optional => 1 },
+ 'acr-values' => { optional => 1 },
+ default => { optional => 1 },
+ comment => { optional => 1 },
+ };
+}
+
+sub authenticate_user {
+ my ($class, $config, $realm, $username, $password) = @_;
+
+ die "OpenID Connect realm does not allow password verification.\n";
+}
+
+1;
--
2.39.5
_______________________________________________
pmg-devel mailing list
pmg-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel
^ permalink raw reply [flat|nested] 19+ messages in thread
* [pmg-devel] [PATCH pmg-api v6 6/12] api: add/update/remove authentication realms like in PVE
2025-02-25 13:36 [pmg-devel] [PATCH pve-common/perl-rs/pmg-api/widget-toolkit/pmg-gui v6 0/12] fix #3892: OpenID Connect Markus Frank
` (4 preceding siblings ...)
2025-02-25 13:36 ` [pmg-devel] [PATCH pmg-api v6 5/12] config: add oidc type authentication realm Markus Frank
@ 2025-02-25 13:36 ` Markus Frank
2025-02-25 13:36 ` [pmg-devel] [PATCH pmg-api v6 7/12] api: oidc login similar to PVE Markus Frank
` (6 subsequent siblings)
12 siblings, 0 replies; 19+ messages in thread
From: Markus Frank @ 2025-02-25 13:36 UTC (permalink / raw)
To: pmg-devel
PMG::API2::AuthRealm is based on pve-access-control's PVE::API2::Domains.
The name AuthRealm.pm was chosen because a Domain.pm already exists.
Signed-off-by: Markus Frank <m.frank@proxmox.com>
---
v6:
* renamed api-path domains to auth-realm
* renamed Realm to AuthRealm
* used PMG::Auth::Plugin->realm_cfg_id() instead of $domainconfigfile
src/Makefile | 1 +
src/PMG/API2/AccessControl.pm | 10 +-
src/PMG/API2/AuthRealm.pm | 274 ++++++++++++++++++++++++++++++++++
src/PMG/HTTPServer.pm | 2 +-
4 files changed, 285 insertions(+), 2 deletions(-)
create mode 100644 src/PMG/API2/AuthRealm.pm
diff --git a/src/Makefile b/src/Makefile
index 3cae7c7..3d3c932 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -151,6 +151,7 @@ LIBSOURCES = \
PMG/API2/Postfix.pm \
PMG/API2/Quarantine.pm \
PMG/API2/AccessControl.pm \
+ PMG/API2/AuthRealm.pm \
PMG/API2/TFA.pm \
PMG/API2/TFAConfig.pm \
PMG/API2/ObjectGroupHelpers.pm \
diff --git a/src/PMG/API2/AccessControl.pm b/src/PMG/API2/AccessControl.pm
index e26ae70..95b28ee 100644
--- a/src/PMG/API2/AccessControl.pm
+++ b/src/PMG/API2/AccessControl.pm
@@ -12,6 +12,7 @@ use PVE::JSONSchema qw(get_standard_option);
use PMG::Utils;
use PMG::UserConfig;
use PMG::AccessControl;
+use PMG::API2::AuthRealm;
use PMG::API2::Users;
use PMG::API2::TFA;
use PMG::TFAConfig;
@@ -30,6 +31,11 @@ __PACKAGE__->register_method ({
path => 'tfa',
});
+__PACKAGE__->register_method ({
+ subclass => "PMG::API2::AuthRealm",
+ path => 'auth-realm',
+});
+
__PACKAGE__->register_method ({
name => 'index',
path => '',
@@ -57,6 +63,7 @@ __PACKAGE__->register_method ({
my $res = [
{ subdir => 'ticket' },
+ { subdir => 'auth-realm' },
{ subdir => 'password' },
{ subdir => 'users' },
];
@@ -248,7 +255,8 @@ __PACKAGE__->register_method ({
my $username = $param->{username};
- if ($username !~ m/\@(pam|pmg|quarantine)$/) {
+ my $realm_regex = PMG::Utils::valid_pmg_realm_regex();
+ if ($username !~ m/\@(${realm_regex})$/) {
my $realm = $param->{realm} // 'quarantine';
$username .= "\@$realm";
}
diff --git a/src/PMG/API2/AuthRealm.pm b/src/PMG/API2/AuthRealm.pm
new file mode 100644
index 0000000..013e5fd
--- /dev/null
+++ b/src/PMG/API2/AuthRealm.pm
@@ -0,0 +1,274 @@
+package PMG::API2::AuthRealm;
+
+use strict;
+use warnings;
+
+use PVE::Exception qw(raise_param_exc);
+use PVE::INotify;
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::RESTHandler;
+use PVE::SafeSyslog;
+use PVE::Tools qw(extract_param);
+
+use PMG::AccessControl;
+use PMG::Auth::Plugin;
+use PVE::Schema::Auth;
+
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method ({
+ name => 'index',
+ path => '',
+ method => 'GET',
+ description => "Authentication realm index.",
+ permissions => {
+ description => "Anyone can access that, because we need that list for the login box (before"
+ ." the user is authenticated).",
+ user => 'world',
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {},
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {
+ realm => { type => 'string' },
+ type => { type => 'string' },
+ tfa => {
+ description => "Two-factor authentication provider.",
+ type => 'string',
+ enum => [ 'yubico', 'oath' ],
+ optional => 1,
+ },
+ comment => {
+ description => "A comment. The GUI use this text when you select a"
+ ." authentication realm on the login window.",
+ type => 'string',
+ optional => 1,
+ },
+ },
+ },
+ links => [ { rel => 'child', href => "{realm}" } ],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $res = [];
+
+ my $cfg = PVE::INotify::read_file(PMG::Auth::Plugin->realm_cfg_id());
+ my $ids = $cfg->{ids};
+
+ for my $realm (keys %$ids) {
+ my $d = $ids->{$realm};
+ my $entry = { realm => $realm, type => $d->{type} };
+ $entry->{comment} = $d->{comment} if $d->{comment};
+ $entry->{default} = 1 if $d->{default};
+ if ($d->{tfa} && (my $tfa_cfg = PVE::Schema::Auth::parse_tfa_config($d->{tfa}))) {
+ $entry->{tfa} = $tfa_cfg->{type};
+ }
+ push @$res, $entry;
+ }
+
+ return $res;
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'create',
+ protected => 1,
+ path => '',
+ method => 'POST',
+ permissions => { check => [ 'admin' ] },
+ description => "Add an authentication server.",
+ parameters => PMG::Auth::Plugin->createSchema(0),
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ # always extract, add it with hook
+ my $password = extract_param($param, 'password');
+
+ PMG::Auth::Plugin::lock_realm_config(
+ sub {
+ my $cfg = PVE::INotify::read_file(PMG::Auth::Plugin->realm_cfg_id());
+ my $ids = $cfg->{ids};
+
+ my $realm = extract_param($param, 'realm');
+ PMG::Auth::Plugin::pmg_verify_realm($realm);
+ my $type = $param->{type};
+ my $check_connection = extract_param($param, 'check-connection');
+
+ die "authentication realm '$realm' already exists\n"
+ if $ids->{$realm};
+
+ die "unable to use reserved name '$realm'\n"
+ if ($realm eq 'pam' || $realm eq 'pmg');
+
+ die "unable to create builtin type '$type'\n"
+ if ($type eq 'pam' || $type eq 'pmg');
+
+ my $plugin = PMG::Auth::Plugin->lookup($type);
+ my $config = $plugin->check_config($realm, $param, 1, 1);
+
+ if ($config->{default}) {
+ for my $r (keys %$ids) {
+ delete $ids->{$r}->{default};
+ }
+ }
+
+ $ids->{$realm} = $config;
+
+ my $opts = $plugin->options();
+ if (defined($password) && !defined($opts->{password})) {
+ $password = undef;
+ warn "ignoring password parameter";
+ }
+ $plugin->on_add_hook($realm, $config, password => $password);
+
+ PVE::INotify::write_file(PMG::Auth::Plugin->realm_cfg_id(), $cfg);
+ },
+ "add auth server failed",
+ );
+ return undef;
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'update',
+ path => '{realm}',
+ method => 'PUT',
+ permissions => { check => [ 'admin' ] },
+ description => "Update authentication server settings.",
+ protected => 1,
+ parameters => PMG::Auth::Plugin->updateSchema(0),
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ # always extract, update in hook
+ my $password = extract_param($param, 'password');
+
+ PMG::Auth::Plugin::lock_realm_config(
+ sub {
+ my $cfg = PVE::INotify::read_file(PMG::Auth::Plugin->realm_cfg_id());
+ my $ids = $cfg->{ids};
+
+ my $digest = extract_param($param, 'digest');
+ PVE::SectionConfig::assert_if_modified($cfg, $digest);
+
+ my $realm = extract_param($param, 'realm');
+ my $type = $ids->{$realm}->{type};
+ my $check_connection = extract_param($param, 'check-connection');
+
+ die "authentication realm '$realm' does not exist\n"
+ if !$ids->{$realm};
+
+ my $delete_str = extract_param($param, 'delete');
+ die "no options specified\n"
+ if !$delete_str && !scalar(keys %$param) && !defined($password);
+
+ my $delete_pw = 0;
+ for my $opt (PVE::Tools::split_list($delete_str)) {
+ delete $ids->{$realm}->{$opt};
+ $delete_pw = 1 if $opt eq 'password';
+ }
+
+ my $plugin = PMG::Auth::Plugin->lookup($type);
+ my $config = $plugin->check_config($realm, $param, 0, 1);
+
+ if ($config->{default}) {
+ for my $r (keys %$ids) {
+ delete $ids->{$r}->{default};
+ }
+ }
+
+ for my $p (keys %$config) {
+ $ids->{$realm}->{$p} = $config->{$p};
+ }
+
+ my $opts = $plugin->options();
+ if ($delete_pw || defined($password)) {
+ $plugin->on_update_hook($realm, $config, password => $password);
+ } else {
+ $plugin->on_update_hook($realm, $config);
+ }
+
+ PVE::INotify::write_file(PMG::Auth::Plugin->realm_cfg_id(), $cfg);
+ },
+ "update auth server failed"
+ );
+ return undef;
+ }});
+
+# fixme: return format!
+__PACKAGE__->register_method ({
+ name => 'read',
+ path => '{realm}',
+ method => 'GET',
+ description => "Get auth server configuration.",
+ permissions => { check => [ 'admin', 'qmanager', 'audit' ] },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ realm => get_standard_option('realm'),
+ },
+ },
+ returns => {},
+ code => sub {
+ my ($param) = @_;
+
+ my $cfg = PVE::INotify::read_file(PMG::Auth::Plugin->realm_cfg_id());
+
+ my $realm = $param->{realm};
+
+ my $data = $cfg->{ids}->{$realm};
+ die "authentication realm '$realm' does not exist\n" if !$data;
+
+ my $type = $data->{type};
+
+ $data->{digest} = $cfg->{digest};
+
+ return $data;
+ }});
+
+
+__PACKAGE__->register_method ({
+ name => 'delete',
+ path => '{realm}',
+ method => 'DELETE',
+ permissions => { check => [ 'admin' ] },
+ description => "Delete an authentication server.",
+ protected => 1,
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ realm => get_standard_option('realm'),
+ }
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ PMG::Auth::Plugin::lock_realm_config(
+ sub {
+ my $cfg = PVE::INotify::read_file(PMG::Auth::Plugin->realm_cfg_id());
+ my $ids = $cfg->{ids};
+ my $realm = $param->{realm};
+
+ die "authentication realm '$realm' does not exist\n" if !$ids->{$realm};
+
+ my $plugin = PMG::Auth::Plugin->lookup($ids->{$realm}->{type});
+
+ $plugin->on_delete_hook($realm, $ids->{$realm});
+
+ delete $ids->{$realm};
+
+ PVE::INotify::write_file(PMG::Auth::Plugin->realm_cfg_id(), $cfg);
+ },
+ "delete auth server failed",
+ );
+ return undef;
+ }});
+
+1;
diff --git a/src/PMG/HTTPServer.pm b/src/PMG/HTTPServer.pm
index 49724fe..5e0ce3d 100644
--- a/src/PMG/HTTPServer.pm
+++ b/src/PMG/HTTPServer.pm
@@ -57,7 +57,7 @@ sub auth_handler {
my $require_auth = 1;
# explicitly allow some calls without auth
- if (($rel_uri eq '/access/domains' && $method eq 'GET') ||
+ if (($rel_uri eq '/access/auth-realm' && $method eq 'GET') ||
($rel_uri eq '/quarantine/sendlink' && ($method eq 'GET' || $method eq 'POST')) ||
($rel_uri eq '/access/ticket' && ($method eq 'GET' || $method eq 'POST'))) {
$require_auth = 0;
--
2.39.5
_______________________________________________
pmg-devel mailing list
pmg-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel
^ permalink raw reply [flat|nested] 19+ messages in thread
* [pmg-devel] [PATCH pmg-api v6 7/12] api: oidc login similar to PVE
2025-02-25 13:36 [pmg-devel] [PATCH pve-common/perl-rs/pmg-api/widget-toolkit/pmg-gui v6 0/12] fix #3892: OpenID Connect Markus Frank
` (5 preceding siblings ...)
2025-02-25 13:36 ` [pmg-devel] [PATCH pmg-api v6 6/12] api: add/update/remove authentication realms like in PVE Markus Frank
@ 2025-02-25 13:36 ` Markus Frank
2025-02-25 13:36 ` [pmg-devel] [PATCH widget-toolkit v6 08/12] fix: window: AuthEditBase: rename variable 'realm' to 'type' Markus Frank
` (5 subsequent siblings)
12 siblings, 0 replies; 19+ messages in thread
From: Markus Frank @ 2025-02-25 13:36 UTC (permalink / raw)
To: pmg-devel
Allow OpenID Connect login using the Rust OIDC module.
PMG::API2::OIDC is based on pve-access-control's PVE::API2::OpenId
Signed-off-by: Markus Frank <m.frank@proxmox.com>
---
v6:
* do not require a email address while autocreate
* fixed 'redundant argument in sprintf' error in syslog
src/Makefile | 1 +
src/PMG/API2/AccessControl.pm | 7 +
src/PMG/API2/OIDC.pm | 243 ++++++++++++++++++++++++++++++++++
src/PMG/HTTPServer.pm | 2 +
4 files changed, 253 insertions(+)
create mode 100644 src/PMG/API2/OIDC.pm
diff --git a/src/Makefile b/src/Makefile
index 3d3c932..6107123 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -152,6 +152,7 @@ LIBSOURCES = \
PMG/API2/Quarantine.pm \
PMG/API2/AccessControl.pm \
PMG/API2/AuthRealm.pm \
+ PMG/API2/OIDC.pm \
PMG/API2/TFA.pm \
PMG/API2/TFAConfig.pm \
PMG/API2/ObjectGroupHelpers.pm \
diff --git a/src/PMG/API2/AccessControl.pm b/src/PMG/API2/AccessControl.pm
index 95b28ee..cf66c80 100644
--- a/src/PMG/API2/AccessControl.pm
+++ b/src/PMG/API2/AccessControl.pm
@@ -13,6 +13,7 @@ use PMG::Utils;
use PMG::UserConfig;
use PMG::AccessControl;
use PMG::API2::AuthRealm;
+use PMG::API2::OIDC;
use PMG::API2::Users;
use PMG::API2::TFA;
use PMG::TFAConfig;
@@ -36,6 +37,11 @@ __PACKAGE__->register_method ({
path => 'auth-realm',
});
+__PACKAGE__->register_method ({
+ subclass => "PMG::API2::OIDC",
+ path => 'oidc',
+});
+
__PACKAGE__->register_method ({
name => 'index',
path => '',
@@ -64,6 +70,7 @@ __PACKAGE__->register_method ({
my $res = [
{ subdir => 'ticket' },
{ subdir => 'auth-realm' },
+ { subdir => 'oidc' },
{ subdir => 'password' },
{ subdir => 'users' },
];
diff --git a/src/PMG/API2/OIDC.pm b/src/PMG/API2/OIDC.pm
new file mode 100644
index 0000000..da9c774
--- /dev/null
+++ b/src/PMG/API2/OIDC.pm
@@ -0,0 +1,243 @@
+package PMG::API2::OIDC;
+
+use strict;
+use warnings;
+
+use PVE::Tools qw(extract_param lock_file);
+use Proxmox::RS::OIDC;
+
+use PVE::Exception qw(raise raise_perm_exc raise_param_exc);
+use PVE::SafeSyslog;
+use PVE::INotify;
+use PVE::JSONSchema qw(get_standard_option);
+
+use PMG::AccessControl;
+use PMG::RESTEnvironment;
+use PVE::RESTHandler;
+
+use base qw(PVE::RESTHandler);
+
+my $oidc_state_path = "/var/lib/pmg";
+
+my $lookup_oidc_auth = sub {
+ my ($realm, $redirect_url) = @_;
+
+ my $cfg = PVE::INotify::read_file('realms.cfg');
+ my $ids = $cfg->{ids};
+
+ die "authentication domain '$realm' does not exist\n" if !$ids->{$realm};
+
+ my $config = $ids->{$realm};
+ die "wrong realm type ($config->{type} != oidc)\n" if $config->{type} ne "oidc";
+
+ my $oidc_config = {
+ issuer_url => $config->{'issuer-url'},
+ client_id => $config->{'client-id'},
+ client_key => $config->{'client-key'},
+ };
+ $oidc_config->{prompt} = $config->{'prompt'} if defined($config->{'prompt'});
+
+ my $scopes = $config->{'scopes'} // 'email profile';
+ $oidc_config->{scopes} = [ PVE::Tools::split_list($scopes) ];
+
+ if (defined(my $acr = $config->{'acr-values'})) {
+ $oidc_config->{acr_values} = [ PVE::Tools::split_list($acr) ];
+ }
+
+ my $oidc = Proxmox::RS::OIDC->discover($oidc_config, $redirect_url);
+ return ($config, $oidc);
+};
+
+__PACKAGE__->register_method ({
+ name => 'index',
+ path => '',
+ method => 'GET',
+ description => "Directory index.",
+ permissions => {
+ user => 'all',
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {},
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {
+ subdir => { type => 'string' },
+ },
+ },
+ links => [ { rel => 'child', href => "{subdir}" } ],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ return [
+ { subdir => 'auth-url' },
+ { subdir => 'login' },
+ ];
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'auth_url',
+ path => 'auth-url',
+ method => 'POST',
+ protected => 1,
+ description => "Get the OpenId Connect Authorization Url for the specified realm.",
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ realm => {
+ description => "Authentication domain ID",
+ type => 'string',
+ pattern => qr/[A-Za-z][A-Za-z0-9\.\-_]+/,
+ maxLength => 32,
+ },
+ 'redirect-url' => {
+ description => "Redirection Url. The client should set this to the used server url (location.origin).",
+ type => 'string',
+ maxLength => 255,
+ },
+ },
+ },
+ returns => {
+ type => "string",
+ description => "Redirection URL.",
+ },
+ permissions => { user => 'world' },
+ code => sub {
+ my ($param) = @_;
+
+ my $realm = extract_param($param, 'realm');
+ my $redirect_url = extract_param($param, 'redirect-url');
+
+ my ($config, $oidc) = $lookup_oidc_auth->($realm, $redirect_url);
+ my $url = $oidc->authorize_url($oidc_state_path , $realm);
+
+ return $url;
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'login',
+ path => 'login',
+ method => 'POST',
+ protected => 1,
+ description => " Verify OpenID Connect authorization code and create a ticket.",
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ 'state' => {
+ description => "OpenId Connect state.",
+ type => 'string',
+ maxLength => 1024,
+ },
+ code => {
+ description => "OpenId Connect authorization code.",
+ type => 'string',
+ maxLength => 4096,
+ },
+ 'redirect-url' => {
+ description => "Redirection Url. The client should set this to the used server url (location.origin).",
+ type => 'string',
+ maxLength => 255,
+ },
+ },
+ },
+ returns => {
+ properties => {
+ role => { type => 'string', optional => 1},
+ username => { type => 'string' },
+ ticket => { type => 'string' },
+ CSRFPreventionToken => { type => 'string' },
+ },
+ },
+ permissions => { user => 'world' },
+ code => sub {
+ my ($param) = @_;
+
+ my $rpcenv = PMG::RESTEnvironment->get();
+
+ my $res;
+ eval {
+ my ($realm, $private_auth_state) = Proxmox::RS::OIDC::verify_public_auth_state(
+ $oidc_state_path, $param->{'state'});
+
+ my $redirect_url = extract_param($param, 'redirect-url');
+
+ my ($config, $oidc) = $lookup_oidc_auth->($realm, $redirect_url);
+
+ my $info = $oidc->verify_authorization_code($param->{code}, $private_auth_state);
+ my $subject = $info->{'sub'};
+
+ my $unique_name;
+
+ my $user_attr = $config->{'username-claim'} // 'sub';
+ if (defined($info->{$user_attr})) {
+ $unique_name = $info->{$user_attr};
+ } elsif ($user_attr eq 'subject') { # stay compat with old versions
+ $unique_name = $subject;
+ } elsif ($user_attr eq 'username') { # stay compat with old versions
+ my $username = $info->{'preferred_username'};
+ die "missing claim 'preferred_username'\n" if !defined($username);
+ $unique_name = $username;
+ } else {
+ # neither the attr nor fallback are defined in info..
+ die "missing configured claim '$user_attr' in returned info object\n";
+ }
+
+ my $username = "${unique_name}\@${realm}";
+ # first, check if $username respects our naming conventions
+ PMG::Utils::verify_username($username);
+ if ($config->{'autocreate'} && !$rpcenv->check_user_exist($username, 1)) {
+ my $code = sub {
+ my $usercfg = PMG::UserConfig->new();
+
+ my $entry = { enable => 1 };
+ if (my $email = $info->{'email'}) {
+ $entry->{email} = $email;
+ }
+ if (defined(my $given_name = $info->{'given_name'})) {
+ $entry->{firstname} = $given_name;
+ }
+ if (defined(my $family_name = $info->{'family_name'})) {
+ $entry->{lastname} = $family_name;
+ }
+ $entry->{role} = $config->{'autocreate-role'} // 'audit';
+ $entry->{userid} = $username;
+ $entry->{username} = $unique_name;
+ $entry->{realm} = $realm;
+
+ die "User '$username' already exists\n"
+ if $usercfg->{$username};
+
+ $usercfg->{$username} = $entry;
+
+ $usercfg->write();
+ };
+ PMG::UserConfig::lock_config($code, "autocreate openid connect user failed");
+ }
+ my $role = $rpcenv->check_user_enabled($username);
+
+ my $ticket = PMG::Ticket::assemble_ticket($username);
+ my $csrftoken = PMG::Ticket::assemble_csrf_prevention_token($username);
+
+ $res = {
+ ticket => $ticket,
+ username => $username,
+ CSRFPreventionToken => $csrftoken,
+ role => $role,
+ };
+
+ };
+ if (my $err = $@) {
+ my $clientip = $rpcenv->get_client_ip() || '';
+ syslog('err', "openid connect authentication failure; rhost=$clientip msg=$err");
+ # do not return any info to prevent user enumeration attacks
+ die PVE::Exception->new("authentication failure $err\n", code => 401);
+ }
+
+ syslog('info', "successful openid connect auth for user '$res->{username}'");
+
+ return $res;
+ }});
diff --git a/src/PMG/HTTPServer.pm b/src/PMG/HTTPServer.pm
index 5e0ce3d..49675cb 100644
--- a/src/PMG/HTTPServer.pm
+++ b/src/PMG/HTTPServer.pm
@@ -58,6 +58,8 @@ sub auth_handler {
# explicitly allow some calls without auth
if (($rel_uri eq '/access/auth-realm' && $method eq 'GET') ||
+ ($rel_uri eq '/access/oidc/login' && $method eq 'POST') ||
+ ($rel_uri eq '/access/oidc/auth-url' && $method eq 'POST') ||
($rel_uri eq '/quarantine/sendlink' && ($method eq 'GET' || $method eq 'POST')) ||
($rel_uri eq '/access/ticket' && ($method eq 'GET' || $method eq 'POST'))) {
$require_auth = 0;
--
2.39.5
_______________________________________________
pmg-devel mailing list
pmg-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel
^ permalink raw reply [flat|nested] 19+ messages in thread
* [pmg-devel] [PATCH widget-toolkit v6 08/12] fix: window: AuthEditBase: rename variable 'realm' to 'type'
2025-02-25 13:36 [pmg-devel] [PATCH pve-common/perl-rs/pmg-api/widget-toolkit/pmg-gui v6 0/12] fix #3892: OpenID Connect Markus Frank
` (6 preceding siblings ...)
2025-02-25 13:36 ` [pmg-devel] [PATCH pmg-api v6 7/12] api: oidc login similar to PVE Markus Frank
@ 2025-02-25 13:36 ` Markus Frank
2025-02-25 13:36 ` [pmg-devel] [PATCH widget-toolkit v6 09/12] fix: panel: AuthView: change API path in pmx-domains model Markus Frank
` (4 subsequent siblings)
12 siblings, 0 replies; 19+ messages in thread
From: Markus Frank @ 2025-02-25 13:36 UTC (permalink / raw)
To: pmg-devel
PVE/PMG API returns a variable called 'type' instead of 'realm'
Fixes: 3822a031ddbe4136aa847476f2e3785934a41547
Signed-off-by: Markus Frank <m.frank@proxmox.com>
---
v6: nothing changed
src/window/AuthEditBase.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/window/AuthEditBase.js b/src/window/AuthEditBase.js
index 73c1fee..e044235 100644
--- a/src/window/AuthEditBase.js
+++ b/src/window/AuthEditBase.js
@@ -91,9 +91,9 @@ Ext.define('Proxmox.window.AuthEditBase', {
var data = response.result.data || {};
// just to be sure (should not happen)
// only check this when the type is not in the api path
- if (!me.useTypeInUrl && data.realm !== me.authType) {
+ if (!me.useTypeInUrl && data.type !== me.authType) {
me.close();
- throw `got wrong auth type '${me.authType}' for realm '${data.realm}'`;
+ throw `got wrong auth type '${me.authType}' for realm '${data.type}'`;
}
me.setValues(data);
},
--
2.39.5
_______________________________________________
pmg-devel mailing list
pmg-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel
^ permalink raw reply [flat|nested] 19+ messages in thread
* [pmg-devel] [PATCH widget-toolkit v6 09/12] fix: panel: AuthView: change API path in pmx-domains model
2025-02-25 13:36 [pmg-devel] [PATCH pve-common/perl-rs/pmg-api/widget-toolkit/pmg-gui v6 0/12] fix #3892: OpenID Connect Markus Frank
` (7 preceding siblings ...)
2025-02-25 13:36 ` [pmg-devel] [PATCH widget-toolkit v6 08/12] fix: window: AuthEditBase: rename variable 'realm' to 'type' Markus Frank
@ 2025-02-25 13:36 ` Markus Frank
2025-02-25 17:33 ` Markus Frank
2025-02-25 13:36 ` [pmg-devel] [PATCH widget-toolkit v6 10/12] form: RealmComboBox: add option to change the API path Markus Frank
` (3 subsequent siblings)
12 siblings, 1 reply; 19+ messages in thread
From: Markus Frank @ 2025-02-25 13:36 UTC (permalink / raw)
To: pmg-devel
Currently it always uses the default path because it was hardcoded into
the 'pmx-domains' model.
Fixes: 8d4faa88212757f274cc177c906af09d4ebe07bb
Signed-off-by: Markus Frank <m.frank@proxmox.com>
Co-authored-by: Dominik Csapak <d.csapak@proxmox.com>
---
This patch is new to v6. Required to change the API path to
/access/auth-realm
src/panel/AuthView.js | 20 ++++++++++++--------
1 file changed, 12 insertions(+), 8 deletions(-)
diff --git a/src/panel/AuthView.js b/src/panel/AuthView.js
index f607532..0e1718e 100644
--- a/src/panel/AuthView.js
+++ b/src/panel/AuthView.js
@@ -47,14 +47,6 @@ Ext.define('Proxmox.panel.AuthView', {
},
],
- store: {
- model: 'pmx-domains',
- sorters: {
- property: 'realm',
- direction: 'ASC',
- },
- },
-
openEditWindow: function(authType, realm) {
let me = this;
const { useTypeInUrl, onlineHelp } = Proxmox.Schema.authDomains[authType];
@@ -111,6 +103,18 @@ Ext.define('Proxmox.panel.AuthView', {
initComponent: function() {
var me = this;
+ me.store = {
+ model: 'pmx-domains',
+ sorters: {
+ property: 'realm',
+ direction: 'ASC',
+ },
+ proxy: {
+ type: 'proxmox',
+ url: `/api2/json${me.baseUrl}`,
+ },
+ };
+
let menuitems = [];
for (const [authType, config] of Object.entries(Proxmox.Schema.authDomains).sort()) {
if (!config.add) { continue; }
--
2.39.5
_______________________________________________
pmg-devel mailing list
pmg-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel
^ permalink raw reply [flat|nested] 19+ messages in thread
* [pmg-devel] [PATCH widget-toolkit v6 10/12] form: RealmComboBox: add option to change the API path
2025-02-25 13:36 [pmg-devel] [PATCH pve-common/perl-rs/pmg-api/widget-toolkit/pmg-gui v6 0/12] fix #3892: OpenID Connect Markus Frank
` (8 preceding siblings ...)
2025-02-25 13:36 ` [pmg-devel] [PATCH widget-toolkit v6 09/12] fix: panel: AuthView: change API path in pmx-domains model Markus Frank
@ 2025-02-25 13:36 ` Markus Frank
2025-02-25 13:36 ` [pmg-devel] [PATCH pmg-gui v6 11/12] login: add option to login with OIDC realm Markus Frank
` (2 subsequent siblings)
12 siblings, 0 replies; 19+ messages in thread
From: Markus Frank @ 2025-02-25 13:36 UTC (permalink / raw)
To: pmg-devel
Signed-off-by: Markus Frank <m.frank@proxmox.com>
---
This patch is new to v6. Required to change the API path to
/access/auth-realm
src/form/RealmComboBox.js | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/form/RealmComboBox.js b/src/form/RealmComboBox.js
index c57b52d..fbd042f 100644
--- a/src/form/RealmComboBox.js
+++ b/src/form/RealmComboBox.js
@@ -7,6 +7,7 @@ Ext.define('Proxmox.form.RealmComboBox', {
init: function(view) {
let store = view.getStore();
+ store.proxy.url = `/api2/json${view.baseUrl}`;
if (view.storeFilter) {
store.setFilters(view.storeFilter);
}
@@ -45,6 +46,7 @@ Ext.define('Proxmox.form.RealmComboBox', {
triggerAction: 'all',
valueField: 'realm',
displayField: 'descr',
+ baseUrl: '/access/domains',
getState: function() {
return { value: this.getValue() };
},
--
2.39.5
_______________________________________________
pmg-devel mailing list
pmg-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel
^ permalink raw reply [flat|nested] 19+ messages in thread
* [pmg-devel] [PATCH pmg-gui v6 11/12] login: add option to login with OIDC realm
2025-02-25 13:36 [pmg-devel] [PATCH pve-common/perl-rs/pmg-api/widget-toolkit/pmg-gui v6 0/12] fix #3892: OpenID Connect Markus Frank
` (9 preceding siblings ...)
2025-02-25 13:36 ` [pmg-devel] [PATCH widget-toolkit v6 10/12] form: RealmComboBox: add option to change the API path Markus Frank
@ 2025-02-25 13:36 ` Markus Frank
2025-02-25 13:36 ` [pmg-devel] [PATCH pmg-gui v6 12/12] add realms panel to user management Markus Frank
2025-02-26 11:08 ` [pmg-devel] [PATCH pve-common/perl-rs/pmg-api/widget-toolkit/pmg-gui v6 0/12] fix #3892: OpenID Connect Mira Limbeck
12 siblings, 0 replies; 19+ messages in thread
From: Markus Frank @ 2025-02-25 13:36 UTC (permalink / raw)
To: pmg-devel
By adding a viewModel with an oidc variable, the username & password
fields are disabled/hidden when an OIDC realm is selected.
Signed-off-by: Markus Frank <m.frank@proxmox.com>
---
v6: changed realm baseUrl to '/access/auth-realm'
js/LoginView.js | 209 ++++++++++++++++++++++++++++++++++++------------
1 file changed, 158 insertions(+), 51 deletions(-)
diff --git a/js/LoginView.js b/js/LoginView.js
index b5da19a..c375d75 100644
--- a/js/LoginView.js
+++ b/js/LoginView.js
@@ -2,6 +2,21 @@ Ext.define('PMG.LoginView', {
extend: 'Ext.container.Container',
xtype: 'loginview',
+ viewModel: {
+ data: {
+ oidc: false,
+ },
+ formulas: {
+ button_text: function(get) {
+ if (get("oidc") === true) {
+ return gettext("Login (OpenID Connect redirect)");
+ } else {
+ return gettext("Login");
+ }
+ },
+ },
+ },
+
controller: {
xclass: 'Ext.app.ViewController',
@@ -46,50 +61,77 @@ Ext.define('PMG.LoginView', {
submitForm: async function() {
let me = this;
- let view = me.getView();
- let loginForm = me.lookupReference('loginForm');
- var unField = me.lookupReference('usernameField');
- var saveunField = me.lookupReference('saveunField');
- if (loginForm.isValid()) {
- if (loginForm.isVisible()) {
- loginForm.mask(gettext('Please wait...'), 'x-mask-loading');
- }
+ let loginForm = this.lookupReference('loginForm');
+ let unField = this.lookupReference('usernameField');
+ let saveunField = this.lookupReference('saveunField');
+ let view = this.getView();
- // set or clear username for admin view
- if (view.targetview !== 'quarantineview') {
- var sp = Ext.state.Manager.getProvider();
- if (saveunField.getValue() === true) {
- sp.set(unField.getStateId(), unField.getValue());
- } else {
- sp.clear(unField.getStateId());
- }
- sp.set(saveunField.getStateId(), saveunField.getValue());
+ if (!loginForm.isValid()) {
+ return;
+ }
+
+ if (loginForm.isVisible()) {
+ loginForm.mask(gettext('Please wait...'), 'x-mask-loading');
+ }
+
+ // set or clear username for admin view
+ if (view.targetview !== 'quarantineview') {
+ let sp = Ext.state.Manager.getProvider();
+ if (saveunField.getValue() === true) {
+ sp.set(unField.getStateId(), unField.getValue());
+ } else {
+ sp.clear(unField.getStateId());
}
+ sp.set(saveunField.getStateId(), saveunField.getValue());
+ }
+
+ let creds = loginForm.getValues();
- let creds = loginForm.getValues();
+ if (this.getViewModel().data.oidc === true) {
+ const redirectURL = location.origin;
+ Proxmox.Utils.API2Request({
+ url: '/api2/extjs/access/oidc/auth-url',
+ params: {
+ realm: creds.realm,
+ "redirect-url": redirectURL,
+ },
+ method: 'POST',
+ success: function(resp, opts) {
+ window.location = resp.result.data;
+ },
+ failure: function(resp, opts) {
+ Proxmox.Utils.authClear();
+ loginForm.unmask();
+ Ext.MessageBox.alert(
+ gettext('Error'),
+ gettext('OpenID Connect redirect failed.') + `<br>${resp.htmlStatus}`,
+ );
+ },
+ });
+ return;
+ }
- try {
- let resp = await Proxmox.Async.api2({
- url: '/api2/extjs/access/ticket',
- params: creds,
- method: 'POST',
- });
+ try {
+ let resp = await Proxmox.Async.api2({
+ url: '/api2/extjs/access/ticket',
+ params: creds,
+ method: 'POST',
+ });
- let data = resp.result.data;
- if (data.ticket.startsWith('PMG:!tfa!')) {
- data = await me.performTFAChallenge(data);
- }
- PMG.Utils.updateLoginData(data);
- PMG.app.changeView(view.targetview);
- } catch (error) {
- Proxmox.Utils.authClear();
- loginForm.unmask();
- Ext.MessageBox.alert(
- gettext('Error'),
- gettext('Login failed. Please try again'),
- );
+ let data = resp.result.data;
+ if (data.ticket.startsWith('PMG:!tfa!')) {
+ data = await me.performTFAChallenge(data);
}
+ PMG.Utils.updateLoginData(data);
+ PMG.app.changeView(view.targetview);
+ } catch (error) {
+ Proxmox.Utils.authClear();
+ loginForm.unmask();
+ Ext.MessageBox.alert(
+ gettext('Error'),
+ gettext('Login failed. Please try again'),
+ );
}
},
@@ -115,6 +157,15 @@ Ext.define('PMG.LoginView', {
return resp.result.data;
},
+ success: function(data) {
+ let me = this;
+ let view = me.getView();
+ let handler = view.handler || Ext.emptyFn;
+ handler.call(me, data);
+ PMG.Utils.updateLoginData(data);
+ PMG.app.changeView(view.targetview);
+ },
+
openQuarantineLinkWindow: function() {
let me = this;
me.lookup('loginwindow').setVisible(false);
@@ -150,6 +201,14 @@ Ext.define('PMG.LoginView', {
window.location.reload();
},
},
+ 'field[name=realm]': {
+ change: function(f, value) {
+ let record = f.store.getById(value);
+ if (record === undefined) return;
+ let data = record.data;
+ this.getViewModel().set("oidc", data.type === "oidc");
+ },
+ },
'button[reference=quarantineButton]': {
click: 'openQuarantineLinkWindow',
},
@@ -161,19 +220,54 @@ Ext.define('PMG.LoginView', {
let me = this;
let view = me.getView();
if (view.targetview !== 'quarantineview') {
- var sp = Ext.state.Manager.getProvider();
- var checkboxField = this.lookupReference('saveunField');
- var unField = this.lookupReference('usernameField');
+ let sp = Ext.state.Manager.getProvider();
+ let checkboxField = this.lookupReference('saveunField');
+ let unField = this.lookupReference('usernameField');
- var checked = sp.get(checkboxField.getStateId());
+ let checked = sp.get(checkboxField.getStateId());
checkboxField.setValue(checked);
if (checked === true) {
- var username = sp.get(unField.getStateId());
+ let username = sp.get(unField.getStateId());
unField.setValue(username);
- var pwField = this.lookupReference('passwordField');
+ let pwField = this.lookupReference('passwordField');
pwField.focus();
}
+
+ let auth = Proxmox.Utils.getOpenIDRedirectionAuthorization();
+ if (auth !== undefined) {
+ Proxmox.Utils.authClear();
+
+ let loginForm = this.lookupReference('loginForm');
+ loginForm.mask(gettext('OpenID Connect login - please wait...'), 'x-mask-loading');
+
+ const redirectURL = location.origin;
+
+ Proxmox.Utils.API2Request({
+ url: '/api2/extjs/access/oidc/login',
+ params: {
+ state: auth.state,
+ code: auth.code,
+ "redirect-url": redirectURL,
+ },
+ method: 'POST',
+ failure: function(response) {
+ loginForm.unmask();
+ let error = response.htmlStatus;
+ Ext.MessageBox.alert(
+ gettext('Error'),
+ gettext('OpenID Connect login failed, please try again') + `<br>${error}`,
+ () => { window.location = redirectURL; },
+ );
+ },
+ success: function(response, options) {
+ loginForm.unmask();
+ let data = response.result.data;
+ history.replaceState(null, '', redirectURL);
+ me.success(data);
+ },
+ });
+ }
}
},
},
@@ -250,6 +344,10 @@ Ext.define('PMG.LoginView', {
reference: 'usernameField',
stateId: 'login-username',
inputAttrTpl: 'autocomplete=username',
+ bind: {
+ visible: "{!oidc}",
+ disabled: "{oidc}",
+ },
},
{
xtype: 'textfield',
@@ -258,6 +356,17 @@ Ext.define('PMG.LoginView', {
name: 'password',
reference: 'passwordField',
inputAttrTpl: 'autocomplete=current-password',
+ bind: {
+ visible: "{!oidc}",
+ disabled: "{oidc}",
+ },
+ },
+ {
+ xtype: 'pmxRealmComboBox',
+ reference: 'realmfield',
+ name: 'realm',
+ baseUrl: '/access/auth-realm',
+ value: 'pam',
},
{
xtype: 'proxmoxLanguageSelector',
@@ -266,12 +375,6 @@ Ext.define('PMG.LoginView', {
name: 'lang',
submitValue: false,
},
- {
- xtype: 'hiddenfield',
- reference: 'realmfield',
- name: 'realm',
- value: 'pmg',
- },
],
buttons: [
{
@@ -283,15 +386,19 @@ Ext.define('PMG.LoginView', {
labelAlign: 'right',
labelWidth: 150,
submitValue: false,
+ bind: {
+ visible: "{!oidc}",
+ },
},
{
text: gettext('Request Quarantine Link'),
reference: 'quarantineButton',
},
{
- text: gettext('Login'),
+ bind: {
+ text: "{button_text}",
+ },
reference: 'loginButton',
- formBind: true,
},
],
},
--
2.39.5
_______________________________________________
pmg-devel mailing list
pmg-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel
^ permalink raw reply [flat|nested] 19+ messages in thread
* [pmg-devel] [PATCH pmg-gui v6 12/12] add realms panel to user management
2025-02-25 13:36 [pmg-devel] [PATCH pve-common/perl-rs/pmg-api/widget-toolkit/pmg-gui v6 0/12] fix #3892: OpenID Connect Markus Frank
` (10 preceding siblings ...)
2025-02-25 13:36 ` [pmg-devel] [PATCH pmg-gui v6 11/12] login: add option to login with OIDC realm Markus Frank
@ 2025-02-25 13:36 ` Markus Frank
2025-02-26 11:08 ` [pmg-devel] [PATCH pve-common/perl-rs/pmg-api/widget-toolkit/pmg-gui v6 0/12] fix #3892: OpenID Connect Mira Limbeck
12 siblings, 0 replies; 19+ messages in thread
From: Markus Frank @ 2025-02-25 13:36 UTC (permalink / raw)
To: pmg-devel
Make the realm configuration available in PMG and disable LDAP/AD
realms for now and use the name oidc instead of openid.
Signed-off-by: Markus Frank <m.frank@proxmox.com>
---
v6:
* changed realm baseUrl to '/access/auth-realm'
* pam: only set edit to false, as the other properties are already false
js/UserManagement.js | 7 +++++++
js/Utils.js | 16 ++++++++++++++++
2 files changed, 23 insertions(+)
diff --git a/js/UserManagement.js b/js/UserManagement.js
index 65fabbf..fce2a52 100644
--- a/js/UserManagement.js
+++ b/js/UserManagement.js
@@ -34,5 +34,12 @@ Ext.define('PMG.UserManagement', {
itemId: 'pop',
iconCls: 'fa fa-reply-all',
},
+ {
+ xtype: 'pmxAuthView',
+ title: gettext('Realms'),
+ itemId: 'realms',
+ baseUrl: '/access/auth-realm',
+ iconCls: 'fa fa-address-book-o',
+ },
],
});
diff --git a/js/Utils.js b/js/Utils.js
index 9b5f054..cd862e1 100644
--- a/js/Utils.js
+++ b/js/Utils.js
@@ -851,6 +851,22 @@ Ext.define('PMG.Utils', {
constructor: function() {
var me = this;
+ // use oidc instead of openid
+ Proxmox.Schema.authDomains.oidc = Proxmox.Schema.authDomains.openid;
+ Proxmox.Schema.authDomains.oidc.useTypeInUrl = false;
+ delete Proxmox.Schema.authDomains.openid;
+
+ // Disable LDAP/AD as a realm until LDAP/AD login is implemented
+ Proxmox.Schema.authDomains.ldap.add = false;
+ Proxmox.Schema.authDomains.ad.add = false;
+
+ Proxmox.Schema.authDomains.pam.edit = false;
+ Proxmox.Schema.authDomains.pmg = {
+ add: false,
+ edit: false,
+ sync: false,
+ };
+
// do whatever you want here
Proxmox.Utils.override_task_descriptions({
applycustomscores: ['', gettext('Apply custom SpamAssassin scores')],
--
2.39.5
_______________________________________________
pmg-devel mailing list
pmg-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [pmg-devel] [PATCH pmg-api v6 4/12] config: add plugin system for authentication realms
2025-02-25 13:36 ` [pmg-devel] [PATCH pmg-api v6 4/12] config: add plugin system for authentication realms Markus Frank
@ 2025-02-25 16:32 ` Stoiko Ivanov
0 siblings, 0 replies; 19+ messages in thread
From: Stoiko Ivanov @ 2025-02-25 16:32 UTC (permalink / raw)
To: Markus Frank; +Cc: pmg-devel
On Tue, 25 Feb 2025 14:36:11 +0100
Markus Frank <m.frank@proxmox.com> wrote:
> To differentiate between usernames, the realm is also stored in the
> user.conf file. Old config file syntax can be read, but will be
> overwritten after a change.
did not get around to check this for you v5 - after seeing Fabian's
feedback in
https://lore.proxmox.com/pmg-devel/Z7YlS-KYHpQMG6Nt@rosa.proxmox.com/T/#ma1084dabd040ce120f0fe831b5748eb931a21982
I wondered if 'foo@bar' is a valid username in PMG - the answer is sadly:
only partially - you can even create this in the GUI successfully, and it
ends up in user.conf - but reading the entry fails with:
```
verify entry failed
username: value does not match the regex pattern
```
we should fix this and forbid creating such users in general, but at least
we can assume that such users are not in use and can keep the 'realm' next
to the username as 'username@realm' as we do for PVE (and for PBS,
discounting for the different format)
issue stems from having 'userid' (root@pam), 'username' ('root') as
2 separate standard_options - userid only enforces a length-boundary and
forbids ':|/', while 'username' forbids '@' in addition.
I'll look into preparing an independent patch for that part of validation.
>
> This is a carryover from PVE. Previously there was no realm for a
> username. Now the realm is also stored after an @ sign in user.conf.
This part here is not true for this version of the patch - and the notes
below, that do mention it would not end up in git.
>
> Utils generates a list of valid realm names, including any newly added
> realms, to ensure proper validation of a specified realm name.
>
> Signed-off-by: Markus Frank <m.frank@proxmox.com>
> ---
> v6:
> * instead of appending @realm to the username, the realm is now saved
> after the other properties in user.conf.
> * authenticate user and store password inside plugin like it is in PVE
> * renamed $domainconfigfile to $realm_cfg_id
>
> src/Makefile | 3 +
> src/PMG/API2/Users.pm | 1 +
> src/PMG/AccessControl.pm | 38 +++++++
> src/PMG/Auth/PAM.pm | 22 ++++
> src/PMG/Auth/PMG.pm | 39 +++++++
> src/PMG/Auth/Plugin.pm | 203 +++++++++++++++++++++++++++++++++++++
> src/PMG/RESTEnvironment.pm | 14 +++
> src/PMG/UserConfig.pm | 24 +++--
> src/PMG/Utils.pm | 29 ++++--
> 9 files changed, 359 insertions(+), 14 deletions(-)
> create mode 100755 src/PMG/Auth/PAM.pm
> create mode 100755 src/PMG/Auth/PMG.pm
> create mode 100755 src/PMG/Auth/Plugin.pm
>
> diff --git a/src/Makefile b/src/Makefile
> index 1232880..659a666 100644
> --- a/src/Makefile
> +++ b/src/Makefile
> @@ -166,6 +166,9 @@ LIBSOURCES = \
> PMG/API2/ACMEPlugin.pm \
> PMG/API2/NodeConfig.pm \
> PMG/API2.pm \
> + PMG/Auth/Plugin.pm \
> + PMG/Auth/PAM.pm \
> + PMG/Auth/PMG.pm \
>
> SOURCES = ${LIBSOURCES} ${CLI_BINARIES} ${TEMPLATES_FILES} ${CONF_MANS} ${CLI_MANS} ${SERVICE_MANS} ${SERVICE_UNITS} ${TIMER_UNITS} pmg-sources.list pmg-initramfs.conf
>
> diff --git a/src/PMG/API2/Users.pm b/src/PMG/API2/Users.pm
> index 99af38d..42c82b7 100644
> --- a/src/PMG/API2/Users.pm
> +++ b/src/PMG/API2/Users.pm
> @@ -124,6 +124,7 @@ __PACKAGE__->register_method ({
> $entry->{enable} //= 0;
> $entry->{expire} //= 0;
> $entry->{role} //= 'audit';
> + $entry->{realm} //= 'pmg';
>
> $cfg->{$param->{userid}} = $entry;
>
> diff --git a/src/PMG/AccessControl.pm b/src/PMG/AccessControl.pm
> index e12d7cf..78105c0 100644
> --- a/src/PMG/AccessControl.pm
> +++ b/src/PMG/AccessControl.pm
> @@ -5,6 +5,7 @@ use warnings;
> use Authen::PAM;
>
> use PVE::Tools;
> +use PVE::INotify;
> use PVE::JSONSchema qw(get_standard_option);
> use PVE::Exception qw(raise raise_perm_exc);
>
> @@ -13,6 +14,14 @@ use PMG::LDAPConfig;
> use PMG::LDAPSet;
> use PMG::TFAConfig;
>
> +use PMG::Auth::Plugin;
> +use PMG::Auth::PAM;
> +use PMG::Auth::PMG;
> +
> +PMG::Auth::PAM->register();
> +PMG::Auth::PMG->register();
> +PMG::Auth::Plugin->init();
> +
> sub normalize_path {
> my $path = shift;
>
> @@ -38,6 +47,7 @@ sub authenticate_user : prototype($$$) {
>
> ($username, $ruid, $realm) = PMG::Utils::verify_username($username);
>
> + my $realm_regex = PMG::Utils::valid_pmg_realm_regex();
> if ($realm eq 'pam') {
> die "invalid pam user (only root allowed)\n" if $ruid ne 'root';
> authenticate_pam_user($ruid, $password);
> @@ -53,6 +63,11 @@ sub authenticate_user : prototype($$$) {
> return ($pmail . '@quarantine', undef);
> }
> die "ldap login failed\n";
> + } elsif ($realm =~ m!(${realm_regex})!) {
> + my $realm_cfg = PVE::INotify::read_file(PMG::Auth::Plugin->realm_cfg_id());
> + my $cfg = $realm_cfg->{ids}->{$realm};
> + my $plugin = PMG::Auth::Plugin->lookup($cfg->{type});
> + $plugin->authenticate_user($cfg, $realm, $ruid, $password);
> } else {
> die "no such realm '$realm'\n";
> }
> @@ -79,6 +94,7 @@ sub set_user_password {
>
> ($username, $ruid, $realm) = PMG::Utils::verify_username($username);
>
> + my $realm_regex = PMG::Utils::valid_pmg_realm_regex();
> if ($realm eq 'pam') {
> die "invalid pam user (only root allowed)\n" if $ruid ne 'root';
>
> @@ -92,6 +108,11 @@ sub set_user_password {
>
> } elsif ($realm eq 'pmg') {
> PMG::UserConfig->set_user_password($username, $password);
> + } elsif ($realm =~ m!(${realm_regex})!) {
> + my $realm_cfg = PVE::INotify::read_file(PMG::Auth::Plugin->realm_cfg_id());
> + my $cfg = $realm_cfg->{ids}->{$realm};
> + my $plugin = PMG::Auth::Plugin->lookup($cfg->{type});
> + $plugin->store_password($cfg, $realm, $username, $password);
> } else {
> die "no such realm '$realm'\n";
> }
> @@ -106,6 +127,7 @@ sub check_user_enabled {
>
> ($username, $ruid, $realm) = PMG::Utils::verify_username($username, 1);
>
> + my $realm_regex = PMG::Utils::valid_pmg_realm_regex();
> if ($realm && $ruid) {
> if ($realm eq 'pam') {
> return 'root' if $ruid eq 'root';
> @@ -115,6 +137,10 @@ sub check_user_enabled {
> return $data->{role} if $data && $data->{enable};
> } elsif ($realm eq 'quarantine') {
> return 'quser';
> + } elsif ($realm =~ m!(${realm_regex})!) {
> + my $usercfg = PMG::UserConfig->new();
> + my $data = $usercfg->lookup_user_data($username, $noerr);
> + return $data->{role} if $data && $data->{enable};
> }
> }
>
> @@ -123,6 +149,18 @@ sub check_user_enabled {
> return undef;
> }
>
> +sub check_user_exist {
> + my ($usercfg, $username, $noerr) = @_;
> +
> + $username = PMG::Utils::verify_username($username, $noerr);
> + return undef if !$username;
> + return $usercfg->{$username} if $usercfg && $usercfg->{$username};
> +
> + die "no such user ('$username')\n" if !$noerr;
> +
> + return undef;
> +}
> +
> sub authenticate_pam_user {
> my ($username, $password) = @_;
>
> diff --git a/src/PMG/Auth/PAM.pm b/src/PMG/Auth/PAM.pm
> new file mode 100755
> index 0000000..3ffd8a6
> --- /dev/null
> +++ b/src/PMG/Auth/PAM.pm
> @@ -0,0 +1,22 @@
> +package PMG::Auth::PAM;
> +
> +use strict;
> +use warnings;
> +
> +use PMG::Auth::Plugin;
> +
> +use base qw(PMG::Auth::Plugin);
> +
> +sub type {
> + return 'pam';
> +}
> +
> +sub options {
> + return {
> + default => { optional => 1 },
> + comment => { optional => 1 },
> + tfa => { optional => 1 },
> + };
> +}
> +
> +1;
> diff --git a/src/PMG/Auth/PMG.pm b/src/PMG/Auth/PMG.pm
> new file mode 100755
> index 0000000..b083692
> --- /dev/null
> +++ b/src/PMG/Auth/PMG.pm
> @@ -0,0 +1,39 @@
> +package PMG::Auth::PMG;
> +
> +use strict;
> +use warnings;
> +
> +use PMG::Auth::Plugin;
> +
> +use base qw(PMG::Auth::Plugin);
> +
> +sub type {
> + return 'pmg';
> +}
> +
> +sub properties {
> + return {
> + default => {
> + description => "Use this as default realm",
> + type => 'boolean',
> + optional => 1,
> + },
> + comment => {
> + description => "Description.",
> + type => 'string',
> + optional => 1,
> + maxLength => 4096,
> + },
> + tfa => PVE::JSONSchema::get_standard_option('tfa'),
> + };
> +}
> +
> +sub options {
> + return {
> + default => { optional => 1 },
> + comment => { optional => 1 },
> + tfa => { optional => 1 },
> + };
> +}
> +
> +1;
> diff --git a/src/PMG/Auth/Plugin.pm b/src/PMG/Auth/Plugin.pm
> new file mode 100755
> index 0000000..8e8492b
> --- /dev/null
> +++ b/src/PMG/Auth/Plugin.pm
> @@ -0,0 +1,203 @@
> +package PMG::Auth::Plugin;
> +
> +use strict;
> +use warnings;
> +
> +use Digest::SHA;
> +use Encode;
> +
> +use PMG::Utils;
> +use PVE::INotify;
> +use PVE::JSONSchema qw(get_standard_option);
> +use PVE::Schema::Auth;
> +use PVE::SectionConfig;
> +use PVE::Tools;
> +
> +use base qw(PVE::SectionConfig);
> +
> +my $realm_cfg_id = "realms.cfg";
> +my $lockfile = "/var/lock/pmg-realms.lck";
> +
> +sub realm_cfg_id {
> + return $realm_cfg_id;
> +}
> +
> +sub read_realms_conf {
> + my ($filename, $fh) = @_;
> +
> + my $raw;
> + $raw = do { local $/ = undef; <$fh> } if defined($fh);
> +
> + return PMG::Auth::Plugin->parse_config($filename, $raw);
> +}
> +
> +sub write_realms_conf {
> + my ($filename, $fh, $cfg) = @_;
> +
> + my $raw = PMG::Auth::Plugin->write_config($filename, $cfg);
> +
> + PVE::Tools::safe_print($filename, $fh, $raw);
> +}
> +
> +PVE::INotify::register_file(
> + $realm_cfg_id,
> + "/etc/pmg/realms.cfg",
> + \&read_realms_conf,
> + \&write_realms_conf,
> + undef,
> + always_call_parser => 1,
> +);
> +
> +sub lock_realm_config {
> + my ($code, $errmsg) = @_;
> +
> + PVE::Tools::lock_file($lockfile, undef, $code);
> + if (my $err = $@) {
> + $errmsg ? die "$errmsg: $err" : die $err;
> + }
> +}
> +
> +my $realm_regex = qr/[A-Za-z][A-Za-z0-9\.\-_]+/;
> +
> +sub pmg_verify_realm {
> + my ($realm, $noerr) = @_;
> +
> + if ($realm !~ m/^${realm_regex}$/) {
> + return undef if $noerr;
> + die "value does not look like a valid realm\n";
> + }
> + return $realm;
> +}
> +
> +my $defaultData = {
> + propertyList => {
> + type => { description => "Realm type." },
> + realm => get_standard_option('realm'),
> + },
> +};
> +
> +sub private {
> + return $defaultData;
> +}
> +
> +sub parse_section_header {
> + my ($class, $line) = @_;
> +
> + if ($line =~ m/^(\S+):\s*(\S+)\s*$/) {
> + my ($type, $realm) = (lc($1), $2);
> + my $errmsg = undef; # set if you want to skip whole section
> + eval { pmg_verify_realm($realm); };
> + $errmsg = $@ if $@;
> + my $config = {}; # to return additional attributes
> + return ($type, $realm, $errmsg, $config);
> + }
> + return undef;
> +}
> +
> +sub parse_config {
> + my ($class, $filename, $raw) = @_;
> +
> + my $cfg = $class->SUPER::parse_config($filename, $raw);
> +
> + my $default;
> + foreach my $realm (keys %{$cfg->{ids}}) {
> + my $data = $cfg->{ids}->{$realm};
> + # make sure there is only one default marker
> + if ($data->{default}) {
> + if ($default) {
> + delete $data->{default};
> + } else {
> + $default = $realm;
> + }
> + }
> +
> + if ($data->{comment}) {
> + $data->{comment} = PVE::Tools::decode_text($data->{comment});
> + }
> +
> + }
> +
> + # add default realms
> + $cfg->{ids}->{pmg}->{type} = 'pmg'; # force type
> + $cfg->{ids}->{pmg}->{comment} = "Proxmox Mail Gateway authentication server"
> + if !$cfg->{ids}->{pmg}->{comment};
> + $cfg->{ids}->{pmg}->{default} = 1
> + if !$cfg->{ids}->{pmg}->{default};
> +
> + $cfg->{ids}->{pam}->{type} = 'pam'; # force type
> + $cfg->{ids}->{pam}->{comment} = "Linux PAM standard authentication"
> + if !$cfg->{ids}->{pam}->{comment};
> +
> + return $cfg;
> +};
> +
> +sub write_config {
> + my ($class, $filename, $cfg) = @_;
> +
> + foreach my $realm (keys %{$cfg->{ids}}) {
> + my $data = $cfg->{ids}->{$realm};
> + if ($data->{comment}) {
> + $data->{comment} = PVE::Tools::encode_text($data->{comment});
> + }
> + }
> +
> + $class->SUPER::write_config($filename, $cfg);
> +}
> +
> +sub authenticate_user {
> + my ($class, $config, $realm, $username, $password) = @_;
> +
> + die "overwrite me";
> +}
> +
> +sub store_password {
> + my ($class, $config, $realm, $username, $password) = @_;
> +
> + my $type = $class->type();
> +
> + die "can't set password on auth type '$type'\n";
> +}
> +
> +sub delete_user {
> + my ($class, $config, $realm, $username) = @_;
> +
> + # do nothing by default
> +}
> +
> +# called during addition of realm (before the new realm config got written)
> +# `password` is moved to %param to avoid writing it out to the config
> +# die to abort addition if there are (grave) problems
> +# NOTE: runs in a realm config *locked* context
> +sub on_add_hook {
> + my ($class, $realm, $config, %param) = @_;
> + # do nothing by default
> +}
> +
> +# called during realm configuration update (before the updated realm config got
> +# written). `password` is moved to %param to avoid writing it out to the config
> +# die to abort the update if there are (grave) problems
> +# NOTE: runs in a realm config *locked* context
> +sub on_update_hook {
> + my ($class, $realm, $config, %param) = @_;
> + # do nothing by default
> +}
> +
> +# called during deletion of realms (before the new realm config got written)
> +# and if the activate check on addition fails, to cleanup all storage traces
> +# which on_add_hook may have created.
> +# die to abort deletion if there are (very grave) problems
> +# NOTE: runs in a realm config *locked* context
> +sub on_delete_hook {
> + my ($class, $realm, $config) = @_;
> + # do nothing by default
> +}
> +
> +# called during addition and updates of realms (before the new realm config gets written)
> +# die to abort addition/update in case the connection/bind fails
> +# NOTE: runs in a realm config *locked* context
> +sub check_connection {
> + my ($class, $realm, $config, %param) = @_;
> + # do nothing by default
> +}
> +
> +1;
> diff --git a/src/PMG/RESTEnvironment.pm b/src/PMG/RESTEnvironment.pm
> index 3875720..f6ff449 100644
> --- a/src/PMG/RESTEnvironment.pm
> +++ b/src/PMG/RESTEnvironment.pm
> @@ -88,6 +88,20 @@ sub get_role {
> return $self->{role};
> }
>
> +sub check_user_enabled {
> + my ($self, $user, $noerr) = @_;
> +
> + my $cfg = $self->{usercfg};
> + return PMG::AccessControl::check_user_enabled($cfg, $user, $noerr);
> +}
> +
> +sub check_user_exist {
> + my ($self, $user, $noerr) = @_;
> +
> + my $cfg = $self->{usercfg};
> + return PMG::AccessControl::check_user_exist($cfg, $user, $noerr);
> +}
> +
> sub check_node_is_master {
> my ($self, $noerr);
>
> diff --git a/src/PMG/UserConfig.pm b/src/PMG/UserConfig.pm
> index b9a83a7..672f152 100644
> --- a/src/PMG/UserConfig.pm
> +++ b/src/PMG/UserConfig.pm
> @@ -80,7 +80,7 @@ my $schema = {
> realm => {
> description => "Authentication realm.",
> type => 'string',
> - enum => ['pam', 'pmg'],
> + format => 'pmg-realm',
> default => 'pmg',
> optional => 1,
> },
> @@ -188,6 +188,7 @@ my $fixup_root_properties = sub {
> $cfg->{'root@pam'}->{expire} = 0;
> $cfg->{'root@pam'}->{comment} = 'Unix Superuser';
> $cfg->{'root@pam'}->{role} = 'root';
> + $cfg->{'root@pam'}->{realm} = 'pam';
> delete $cfg->{'root@pam'}->{crypt_pass};
> };
>
> @@ -217,26 +218,29 @@ sub read_user_conf {
> (?<firstname>(?:[^:]*)) :
> (?<lastname>(?:[^:]*)) :
> (?<keys>(?:[^:]*)) :
> + ((?<realm>(?:[^:]*)) :)?
> $/x
> ) {
> + my $default_realm = ($+{userid} eq 'root') ? 'pam' : 'pmg';
> + my $realm = $+{realm} || $default_realm;
> my $d = {
> username => $+{userid},
> - userid => $+{userid} . '@pmg',
> - realm => 'pmg',
> + userid => $+{userid} . '@' . $realm,
> + realm => $realm,
> enable => $+{enable} || 0,
> expire => $+{expire} || 0,
> role => $+{role},
> };
> $d->{comment} = $comment if $comment;
> $comment = '';
> - foreach my $k (qw(crypt_pass email firstname lastname keys)) {
> + foreach my $k (qw(crypt_pass email firstname lastname keys realm)) {
> $d->{$k} = $+{$k} if $+{$k};
> }
> eval {
> $verify_entry->($d);
> $cfg->{$d->{userid}} = $d;
> die "role 'root' is reserved\n"
> - if $d->{role} eq 'root' && $d->{userid} ne 'root@pmg';
> + if $d->{role} eq 'root' && $d->{userid} ne 'root@pam';
> };
> if (my $err = $@) {
> warn "$filename: $err";
> @@ -274,10 +278,11 @@ sub write_user_conf {
> $verify_entry->($d);
> $cfg->{$d->{userid}} = $d;
>
> + my $realm_regex = PMG::Utils::valid_pmg_realm_regex();
> if ($d->{userid} ne 'root@pam') {
> die "role 'root' is reserved\n" if $d->{role} eq 'root';
> die "unable to add users for realm '$d->{realm}'\n"
> - if $d->{realm} && $d->{realm} ne 'pmg';
> + if $d->{realm} && $d->{realm} !~ m!(${realm_regex})!;
> }
>
> my $line;
> @@ -287,12 +292,15 @@ sub write_user_conf {
> $d->{crypt_pass} = '',
> $d->{expire} = '0',
> $d->{role} = 'root';
> + $d->{realm} = 'pam';
> } else {
> - next if $userid !~ m/^(?<username>.+)\@pmg$/;
> + next if $userid !~ m/^(?<username>.+)\@(${realm_regex})$/;
> $line = "$+{username}:";
> }
> + # default to pmg
> + $d->{realm} //= 'pmg';
>
> - for my $k (qw(enable expire crypt_pass role email firstname lastname keys)) {
> + for my $k (qw(enable expire crypt_pass role email firstname lastname keys realm)) {
> $line .= ($d->{$k} // '') . ':';
> }
> if (my $comment = $d->{comment}) {
> diff --git a/src/PMG/Utils.pm b/src/PMG/Utils.pm
> index 0b8945f..04a3a2e 100644
> --- a/src/PMG/Utils.pm
> +++ b/src/PMG/Utils.pm
> @@ -49,12 +49,30 @@ postgres_admin_cmd
> try_decode_utf8
> );
>
> -my $valid_pmg_realms = ['pam', 'pmg', 'quarantine'];
> +my $user_regex = qr![^\s:/]+!;
> +
> +sub valid_pmg_realm_regex {
> + my $cfg = PVE::INotify::read_file('realms.cfg');
> + my $ids = $cfg->{ids};
> + my $realms = ['pam', 'quarantine', sort keys $cfg->{ids}->%* ];
> + return join('|', @$realms);
> +}
> +
> +sub is_valid_realm {
> + my ($realm) = @_;
> + return 0 if !$realm;
> + return 1 if $realm eq 'pam' || $realm eq 'quarantine'; # built-in ones
> +
> + my $cfg = PVE::INotify::read_file('realms.cfg');
> + return exists($cfg->{ids}->{$realm}) ? 1 : 0;
> +}
> +
> +PVE::JSONSchema::register_format('pmg-realm', \&is_valid_realm);
>
> PVE::JSONSchema::register_standard_option('realm', {
> description => "Authentication domain ID",
> type => 'string',
> - enum => $valid_pmg_realms,
> + format => 'pmg-realm',
> maxLength => 32,
> });
>
> @@ -82,16 +100,15 @@ sub verify_username {
> die "user name '$username' is too short\n" if !$noerr;
> return undef;
> }
> - if ($len > 64) {
> - die "user name '$username' is too long ($len > 64)\n" if !$noerr;
> + if ($len > 128) {
> + die "user name '$username' is too long ($len > 128)\n" if !$noerr;
> return undef;
> }
>
> # we only allow a limited set of characters. Colons aren't allowed, because we store usernames
> # with colon separated lists! slashes aren't allowed because it is used as pve API delimiter
> # also see "man useradd"
> - my $realm_list = join('|', @$valid_pmg_realms);
> - if ($username =~ m!^([^\s:/]+)\@(${realm_list})$!) {
> + if ($username =~ m!^(${user_regex})\@([A-Za-z][A-Za-z0-9\.\-_]+)$!) {
> return wantarray ? ($username, $1, $2) : $username;
> }
>
_______________________________________________
pmg-devel mailing list
pmg-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [pmg-devel] [PATCH pve-common v6 1/12] add Schema package with auth module that contains realm sync options
2025-02-25 13:36 ` [pmg-devel] [PATCH pve-common v6 1/12] add Schema package with auth module that contains realm sync options Markus Frank
@ 2025-02-25 17:24 ` Thomas Lamprecht
0 siblings, 0 replies; 19+ messages in thread
From: Thomas Lamprecht @ 2025-02-25 17:24 UTC (permalink / raw)
To: Markus Frank, pmg-devel
Am 25.02.25 um 14:36 schrieb Markus Frank:
> This is because these standard options & formats are used by both PVE
> and PMG. Schema-definitions are based on:
> pve-access-control/src/PVE/Auth/Plugin.pm
For now I'd favor having this in PMG as registered pmg-tfa-config format,
or won't that work?
And tbh. I'm not sure if that's really required in the first place, as IIRC
this is the ancient per-realm format that existed before our modern TFA
implementation. Also, exposing TOTP digits/seconds does not make much sense
as there are some very popular clients that cannot cope with non-defaul values
here.
This can be refactored/cleaned-up when there is more time so that we can also
look deeper into pve-access-control to see what's sharable under a more generic
'pmx-' format name prefix.
>
> Signed-off-by: Markus Frank <m.frank@proxmox.com>
> ---
> v6: removed schema-definitions only used by PVE
>
> src/Makefile | 2 ++
> src/PVE/Schema/Auth.pm | 46 ++++++++++++++++++++++++++++++++++++++++++
> 2 files changed, 48 insertions(+)
> create mode 100644 src/PVE/Schema/Auth.pm
>
> diff --git a/src/Makefile b/src/Makefile
> index 2d8bdc4..833bbc1 100644
> --- a/src/Makefile
> +++ b/src/Makefile
> @@ -29,6 +29,7 @@ LIB_SOURCES = \
> RESTEnvironment.pm \
> RESTHandler.pm \
> SafeSyslog.pm \
> + Schema/Auth.pm \
> SectionConfig.pm \
> SysFSTools.pm \
> Syscall.pm \
> @@ -41,6 +42,7 @@ all:
> install: $(addprefix PVE/,${LIB_SOURCES})
> install -d -m 0755 ${DESTDIR}${PERLDIR}/PVE
> install -d -m 0755 ${DESTDIR}${PERLDIR}/PVE/Job
> + install -d -m 0755 ${DESTDIR}${PERLDIR}/PVE/Schema
> for i in ${LIB_SOURCES}; do install -D -m 0644 PVE/$$i ${DESTDIR}${PERLDIR}/PVE/$$i; done
>
>
> diff --git a/src/PVE/Schema/Auth.pm b/src/PVE/Schema/Auth.pm
> new file mode 100644
> index 0000000..031301e
> --- /dev/null
> +++ b/src/PVE/Schema/Auth.pm
> @@ -0,0 +1,46 @@
> +package PVE::Schema::Auth;
> +
> +use strict;
> +use warnings;
> +
> +use PVE::JSONSchema qw(parse_property_string);
> +
> +my $tfa_format = {
> + type => {
> + description => "The type of 2nd factor authentication.",
> + format_description => 'TFATYPE',
> + type => 'string',
> + enum => [qw(oath)],
> + },
> + digits => {
> + description => "TOTP digits.",
> + format_description => 'COUNT',
> + type => 'integer',
> + minimum => 6, maximum => 8,
> + default => 6,
> + optional => 1,
> + },
> + step => {
> + description => "TOTP time period.",
> + format_description => 'SECONDS',
> + type => 'integer',
> + minimum => 10,
> + default => 30,
> + optional => 1,
> + },
> +};
> +
> +PVE::JSONSchema::register_format('pve-tfa-config', $tfa_format);
> +
> +PVE::JSONSchema::register_standard_option('tfa', {
> + description => "Use Two-factor authentication.",
> + type => 'string', format => 'pve-tfa-config',
> + optional => 1,
> + maxLength => 128,
the maxLength looks a bit odd to me, makes not much sense if this property
has a format anyway? Actually the whole format is pretty borked and borderline
useless, but that's pre-existing...
> +});
> +
> +sub parse_tfa_config {
> + my ($data) = @_;
> +
> + return parse_property_string($tfa_format, $data);
> +}
_______________________________________________
pmg-devel mailing list
pmg-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [pmg-devel] [PATCH widget-toolkit v6 09/12] fix: panel: AuthView: change API path in pmx-domains model
2025-02-25 13:36 ` [pmg-devel] [PATCH widget-toolkit v6 09/12] fix: panel: AuthView: change API path in pmx-domains model Markus Frank
@ 2025-02-25 17:33 ` Markus Frank
0 siblings, 0 replies; 19+ messages in thread
From: Markus Frank @ 2025-02-25 17:33 UTC (permalink / raw)
To: pmg-devel
This patch breaks pbs WebUI. I will send a fix tomorrow.
On 2025-02-25 14:36, Markus Frank wrote:
> Currently it always uses the default path because it was hardcoded into
> the 'pmx-domains' model.
>
> Fixes: 8d4faa88212757f274cc177c906af09d4ebe07bb
> Signed-off-by: Markus Frank <m.frank@proxmox.com>
> Co-authored-by: Dominik Csapak <d.csapak@proxmox.com>
> ---
> This patch is new to v6. Required to change the API path to
> /access/auth-realm
>
> src/panel/AuthView.js | 20 ++++++++++++--------
> 1 file changed, 12 insertions(+), 8 deletions(-)
>
> diff --git a/src/panel/AuthView.js b/src/panel/AuthView.js
> index f607532..0e1718e 100644
> --- a/src/panel/AuthView.js
> +++ b/src/panel/AuthView.js
> @@ -47,14 +47,6 @@ Ext.define('Proxmox.panel.AuthView', {
> },
> ],
>
> - store: {
> - model: 'pmx-domains',
> - sorters: {
> - property: 'realm',
> - direction: 'ASC',
> - },
> - },
> -
> openEditWindow: function(authType, realm) {
> let me = this;
> const { useTypeInUrl, onlineHelp } = Proxmox.Schema.authDomains[authType];
> @@ -111,6 +103,18 @@ Ext.define('Proxmox.panel.AuthView', {
> initComponent: function() {
> var me = this;
>
> + me.store = {
> + model: 'pmx-domains',
> + sorters: {
> + property: 'realm',
> + direction: 'ASC',
> + },
> + proxy: {
> + type: 'proxmox',
> + url: `/api2/json${me.baseUrl}`,
> + },
> + };
> +
> let menuitems = [];
> for (const [authType, config] of Object.entries(Proxmox.Schema.authDomains).sort()) {
> if (!config.add) { continue; }
_______________________________________________
pmg-devel mailing list
pmg-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [pmg-devel] [PATCH pmg-api v6 5/12] config: add oidc type authentication realm
2025-02-25 13:36 ` [pmg-devel] [PATCH pmg-api v6 5/12] config: add oidc type authentication realm Markus Frank
@ 2025-02-26 10:20 ` Mira Limbeck
0 siblings, 0 replies; 19+ messages in thread
From: Mira Limbeck @ 2025-02-26 10:20 UTC (permalink / raw)
To: pmg-devel
On 2/25/25 14:36, Markus Frank wrote:
> PMG::Auth::OIDC is based on pve-access-control's PVE::Auth::OpenId and
> adds an autocreate-role option. If the autocreate option is enabled, the
> user is automatically created with the Audit role. With autocreate-role
> this role can be changed.
>
> Signed-off-by: Markus Frank <m.frank@proxmox.com>
> ---
> v6: added regex patterns to restrict the string types
>
> src/Makefile | 1 +
> src/PMG/AccessControl.pm | 2 +
> src/PMG/Auth/OIDC.pm | 101 +++++++++++++++++++++++++++++++++++++++
> 3 files changed, 104 insertions(+)
> create mode 100755 src/PMG/Auth/OIDC.pm
>
> diff --git a/src/Makefile b/src/Makefile
> index 659a666..3cae7c7 100644
> --- a/src/Makefile
> +++ b/src/Makefile
> @@ -169,6 +169,7 @@ LIBSOURCES = \
> PMG/Auth/Plugin.pm \
> PMG/Auth/PAM.pm \
> PMG/Auth/PMG.pm \
> + PMG/Auth/OIDC.pm \
>
> SOURCES = ${LIBSOURCES} ${CLI_BINARIES} ${TEMPLATES_FILES} ${CONF_MANS} ${CLI_MANS} ${SERVICE_MANS} ${SERVICE_UNITS} ${TIMER_UNITS} pmg-sources.list pmg-initramfs.conf
>
> diff --git a/src/PMG/AccessControl.pm b/src/PMG/AccessControl.pm
> index 78105c0..57d80f8 100644
> --- a/src/PMG/AccessControl.pm
> +++ b/src/PMG/AccessControl.pm
> @@ -15,9 +15,11 @@ use PMG::LDAPSet;
> use PMG::TFAConfig;
>
> use PMG::Auth::Plugin;
> +use PMG::Auth::OIDC;
> use PMG::Auth::PAM;
> use PMG::Auth::PMG;
>
> +PMG::Auth::OIDC->register();
> PMG::Auth::PAM->register();
> PMG::Auth::PMG->register();
> PMG::Auth::Plugin->init();
> diff --git a/src/PMG/Auth/OIDC.pm b/src/PMG/Auth/OIDC.pm
> new file mode 100755
> index 0000000..600e124
> --- /dev/null
> +++ b/src/PMG/Auth/OIDC.pm
> @@ -0,0 +1,101 @@
> +package PMG::Auth::OIDC;
> +
> +use strict;
> +use warnings;
> +
> +use PVE::Tools;
> +use PMG::Auth::Plugin;
> +
> +use base qw(PMG::Auth::Plugin);
> +
> +sub type {
> + return 'oidc';
> +}
> +
> +sub properties {
> + return {
> + 'issuer-url' => {
> + description => "OpenID Connect Issuer Url",
> + type => 'string',
> + maxLength => 256,
> + pattern => qr/^(https?):\/\/([a-zA-Z0-9.-]+)(:[0-9]{1,5})?(\/[^\s]*)?$/,
> + },
> + 'client-id' => {
> + description => "OpenID Connect Client ID",
> + type => 'string',
> + maxLength => 256,
> + pattern => qr/^[a-zA-Z0-9._:-]+$/,
> + },
> + 'client-key' => {
> + description => "OpenID Connect Client Key",
> + type => 'string',
> + optional => 1,
> + maxLength => 256,
> + pattern => qr/^[a-zA-Z0-9._:-]+$/,
> + },
> + autocreate => {
> + description => "Automatically create users if they do not exist.",
> + optional => 1,
> + type => 'boolean',
> + default => 0,
> + },
> + 'autocreate-role' => {
> + description => "Automatically create users with a specific role.",
> + type => 'string',
> + enum => ['admin', 'qmanager', 'audit', 'helpdesk'],
> + optional => 1,
> + },
In code (patch 7/12) we default to `audit`, maybe `default => 'audit'`
should be added here?
> + 'username-claim' => {
> + description => "OpenID Connect claim used to generate the unique username.",
> + type => 'string',
> + optional => 1,
> + pattern => qr/^[a-zA-Z0-9._:-]+$/,
> + },
same as above, maybe we should add `default => 'sub'` here. In the code
(patch 7/12) we default to `sub`, same as in PVE.
> + prompt => {
> + description => "Specifies whether the Authorization Server prompts the End-User for"
> + ." reauthentication and consent.",
> + type => 'string',
> + pattern => '(?:none|login|consent|select_account|\S+)', # \S+ is the extension variant
> + optional => 1,
> + },
> + scopes => {
> + description => "Specifies the scopes (user details) that should be authorized and"
> + ." returned, for example 'email' or 'profile'.",
> + type => 'string', # format => 'some-safe-id-list', # FIXME: TODO
> + default => "email profile",
> + pattern => qr/^[a-zA-Z0-9._:-]+$/,
> + optional => 1,
> + },
> + 'acr-values' => {
> + description => "Specifies the Authentication Context Class Reference values that the"
> + ."Authorization Server is being requested to use for the Auth Request.",
> + type => 'string', # format => 'some-safe-id-list', # FIXME: TODO
> + pattern => qr/^[a-zA-Z0-9._:-]+$/,
> + optional => 1,
> + },
> + };
> +}
> +
> +sub options {
> + return {
> + 'issuer-url' => {},
> + 'client-id' => {},
> + 'client-key' => { optional => 1 },
> + autocreate => { optional => 1 },
> + 'autocreate-role' => { optional => 1 },
> + 'username-claim' => { optional => 1, fixed => 1 },
> + prompt => { optional => 1 },
> + scopes => { optional => 1 },
> + 'acr-values' => { optional => 1 },
> + default => { optional => 1 },
> + comment => { optional => 1 },
> + };
> +}
> +
> +sub authenticate_user {
> + my ($class, $config, $realm, $username, $password) = @_;
> +
> + die "OpenID Connect realm does not allow password verification.\n";
> +}
> +
> +1;
_______________________________________________
pmg-devel mailing list
pmg-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [pmg-devel] [PATCH pve-common/perl-rs/pmg-api/widget-toolkit/pmg-gui v6 0/12] fix #3892: OpenID Connect
2025-02-25 13:36 [pmg-devel] [PATCH pve-common/perl-rs/pmg-api/widget-toolkit/pmg-gui v6 0/12] fix #3892: OpenID Connect Markus Frank
` (11 preceding siblings ...)
2025-02-25 13:36 ` [pmg-devel] [PATCH pmg-gui v6 12/12] add realms panel to user management Markus Frank
@ 2025-02-26 11:08 ` Mira Limbeck
2025-02-26 11:30 ` Lukas Wagner
12 siblings, 1 reply; 19+ messages in thread
From: Mira Limbeck @ 2025-02-26 11:08 UTC (permalink / raw)
To: pmg-devel
On 2/25/25 14:36, Markus Frank wrote:
> Patch-series to enable OpenID Connect Login for PMG
>
> apply/compile order:
>
> pve-common:
> 1 add Schema package with auth module that contains realm sync options
>
> proxmox-perl-rs:
> 2 move openid code from pve-rs to common
> 3 remove empty PMG::RS::OpenId package to avoid confusion
>
> pmg-api:
> 4 config: add plugin system for authentication realms
> 5 config: add oidc type authentication realm
> 6 api: add/update/remove authentication realms like in PVE
> 7 api: oidc login similar to PVE
>
> proxmox-widget-toolkit:
> 8 fix: window: AuthEditBase: rename variable 'realm' to 'type'
> 9 fix: panel: AuthView: change API path in pmx-domains model
> 10 form: RealmComboBox: add option to change the API path
>
> pmg-gui:
> 11 login: add option to login with OIDC realm
> 12 add realms panel to user management
>
>
>
>
> I still need to add the option to create users for other realms than PMG
> in the API and WebUI. The autocreate option of the OIDC realm can be
> used instead for now. Also the autocreate-role option needs to be
> exposed to the WebUI. I will send these things as follow-up patches or
> in the next iteration if this series cannot be applied yet.
>
>
>
>
> v6:
> * renamed Realm to AuthRealm and renamed every domain variable to realm
> * changed realm API-path from access/domains to access/auth-realm
> * more v6-changes described in the individual patches
>
> v5:
> * renamed openid/OpenId variables, filenames and modules to oidc/OIDC
> wherever possible
> * renamed Authdomains to Realm
>
> v4:
> * split "config: add plugin system for realms & add openid type realms"
> patch into two patches
> * use the name 'OpenId' for filenames, but use 'OIDC' as realm type name
> * added autocreate-role option to set the role for automatically created
> users in a realm, but currently not exposed in GUI (needs a lot of
> changes in pmg-gui and proxmox-widget-toolkit)
>
>
> pve-common:
>
> Markus Frank (1):
> add Schema package with auth module that contains realm sync options
>
> src/Makefile | 2 ++
> src/PVE/Schema/Auth.pm | 46 ++++++++++++++++++++++++++++++++++++++++++
> 2 files changed, 48 insertions(+)
> create mode 100644 src/PVE/Schema/Auth.pm
>
>
> proxmox-perl-rs:
>
> Markus Frank (2):
> move openid code from pve-rs to common
> remove empty PMG::RS::OpenId package to avoid confusion
>
> common/pkg/Makefile | 1 +
> common/src/mod.rs | 1 +
> common/src/oidc/mod.rs | 63 ++++++++++++++++++++++++++++++++++++++++
> pmg-rs/Cargo.toml | 1 +
> pmg-rs/Makefile | 1 -
> pmg-rs/debian/control | 1 +
> pve-rs/src/openid/mod.rs | 32 +++++---------------
> 7 files changed, 75 insertions(+), 25 deletions(-)
> create mode 100644 common/src/oidc/mod.rs
>
>
> pmg-api:
>
> Markus Frank (4):
> config: add plugin system for authentication realms
> config: add oidc type authentication realm
> api: add/update/remove authentication realms like in PVE
> api: oidc login similar to PVE
>
> src/Makefile | 6 +
> src/PMG/API2/AccessControl.pm | 17 ++-
> src/PMG/API2/AuthRealm.pm | 274 ++++++++++++++++++++++++++++++++++
> src/PMG/API2/OIDC.pm | 243 ++++++++++++++++++++++++++++++
> src/PMG/API2/Users.pm | 1 +
> src/PMG/AccessControl.pm | 40 +++++
> src/PMG/Auth/OIDC.pm | 101 +++++++++++++
> src/PMG/Auth/PAM.pm | 22 +++
> src/PMG/Auth/PMG.pm | 39 +++++
> src/PMG/Auth/Plugin.pm | 203 +++++++++++++++++++++++++
> src/PMG/HTTPServer.pm | 4 +-
> src/PMG/RESTEnvironment.pm | 14 ++
> src/PMG/UserConfig.pm | 24 ++-
> src/PMG/Utils.pm | 29 +++-
> 14 files changed, 1001 insertions(+), 16 deletions(-)
> create mode 100644 src/PMG/API2/AuthRealm.pm
> create mode 100644 src/PMG/API2/OIDC.pm
> create mode 100755 src/PMG/Auth/OIDC.pm
> create mode 100755 src/PMG/Auth/PAM.pm
> create mode 100755 src/PMG/Auth/PMG.pm
> create mode 100755 src/PMG/Auth/Plugin.pm
>
>
> widget-toolkit:
>
> Markus Frank (3):
> fix: window: AuthEditBase: rename variable 'realm' to 'type'
> fix: panel: AuthView: change API path in pmx-domains model
> form: RealmComboBox: add option to change the API path
>
> src/form/RealmComboBox.js | 2 ++
> src/panel/AuthView.js | 20 ++++++++++++--------
> src/window/AuthEditBase.js | 4 ++--
> 3 files changed, 16 insertions(+), 10 deletions(-)
>
>
> pmg-gui:
>
> Markus Frank (2):
> login: add option to login with OIDC realm
> add realms panel to user management
>
> js/LoginView.js | 209 ++++++++++++++++++++++++++++++++-----------
> js/UserManagement.js | 7 ++
> js/Utils.js | 16 ++++
> 3 files changed, 181 insertions(+), 51 deletions(-)
>
Gave it a quick test with Authentik as OIDC provider.
Login with a non-existent (on PMG) user without an email address works now.
Setting the username-claim to `email` fails with:
```
openid connect authentication failure; rhost=<host> msg=autocreate
openid connect user failed: verify entry failed
username: value does not match the regex pattern
```
And setting username-claim to `sub` fails with:
```
openid connect authentication failure; rhost=<host> msg=autocreate
openid connect user failed: verify entry
userid: value may only be 64 characters long
```
I've documented this in bugzilla [1].
[0] https://openid.net/specs/openid-connect-core-1_0.html#IDToken
[1] https://bugzilla.proxmox.com/show_bug.cgi?id=6200
_______________________________________________
pmg-devel mailing list
pmg-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [pmg-devel] [PATCH pve-common/perl-rs/pmg-api/widget-toolkit/pmg-gui v6 0/12] fix #3892: OpenID Connect
2025-02-26 11:08 ` [pmg-devel] [PATCH pve-common/perl-rs/pmg-api/widget-toolkit/pmg-gui v6 0/12] fix #3892: OpenID Connect Mira Limbeck
@ 2025-02-26 11:30 ` Lukas Wagner
0 siblings, 0 replies; 19+ messages in thread
From: Lukas Wagner @ 2025-02-26 11:30 UTC (permalink / raw)
To: Mira Limbeck, pmg-devel
On 2025-02-26 12:08, Mira Limbeck wrote:
> Gave it a quick test with Authentik as OIDC provider.
> Login with a non-existent (on PMG) user without an email address works now.
> Setting the username-claim to `email` fails with:
> ```
> openid connect authentication failure; rhost=<host> msg=autocreate
> openid connect user failed: verify entry failed
> username: value does not match the regex pattern
> ```
>
> And setting username-claim to `sub` fails with:
> ```
> openid connect authentication failure; rhost=<host> msg=autocreate
> openid connect user failed: verify entry
> userid: value may only be 64 characters long
> ```
>
> I've documented this in bugzilla [1].
>
>
> [0] https://openid.net/specs/openid-connect-core-1_0.html#IDToken
> [1] https://bugzilla.proxmox.com/show_bug.cgi?id=6200
>
>
Just for the record, tested the same two scenarios against Keycloak:
- username-claim = sub works there, since the generated IDs are shorter (36 chars)
- username-claim = email also does not work, no surprise there
Apart from that it seems to work (but I don't know much about OIDC yet, so maybe
I didn't test some edge cases)
--
- Lukas
_______________________________________________
pmg-devel mailing list
pmg-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel
^ permalink raw reply [flat|nested] 19+ messages in thread
end of thread, other threads:[~2025-02-26 11:30 UTC | newest]
Thread overview: 19+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-02-25 13:36 [pmg-devel] [PATCH pve-common/perl-rs/pmg-api/widget-toolkit/pmg-gui v6 0/12] fix #3892: OpenID Connect Markus Frank
2025-02-25 13:36 ` [pmg-devel] [PATCH pve-common v6 1/12] add Schema package with auth module that contains realm sync options Markus Frank
2025-02-25 17:24 ` Thomas Lamprecht
2025-02-25 13:36 ` [pmg-devel] [PATCH proxmox-perl-rs v6 2/12] move openid code from pve-rs to common Markus Frank
2025-02-25 13:36 ` [pmg-devel] [PATCH proxmox-perl-rs v6 3/12] remove empty PMG::RS::OpenId package to avoid confusion Markus Frank
2025-02-25 13:36 ` [pmg-devel] [PATCH pmg-api v6 4/12] config: add plugin system for authentication realms Markus Frank
2025-02-25 16:32 ` Stoiko Ivanov
2025-02-25 13:36 ` [pmg-devel] [PATCH pmg-api v6 5/12] config: add oidc type authentication realm Markus Frank
2025-02-26 10:20 ` Mira Limbeck
2025-02-25 13:36 ` [pmg-devel] [PATCH pmg-api v6 6/12] api: add/update/remove authentication realms like in PVE Markus Frank
2025-02-25 13:36 ` [pmg-devel] [PATCH pmg-api v6 7/12] api: oidc login similar to PVE Markus Frank
2025-02-25 13:36 ` [pmg-devel] [PATCH widget-toolkit v6 08/12] fix: window: AuthEditBase: rename variable 'realm' to 'type' Markus Frank
2025-02-25 13:36 ` [pmg-devel] [PATCH widget-toolkit v6 09/12] fix: panel: AuthView: change API path in pmx-domains model Markus Frank
2025-02-25 17:33 ` Markus Frank
2025-02-25 13:36 ` [pmg-devel] [PATCH widget-toolkit v6 10/12] form: RealmComboBox: add option to change the API path Markus Frank
2025-02-25 13:36 ` [pmg-devel] [PATCH pmg-gui v6 11/12] login: add option to login with OIDC realm Markus Frank
2025-02-25 13:36 ` [pmg-devel] [PATCH pmg-gui v6 12/12] add realms panel to user management Markus Frank
2025-02-26 11:08 ` [pmg-devel] [PATCH pve-common/perl-rs/pmg-api/widget-toolkit/pmg-gui v6 0/12] fix #3892: OpenID Connect Mira Limbeck
2025-02-26 11:30 ` Lukas Wagner
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