From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 3CD9E1FF13B for ; Wed, 03 Jun 2026 16:56:54 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id D424310861; Wed, 3 Jun 2026 16:56:50 +0200 (CEST) From: David Riley To: pve-devel@lists.proxmox.com Subject: [PATCH pve-access-control v3 3/5] fix: #7520: sdn: add VNet ACL migration Date: Wed, 3 Jun 2026 16:55:21 +0200 Message-ID: <20260603145523.120075-4-d.riley@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260603145523.120075-1-d.riley@proxmox.com> References: <20260603145523.120075-1-d.riley@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1780498532151 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.220 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 Message-ID-Hash: KZ7H6YAUDKPGVABOOCRZWLJXRFFIYX6I X-Message-ID-Hash: KZ7H6YAUDKPGVABOOCRZWLJXRFFIYX6I X-MailFrom: d.riley@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: David Riley X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Moving VNets between zones can lead to stale or orphaned ACL entries. Introduce a migration routine to safely relocate ACLs during VNet moves to maintain configuration integrity. * Conflict Validation: Abort the migration and the SDN apply operation if the destination path has existing permissions. This prevents accidental privilege escalation or overwrites. * Relocation: Move ACLs to the new zone path. * Error Handling: Report both the source and destination paths on failure to guide manual resolution. Link: https://bugzilla.proxmox.com/show_bug.cgi?id=7520 Signed-off-by: David Riley --- src/PVE/AccessControl.pm | 86 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/src/PVE/AccessControl.pm b/src/PVE/AccessControl.pm index 7581394..b7bb3eb 100644 --- a/src/PVE/AccessControl.pm +++ b/src/PVE/AccessControl.pm @@ -2020,6 +2020,92 @@ sub lookup_acl_node { return ($parent, $last_key, $current); } +sub migrate_sdn_resource_access { + my ($migrations) = @_; # [ { src_path => [...], dest_path => [...] } ] + return if !$migrations || !@$migrations; + + my $migrate_fn = sub { + my $usercfg = cfs_read_file("user.cfg"); + my $modified = 0; + + my @prepared_moves; + foreach my $migration (@$migrations) { + my $src_path = ['sdn', @{ $migration->{src_path} }]; + my $dest_path = ['sdn', @{ $migration->{dest_path} }]; + + my ($src_parent, $src_last_key, $acl_node) = + lookup_acl_node($usercfg->{acl_root}, $src_path); + + if ($src_parent && $src_last_key && $acl_node) { + # probe destination for conflicts + my (undef, undef, $existing_dest_node) = + lookup_acl_node($usercfg->{acl_root}, $dest_path); + + if (acl_tree_has_permissions($existing_dest_node)) { + my $conflict_path = '/' . join('/', @$dest_path); + my $source_path = '/' . join('/', @$src_path); + die + "Destination '$conflict_path' already has permissions configured (Source:" + . " '$source_path'). Please remove the target ACLs manually before " + . "retrying.\n"; + } + # stage move + push @prepared_moves, + { + src_parent => $src_parent, + src_last_key => $src_last_key, + dest_path => $dest_path, + node => $acl_node, + }; + } + } + + foreach my $move (@prepared_moves) { + delete $move->{src_parent}->{children}->{ $move->{src_last_key} }; + + my $current = $usercfg->{acl_root}; + my @dest_segments = @{ $move->{dest_path} }; + my $dest_last_key = pop @dest_segments; + + foreach my $segment (@dest_segments) { + $current = ($current->{children}->{$segment} //= {}); + } + + $current->{children} //= {}; + $current->{children}->{$dest_last_key} = $move->{node}; + + $modified = 1; + } + + if ($modified) { + cfs_write_file("user.cfg", $usercfg); + } + }; + + lock_user_config($migrate_fn, "ACL migration failed"); +} + +# Recursively checks a node and all its children for active permissions +sub acl_tree_has_permissions { + my ($node) = @_; + + return 0 unless ref($node) eq 'HASH'; + + foreach my $key (keys %$node) { + return 1 if $key ne 'children'; + } + + if (ref($node->{children}) eq 'HASH') { + foreach my $child_key (keys %{ $node->{children} }) { + if (acl_tree_has_permissions($node->{children}->{$child_key})) { + return 1; + } + } + } + + return 0; +} + my $USER_CONTROLLED_TFA_TYPES = { u2f => 1, oath => 1, -- 2.47.3