From 44cef9cca23e9dd59ded0039efe1fb4454c2eb57 Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Mon, 22 May 2017 14:47:27 +0000 Subject: Bug 1361151 - Bugzilla Security groups are periodically synced to Phabricator project membership --- extensions/PhabBugz/Config.pm | 16 ++ extensions/PhabBugz/Extension.pm | 23 +++ extensions/PhabBugz/bin/update_project_members.pl | 187 +++++++++++++++++++++ extensions/PhabBugz/lib/Config.pm | 43 +++++ .../en/default/admin/params/phabbugz.html.tmpl | 20 +++ .../hook/global/code-error-errors.html.tmpl | 13 ++ .../hook/global/user-error-errors.html.tmpl | 22 +++ t/Support/Files.pm | 2 +- 8 files changed, 325 insertions(+), 1 deletion(-) create mode 100644 extensions/PhabBugz/Config.pm create mode 100644 extensions/PhabBugz/Extension.pm create mode 100755 extensions/PhabBugz/bin/update_project_members.pl create mode 100644 extensions/PhabBugz/lib/Config.pm create mode 100644 extensions/PhabBugz/template/en/default/admin/params/phabbugz.html.tmpl create mode 100644 extensions/PhabBugz/template/en/default/hook/global/code-error-errors.html.tmpl create mode 100644 extensions/PhabBugz/template/en/default/hook/global/user-error-errors.html.tmpl diff --git a/extensions/PhabBugz/Config.pm b/extensions/PhabBugz/Config.pm new file mode 100644 index 000000000..008838c1b --- /dev/null +++ b/extensions/PhabBugz/Config.pm @@ -0,0 +1,16 @@ +# 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. + +package Bugzilla::Extension::PhabBugz; + +use 5.10.1; +use strict; +use warnings; + +use constant NAME => 'PhabBugz'; + +__PACKAGE__->NAME; diff --git a/extensions/PhabBugz/Extension.pm b/extensions/PhabBugz/Extension.pm new file mode 100644 index 000000000..501fbc65d --- /dev/null +++ b/extensions/PhabBugz/Extension.pm @@ -0,0 +1,23 @@ +# 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. + +package Bugzilla::Extension::PhabBugz; + +use 5.10.1; +use strict; +use warnings; +use parent qw(Bugzilla::Extension); + +our $VERSION = '0.01'; + +sub config_add_panels { + my ($self, $args) = @_; + my $modules = $args->{panel_modules}; + $modules->{PhabBugz} = "Bugzilla::Extension::PhabBugz::Config"; +} + +__PACKAGE__->NAME; diff --git a/extensions/PhabBugz/bin/update_project_members.pl b/extensions/PhabBugz/bin/update_project_members.pl new file mode 100755 index 000000000..712368d2d --- /dev/null +++ b/extensions/PhabBugz/bin/update_project_members.pl @@ -0,0 +1,187 @@ +#!/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 5.10.1; +use strict; +use warnings; + +use lib qw(. lib local/lib/perl5); + +use Bugzilla; +BEGIN { Bugzilla->extensions() } + +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Group; + +use LWP::UserAgent; +use JSON qw(encode_json decode_json); + +Bugzilla->usage_mode(USAGE_MODE_CMDLINE); + +my ($phab_uri, $phab_api_key, $phab_sync_groups, $ua); + +# Sanity checks +unless ($phab_uri = Bugzilla->params->{phabricator_base_uri}) { + ThrowUserError('invalid_phabricator_uri'); +} + +unless ($phab_api_key = Bugzilla->params->{phabricator_api_key}) { + ThrowUserError('invalid_phabricator_api_key'); +} + +unless ($phab_sync_groups = Bugzilla->params->{phabricator_sync_groups}) { + ThrowUserError('invalid_phabricator_sync_groups'); +} + +# Loop through each group and perform the following: +# +# 1. Load flattened list of group members +# 2. Check to see if Phab project exists for 'bmo-' +# 3. Create if does not exist with locked down policy. +# 4. Set project members to exact list +# 5. Profit + +my $sync_groups = Bugzilla::Group->match({ name => [ split('[,\s]+', $phab_sync_groups) ] }); + +foreach my $group (@$sync_groups) { + my @users = get_group_members($group); + + # Create group project if one does not yet exist + my $phab_project_name = 'bmo-' . $group->name; + my $project_id = get_phab_project($phab_project_name); + if (!$project_id) { + $project_id = create_phab_project($phab_project_name, 'BMO Security Group for ' . $group->name); + } + + # Get the internal user ids for the bugzilla group members + my $phab_user_ids = get_phab_members_by_bmo_id(\@users); + + # Set the project members to the exact list + set_phab_project_members($project_id, $phab_user_ids); +} + +# Bugzilla + +sub get_group_members { + my ($group) = @_; + my $group_obj = ref $group ? $group : Bugzilla::Group->check({ name => $group }); + my $members_all = $group_obj->members_complete(); + my %users; + foreach my $name (keys %$members_all) { + foreach my $user (@{ $members_all->{$name} }) { + $users{$user->id} = $user; + } + } + return values %users; +} + +# Projects + +sub get_phab_project { + my ($project) = @_; + + my $data = { + queryKey => 'active', + constraints => { + name => $project + } + }; + + my $result = request('project.search', $data); + if (!$result->{result}{data}) { + return undef; + } + return $result->{result}{data}[0]{phid}; +} + +sub create_phab_project { + my ($project, $description, $members) = @_; + + my $data = { + transactions => [ + { type => 'name', value => $project }, + { type => 'description', value => $description }, + { type => 'edit', value => 'admin'}, + { type => 'join', value => 'admin' }, + { type => 'icon', value => 'group' }, + { type => 'color', value => 'red' } + ] + }; + + my $result = request('project.edit', $data); + return $result->{result}{object}{phid}; +} + +sub set_phab_project_members { + my ($project_id, $phab_user_ids) = @_; + + my $data = { + objectIdentifier => $project_id, + transactions => [ + { type => 'members.set', value => $phab_user_ids } + ] + }; + + my $result = request('project.edit', $data); + return $result->{result}{object}{phid}; +} + +# Members + +sub get_phab_members_by_bmo_id { + my ($users) = @_; + + my $data = { + accountids => [ map { $_->id } @$users ] + }; + + my $result = request('bmoexternalaccount.search', $data); + if (!$result->{result}) { + return []; + } + + my @phab_ids; + foreach my $user (@{ $result->{result} }) { + push(@phab_ids, $user->{phid}); + } + return \@phab_ids; +} + +# Utility + +sub request { + my ($method, $data) = @_; + + if (!$ua) { + $ua = LWP::UserAgent->new(timeout => 10); + if (Bugzilla->params->{proxy_url}) { + $ua->proxy('https', Bugzilla->params->{proxy_url}); + } + $ua->default_header('Content-Type' => 'application/x-www-form-urlencoded'); + } + + my $full_uri = $phab_uri . '/api/' . $method; + + $data->{__conduit__} = { token => $phab_api_key }; + + my $response = $ua->post($full_uri, { params => encode_json($data) }); + + $response->is_error + && ThrowCodeError('phabricator_api_error', + { reason => $response->message }); + + my $result = decode_json($response->content); + if ($result->{error_code}) { + ThrowCodeError('phabricator_api_error', + { code => $result->{error_code}, + reason => $result->{error_info} }); + } + return $result; +} diff --git a/extensions/PhabBugz/lib/Config.pm b/extensions/PhabBugz/lib/Config.pm new file mode 100644 index 000000000..686198a25 --- /dev/null +++ b/extensions/PhabBugz/lib/Config.pm @@ -0,0 +1,43 @@ +# 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. + +package Bugzilla::Extension::PhabBugz::Config; + +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::Config::Common; + +our $sortkey = 1300; + +sub get_param_list { + my ($class) = @_; + + my @params = ( + { + name => 'phabricator_base_uri', + type => 't', + default => '', + checker => \&check_urlbase + }, + { + name => 'phabricator_sync_groups', + type => 't', + default => '', + }, + { + name => 'phabricator_api_key', + type => 't', + default => '', + }, + ); + + return @params; +} + +1; diff --git a/extensions/PhabBugz/template/en/default/admin/params/phabbugz.html.tmpl b/extensions/PhabBugz/template/en/default/admin/params/phabbugz.html.tmpl new file mode 100644 index 000000000..922fcf709 --- /dev/null +++ b/extensions/PhabBugz/template/en/default/admin/params/phabbugz.html.tmpl @@ -0,0 +1,20 @@ +[%# 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. + #%] + +[% + title = "PhabBugz" + desc = "Configure Phabricator Integration" +%] + +[% + param_descs = { + phabricator_base_uri => 'Phabricator Base URI', + phabricator_api_key => 'Phabricator User API Key', + phabricator_sync_groups => 'Comma delimited list of Bugzilla groups to sync to Phabricator projects', + } +%] diff --git a/extensions/PhabBugz/template/en/default/hook/global/code-error-errors.html.tmpl b/extensions/PhabBugz/template/en/default/hook/global/code-error-errors.html.tmpl new file mode 100644 index 000000000..11dbfc2eb --- /dev/null +++ b/extensions/PhabBugz/template/en/default/hook/global/code-error-errors.html.tmpl @@ -0,0 +1,13 @@ +[%# 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. + #%] + +[% IF error == "phabricator_api_error" %] + [% title = "Phabricator API Error" %] + [% IF code %]Code: [% code FILTER html %]
[% END %] + Reason: [% reason FILTER html %] +[% END %] diff --git a/extensions/PhabBugz/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/PhabBugz/template/en/default/hook/global/user-error-errors.html.tmpl new file mode 100644 index 000000000..6959c759d --- /dev/null +++ b/extensions/PhabBugz/template/en/default/hook/global/user-error-errors.html.tmpl @@ -0,0 +1,22 @@ +[%# 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. + #%] + +[% IF error == "invalid_phabricator_uri" %] + [% title = "Invalid Phabricator URI" %] + You must provide a valid Phabricator URI. + +[% ELSIF error == "invalid_phabricator_api_key" %] + [% title = "Invalid Phabricator API Key" %] + You must provide a valid Phabricator API Key. + +[% ELSIF error == "invalid_phabricator_sync_groups" %] + [% title = "Invalid Phabricator Sync Groups" %] + You must provide a comma delimited list of security groups + to sync with Phabricator. + +[% END %] diff --git a/t/Support/Files.pm b/t/Support/Files.pm index 1bdf2eac7..603d95cf9 100644 --- a/t/Support/Files.pm +++ b/t/Support/Files.pm @@ -30,7 +30,7 @@ our @extensions = glob('extensions/*'); foreach my $extension (@extensions) { - find(sub { push(@files, $File::Find::name) if $_ =~ /\.pm$/;}, $extension); + find(sub { push(@files, $File::Find::name) if $_ =~ /\.pm$|\.pl$/;}, $extension); } our @test_files = glob('t/*.t xt/*/*.t'); -- cgit v1.2.3-24-g4f1b