[pmg-devel] [PATCH pmg-api v5 6/10] api: add/update/remove realms like in PVE
Markus Frank
m.frank at proxmox.com
Fri Feb 21 14:44:20 CET 2025
Thank you for reviewing this patch series.
On 2025-02-21 13:41, Fabian Grünbichler wrote:
>
>> Markus Frank <m.frank at proxmox.com> hat am 18.02.2025 17:19 CET geschrieben:
>>
>>
>> The name Realm.pm was chosen because a Domain.pm already exists.
>
> but the API path is still domains, and the naming inside the code/descriptions/.. is also rather inconsistent. should we settle on one or the other?
We use /access/domain in PVE/PBS and already allow /access/domains in PMG/HTTPServer.pm:
```
# explicitly allow some calls without auth
if (($rel_uri eq '/access/domains' && $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'))) {
```
Before renaming it to Realm, I was using Authdomain as the file/module name.
If we want to stick to one name, we either use Authdomains (or something similar) again, or we change everything to realm and use a different api path than PVE/PBS.
I think I would prefer using Authdomains and /access/domain.
Any opinions?
>
>>
>> Signed-off-by: Markus Frank <m.frank at proxmox.com>
>> ---
>> src/Makefile | 1 +
>> src/PMG/API2/AccessControl.pm | 10 +-
>> src/PMG/API2/Realm.pm | 274 ++++++++++++++++++++++++++++++++++
>> 3 files changed, 284 insertions(+), 1 deletion(-)
>> create mode 100644 src/PMG/API2/Realm.pm
>>
>> diff --git a/src/Makefile b/src/Makefile
>> index 3cae7c7..e939bbd 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/Realm.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..f4cbc81 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::Realm;
>> 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::Realm",
>> + path => 'domains',
>
> realm vs domains
>
>> +});
>> +
>> __PACKAGE__->register_method ({
>> name => 'index',
>> path => '',
>> @@ -57,6 +63,7 @@ __PACKAGE__->register_method ({
>>
>> my $res = [
>> { subdir => 'ticket' },
>> + { subdir => 'domains' },
>> { 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/Realm.pm b/src/PMG/API2/Realm.pm
>> new file mode 100644
>> index 0000000..da2072a
>> --- /dev/null
>> +++ b/src/PMG/API2/Realm.pm
>> @@ -0,0 +1,274 @@
>> +package PMG::API2::Realm;
>> +
>> +use strict;
>> +use warnings;
>> +
>> +use PVE::Exception qw(raise_param_exc);
>> +use PVE::INotify;
>> +use PVE::JSONSchema qw(get_standard_option);
>> +use PVE::RESTHandler;
>> +use PVE::SafeSyslog;
>> +use PVE::Tools qw(extract_param);
>> +
>> +use PMG::AccessControl;
>> +use PMG::Auth::Plugin;
>> +use PVE::Schema::Auth;
>> +
>> +my $domainconfigfile = "realms.cfg";
>> +
>> +use base qw(PVE::RESTHandler);
>> +
>> +__PACKAGE__->register_method ({
>> + name => 'index',
>> + path => '',
>> + method => 'GET',
>> + description => "Authentication domain index.",
>
> and here it is still an authentication domain ;)
>
>> + 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' },
>
> even though it returns realms
>
>> + type => { type => 'string' },
>> + tfa => {
>> + description => "Two-factor authentication provider.",
>> + type => 'string',
>> + enum => [ 'yubico', 'oath' ],
>> + optional => 1,
>> + },
>> + comment => {
>> + description => "A comment. The GUI use this text when you select a domain (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($domainconfigfile);
>> + my $ids = $cfg->{ids};
>> +
>> + for my $realm (keys %$ids) {
>> + my $d = $ids->{$realm};
>> + my $entry = { realm => $realm, type => $d->{type} };
>> + $entry->{comment} = $d->{comment} if $d->{comment};
>> + $entry->{default} = 1 if $d->{default};
>> + if ($d->{tfa} && (my $tfa_cfg = PVE::Schema::Auth::parse_tfa_config($d->{tfa}))) {
>> + $entry->{tfa} = $tfa_cfg->{type};
>> + }
>> + push @$res, $entry;
>> + }
>> +
>> + return $res;
>> + }});
>> +
>> +__PACKAGE__->register_method ({
>> + name => 'create',
>> + protected => 1,
>> + path => '',
>> + method => 'POST',
>> + permissions => { check => [ 'admin' ] },
>> + description => "Add an authentication server.",
>> + parameters => PMG::Auth::Plugin->createSchema(0),
>> + returns => { type => 'null' },
>> + code => sub {
>> + my ($param) = @_;
>> +
>> + # always extract, add it with hook
>> + my $password = extract_param($param, 'password');
>> +
>> + PMG::Auth::Plugin::lock_domain_config(
>> + sub {
>> + my $cfg = PVE::INotify::read_file($domainconfigfile);
>> + 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 "domain '$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($domainconfigfile, $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_domain_config(
>> + sub {
>> + my $cfg = PVE::INotify::read_file($domainconfigfile);
>> + 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 "domain '$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($domainconfigfile, $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($domainconfigfile);
>> +
>> + my $realm = $param->{realm};
>> +
>> + my $data = $cfg->{ids}->{$realm};
>> + die "domain '$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_domain_config(
>> + sub {
>> + my $cfg = PVE::INotify::read_file($domainconfigfile);
>> + my $ids = $cfg->{ids};
>> + my $realm = $param->{realm};
>> +
>> + die "authentication domain '$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($domainconfigfile, $cfg);
>> + },
>> + "delete auth server failed",
>> + );
>> + return undef;
>> + }});
>> +
>> +1;
>> --
>> 2.39.5
>>
>>
>>
>> _______________________________________________
>> pmg-devel mailing list
>> pmg-devel at lists.proxmox.com
>> https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel
More information about the pmg-devel
mailing list