Repository navigation
Bug 2065173 - Migrate Group REST resource to native Mojo API #2745
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
060d6a8
Bug 2065173 - Migrate Group REST resource to native Mojo API
Xzzz 740a663
Bug 2065173 - Fix i_am_webservice() to recognize USAGE_MODE_MOJO_REST
Xzzz 5aa903c
Bug 2065173 - Fix object => 'group' typo in Group.create/update auth_…
Xzzz bedeacc
Bug 2065173 - Use shared merge_request_params helper
Xzzz 20194f1
Merge remote-tracking branch 'origin/master' into bug-2065173
Xzzz 9bf9dd8
Bug 2065173 - Fix crash filtering groups by blessability
Xzzz 6712d87
Bug 2065173 - Whitelist update() fields instead of blacklisting names…
Xzzz 26c885e
Bug 2065173 - Use relaxed #id placeholder to allow dots in group names
Xzzz 44d1ec9
Bug 2065173 - Preserve null in changes when a field goes to/from undef
Xzzz a0fe532
Merge remote-tracking branch 'origin/master' into bug-2065173
Xzzz f3766b7
Bug 2065173 - Adapt to merge_request_params returning ($params, $error)
Xzzz b8bbbfa
Bug 2065173 - Let unknown update fields error instead of dropping them
Xzzz 581c9c5
Bug 2065173 - Advertise the methods each group route actually serves
Xzzz c0b0db2
Bug 2065173 - Correct the blessability comment and cover the fixed case
Xzzz 9eaf66e
Bug 2065173 - Treat an empty visible group list as no visible groups
Xzzz 381c8f3
Merge remote-tracking branch 'origin/master' into bug-2065173
Xzzz 7cacefc
Merge remote-tracking branch 'origin/master' into bug-2065173
Xzzz 6cba9cf
Bug 2065173 - Declare ids and names as list parameters
Xzzz d841e36
Merge remote-tracking branch 'origin/master' into bug-2065173
Xzzz ecd3e41
Merge remote-tracking branch 'origin/master' into bug-2065173
Xzzz e81a7fd
Bug 2065173 - Drop api_key, login, password and token before set_all(…
Xzzz 4505b0a
Bug 2065173 - Assert a blesser still gets the group they can bless
Xzzz 033ea62
Bug 2065173 - Use stash('id') for the path id in Group get/update, wi…
Xzzz a3ee62d
Bug 2065173 - Accept only true/false for is_active in Group create/up…
Xzzz 9fba88e
Bug 2065173 - Move the request-level keys dropped before set_all() to…
Xzzz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,326 @@ | ||
| # 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::API::V1::Group; | ||
|
|
||
| use 5.10.1; | ||
| use Mojo::Base qw( Mojolicious::Controller ); | ||
|
|
||
| use Mojo::JSON qw(true false); | ||
|
|
||
| use Bugzilla::Constants; | ||
| use Bugzilla::Error; | ||
| use Bugzilla::Group; | ||
| use Bugzilla::User; | ||
| use Bugzilla::WebService::Util | ||
| qw(merge_request_params params_to_objects strip_request_params translate validate); | ||
|
|
||
| use constant MAPPED_RETURNS => | ||
| {userregexp => 'user_regexp', isactive => 'is_active'}; | ||
|
|
||
| sub setup_routes { | ||
| my ($class, $r) = @_; | ||
| my $routes = $r->under( | ||
| '/group' => sub { Bugzilla->usage_mode(USAGE_MODE_MOJO_REST); }); | ||
| $routes->get('/')->to('V1::Group#get'); | ||
| $routes->get('/#id')->to('V1::Group#get'); | ||
| $routes->post('/')->to('V1::Group#create'); | ||
| $routes->put('/#id')->to('V1::Group#update'); | ||
|
|
||
| $routes->options('/')->to('V1::Group#options', allow => 'GET, POST'); | ||
| $routes->options('/#id')->to('V1::Group#options', allow => 'GET, PUT'); | ||
| } | ||
|
|
||
| sub options { | ||
| my ($self) = @_; | ||
|
|
||
| my $allow = $self->stash('allow'); | ||
| $self->res->headers->header('Allow' => $allow); | ||
| $self->res->headers->header('Access-Control-Allow-Methods' => $allow); | ||
|
|
||
| return $self->rendered(200); | ||
| } | ||
|
|
||
| sub create { | ||
| my ($self) = @_; | ||
|
|
||
| my $user = $self->bugzilla->login; | ||
| $user->id || return $self->user_error('login_required'); | ||
| $user->in_group('creategroups') | ||
| || return $self->user_error('auth_failure', | ||
| {group => 'creategroups', action => 'add', object => 'groups'}); | ||
|
|
||
| my ($params, $error) = merge_request_params($self); | ||
| return $self->user_error($error) if $error; | ||
|
|
||
| my $type_error = _coerce_is_active($params); | ||
| return $self->user_error('invalid_params', {type_error => $type_error}) | ||
| if $type_error; | ||
|
|
||
| my $group = Bugzilla::Group->create({ | ||
| name => $params->{name}, | ||
| description => $params->{description}, | ||
| userregexp => $params->{user_regexp}, | ||
| isactive => $params->{is_active}, | ||
| isbuggroup => 1, | ||
| icon_url => $params->{icon_url}, | ||
| }); | ||
|
|
||
| return $self->render(json => {id => 0 + $group->id}, status => 201); | ||
| } | ||
|
|
||
| sub update { | ||
| my ($self) = @_; | ||
|
|
||
| my $user = $self->bugzilla->login; | ||
| $user->id || return $self->user_error('login_required'); | ||
| $user->in_group('creategroups') | ||
| || return $self->user_error('auth_failure', | ||
| {group => 'creategroups', action => 'edit', object => 'groups'}); | ||
|
|
||
| my ($params, $error) = merge_request_params($self, ['ids', 'names']); | ||
| return $self->user_error($error) if $error; | ||
|
|
||
| my $type_error = _coerce_is_active($params); | ||
| return $self->user_error('invalid_params', {type_error => $type_error}) | ||
| if $type_error; | ||
|
|
||
| if (defined(my $id_or_name = $self->stash('id'))) { | ||
| $params | ||
| = $id_or_name =~ /^\d+$/ | ||
| ? {%$params, ids => [$id_or_name], names => undef} | ||
| : {%$params, names => [$id_or_name], ids => undef}; | ||
| } | ||
|
|
||
| defined($params->{names}) || defined($params->{ids}) | ||
| || return $self->code_error('params_required', | ||
| {function => 'Group.update', params => ['ids', 'names']}); | ||
|
|
||
| my $group_objects = params_to_objects($params, 'Bugzilla::Group'); | ||
|
|
||
| # Some groups are protected from being edited by non-admins. | ||
| foreach my $group (@$group_objects) { | ||
| $group->check_can_be_edited(); | ||
| } | ||
|
|
||
| # Drop the request-level keys that are not group fields and pass everything | ||
| # else through, so set_all() still raises unknown_method on an unrecognized | ||
| # field rather than silently ignoring it. | ||
| my $values = strip_request_params($params, qw(ids names)); | ||
|
|
||
| my $dbh = Bugzilla->dbh; | ||
| $dbh->bz_start_transaction(); | ||
| foreach my $group (@$group_objects) { | ||
| $group->set_all($values); | ||
| } | ||
|
|
||
| my %changes; | ||
| foreach my $group (@$group_objects) { | ||
| my $returned_changes = $group->update(); | ||
| $changes{$group->id} = translate($returned_changes, MAPPED_RETURNS); | ||
| } | ||
| $dbh->bz_commit_transaction(); | ||
|
|
||
| my @result; | ||
| foreach my $group (@$group_objects) { | ||
| my %hash = (id => 0 + $group->id, changes => {}); | ||
| foreach my $field (keys %{$changes{$group->id}}) { | ||
| my $change = $changes{$group->id}->{$field}; | ||
| $hash{changes}{$field} = { | ||
| removed => defined $change->[0] ? "$change->[0]" : undef, | ||
| added => defined $change->[1] ? "$change->[1]" : undef, | ||
| }; | ||
| } | ||
| push(@result, \%hash); | ||
| } | ||
|
|
||
| return $self->render(json => {groups => \@result}); | ||
| } | ||
|
|
||
| sub get { | ||
| my ($self) = @_; | ||
|
|
||
| my $user = $self->bugzilla->login; | ||
| $user->id || return $self->user_error('login_required'); | ||
|
|
||
| my ($params, $error) = merge_request_params($self, ['ids', 'names']); | ||
| return $self->user_error($error) if $error; | ||
|
|
||
| if (defined(my $id_or_name = $self->stash('id'))) { | ||
| $params | ||
| = $id_or_name =~ /^\d+$/ | ||
| ? {%$params, ids => [$id_or_name]} | ||
| : {%$params, names => [$id_or_name]}; | ||
| } | ||
| (undef, $params) = validate($self, $params, qw(ids names type)); | ||
|
|
||
| my $can_see_groups = $user->in_group('can_see_groups'); | ||
| return $self->user_error('group_cannot_view') | ||
| if !$can_see_groups && !$user->can_bless; | ||
|
|
||
| Bugzilla->switch_to_shadow_db(); | ||
|
|
||
| my $groups = []; | ||
|
|
||
| if (defined $params->{ids}) { | ||
|
|
||
| # Get the groups by id | ||
| $groups = Bugzilla::Group->new_from_list($params->{ids}); | ||
| } | ||
|
|
||
| if (defined $params->{names}) { | ||
|
|
||
| # Get the groups by name. check() will throw an error if a bad name is | ||
| # given. | ||
| foreach my $name (@{$params->{names}}) { | ||
|
|
||
| # Skip if we got this from params->{ids} | ||
| next if grep { $_->name eq $name } @$groups; | ||
|
|
||
| push @$groups, Bugzilla::Group->check({name => $name}); | ||
| } | ||
| } | ||
|
|
||
| if (!defined $params->{ids} && !defined $params->{names}) { | ||
| if ($can_see_groups) { | ||
| @$groups = Bugzilla::Group->get_all; | ||
| } | ||
| else { | ||
| # Get only groups the user has bless privileges for. | ||
| $groups = $user->bless_groups; | ||
| } | ||
| } | ||
|
|
||
| # Filter groups by blessability if the user is not allowed to see all | ||
| # groups. can_bless() takes a group id, not a Group object. The legacy | ||
| # WebService code mapped (not grepped) can_bless($group_object) over the | ||
| # list; that numifies the ref and always returns 0, so every element became | ||
| # 0 and _group_to_hash then called ->id on it, i.e. any blesser without | ||
| # can_see_groups got a 500. Passing the id filters the list instead. | ||
| if (!$can_see_groups) { | ||
| $groups = [grep { $user->can_bless($_->id) } @{$groups}]; | ||
| } | ||
|
|
||
| my @result = map { $self->_group_to_hash($params, $_) } @$groups; | ||
|
|
||
| return $self->render(json => {groups => \@result}); | ||
| } | ||
|
|
||
| # A JSON body sends is_active as a real boolean, but the query string and a | ||
| # form body send the literal string "true" or "false", which Perl treats as | ||
| # true either way. Same check as Product and Component. Returns the error, if | ||
| # any. | ||
| sub _coerce_is_active { | ||
| my ($params) = @_; | ||
|
|
||
| return undef if !defined $params->{is_active} || ref $params->{is_active}; | ||
| my $value = lc $params->{is_active}; | ||
| return 'is_active must be true or false' | ||
| if $value !~ /^(?:true|false|1|0)$/; | ||
| $params->{is_active} = ($value eq 'true' || $value eq '1') ? 1 : 0; | ||
|
|
||
| return undef; | ||
| } | ||
|
|
||
| sub _group_to_hash { | ||
| my ($self, $params, $group) = @_; | ||
| my $user = Bugzilla->user; | ||
|
|
||
| my $field_data | ||
| = {id => 0 + $group->id, name => $group->name, description => $group->description}; | ||
|
|
||
| if ($user->in_group('creategroups')) { | ||
| $field_data->{is_active} = $group->is_active ? true : false; | ||
| $field_data->{is_bug_group} = $group->is_bug_group ? true : false; | ||
| $field_data->{user_regexp} = $group->user_regexp; | ||
| } | ||
|
|
||
| if ($params->{membership}) { | ||
| $field_data->{membership} = $self->_get_group_membership($group); | ||
| } | ||
|
|
||
| return $field_data; | ||
| } | ||
|
|
||
| sub _get_group_membership { | ||
| my ($self, $group) = @_; | ||
| my $user = Bugzilla->user; | ||
|
|
||
| my $dbh = Bugzilla->dbh; | ||
| my $editusers = $user->in_group('editusers'); | ||
|
|
||
| my $query = 'SELECT userid FROM profiles'; | ||
| my $visible_groups; | ||
|
|
||
| if (!$editusers && Bugzilla->params->{usevisibilitygroups}) { | ||
|
|
||
| # Show only users in visible groups. An empty arrayref is still truthy, so | ||
| # normalise it to undef: otherwise the group_not_visible check below passes | ||
| # and the query is left as a bare SELECT with an ' AND ...' appended to it. | ||
| $visible_groups = $user->visible_groups_inherited; | ||
| $visible_groups = undef unless @$visible_groups; | ||
|
|
||
| if ($visible_groups) { | ||
| $query .= qq{, user_group_map AS ugm | ||
| WHERE ugm.user_id = profiles.userid | ||
| AND ugm.isbless = 0 | ||
| AND } . $dbh->sql_in('ugm.group_id', $visible_groups); | ||
| } | ||
| } | ||
| elsif ($editusers | ||
| || $user->can_bless($group->id) | ||
| || $user->in_group('creategroups')) | ||
| { | ||
| $visible_groups = 1; | ||
| $query .= qq{, user_group_map AS ugm | ||
| WHERE ugm.user_id = profiles.userid | ||
| AND ugm.isbless = 0 | ||
| }; | ||
| } | ||
|
|
||
| # Use ThrowUserError (not $self->user_error) so an invisible group aborts | ||
| # the request cleanly even though this runs nested inside the map() in | ||
| # get() -- returning a rendered response from here would leave get()'s | ||
| # own render() call to fire a second time. | ||
| ThrowUserError('group_not_visible', {group => $group}) unless $visible_groups; | ||
|
|
||
| my $grouplist = Bugzilla::Group->flatten_group_membership($group->id); | ||
| $query .= ' AND ' . $dbh->sql_in('ugm.group_id', $grouplist); | ||
|
|
||
| my $userids = $dbh->selectcol_arrayref($query); | ||
| my $user_objects = Bugzilla::User->new_from_list($userids); | ||
|
|
||
| return [ | ||
| map { | ||
| { | ||
| id => 0 + $_->id, | ||
| real_name => $_->name, | ||
| nick => $_->nick, | ||
| name => $_->login, | ||
| email => $_->email, | ||
| can_login => $_->is_enabled ? true : false, | ||
| email_enabled => $_->email_enabled ? true : false, | ||
| login_denied_text => $_->disabledtext, | ||
| } | ||
| } @$user_objects | ||
| ]; | ||
| } | ||
|
|
||
| 1; | ||
|
|
||
| __END__ | ||
|
|
||
| =head1 NAME | ||
|
|
||
| Bugzilla::API::V1::Group - The API for creating, changing, and getting | ||
| information about Groups. | ||
|
|
||
| =head1 DESCRIPTION | ||
|
|
||
| This part of the Bugzilla API allows you to create Groups and get | ||
| information about them. See L<docs/en/rst/api/core/v1/group.rst> for the | ||
| public REST documentation. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.