Index: appengine/monorail/search/backendnonviewable.py |
diff --git a/appengine/monorail/search/backendnonviewable.py b/appengine/monorail/search/backendnonviewable.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..e7ec0a11a30ee43dcc59e62c1d7a42d80a875474 |
--- /dev/null |
+++ b/appengine/monorail/search/backendnonviewable.py |
@@ -0,0 +1,143 @@ |
+# Copyright 2016 The Chromium Authors. All rights reserved. |
+# Use of this source code is govered 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". |
+""" |
+ |
+import logging |
+ |
+from google.appengine.api import memcache |
+ |
+from framework import framework_constants |
+from framework import framework_helpers |
+from framework import jsonfeed |
+from framework import permissions |
+from framework import sql |
+ |
+ |
+RESTRICT_VIEW_PATTERN = 'restrict-view-%' |
+ |
+# 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 |
+ user = self.services.user.GetUser(mr.cnxn, user_id) |
+ effective_ids = self.services.usergroup.LookupMemberships(mr.cnxn, user_id) |
+ if user_id: |
+ effective_ids.add(user_id) |
+ project_id = mr.specified_project_id |
+ project = self.services.project.GetProject(mr.cnxn, project_id) |
+ |
+ perms = permissions.GetPermissions(user, effective_ids, project) |
+ |
+ nonviewable_iids = self.GetNonviewableIIDs( |
+ mr.cnxn, user, 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) |
+ else: |
+ memcache.set( |
+ 'nonviewable:all;%d;%d' % (user_id, mr.shard_id), |
+ (nonviewable_iids, cached_ts), |
+ time=NONVIEWABLE_MEMCACHE_EXPIRATION) |
+ |
+ 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): |
+ """Return IIDs of restricted issues that user might not be able to view.""" |
+ at_risk_label_ids = self.GetPersonalAtRiskLabelIDs( |
+ cnxn, user, 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 GetPersonalAtRiskLabelIDs( |
+ self, cnxn, _user, effective_ids, project, perms): |
+ """Return list of label_ids for restriction labels that user can't view.""" |
+ at_risk_label_ids = [] |
+ label_def_rows = self.services.config.GetLabelDefRowsAnyProject( |
+ cnxn, where=[('LOWER(label) LIKE %s', [RESTRICT_VIEW_PATTERN])]) |
+ for label_id, _pid, _rank, label, _docstring, _hidden in label_def_rows: |
+ label_lower = label.lower() |
+ needed_perm = label_lower.split('-', 2)[-1] |
+ if not perms.CanUsePerm(needed_perm, effective_ids, project, []): |
+ at_risk_label_ids.append(label_id) |
+ |
+ return at_risk_label_ids |
+ |
+ 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 |