diff --git a/PVE/API2/Qemu.pm b/PVE/API2/Qemu.pm index c6a4cd2a..78c5a7f6 100644 --- a/PVE/API2/Qemu.pm +++ b/PVE/API2/Qemu.pm @@ -32,6 +32,7 @@ use PVE::QemuServer::Drive; use PVE::QemuServer::ImportDisk; use PVE::QemuServer::Monitor qw(mon_cmd); use PVE::QemuServer::Machine; +use PVE::QemuServer::USB qw(parse_usb_device); use PVE::QemuMigrate; use PVE::RPCEnvironment; use PVE::AccessControl; @@ -588,11 +589,15 @@ my sub check_usb_perm { return 1 if $authuser eq 'root@pam'; + $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']); + my $device = PVE::JSONSchema::parse_property_string('pve-qm-usb', $value); - if ($device->{host} =~ m/^spice$/i) { - $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.HWType']); - } else { + if ($device->{host} && $device->{host} !~ m/^spice$/i) { die "only root can set '$opt' config for real devices\n"; + } elsif ($device->{mapping}) { + $rpcenv->check_full($authuser, "/mapping/usb/$device->{mapping}", ['Mapping.Use']); + } else { + die "either 'host' or 'mapping' must be set.\n"; } return 1; @@ -3517,6 +3522,7 @@ __PACKAGE__->register_method({ my $oldconf = $snapname ? $conf->{snapshots}->{$snapname} : $conf; my $sharedvm = &$check_storage_access_clone($rpcenv, $authuser, $storecfg, $oldconf, $storage); + PVE::QemuServer::check_mapping_access($rpcenv, $authuser, $oldconf); PVE::QemuServer::check_bridge_access($rpcenv, $authuser, $oldconf); diff --git a/PVE/QemuServer.pm b/PVE/QemuServer.pm index 38200fea..9341b1ae 100644 --- a/PVE/QemuServer.pm +++ b/PVE/QemuServer.pm @@ -6473,6 +6473,31 @@ sub check_bridge_access { return 1; }; +sub check_mapping_access { + my ($rpcenv, $user, $conf) = @_; + + for my $opt (keys $conf->%*) { + if ($opt =~ m/^usb\d+$/) { + my $device = PVE::JSONSchema::parse_property_string('pve-qm-usb', $conf->{$opt}); + if (my $host = $device->{host}) { + die "only root can set '$opt' config for real devices\n" + if $host !~ m/^spice$/i && $user ne 'root@pam'; + } elsif ($device->{mapping}) { + $rpcenv->check_full($user, "/mapping/usb/$device->{mapping}", ['Mapping.Use']); + } else { + die "either 'host' or 'mapping' must be set.\n"; + } + } + } +}; + +# FIXME: improve checks on restore by checking before actually extracing and +# merging the new config +sub check_restore_permissions { + my ($rpcenv, $user, $conf) = @_; + check_bridge_access($rpcenv, $user, $conf); + check_mapping_access($rpcenv, $user, $conf); +} # vzdump restore implementaion sub tar_archive_read_firstfile { @@ -7117,7 +7142,7 @@ sub restore_proxmox_backup_archive { } my $new_conf = $restore_merge_config->($conffile, $new_conf_raw, $options->{override_conf}); - check_bridge_access($rpcenv, $user, $new_conf); + check_restore_permissions($rpcenv, $user, $new_conf); PVE::QemuConfig->write_config($vmid, $new_conf); eval { rescan($vmid, 1); }; @@ -7431,7 +7456,7 @@ sub restore_vma_archive { } my $new_conf = $restore_merge_config->($conffile, $new_conf_raw, $opts->{override_conf}); - check_bridge_access($rpcenv, $user, $new_conf); + check_restore_permissions($rpcenv, $user, $new_conf); PVE::QemuConfig->write_config($vmid, $new_conf); eval { rescan($vmid, 1); }; diff --git a/PVE/QemuServer/USB.pm b/PVE/QemuServer/USB.pm index fe541c1f..49957444 100644 --- a/PVE/QemuServer/USB.pm +++ b/PVE/QemuServer/USB.pm @@ -6,6 +6,7 @@ use PVE::QemuServer::PCI qw(print_pci_addr); use PVE::QemuServer::Machine; use PVE::QemuServer::Helpers qw(min_version windows_version); use PVE::JSONSchema; +use PVE::Mapping::USB; use base 'Exporter'; our @EXPORT_OK = qw( @@ -24,6 +25,7 @@ my $USB_PATH_RE = qr/(\d+)\-(\d+(\.\d+)*)/; my $usb_fmt = { host => { default_key => 1, + optional => 1, type => 'string', pattern => qr/(?:(?:$USB_ID_RE)|(?:$USB_PATH_RE)|[Ss][Pp][Ii][Cc][Ee])/, format_description => 'HOSTUSBDEVICE|spice', @@ -40,8 +42,18 @@ NOTE: This option allows direct access to host hardware. So it is no longer poss machines - use with special care. The value 'spice' can be used to add a usb redirection devices for spice. + +Either this or the 'mapping' key must be set. EODESCR }, + mapping => { + optional => 1, + type => 'string', + format_description => 'mapping-id', + format => 'pve-configid', + description => "The ID of a cluster wide mapping. Either this or the default-key 'host'" + ." must be set.", + }, usb3 => { optional => 1, type => 'boolean', @@ -63,21 +75,38 @@ our $usbdesc = { PVE::JSONSchema::register_standard_option("pve-qm-usb", $usbdesc); sub parse_usb_device { - my ($value) = @_; + my ($value, $mapping) = @_; - return if !$value; + return if $value && $mapping; # not a valid configuration my $res = {}; - if ($value =~ m/^$USB_ID_RE$/) { - $res->{vendorid} = $2; - $res->{productid} = $4; - } elsif ($value =~ m/^$USB_PATH_RE$/) { - $res->{hostbus} = $1; - $res->{hostport} = $2; - } elsif ($value =~ m/^spice$/i) { - $res->{spice} = 1; - } else { - return; + if (defined($value)) { + if ($value =~ m/^$USB_ID_RE$/) { + $res->{vendorid} = $2; + $res->{productid} = $4; + } elsif ($value =~ m/^$USB_PATH_RE$/) { + $res->{hostbus} = $1; + $res->{hostport} = $2; + } elsif ($value =~ m/^spice$/i) { + $res->{spice} = 1; + } + } elsif (defined($mapping)) { + my $devices = PVE::Mapping::USB::find_on_current_node($mapping); + die "USB device mapping not found for '$mapping'\n" if !$devices || !scalar($devices->@*); + die "More than one USB mapping per host not supported\n" if scalar($devices->@*) > 1; + eval { + PVE::Mapping::USB::assert_valid($mapping, $devices->[0]); + }; + if (my $err = $@) { + die "USB Mapping invalid (hardware probably changed): $err\n"; + } + my $device = $devices->[0]; + + if ($device->{path}) { + $res = parse_usb_device($device->{path}); + } else { + $res = parse_usb_device($device->{id}); + } } return $res; @@ -199,7 +228,7 @@ sub print_usbdevice_full { $usbdevice .= ",port=$port" if defined($port); } - my $parsed = parse_usb_device($device->{host}); + my $parsed = parse_usb_device($device->{host}, $device->{mapping}); if (defined($parsed->{vendorid}) && defined($parsed->{productid})) { $usbdevice .= ",vendorid=0x$parsed->{vendorid},productid=0x$parsed->{productid}";