| # Copyright 2017 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. |
| |
| import json |
| import logging |
| import re |
| import traceback |
| |
| import webapp2 |
| |
| from dashboard.api import api_auth |
| from dashboard.common import utils |
| |
| _ALLOWED_ORIGINS = [ |
| 'chromeperf.appspot.com', |
| 'chromiumdash.appspot.com', |
| 'chromiumdash-staging.googleplex.com', |
| ] |
| |
| |
| class BadRequestError(Exception): |
| pass |
| |
| |
| class ForbiddenError(Exception): |
| def __init__(self): |
| super(ForbiddenError, self).__init__('Access denied') |
| |
| |
| class NotFoundError(Exception): |
| def __init__(self): |
| super(NotFoundError, self).__init__('Not found') |
| |
| |
| class ApiRequestHandler(webapp2.RequestHandler): |
| """API handler for api requests. |
| |
| Convenience methods handling authentication errors and surfacing them. |
| """ |
| |
| def _CheckUser(self): |
| """Checks whether the user has permission to make requests. |
| |
| This method must be overridden by subclasses to perform access control. |
| |
| Raises: |
| api_auth.NotLoggedInError: The user was not logged in, |
| and must be to be to make this request. |
| api_auth.OAuthError: The request was not a valid OAuth request, |
| or the client ID was not in the whitelist. |
| ForbiddenError: The user does not have permission to make this request. |
| """ |
| raise NotImplementedError() |
| |
| def _CheckIsInternalUser(self): |
| self._CheckIsLoggedIn() |
| if not utils.IsInternalUser(): |
| raise ForbiddenError() |
| |
| def _CheckIsLoggedIn(self): |
| api_auth.Authorize() |
| |
| def post(self, *args): |
| """Returns alert data in response to API requests. |
| |
| Outputs: |
| JSON results. |
| """ |
| self._SetCorsHeadersIfAppropriate() |
| |
| try: |
| self._CheckUser() |
| except api_auth.NotLoggedInError as e: |
| self.WriteErrorMessage(e.message, 401) |
| return |
| except api_auth.OAuthError as e: |
| self.WriteErrorMessage(e.message, 403) |
| return |
| except ForbiddenError as e: |
| self.WriteErrorMessage(e.message, 403) |
| return |
| # Allow oauth.Error to manifest as HTTP 500. |
| |
| try: |
| results = self.Post(*args) |
| self.response.out.write(json.dumps(results)) |
| except NotFoundError as e: |
| self.WriteErrorMessage(e.message, 404) |
| except (BadRequestError, KeyError, TypeError, ValueError) as e: |
| self.WriteErrorMessage(e.message, 400) |
| |
| def options(self, *_): # pylint: disable=invalid-name |
| self._SetCorsHeadersIfAppropriate() |
| |
| def Post(self, *_): |
| raise NotImplementedError() |
| |
| def _SetCorsHeadersIfAppropriate(self): |
| self.response.headers['Content-Type'] = 'application/json; charset=utf-8' |
| set_cors_headers = False |
| origin = self.request.headers.get('Origin', '') |
| for allowed in _ALLOWED_ORIGINS: |
| dev_pattern = re.compile( |
| r'https://[A-Za-z0-9]+-dot-' + re.escape(allowed)) |
| prod_pattern = re.compile(r'https://' + re.escape(allowed)) |
| if dev_pattern.match(origin) or prod_pattern.match(origin): |
| set_cors_headers = True |
| if not set_cors_headers: |
| return |
| self.response.headers.add_header('Access-Control-Allow-Origin', origin) |
| self.response.headers.add_header('Access-Control-Allow-Credentials', 'true') |
| self.response.headers.add_header( |
| 'Access-Control-Allow-Methods', 'GET,OPTIONS,POST') |
| self.response.headers.add_header( |
| 'Access-Control-Allow-Headers', 'Accept,Authorization,Content-Type') |
| self.response.headers.add_header('Access-Control-Max-Age', '3600') |
| |
| def WriteErrorMessage(self, message, status): |
| logging.error(traceback.format_exc()) |
| self.response.set_status(status) |
| self.response.out.write(json.dumps({'error': message})) |