blob: 7b67be1fd101bbb2e8567e7930e70e33d186784b [file] [log] [blame]
# Copyright 2015 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.
"""Adds pre hook to data store queries to hide internal-only data.
Checks if the user has a google.com address, and hides data with the
internal_only property set if not.
"""
import webapp2
from google.appengine.api import apiproxy_stub_map
from google.appengine.api import users
from google.appengine.datastore import datastore_pb
from dashboard import utils
# The list below contains all kinds that have an internal_only property.
# IMPORTANT: any new data types with internal_only properties must be added
# here in order to be restricted to internal users.
_INTERNAL_ONLY_KINDS = [
'Bot',
'Test',
'Row',
'Sheriff',
'Anomaly',
'StoppageAlert',
'TryJob',
]
# Permissions namespaces.
EXTERNAL = 'externally_visible'
INTERNAL = 'internal_only'
def InstallHooks():
"""Installs datastore pre hook to add access checks to queries.
This only needs to be called once, when doing config (currently in
appengine_config.py).
"""
apiproxy_stub_map.apiproxy.GetPreCallHooks().Push(
'_DatastorePreHook', _DatastorePreHook, 'datastore_v3')
def SetPrivilegedRequest():
"""Allows the current request to act as a privileged user.
This should ONLY be called for handlers that are restricted from end users
by some other mechanism (IP whitelisting, admin-only pages).
This should be set once per request, before accessing the data store.
"""
request = webapp2.get_request()
request.registry['privileged'] = True
def SetSinglePrivilegedRequest():
"""Allows the current request to act as a privileged user only ONCE.
This should be called ONLY by handlers that have checked privilege immediately
before making a query. It will be automatically unset when the next query is
made.
"""
request = webapp2.get_request()
request.registry['single_privileged'] = True
def CancelSinglePrivilegedRequest():
"""Disallows the current request to act as a privileged user only."""
request = webapp2.get_request()
request.registry['single_privileged'] = False
def _IsServicingPrivilegedRequest():
"""Checks whether the request is considered privileged."""
try:
request = webapp2.get_request()
except AssertionError:
# This happens in unit tests, when code gets called outside of a request.
return False
path = getattr(request, 'path', '')
if path.startswith('/mapreduce'):
return True
if path.startswith('/_ah/queue/deferred'):
return True
if path.startswith('/_ah/pipeline/'):
return True
if request.registry.get('privileged', False):
return True
if request.registry.get('single_privileged', False):
request.registry['single_privileged'] = False
return True
whitelist = utils.GetIpWhitelist()
if whitelist and hasattr(request, 'remote_addr'):
return request.remote_addr in whitelist
return False
def IsUnalteredQueryPermitted():
"""Checks if the current user is internal, or the request is privileged.
"Internal users" are users whose email address belongs to a certain
privileged domain; but some privileged requests, such as task queue tasks,
are also considered privileged.
Returns:
True for users with google.com emails and privileged requests.
"""
if utils.IsInternalUser():
return True
if users.is_current_user_admin():
# It's possible to be an admin with a non-internal account; For example,
# the default login for dev appserver instances is test@example.com.
return True
return _IsServicingPrivilegedRequest()
def GetNamespace():
"""Returns a namespace prefix string indicating internal or external user."""
return INTERNAL if IsUnalteredQueryPermitted() else EXTERNAL
def _DatastorePreHook(service, call, request, _):
"""Adds a filter which checks whether to return internal data for queries.
If the user is not privileged, we don't want to return any entities that
have internal_only set to True. That is done here in a datastore hook.
See: https://developers.google.com/appengine/articles/hooks
Args:
service: Service name, must be 'datastore_v3'.
call: String representing function to call. One of 'Put', Get', 'Delete',
or 'RunQuery'.
request: Request protobuf.
_: Response protobuf (not used).
"""
assert service == 'datastore_v3'
if call != 'RunQuery':
return
if request.kind() not in _INTERNAL_ONLY_KINDS:
return
if IsUnalteredQueryPermitted():
return
# Add a filter for internal_only == False, because the user is external.
try:
external_filter = request.filter_list().add()
except AttributeError:
# This is required to support proto1, which may be used by the unit tests.
# Later, if we don't need to support proto1, then this can be removed.
external_filter = request.add_filter()
external_filter.set_op(datastore_pb.Query_Filter.EQUAL)
new_property = external_filter.add_property()
new_property.set_name('internal_only')
new_property.mutable_value().set_booleanvalue(False)
new_property.set_multiple(False)