]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Fix for bug 179510: takes group restrictions into account when sending request notifi...
authormyk%mozilla.org <>
Fri, 25 Apr 2003 12:41:20 +0000 (12:41 +0000)
committermyk%mozilla.org <>
Fri, 25 Apr 2003 12:41:20 +0000 (12:41 +0000)
r=bbaetz,jpreed
a=justdave

Attachment.pm
Bugzilla/Attachment.pm
Bugzilla/Flag.pm
Bugzilla/FlagType.pm
Bugzilla/Util.pm
attachment.cgi
globals.pl
process_bug.cgi
template/en/default/global/user-error.html.tmpl

index 322a3b2bab17e441dc517ea283ec1d1814b9684a..ea5cd531c0d9cc8eed24c4ff4e45732e932b1005 100644 (file)
@@ -48,10 +48,12 @@ sub new {
     bless($self, $class);
     
     &::PushGlobalSQLState();
-    &::SendSQL("SELECT 1, description, bug_id FROM attachments " . 
+    &::SendSQL("SELECT 1, description, bug_id, isprivate FROM attachments " . 
                "WHERE attach_id = $id");
-    ($self->{'exists'}, $self->{'summary'}, $self->{'bug_id'}) = 
-      &::FetchSQLData();
+    ($self->{'exists'},
+     $self->{'summary'},
+     $self->{'bug_id'},
+     $self->{'isprivate'}) = &::FetchSQLData();
     &::PopGlobalSQLState();
 
     return $self;
index 322a3b2bab17e441dc517ea283ec1d1814b9684a..ea5cd531c0d9cc8eed24c4ff4e45732e932b1005 100644 (file)
@@ -48,10 +48,12 @@ sub new {
     bless($self, $class);
     
     &::PushGlobalSQLState();
-    &::SendSQL("SELECT 1, description, bug_id FROM attachments " . 
+    &::SendSQL("SELECT 1, description, bug_id, isprivate FROM attachments " . 
                "WHERE attach_id = $id");
-    ($self->{'exists'}, $self->{'summary'}, $self->{'bug_id'}) = 
-      &::FetchSQLData();
+    ($self->{'exists'},
+     $self->{'summary'},
+     $self->{'bug_id'},
+     $self->{'isprivate'}) = &::FetchSQLData();
     &::PopGlobalSQLState();
 
     return $self;
index 41cc18071f0fad3cfb0bd93d4e3749a0b1d602c9..a327f292214671a02ecb0dca02fb8abd6dfa79c0 100644 (file)
@@ -33,9 +33,12 @@ use Bugzilla::FlagType;
 use Bugzilla::User;
 use Bugzilla::Config;
 use Bugzilla::Util;
+use Bugzilla::Error;
 
 use Attachment;
 
+use constant TABLES_ALREADY_LOCKED => 1;
+
 # Note that this line doesn't actually import these variables for some reason,
 # so I have to use them as $::template and $::vars in the package code.
 use vars qw($template $vars); 
@@ -135,8 +138,8 @@ sub count {
 
 sub validate {
     # Validates fields containing flag modifications.
-    
-    my ($data) = @_;
+
+    my ($data, $bug_id) = @_;
   
     # Get a list of flags to validate.  Uses the "map" function
     # to extract flag IDs from form field names by matching fields
@@ -152,13 +155,62 @@ sub validate {
         my $flag = get($id);
         $flag || &::ThrowCodeError("flag_nonexistent", { id => $id });
 
-        # Don't bother validating flags the user didn't change.
-        next if $status eq $flag->{'status'};
-
         # Make sure the user chose a valid status.
         grep($status eq $_, qw(X + - ?))
           || &::ThrowCodeError("flag_status_invalid", 
                                { id => $id , status => $status });
+                
+        # Make sure the user didn't request the flag unless it's requestable.
+        if ($status eq '?' && !$flag->{type}->{is_requestable}) {
+            ThrowCodeError("flag_status_invalid", 
+                              { id => $id , status => $status });
+        }
+        
+        # Make sure the requestee is authorized to access the bug.
+        # (and attachment, if this installation is using the "insider group"
+        # feature and the attachment is marked private).
+        if ($status eq '?'
+            && $flag->{type}->{is_requesteeble}
+            && trim($data->{"requestee-$id"}))
+        {
+            my $requestee_email = trim($data->{"requestee-$id"});
+            if ($requestee_email ne $flag->{'requestee'}->{'email'}) {
+                # We know the requestee exists because we ran
+                # Bugzilla::User::match_field before getting here.
+                # ConfirmGroup makes sure their group settings
+                # are up-to-date or calls DeriveGroups to update them.
+                my $requestee_id = &::DBname_to_id($requestee_email);
+                &::ConfirmGroup($requestee_id);
+
+                # Throw an error if the user can't see the bug.
+                if (!&::CanSeeBug($bug_id, $requestee_id))
+                {
+                    ThrowUserError("flag_requestee_unauthorized",
+                                   { flag_type => $flag->{'type'},
+                                     requestee =>
+                                       new Bugzilla::User($requestee_id),
+                                     bug_id => $bug_id,
+                                     attach_id =>
+                                       $flag->{target}->{attachment}->{id} });
+                }
+
+                # Throw an error if the target is a private attachment and
+                # the requestee isn't in the group of insiders who can see it.
+                if ($flag->{target}->{attachment}->{exists}
+                    && $data->{'isprivate'}
+                    && &::Param("insidergroup")
+                    && !&::UserInGroup(&::Param("insidergroup"), $requestee_id))
+                {
+                    ThrowUserError("flag_requestee_unauthorized_attachment",
+                                   { flag_type => $flag->{'type'},
+                                     requestee =>
+                                       new Bugzilla::User($requestee_id),
+                                     bug_id    => $bug_id,
+                                     attach_id =>
+                                      $flag->{target}->{attachment}->{id} });
+                }
+            }
+        }
     }
 }
 
@@ -457,14 +509,17 @@ sub GetBug {
     # Save the currently running query (if any) so we do not overwrite it.
     &::PushGlobalSQLState();
 
-    &::SendSQL("SELECT  1, short_desc, product_id, component_id
-                  FROM  bugs
-                 WHERE  bug_id = $id");
+    &::SendSQL("SELECT    1, short_desc, product_id, component_id,
+                          COUNT(bug_group_map.group_id)
+                FROM      bugs LEFT JOIN bug_group_map
+                            ON (bugs.bug_id = bug_group_map.bug_id)
+                WHERE     bugs.bug_id = $id
+                GROUP BY  bugs.bug_id");
 
     my $bug = { 'id' => $id };
     
     ($bug->{'exists'}, $bug->{'summary'}, $bug->{'product_id'}, 
-     $bug->{'component_id'}) = &::FetchSQLData();
+     $bug->{'component_id'}, $bug->{'restricted'}) = &::FetchSQLData();
 
     # Restore the previously running query (if any).
     &::PopGlobalSQLState();
@@ -504,6 +559,28 @@ sub notify {
     
     my ($flag, $template_file) = @_;
     
+    # If the target bug is restricted to one or more groups, then we need
+    # to make sure we don't send email about it to unauthorized users
+    # on the request type's CC: list, so we have to trawl the list for users
+    # not in those groups or email addresses that don't have an account.
+    if ($flag->{'target'}->{'bug'}->{'restricted'}
+        || $flag->{'target'}->{'attachment'}->{'isprivate'})
+    {
+        my @new_cc_list;
+        foreach my $cc (split(/[, ]+/, $flag->{'type'}->{'cc_list'})) {
+            my $user_id = &::DBname_to_id($cc) || next;
+            # re-derive permissions if necessary
+            &::ConfirmGroup($user_id, TABLES_ALREADY_LOCKED);
+            next if $flag->{'target'}->{'bug'}->{'restricted'}
+              && !&::CanSeeBug($flag->{'target'}->{'bug'}->{'id'}, $user_id);
+            next if $flag->{'target'}->{'attachment'}->{'isprivate'}
+              && Param("insidergroup")
+              && !&::UserInGroup(Param("insidergroup"), $user_id);
+            push(@new_cc_list, $cc);
+        }
+        $flag->{'type'}->{'cc_list'} = join(", ", @new_cc_list);
+    }
+
     $::vars->{'flag'} = $flag;
     
     my $message;
index 2e272f67c75eee8db5f1766ff20e30a38b7c72f9..523f601901bee148b44beca79a056de798aed325 100644 (file)
@@ -32,6 +32,9 @@ package Bugzilla::FlagType;
 # Use Bugzilla's User module which contains utilities for handling users.
 use Bugzilla::User;
 
+use Bugzilla::Error;
+use Bugzilla::Util;
+
 # Note!  This module requires that its caller have said "require CGI.pl" 
 # to import relevant functions from that script and its companion globals.pl.
 
@@ -177,9 +180,9 @@ sub count {
 }
 
 sub validate {
-    my ($data) = @_;
+    my ($data, $bug_id, $attach_id) = @_;
   
-    # Get a list of flags types to validate.  Uses the "map" function
+    # Get a list of flag types to validate.  Uses the "map" function
     # to extract flag type IDs from form field names by matching columns
     # whose name looks like "flag_type-nnn", where "nnn" is the ID,
     # and returning just the ID portion of matching field names.
@@ -192,14 +195,62 @@ sub validate {
         # Don't bother validating types the user didn't touch.
         next if $status eq "X";
 
-        # Make sure the flag exists.
-        get($id) 
+        # Make sure the flag type exists.
+        my $flag_type = get($id);
+        $flag_type 
           || &::ThrowCodeError("flag_type_nonexistent", { id => $id });
 
         # Make sure the value of the field is a valid status.
         grep($status eq $_, qw(X + - ?))
           || &::ThrowCodeError("flag_status_invalid", 
                                { id => $id , status => $status });
+                
+        # Make sure the user didn't request the flag unless it's requestable.
+        if ($status eq '?' && !$flag_type->{is_requestable}) {
+            ThrowCodeError("flag_status_invalid", 
+                           { id => $id , status => $status });
+        }
+        
+        # Make sure the requestee is authorized to access the bug
+        # (and attachment, if this installation is using the "insider group"
+        # feature and the attachment is marked private).
+        if ($status eq '?'
+            && $flag_type->{is_requesteeble}
+            && trim($data->{"requestee_type-$id"}))
+        {
+            my $requestee_email = trim($data->{"requestee_type-$id"});
+            my $requestee_id = &::DBname_to_id($requestee_email);
+
+            # We know the requestee exists because we ran
+            # Bugzilla::User::match_field before getting here.
+            # ConfirmGroup makes sure their group settings
+            # are up-to-date or calls DeriveGroups to update them.
+            &::ConfirmGroup($requestee_id);
+
+            # Throw an error if the user can't see the bug.
+            if (!&::CanSeeBug($bug_id, $requestee_id))
+            {
+                ThrowUserError("flag_requestee_unauthorized",
+                               { flag_type => $flag_type,
+                                 requestee => new Bugzilla::User($requestee_id),
+                                 bug_id    => $bug_id,
+                                 attach_id => $attach_id });
+            }
+            
+            # Throw an error if the target is a private attachment and
+            # the requestee isn't in the group of insiders who can see it.
+            if ($attach_id
+                && &::Param("insidergroup")
+                && $data->{'isprivate'}
+                && !&::UserInGroup(&::Param("insidergroup"), $requestee_id))
+            {
+                ThrowUserError("flag_requestee_unauthorized_attachment",
+                               { flag_type => $flag_type,
+                                 requestee => new Bugzilla::User($requestee_id),
+                                 bug_id    => $bug_id,
+                                 attach_id => $attach_id });
+            }
+        }
     }
 }
 
index 5aecb5ad97a571de12ae7973ba4bf55ceb66398f..511ba2592c5c69d0e1dd1de2b78d1dd1f62d3b44 100644 (file)
@@ -129,8 +129,10 @@ sub min {
 
 sub trim {
     my ($str) = @_;
-    $str =~ s/^\s+//g;
-    $str =~ s/\s+$//g;
+    if ($str) {
+      $str =~ s/^\s+//g;
+      $str =~ s/\s+$//g;
+    }
     return $str;
 }
 
index 51a15464506086e959c988483c7778609be06c3f..621477ed5acc201b61dad7973ccb4a1702d2413d 100755 (executable)
@@ -52,6 +52,17 @@ ConnectToDatabase();
 # Check whether or not the user is logged in and, if so, set the $::userid 
 quietly_check_login();
 
+# The ID of the bug to which the attachment is attached.  Gets set
+# by validateID() (which validates the attachment ID, not the bug ID, but has
+# to check if the user is authorized to access this attachment) and is used 
+# by Flag:: and FlagType::validate() to ensure the requestee (if any) for a 
+# requested flag is authorized to see the bug in question.  Note: This should 
+# really be defined just above validateID() itself, but it's used in the main 
+# body of the script before that function is defined, so we define it up here 
+# instead.  We should move the validation into each function and then move this
+# to just above validateID().
+my $bugid;
+
 ################################################################################
 # Main Body Execution
 ################################################################################
@@ -113,10 +124,15 @@ elsif ($action eq "update")
   validateContentType() unless $::FORM{'ispatch'};
   validateIsObsolete();
   validatePrivate();
+  
+  # The order of these function calls is important, as both Flag::validate
+  # and FlagType::validate assume User::match_field has ensured that the values
+  # in the requestee fields are legitimate user email addresses.
   Bugzilla::User::match_field({ '^requestee(_type)?-(\d+)$' => 
                                     { 'type' => 'single' } });
-  Bugzilla::Flag::validate(\%::FORM);
-  Bugzilla::FlagType::validate(\%::FORM);
+  Bugzilla::Flag::validate(\%::FORM, $bugid);
+  Bugzilla::FlagType::validate(\%::FORM, $bugid, $::FORM{'id'});
+  
   update();
 }
 else 
@@ -146,7 +162,7 @@ sub validateID
       || ThrowUserError("invalid_attach_id");
 
     # Make sure the user is authorized to access this attachment's bug.
-    my ($bugid, $isprivate) = FetchSQLData();
+    ($bugid, my $isprivate) = FetchSQLData();
     ValidateBugID($bugid);
     if (($isprivate > 0 ) && Param("insidergroup") && !(UserInGroup(Param("insidergroup")))) {
         ThrowUserError("attachment_access_denied");
@@ -677,7 +693,16 @@ sub update
   SendSQL("LOCK TABLES attachments WRITE , flags WRITE , " . 
           "flagtypes READ , fielddefs READ , bugs_activity WRITE, " . 
           "flaginclusions AS i READ, flagexclusions AS e READ, " . 
-          "bugs READ, profiles READ");
+          # cc, bug_group_map, user_group_map, and groups are in here so we
+          # can check the permissions of flag requestees and email addresses
+          # on the flag type cc: lists via the ConfirmGroup and CanSeeBug
+          # function calls in Flag::notify.  group_group_map is in here in case
+          # ConfirmGroup needs to call DeriveGroup.  profiles and user_group_map
+          # would be READ locks instead of WRITE locks if it weren't for
+          # DeriveGroup, which needs to write to those tables.
+          "bugs READ, profiles WRITE, " . 
+          "cc READ, bug_group_map READ, user_group_map WRITE, " . 
+          "group_group_map READ, groups READ");
   
   # Get a copy of the attachment record before we make changes
   # so we can record those changes in the activity table.
index 911f9927816ba55054af4260cfe59e3c3a35d6fe..4caaa3f84306b01e3b50751ce2c2ff5d1a750c95 100644 (file)
@@ -549,9 +549,15 @@ sub CanEnterProduct {
 
 #
 # This function returns an alphabetical list of product names to which
-# the user can enter bugs.
+# the user can enter bugs.  If the $by_id parameter is true, also retrieves IDs
+# and pushes them onto the list as id, name [, id, name...] for easy slurping
+# into a hash by the calling code.
 sub GetSelectableProducts {
-    my $query = "SELECT name " .
+    my ($by_id) = @_;
+
+    my $extra_sql = $by_id ? "id, " : "";
+
+    my $query = "SELECT $extra_sql name " .
                 "FROM products " .
                 "LEFT JOIN group_control_map " .
                 "ON group_control_map.product_id = products.id ";
@@ -570,9 +576,7 @@ sub GetSelectableProducts {
     PushGlobalSQLState();
     SendSQL($query);
     my @products = ();
-    while (MoreSQLData()) {
-        push @products,FetchOneColumn();
-    }
+    push(@products, FetchSQLData()) while MoreSQLData();
     PopGlobalSQLState();
     return (@products);
 }
@@ -580,50 +584,50 @@ sub GetSelectableProducts {
 # GetSelectableProductHash
 # returns a hash containing 
 # legal_products => an enterable product list
-# legal_components => the list of components of enterable products
-# components => a hash of component lists for each enterable product
+# legal_(components|versions|milestones) =>
+#   the list of components, versions, and milestones of enterable products
+# (components|versions|milestones)_by_product
+#    => a hash of component lists for each enterable product
+# Milestones only get returned if the usetargetmilestones parameter is set.
 sub GetSelectableProductHash {
-    my $query = "SELECT products.name, components.name " .
-                "FROM products " .
-                "LEFT JOIN components " .
-                "ON components.product_id = products.id " .
-                "LEFT JOIN group_control_map " .
-                "ON group_control_map.product_id = products.id ";
-    if (Param('useentrygroupdefault')) {
-        $query .= "AND group_control_map.entry != 0 ";
-    } else {
-        $query .= "AND group_control_map.membercontrol = " .
-                  CONTROLMAPMANDATORY . " ";
-    }
-    if ((defined @{$::vars->{user}{groupids}}) 
-        && (@{$::vars->{user}{groupids}} > 0)) {
-        $query .= "AND group_id NOT IN(" . 
-                   join(',', @{$::vars->{user}{groupids}}) . ") ";
-    }
-    $query .= "WHERE group_id IS NULL " .
-              "ORDER BY products.name, components.name";
+    # The hash of selectable products and their attributes that gets returned
+    # at the end of this function.
+    my $selectables = {};
+
+    my %products = GetSelectableProducts(1);
+
+    $selectables->{legal_products} = [sort values %products];
+
+    # Run queries that retrieve the list of components, versions,
+    # and target milestones (if used) for the selectable products.
+    my @tables = qw(components versions);
+    push(@tables, 'milestones') if Param('usetargetmilestone');
+
     PushGlobalSQLState();
-    SendSQL($query);
-    my @products = ();
-    my %components = ();
-    my %components_by_product = ();
-    while (MoreSQLData()) {
-        my ($product, $component) = FetchSQLData();
-        if (!grep($_ eq $product, @products)) {
-            push @products, $product;
-        }
-        if ($component) {
-            $components{$component} = 1;
-            push @{$components_by_product{$product}}, $component;
+    foreach my $table (@tables) {
+        # Why oh why can't we standardize on these names?!?
+        my $fld = ($table eq "components" ? "name" : "value");
+
+        my $query = "SELECT $fld, product_id FROM $table WHERE product_id IN " .
+                    "(" . join(",", keys %products) . ") ORDER BY $fld";
+        SendSQL($query);
+
+        my %values;
+        my %values_by_product;
+
+        while (MoreSQLData()) {
+            my ($name, $product_id) = FetchSQLData();
+            next unless $name;
+            $values{$name} = 1;
+            push @{$values_by_product{$products{$product_id}}}, $name;
         }
+
+        $selectables->{"legal_$table"} = [sort keys %values];
+        $selectables->{"${table}_by_product"} = \%values_by_product;
     }
     PopGlobalSQLState();
-    my @componentlist = (sort keys %components);
-    return {
-        legal_products => \@products,
-        legal_components => \@componentlist,
-        components => \%components_by_product,
-    };
+
+    return $selectables;
 }
 
 
@@ -724,24 +728,28 @@ sub Crypt {
 # Permissions must be rederived if ANY groups have a last_changed newer
 # than the profiles.refreshed_when value.
 sub ConfirmGroup {
-    my ($user) = (@_);
+    my ($user, $locked) = (@_);
     PushGlobalSQLState();
     SendSQL("SELECT userid FROM profiles, groups WHERE userid = $user " .
             "AND profiles.refreshed_when <= groups.last_changed ");
     my $ret = FetchSQLData();
     PopGlobalSQLState();
     if ($ret) {
-        DeriveGroup($user);
+        DeriveGroup($user, $locked);
     }
 }
 
 # DeriveGroup removes and rederives all derived group permissions for
-# the specified user.
+# the specified user.  If $locked is true, Bugzilla has already locked
+# the necessary tables as part of a larger transaction, so this function
+# shouldn't lock them again (since then tables not part of this function's
+# lock will get unlocked).
 sub DeriveGroup {
-    my ($user) = (@_);
+    my ($user, $locked) = (@_);
     PushGlobalSQLState();
 
-    SendSQL("LOCK TABLES profiles WRITE, user_group_map WRITE, group_group_map READ, groups READ");
+    SendSQL("LOCK TABLES profiles WRITE, user_group_map WRITE, group_group_map READ, groups READ")
+      unless $locked;
 
     # avoid races,  we are only as up to date as the BEGINNING of this process
     SendSQL("SELECT login_name, NOW() FROM profiles WHERE userid = $user");
index 09b45cf92fc135f072a72f56abd9d6c98ab404a5..83d601d332d9926d9a18b9de2959d0f348e5d525 100755 (executable)
@@ -91,12 +91,21 @@ scalar(@idlist) || ThrowUserError("no_bugs_chosen");
 
 # do a match on the fields if applicable
 
+# The order of these function calls is important, as both Flag::validate
+# and FlagType::validate assume User::match_field has ensured that the values
+# in the requestee fields are legitimate user email addresses.
 &Bugzilla::User::match_field({
     'qa_contact'                => { 'type' => 'single' },
     'newcc'                     => { 'type' => 'multi'  },
     'assigned_to'               => { 'type' => 'single' },
     '^requestee(_type)?-(\d+)$' => { 'type' => 'single' },
 });
+# Validate flags, but only if the user is changing a single bug,
+# since the multi-change form doesn't include flag changes.
+if (defined $::FORM{'id'}) {
+    Bugzilla::Flag::validate(\%::FORM, $::FORM{'id'});
+    Bugzilla::FlagType::validate(\%::FORM, $::FORM{'id'});
+}
 
 # If we are duping bugs, let's also make sure that we can change 
 # the original.  This takes care of issue A on bug 96085.
@@ -1080,7 +1089,11 @@ foreach my $id (@idlist) {
             "products READ, components READ, " .
             "keywords $write, longdescs $write, fielddefs $write, " .
             "bug_group_map $write, flags $write, duplicates $write," .
-            "user_group_map READ, flagtypes READ, " . 
+            # user_group_map would be a READ lock except that Flag::process
+            # may call Flag::notify, which calls ConfirmGroup, which might
+            # call DeriveGroup, which wants a WRITE lock on that table.
+            # group_group_map is in here at all because DeriveGroups needs it.
+            "user_group_map $write, group_group_map READ, flagtypes READ, " . 
             "flaginclusions AS i READ, flagexclusions AS e READ, " .
             "keyworddefs READ, groups READ, attachments READ, " .
             "group_control_map AS oldcontrolmap READ, " .
index 934c0511fa98d362e2686ceb0359dcec3a1acb99..1aaa581b64d5fb230425c95dbc5abd9314e68819 100644 (file)
     format like JPG or PNG, or put it elsewhere on the web and
     link to it from the bug's URL field or in a comment on the bug.
       
+  [% ELSIF error == "flag_requestee_unauthorized" %]
+    [% title = "Flag Requestee Not Authorized" %]
+
+    You asked [% requestee.identity FILTER html %]
+    for <code>[% flag_type.name FILTER html %]</code> on bug [% bug_id -%] 
+    [% IF attach_id %], attachment [% attach_id %][% END %], but that bug
+    has been restricted to users in certain groups, and the user you asked
+    isn't in all the groups to which the bug has been restricted.
+    Please choose someone else to ask, or make the bug accessible to users
+    on its CC: list and add that user to the list.
+    
+  [% ELSIF error == "flag_requestee_unauthorized_attachment" %]
+    [% title = "Flag Requestee Not Authorized" %]
+
+    You asked [% requestee.identity FILTER html %]
+    for <code>[% flag_type.name FILTER html %]</code> on bug [% bug_id %],
+    attachment [% attach_id %], but that attachment is restricted to users 
+    in the [% Param("insidergroup") FILTER html %] group, and the user
+    you asked isn't in that group.  Please choose someone else to ask,
+    or ask an administrator to add the user to the group.
+    
   [% ELSIF error == "flag_type_cc_list_invalid" %]
     [% title = "Flag Type CC List Invalid" %]
     The CC list [% cc_list FILTER html %] must be less than 200 characters long.