diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm
index 2cac77ed34e7a11bb4c8149e76990e53a2c76316..01d2321c40e6ca389f806ea475fecda5e915bca4 100755
--- a/Bugzilla/Bug.pm
+++ b/Bugzilla/Bug.pm
@@ -133,7 +133,7 @@ sub initBug  {
      }
   }
 
-  $self->{'whoid'} = $user_id;
+  $self->{'who'} = new Bugzilla::User($user_id);
 
   my $query = "
     SELECT
@@ -156,7 +156,7 @@ sub initBug  {
   &::SendSQL($query);
   my @row = ();
 
-  if ((@row = &::FetchSQLData()) && &::CanSeeBug($bug_id, $self->{'whoid'})) {
+  if ((@row = &::FetchSQLData()) && $self->{'who'}->can_see_bug($bug_id)) {
     my $count = 0;
     my %fields;
     foreach my $field ("bug_id", "alias", "product_id", "product", "version", 
diff --git a/Bugzilla/BugMail.pm b/Bugzilla/BugMail.pm
index 40a40dc2beb49a8488930932b0c3b157c5c7f361..8731a9f7243aea21b7311cf326c5412c2387ea86 100644
--- a/Bugzilla/BugMail.pm
+++ b/Bugzilla/BugMail.pm
@@ -720,7 +720,7 @@ sub NewProcessOnePerson ($$$$$$$$$$$$$) {
     # see the action of restricting the bug itself; the bug will just 
     # quietly disappear from their radar.
     #
-    return unless CanSeeBug($id, $userid);
+    return unless $user->can_see_bug($id);
 
     #  Drop any non-insiders if the comment is private
     return if (Param("insidergroup") && 
@@ -733,7 +733,7 @@ sub NewProcessOnePerson ($$$$$$$$$$$$$) {
         my $save_id = $dep_id;
         detaint_natural($dep_id) || warn("Unexpected Error: \@depbugs contains a non-numeric value: '$save_id'")
                                  && return;
-        return unless CanSeeBug($dep_id, $userid);
+        return unless $user->can_see_bug($dep_id);
     }
 
     my %mailhead = %defmailhead;
diff --git a/Bugzilla/Flag.pm b/Bugzilla/Flag.pm
index d4dac90532b08513c2b28b6871f61e46b0b0a187..3b2ae36c483056195c5d8b499503da952f96e8cc 100644
--- a/Bugzilla/Flag.pm
+++ b/Bugzilla/Flag.pm
@@ -185,7 +185,7 @@ sub validate {
                 my $requestee = Bugzilla::User->new_from_login($requestee_email);
 
                 # Throw an error if the user can't see the bug.
-                if (!&::CanSeeBug($bug_id, $requestee->id))
+                if (!$requestee->can_see_bug($bug_id))
                 {
                     ThrowUserError("flag_requestee_unauthorized",
                                    { flag_type => $flag->{'type'},
@@ -592,7 +592,7 @@ sub notify {
               || next;
 
             next if $flag->{'target'}->{'bug'}->{'restricted'}
-              && !&::CanSeeBug($flag->{'target'}->{'bug'}->{'id'}, $ccuser->id);
+              && !$ccuser->can_see_bug($flag->{'target'}->{'bug'}->{'id'});
             next if $flag->{'target'}->{'attachment'}->{'isprivate'}
               && Param("insidergroup")
               && !$ccuser->in_group(Param("insidergroup"));
diff --git a/Bugzilla/FlagType.pm b/Bugzilla/FlagType.pm
index f1cb00c5da098576a59742583bfc7ef03b5cb5c2..687a01768eea4bd5702bc0ed1665b7dc7fa8cd00 100644
--- a/Bugzilla/FlagType.pm
+++ b/Bugzilla/FlagType.pm
@@ -226,7 +226,7 @@ sub validate {
             my $requestee = Bugzilla::User->new_from_login($requestee_email);
 
             # Throw an error if the user can't see the bug.
-            if (!&::CanSeeBug($bug_id, $requestee->id))
+            if (!$requestee->can_see_bug($bug_id))
             {
                 ThrowUserError("flag_requestee_unauthorized",
                                { flag_type => $flag_type,
diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm
index 8396d183f28a95b2c9e54fd565b1ee94ce62306e..66087b81c941362a44b5f925d15ec9c066e4a042 100644
--- a/Bugzilla/User.pm
+++ b/Bugzilla/User.pm
@@ -244,6 +244,75 @@ sub in_group {
     return defined($res);
 }
 
+sub can_see_bug {
+    my ($self, $bugid) = @_;
+    my $dbh = Bugzilla->dbh;
+    my $sth  = $self->{sthCanSeeBug};
+    my $userid  = $self->{id};
+    # Get fields from bug, presence of user on cclist, and determine if
+    # the user is missing any groups required by the bug. The prepared query
+    # is cached because this may be called for every row in buglists or
+    # every bug in a dependency list.
+    unless ($sth) {
+        $sth = $dbh->prepare("SELECT reporter, assigned_to, qa_contact,
+                             reporter_accessible, cclist_accessible,
+                             COUNT(cc.who), COUNT(bug_group_map.bug_id)
+                             FROM bugs
+                             LEFT JOIN cc 
+                               ON cc.bug_id = bugs.bug_id
+                               AND cc.who = $userid
+                             LEFT JOIN bug_group_map 
+                               ON bugs.bug_id = bug_group_map.bug_id
+                               AND bug_group_map.group_ID NOT IN(" .
+                               join(',',(-1, values(%{$self->groups}))) .
+                               ") WHERE bugs.bug_id = ? GROUP BY bugs.bug_id");
+    }
+    $sth->execute($bugid);
+    my ($reporter, $owner, $qacontact, $reporter_access, $cclist_access,
+        $isoncclist, $missinggroup) = $sth->fetchrow_array();
+    $self->{sthCanSeeBug} = $sth;
+    return ( (($reporter == $userid) && $reporter_access)
+           || (Param('qacontact') && ($qacontact == $userid) && $userid)
+           || ($owner == $userid)
+           || ($isoncclist && $cclist_access)
+           || (!$missinggroup) );
+}
+
+sub get_selectable_products {
+    my ($self, $by_id) = @_;
+
+    if (defined $self->{SelectableProducts}) {
+        my %list = @{$self->{SelectableProducts}};
+        return \%list if $by_id;
+        return values(%list);
+    }
+
+    my $query = "SELECT id, name " .
+                "FROM products " .
+                "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 . " ";
+    }
+    $query .= "AND group_id NOT IN(" . 
+               join(',', (-1,values(%{Bugzilla->user->groups}))) . ") " .
+              "WHERE group_id IS NULL ORDER BY name";
+    my $dbh = Bugzilla->dbh;
+    my $sth = $dbh->prepare($query);
+    $sth->execute();
+    my @products = ();
+    while (my @row = $sth->fetchrow_array) {
+        push(@products, @row);
+    }
+    $self->{SelectableProducts} = \@products;
+    my %list = @products;
+    return \%list if $by_id;
+    return values(%list);
+}
+
 # visible_groups_inherited returns a reference to a list of all the groups
 # whose members are visible to this user.
 sub visible_groups_inherited {
@@ -939,6 +1008,10 @@ intended for cases where we are not looking at the currently logged in user,
 and only need to make a quick check for the group, where calling C<groups>
 and getting all of the groups would be overkill.
 
+=item C<can_see_bug(bug_id)>
+
+Determines if the user can see the specified bug.
+
 =item C<derive_groups>
 
 Bugzilla allows for group inheritance. When data about the user (or any of the
@@ -947,6 +1020,13 @@ care of by the constructor. However, when updating the email address, the
 user may be placed into different groups, based on a new email regexp. This
 method should be called in such a case to force reresolution of these groups.
 
+=item C<get_selectable_products(by_id)>
+
+Returns an alphabetical list of product names from which
+the user can select bugs.  If the $by_id parameter is true, it returns
+a hash where the keys are the product ids and the values are the
+product names.
+
 =item C<visible_groups_inherited>
 
 Returns a list of all groups whose members should be visible to this user.
diff --git a/CGI.pl b/CGI.pl
index 5be0261d06d2d77a032348667760c088c2ac0fb1..4f5b79f72fa9c4400e0aa302c56a541268517756 100644
--- a/CGI.pl
+++ b/CGI.pl
@@ -172,7 +172,7 @@ sub ValidateBugID {
 
     return if $skip_authorization;
     
-    return if CanSeeBug($id, $::userid);
+    return if Bugzilla->user->can_see_bug($id);
 
     # The user did not pass any of the authorization tests, which means they
     # are not authorized to see the bug.  Display an error and stop execution.
diff --git a/globals.pl b/globals.pl
index d1680959e84f28252c6efdcb6adb43776d85e5eb..9872dff709ede959fb0f2ba9d6c189a755ecd004 100644
--- a/globals.pl
+++ b/globals.pl
@@ -630,48 +630,6 @@ sub GetFieldDefs {
 }
 
 
-sub CanSeeBug {
-
-    my ($id, $userid) = @_;
-
-    # Query the database for the bug, retrieving a boolean value that
-    # represents whether or not the user is authorized to access the bug.
-
-    # if no groups are found --> user is permitted to access
-    # if no user is found for any group --> user is not permitted to access
-    my $query = "SELECT bugs.bug_id, reporter, assigned_to, qa_contact," .
-        " reporter_accessible, cclist_accessible," .
-        " cc.who IS NOT NULL," .
-        " COUNT(DISTINCT(bug_group_map.group_id)) as cntbugingroups," .
-        " COUNT(DISTINCT(user_group_map.group_id)) as cntuseringroups" .
-        " FROM bugs" .
-        " LEFT JOIN cc ON bugs.bug_id = cc.bug_id" .
-        " AND cc.who = $userid" .
-        " LEFT JOIN bug_group_map ON bugs.bug_id = bug_group_map.bug_id" .
-        " LEFT JOIN user_group_map ON" .
-        " user_group_map.group_id = bug_group_map.group_id" .
-        " AND user_group_map.isbless = 0" .
-        " AND user_group_map.user_id = $userid" .
-        " WHERE bugs.bug_id = $id GROUP BY bugs.bug_id";
-    PushGlobalSQLState();
-    SendSQL($query);
-    my ($found_id, $reporter, $assigned_to, $qa_contact,
-        $rep_access, $cc_access,
-        $found_cc, $found_groups, $found_members) 
-        = FetchSQLData();
-    PopGlobalSQLState();
-    return (
-               ($found_groups == 0) 
-               || (($userid > 0) && 
-                  (
-                       ($assigned_to == $userid) 
-                    || (Param('useqacontact') && $qa_contact == $userid)
-                    || (($reporter == $userid) && $rep_access) 
-                    || ($found_cc && $cc_access) 
-                    || ($found_groups == $found_members)
-                  ))
-           );
-}
 
 sub ValidatePassword {
     # Determines whether or not a password is valid (i.e. meets Bugzilla's
@@ -947,7 +905,7 @@ sub GetAttachmentLink {
             my ($bugid, $isobsolete, $desc) = FetchSQLData();
             my $title = "";
             my $className = "";
-            if (CanSeeBug($bugid, $::userid)) {
+            if (Bugzilla->user->can_see_bug($bugid)) {
                 $title = $desc;
             }
             if ($isobsolete) {
@@ -1018,7 +976,7 @@ sub GetBugLink {
                 $title .= " $bug_res";
                 $post = '</span>';
             }
-            if (CanSeeBug($bug_num, $::userid)) {
+            if (Bugzilla->user->can_see_bug($bug_num)) {
                 $title .= " - $bug_desc";
             }
             $::buglink{$bug_num} = [$pre, value_quote($title), $post];
diff --git a/long_list.cgi b/long_list.cgi
index 5644a53232d054c2cc2a6874864ba5fed1060609..757d002396cd97716027e81c7164796d0b0d3682 100755
--- a/long_list.cgi
+++ b/long_list.cgi
@@ -75,7 +75,7 @@ my @bugs;
 
 foreach my $bug_id (split(/[:,]/, $buglist)) {
     detaint_natural($bug_id) || next;
-    CanSeeBug($bug_id, $::userid) || next;
+    Bugzilla->user->can_see_bug($bug_id) || next;
     SendSQL("$generic_query AND bugs.bug_id = $bug_id");
 
     my %bug;
diff --git a/process_bug.cgi b/process_bug.cgi
index b5d641f7740dcb5b17284d194ac276fb0cca68cf..2810d3b395bce55ed98a77e2792c5af97db3aeb8 100755
--- a/process_bug.cgi
+++ b/process_bug.cgi
@@ -493,8 +493,9 @@ sub DuplicateUserConfirm {
     
     SendSQL("SELECT reporter FROM bugs WHERE bug_id = " . SqlQuote($dupe));
     my $reporter = FetchOneColumn();
+    my $rep_user = Bugzilla::User->new($reporter);
 
-    if (CanSeeBug($original, $reporter)) {
+    if ($rep_user->can_see_bug($original)) {
         $::FORM{'confirm_add_duplicate'} = "1";
         return;
     }
@@ -1773,7 +1774,7 @@ foreach my $id (@idlist) {
 
 # now show the next bug
 if ($next_bug) {
-    if (detaint_natural($next_bug) && CanSeeBug($next_bug, $::userid)) {
+    if (detaint_natural($next_bug) && Bugzilla->user->can_see_bug($next_bug)) {
         my $bug = new Bugzilla::Bug($next_bug, $::userid);
         ThrowCodeError("bug_error", { bug => $bug }) if $bug->error;
 
diff --git a/showdependencygraph.cgi b/showdependencygraph.cgi
index b11562e1e421dd234b915218a8806eabcfae3c46..0d33d316d6275a76518a8f2d9c326f92040472cc 100755
--- a/showdependencygraph.cgi
+++ b/showdependencygraph.cgi
@@ -170,7 +170,7 @@ foreach my $k (keys(%seen)) {
     $summary ||= '';
 
     # Resolution and summary are shown only if user can see the bug
-    if (!CanSeeBug($k, $::userid)) {
+    if (!Bugzilla->user->can_see_bug($k)) {
         $resolution = $summary = '';
     }
 
diff --git a/showdependencytree.cgi b/showdependencytree.cgi
index 202043acdc538140cc4bb21f5609a3cc98cb7e63..f1a495a6d434a99f73bcab14f93772937fcd973c 100755
--- a/showdependencytree.cgi
+++ b/showdependencytree.cgi
@@ -146,7 +146,7 @@ sub GetBug {
     my ($id) = @_;
     
     my $bug = {};
-    if (CanSeeBug($id, $::userid)) {
+    if (Bugzilla->user->can_see_bug($id)) {
         SendSQL("SELECT 1, 
                                   bug_status, 
                                   short_desc, 
diff --git a/votes.cgi b/votes.cgi
index 8bd007d602ac89c62d8870ab9b5e713df9e101c4..f2590b3244a681cbfdbf03e8b69ab03b794ca048 100755
--- a/votes.cgi
+++ b/votes.cgi
@@ -185,7 +185,7 @@ sub show_user {
             # and they can see there are votes 'missing', but not on what bug
             # they are. This seems a reasonable compromise; the alternative is
             # to lie in the totals.
-            next if !CanSeeBug($id, $userid);            
+            next if !Bugzilla->user->can_see_bug($id);            
             
             push (@bugs, { id => $id, 
                            summary => $summary,