Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(265)

Unified Diff: third_party/google-endpoints/google/api/control/wsgi.py

Issue 2666783008: Add google-endpoints to third_party/. (Closed)
Patch Set: Created 3 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: third_party/google-endpoints/google/api/control/wsgi.py
diff --git a/third_party/google-endpoints/google/api/control/wsgi.py b/third_party/google-endpoints/google/api/control/wsgi.py
new file mode 100644
index 0000000000000000000000000000000000000000..ff8b88bc82851ea52492f2339510b3878ea948d7
--- /dev/null
+++ b/third_party/google-endpoints/google/api/control/wsgi.py
@@ -0,0 +1,624 @@
+# Copyright 2016 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""wsgi implement behaviour that provides service control as wsgi
+middleware.
+
+It provides the :class:`Middleware`, which is a WSGI middleware implementation
+that wraps another WSGI application to uses a provided
+:class:`google.api.control.client.Client` to provide service control.
+
+"""
+#pylint: disable=too-many-arguments
+
+from __future__ import absolute_import
+
+from datetime import datetime
+import httplib
+import logging
+import os
+import socket
+import uuid
+import urllib2
+import urlparse
+import wsgiref.util
+
+from google.api.auth import suppliers, tokens
+from . import check_request, messages, report_request, service
+
+
+logger = logging.getLogger(__name__)
+
+
+_CONTENT_LENGTH = 'content-length'
+_DEFAULT_LOCATION = 'global'
+
+_METADATA_SERVER_URL = 'http://metadata.google.internal'
+
+
+def _running_on_gce():
+ headers = {'Metadata-Flavor': 'Google'}
+
+ try:
+ request = urllib2.Request(_METADATA_SERVER_URL, headers=headers)
+ response = urllib2.urlopen(request)
+ if response.info().getheader('Metadata-Flavor') == 'Google':
+ return True
+ except (urllib2.URLError, socket.error):
+ pass
+
+ return False
+
+
+def _get_platform():
+ server_software = os.environ.get('SERVER_SOFTWARE', '')
+
+ if server_software.startswith('Development'):
+ return report_request.ReportedPlatforms.DEVELOPMENT
+ elif os.environ.get('KUBERNETES_SERVICE_HOST'):
+ return report_request.ReportedPlatforms.GKE
+ elif _running_on_gce():
+ # We're either in GAE Flex or GCE
+ if os.environ.get('GAE_MODULE_NAME'):
+ return report_request.ReportedPlatforms.GAE_FLEX
+ else:
+ return report_request.ReportedPlatforms.GCE
+ elif os.environ.get('GAE_MODULE_NAME'):
+ return report_request.ReportedPlatforms.GAE_STANDARD
+
+ return report_request.ReportedPlatforms.UNKNOWN
+
+
+platform = _get_platform()
+
+
+def running_on_devserver():
+ return platform == report_request.ReportedPlatforms.DEVELOPMENT
+
+
+def add_all(application, project_id, control_client,
+ loader=service.Loaders.FROM_SERVICE_MANAGEMENT):
+ """Adds all endpoints middleware to a wsgi application.
+
+ Sets up application to use all default endpoints middleware.
+
+ Example:
+
+ >>> application = MyWsgiApp() # an existing WSGI application
+ >>>
+ >>> # the name of the controlled service
+ >>> service_name = 'my-service-name'
+ >>>
+ >>> # A GCP project with service control enabled
+ >>> project_id = 'my-project-id'
+ >>>
+ >>> # wrap the app for service control
+ >>> from google.api.control import wsgi
+ >>> control_client = client.Loaders.DEFAULT.load(service_name)
+ >>> control_client.start()
+ >>> wrapped_app = add_all(application, project_id, control_client)
+ >>>
+ >>> # now use wrapped_app in place of app
+
+ Args:
+ application: the wrapped wsgi application
+ project_id: the project_id thats providing service control support
+ control_client: the service control client instance
+ loader (:class:`google.api.control.service.Loader`): loads the service
+ instance that configures this instance's behaviour
+ """
+ a_service = loader.load()
+ if not a_service:
+ raise ValueError("Failed to load service config")
+ authenticator = _create_authenticator(a_service)
+
+ wrapped_app = Middleware(application, project_id, control_client)
+ if authenticator:
+ wrapped_app = AuthenticationMiddleware(wrapped_app, authenticator)
+ return EnvironmentMiddleware(wrapped_app, a_service)
+
+
+def _next_operation_uuid():
+ return uuid.uuid4().hex
+
+
+class EnvironmentMiddleware(object):
+ """A WSGI middleware that sets related variables in the environment.
+
+ It attempts to add the following vars:
+
+ - google.api.config.service
+ - google.api.config.service_name
+ - google.api.config.method_registry
+ - google.api.config.reporting_rules
+ - google.api.config.method_info
+ """
+ # pylint: disable=too-few-public-methods
+
+ SERVICE = 'google.api.config.service'
+ SERVICE_NAME = 'google.api.config.service_name'
+ METHOD_REGISTRY = 'google.api.config.method_registry'
+ METHOD_INFO = 'google.api.config.method_info'
+ REPORTING_RULES = 'google.api.config.reporting_rules'
+
+ def __init__(self, application, a_service):
+ """Initializes a new Middleware instance.
+
+ Args:
+ application: the wrapped wsgi application
+ a_service (:class:`google.api.gen.servicecontrol_v1_messages.Service`):
+ a service instance
+ """
+ if not isinstance(a_service, messages.Service):
+ raise ValueError("service is None or not an instance of Service")
+
+ self._application = application
+ self._service = a_service
+
+ method_registry, reporting_rules = self._configure()
+ self._method_registry = method_registry
+ self._reporting_rules = reporting_rules
+
+ def _configure(self):
+ registry = service.MethodRegistry(self._service)
+ logs, metric_names, label_names = service.extract_report_spec(self._service)
+ reporting_rules = report_request.ReportingRules.from_known_inputs(
+ logs=logs,
+ metric_names=metric_names,
+ label_names=label_names)
+
+ return registry, reporting_rules
+
+ def __call__(self, environ, start_response):
+ environ[self.SERVICE] = self._service
+ environ[self.SERVICE_NAME] = self._service.name
+ environ[self.METHOD_REGISTRY] = self._method_registry
+ environ[self.REPORTING_RULES] = self._reporting_rules
+ parsed_uri = urlparse.urlparse(wsgiref.util.request_uri(environ))
+ http_method = environ.get('REQUEST_METHOD')
+ method_info = self._method_registry.lookup(http_method, parsed_uri.path)
+ if method_info:
+ environ[self.METHOD_INFO] = method_info
+
+ return self._application(environ, start_response)
+
+
+class Middleware(object):
+ """A WSGI middleware implementation that provides service control.
+
+ Example:
+
+ >>> app = MyWsgiApp() # an existing WSGI application
+ >>>
+ >>> # the name of the controlled service
+ >>> service_name = 'my-service-name'
+ >>>
+ >>> # A GCP project with service control enabled
+ >>> project_id = 'my-project-id'
+ >>>
+ >>> # wrap the app for service control
+ >>> from google.api.control import client, wsgi, service
+ >>> control_client = client.Loaders.DEFAULT.load(service_name)
+ >>> control_client.start()
+ >>> wrapped_app = wsgi.Middleware(app, control_client, project_id)
+ >>> env_app = wsgi.EnvironmentMiddleware(wrapped,app)
+ >>>
+ >>> # now use env_app in place of app
+
+ """
+ # pylint: disable=too-few-public-methods, fixme
+ _NO_API_KEY_MSG = (
+ 'Method does not allow callers without established identity.'
+ ' Please use an API key or other form of API consumer identity'
+ ' to call this API.'
+ )
+
+ def __init__(self,
+ application,
+ project_id,
+ control_client,
+ next_operation_id=_next_operation_uuid,
+ timer=datetime.utcnow):
+ """Initializes a new Middleware instance.
+
+ Args:
+ application: the wrapped wsgi application
+ project_id: the project_id thats providing service control support
+ control_client: the service control client instance
+ next_operation_id (func): produces the next operation
+ timer (func[[datetime.datetime]]): a func that obtains the current time
+ """
+ self._application = application
+ self._project_id = project_id
+ self._next_operation_id = next_operation_id
+ self._control_client = control_client
+ self._timer = timer
+
+ def __call__(self, environ, start_response):
+ # pylint: disable=too-many-locals
+ method_info = environ.get(EnvironmentMiddleware.METHOD_INFO)
+ if not method_info:
+ # just allow the wrapped application to handle the request
+ logger.debug('method_info not present in the wsgi environment'
+ ', no service control')
+ return self._application(environ, start_response)
+
+ latency_timer = _LatencyTimer(self._timer)
+ latency_timer.start()
+
+ # Determine if the request can proceed
+ http_method = environ.get('REQUEST_METHOD')
+ parsed_uri = urlparse.urlparse(wsgiref.util.request_uri(environ))
+ app_info = _AppInfo()
+ # TODO: determine if any of the more complex ways of getting the request size
+ # (e.g) buffering and counting the wsgi input stream is more appropriate here
+ try:
+ app_info.request_size = int(environ.get('CONTENT_LENGTH',
+ report_request.SIZE_NOT_SET))
+ except ValueError:
+ logger.warn('ignored bad content-length: %s', environ.get('CONTENT_LENGTH'))
+
+ app_info.http_method = http_method
+ app_info.url = parsed_uri
+
+ check_info = self._create_check_info(method_info, parsed_uri, environ)
+ if not check_info.api_key and not method_info.allow_unregistered_calls:
+ logger.debug("skipping %s, no api key was provided", parsed_uri)
+ error_msg = self._handle_missing_api_key(app_info, start_response)
+ else:
+ check_req = check_info.as_check_request()
+ logger.debug('checking %s with %s', method_info, check_request)
+ check_resp = self._control_client.check(check_req)
+ error_msg = self._handle_check_response(app_info, check_resp, start_response)
+
+ if error_msg:
+ # send a report request that indicates that the request failed
+ rules = environ.get(EnvironmentMiddleware.REPORTING_RULES)
+ latency_timer.end()
+ report_req = self._create_report_request(method_info,
+ check_info,
+ app_info,
+ latency_timer,
+ rules)
+ logger.debug('scheduling report_request %s', report_req)
+ self._control_client.report(report_req)
+ return error_msg
+
+ # update the client with the response
+ latency_timer.app_start()
+
+ # run the application request in an inner handler that sets the status
+ # and response code on app_info
+ def inner_start_response(status, response_headers, exc_info=None):
+ app_info.response_code = int(status.partition(' ')[0])
+ for name, value in response_headers:
+ if name.lower() == _CONTENT_LENGTH:
+ app_info.response_size = int(value)
+ break
+ return start_response(status, response_headers, exc_info)
+
+ result = self._application(environ, inner_start_response)
+
+ # perform reporting, result must be joined otherwise the latency record
+ # is incorrect
+ result = b''.join(result)
+ latency_timer.end()
+ app_info.response_size = len(result)
+ rules = environ.get(EnvironmentMiddleware.REPORTING_RULES)
+ report_req = self._create_report_request(method_info,
+ check_info,
+ app_info,
+ latency_timer,
+ rules)
+ logger.debug('scheduling report_request %s', report_req)
+ self._control_client.report(report_req)
+ return result
+
+ def _create_report_request(self,
+ method_info,
+ check_info,
+ app_info,
+ latency_timer,
+ reporting_rules):
+ # TODO: determine how to obtain the consumer_project_id and the location
+ # correctly
+ report_info = report_request.Info(
+ api_key=check_info.api_key,
+ api_key_valid=app_info.api_key_valid,
+ api_method=method_info.selector,
+ consumer_project_id=self._project_id, # TODO: see above
+ location=_DEFAULT_LOCATION, # TODO: see above
+ method=app_info.http_method,
+ operation_id=check_info.operation_id,
+ operation_name=check_info.operation_name,
+ backend_time=latency_timer.backend_time,
+ overhead_time=latency_timer.overhead_time,
+ platform=platform,
+ producer_project_id=self._project_id,
+ protocol=report_request.ReportedProtocols.HTTP,
+ request_size=app_info.request_size,
+ request_time=latency_timer.request_time,
+ response_code=app_info.response_code,
+ response_size=app_info.response_size,
+ referer=check_info.referer,
+ service_name=check_info.service_name,
+ url=app_info.url
+ )
+ return report_info.as_report_request(reporting_rules, timer=self._timer)
+
+ def _create_check_info(self, method_info, parsed_uri, environ):
+ service_name = environ.get(EnvironmentMiddleware.SERVICE_NAME)
+ operation_id = self._next_operation_id()
+ api_key_valid = False
+ api_key = _find_api_key_param(method_info, parsed_uri)
+ if not api_key:
+ api_key = _find_api_key_header(method_info, environ)
+ if not api_key:
+ api_key = _find_default_api_key_param(parsed_uri)
+
+ if api_key:
+ api_key_valid = True
+
+ check_info = check_request.Info(
+ api_key=api_key,
+ api_key_valid=api_key_valid,
+ client_ip=environ.get('REMOTE_ADDR', ''),
+ consumer_project_id=self._project_id, # TODO: switch this to producer_project_id
+ operation_id=operation_id,
+ operation_name=method_info.selector,
+ referer=environ.get('HTTP_REFERER', ''),
+ service_name=service_name
+ )
+ return check_info
+
+ def _handle_check_response(self, app_info, check_resp, start_response):
+ code, detail, api_key_valid = check_request.convert_response(
+ check_resp, self._project_id)
+ if code == httplib.OK:
+ return None # the check was OK
+
+ # there was problem; the request cannot proceed
+ logger.warn('Check failed %d, %s', code, detail)
+ error_msg = '%d %s' % (code, detail)
+ start_response(error_msg, [])
+ app_info.response_code = code
+ app_info.api_key_valid = api_key_valid
+ return error_msg # the request cannot continue
+
+ def _handle_missing_api_key(self, app_info, start_response):
+ code = httplib.UNAUTHORIZED
+ detail = self._NO_API_KEY_MSG
+ logger.warn('Check not performed %d, %s', code, detail)
+ error_msg = '%d %s' % (code, detail)
+ start_response(error_msg, [])
+ app_info.response_code = code
+ app_info.api_key_valid = False
+ return error_msg # the request cannot continue
+
+
+class _AppInfo(object):
+ # pylint: disable=too-few-public-methods
+
+ def __init__(self):
+ self.api_key_valid = True
+ self.response_code = httplib.INTERNAL_SERVER_ERROR
+ self.response_size = report_request.SIZE_NOT_SET
+ self.request_size = report_request.SIZE_NOT_SET
+ self.http_method = None
+ self.url = None
+
+
+class _LatencyTimer(object):
+
+ def __init__(self, timer):
+ self._timer = timer
+ self._start = None
+ self._app_start = None
+ self._end = None
+
+ def start(self):
+ self._start = self._timer()
+
+ def app_start(self):
+ self._app_start = self._timer()
+
+ def end(self):
+ self._end = self._timer()
+ if self._app_start is None:
+ self._app_start = self._end
+
+ @property
+ def request_time(self):
+ if self._start and self._end:
+ return self._end - self._start
+ return None
+
+ @property
+ def overhead_time(self):
+ if self._start and self._app_start:
+ return self._app_start - self._start
+ return None
+
+ @property
+ def backend_time(self):
+ if self._end and self._app_start:
+ return self._end - self._app_start
+ return None
+
+
+def _find_api_key_param(info, parsed_uri):
+ params = info.api_key_url_query_params
+ if not params:
+ return None
+
+ param_dict = urlparse.parse_qs(parsed_uri.query)
+ if not param_dict:
+ return None
+
+ for q in params:
+ value = param_dict.get(q)
+ if value:
+ # param's values are lists, assume the first value
+ # is what's needed
+ return value[0]
+
+ return None
+
+
+_DEFAULT_API_KEYS = ('key', 'api_key')
+
+
+def _find_default_api_key_param(parsed_uri):
+ param_dict = urlparse.parse_qs(parsed_uri.query)
+ if not param_dict:
+ return None
+
+ for q in _DEFAULT_API_KEYS:
+ value = param_dict.get(q)
+ if value:
+ # param's values are lists, assume the first value
+ # is what's needed
+ return value[0]
+
+ return None
+
+
+def _find_api_key_header(info, environ):
+ headers = info.api_key_http_header
+ if not headers:
+ return None
+
+ for h in headers:
+ value = environ.get('HTTP_' + h.upper())
+ if value:
+ return value # headers have single values
+
+ return None
+
+def _create_authenticator(a_service):
+ """Create an instance of :class:`google.auth.tokens.Authenticator`.
+
+ Args:
+ a_service (:class:`google.api.gen.servicecontrol_v1_messages.Service`): a
+ service instance
+ """
+ if not isinstance(a_service, messages.Service):
+ raise ValueError("service is None or not an instance of Service")
+
+ authentication = a_service.authentication
+ if not authentication:
+ logger.info("authentication is not configured in service, "
+ "authentication checks will be disabled")
+ return
+
+ issuers_to_provider_ids = {}
+ issuer_uri_configs = {}
+ for provider in authentication.providers:
+ issuer = provider.issuer
+ jwks_uri = provider.jwksUri
+
+ # Enable openID discovery if jwks_uri is unset
+ open_id = jwks_uri is None
+ issuer_uri_configs[issuer] = suppliers.IssuerUriConfig(open_id, jwks_uri)
+ issuers_to_provider_ids[issuer] = provider.id
+
+ key_uri_supplier = suppliers.KeyUriSupplier(issuer_uri_configs)
+ jwks_supplier = suppliers.JwksSupplier(key_uri_supplier)
+ authenticator = tokens.Authenticator(issuers_to_provider_ids, jwks_supplier)
+ return authenticator
+
+
+class AuthenticationMiddleware(object):
+ """A WSGI middleware that does authentication checks for incoming
+ requests.
+
+ In environments where os.environ is replaced with a request-local and
+ thread-independent copy (e.g. Google Appengine), authentication result is
+ added to os.environ so that the wrapped application can make use of the
+ authentication result.
+ """
+ # pylint: disable=too-few-public-methods
+
+ USER_INFO = "google.api.auth.user_info"
+
+ def __init__(self, application, authenticator):
+ """Initializes an authentication middleware instance.
+
+ Args:
+ application: a WSGI application to be wrapped
+ authenticator (:class:`google.auth.tokens.Authenticator`): an
+ authenticator that authenticates incoming requests
+ """
+ if not isinstance(authenticator, tokens.Authenticator):
+ raise ValueError("Invalid authenticator")
+
+ self._application = application
+ self._authenticator = authenticator
+
+ def __call__(self, environ, start_response):
+ method_info = environ.get(EnvironmentMiddleware.METHOD_INFO)
+ if not method_info or not method_info.auth_info:
+ # No authentication configuration for this method
+ logger.debug("authentication is not configured")
+ return self._application(environ, start_response)
+
+ auth_token = _extract_auth_token(environ)
+ user_info = None
+ if not auth_token:
+ logger.debug("No auth token is attached to the request")
+ else:
+ try:
+ service_name = environ.get(EnvironmentMiddleware.SERVICE_NAME)
+ user_info = self._authenticator.authenticate(auth_token,
+ method_info.auth_info,
+ service_name)
+ except Exception: # pylint: disable=broad-except
+ logger.debug("Cannot decode and verify the auth token. The backend "
+ "will not be able to retrieve user info", exc_info=True)
+
+ environ[self.USER_INFO] = user_info
+
+ # pylint: disable=protected-access
+ if user_info and not isinstance(os.environ, os._Environ):
+ # Set user info into os.environ only if os.environ is replaced
+ # with a request-local copy
+ os.environ[self.USER_INFO] = user_info
+
+ response = self._application(environ, start_response)
+
+ # Erase user info from os.environ for safety and sanity.
+ if self.USER_INFO in os.environ:
+ del os.environ[self.USER_INFO]
+
+ return response
+
+
+_ACCESS_TOKEN_PARAM_NAME = "access_token"
+_BEARER_TOKEN_PREFIX = "Bearer "
+_BEARER_TOKEN_PREFIX_LEN = len(_BEARER_TOKEN_PREFIX)
+
+
+def _extract_auth_token(environ):
+ # First try to extract auth token from HTTP authorization header.
+ auth_header = environ.get("HTTP_AUTHORIZATION")
+ if auth_header:
+ if auth_header.startswith(_BEARER_TOKEN_PREFIX):
+ return auth_header[_BEARER_TOKEN_PREFIX_LEN:]
+ return
+
+ # Then try to read auth token from query.
+ parameters = urlparse.parse_qs(environ.get("QUERY_STRING", ""))
+ if _ACCESS_TOKEN_PARAM_NAME in parameters:
+ auth_token, = parameters[_ACCESS_TOKEN_PARAM_NAME]
+ return auth_token

Powered by Google App Engine
This is Rietveld 408576698