#!/usr/bin/env perl use strict; use warnings; use lib qw(..); use Test::More; use Test::MockModule; use PVE::AccessControl; use PVE::RPCEnvironment; use PVE::API2::OpenId; my $realm_config = { type => 'openid', 'issuer-url' => 'https://idp.example.com', 'client-id' => 'pve', autocreate => 1, 'groups-claim' => 'groups', }; my $user_cfg_raw; my $user_cfg_version = 1; my $read_file = sub { my ($filename) = @_; return PVE::AccessControl::parse_user_config('/etc/pve/user.cfg', $user_cfg_raw) if $filename eq 'user.cfg'; return { ids => { oidc => $realm_config } } if $filename eq 'domains.cfg'; return {} if $filename eq 'datacenter.cfg'; die "unexpected read of '$filename'\n"; }; my $write_file = sub { my ($filename, $cfg) = @_; die "unexpected write of '$filename'\n" if $filename ne 'user.cfg'; $user_cfg_raw = PVE::AccessControl::write_user_config('/etc/pve/user.cfg', $cfg); $user_cfg_version++; }; my $cluster_module = Test::MockModule->new('PVE::Cluster'); $cluster_module->noop('cfs_update', 'log_msg'); $cluster_module->mock( cfs_file_version => sub { return ($user_cfg_version, {}) }, cfs_read_file => $read_file, get_clinfo => sub { return {} }, ); my $access_control_module = Test::MockModule->new('PVE::AccessControl'); $access_control_module->mock( cfs_lock_file => sub { my ($filename, $timeout, $code) = @_; my $res = eval { $code->() }; return $res; }, assemble_ticket => sub { return "ticket:$_[0]" }, assemble_csrf_prevention_token => sub { return "csrf:$_[0]" }, ); my $openid_api_module = Test::MockModule->new('PVE::API2::OpenId'); $openid_api_module->mock( cfs_read_file => $read_file, cfs_write_file => $write_file, syslog => sub { }, ); my $claims; my $openid_module = Test::MockModule->new('PVE::RS::OpenId'); $openid_module->mock( verify_public_auth_state => sub { return ('oidc', 'private-state') }, discover => sub { return bless {}, 'PVE::RS::OpenId' }, verify_authorization_code => sub { return $claims }, ); my $rpcenv = PVE::RPCEnvironment->init('cli'); my ($handler, $handler_info) = PVE::API2::OpenId->find_handler('POST', 'login'); # each test is comprised of the following keys: # description => what the test is about # user_cfg => user.cfg before the login # claims => claims returned by the OpenID provider # expected_vms_cap => VM privileges the login result must contain my $tests = [ { description => 'new user added to a group with ACLs', user_cfg => <<"EOF", group:tenant-oidc::: acl:1:/vms:\@tenant-oidc:PVEVMAdmin: EOF claims => { sub => 'alice', groups => ['tenant'] }, expected_vms_cap => ['VM.Allocate', 'VM.PowerMgmt'], }, { description => 'existing user newly added to a group with ACLs', user_cfg => <<"EOF", user:bob\@oidc:1:0::::: group:tenant-oidc::: acl:1:/vms:\@tenant-oidc:PVEVMAdmin: EOF claims => { sub => 'bob', groups => ['tenant'] }, expected_vms_cap => ['VM.Allocate', 'VM.PowerMgmt'], }, ]; for my $test (@$tests) { $user_cfg_raw = $test->{user_cfg}; $claims = $test->{claims}; $rpcenv->init_request(); my $login = eval { $handler->handle( $handler_info, { state => 'public-state', code => 'authorization-code', 'redirect-url' => 'https://pve.example.com:8006', }, ); }; is($@, '', "$test->{description}: login succeeds"); my $vms_cap = $login->{cap}->{vms} // {}; my $missing = [grep { !$vms_cap->{$_} } $test->{expected_vms_cap}->@*]; is_deeply($missing, [], "$test->{description}: permissions are active right away"); } done_testing();