[pve-devel] [PATCH pve-access-control v3 1/1] fix #5076: Added an optional "audiences" field
Alexander Abraham
a.abraham at proxmox.com
Tue Jun 3 11:12:55 CEST 2025
An optional "audiences" field was added to the schema
to accept an array of strings for any audiences communicated
by using an Open ID realm in PVE.
Signed-off-by: Alexander Abraham <a.abraham at proxmox.com>
---
src/PVE/API2/OpenId.pm | 463 +++++++++++++++++++++++------------------
src/PVE/Auth/OpenId.pm | 141 +++++++------
2 files changed, 335 insertions(+), 269 deletions(-)
diff --git a/src/PVE/API2/OpenId.pm b/src/PVE/API2/OpenId.pm
index 77410e6..90e047d 100644
--- a/src/PVE/API2/OpenId.pm
+++ b/src/PVE/API2/OpenId.pm
@@ -21,7 +21,7 @@ use base qw(PVE::RESTHandler);
my $openid_state_path = "/var/lib/pve-manager";
my $lookup_openid_auth = sub {
- my ($realm, $redirect_url) = @_;
+ my ( $realm, $redirect_url ) = @_;
my $cfg = cfs_read_file('domains.cfg');
my $ids = $cfg->{ids};
@@ -29,221 +29,272 @@ my $lookup_openid_auth = sub {
die "authentication domain '$realm' does not exist\n" if !$ids->{$realm};
my $config = $ids->{$realm};
- die "wrong realm type ($config->{type} != openid)\n" if $config->{type} ne "openid";
+ die "wrong realm type ($config->{type} != openid)\n"
+ if $config->{type} ne "openid";
my $openid_config = {
- issuer_url => $config->{'issuer-url'},
- client_id => $config->{'client-id'},
- client_key => $config->{'client-key'},
+ issuer_url => $config->{'issuer-url'},
+ client_id => $config->{'client-id'},
+ client_key => $config->{'client-key'},
};
- $openid_config->{prompt} = $config->{'prompt'} if defined($config->{'prompt'});
+ $openid_config->{prompt} = $config->{'prompt'}
+ if defined( $config->{'prompt'} );
my $scopes = $config->{'scopes'} // 'email profile';
$openid_config->{scopes} = [ PVE::Tools::split_list($scopes) ];
- if (defined(my $acr = $config->{'acr-values'})) {
- $openid_config->{acr_values} = [ PVE::Tools::split_list($acr) ];
+ if ( defined( my $acr = $config->{'acr-values'} ) ) {
+ $openid_config->{acr_values} = [ PVE::Tools::split_list($acr) ];
}
- my $openid = PVE::RS::OpenId->discover($openid_config, $redirect_url);
- return ($config, $openid);
+ if ( defined( my $audiences = $config->{'audiences'} ) ) {
+ $openid_config->{audiences} = $config->{'audiences'};
+ }
+
+ my $openid = PVE::RS::OpenId->discover( $openid_config, $redirect_url );
+ return ( $config, $openid );
};
-__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 Authorization Url for the specified realm.",
- parameters => {
- additionalProperties => 0,
- properties => {
- realm => get_standard_option('realm'),
- '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 $dcconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
- local $ENV{all_proxy} = $dcconf->{http_proxy} if exists $dcconf->{http_proxy};
-
- my $realm = extract_param($param, 'realm');
- my $redirect_url = extract_param($param, 'redirect-url');
-
- my ($config, $openid) = $lookup_openid_auth->($realm, $redirect_url);
- my $url = $openid->authorize_url($openid_state_path , $realm);
-
- return $url;
- }});
-
-__PACKAGE__->register_method ({
- name => 'login',
- path => 'login',
- method => 'POST',
- protected => 1,
- description => " Verify OpenID authorization code and create a ticket.",
- parameters => {
- additionalProperties => 0,
- properties => {
- 'state' => {
- description => "OpenId state.",
- type => 'string',
- maxLength => 1024,
+__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 Authorization Url for the specified realm.",
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ realm => get_standard_option('realm'),
+ '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 $dcconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
+ local $ENV{all_proxy} = $dcconf->{http_proxy}
+ if exists $dcconf->{http_proxy};
+
+ my $realm = extract_param( $param, 'realm' );
+ my $redirect_url = extract_param( $param, 'redirect-url' );
+
+ my ( $config, $openid ) =
+ $lookup_openid_auth->( $realm, $redirect_url );
+ my $url = $openid->authorize_url( $openid_state_path, $realm );
+
+ return $url;
+ }
+ }
+);
+
+__PACKAGE__->register_method(
+ {
+ name => 'login',
+ path => 'login',
+ method => 'POST',
+ protected => 1,
+ description => " Verify OpenID authorization code and create a ticket.",
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ 'state' => {
+ description => "OpenId state.",
+ type => 'string',
+ maxLength => 1024,
+ },
+ code => {
+ description => "OpenId 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,
+ },
},
- code => {
- description => "OpenId authorization code.",
- type => 'string',
- maxLength => 4096,
+ },
+ returns => {
+ properties => {
+ username => { type => 'string' },
+ ticket => { type => 'string' },
+ CSRFPreventionToken => { type => 'string' },
+ cap => { type => 'object' }, # computed api permissions
+ clustername => { type => 'string', optional => 1 },
},
- 'redirect-url' => {
- description => "Redirection Url. The client should set this to the used server url (location.origin).",
- type => 'string',
- maxLength => 255,
- },
- },
- },
- returns => {
- properties => {
- username => { type => 'string' },
- ticket => { type => 'string' },
- CSRFPreventionToken => { type => 'string' },
- cap => { type => 'object' }, # computed api permissions
- clustername => { type => 'string', optional => 1 },
- },
- },
- permissions => { user => 'world' },
- code => sub {
- my ($param) = @_;
-
- my $rpcenv = PVE::RPCEnvironment::get();
-
- my $res;
- eval {
- my $dcconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
- local $ENV{all_proxy} = $dcconf->{http_proxy} if exists $dcconf->{http_proxy};
-
- my ($realm, $private_auth_state) = PVE::RS::OpenId::verify_public_auth_state(
- $openid_state_path, $param->{'state'});
-
- my $redirect_url = extract_param($param, 'redirect-url');
-
- my ($config, $openid) = $lookup_openid_auth->($realm, $redirect_url);
-
- my $info = $openid->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
- PVE::Auth::Plugin::verify_username($username);
-
- if ($config->{'autocreate'} && !$rpcenv->check_user_exist($username, 1)) {
- PVE::AccessControl::lock_user_config(sub {
- my $usercfg = cfs_read_file("user.cfg");
-
- die "user '$username' already exists\n" if $usercfg->{users}->{$username};
-
- my $entry = { enable => 1 };
- if (defined(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;
- }
-
- $usercfg->{users}->{$username} = $entry;
-
- cfs_write_file("user.cfg", $usercfg);
- }, "autocreate openid user failed");
- } else {
- # test if user exists and is enabled
- $rpcenv->check_user_enabled($username);
- }
-
- my $ticket = PVE::AccessControl::assemble_ticket($username);
- my $csrftoken = PVE::AccessControl::assemble_csrf_prevention_token($username);
- my $cap = $rpcenv->compute_api_permission($username);
-
- $res = {
- ticket => $ticket,
- username => $username,
- CSRFPreventionToken => $csrftoken,
- cap => $cap,
- };
-
- my $clinfo = PVE::Cluster::get_clinfo();
- if ($clinfo->{cluster}->{name} && $rpcenv->check($username, '/', ['Sys.Audit'], 1)) {
- $res->{clustername} = $clinfo->{cluster}->{name};
- }
- };
- if (my $err = $@) {
- my $clientip = $rpcenv->get_client_ip() || '';
- syslog('err', "openid authentication failure; rhost=$clientip msg=$err");
- # do not return any info to prevent user enumeration attacks
- die PVE::Exception->new("authentication failure\n", code => 401);
- }
-
- PVE::Cluster::log_msg('info', 'root at pam', "successful openid auth for user '$res->{username}'");
-
- return $res;
- }});
+ },
+ permissions => { user => 'world' },
+ code => sub {
+ my ($param) = @_;
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+
+ my $res;
+ eval {
+ my $dcconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
+ local $ENV{all_proxy} = $dcconf->{http_proxy}
+ if exists $dcconf->{http_proxy};
+
+ my ( $realm, $private_auth_state ) =
+ PVE::RS::OpenId::verify_public_auth_state( $openid_state_path,
+ $param->{'state'} );
+
+ my $redirect_url = extract_param( $param, 'redirect-url' );
+
+ my ( $config, $openid ) =
+ $lookup_openid_auth->( $realm, $redirect_url );
+ my $info = $openid->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
+ PVE::Auth::Plugin::verify_username($username);
+
+ if ( $config->{'autocreate'}
+ && !$rpcenv->check_user_exist( $username, 1 ) )
+ {
+ PVE::AccessControl::lock_user_config(
+ sub {
+ my $usercfg = cfs_read_file("user.cfg");
+
+ die "user '$username' already exists\n"
+ if $usercfg->{users}->{$username};
+
+ my $entry = { enable => 1 };
+ if ( defined( 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;
+ }
+
+ $usercfg->{users}->{$username} = $entry;
+
+ cfs_write_file( "user.cfg", $usercfg );
+ },
+ "autocreate openid user failed"
+ );
+ }
+ else {
+ # test if user exists and is enabled
+ $rpcenv->check_user_enabled($username);
+ }
+
+ my $ticket = PVE::AccessControl::assemble_ticket($username);
+ my $csrftoken =
+ PVE::AccessControl::assemble_csrf_prevention_token($username);
+ my $cap = $rpcenv->compute_api_permission($username);
+
+ $res = {
+ ticket => $ticket,
+ username => $username,
+ CSRFPreventionToken => $csrftoken,
+ cap => $cap,
+ };
+
+ my $clinfo = PVE::Cluster::get_clinfo();
+ if ( $clinfo->{cluster}->{name}
+ && $rpcenv->check( $username, '/', ['Sys.Audit'], 1 ) )
+ {
+ $res->{clustername} = $clinfo->{cluster}->{name};
+ }
+ };
+ if ( my $err = $@ ) {
+ my $clientip = $rpcenv->get_client_ip() || '';
+ syslog( 'err',
+ "openid authentication failure; rhost=$clientip msg=$err" );
+
+ # do not return any info to prevent user enumeration attacks
+ die PVE::Exception->new( "authentication failure\n",
+ code => 401 );
+ }
+
+ PVE::Cluster::log_msg( 'info', 'root at pam',
+ "successful openid auth for user '$res->{username}'" );
+
+ return $res;
+ }
+ }
+);
diff --git a/src/PVE/Auth/OpenId.pm b/src/PVE/Auth/OpenId.pm
index c8e4db9..243f6f0 100755
--- a/src/PVE/Auth/OpenId.pm
+++ b/src/PVE/Auth/OpenId.pm
@@ -5,7 +5,8 @@ use warnings;
use PVE::Tools;
use PVE::Auth::Plugin;
-use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_write_file cfs_lock_file);
+use PVE::Cluster
+ qw(cfs_register_file cfs_read_file cfs_write_file cfs_lock_file);
use base qw(PVE::Auth::Plugin);
@@ -15,77 +16,91 @@ sub type {
sub properties {
return {
- "issuer-url" => {
- description => "OpenID Issuer Url",
- type => 'string',
- maxLength => 256,
- },
- "client-id" => {
- description => "OpenID Client ID",
- type => 'string',
- maxLength => 256,
- },
- "client-key" => {
- description => "OpenID Client Key",
- type => 'string',
- optional => 1,
- maxLength => 256,
- },
- autocreate => {
- description => "Automatically create users if they do not exist.",
- optional => 1,
- type => 'boolean',
- default => 0,
- },
- "username-claim" => {
- description => "OpenID claim used to generate the unique username.",
- type => 'string',
- optional => 1,
- },
- 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",
- 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',
- pattern => '^[^\x00-\x1F\x7F <>#"]*$', # Prohibit characters not allowed in URI RFC 2396.
- optional => 1,
- },
- };
+ "issuer-url" => {
+ description => "OpenID Issuer Url",
+ type => 'string',
+ maxLength => 256,
+ },
+ "client-id" => {
+ description => "OpenID Client ID",
+ type => 'string',
+ maxLength => 256,
+ },
+ "client-key" => {
+ description => "OpenID Client Key",
+ type => 'string',
+ optional => 1,
+ maxLength => 256,
+ },
+ autocreate => {
+ description => "Automatically create users if they do not exist.",
+ optional => 1,
+ type => 'boolean',
+ default => 0,
+ },
+ "username-claim" => {
+ description => "OpenID claim used to generate the unique username.",
+ type => 'string',
+ optional => 1,
+ },
+ 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",
+ 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',
+ pattern => '^[^\x00-\x1F\x7F <>#"]*$'
+ , # Prohibit characters not allowed in URI RFC 2396.
+ optional => 1,
+ },
+ 'audiences' => {
+ description =>
+"Specifies the authentication claims neccessary for checking the privileges the requesting user has.",
+ type => 'array',
+ 'items' => {
+ type => 'string',
+ pattern => '^[a-zA-Z0-9-_+.]+$',
+ optional => 1
+ }
+ },
+ };
}
sub options {
return {
- "issuer-url" => {},
- "client-id" => {},
- "client-key" => { optional => 1 },
- autocreate => { optional => 1 },
- "username-claim" => { optional => 1, fixed => 1 },
- prompt => { optional => 1 },
- scopes => { optional => 1 },
- "acr-values" => { optional => 1 },
- default => { optional => 1 },
- comment => { optional => 1 },
+ "issuer-url" => {},
+ "client-id" => {},
+ "client-key" => { optional => 1 },
+ autocreate => { optional => 1 },
+ "username-claim" => { optional => 1, fixed => 1 },
+ prompt => { optional => 1 },
+ scopes => { optional => 1 },
+ "acr-values" => { optional => 1 },
+ "audiences" => { optional => 1 },
+ default => { optional => 1 },
+ comment => { optional => 1 },
};
}
sub authenticate_user {
- my ($class, $config, $realm, $username, $password) = @_;
-
+ my ( $class, $config, $realm, $username, $password ) = @_;
die "OpenID realm does not allow password verification.\n";
}
-
1;
--
2.39.5
More information about the pve-devel
mailing list