From mboxrd@z Thu Jan  1 00:00:00 1970
Return-Path: <d.csapak@proxmox.com>
Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68])
 (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
 key-exchange X25519 server-signature RSA-PSS (2048 bits))
 (No client certificate requested)
 by lists.proxmox.com (Postfix) with ESMTPS id 3E14AA1F8E
 for <pve-devel@lists.proxmox.com>; Fri, 16 Jun 2023 15:05:45 +0200 (CEST)
Received: from firstgate.proxmox.com (localhost [127.0.0.1])
 by firstgate.proxmox.com (Proxmox) with ESMTP id 12A2733264
 for <pve-devel@lists.proxmox.com>; Fri, 16 Jun 2023 15:05:45 +0200 (CEST)
Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com
 [94.136.29.106])
 (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
 key-exchange X25519 server-signature RSA-PSS (2048 bits))
 (No client certificate requested)
 by firstgate.proxmox.com (Proxmox) with ESMTPS
 for <pve-devel@lists.proxmox.com>; Fri, 16 Jun 2023 15:05:43 +0200 (CEST)
Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1])
 by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 959E545B6C
 for <pve-devel@lists.proxmox.com>; Fri, 16 Jun 2023 15:05:43 +0200 (CEST)
From: Dominik Csapak <d.csapak@proxmox.com>
To: pve-devel@lists.proxmox.com
Date: Fri, 16 Jun 2023 15:05:22 +0200
Message-Id: <20230616130542.199182-3-d.csapak@proxmox.com>
X-Mailer: git-send-email 2.30.2
In-Reply-To: <20230616130542.199182-1-d.csapak@proxmox.com>
References: <20230616130542.199182-1-d.csapak@proxmox.com>
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit
X-SPAM-LEVEL: Spam detection results:  0
 AWL 0.015 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DMARC_MISSING             0.1 Missing DMARC policy
 KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment
 SPF_HELO_NONE           0.001 SPF: HELO does not publish an SPF Record
 SPF_PASS               -0.001 SPF: sender matches SPF record
 T_SCC_BODY_TEXT_LINE    -0.01 -
Subject: [pve-devel] [PATCH qemu-server v7 2/7] enable cluster mapped USB
 devices for guests
X-BeenThere: pve-devel@lists.proxmox.com
X-Mailman-Version: 2.1.29
Precedence: list
List-Id: Proxmox VE development discussion <pve-devel.lists.proxmox.com>
List-Unsubscribe: <https://lists.proxmox.com/cgi-bin/mailman/options/pve-devel>, 
 <mailto:pve-devel-request@lists.proxmox.com?subject=unsubscribe>
List-Archive: <http://lists.proxmox.com/pipermail/pve-devel/>
List-Post: <mailto:pve-devel@lists.proxmox.com>
List-Help: <mailto:pve-devel-request@lists.proxmox.com?subject=help>
List-Subscribe: <https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel>, 
 <mailto:pve-devel-request@lists.proxmox.com?subject=subscribe>
X-List-Received-Date: Fri, 16 Jun 2023 13:05:45 -0000

this patch allows configuring usb devices that are mapped via
cluster resource mapping when the user has 'Mapping.Use' on the ACL
path '/mapping/usb/{ID}' (in addition to the usual required vm config
privileges)

for now, this is only valid if there is exactly one mapping for the
host, since we don't track passed through usb devices yet

This now also checks permissions on clone/restore, meaning a
'non-mapped' device can only be cloned/restored as root@pam user.
That is a breaking change.

Refactor the checks for restoring into a sub, so we have central place
where we can add such checks

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from v6:
* adapted to refactor
* deduplicated permission checks
* use seperate 'mapping' property for the mapping instead of reusing 'host'
* added a FIXME comment for the restore permission checks

 PVE/API2/Qemu.pm      | 12 +++++++---
 PVE/QemuServer.pm     | 29 +++++++++++++++++++++--
 PVE/QemuServer/USB.pm | 55 +++++++++++++++++++++++++++++++++----------
 3 files changed, 78 insertions(+), 18 deletions(-)

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}";
-- 
2.30.2