[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