From: Stoiko Ivanov <s.ivanov@proxmox.com>
To: Markus Frank <m.frank@proxmox.com>
Cc: pmg-devel@lists.proxmox.com
Subject: Re: [pmg-devel] [PATCH pmg-api v8 3/13] config: add plugin system for authentication realms
Date: Wed, 26 Feb 2025 15:40:15 +0100 [thread overview]
Message-ID: <20250226154015.1004f27c@rosa.proxmox.com> (raw)
In-Reply-To: <20250226140740.55612-4-m.frank@proxmox.com>
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
next prev parent reply other threads:[~2025-02-26 14:40 UTC|newest]
Thread overview: 23+ messages / expand[flat|nested] mbox.gz Atom feed top
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 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
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
2025-02-26 14:40 ` Stoiko Ivanov [this message]
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 ` [pmg-devel] [PATCH pmg-api v8 5/13] api: add/update/remove authentication realms like in PVE Markus Frank
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
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
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 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
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 ` [pmg-devel] [PATCH pmg-gui v8 11/13] login: add option to login with OIDC realm Markus Frank
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 ` [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
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20250226154015.1004f27c@rosa.proxmox.com \
--to=s.ivanov@proxmox.com \
--cc=m.frank@proxmox.com \
--cc=pmg-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal