From dd39ffe2180b0471dd5cae3c62d2e69b2f574328 Mon Sep 17 00:00:00 2001
From: Peter Balcirak <peter.balcirak@gmail.com>
Date: Wed, 1 Jul 2020 16:01:58 +0200
Subject: [PATCH] Added filling of guest group in ad_user_mu_ucn

- ad_user_mu_ucn was creating and updating guests in ucn.muni.cz.
  Now it is also adding these users to the group HostPerun which is
  located in a separate tree, so the ad_group_mu_ucn cannot empty the
  group.
---
 send/ad_user_mu_ucn | 163 +++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 162 insertions(+), 1 deletion(-)

diff --git a/send/ad_user_mu_ucn b/send/ad_user_mu_ucn
index 1c85af4c..4616367d 100755
--- a/send/ad_user_mu_ucn
+++ b/send/ad_user_mu_ucn
@@ -15,11 +15,18 @@ use ScriptLock;
 sub process_add;
 sub process_update;
 sub ping_password_setter;
+sub update_group_members;
+sub add_members_to_entry;
+sub remove_members_from_entry;
+sub update_group_membership;
 
 # log counters
 my $counter_add = 0;
 my $counter_update = 0;
 my $counter_fail = 0;
+my $counter_group_updated = 0;
+my $counter_group_updated_with_errors = 0;
+my $counter_group_failed = 0;
 
 # define service
 my $service_name = "ad_user_mu_ucn";
@@ -72,6 +79,7 @@ foreach my $ad_entry (@ad_entries) {
 # process data
 process_add();
 process_update();
+update_group_members();
 
 # disconnect
 ldap_unbind($ldap);
@@ -86,9 +94,25 @@ print "Added: " . $counter_add . " entries.\n";
 print "Updated: " . $counter_update. " entries.\n";
 print "Failed: " . $counter_fail. " entries.\n";
 
+if ($counter_group_updated) {
+	ldap_log($service_name, "Members of the guest group were updated.");
+	print "Members of the guest group were updated.\n";
+} elsif ($counter_group_failed) {
+	ldap_log($service_name, "Updating members of the guest group failed.");
+	print "Updating members of the guest group failed.\n";
+} elsif ($counter_group_updated_with_errors) {
+	ldap_log($service_name, "Members of the guest group were updated with errors.");
+	print "Members of the guest group were updated with errors.\n";
+} else {
+	ldap_log($service_name, "Members of the guest group were not changed.");
+	print "Members of the guest group were not changed.\n";
+}
+
 $lock->unlock();
 
-if ($counter_fail > 0) { die "Failed to process: " . $counter_fail . " entries.\nSee log at: ~/perun-engine/send/logs/$service_name.log";}
+if ($counter_fail or $counter_group_failed or $counter_group_updated_with_errors) {
+	die "Process ended up with errors.\nSee log at: ~/perun-engine/send/logs/$service_name.log";
+}
 
 # END of main script
 
@@ -250,3 +274,140 @@ sub ping_password_setter() {
 	$dbh->disconnect();
 
 }
+
+sub update_group_members {
+	my $filter_groups = '(objectClass=group)';
+	my $group_dn = 'CN=HostPerun,OU=Types,OU=MU,DC=ucn,DC=muni,DC=cz';
+
+	my @per_val = ();
+	foreach my $perun_entry (@perun_entries) {
+		push (@per_val, $perun_entry->dn());
+	}
+
+	# load members of a group from AD based on DN in Perun => Group must exists in AD
+	my @ad_val = load_group_members($ldap, $group_dn, $filter_groups);
+
+	if ($? != 0) {
+		ldap_log($service_name, "Unable to load Perun group members from AD: " . $group_dn);
+		$counter_group_failed++;
+		return;
+	}
+
+	# sort to compare
+	my @sorted_ad_val = sort(@ad_val);
+	my @sorted_per_val = sort(@per_val);
+
+	# compare using smart-match (perl 5.10.1+)
+	unless(@sorted_ad_val ~~ @sorted_per_val) {
+
+		my %ad_val_map = map { $_ => 1 } @sorted_ad_val;
+		my %per_val_map = map { $_ => 1 } @sorted_per_val;
+
+		# we must get reference to real group from AD in order to call "replace"
+		my $response_ad = $ldap->search( base => $group_dn, filter => $filter_groups, scope => 'base' );
+		unless ($response_ad->is_error()) {
+			# SUCCESS
+			my $ad_entry = $response_ad->entry(0);
+			update_group_membership($ad_entry, \%ad_val_map, \%per_val_map);
+
+		} else {
+			# FAIL (to get group from AD)
+			$counter_group_failed++;
+			ldap_log($service_name, "Group members NOT updated: " . $group_dn . " | " . $response_ad->error());
+		}
+	}
+}
+
+sub add_members_to_entry {
+
+	my $ad_entry = shift;
+	my $to_be_added = shift;
+	my $return_code = 0;
+
+	# chunks of size less than 5000 have to be used, because LDAP cannot process more than 5000 operations at once.
+	my @chunks_to_add = ();
+
+	push @chunks_to_add, [ splice @$to_be_added, 0, 4999 ] while @$to_be_added;
+
+	foreach (@chunks_to_add) {
+		$ad_entry->add(
+			'member' => $_
+		);
+		my $response = $ad_entry->update($ldap);
+		if ($response) {
+			unless ($response->is_error()) {
+				ldap_log($service_name, "Group members added: " . $ad_entry->dn() . " | \n" . join(",\n", @$_));
+			} else {
+				ldap_log($service_name, "Group members NOT added: " . $ad_entry->dn() . " | " . $response->error() . " | \n" . join(",\n", @$_));
+				$return_code = 1;
+			}
+		}
+	}
+
+	return $return_code;
+}
+
+sub remove_members_from_entry {
+
+	my $ad_entry = shift;
+	my $to_be_removed = shift;
+	my $return_code = 0;
+
+	# chunks of size less than 5000 have to be used, because LDAP cannot process more than 5000 operations at once.
+	my @chunks_to_remove = ();
+
+	push @chunks_to_remove, [ splice @$to_be_removed, 0, 4999 ] while @$to_be_removed;
+
+	foreach (@chunks_to_remove) {
+		$ad_entry->delete(
+			'member' => $_
+		);
+		my $response = $ad_entry->update($ldap);
+		if ($response) {
+			unless ($response->is_error()) {
+				ldap_log($service_name, "Group members removed: " . $ad_entry->dn() . " | \n" . join(",\n", @$_));
+			} else {
+				ldap_log($service_name, "Group members NOT removed: " . $ad_entry->dn() . " | " . $response->error() . " | \n" . join(",\n", @$_));
+				$return_code = 1;
+			}
+		}
+	}
+
+	return $return_code;
+}
+
+sub update_group_membership {
+
+	my $ad_entry = shift;
+	my $ad_members_state = shift;
+	my $perun_members_state = shift;
+
+	my @to_be_added = ();
+	my @to_be_removed = ();
+
+	foreach (keys %{$perun_members_state}) {
+		unless (defined $ad_members_state->{$_}) {
+			push (@to_be_added, $_);
+		}
+	}
+
+	foreach (keys %{$ad_members_state}) {
+		unless (defined $perun_members_state->{$_}) {
+			push (@to_be_removed, $_);
+		}
+	}
+
+	if (@to_be_added or @to_be_removed) {
+		@to_be_added = sort @to_be_added;
+		@to_be_removed = sort @to_be_removed;
+
+		my $response_add = add_members_to_entry($ad_entry, \@to_be_added);
+		my $response_remove = remove_members_from_entry($ad_entry, \@to_be_removed);
+
+		if ($response_add or $response_remove) {
+			$counter_group_updated_with_errors++;
+		} else {
+			$counter_group_updated++;
+		}
+	}
+}
-- 
GitLab