public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Dominik Csapak <d.csapak@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH access-control v4 2/4] fix #3668: realm-sync: replace 'full' and 'purge' options with 'remove-vanished'
Date: Mon, 28 Mar 2022 14:38:03 +0200	[thread overview]
Message-ID: <20220328123807.233098-3-d.csapak@proxmox.com> (raw)
In-Reply-To: <20220328123807.233098-1-d.csapak@proxmox.com>

since both configure what is removed when an entry (or property)
is not returned from the sync result.

'remove-vanished' is a list of things to remove: 'acl', 'entry',
'properties'.

'full' maps to 'entry;properties' and
'purge' to 'acl'

Keep the old properties for backwards-compatibiltiy. On create/update, replace
the old values with the new equivalent in 'remove-vanish'.

add a deprecation notice to the description, and a todo to remove with 8.0

this changes two things slightly:
* 'purge' (or remove-vanished acl) does now remove ACLs for vanished
  entries even when we keep those entries. Previously those were only
  deleted when we also removed the entries
  (this can be seen by the changed regression test)
* since 'remove-vanished' is empty by default, only the scope must be
  given explicitely on sync or the sync-default. previosly 'full' and
  'purge' must have been configured explicitely
  (no big deal, since the default is to not remove anything)

bug #3668 is fixed when not selecting 'properties' for the sync, so
that existing (custom) properties are not deleted on update

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 src/PVE/API2/Domains.pm     | 168 ++++++++++++++++++++++++------------
 src/PVE/Auth/Plugin.pm      |  27 ++++--
 src/test/realm_sync_test.pl |   4 +-
 3 files changed, 133 insertions(+), 66 deletions(-)

diff --git a/src/PVE/API2/Domains.pm b/src/PVE/API2/Domains.pm
index 56e8394..870e244 100644
--- a/src/PVE/API2/Domains.pm
+++ b/src/PVE/API2/Domains.pm
@@ -17,6 +17,40 @@ my $domainconfigfile = "domains.cfg";
 
 use base qw(PVE::RESTHandler);
 
+# maps old 'full'/'purge' parameters to new 'remove-vanished'
+# TODO remove when we delete the 'full'/'purge' parameters
+my $map_remove_vanished = sub {
+    my ($opt, $delete_deprecated) = @_;
+
+    if (!defined($opt->{'remove-vanished'}) && ($opt->{full} || $opt->{purge})) {
+	my $props = [];
+	push @$props, 'entry', 'properties' if $opt->{full};
+	push @$props, 'acl' if $opt->{purge};
+	$opt->{'remove-vanished'} = join(';', @$props);
+    }
+
+    if ($delete_deprecated) {
+	delete $opt->{full};
+	delete $opt->{purge};
+    }
+
+    return $opt;
+};
+
+my $map_sync_default_options = sub {
+    my ($cfg, $delete_deprecated) = @_;
+
+    my $opt = $cfg->{'sync-defaults-options'};
+    return if !defined($opt);
+    my $sync_opts_fmt = PVE::JSONSchema::get_format('realm-sync-options');
+
+    my $old_opt = PVE::JSONSchema::parse_property_string($sync_opts_fmt, $opt);
+
+    my $new_opt = $map_remove_vanished->($old_opt, $delete_deprecated);
+
+    $cfg->{'sync-defaults-options'} = PVE::JSONSchema::print_property_string($new_opt, $sync_opts_fmt);
+};
+
 __PACKAGE__->register_method ({
     name => 'index',
     path => '',
@@ -109,6 +143,10 @@ __PACKAGE__->register_method ({
 		die "unable to create builtin type '$type'\n"
 		    if ($type eq 'pam' || $type eq 'pve');
 
+		if ($type eq 'ad' || $type eq 'ldap') {
+		    $map_sync_default_options->($param, 1);
+		}
+
 		my $plugin = PVE::Auth::Plugin->lookup($type);
 		my $config = $plugin->check_config($realm, $param, 1, 1);
 
@@ -173,7 +211,12 @@ __PACKAGE__->register_method ({
 		    $delete_pw = 1 if $opt eq 'password';
 		}
 
-		my $plugin = PVE::Auth::Plugin->lookup($ids->{$realm}->{type});
+		my $type = $ids->{$realm}->{type};
+		if ($type eq 'ad' || $type eq 'ldap') {
+		    $map_sync_default_options->($param, 1);
+		}
+
+		my $plugin = PVE::Auth::Plugin->lookup($type);
 		my $config = $plugin->check_config($realm, $param, 0, 1);
 
 		if ($config->{default}) {
@@ -225,6 +268,11 @@ __PACKAGE__->register_method ({
 	my $data = $cfg->{ids}->{$realm};
 	die "domain '$realm' does not exist\n" if !$data;
 
+	my $type = $data->{type};
+	if ($type eq 'ad' || $type eq 'ldap') {
+	    $map_sync_default_options->($data);
+	}
+
 	$data->{digest} = $cfg->{digest};
 
 	return $data;
@@ -275,51 +323,50 @@ my $update_users = sub {
     my ($usercfg, $realm, $synced_users, $opts) = @_;
 
     print "syncing users\n";
+    print "remove-vanished: $opts->{'remove-vanished'}\n" if defined($opts->{'remove-vanished'});
+
     $usercfg->{users} = {} if !defined($usercfg->{users});
     my $users = $usercfg->{users};
+    my $to_remove = { map { $_ => 1 } split(';', $opts->{'remove-vanished'} // '') };
 
-    my $oldusers = {};
-    if ($opts->{'full'}) {
-	print "full sync, deleting outdated existing users first\n";
-	foreach my $userid (sort keys %$users) {
-	    next if $userid !~ m/\@$realm$/;
-
-	    $oldusers->{$userid} = delete $users->{$userid};
-	    if ($opts->{'purge'} && !$synced_users->{$userid}) {
-		PVE::AccessControl::delete_user_acl($userid, $usercfg);
-		print "purged user '$userid' and all its ACL entries\n";
-	    } elsif (!defined($synced_users->{$userid})) {
-		print "remove user '$userid'\n";
-	    }
+    print "deleting outdated existing users first\n" if $to_remove->{entry};
+    foreach my $userid (sort keys %$users) {
+	next if $userid !~ m/\@$realm$/;
+	next if defined($synced_users->{$userid});
+
+	if ($to_remove->{entry}) {
+	    print "remove user '$userid'\n";
+	    delete $users->{$userid};
+	}
+
+	if ($to_remove->{acl}) {
+	    print "purge users '$userid' ACL entries\n";
+	    PVE::AccessControl::delete_user_acl($userid, $usercfg);
 	}
     }
 
     foreach my $userid (sort keys %$synced_users) {
 	my $synced_user = $synced_users->{$userid} // {};
-	if (!defined($users->{$userid})) {
+	my $olduser = $users->{$userid};
+	if ($to_remove->{properties} || !defined($olduser)) {
+	    # we use the synced user, but want to keep some properties on update
+	    if (defined($olduser)) {
+		print "overwriting user '$userid'\n";
+	    } else {
+		$olduser = {};
+		print "adding user '$userid'\n";
+	    }
 	    my $user = $users->{$userid} = $synced_user;
 
-	    my $olduser = $oldusers->{$userid} // {};
-	    if (defined(my $enabled = $olduser->{enable})) {
-		$user->{enable} = $enabled;
-	    } elsif ($opts->{'enable-new'}) {
-		$user->{enable} = 1;
-	    }
+	    my $enabled = $olduser->{enable} // $opts->{'enable-new'};
+	    $user->{enable} = $enabled if defined($enabled);
+	    $user->{tokens} = $olduser->{tokens} if defined($olduser->{tokens});
 
-	    if (defined($olduser->{tokens})) {
-		$user->{tokens} = $olduser->{tokens};
-	    }
-	    if (defined($oldusers->{$userid})) {
-		print "updated user '$userid'\n";
-	    } else {
-		print "added user '$userid'\n";
-	    }
 	} else {
-	    my $olduser = $users->{$userid};
 	    foreach my $attr (keys %$synced_user) {
 		$olduser->{$attr} = $synced_user->{$attr};
 	    }
-	    print "updated user '$userid'\n";
+	    print "updating user '$userid'\n";
 	}
     }
 };
@@ -328,40 +375,43 @@ my $update_groups = sub {
     my ($usercfg, $realm, $synced_groups, $opts) = @_;
 
     print "syncing groups\n";
+    print "remove-vanished: $opts->{'remove-vanished'}\n" if defined($opts->{'remove-vanished'});
+
     $usercfg->{groups} = {} if !defined($usercfg->{groups});
     my $groups = $usercfg->{groups};
-    my $oldgroups = {};
-
-    if ($opts->{full}) {
-	print "full sync, deleting outdated existing groups first\n";
-	foreach my $groupid (sort keys %$groups) {
-	    next if $groupid !~ m/\-$realm$/;
-
-	    my $oldgroups->{$groupid} = delete $groups->{$groupid};
-	    if ($opts->{purge} && !$synced_groups->{$groupid}) {
-		print "purged group '$groupid' and all its ACL entries\n";
-		PVE::AccessControl::delete_group_acl($groupid, $usercfg)
-	    } elsif (!defined($synced_groups->{$groupid})) {
-		print "removed group '$groupid'\n";
-	    }
+    my $to_remove = { map { $_ => 1 } split(';', $opts->{'remove-vanished'} // '') };
+
+    print "deleting outdated existing groups first\n" if $to_remove->{entry};
+    foreach my $groupid (sort keys %$groups) {
+	next if $groupid !~ m/\-$realm$/;
+	next if defined($synced_groups->{$groupid});
+
+	if ($to_remove->{entry}) {
+	    print "remove group '$groupid'\n";
+	    delete $groups->{$groupid};
+	}
+
+	if ($to_remove->{acl}) {
+	    print "purge groups '$groupid' ACL entries\n";
+	    PVE::AccessControl::delete_group_acl($groupid, $usercfg);
 	}
     }
 
     foreach my $groupid (sort keys %$synced_groups) {
 	my $synced_group = $synced_groups->{$groupid};
-	if (!defined($groups->{$groupid})) {
-	    $groups->{$groupid} = $synced_group;
-	    if (defined($oldgroups->{$groupid})) {
-		print "updated group '$groupid'\n";
+	my $oldgroup = $groups->{$groupid};
+	if ($to_remove->{properties} || !defined($oldgroup)) {
+	    if (defined($oldgroup)) {
+		print "overwriting group '$groupid'\n";
 	    } else {
-		print "added group '$groupid'\n";
+		print "adding group '$groupid'\n";
 	    }
+	    $groups->{$groupid} = $synced_group;
 	} else {
-	    my $group = $groups->{$groupid};
 	    foreach my $attr (keys %$synced_group) {
-		$group->{$attr} = $synced_group->{$attr};
+		$oldgroup->{$attr} = $synced_group->{$attr};
 	    }
-	    print "updated group '$groupid'\n";
+	    print "updating group '$groupid'\n";
 	}
     }
 };
@@ -381,11 +431,15 @@ my $parse_sync_opts = sub {
 	my $fmt = $sync_opts_fmt->{$opt};
 
 	$res->{$opt} = $param->{$opt} // $cfg_defaults->{$opt} // $fmt->{default};
-
-	raise_param_exc({
-	    "$opt" => 'Not passed as parameter and not defined in realm default sync options.'
-	}) if !defined($res->{$opt});
     }
+
+    $map_remove_vanished->($res, 1);
+
+    # only scope has no implicit value
+    raise_param_exc({
+	"scope" => 'Not passed as parameter and not defined in realm default sync options.'
+    }) if !defined($res->{scope});
+
     return $res;
 };
 
diff --git a/src/PVE/Auth/Plugin.pm b/src/PVE/Auth/Plugin.pm
index 1413053..7e431f3 100755
--- a/src/PVE/Auth/Plugin.pm
+++ b/src/PVE/Auth/Plugin.pm
@@ -49,6 +49,8 @@ PVE::JSONSchema::register_standard_option('realm', {
     maxLength => 32,
 });
 
+my $remove_options = "(?:acl|properties|entry)";
+
 my $realm_sync_options_desc = {
     scope => {
 	description => "Select what to sync.",
@@ -56,11 +58,24 @@ my $realm_sync_options_desc = {
 	enum => [qw(users groups both)],
 	optional => '1',
     },
+    'remove-vanished' => {
+	description => "A semicolon-seperated list of things to remove when they or the user"
+	    ." vanishes during a sync. The following values are possible: 'entry' removes the"
+	    ." user/group when not returned from the sync. 'properties' removes the set"
+	    ." properties on existing user/group that do not appear in the source (even custom ones)."
+	    ." 'acl' removes acls when the user/group is not returned from the sync.",
+	type => 'string',
+	typetext => "[acl];[properties];[entry]",
+	pattern => "(?:$remove_options\;)*$remove_options",
+	optional => '1',
+    },
+    # TODO check/rewrite in pve7to8, and remove with 8.0
     full => {
-	description => "If set, uses the LDAP Directory as source of truth,"
-	    ." deleting users or groups not returned from the sync. Otherwise"
-	    ." only syncs information which is not already present, and does not"
-	    ." deletes or modifies anything else.",
+	description => "DEPRECATED: use 'remove-vanished' instead. If set, uses the LDAP Directory as source of truth,"
+	    ." deleting users or groups not returned from the sync and removing"
+	    ." all locally modified properties of synced users. If not set,"
+	    ." only syncs information which is present in the synced data, and does not"
+	    ." delete or modify anything else.",
 	type => 'boolean',
 	optional => '1',
     },
@@ -71,8 +86,8 @@ my $realm_sync_options_desc = {
 	optional => '1',
     },
     purge => {
-	description => "Remove ACLs for users or groups which were removed from"
-	    ." the config during a sync.",
+	description => "DEPRECATED: use 'remove-vanished' instead. Remove ACLs for users or"
+	    ." groups which were removed from the config during a sync.",
 	type => 'boolean',
 	optional => '1',
     },
diff --git a/src/test/realm_sync_test.pl b/src/test/realm_sync_test.pl
index 2a4a132..78089fa 100755
--- a/src/test/realm_sync_test.pl
+++ b/src/test/realm_sync_test.pl
@@ -276,9 +276,7 @@ my $tests = [
 	    },
 	    acl => {
 		'/' => {
-		    users => {
-			'user3@syncedrealm' => {},
-		    },
+		    users => {},
 		    groups => {},
 		},
 	    },
-- 
2.30.2





  parent reply	other threads:[~2022-03-28 12:38 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2022-03-28 12:38 [pve-devel] [PATCH access-control/manager/docs v4] fix #3668: improving realm sync Dominik Csapak
2022-03-28 12:38 ` [pve-devel] [PATCH access-control v4 1/4] add regression tests for realm-sync Dominik Csapak
2022-03-28 12:38 ` Dominik Csapak [this message]
2022-03-28 12:38 ` [pve-devel] [PATCH access-control v4 3/4] convert regression tests to new 'remove-vanished' parameter Dominik Csapak
2022-03-28 12:38 ` [pve-devel] [PATCH access-control v4 4/4] add realm-sync regression test for new 'remove-vanished' Dominik Csapak
2022-03-28 12:38 ` [pve-devel] [PATCH manager v4 1/1] ui: realm sync: replace 'full' and 'purge' with 'remove-vanished' Dominik Csapak
2022-03-28 12:38 ` [pve-devel] [PATCH docs v4 1/1] update documentation about sync-options Dominik Csapak
2022-04-26 12:27 ` [pve-devel] applied-series: [PATCH access-control/manager/docs v4] fix #3668: improving realm sync Thomas Lamprecht

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20220328123807.233098-3-d.csapak@proxmox.com \
    --to=d.csapak@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal