diff options
Diffstat (limited to 'contrib/moco-ldap-check.pl')
-rwxr-xr-x | contrib/moco-ldap-check.pl | 542 |
1 files changed, 542 insertions, 0 deletions
diff --git a/contrib/moco-ldap-check.pl b/contrib/moco-ldap-check.pl new file mode 100755 index 000000000..7a3a6ca8c --- /dev/null +++ b/contrib/moco-ldap-check.pl @@ -0,0 +1,542 @@ +#!/usr/bin/perl + +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +use strict; +use warnings; + +use FindBin qw($Bin); +use lib "$Bin/.."; +use lib "$Bin/../lib"; + +use Bugzilla; +use Bugzilla::Constants; +use Bugzilla::Group; +use Bugzilla::Mailer; +use Data::Dumper; +use File::Slurp; +use Getopt::Long; +use Net::LDAP; +use Safe; + +# + +use constant BUGZILLA_IGNORE => <<'EOF'; + infra+bot@mozilla.com # Mozilla Infrastructure Bot + qa-auto@mozilla.com # QA Desktop Automation + qualys@mozilla.com # Qualys Security Scanner + recruiting@mozilla.com # Recruiting + release@mozilla.com # Mozilla RelEng Bot + sumo-dev@mozilla.com # SUMOdev [:sumodev] + airmozilla@mozilla.com # Air Mozilla + ux-review@mozilla.com + release-mgmt@mozilla.com + reps@mozilla.com + moz_bug_r_a4@mozilla.com # Security contractor + nightwatch@mozilla.com # Security distribution list for whines +EOF + +use constant LDAP_IGNORE => <<'EOF'; + airmozilla@mozilla.com # Air Mozilla +EOF + +# REPORT_SENDER has to be a valid @mozilla.com LDAP account +use constant REPORT_SENDER => 'bjones@mozilla.com'; + +use constant BMO_RECIPIENTS => qw( + glob@mozilla.com + dkl@mozilla.com +); + +use constant SUPPORT_RECIPIENTS => qw( + desktop@mozilla.com +); + +# + +my ($ldap_host, $ldap_user, $ldap_pass, $debug, $no_update); +GetOptions('h=s' => \$ldap_host, + 'u=s' => \$ldap_user, + 'p=s' => \$ldap_pass, + 'd' => \$debug, + 'n' => \$no_update); +die "syntax: -h ldap_host -u ldap_user -p ldap_pass\n" + unless $ldap_host && $ldap_user && $ldap_pass; + +my $data_dir = bz_locations()->{'datadir'} . '/moco-ldap-check'; +mkdir($data_dir) unless -d $data_dir; + +if ($ldap_user !~ /,/) { + $ldap_user = "mail=$ldap_user,o=com,dc=mozilla"; +} + +# +# group members +# + +my @bugzilla_ignore; +foreach my $line (split(/\n/, BUGZILLA_IGNORE)) { + $line =~ s/^([^#]+)#.*$/$1/; + $line =~ s/(^\s+|\s+$)//g; + push @bugzilla_ignore, clean_email($line); +} + +my @bugzilla_moco; +if ($no_update && -s "$data_dir/bugzilla_moco.last") { + $debug && print "Using cached user list from Bugzilla...\n"; + my $ra = deserialise("$data_dir/bugzilla_moco.last"); + @bugzilla_moco = @$ra; +} else { + $debug && print "Getting user list from Bugzilla...\n"; + + my $group = Bugzilla::Group->new({ name => 'mozilla-corporation' }) + or die "Failed to find group mozilla-corporation\n"; + + foreach my $user (@{ $group->members_non_inherited }) { + next unless $user->is_enabled; + my $mail = clean_email($user->login); + my $name = trim($user->name); + $name =~ s/\s+/ /g; + next if grep { $mail eq $_ } @bugzilla_ignore; + push @bugzilla_moco, { + mail => $user->login, + canon => $mail, + name => $name, + }; + } + + @bugzilla_moco = sort { $a->{mail} cmp $b->{mail} } @bugzilla_moco; + serialise("$data_dir/bugzilla_moco.last", \@bugzilla_moco); +} + +# +# build list of current mo-co bugmail accounts +# + +my @ldap_ignore; +foreach my $line (split(/\n/, LDAP_IGNORE)) { + $line =~ s/^([^#]+)#.*$/$1/; + $line =~ s/(^\s+|\s+$)//g; + push @ldap_ignore, canon_email($line); +} + +my %ldap; +if ($no_update && -s "$data_dir/ldap.last") { + $debug && print "Using cached user list from LDAP...\n"; + my $rh = deserialise("$data_dir/ldap.last"); + %ldap = %$rh; +} else { + $debug && print "Logging into LDAP as $ldap_user...\n"; + my $ldap = Net::LDAP->new($ldap_host, + scheme => 'ldaps', onerror => 'die') or die "$@"; + $ldap->bind($ldap_user, password => $ldap_pass); + foreach my $ldap_base ('o=com,dc=mozilla', 'o=org,dc=mozilla') { + $debug && print "Getting user list from LDAP $ldap_base...\n"; + my $result = $ldap->search( + base => $ldap_base, + scope => 'sub', + filter => '(mail=*)', + attrs => ['mail', 'bugzillaEmail', 'emailAlias', 'cn', 'employeeType'], + ); + foreach my $entry ($result->entries) { + my ($name, $bugMail, $mail, $type) = + map { $entry->get_value($_) || '' } + qw(cn bugzillaEmail mail employeeType); + next if $type eq 'DISABLED'; + $mail = lc $mail; + next if grep { $_ eq canon_email($mail) } @ldap_ignore; + $bugMail = '' if $bugMail !~ /\@/; + $bugMail =~ s/(^\s+|\s+$)//g; + if ($bugMail =~ / /) { + $bugMail = (grep { /\@/ } split / /, $bugMail)[0]; + } + $name =~ s/\s+/ /g; + $ldap{$mail}{name} = trim($name); + $ldap{$mail}{bugmail} = $bugMail; + $ldap{$mail}{bugmail_canon} = canon_email($bugMail); + $ldap{$mail}{aliases} = []; + foreach my $alias ( + @{$entry->get_value('emailAlias', asref => 1) || []} + ) { + push @{$ldap{$mail}{aliases}}, canon_email($alias); + } + } + $debug && printf "Found %s entries\n", scalar($result->entries); + } + serialise("$data_dir/ldap.last", \%ldap); +} + +# +# validate all bugmail entries from the phonebook +# + +my %bugzilla_login; +if ($no_update && -s "$data_dir/bugzilla_login.last") { + $debug && print "Using cached bugzilla checks...\n"; + my $rh = deserialise("$data_dir/bugzilla_login.last"); + %bugzilla_login = %$rh; +} else { + my %logins; + foreach my $mail (keys %ldap) { + $logins{$mail} = 1; + $logins{$ldap{$mail}{bugmail}} = 1 if $ldap{$mail}{bugmail}; + } + my @logins = sort keys %logins; + $debug && print "Checking " . scalar(@logins) . " bugmail accounts...\n"; + + foreach my $login (@logins) { + if (Bugzilla::User->new({ name => $login })) { + $bugzilla_login{$login} = 1; + } + } + serialise("$data_dir/bugzilla_login.last", \%bugzilla_login); +} + +# +# load previous ldap list +# + +my %ldap_old; +{ + my $rh = deserialise("$data_dir/ldap.data"); + %ldap_old = %$rh if $rh; +} + +# +# save current ldap list +# + +{ + serialise("$data_dir/ldap.data", \%ldap); +} + +# +# new ldap accounts +# + +my @new_ldap; +{ + foreach my $mail (sort keys %ldap) { + next if exists $ldap_old{$mail}; + push @new_ldap, { + mail => $mail, + name => $ldap{$mail}{name}, + bugmail => $ldap{$mail}{bugmail}, + }; + } +} + +# +# deleted ldap accounts +# + +my @gone_ldap_bmo; +my @gone_ldap_no_bmo; +{ + foreach my $mail (sort keys %ldap_old) { + next if exists $ldap{$mail}; + if ($ldap_old{$mail}{bugmail}) { + push @gone_ldap_bmo, { + mail => $mail, + name => $ldap_old{$mail}{name}, + bugmail => $ldap_old{$mail}{bugmail}, + } + } else { + push @gone_ldap_no_bmo, { + mail => $mail, + name => $ldap_old{$mail}{name}, + } + } + } +} + +# +# check bugmail entry for all users in bmo/moco group +# + +my @suspect_bugzilla; +my @invalid_bugzilla; +foreach my $rh (@bugzilla_moco) { + my @check = ($rh->{mail}, $rh->{canon}); + if ($rh->{mail} =~ /^([^\@]+)\@mozilla\.org$/) { + push @check, "$1\@mozilla.com"; + } + + my $exists; + foreach my $check (@check) { + $exists = 0; + + # don't complain about deleted accounts + if (grep { $_->{mail} eq $check } (@gone_ldap_bmo, @gone_ldap_no_bmo)) { + $exists = 1; + last; + } + + # check for matching bugmail entry + foreach my $mail (sort keys %ldap) { + next unless $ldap{$mail}{bugmail_canon} eq $check; + $exists = 1; + last; + } + last if $exists; + + # check for matching mail + $exists = 0; + foreach my $mail (sort keys %ldap) { + next unless $mail eq $check; + $exists = 1; + last; + } + last if $exists; + + # check for matching email alias + $exists = 0; + foreach my $mail (sort keys %ldap) { + next unless grep { $check eq $_ } @{$ldap{$mail}{aliases}}; + $exists = 1; + last; + } + last if $exists; + } + + if (!$exists) { + # flag the account + if ($rh->{mail} =~ /\@mozilla\.(com|org)$/i) { + push @invalid_bugzilla, { + mail => $rh->{mail}, + name => $rh->{name}, + }; + } else { + push @suspect_bugzilla, { + mail => $rh->{mail}, + name => $rh->{name}, + }; + } + } +} + +# +# check bugmail entry for ldap users +# + +my @ldap_unblessed; +my @invalid_ldap; +my @invalid_bugmail; +foreach my $mail (sort keys %ldap) { + # try to find the bmo account + my $found; + foreach my $address ($ldap{$mail}{bugmail}, $ldap{$mail}{bugmail_canon}, $mail, @{$ldap{$mail}{aliases}}) { + if (exists $bugzilla_login{$address}) { + $found = $address; + last; + } + } + + # not on bmo + if (!$found) { + # if they have specified a bugmail account, warn, otherwise ignore + if ($ldap{$mail}{bugmail}) { + if (grep { $_->{canon} eq $ldap{$mail}{bugmail_canon} } @bugzilla_moco) { + push @invalid_bugmail, { + mail => $mail, + name => $ldap{$mail}{name}, + bugmail => $ldap{$mail}{bugmail}, + }; + } else { + push @invalid_ldap, { + mail => $mail, + name => $ldap{$mail}{name}, + bugmail => $ldap{$mail}{bugmail}, + }; + } + } + next; + } + + # warn about mismatches + if ($ldap{$mail}{bugmail} && $found ne $ldap{$mail}{bugmail}) { + push @invalid_bugmail, { + mail => $mail, + name => $ldap{$mail}{name}, + bugmail => $ldap{$mail}{bugmail}, + }; + } + + # warn about unblessed accounts + if ($mail =~ /\@mozilla\.com$/) { + unless (grep { $_->{mail} eq $found || $_->{canon} eq canon_email($found) } @bugzilla_moco) { + push @ldap_unblessed, { + mail => $mail, + name => $ldap{$mail}{name}, + bugmail => $ldap{$mail}{bugmail} || $mail, + }; + } + } +} + +# +# reports +# + +my @bmo_report; +push @bmo_report, generate_report( + 'new ldap accounts', + 'no action required', + @new_ldap); + +push @bmo_report, generate_report( + 'deleted ldap accounts', + 'disable bmo account', + @gone_ldap_bmo); + +push @bmo_report, generate_report( + 'deleted ldap accounts', + 'no action required (no bmo account)', + @gone_ldap_no_bmo); + +push @bmo_report, generate_report( + 'suspect bugzilla accounts', + 'remove from mo-co if required', + @suspect_bugzilla); + +push @bmo_report, generate_report( + 'miss-configured bugzilla accounts', + 'ask owner to update phonebook, disable if not on phonebook', + @invalid_bugzilla); + +push @bmo_report, generate_report( + 'ldap accounts without mo-co group', + 'verify, and add mo-co group to bmo account', + @ldap_unblessed); + +push @bmo_report, generate_report( + 'missmatched bugmail entries on ldap accounts', + 'ask owner to update phonebook', + @invalid_bugmail); + +push @bmo_report, generate_report( + 'invalid bugmail entries on ldap accounts', + 'ask owner to update phonebook', + @invalid_ldap); + +if (!scalar @bmo_report) { + push @bmo_report, '**'; + push @bmo_report, '** nothing to report \o/'; + push @bmo_report, '**'; +} + +email_report(\@bmo_report, 'moco-ldap-check', BMO_RECIPIENTS); + +my @support_report; + +push @support_report, generate_report( + 'Missmatched "Bugzilla Email" entries on LDAP accounts', + 'Ask owner to update phonebook, or update directly', + @invalid_bugmail); + +push @support_report, generate_report( + 'Invalid "Bugzilla Email" entries on LDAP accounts', + 'Ask owner to update phonebook', + @invalid_ldap); + +if (scalar @support_report) { + email_report(\@support_report, 'Invalid "Bugzilla Email" entries in LDAP', SUPPORT_RECIPIENTS); +} + +# +# +# + +sub generate_report { + my ($title, $action, @lines) = @_; + + my $count = scalar @lines; + return unless $count; + + my @report; + push @report, ''; + push @report, '**'; + push @report, "** $title ($count)"; + push @report, "** [ $action ]"; + push @report, '**'; + push @report, ''; + + my $max_length = 0; + foreach my $rh (@lines) { + $max_length = length($rh->{mail}) if length($rh->{mail}) > $max_length; + } + + foreach my $rh (@lines) { + my $template = "%-${max_length}s %s"; + my @fields = ($rh->{mail}, $rh->{name}); + + if ($rh->{bugmail}) { + $template .= ' (%s)'; + push @fields, $rh->{bugmail}; + }; + + push @report, sprintf($template, @fields); + } + + return @report; +} + +sub email_report { + my ($report, $subject, @recipients) = @_; + unshift @$report, ( + "Subject: $subject", + 'X-Bugzilla-Type: moco-ldap-check', + 'From: ' . REPORT_SENDER, + 'To: ' . join(',', @recipients), + ); + if ($debug) { + print "\n", join("\n", @$report), "\n"; + } else { + MessageToMTA(join("\n", @$report)); + } +} + +sub clean_email { + my $email = shift; + $email = trim($email); + $email = $1 if $email =~ /^(\S+)/; + $email =~ s/@/@/; + $email = lc $email; + return $email; +} + +sub canon_email { + my $email = shift; + $email = clean_email($email); + $email =~ s/^([^\+]+)\+[^\@]+(\@.+)$/$1$2/; + return $email; +} + +sub trim { + my $value = shift; + $value =~ s/(^\s+|\s+$)//g; + return $value; +} + +sub serialise { + my ($filename, $ref) = @_; + local $Data::Dumper::Purity = 1; + local $Data::Dumper::Deepcopy = 1; + local $Data::Dumper::Sortkeys = 1; + write_file($filename, Dumper($ref)); +} + +sub deserialise { + my ($filename) = @_; + return unless -s $filename; + my $cpt = Safe->new(); + $cpt->reval('our ' . read_file($filename)) + || die "$!"; + return ${$cpt->varglob('VAR1')}; +} + |