Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/search/backendnonviewable.py b/search/backendnonviewable.py
new file mode 100644
index 0000000..d76eeef
--- /dev/null
+++ b/search/backendnonviewable.py
@@ -0,0 +1,137 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Servlet that searches for issues that the specified user cannot view.
+
+The GET request to a backend has query string parameters for the
+shard_id, a user_id, and list of project IDs.  It returns a
+JSON-formatted dict with issue_ids that that user is not allowed to
+view.  As a side-effect, this servlet updates multiple entries
+in memcache, including each "nonviewable:USER_ID;PROJECT_ID;SHARD_ID".
+"""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+
+from google.appengine.api import memcache
+
+import settings
+from framework import authdata
+from framework import framework_constants
+from framework import framework_helpers
+from framework import jsonfeed
+from framework import permissions
+from framework import sql
+from search import search_helpers
+
+
+
+# We cache the set of IIDs that a given user cannot view, and we invalidate
+# that set when the issues are changed via Monorail.  Also, we limit the live
+# those cache entries so that changes in a user's (direct or indirect) roles
+# in a project will take effect.
+NONVIEWABLE_MEMCACHE_EXPIRATION = 15 * framework_constants.SECS_PER_MINUTE
+
+
+class BackendNonviewable(jsonfeed.InternalTask):
+  """JSON servlet for getting issue IDs that the specified user cannot view."""
+
+  CHECK_SAME_APP = True
+
+  def HandleRequest(self, mr):
+    """Get all the user IDs that the specified user cannot view.
+
+    Args:
+      mr: common information parsed from the HTTP request.
+
+    Returns:
+      Results dictionary {project_id: [issue_id]} in JSON format.
+    """
+    if mr.shard_id is None:
+      return {'message': 'Cannot proceed without a valid shard_id.'}
+    user_id = mr.specified_logged_in_user_id
+    auth = authdata.AuthData.FromUserID(mr.cnxn, user_id, self.services)
+    project_id = mr.specified_project_id
+    project = self.services.project.GetProject(mr.cnxn, project_id)
+
+    perms = permissions.GetPermissions(
+        auth.user_pb, auth.effective_ids, project)
+
+    nonviewable_iids = self.GetNonviewableIIDs(
+      mr.cnxn, auth.user_pb, auth.effective_ids, project, perms, mr.shard_id)
+
+    cached_ts = mr.invalidation_timestep
+    if mr.specified_project_id:
+      memcache.set(
+        'nonviewable:%d;%d;%d' % (project_id, user_id, mr.shard_id),
+        (nonviewable_iids, cached_ts),
+        time=NONVIEWABLE_MEMCACHE_EXPIRATION,
+        namespace=settings.memcache_namespace)
+    else:
+      memcache.set(
+        'nonviewable:all;%d;%d' % (user_id, mr.shard_id),
+        (nonviewable_iids, cached_ts),
+        time=NONVIEWABLE_MEMCACHE_EXPIRATION,
+        namespace=settings.memcache_namespace)
+
+    logging.info('set nonviewable:%s;%d;%d to %r', project_id, user_id,
+                 mr.shard_id, nonviewable_iids)
+
+    return {
+      'nonviewable': nonviewable_iids,
+
+      # These are not used in the frontend, but useful for debugging.
+      'project_id': project_id,
+      'user_id': user_id,
+      'shard_id': mr.shard_id,
+      }
+
+  def GetNonviewableIIDs(
+    self, cnxn, user, effective_ids, project, perms, shard_id):
+    """Return a list of IIDs that the user cannot view in the project shard."""
+    # Project owners and site admins can see all issues.
+    if not perms.consider_restrictions:
+      return []
+
+    # There are two main parts to the computation that we do in parallel:
+    # getting at-risk IIDs and getting OK-iids.
+    cnxn_2 = sql.MonorailConnection()
+    at_risk_iids_promise = framework_helpers.Promise(
+      self.GetAtRiskIIDs, cnxn_2, user, effective_ids, project, perms, shard_id)
+    ok_iids = self.GetViewableIIDs(
+      cnxn, effective_ids, project.project_id, shard_id)
+    at_risk_iids = at_risk_iids_promise.WaitAndGetValue()
+
+    # The set of non-viewable issues is the at-risk ones minus the ones where
+    # the user is the reporter, owner, CC'd, or granted "View" permission.
+    nonviewable_iids = set(at_risk_iids).difference(ok_iids)
+
+    return list(nonviewable_iids)
+
+  def GetAtRiskIIDs(
+    self, cnxn, user, effective_ids, project, perms, shard_id):
+    # type: (MonorailConnection, proto.user_pb2.User, Sequence[int], Project,
+    #     permission_objects_pb2.PermissionSet, int) -> Sequence[int]
+    """Return IIDs of restricted issues that user might not be able to view."""
+    at_risk_label_ids = search_helpers.GetPersonalAtRiskLabelIDs(
+      cnxn, user, self.services.config, effective_ids, project, perms)
+    at_risk_iids = self.services.issue.GetIIDsByLabelIDs(
+      cnxn, at_risk_label_ids, project.project_id, shard_id)
+
+    return at_risk_iids
+
+
+  def GetViewableIIDs(self, cnxn, effective_ids, project_id, shard_id):
+    """Return IIDs of issues that user can view because they participate."""
+    # Anon user is never reporter, owner, CC'd or granted perms.
+    if not effective_ids:
+      return []
+
+    ok_iids = self.services.issue.GetIIDsByParticipant(
+      cnxn, effective_ids, [project_id], shard_id)
+
+    return ok_iids