From 75e5744c27844bc48d5a01aeb04f7d1c31237b7d Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Thu, 9 Oct 2014 18:01:03 +0000 Subject: Bug 1079463: Bugzilla::WebService::User missing update method --- Bugzilla/User.pm | 176 +++++++++++++++++++++++++++++++++++ Bugzilla/WebService/User.pm | 218 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 392 insertions(+), 2 deletions(-) diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm index 259a7ea90..20cc061dd 100644 --- a/Bugzilla/User.pm +++ b/Bugzilla/User.pm @@ -177,12 +177,80 @@ sub super_user { return $user; } +sub _update_groups { + my $self = shift; + my $group_changes = shift; + my $changes = shift; + my $dbh = Bugzilla->dbh; + + # Update group settings. + my $sth_add_mapping = $dbh->prepare( + qq{INSERT INTO user_group_map ( + user_id, group_id, isbless, grant_type + ) VALUES ( + ?, ?, ?, ? + ) + }); + my $sth_remove_mapping = $dbh->prepare( + qq{DELETE FROM user_group_map + WHERE user_id = ? + AND group_id = ? + AND isbless = ? + AND grant_type = ? + }); + + foreach my $is_bless (keys %$group_changes) { + my ($removed, $added) = @{$group_changes->{$is_bless}}; + + foreach my $group (@$removed) { + $sth_remove_mapping->execute( + $self->id, $group->id, $is_bless, GRANT_DIRECT + ); + } + foreach my $group (@$added) { + $sth_add_mapping->execute( + $self->id, $group->id, $is_bless, GRANT_DIRECT + ); + } + + if (! $is_bless) { + my $query = qq{ + INSERT INTO profiles_activity + (userid, who, profiles_when, fieldid, oldvalue, newvalue) + VALUES ( ?, ?, now(), ?, ?, ?) + }; + + $dbh->do( + $query, undef, + $self->id, Bugzilla->user->id, + get_field_id('bug_group'), + join(', ', map { $_->name } @$removed), + join(', ', map { $_->name } @$added) + ); + } + else { + # XXX: should create profiles_activity entries for blesser changes. + } + + Bugzilla->memcached->clear_config({ key => 'user_groups.' . $self->id }); + + my $type = $is_bless ? 'bless_groups' : 'groups'; + $changes->{$type} = [ + [ map { $_->name } @$removed ], + [ map { $_->name } @$added ], + ]; + } +} + sub update { my $self = shift; my $options = shift; + my $group_changes = delete $self->{_group_changes}; + my $changes = $self->SUPER::update(@_); my $dbh = Bugzilla->dbh; + $self->_update_groups($group_changes, $changes); if (exists $changes->{login_name}) { # Delete all the tokens related to the userid @@ -298,6 +366,114 @@ sub set_disabledtext { $_[0]->set('is_enabled', $_[1] ? 0 : 1); } +sub set_groups { + my $self = shift; + $self->_set_groups(GROUP_MEMBERSHIP, @_); +} + +sub set_bless_groups { + my $self = shift; + + # The person making the change needs to be in the editusers group + Bugzilla->user->in_group('editusers') + || ThrowUserError("auth_failure", {group => "editusers", + reason => "cant_bless", + action => "edit", + object => "users"}); + + $self->_set_groups(GROUP_BLESS, @_); +} + +sub _set_groups { + my $self = shift; + my $is_bless = shift; + my $changes = shift; + my $dbh = Bugzilla->dbh; + + use Data::Dumper; + + # The person making the change is $user, $self is the person being changed + my $user = Bugzilla->user; + + # Input is a hash of arrays. Key is 'set', 'add' or 'remove'. The array + # is a list of group ids and/or names. + + # First turn the arrays into group objects. + $changes = $self->_set_groups_to_object($changes); + + # Get a list of the groups the user currently is a member of + my $ids = $dbh->selectcol_arrayref( + q{SELECT DISTINCT group_id + FROM user_group_map + WHERE user_id = ? AND isbless = ? AND grant_type = ?}, + undef, $self->id, $is_bless, GRANT_DIRECT); + + my $current_groups = Bugzilla::Group->new_from_list($ids); + my $new_groups = dclone($current_groups); + + # Record the changes + if (exists $changes->{set}) { + $new_groups = $changes->{set}; + + # We need to check the user has bless rights on the existing groups + # If they don't, then we need to add them back to new_groups + foreach my $group (@$current_groups) { + if (! $user->can_bless($group->id)) { + push @$new_groups, $group + unless grep { $_->id eq $group->id } @$new_groups; + } + } + } + else { + foreach my $group (@{$changes->{remove} // []}) { + @$new_groups = grep { $_->id ne $group->id } @$new_groups; + } + foreach my $group (@{$changes->{add} // []}) { + push @$new_groups, $group + unless grep { $_->id eq $group->id } @$new_groups; + } + } + + # Stash the changes, so self->update can actually make them + my @diffs = diff_arrays($current_groups, $new_groups, 'id'); + if (scalar(@{$diffs[0]}) || scalar(@{$diffs[1]})) { + $self->{_group_changes}{$is_bless} = \@diffs; + } +} + +sub _set_groups_to_object { + my $self = shift; + my $changes = shift; + my $user = Bugzilla->user; + + foreach my $key (keys %$changes) { + # Check we were given an array + unless (ref($changes->{$key}) eq 'ARRAY') { + ThrowCodeError( + 'param_invalid', + { param => $changes->{$key}, function => $key } + ); + } + + # Go through the array, and turn items into group objects + my @groups = (); + foreach my $value (@{$changes->{$key}}) { + my $type = $value =~ /^\d+$/ ? 'id' : 'name'; + my $group = Bugzilla::Group->new({$type => $value}); + + if (! $group || ! $user->can_bless($group->id)) { + ThrowUserError('auth_failure', + { group => $value, reason => 'cant_bless', + action => 'edit', object => 'users' }); + } + push @groups, $group; + } + $changes->{$key} = \@groups; + } + + return $changes; +} + sub update_last_seen_date { my $self = shift; return unless $self->id; diff --git a/Bugzilla/WebService/User.pm b/Bugzilla/WebService/User.pm index 988ae3cd5..1e6de143c 100644 --- a/Bugzilla/WebService/User.pm +++ b/Bugzilla/WebService/User.pm @@ -28,7 +28,8 @@ use Bugzilla::Error; use Bugzilla::Group; use Bugzilla::User; use Bugzilla::Util qw(trim); -use Bugzilla::WebService::Util qw(filter filter_wants validate); +use Bugzilla::WebService::Util qw(filter filter_wants validate + translate params_to_objects); use Bugzilla::Hook; use List::Util qw(first); @@ -43,6 +44,20 @@ use constant READ_ONLY => qw( get ); +use constant MAPPED_FIELDS => { + email => 'login', + full_name => 'name', + login_denied_text => 'disabledtext', + email_enabled => 'disable_mail' +}; + +use constant MAPPED_RETURNS => { + login_name => 'email', + realname => 'full_name', + disabledtext => 'login_denied_text', + disable_mail => 'email_enabled' +}; + ############## # User Login # ############## @@ -267,6 +282,76 @@ sub get { return { users => \@users }; } +############### +# User Update # +############### + +sub update { + my ($self, $params) = @_; + + my $dbh = Bugzilla->dbh; + + my $user = Bugzilla->login(LOGIN_REQUIRED); + + # Reject access if there is no sense in continuing. + $user->in_group('editusers') + || ThrowUserError("auth_failure", {group => "editusers", + action => "edit", + object => "users"}); + + defined($params->{names}) || defined($params->{ids}) + || ThrowCodeError('params_required', + { function => 'User.update', params => ['ids', 'names'] }); + + my $user_objects = params_to_objects($params, 'Bugzilla::User'); + + my $values = translate($params, MAPPED_FIELDS); + + # We delete names and ids to keep only new values to set. + delete $values->{names}; + delete $values->{ids}; + + $dbh->bz_start_transaction(); + foreach my $user (@$user_objects){ + $user->set_all($values); + } + + my %changes; + foreach my $user (@$user_objects){ + my $returned_changes = $user->update(); + $changes{$user->id} = translate($returned_changes, MAPPED_RETURNS); + } + $dbh->bz_commit_transaction(); + + my @result; + foreach my $user (@$user_objects) { + my %hash = ( + id => $user->id, + changes => {}, + ); + + foreach my $field (keys %{ $changes{$user->id} }) { + my $change = $changes{$user->id}->{$field}; + # We normalize undef to an empty string, so that the API + # stays consistent for things that can become empty. + $change->[0] = '' if !defined $change->[0]; + $change->[1] = '' if !defined $change->[1]; + # We also flatten arrays (used by groups and blessed_groups) + $change->[0] = join(',', @{$change->[0]}) if ref $change->[0]; + $change->[1] = join(',', @{$change->[1]}) if ref $change->[1]; + + $hash{changes}{$field} = { + removed => $self->type('string', $change->[0]), + added => $self->type('string', $change->[1]) + }; + } + + push(@result, \%hash); + } + + return { users => \@result }; +} + sub _filter_users_by_group { my ($self, $users, $params) = @_; my ($group_ids, $group_names) = @$params{qw(group_ids groups)}; @@ -487,7 +572,7 @@ for the provided username. =back -=head1 Account Creation +=head1 Account Creation and Modification =head2 offer_account_by_email @@ -602,6 +687,135 @@ password is under three characters.) =back +=head2 update + +B + +=over + +=item B + +Updates user accounts in Bugzilla. + +=item B + +=over + +=item C + +C Contains ids of user to update. + +=item C + +C Contains email/login of user to update. + +=item C + +C The new name of the user. + +=item C + +C The email of the user. Note that email used to login to bugzilla. +Also note that you can only update one user at a time when changing the +login name / email. (An error will be thrown if you try to update this field +for multiple users at once.) + +=item C + +C The password of the user. + +=item C + +C A boolean value to enable/disable sending bug-related mail to the user. + +=item C + +C A text field that holds the reason for disabling a user from logging +into bugzilla, if empty then the user account is enabled otherwise it is +disabled/closed. + +=item C + +C These specify the groups that this user is directly a member of. +To set these, you should pass a hash as the value. The hash may contain +the following fields: + +=over + +=item C An array of Cs or Cs. The group ids or group names +that the user should be added to. + +=item C An array of Cs or Cs. The group ids or group names +that the user should be removed from. + +=item C An array of Cs or Cs. An exact set of group ids +and group names that the user should be a member of. NOTE: This does not +remove groups from the user where the person making the change does not +have the bless privilege for. + +If you specify C, then C and C will be ignored. A group in +both the C and C list will be added. Specifying a group that the +user making the change does not have bless rights will generate an error. + +=back + +=item C + +C - This is the same as set_groups, but affects what groups a user +has direct membership to bless that group. It takes the same inputs as +set_groups. + +=back + +=item B + +A C with a single field "users". This points to an array of hashes +with the following fields: + +=over + +=item C + +C The id of the user that was updated. + +=item C + +C The changes that were actually done on this user. The keys are +the names of the fields that were changed, and the values are a hash +with two keys: + +=over + +=item C + +C The values that were added to this field, +possibly a comma-and-space-separated list if multiple values were added. + +=item C + +C The values that were removed from this field, possibly a +comma-and-space-separated list if multiple values were removed. + +=back + +=back + +=item B + +=over + +=item 51 (Bad Login Name) + +You passed an invalid login name in the "names" array. + +=item 304 (Authorization Required) + +Logged-in users are not authorized to edit other users. + +=back + +=back + =head1 User Info =head2 get -- cgit v1.2.3-24-g4f1b