• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2015 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Adds pre hook to data store queries to hide internal-only data.
6
7Checks if the user has a google.com address, and hides data with the
8internal_only property set if not.
9"""
10
11import webapp2
12
13from google.appengine.api import apiproxy_stub_map
14from google.appengine.api import users
15from google.appengine.datastore import datastore_pb
16
17from dashboard import utils
18
19# The list below contains all kinds that have an internal_only property.
20# IMPORTANT: any new data types with internal_only properties must be added
21# here in order to be restricted to internal users.
22_INTERNAL_ONLY_KINDS = [
23    'Bot',
24    'Test',
25    'Row',
26    'Sheriff',
27    'Anomaly',
28    'StoppageAlert',
29    'TryJob',
30]
31
32# Permissions namespaces.
33EXTERNAL = 'externally_visible'
34INTERNAL = 'internal_only'
35
36
37def InstallHooks():
38  """Installs datastore pre hook to add access checks to queries.
39
40  This only needs to be called once, when doing config (currently in
41  appengine_config.py).
42  """
43  apiproxy_stub_map.apiproxy.GetPreCallHooks().Push(
44      '_DatastorePreHook', _DatastorePreHook, 'datastore_v3')
45
46
47def SetPrivilegedRequest():
48  """Allows the current request to act as a privileged user.
49
50  This should ONLY be called for handlers that are restricted from end users
51  by some other mechanism (IP whitelisting, admin-only pages).
52
53  This should be set once per request, before accessing the data store.
54  """
55  request = webapp2.get_request()
56  request.registry['privileged'] = True
57
58
59def SetSinglePrivilegedRequest():
60  """Allows the current request to act as a privileged user only ONCE.
61
62  This should be called ONLY by handlers that have checked privilege immediately
63  before making a query. It will be automatically unset when the next query is
64  made.
65  """
66  request = webapp2.get_request()
67  request.registry['single_privileged'] = True
68
69
70def CancelSinglePrivilegedRequest():
71  """Disallows the current request to act as a privileged user only."""
72  request = webapp2.get_request()
73  request.registry['single_privileged'] = False
74
75
76def _IsServicingPrivilegedRequest():
77  """Checks whether the request is considered privileged."""
78  try:
79    request = webapp2.get_request()
80  except AssertionError:
81    # This happens in unit tests, when code gets called outside of a request.
82    return False
83  path = getattr(request, 'path', '')
84  if path.startswith('/mapreduce'):
85    return True
86  if path.startswith('/_ah/queue/deferred'):
87    return True
88  if path.startswith('/_ah/pipeline/'):
89    return True
90  if request.registry.get('privileged', False):
91    return True
92  if request.registry.get('single_privileged', False):
93    request.registry['single_privileged'] = False
94    return True
95  whitelist = utils.GetIpWhitelist()
96  if whitelist and hasattr(request, 'remote_addr'):
97    return request.remote_addr in whitelist
98  return False
99
100
101def IsUnalteredQueryPermitted():
102  """Checks if the current user is internal, or the request is privileged.
103
104  "Internal users" are users whose email address belongs to a certain
105  privileged domain; but some privileged requests, such as task queue tasks,
106  are also considered privileged.
107
108  Returns:
109    True for users with google.com emails and privileged requests.
110  """
111  if utils.IsInternalUser():
112    return True
113  if users.is_current_user_admin():
114    # It's possible to be an admin with a non-internal account; For example,
115    # the default login for dev appserver instances is test@example.com.
116    return True
117  return _IsServicingPrivilegedRequest()
118
119
120def GetNamespace():
121  """Returns a namespace prefix string indicating internal or external user."""
122  return INTERNAL if IsUnalteredQueryPermitted() else EXTERNAL
123
124
125def _DatastorePreHook(service, call, request, _):
126  """Adds a filter which checks whether to return internal data for queries.
127
128  If the user is not privileged, we don't want to return any entities that
129  have internal_only set to True. That is done here in a datastore hook.
130  See: https://developers.google.com/appengine/articles/hooks
131
132  Args:
133    service: Service name, must be 'datastore_v3'.
134    call: String representing function to call. One of 'Put', Get', 'Delete',
135        or 'RunQuery'.
136    request: Request protobuf.
137    _: Response protobuf (not used).
138  """
139  assert service == 'datastore_v3'
140  if call != 'RunQuery':
141    return
142  if request.kind() not in _INTERNAL_ONLY_KINDS:
143    return
144  if IsUnalteredQueryPermitted():
145    return
146
147  # Add a filter for internal_only == False, because the user is external.
148  try:
149    external_filter = request.filter_list().add()
150  except AttributeError:
151    # This is required to support proto1, which may be used by the unit tests.
152    # Later, if we don't need to support proto1, then this can be removed.
153    external_filter = request.add_filter()
154  external_filter.set_op(datastore_pb.Query_Filter.EQUAL)
155  new_property = external_filter.add_property()
156  new_property.set_name('internal_only')
157  new_property.mutable_value().set_booleanvalue(False)
158  new_property.set_multiple(False)
159