* [pmg-devel] [PATCH proxmox-perl-rs v8 1/13] move openid code from pve-rs to common
2025-02-26 14:07 [pmg-devel] [PATCH perl-rs/pmg-api/widget-toolkit/pmg-gui v8 0/13] fix #3892: OpenID Connect Markus Frank
@ 2025-02-26 14:07 ` Markus Frank
2025-02-26 16:57 ` [pmg-devel] applied: " Thomas Lamprecht
2025-02-26 14:07 ` [pmg-devel] [PATCH proxmox-perl-rs v8 2/13] remove empty PMG::RS::OpenId package to avoid confusion Markus Frank
` (12 subsequent siblings)
13 siblings, 1 reply; 23+ messages in thread
From: Markus Frank @ 2025-02-26 14:07 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>
---
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] 23+ messages in thread
* [pmg-devel] [PATCH proxmox-perl-rs v8 2/13] remove empty PMG::RS::OpenId package to avoid confusion
2025-02-26 14:07 [pmg-devel] [PATCH perl-rs/pmg-api/widget-toolkit/pmg-gui v8 0/13] fix #3892: OpenID Connect Markus Frank
2025-02-26 14:07 ` [pmg-devel] [PATCH proxmox-perl-rs v8 1/13] move openid code from pve-rs to common Markus Frank
@ 2025-02-26 14:07 ` Markus Frank
2025-02-26 16:58 ` [pmg-devel] applied: " Thomas Lamprecht
2025-02-26 17:55 ` [pmg-devel] " Stoiko Ivanov
2025-02-26 14:07 ` [pmg-devel] [PATCH pmg-api v8 3/13] config: add plugin system for authentication realms Markus Frank
` (11 subsequent siblings)
13 siblings, 2 replies; 23+ messages in thread
From: Markus Frank @ 2025-02-26 14:07 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>
---
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] 23+ messages in thread
* [pmg-devel] applied: [PATCH proxmox-perl-rs v8 2/13] remove empty PMG::RS::OpenId package to avoid confusion
2025-02-26 14:07 ` [pmg-devel] [PATCH proxmox-perl-rs v8 2/13] remove empty PMG::RS::OpenId package to avoid confusion Markus Frank
@ 2025-02-26 16:58 ` Thomas Lamprecht
2025-02-26 17:55 ` [pmg-devel] " Stoiko Ivanov
1 sibling, 0 replies; 23+ messages in thread
From: Thomas Lamprecht @ 2025-02-26 16:58 UTC (permalink / raw)
To: Markus Frank, pmg-devel
Am 26.02.25 um 15:07 schrieb Markus Frank:
> The common Proxmox::RS:OpenId package can be used instead.
>
> Signed-off-by: Markus Frank <m.frank@proxmox.com>
> ---
> pmg-rs/Makefile | 1 -
> 1 file changed, 1 deletion(-)
>
>
applied, thanks!
_______________________________________________
pmg-devel mailing list
pmg-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel
^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: [pmg-devel] [PATCH proxmox-perl-rs v8 2/13] remove empty PMG::RS::OpenId package to avoid confusion
2025-02-26 14:07 ` [pmg-devel] [PATCH proxmox-perl-rs v8 2/13] remove empty PMG::RS::OpenId package to avoid confusion Markus Frank
2025-02-26 16:58 ` [pmg-devel] applied: " Thomas Lamprecht
@ 2025-02-26 17:55 ` Stoiko Ivanov
1 sibling, 0 replies; 23+ messages in thread
From: Stoiko Ivanov @ 2025-02-26 17:55 UTC (permalink / raw)
To: Markus Frank; +Cc: pmg-devel
On Wed, 26 Feb 2025 15:07:29 +0100
Markus Frank <m.frank@proxmox.com> wrote:
> The common Proxmox::RS:OpenId package can be used instead.
nit: please update the commit messages along with the code the next time
(The common Proxmox::RS::OIDC package...)
>
> Signed-off-by: Markus Frank <m.frank@proxmox.com>
> ---
> 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)))
_______________________________________________
pmg-devel mailing list
pmg-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel
^ permalink raw reply [flat|nested] 23+ messages in thread
* [pmg-devel] [PATCH pmg-api v8 3/13] config: add plugin system for authentication realms
2025-02-26 14:07 [pmg-devel] [PATCH perl-rs/pmg-api/widget-toolkit/pmg-gui v8 0/13] fix #3892: OpenID Connect Markus Frank
2025-02-26 14:07 ` [pmg-devel] [PATCH proxmox-perl-rs v8 1/13] move openid code from pve-rs to common Markus Frank
2025-02-26 14:07 ` [pmg-devel] [PATCH proxmox-perl-rs v8 2/13] remove empty PMG::RS::OpenId package to avoid confusion Markus Frank
@ 2025-02-26 14:07 ` Markus Frank
2025-02-26 14:40 ` Stoiko Ivanov
2025-02-26 14:07 ` [pmg-devel] [PATCH pmg-api v8 4/13] config: add oidc type authentication realm Markus Frank
` (10 subsequent siblings)
13 siblings, 1 reply; 23+ messages in thread
From: Markus Frank @ 2025-02-26 14:07 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>
---
src/Makefile | 3 +
src/PMG/API2/Users.pm | 1 +
src/PMG/AccessControl.pm | 38 +++++++
src/PMG/Auth/PAM.pm | 21 ++++
src/PMG/Auth/PMG.pm | 37 +++++++
src/PMG/Auth/Plugin.pm | 202 +++++++++++++++++++++++++++++++++++++
src/PMG/RESTEnvironment.pm | 14 +++
src/PMG/UserConfig.pm | 25 +++--
src/PMG/Utils.pm | 29 ++++--
9 files changed, 354 insertions(+), 16 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..002c8e1
--- /dev/null
+++ b/src/PMG/Auth/PAM.pm
@@ -0,0 +1,21 @@
+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 },
+ };
+}
+
+1;
diff --git a/src/PMG/Auth/PMG.pm b/src/PMG/Auth/PMG.pm
new file mode 100755
index 0000000..0706c97
--- /dev/null
+++ b/src/PMG/Auth/PMG.pm
@@ -0,0 +1,37 @@
+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,
+ },
+ };
+}
+
+sub options {
+ return {
+ default => { optional => 1 },
+ comment => { optional => 1 },
+ };
+}
+
+1;
diff --git a/src/PMG/Auth/Plugin.pm b/src/PMG/Auth/Plugin.pm
new file mode 100755
index 0000000..beb8fc4
--- /dev/null
+++ b/src/PMG/Auth/Plugin.pm
@@ -0,0 +1,202 @@
+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::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..fe6d2c8 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,
},
@@ -219,10 +219,13 @@ sub read_user_conf {
(?<keys>(?:[^:]*)) :
$/x
) {
+ my @username_parts = split('@', $+{userid});
+ my $username = $username_parts[0];
+ my $realm = defined($username_parts[1]) ? $username_parts[1] : "pmg";
my $d = {
- username => $+{userid},
- userid => $+{userid} . '@pmg',
- realm => 'pmg',
+ username => $username,
+ userid => $username . '@' . $realm,
+ realm => $realm,
enable => $+{enable} || 0,
expire => $+{expire} || 0,
role => $+{role},
@@ -235,8 +238,9 @@ sub read_user_conf {
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} !~ /^root@(pmg|pam)$/) {
+ die "role 'root' is reserved\n";
+ }
};
if (my $err = $@) {
warn "$filename: $err";
@@ -274,22 +278,23 @@ 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;
if ($userid eq 'root@pam') {
- $line = 'root:';
+ $line = 'root@pam:';
$d->{crypt_pass} = '',
$d->{expire} = '0',
$d->{role} = 'root';
} else {
- next if $userid !~ m/^(?<username>.+)\@pmg$/;
- $line = "$+{username}:";
+ next if $userid !~ m/^(?<username>.+)\@(${realm_regex})$/;
+ $line = "$d->{userid}:";
}
for my $k (qw(enable expire crypt_pass role email firstname lastname keys)) {
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] 23+ messages in thread
* Re: [pmg-devel] [PATCH pmg-api v8 3/13] config: add plugin system for authentication realms
2025-02-26 14:07 ` [pmg-devel] [PATCH pmg-api v8 3/13] config: add plugin system for authentication realms Markus Frank
@ 2025-02-26 14:40 ` Stoiko Ivanov
0 siblings, 0 replies; 23+ messages in thread
From: Stoiko Ivanov @ 2025-02-26 14:40 UTC (permalink / raw)
To: Markus Frank; +Cc: pmg-devel
While trying to add the realm config to the cluster-sync I noticed a small
mismatch (cfg vs. conf):
* user.conf, ldap.conf vs. realms.cfg
While its mostly cosmetic - now would be an easy time to change
this if we want to have it aligned (w/o version-guarded copying in
maintscripts)
probably can be fixed up on applying if nothing else comes up
(occurences should be here and in your 6/13 (will reply there as well))
the places where a rename has to happen as comments inline:
On Wed, 26 Feb 2025 15:07:30 +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.
>
> 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>
> ---
> src/Makefile | 3 +
> src/PMG/API2/Users.pm | 1 +
> src/PMG/AccessControl.pm | 38 +++++++
> src/PMG/Auth/PAM.pm | 21 ++++
> src/PMG/Auth/PMG.pm | 37 +++++++
> src/PMG/Auth/Plugin.pm | 202 +++++++++++++++++++++++++++++++++++++
> src/PMG/RESTEnvironment.pm | 14 +++
> src/PMG/UserConfig.pm | 25 +++--
> src/PMG/Utils.pm | 29 ++++--
> 9 files changed, 354 insertions(+), 16 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..002c8e1
> --- /dev/null
> +++ b/src/PMG/Auth/PAM.pm
> @@ -0,0 +1,21 @@
> +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 },
> + };
> +}
> +
> +1;
> diff --git a/src/PMG/Auth/PMG.pm b/src/PMG/Auth/PMG.pm
> new file mode 100755
> index 0000000..0706c97
> --- /dev/null
> +++ b/src/PMG/Auth/PMG.pm
> @@ -0,0 +1,37 @@
> +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,
> + },
> + };
> +}
> +
> +sub options {
> + return {
> + default => { optional => 1 },
> + comment => { optional => 1 },
> + };
> +}
> +
> +1;
> diff --git a/src/PMG/Auth/Plugin.pm b/src/PMG/Auth/Plugin.pm
> new file mode 100755
> index 0000000..beb8fc4
> --- /dev/null
> +++ b/src/PMG/Auth/Plugin.pm
> @@ -0,0 +1,202 @@
> +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::SectionConfig;
> +use PVE::Tools;
> +
> +use base qw(PVE::SectionConfig);
> +
> +my $realm_cfg_id = "realms.cfg";
here (else it's even more confusing)
> +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",
here
> + \&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..fe6d2c8 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,
> },
> @@ -219,10 +219,13 @@ sub read_user_conf {
> (?<keys>(?:[^:]*)) :
> $/x
> ) {
> + my @username_parts = split('@', $+{userid});
> + my $username = $username_parts[0];
> + my $realm = defined($username_parts[1]) ? $username_parts[1] : "pmg";
> my $d = {
> - username => $+{userid},
> - userid => $+{userid} . '@pmg',
> - realm => 'pmg',
> + username => $username,
> + userid => $username . '@' . $realm,
> + realm => $realm,
> enable => $+{enable} || 0,
> expire => $+{expire} || 0,
> role => $+{role},
> @@ -235,8 +238,9 @@ sub read_user_conf {
> 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} !~ /^root@(pmg|pam)$/) {
> + die "role 'root' is reserved\n";
> + }
> };
> if (my $err = $@) {
> warn "$filename: $err";
> @@ -274,22 +278,23 @@ 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;
>
> if ($userid eq 'root@pam') {
> - $line = 'root:';
> + $line = 'root@pam:';
> $d->{crypt_pass} = '',
> $d->{expire} = '0',
> $d->{role} = 'root';
> } else {
> - next if $userid !~ m/^(?<username>.+)\@pmg$/;
> - $line = "$+{username}:";
> + next if $userid !~ m/^(?<username>.+)\@(${realm_regex})$/;
> + $line = "$d->{userid}:";
> }
>
> for my $k (qw(enable expire crypt_pass role email firstname lastname keys)) {
> 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');
here
> + 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');
here
> + 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] 23+ messages in thread
* [pmg-devel] [PATCH pmg-api v8 4/13] config: add oidc type authentication realm
2025-02-26 14:07 [pmg-devel] [PATCH perl-rs/pmg-api/widget-toolkit/pmg-gui v8 0/13] fix #3892: OpenID Connect Markus Frank
` (2 preceding siblings ...)
2025-02-26 14:07 ` [pmg-devel] [PATCH pmg-api v8 3/13] config: add plugin system for authentication realms Markus Frank
@ 2025-02-26 14:07 ` Markus Frank
2025-02-26 14:07 ` [pmg-devel] [PATCH pmg-api v8 5/13] api: add/update/remove authentication realms like in PVE Markus Frank
` (9 subsequent siblings)
13 siblings, 0 replies; 23+ messages in thread
From: Markus Frank @ 2025-02-26 14:07 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>
---
v8:
* added defaults for autocreate-role and username-claim
src/Makefile | 1 +
src/PMG/AccessControl.pm | 2 +
src/PMG/Auth/OIDC.pm | 103 +++++++++++++++++++++++++++++++++++++++
3 files changed, 106 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..4129d47
--- /dev/null
+++ b/src/PMG/Auth/OIDC.pm
@@ -0,0 +1,103 @@
+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'],
+ default => 'audit',
+ optional => 1,
+ },
+ 'username-claim' => {
+ description => "OpenID Connect claim used to generate the unique username.",
+ type => 'string',
+ optional => 1,
+ default => 'sub',
+ 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] 23+ messages in thread
* [pmg-devel] [PATCH pmg-api v8 5/13] api: add/update/remove authentication realms like in PVE
2025-02-26 14:07 [pmg-devel] [PATCH perl-rs/pmg-api/widget-toolkit/pmg-gui v8 0/13] fix #3892: OpenID Connect Markus Frank
` (3 preceding siblings ...)
2025-02-26 14:07 ` [pmg-devel] [PATCH pmg-api v8 4/13] config: add oidc type authentication realm Markus Frank
@ 2025-02-26 14:07 ` Markus Frank
2025-02-26 14:07 ` [pmg-devel] [PATCH pmg-api v8 6/13] api: oidc login similar to PVE Markus Frank
` (8 subsequent siblings)
13 siblings, 0 replies; 23+ messages in thread
From: Markus Frank @ 2025-02-26 14:07 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>
---
src/Makefile | 1 +
src/PMG/API2/AccessControl.pm | 10 +-
src/PMG/API2/AuthRealm.pm | 264 ++++++++++++++++++++++++++++++++++
src/PMG/HTTPServer.pm | 2 +-
4 files changed, 275 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..57c5fea
--- /dev/null
+++ b/src/PMG/API2/AuthRealm.pm
@@ -0,0 +1,264 @@
+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 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' },
+ 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};
+ 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] 23+ messages in thread
* [pmg-devel] [PATCH pmg-api v8 6/13] api: oidc login similar to PVE
2025-02-26 14:07 [pmg-devel] [PATCH perl-rs/pmg-api/widget-toolkit/pmg-gui v8 0/13] fix #3892: OpenID Connect Markus Frank
` (4 preceding siblings ...)
2025-02-26 14:07 ` [pmg-devel] [PATCH pmg-api v8 5/13] api: add/update/remove authentication realms like in PVE Markus Frank
@ 2025-02-26 14:07 ` Markus Frank
2025-02-26 14:41 ` Stoiko Ivanov
2025-02-26 14:07 ` [pmg-devel] [PATCH pmg-api v8 7/13] api: users: create user with a specified realm Markus Frank
` (7 subsequent siblings)
13 siblings, 1 reply; 23+ messages in thread
From: Markus Frank @ 2025-02-26 14:07 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>
---
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] 23+ messages in thread
* Re: [pmg-devel] [PATCH pmg-api v8 6/13] api: oidc login similar to PVE
2025-02-26 14:07 ` [pmg-devel] [PATCH pmg-api v8 6/13] api: oidc login similar to PVE Markus Frank
@ 2025-02-26 14:41 ` Stoiko Ivanov
0 siblings, 0 replies; 23+ messages in thread
From: Stoiko Ivanov @ 2025-02-26 14:41 UTC (permalink / raw)
To: Markus Frank; +Cc: pmg-devel
location of realms.cfg (see my comment for your 8/13 for context):
On Wed, 26 Feb 2025 15:07:33 +0100
Markus Frank <m.frank@proxmox.com> wrote:
> 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>
> ---
> 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');
here
> + 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;
_______________________________________________
pmg-devel mailing list
pmg-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel
^ permalink raw reply [flat|nested] 23+ messages in thread
* [pmg-devel] [PATCH pmg-api v8 7/13] api: users: create user with a specified realm
2025-02-26 14:07 [pmg-devel] [PATCH perl-rs/pmg-api/widget-toolkit/pmg-gui v8 0/13] fix #3892: OpenID Connect Markus Frank
` (5 preceding siblings ...)
2025-02-26 14:07 ` [pmg-devel] [PATCH pmg-api v8 6/13] api: oidc login similar to PVE Markus Frank
@ 2025-02-26 14:07 ` Markus Frank
2025-02-26 15:36 ` Mira Limbeck
2025-02-26 14:07 ` [pmg-devel] [PATCH widget-toolkit v8 08/13] fix: window: AuthEditBase: rename variable 'realm' to 'type' Markus Frank
` (6 subsequent siblings)
13 siblings, 1 reply; 23+ messages in thread
From: Markus Frank @ 2025-02-26 14:07 UTC (permalink / raw)
To: pmg-devel
Signed-off-by: Markus Frank <m.frank@proxmox.com>
---
v8: fixed "Use of uninitialized value in string" when realm is not set
at user creation
src/PMG/API2/Users.pm | 3 +++
src/PMG/UserConfig.pm | 1 -
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/PMG/API2/Users.pm b/src/PMG/API2/Users.pm
index 42c82b7..f137b87 100644
--- a/src/PMG/API2/Users.pm
+++ b/src/PMG/API2/Users.pm
@@ -111,6 +111,9 @@ __PACKAGE__->register_method ({
die "User '$param->{userid}' already exists\n"
if $cfg->{$param->{userid}};
+ die "Currently you cannot create user in the PAM realm\n"
+ if $param->{realm} && $param->{realm} eq 'pam';
+
my $entry = {};
foreach my $k (keys %$param) {
my $v = $param->{$k};
diff --git a/src/PMG/UserConfig.pm b/src/PMG/UserConfig.pm
index fe6d2c8..9a6f142 100644
--- a/src/PMG/UserConfig.pm
+++ b/src/PMG/UserConfig.pm
@@ -141,7 +141,6 @@ my $schema = {
our $create_schema = clone($schema);
delete $create_schema->{properties}->{username};
-delete $create_schema->{properties}->{realm};
$create_schema->{properties}->{password} = {
description => "Password",
type => 'string',
--
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] 23+ messages in thread
* Re: [pmg-devel] [PATCH pmg-api v8 7/13] api: users: create user with a specified realm
2025-02-26 14:07 ` [pmg-devel] [PATCH pmg-api v8 7/13] api: users: create user with a specified realm Markus Frank
@ 2025-02-26 15:36 ` Mira Limbeck
2025-02-26 16:29 ` Mira Limbeck
0 siblings, 1 reply; 23+ messages in thread
From: Mira Limbeck @ 2025-02-26 15:36 UTC (permalink / raw)
To: pmg-devel
On 2/26/25 15:07, Markus Frank wrote:
> Signed-off-by: Markus Frank <m.frank@proxmox.com>
> ---
> v8: fixed "Use of uninitialized value in string" when realm is not set
> at user creation
>
> src/PMG/API2/Users.pm | 3 +++
> src/PMG/UserConfig.pm | 1 -
> 2 files changed, 3 insertions(+), 1 deletion(-)
>
> diff --git a/src/PMG/API2/Users.pm b/src/PMG/API2/Users.pm
> index 42c82b7..f137b87 100644
> --- a/src/PMG/API2/Users.pm
> +++ b/src/PMG/API2/Users.pm
> @@ -111,6 +111,9 @@ __PACKAGE__->register_method ({
> die "User '$param->{userid}' already exists\n"
> if $cfg->{$param->{userid}};
>
> + die "Currently you cannot create user in the PAM realm\n"
> + if $param->{realm} && $param->{realm} eq 'pam';
> +
the warning no longer shows, but you can still create a user for `pam`
by just specifying the userid:
```
pmg:/access/users> create --role audit --userid test123@pam
200 OK
```
> my $entry = {};
> foreach my $k (keys %$param) {
> my $v = $param->{$k};
> diff --git a/src/PMG/UserConfig.pm b/src/PMG/UserConfig.pm
> index fe6d2c8..9a6f142 100644
> --- a/src/PMG/UserConfig.pm
> +++ b/src/PMG/UserConfig.pm
> @@ -141,7 +141,6 @@ my $schema = {
>
> our $create_schema = clone($schema);
> delete $create_schema->{properties}->{username};
> -delete $create_schema->{properties}->{realm};
> $create_schema->{properties}->{password} = {
> description => "Password",
> type => 'string',
_______________________________________________
pmg-devel mailing list
pmg-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel
^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: [pmg-devel] [PATCH pmg-api v8 7/13] api: users: create user with a specified realm
2025-02-26 15:36 ` Mira Limbeck
@ 2025-02-26 16:29 ` Mira Limbeck
0 siblings, 0 replies; 23+ messages in thread
From: Mira Limbeck @ 2025-02-26 16:29 UTC (permalink / raw)
To: pmg-devel
On 2/26/25 16:36, Mira Limbeck wrote:
> On 2/26/25 15:07, Markus Frank wrote:
>> Signed-off-by: Markus Frank <m.frank@proxmox.com>
>> ---
>> v8: fixed "Use of uninitialized value in string" when realm is not set
>> at user creation
>>
>> src/PMG/API2/Users.pm | 3 +++
>> src/PMG/UserConfig.pm | 1 -
>> 2 files changed, 3 insertions(+), 1 deletion(-)
>>
>> diff --git a/src/PMG/API2/Users.pm b/src/PMG/API2/Users.pm
>> index 42c82b7..f137b87 100644
>> --- a/src/PMG/API2/Users.pm
>> +++ b/src/PMG/API2/Users.pm
>> @@ -111,6 +111,9 @@ __PACKAGE__->register_method ({
>> die "User '$param->{userid}' already exists\n"
>> if $cfg->{$param->{userid}};
>>
>> + die "Currently you cannot create user in the PAM realm\n"
>> + if $param->{realm} && $param->{realm} eq 'pam';
>> +
> the warning no longer shows, but you can still create a user for `pam`
> by just specifying the userid:
> ```
> pmg:/access/users> create --role audit --userid test123@pam
> 200 OK
> ```
api2/json/access/users returns the `realm` as a separate field. it is
extracted by splitting the userid on the first '@'. see
PMG/UserConfig.pm -> read_user_conf
maybe we could use this here as well via a helper sub in UserConfig.pm?
>
>
>
>> my $entry = {};
>> foreach my $k (keys %$param) {
>> my $v = $param->{$k};
>> diff --git a/src/PMG/UserConfig.pm b/src/PMG/UserConfig.pm
>> index fe6d2c8..9a6f142 100644
>> --- a/src/PMG/UserConfig.pm
>> +++ b/src/PMG/UserConfig.pm
>> @@ -141,7 +141,6 @@ my $schema = {
>>
>> our $create_schema = clone($schema);
>> delete $create_schema->{properties}->{username};
>> -delete $create_schema->{properties}->{realm};
>> $create_schema->{properties}->{password} = {
>> description => "Password",
>> type => 'string',
>
>
>
> _______________________________________________
> pmg-devel mailing list
> pmg-devel@lists.proxmox.com
> https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel
>
>
_______________________________________________
pmg-devel mailing list
pmg-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel
^ permalink raw reply [flat|nested] 23+ messages in thread
* [pmg-devel] [PATCH widget-toolkit v8 08/13] fix: window: AuthEditBase: rename variable 'realm' to 'type'
2025-02-26 14:07 [pmg-devel] [PATCH perl-rs/pmg-api/widget-toolkit/pmg-gui v8 0/13] fix #3892: OpenID Connect Markus Frank
` (6 preceding siblings ...)
2025-02-26 14:07 ` [pmg-devel] [PATCH pmg-api v8 7/13] api: users: create user with a specified realm Markus Frank
@ 2025-02-26 14:07 ` Markus Frank
2025-02-26 17:52 ` [pmg-devel] partially-applied-series: " Thomas Lamprecht
2025-02-26 14:07 ` [pmg-devel] [PATCH widget-toolkit v8 09/13] panel: AuthView: change API path in pmx-domains model Markus Frank
` (5 subsequent siblings)
13 siblings, 1 reply; 23+ messages in thread
From: Markus Frank @ 2025-02-26 14:07 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>
---
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] 23+ messages in thread
* [pmg-devel] [PATCH widget-toolkit v8 09/13] panel: AuthView: change API path in pmx-domains model
2025-02-26 14:07 [pmg-devel] [PATCH perl-rs/pmg-api/widget-toolkit/pmg-gui v8 0/13] fix #3892: OpenID Connect Markus Frank
` (7 preceding siblings ...)
2025-02-26 14:07 ` [pmg-devel] [PATCH widget-toolkit v8 08/13] fix: window: AuthEditBase: rename variable 'realm' to 'type' Markus Frank
@ 2025-02-26 14:07 ` Markus Frank
2025-02-26 14:07 ` [pmg-devel] [PATCH widget-toolkit v8 10/13] form: RealmComboBox: add option to change the API path Markus Frank
` (4 subsequent siblings)
13 siblings, 0 replies; 23+ messages in thread
From: Markus Frank @ 2025-02-26 14:07 UTC (permalink / raw)
To: pmg-devel
Currently is always using the default path because it was hardcoded into
the model 'pmx-domains'. Introduce new variable storeBaseUrl because PBS
uses two different paths to access the realm/domains API.
Signed-off-by: Markus Frank <m.frank@proxmox.com>
Co-authored-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/panel/AuthView.js | 21 +++++++++++++--------
1 file changed, 13 insertions(+), 8 deletions(-)
diff --git a/src/panel/AuthView.js b/src/panel/AuthView.js
index f607532..7bebf0d 100644
--- a/src/panel/AuthView.js
+++ b/src/panel/AuthView.js
@@ -13,6 +13,7 @@ Ext.define('Proxmox.panel.AuthView', {
},
baseUrl: '/access/domains',
+ storeBaseUrl: '/access/domains',
columns: [
{
@@ -47,14 +48,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 +104,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.storeBaseUrl}`,
+ },
+ };
+
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] 23+ messages in thread
* [pmg-devel] [PATCH widget-toolkit v8 10/13] form: RealmComboBox: add option to change the API path
2025-02-26 14:07 [pmg-devel] [PATCH perl-rs/pmg-api/widget-toolkit/pmg-gui v8 0/13] fix #3892: OpenID Connect Markus Frank
` (8 preceding siblings ...)
2025-02-26 14:07 ` [pmg-devel] [PATCH widget-toolkit v8 09/13] panel: AuthView: change API path in pmx-domains model Markus Frank
@ 2025-02-26 14:07 ` Markus Frank
2025-02-26 14:07 ` [pmg-devel] [PATCH pmg-gui v8 11/13] login: add option to login with OIDC realm Markus Frank
` (3 subsequent siblings)
13 siblings, 0 replies; 23+ messages in thread
From: Markus Frank @ 2025-02-26 14:07 UTC (permalink / raw)
To: pmg-devel
Signed-off-by: Markus Frank <m.frank@proxmox.com>
---
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] 23+ messages in thread
* [pmg-devel] [PATCH pmg-gui v8 11/13] login: add option to login with OIDC realm
2025-02-26 14:07 [pmg-devel] [PATCH perl-rs/pmg-api/widget-toolkit/pmg-gui v8 0/13] fix #3892: OpenID Connect Markus Frank
` (9 preceding siblings ...)
2025-02-26 14:07 ` [pmg-devel] [PATCH widget-toolkit v8 10/13] form: RealmComboBox: add option to change the API path Markus Frank
@ 2025-02-26 14:07 ` Markus Frank
2025-02-26 14:07 ` [pmg-devel] [PATCH pmg-gui v8 12/13] add realms panel to user management Markus Frank
` (2 subsequent siblings)
13 siblings, 0 replies; 23+ messages in thread
From: Markus Frank @ 2025-02-26 14:07 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>
---
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] 23+ messages in thread
* [pmg-devel] [PATCH pmg-gui v8 12/13] add realms panel to user management
2025-02-26 14:07 [pmg-devel] [PATCH perl-rs/pmg-api/widget-toolkit/pmg-gui v8 0/13] fix #3892: OpenID Connect Markus Frank
` (10 preceding siblings ...)
2025-02-26 14:07 ` [pmg-devel] [PATCH pmg-gui v8 11/13] login: add option to login with OIDC realm Markus Frank
@ 2025-02-26 14:07 ` Markus Frank
2025-02-26 14:07 ` [pmg-devel] [PATCH pmg-gui v8 13/13] user: add realm field for user creation Markus Frank
2025-02-26 20:17 ` [pmg-devel] applied: [PATCH perl-rs/pmg-api/widget-toolkit/pmg-gui v8 0/13] fix #3892: OpenID Connect Thomas Lamprecht
13 siblings, 0 replies; 23+ messages in thread
From: Markus Frank @ 2025-02-26 14:07 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>
---
js/UserManagement.js | 8 ++++++++
js/Utils.js | 16 ++++++++++++++++
2 files changed, 24 insertions(+)
diff --git a/js/UserManagement.js b/js/UserManagement.js
index 65fabbf..f6ada1b 100644
--- a/js/UserManagement.js
+++ b/js/UserManagement.js
@@ -34,5 +34,13 @@ Ext.define('PMG.UserManagement', {
itemId: 'pop',
iconCls: 'fa fa-reply-all',
},
+ {
+ xtype: 'pmxAuthView',
+ title: gettext('Realms'),
+ itemId: 'realms',
+ baseUrl: '/access/auth-realm',
+ storeBaseUrl: '/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] 23+ messages in thread
* [pmg-devel] [PATCH pmg-gui v8 13/13] user: add realm field for user creation
2025-02-26 14:07 [pmg-devel] [PATCH perl-rs/pmg-api/widget-toolkit/pmg-gui v8 0/13] fix #3892: OpenID Connect Markus Frank
` (11 preceding siblings ...)
2025-02-26 14:07 ` [pmg-devel] [PATCH pmg-gui v8 12/13] add realms panel to user management Markus Frank
@ 2025-02-26 14:07 ` Markus Frank
2025-02-26 20:17 ` [pmg-devel] applied: [PATCH perl-rs/pmg-api/widget-toolkit/pmg-gui v8 0/13] fix #3892: OpenID Connect Thomas Lamprecht
13 siblings, 0 replies; 23+ messages in thread
From: Markus Frank @ 2025-02-26 14:07 UTC (permalink / raw)
To: pmg-devel
Signed-off-by: Markus Frank <m.frank@proxmox.com>
---
js/UserEdit.js | 47 ++++++++++++++++++++++++++++++++++++++++-------
1 file changed, 40 insertions(+), 7 deletions(-)
diff --git a/js/UserEdit.js b/js/UserEdit.js
index bd94686..4c1e540 100644
--- a/js/UserEdit.js
+++ b/js/UserEdit.js
@@ -29,6 +29,26 @@ Ext.define('PMG.UserEdit', {
};
},
+ viewModel: {
+ data: {
+ realm: 'pmg',
+ },
+ formulas: {
+ maySetPassword: function(get) {
+ let realm = get('realm');
+
+ let view = this.getView();
+ let realmStore = view.down('pmxRealmComboBox').getStore();
+ if (realmStore.isLoaded()) {
+ let rec = realmStore.findRecord('realm', realm, 0, false, true, true);
+ return rec.data.type === 'pmg' && view.isCreate;
+ } else {
+ return view.isCreate;
+ }
+ },
+ },
+ },
+
items: {
xtype: 'inputpanel',
column1: [
@@ -58,9 +78,9 @@ Ext.define('PMG.UserEdit', {
field.next().validate();
},
},
- cbind: {
- hidden: '{!isCreate}',
- disabled: '{!isCreate}',
+ bind: {
+ hidden: '{!maySetPassword}',
+ disabled: '{!maySetPassword}',
},
},
{
@@ -72,9 +92,9 @@ Ext.define('PMG.UserEdit', {
initialPassField: 'password',
allowBlank: false,
submitValue: false,
- cbind: {
- hidden: '{!isCreate}',
- disabled: '{!isCreate}',
+ bind: {
+ hidden: '{!maySetPassword}',
+ disabled: '{!maySetPassword}',
},
},
{
@@ -111,6 +131,19 @@ Ext.define('PMG.UserEdit', {
],
column2: [
+ {
+ xtype: 'pmxRealmComboBox',
+ reference: 'realmfield',
+ name: 'realm',
+ baseUrl: '/access/auth-realm',
+ bind: {
+ value: '{realm}',
+ },
+ cbind: {
+ disabled: '{!isCreate}',
+ hidden: '{!isCreate}',
+ },
+ },
{
xtype: 'proxmoxtextfield',
name: 'firstname',
@@ -170,7 +203,7 @@ Ext.define('PMG.UserEdit', {
}
if (me.isCreate) {
- values.userid = values.username + '@pmg';
+ values.userid = values.username + '@' + values.realm;
}
delete values.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] 23+ messages in thread
* [pmg-devel] applied: [PATCH perl-rs/pmg-api/widget-toolkit/pmg-gui v8 0/13] fix #3892: OpenID Connect
2025-02-26 14:07 [pmg-devel] [PATCH perl-rs/pmg-api/widget-toolkit/pmg-gui v8 0/13] fix #3892: OpenID Connect Markus Frank
` (12 preceding siblings ...)
2025-02-26 14:07 ` [pmg-devel] [PATCH pmg-gui v8 13/13] user: add realm field for user creation Markus Frank
@ 2025-02-26 20:17 ` Thomas Lamprecht
13 siblings, 0 replies; 23+ messages in thread
From: Thomas Lamprecht @ 2025-02-26 20:17 UTC (permalink / raw)
To: Markus Frank, pmg-devel
Am 26.02.25 um 15:07 schrieb Markus Frank:
> Patch-series to enable OpenID Connect Login for PMG
>
applied series, thanks!
Some basic documentation would be nice to have in such a series, could be even
added from quite early on, as the OIDC principle itself won't change and most
higher level design choices won't change either..
Allowing to set a custom scope where the user-role is determined from might
be nice for a future enhancement.
_______________________________________________
pmg-devel mailing list
pmg-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel
^ permalink raw reply [flat|nested] 23+ messages in thread